687 lines
29 KiB
C#
687 lines
29 KiB
C#
// Copyright (c) .NET Foundation. All rights reserved.
|
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.Extensions.Primitives;
|
|
using static Microsoft.AspNetCore.Server.HttpSys.UnsafeNclNativeMethods;
|
|
|
|
namespace Microsoft.AspNetCore.Server.HttpSys
|
|
{
|
|
internal sealed class Response
|
|
{
|
|
private ResponseState _responseState;
|
|
private string _reasonPhrase;
|
|
private ResponseBody _nativeStream;
|
|
private AuthenticationSchemes _authChallenges;
|
|
private TimeSpan? _cacheTtl;
|
|
private long _expectedBodyLength;
|
|
private BoundaryType _boundaryType;
|
|
private HttpApi.HTTP_RESPONSE_V2 _nativeResponse;
|
|
|
|
internal Response(RequestContext requestContext)
|
|
{
|
|
// TODO: Verbose log
|
|
RequestContext = requestContext;
|
|
Headers = new HeaderCollection();
|
|
// We haven't started yet, or we're just buffered, we can clear any data, headers, and state so
|
|
// that we can start over (e.g. to write an error message).
|
|
_nativeResponse = new HttpApi.HTTP_RESPONSE_V2();
|
|
Headers.IsReadOnly = false;
|
|
Headers.Clear();
|
|
_reasonPhrase = null;
|
|
_boundaryType = BoundaryType.None;
|
|
_nativeResponse.Response_V1.StatusCode = (ushort)StatusCodes.Status200OK;
|
|
_nativeResponse.Response_V1.Version.MajorVersion = 1;
|
|
_nativeResponse.Response_V1.Version.MinorVersion = 1;
|
|
_responseState = ResponseState.Created;
|
|
_expectedBodyLength = 0;
|
|
_nativeStream = null;
|
|
_cacheTtl = null;
|
|
_authChallenges = RequestContext.Server.Options.Authentication.Schemes;
|
|
}
|
|
|
|
private enum ResponseState
|
|
{
|
|
Created,
|
|
ComputedHeaders,
|
|
Started,
|
|
Closed,
|
|
}
|
|
|
|
private RequestContext RequestContext { get; }
|
|
|
|
private Request Request => RequestContext.Request;
|
|
|
|
public int StatusCode
|
|
{
|
|
get { return _nativeResponse.Response_V1.StatusCode; }
|
|
set
|
|
{
|
|
// Http.Sys automatically sends 100 Continue responses when you read from the request body.
|
|
if (value <= 100 || 999 < value)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(value), value, string.Format(Resources.Exception_InvalidStatusCode, value));
|
|
}
|
|
CheckResponseStarted();
|
|
_nativeResponse.Response_V1.StatusCode = (ushort)value;
|
|
}
|
|
}
|
|
|
|
public string ReasonPhrase
|
|
{
|
|
get { return _reasonPhrase; }
|
|
set
|
|
{
|
|
// TODO: Validate user input for illegal chars, length limit, etc.?
|
|
CheckResponseStarted();
|
|
_reasonPhrase = value;
|
|
}
|
|
}
|
|
|
|
public Stream Body
|
|
{
|
|
get
|
|
{
|
|
EnsureResponseStream();
|
|
return _nativeStream;
|
|
}
|
|
}
|
|
|
|
internal bool BodyIsFinished => _nativeStream?.IsDisposed ?? _responseState >= ResponseState.Closed;
|
|
|
|
/// <summary>
|
|
/// The authentication challenges that will be added to the response if the status code is 401.
|
|
/// This must be a subset of the AuthenticationSchemes enabled on the server.
|
|
/// </summary>
|
|
public AuthenticationSchemes AuthenticationChallenges
|
|
{
|
|
get { return _authChallenges; }
|
|
set
|
|
{
|
|
CheckResponseStarted();
|
|
_authChallenges = value;
|
|
}
|
|
}
|
|
|
|
private string GetReasonPhrase(int statusCode)
|
|
{
|
|
string reasonPhrase = ReasonPhrase;
|
|
if (string.IsNullOrWhiteSpace(reasonPhrase))
|
|
{
|
|
// If the user hasn't set this then it is generated on the fly if possible.
|
|
reasonPhrase = HttpReasonPhrase.Get(statusCode) ?? string.Empty;
|
|
}
|
|
return reasonPhrase;
|
|
}
|
|
|
|
// We MUST NOT send message-body when we send responses with these Status codes
|
|
private static readonly int[] StatusWithNoResponseBody = { 100, 101, 204, 205, 304 };
|
|
|
|
private static bool CanSendResponseBody(int responseCode)
|
|
{
|
|
for (int i = 0; i < StatusWithNoResponseBody.Length; i++)
|
|
{
|
|
if (responseCode == StatusWithNoResponseBody[i])
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public HeaderCollection Headers { get; }
|
|
|
|
internal long ExpectedBodyLength
|
|
{
|
|
get { return _expectedBodyLength; }
|
|
}
|
|
|
|
// Header accessors
|
|
public long? ContentLength
|
|
{
|
|
get
|
|
{
|
|
string contentLengthString = Headers[HttpKnownHeaderNames.ContentLength];
|
|
long contentLength;
|
|
if (!string.IsNullOrWhiteSpace(contentLengthString))
|
|
{
|
|
contentLengthString = contentLengthString.Trim();
|
|
if (string.Equals(Constants.Zero, contentLengthString, StringComparison.Ordinal))
|
|
{
|
|
return 0;
|
|
}
|
|
else if (long.TryParse(contentLengthString, NumberStyles.None, CultureInfo.InvariantCulture.NumberFormat, out contentLength))
|
|
{
|
|
return contentLength;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
set
|
|
{
|
|
CheckResponseStarted();
|
|
if (!value.HasValue)
|
|
{
|
|
Headers.Remove(HttpKnownHeaderNames.ContentLength);
|
|
}
|
|
else
|
|
{
|
|
if (value.Value < 0)
|
|
{
|
|
throw new ArgumentOutOfRangeException("value", value.Value, "Cannot be negative.");
|
|
}
|
|
|
|
if (value.Value == 0)
|
|
{
|
|
Headers[HttpKnownHeaderNames.ContentLength] = Constants.Zero;
|
|
}
|
|
else
|
|
{
|
|
Headers[HttpKnownHeaderNames.ContentLength] = value.Value.ToString(CultureInfo.InvariantCulture);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public string ContentType
|
|
{
|
|
get
|
|
{
|
|
return Headers[HttpKnownHeaderNames.ContentType];
|
|
}
|
|
set
|
|
{
|
|
CheckResponseStarted();
|
|
if (string.IsNullOrEmpty(value))
|
|
{
|
|
Headers.Remove(HttpKnownHeaderNames.ContentType);
|
|
}
|
|
else
|
|
{
|
|
Headers[HttpKnownHeaderNames.ContentType] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enable kernel caching for the response with the given timeout. Http.Sys determines if the response
|
|
/// can be cached.
|
|
/// </summary>
|
|
public TimeSpan? CacheTtl
|
|
{
|
|
get { return _cacheTtl; }
|
|
set
|
|
{
|
|
CheckResponseStarted();
|
|
_cacheTtl = value;
|
|
}
|
|
}
|
|
|
|
internal void Abort()
|
|
{
|
|
// Update state for HasStarted. Do not attempt a graceful Dispose.
|
|
_responseState = ResponseState.Closed;
|
|
}
|
|
|
|
// should only be called from RequestContext
|
|
internal void Dispose()
|
|
{
|
|
if (_responseState >= ResponseState.Closed)
|
|
{
|
|
return;
|
|
}
|
|
// TODO: Verbose log
|
|
EnsureResponseStream();
|
|
_nativeStream.Dispose();
|
|
_responseState = ResponseState.Closed;
|
|
}
|
|
|
|
internal BoundaryType BoundaryType
|
|
{
|
|
get { return _boundaryType; }
|
|
}
|
|
|
|
internal bool HasComputedHeaders
|
|
{
|
|
get { return _responseState >= ResponseState.ComputedHeaders; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Indicates if the response status, reason, and headers are prepared to send and can
|
|
/// no longer be modified. This is caused by the first write or flush to the response body.
|
|
/// </summary>
|
|
public bool HasStarted
|
|
{
|
|
get { return _responseState >= ResponseState.Started; }
|
|
}
|
|
|
|
private void CheckResponseStarted()
|
|
{
|
|
if (HasStarted)
|
|
{
|
|
throw new InvalidOperationException("Headers already sent.");
|
|
}
|
|
}
|
|
|
|
private void EnsureResponseStream()
|
|
{
|
|
if (_nativeStream == null)
|
|
{
|
|
_nativeStream = new ResponseBody(RequestContext);
|
|
}
|
|
}
|
|
|
|
/*
|
|
12.3
|
|
HttpSendHttpResponse() and HttpSendResponseEntityBody() Flag Values.
|
|
The following flags can be used on calls to HttpSendHttpResponse() and HttpSendResponseEntityBody() API calls:
|
|
|
|
#define HTTP_SEND_RESPONSE_FLAG_DISCONNECT 0x00000001
|
|
#define HTTP_SEND_RESPONSE_FLAG_MORE_DATA 0x00000002
|
|
#define HTTP_SEND_RESPONSE_FLAG_RAW_HEADER 0x00000004
|
|
#define HTTP_SEND_RESPONSE_FLAG_VALID 0x00000007
|
|
|
|
HTTP_SEND_RESPONSE_FLAG_DISCONNECT:
|
|
specifies that the network connection should be disconnected immediately after
|
|
sending the response, overriding the HTTP protocol's persistent connection features.
|
|
HTTP_SEND_RESPONSE_FLAG_MORE_DATA:
|
|
specifies that additional entity body data will be sent by the caller. Thus,
|
|
the last call HttpSendResponseEntityBody for a RequestId, will have this flag reset.
|
|
HTTP_SEND_RESPONSE_RAW_HEADER:
|
|
specifies that a caller of HttpSendResponseEntityBody() is intentionally omitting
|
|
a call to HttpSendHttpResponse() in order to bypass normal header processing. The
|
|
actual HTTP header will be generated by the application and sent as entity body.
|
|
This flag should be passed on the first call to HttpSendResponseEntityBody, and
|
|
not after. Thus, flag is not applicable to HttpSendHttpResponse.
|
|
*/
|
|
|
|
// TODO: Consider using HTTP_SEND_RESPONSE_RAW_HEADER with HttpSendResponseEntityBody instead of calling HttpSendHttpResponse.
|
|
// This will give us more control of the bytes that hit the wire, including encodings, HTTP 1.0, etc..
|
|
// It may also be faster to do this work in managed code and then pass down only one buffer.
|
|
// What would we loose by bypassing HttpSendHttpResponse?
|
|
//
|
|
// TODO: Consider using the HTTP_SEND_RESPONSE_FLAG_BUFFER_DATA flag for most/all responses rather than just Opaque.
|
|
internal unsafe uint SendHeaders(HttpApi.HTTP_DATA_CHUNK[] dataChunks,
|
|
ResponseStreamAsyncResult asyncResult,
|
|
HttpApi.HTTP_FLAGS flags,
|
|
bool isOpaqueUpgrade)
|
|
{
|
|
Debug.Assert(!HasStarted, "HttpListenerResponse::SendHeaders()|SentHeaders is true.");
|
|
|
|
_responseState = ResponseState.Started;
|
|
var reasonPhrase = GetReasonPhrase(StatusCode);
|
|
|
|
uint statusCode;
|
|
uint bytesSent;
|
|
List<GCHandle> pinnedHeaders = SerializeHeaders(isOpaqueUpgrade);
|
|
try
|
|
{
|
|
if (dataChunks != null)
|
|
{
|
|
if (pinnedHeaders == null)
|
|
{
|
|
pinnedHeaders = new List<GCHandle>();
|
|
}
|
|
var handle = GCHandle.Alloc(dataChunks, GCHandleType.Pinned);
|
|
pinnedHeaders.Add(handle);
|
|
_nativeResponse.Response_V1.EntityChunkCount = (ushort)dataChunks.Length;
|
|
_nativeResponse.Response_V1.pEntityChunks = (HttpApi.HTTP_DATA_CHUNK*)handle.AddrOfPinnedObject();
|
|
}
|
|
else if (asyncResult != null && asyncResult.DataChunks != null)
|
|
{
|
|
_nativeResponse.Response_V1.EntityChunkCount = asyncResult.DataChunkCount;
|
|
_nativeResponse.Response_V1.pEntityChunks = asyncResult.DataChunks;
|
|
}
|
|
else
|
|
{
|
|
_nativeResponse.Response_V1.EntityChunkCount = 0;
|
|
_nativeResponse.Response_V1.pEntityChunks = null;
|
|
}
|
|
|
|
var cachePolicy = new HttpApi.HTTP_CACHE_POLICY();
|
|
if (_cacheTtl.HasValue && _cacheTtl.Value > TimeSpan.Zero)
|
|
{
|
|
cachePolicy.Policy = HttpApi.HTTP_CACHE_POLICY_TYPE.HttpCachePolicyTimeToLive;
|
|
cachePolicy.SecondsToLive = (uint)Math.Min(_cacheTtl.Value.Ticks / TimeSpan.TicksPerSecond, Int32.MaxValue);
|
|
}
|
|
|
|
byte[] reasonPhraseBytes = HeaderEncoding.GetBytes(reasonPhrase);
|
|
fixed (byte* pReasonPhrase = reasonPhraseBytes)
|
|
{
|
|
_nativeResponse.Response_V1.ReasonLength = (ushort)reasonPhraseBytes.Length;
|
|
_nativeResponse.Response_V1.pReason = (sbyte*)pReasonPhrase;
|
|
fixed (HttpApi.HTTP_RESPONSE_V2* pResponse = &_nativeResponse)
|
|
{
|
|
statusCode =
|
|
HttpApi.HttpSendHttpResponse(
|
|
RequestContext.Server.RequestQueue.Handle,
|
|
Request.RequestId,
|
|
(uint)flags,
|
|
pResponse,
|
|
&cachePolicy,
|
|
&bytesSent,
|
|
SafeLocalFree.Zero,
|
|
0,
|
|
asyncResult == null ? SafeNativeOverlapped.Zero : asyncResult.NativeOverlapped,
|
|
IntPtr.Zero);
|
|
|
|
if (asyncResult != null &&
|
|
statusCode == ErrorCodes.ERROR_SUCCESS &&
|
|
HttpSysListener.SkipIOCPCallbackOnSuccess)
|
|
{
|
|
asyncResult.BytesSent = bytesSent;
|
|
// The caller will invoke IOCompleted
|
|
}
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
FreePinnedHeaders(pinnedHeaders);
|
|
}
|
|
return statusCode;
|
|
}
|
|
|
|
internal HttpApi.HTTP_FLAGS ComputeHeaders(long writeCount, bool endOfRequest = false)
|
|
{
|
|
if (StatusCode == (ushort)StatusCodes.Status401Unauthorized)
|
|
{
|
|
RequestContext.Server.Options.Authentication.SetAuthenticationChallenge(RequestContext);
|
|
}
|
|
|
|
var flags = HttpApi.HTTP_FLAGS.NONE;
|
|
Debug.Assert(!HasComputedHeaders, nameof(HasComputedHeaders) + " is true.");
|
|
_responseState = ResponseState.ComputedHeaders;
|
|
|
|
// Gather everything from the request that affects the response:
|
|
var requestVersion = Request.ProtocolVersion;
|
|
var requestConnectionString = Request.Headers[HttpKnownHeaderNames.Connection];
|
|
var isHeadRequest = Request.IsHeadMethod;
|
|
var requestCloseSet = Matches(Constants.Close, requestConnectionString);
|
|
|
|
// Gather everything the app may have set on the response:
|
|
// Http.Sys does not allow us to specify the response protocol version, assume this is a HTTP/1.1 response when making decisions.
|
|
var responseConnectionString = Headers[HttpKnownHeaderNames.Connection];
|
|
var transferEncodingString = Headers[HttpKnownHeaderNames.TransferEncoding];
|
|
var responseContentLength = ContentLength;
|
|
var responseCloseSet = Matches(Constants.Close, responseConnectionString);
|
|
var responseChunkedSet = Matches(Constants.Chunked, transferEncodingString);
|
|
var statusCanHaveBody = CanSendResponseBody(RequestContext.Response.StatusCode);
|
|
|
|
// Determine if the connection will be kept alive or closed.
|
|
var keepConnectionAlive = true;
|
|
if (requestVersion <= Constants.V1_0 // Http.Sys does not support "Keep-Alive: true" or "Connection: Keep-Alive"
|
|
|| (requestVersion == Constants.V1_1 && requestCloseSet)
|
|
|| responseCloseSet)
|
|
{
|
|
keepConnectionAlive = false;
|
|
}
|
|
|
|
// Determine the body format. If the user asks to do something, let them, otherwise choose a good default for the scenario.
|
|
if (responseContentLength.HasValue)
|
|
{
|
|
_boundaryType = BoundaryType.ContentLength;
|
|
// ComputeLeftToWrite checks for HEAD requests when setting _leftToWrite
|
|
_expectedBodyLength = responseContentLength.Value;
|
|
if (_expectedBodyLength == writeCount && !isHeadRequest)
|
|
{
|
|
// A single write with the whole content-length. Http.Sys will set the content-length for us in this scenario.
|
|
// If we don't remove it then range requests served from cache will have two.
|
|
// https://github.com/aspnet/WebListener/issues/167
|
|
ContentLength = null;
|
|
}
|
|
}
|
|
else if (responseChunkedSet)
|
|
{
|
|
// The application is performing it's own chunking.
|
|
_boundaryType = BoundaryType.PassThrough;
|
|
}
|
|
else if (endOfRequest && !(isHeadRequest && statusCanHaveBody)) // HEAD requests should always end without a body. Assume a GET response would have a body.
|
|
{
|
|
if (statusCanHaveBody)
|
|
{
|
|
Headers[HttpKnownHeaderNames.ContentLength] = Constants.Zero;
|
|
}
|
|
_boundaryType = BoundaryType.ContentLength;
|
|
_expectedBodyLength = 0;
|
|
}
|
|
else if (requestVersion == Constants.V1_1)
|
|
{
|
|
_boundaryType = BoundaryType.Chunked;
|
|
Headers[HttpKnownHeaderNames.TransferEncoding] = Constants.Chunked;
|
|
}
|
|
else
|
|
{
|
|
// v1.0 and the length cannot be determined, so we must close the connection after writing data
|
|
keepConnectionAlive = false;
|
|
_boundaryType = BoundaryType.Close;
|
|
}
|
|
|
|
// Managed connection lifetime
|
|
if (!keepConnectionAlive)
|
|
{
|
|
// All Http.Sys responses are v1.1, so use 1.1 response headers
|
|
// Note that if we don't add this header, Http.Sys will often do it for us.
|
|
if (!responseCloseSet)
|
|
{
|
|
Headers.Append(HttpKnownHeaderNames.Connection, Constants.Close);
|
|
}
|
|
flags = HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_DISCONNECT;
|
|
}
|
|
|
|
return flags;
|
|
}
|
|
|
|
private static bool Matches(string knownValue, string input)
|
|
{
|
|
return string.Equals(knownValue, input?.Trim(), StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private unsafe List<GCHandle> SerializeHeaders(bool isOpaqueUpgrade)
|
|
{
|
|
Headers.IsReadOnly = true; // Prohibit further modifications.
|
|
HttpApi.HTTP_UNKNOWN_HEADER[] unknownHeaders = null;
|
|
HttpApi.HTTP_RESPONSE_INFO[] knownHeaderInfo = null;
|
|
List<GCHandle> pinnedHeaders;
|
|
GCHandle gcHandle;
|
|
|
|
if (Headers.Count == 0)
|
|
{
|
|
return null;
|
|
}
|
|
string headerName;
|
|
string headerValue;
|
|
int lookup;
|
|
byte[] bytes = null;
|
|
pinnedHeaders = new List<GCHandle>();
|
|
|
|
int numUnknownHeaders = 0;
|
|
int numKnownMultiHeaders = 0;
|
|
foreach (var headerPair in Headers)
|
|
{
|
|
if (headerPair.Value.Count == 0)
|
|
{
|
|
continue;
|
|
}
|
|
// See if this is an unknown header
|
|
lookup = HttpApi.HTTP_RESPONSE_HEADER_ID.IndexOfKnownHeader(headerPair.Key);
|
|
|
|
// Http.Sys doesn't let us send the Connection: Upgrade header as a Known header.
|
|
if (lookup == -1 ||
|
|
(isOpaqueUpgrade && lookup == (int)HttpApi.HTTP_RESPONSE_HEADER_ID.Enum.HttpHeaderConnection))
|
|
{
|
|
numUnknownHeaders += headerPair.Value.Count;
|
|
}
|
|
else if (headerPair.Value.Count > 1)
|
|
{
|
|
numKnownMultiHeaders++;
|
|
}
|
|
// else known single-value header.
|
|
}
|
|
|
|
try
|
|
{
|
|
fixed (HttpApi.HTTP_KNOWN_HEADER* pKnownHeaders = &_nativeResponse.Response_V1.Headers.KnownHeaders)
|
|
{
|
|
foreach (var headerPair in Headers)
|
|
{
|
|
if (headerPair.Value.Count == 0)
|
|
{
|
|
continue;
|
|
}
|
|
headerName = headerPair.Key;
|
|
StringValues headerValues = headerPair.Value;
|
|
lookup = HttpApi.HTTP_RESPONSE_HEADER_ID.IndexOfKnownHeader(headerName);
|
|
|
|
// Http.Sys doesn't let us send the Connection: Upgrade header as a Known header.
|
|
if (lookup == -1 ||
|
|
(isOpaqueUpgrade && lookup == (int)HttpApi.HTTP_RESPONSE_HEADER_ID.Enum.HttpHeaderConnection))
|
|
{
|
|
if (unknownHeaders == null)
|
|
{
|
|
unknownHeaders = new HttpApi.HTTP_UNKNOWN_HEADER[numUnknownHeaders];
|
|
gcHandle = GCHandle.Alloc(unknownHeaders, GCHandleType.Pinned);
|
|
pinnedHeaders.Add(gcHandle);
|
|
_nativeResponse.Response_V1.Headers.pUnknownHeaders = (HttpApi.HTTP_UNKNOWN_HEADER*)gcHandle.AddrOfPinnedObject();
|
|
}
|
|
|
|
for (int headerValueIndex = 0; headerValueIndex < headerValues.Count; headerValueIndex++)
|
|
{
|
|
// Add Name
|
|
bytes = HeaderEncoding.GetBytes(headerName);
|
|
unknownHeaders[_nativeResponse.Response_V1.Headers.UnknownHeaderCount].NameLength = (ushort)bytes.Length;
|
|
gcHandle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
|
|
pinnedHeaders.Add(gcHandle);
|
|
unknownHeaders[_nativeResponse.Response_V1.Headers.UnknownHeaderCount].pName = (sbyte*)gcHandle.AddrOfPinnedObject();
|
|
|
|
// Add Value
|
|
headerValue = headerValues[headerValueIndex] ?? string.Empty;
|
|
bytes = HeaderEncoding.GetBytes(headerValue);
|
|
unknownHeaders[_nativeResponse.Response_V1.Headers.UnknownHeaderCount].RawValueLength = (ushort)bytes.Length;
|
|
gcHandle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
|
|
pinnedHeaders.Add(gcHandle);
|
|
unknownHeaders[_nativeResponse.Response_V1.Headers.UnknownHeaderCount].pRawValue = (sbyte*)gcHandle.AddrOfPinnedObject();
|
|
_nativeResponse.Response_V1.Headers.UnknownHeaderCount++;
|
|
}
|
|
}
|
|
else if (headerPair.Value.Count == 1)
|
|
{
|
|
headerValue = headerValues[0] ?? string.Empty;
|
|
bytes = HeaderEncoding.GetBytes(headerValue);
|
|
pKnownHeaders[lookup].RawValueLength = (ushort)bytes.Length;
|
|
gcHandle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
|
|
pinnedHeaders.Add(gcHandle);
|
|
pKnownHeaders[lookup].pRawValue = (sbyte*)gcHandle.AddrOfPinnedObject();
|
|
}
|
|
else
|
|
{
|
|
if (knownHeaderInfo == null)
|
|
{
|
|
knownHeaderInfo = new HttpApi.HTTP_RESPONSE_INFO[numKnownMultiHeaders];
|
|
gcHandle = GCHandle.Alloc(knownHeaderInfo, GCHandleType.Pinned);
|
|
pinnedHeaders.Add(gcHandle);
|
|
_nativeResponse.pResponseInfo = (HttpApi.HTTP_RESPONSE_INFO*)gcHandle.AddrOfPinnedObject();
|
|
}
|
|
|
|
knownHeaderInfo[_nativeResponse.ResponseInfoCount].Type = HttpApi.HTTP_RESPONSE_INFO_TYPE.HttpResponseInfoTypeMultipleKnownHeaders;
|
|
knownHeaderInfo[_nativeResponse.ResponseInfoCount].Length = (uint)Marshal.SizeOf<HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS>();
|
|
|
|
HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS header = new HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS();
|
|
|
|
header.HeaderId = (HttpApi.HTTP_RESPONSE_HEADER_ID.Enum)lookup;
|
|
header.Flags = HttpApi.HTTP_RESPONSE_INFO_FLAGS.PreserveOrder; // TODO: The docs say this is for www-auth only.
|
|
|
|
HttpApi.HTTP_KNOWN_HEADER[] nativeHeaderValues = new HttpApi.HTTP_KNOWN_HEADER[headerValues.Count];
|
|
gcHandle = GCHandle.Alloc(nativeHeaderValues, GCHandleType.Pinned);
|
|
pinnedHeaders.Add(gcHandle);
|
|
header.KnownHeaders = (HttpApi.HTTP_KNOWN_HEADER*)gcHandle.AddrOfPinnedObject();
|
|
|
|
for (int headerValueIndex = 0; headerValueIndex < headerValues.Count; headerValueIndex++)
|
|
{
|
|
// Add Value
|
|
headerValue = headerValues[headerValueIndex] ?? string.Empty;
|
|
bytes = HeaderEncoding.GetBytes(headerValue);
|
|
nativeHeaderValues[header.KnownHeaderCount].RawValueLength = (ushort)bytes.Length;
|
|
gcHandle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
|
|
pinnedHeaders.Add(gcHandle);
|
|
nativeHeaderValues[header.KnownHeaderCount].pRawValue = (sbyte*)gcHandle.AddrOfPinnedObject();
|
|
header.KnownHeaderCount++;
|
|
}
|
|
|
|
// This type is a struct, not an object, so pinning it causes a boxed copy to be created. We can't do that until after all the fields are set.
|
|
gcHandle = GCHandle.Alloc(header, GCHandleType.Pinned);
|
|
pinnedHeaders.Add(gcHandle);
|
|
knownHeaderInfo[_nativeResponse.ResponseInfoCount].pInfo = (HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS*)gcHandle.AddrOfPinnedObject();
|
|
|
|
_nativeResponse.ResponseInfoCount++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
FreePinnedHeaders(pinnedHeaders);
|
|
throw;
|
|
}
|
|
return pinnedHeaders;
|
|
}
|
|
|
|
private static void FreePinnedHeaders(List<GCHandle> pinnedHeaders)
|
|
{
|
|
if (pinnedHeaders != null)
|
|
{
|
|
foreach (GCHandle gcHandle in pinnedHeaders)
|
|
{
|
|
if (gcHandle.IsAllocated)
|
|
{
|
|
gcHandle.Free();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Subset of ComputeHeaders
|
|
internal void SendOpaqueUpgrade()
|
|
{
|
|
_boundaryType = BoundaryType.Close;
|
|
|
|
// TODO: Send headers async?
|
|
ulong errorCode = SendHeaders(null, null,
|
|
HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_OPAQUE |
|
|
HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA |
|
|
HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_BUFFER_DATA,
|
|
true);
|
|
|
|
if (errorCode != ErrorCodes.ERROR_SUCCESS)
|
|
{
|
|
throw new HttpSysException((int)errorCode);
|
|
}
|
|
}
|
|
|
|
internal void CancelLastWrite()
|
|
{
|
|
_nativeStream?.CancelLastWrite();
|
|
}
|
|
|
|
public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancel)
|
|
{
|
|
EnsureResponseStream();
|
|
return _nativeStream.SendFileAsync(path, offset, count, cancel);
|
|
}
|
|
|
|
internal void SwitchToOpaqueMode()
|
|
{
|
|
EnsureResponseStream();
|
|
_nativeStream.SwitchToOpaqueMode();
|
|
}
|
|
}
|
|
}
|