#120 Implement response buffering.
This commit is contained in:
parent
0acb8f3ed4
commit
3c044fb92e
|
|
@ -39,6 +39,7 @@ namespace Microsoft.AspNet.Server.WebListener
|
||||||
IHttpSendFileFeature,
|
IHttpSendFileFeature,
|
||||||
ITlsConnectionFeature,
|
ITlsConnectionFeature,
|
||||||
ITlsTokenBindingFeature,
|
ITlsTokenBindingFeature,
|
||||||
|
IHttpBufferingFeature,
|
||||||
IHttpRequestLifetimeFeature,
|
IHttpRequestLifetimeFeature,
|
||||||
IHttpWebSocketFeature,
|
IHttpWebSocketFeature,
|
||||||
IHttpAuthenticationFeature,
|
IHttpAuthenticationFeature,
|
||||||
|
|
@ -97,6 +98,7 @@ namespace Microsoft.AspNet.Server.WebListener
|
||||||
_features.Add(typeof(IHttpConnectionFeature), this);
|
_features.Add(typeof(IHttpConnectionFeature), this);
|
||||||
_features.Add(typeof(IHttpResponseFeature), this);
|
_features.Add(typeof(IHttpResponseFeature), this);
|
||||||
_features.Add(typeof(IHttpSendFileFeature), this);
|
_features.Add(typeof(IHttpSendFileFeature), this);
|
||||||
|
_features.Add(typeof(IHttpBufferingFeature), this);
|
||||||
_features.Add(typeof(IHttpRequestLifetimeFeature), this);
|
_features.Add(typeof(IHttpRequestLifetimeFeature), this);
|
||||||
_features.Add(typeof(IHttpAuthenticationFeature), this);
|
_features.Add(typeof(IHttpAuthenticationFeature), this);
|
||||||
_features.Add(typeof(IRequestIdentifierFeature), this);
|
_features.Add(typeof(IRequestIdentifierFeature), this);
|
||||||
|
|
@ -328,6 +330,16 @@ namespace Microsoft.AspNet.Server.WebListener
|
||||||
return Request.GetReferredTokenBindingId();
|
return Request.GetReferredTokenBindingId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void IHttpBufferingFeature.DisableRequestBuffering()
|
||||||
|
{
|
||||||
|
// There is no request buffering.
|
||||||
|
}
|
||||||
|
|
||||||
|
void IHttpBufferingFeature.DisableResponseBuffering()
|
||||||
|
{
|
||||||
|
Response.ShouldBuffer = false;
|
||||||
|
}
|
||||||
|
|
||||||
Stream IHttpResponseFeature.Body
|
Stream IHttpResponseFeature.Body
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
@ -356,7 +368,7 @@ namespace Microsoft.AspNet.Server.WebListener
|
||||||
|
|
||||||
bool IHttpResponseFeature.HeadersSent
|
bool IHttpResponseFeature.HeadersSent
|
||||||
{
|
{
|
||||||
get { return Response.HeadersSent; }
|
get { return Response.HasStarted; }
|
||||||
}
|
}
|
||||||
|
|
||||||
void IHttpResponseFeature.OnSendingHeaders(Action<object> callback, object state)
|
void IHttpResponseFeature.OnSendingHeaders(Action<object> callback, object state)
|
||||||
|
|
|
||||||
|
|
@ -167,13 +167,14 @@ namespace Microsoft.AspNet.Server.WebListener
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
LogHelper.LogException(_logger, "ProcessRequestAsync", ex);
|
LogHelper.LogException(_logger, "ProcessRequestAsync", ex);
|
||||||
if (requestContext.Response.HeadersSent)
|
if (requestContext.Response.HasStartedSending)
|
||||||
{
|
{
|
||||||
requestContext.Abort();
|
requestContext.Abort();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// We haven't sent a response yet, try to send a 500 Internal Server Error
|
// We haven't sent a response yet, try to send a 500 Internal Server Error
|
||||||
|
requestContext.Response.Reset();
|
||||||
SetFatalResponse(requestContext, 500);
|
SetFatalResponse(requestContext, 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -195,8 +196,6 @@ namespace Microsoft.AspNet.Server.WebListener
|
||||||
private static void SetFatalResponse(RequestContext context, int status)
|
private static void SetFatalResponse(RequestContext context, int status)
|
||||||
{
|
{
|
||||||
context.Response.StatusCode = status;
|
context.Response.StatusCode = status;
|
||||||
context.Response.ReasonPhrase = string.Empty;
|
|
||||||
context.Response.Headers.Clear();
|
|
||||||
context.Response.ContentLength = 0;
|
context.Response.ContentLength = 0;
|
||||||
context.Dispose();
|
context.Dispose();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,13 +21,18 @@
|
||||||
// </copyright>
|
// </copyright>
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Microsoft.Net.Http.Server
|
namespace Microsoft.Net.Http.Server
|
||||||
{
|
{
|
||||||
internal static class Helpers
|
internal static class Helpers
|
||||||
{
|
{
|
||||||
|
internal static readonly byte[] ChunkTerminator = new byte[] { (byte)'0', (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n' };
|
||||||
|
internal static readonly byte[] CRLF = new byte[] { (byte)'\r', (byte)'\n' };
|
||||||
|
|
||||||
internal static Task CompletedTask()
|
internal static Task CompletedTask()
|
||||||
{
|
{
|
||||||
return Task.FromResult<object>(null);
|
return Task.FromResult<object>(null);
|
||||||
|
|
@ -49,5 +54,94 @@ namespace Microsoft.Net.Http.Server
|
||||||
{
|
{
|
||||||
return task.ConfigureAwait(continueOnCapturedContext: false);
|
return task.ConfigureAwait(continueOnCapturedContext: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static IAsyncResult ToIAsyncResult(this Task task, AsyncCallback callback, object state)
|
||||||
|
{
|
||||||
|
var tcs = new TaskCompletionSource<int>(state);
|
||||||
|
task.ContinueWith(t =>
|
||||||
|
{
|
||||||
|
if (t.IsFaulted)
|
||||||
|
{
|
||||||
|
tcs.TrySetException(t.Exception.InnerExceptions);
|
||||||
|
}
|
||||||
|
else if (t.IsCanceled)
|
||||||
|
{
|
||||||
|
tcs.TrySetCanceled();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tcs.TrySetResult(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callback != null)
|
||||||
|
{
|
||||||
|
callback(tcs.Task);
|
||||||
|
}
|
||||||
|
}, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default);
|
||||||
|
return tcs.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A private utility routine to convert an integer to a chunk header,
|
||||||
|
/// which is an ASCII hex number followed by a CRLF.The header is returned
|
||||||
|
/// as a byte array.
|
||||||
|
/// Generates a right-aligned hex string and returns the start offset.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="size">Chunk size to be encoded</param>
|
||||||
|
/// <param name="offset">Out parameter where we store offset into buffer.</param>
|
||||||
|
/// <returns>A byte array with the header in int.</returns>
|
||||||
|
internal static ArraySegment<byte> GetChunkHeader(int size)
|
||||||
|
{
|
||||||
|
uint mask = 0xf0000000;
|
||||||
|
byte[] header = new byte[10];
|
||||||
|
int i;
|
||||||
|
int offset = -1;
|
||||||
|
|
||||||
|
// Loop through the size, looking at each nibble. If it's not 0
|
||||||
|
// convert it to hex. Save the index of the first non-zero
|
||||||
|
// byte.
|
||||||
|
|
||||||
|
for (i = 0; i < 8; i++, size <<= 4)
|
||||||
|
{
|
||||||
|
// offset == -1 means that we haven't found a non-zero nibble
|
||||||
|
// yet. If we haven't found one, and the current one is zero,
|
||||||
|
// don't do anything.
|
||||||
|
|
||||||
|
if (offset == -1)
|
||||||
|
{
|
||||||
|
if ((size & mask) == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Either we have a non-zero nibble or we're no longer skipping
|
||||||
|
// leading zeros. Convert this nibble to ASCII and save it.
|
||||||
|
|
||||||
|
uint temp = (uint)size >> 28;
|
||||||
|
|
||||||
|
if (temp < 10)
|
||||||
|
{
|
||||||
|
header[i] = (byte)(temp + '0');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
header[i] = (byte)((temp - 10) + 'A');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we haven't found a non-zero nibble yet, we've found one
|
||||||
|
// now, so remember that.
|
||||||
|
|
||||||
|
if (offset == -1)
|
||||||
|
{
|
||||||
|
offset = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header[8] = (byte)'\r';
|
||||||
|
header[9] = (byte)'\n';
|
||||||
|
|
||||||
|
return new ArraySegment<byte>(header, offset, header.Length - offset);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. 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.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.Net.Http.Server
|
||||||
|
{
|
||||||
|
internal class BufferBuilder
|
||||||
|
{
|
||||||
|
private List<ArraySegment<byte>> _buffers = new List<ArraySegment<byte>>();
|
||||||
|
|
||||||
|
internal IEnumerable<ArraySegment<byte>> Buffers
|
||||||
|
{
|
||||||
|
get { return _buffers; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal int BufferCount
|
||||||
|
{
|
||||||
|
get { return _buffers.Count; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal int TotalBytes { get; private set; }
|
||||||
|
|
||||||
|
internal void Add(ArraySegment<byte> data)
|
||||||
|
{
|
||||||
|
_buffers.Add(data);
|
||||||
|
TotalBytes += data.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CopyAndAdd(ArraySegment<byte> data)
|
||||||
|
{
|
||||||
|
if (data.Count > 0)
|
||||||
|
{
|
||||||
|
var temp = new byte[data.Count];
|
||||||
|
Buffer.BlockCopy(data.Array, data.Offset, temp, 0, data.Count);
|
||||||
|
_buffers.Add(new ArraySegment<byte>(temp));
|
||||||
|
TotalBytes += data.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
_buffers.Clear();
|
||||||
|
TotalBytes = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,15 +21,15 @@ namespace Microsoft.Net.Http.Server
|
||||||
|
|
||||||
private IDictionary<string, string[]> Store { get; set; }
|
private IDictionary<string, string[]> Store { get; set; }
|
||||||
|
|
||||||
// Readonly after the response has been sent.
|
// Readonly after the response has been started.
|
||||||
internal bool Sent { get; set; }
|
public bool IsReadOnly { get; internal set; }
|
||||||
|
|
||||||
public string this[string key]
|
public string this[string key]
|
||||||
{
|
{
|
||||||
get { return Get(key); }
|
get { return Get(key); }
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
ThrowIfSent();
|
ThrowIfReadOnly();
|
||||||
if (string.IsNullOrEmpty(value))
|
if (string.IsNullOrEmpty(value))
|
||||||
{
|
{
|
||||||
Remove(key);
|
Remove(key);
|
||||||
|
|
@ -46,7 +46,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
get { return Store[key]; }
|
get { return Store[key]; }
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
ThrowIfSent();
|
ThrowIfReadOnly();
|
||||||
Store[key] = value;
|
Store[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -56,11 +56,6 @@ namespace Microsoft.Net.Http.Server
|
||||||
get { return Store.Count; }
|
get { return Store.Count; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsReadOnly
|
|
||||||
{
|
|
||||||
get { return Sent; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public ICollection<string> Keys
|
public ICollection<string> Keys
|
||||||
{
|
{
|
||||||
get { return Store.Keys; }
|
get { return Store.Keys; }
|
||||||
|
|
@ -73,19 +68,19 @@ namespace Microsoft.Net.Http.Server
|
||||||
|
|
||||||
public void Add(KeyValuePair<string, string[]> item)
|
public void Add(KeyValuePair<string, string[]> item)
|
||||||
{
|
{
|
||||||
ThrowIfSent();
|
ThrowIfReadOnly();
|
||||||
Store.Add(item);
|
Store.Add(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Add(string key, string[] value)
|
public void Add(string key, string[] value)
|
||||||
{
|
{
|
||||||
ThrowIfSent();
|
ThrowIfReadOnly();
|
||||||
Store.Add(key, value);
|
Store.Add(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Append(string key, string value)
|
public void Append(string key, string value)
|
||||||
{
|
{
|
||||||
ThrowIfSent();
|
ThrowIfReadOnly();
|
||||||
string[] values;
|
string[] values;
|
||||||
if (Store.TryGetValue(key, out values))
|
if (Store.TryGetValue(key, out values))
|
||||||
{
|
{
|
||||||
|
|
@ -102,7 +97,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
|
|
||||||
public void AppendValues(string key, params string[] values)
|
public void AppendValues(string key, params string[] values)
|
||||||
{
|
{
|
||||||
ThrowIfSent();
|
ThrowIfReadOnly();
|
||||||
string[] oldValues;
|
string[] oldValues;
|
||||||
if (Store.TryGetValue(key, out oldValues))
|
if (Store.TryGetValue(key, out oldValues))
|
||||||
{
|
{
|
||||||
|
|
@ -119,7 +114,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
ThrowIfSent();
|
ThrowIfReadOnly();
|
||||||
Store.Clear();
|
Store.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,25 +160,25 @@ namespace Microsoft.Net.Http.Server
|
||||||
|
|
||||||
public bool Remove(KeyValuePair<string, string[]> item)
|
public bool Remove(KeyValuePair<string, string[]> item)
|
||||||
{
|
{
|
||||||
ThrowIfSent();
|
ThrowIfReadOnly();
|
||||||
return Store.Remove(item);
|
return Store.Remove(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Remove(string key)
|
public bool Remove(string key)
|
||||||
{
|
{
|
||||||
ThrowIfSent();
|
ThrowIfReadOnly();
|
||||||
return Store.Remove(key);
|
return Store.Remove(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Set(string key, string value)
|
public void Set(string key, string value)
|
||||||
{
|
{
|
||||||
ThrowIfSent();
|
ThrowIfReadOnly();
|
||||||
Store[key] = new[] { value };
|
Store[key] = new[] { value };
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetValues(string key, params string[] values)
|
public void SetValues(string key, params string[] values)
|
||||||
{
|
{
|
||||||
ThrowIfSent();
|
ThrowIfReadOnly();
|
||||||
Store[key] = values;
|
Store[key] = values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -197,11 +192,11 @@ namespace Microsoft.Net.Http.Server
|
||||||
return GetEnumerator();
|
return GetEnumerator();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ThrowIfSent()
|
private void ThrowIfReadOnly()
|
||||||
{
|
{
|
||||||
if (Sent)
|
if (IsReadOnly)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("The response headers cannot be modified because they have already been sent.");
|
throw new InvalidOperationException("The response headers cannot be modified because the response has already started.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -158,9 +158,9 @@ namespace Microsoft.Net.Http.Server
|
||||||
|
|
||||||
public Task<Stream> UpgradeAsync()
|
public Task<Stream> UpgradeAsync()
|
||||||
{
|
{
|
||||||
if (!IsUpgradableRequest || _response.HeadersSent)
|
if (!IsUpgradableRequest || _response.HasStarted)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException();
|
throw new InvalidOperationException("This request cannot be upgraded. It is incompatible, or the response has already started.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the status code and reason phrase
|
// Set the status code and reason phrase
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Framework.Logging;
|
using Microsoft.Framework.Logging;
|
||||||
|
using static Microsoft.Net.Http.Server.UnsafeNclNativeMethods;
|
||||||
|
|
||||||
namespace Microsoft.Net.Http.Server
|
namespace Microsoft.Net.Http.Server
|
||||||
{
|
{
|
||||||
|
|
@ -46,18 +47,34 @@ namespace Microsoft.Net.Http.Server
|
||||||
private ResponseStream _nativeStream;
|
private ResponseStream _nativeStream;
|
||||||
private long _expectedBodyLength;
|
private long _expectedBodyLength;
|
||||||
private BoundaryType _boundaryType;
|
private BoundaryType _boundaryType;
|
||||||
private UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_V2 _nativeResponse;
|
private HttpApi.HTTP_RESPONSE_V2 _nativeResponse;
|
||||||
private IList<Tuple<Action<object>, object>> _onSendingHeadersActions;
|
private IList<Tuple<Action<object>, object>> _onSendingHeadersActions;
|
||||||
private IList<Tuple<Action<object>, object>> _onResponseCompletedActions;
|
private IList<Tuple<Action<object>, object>> _onResponseCompletedActions;
|
||||||
|
|
||||||
private RequestContext _requestContext;
|
private RequestContext _requestContext;
|
||||||
|
private bool _bufferingEnabled;
|
||||||
|
|
||||||
internal Response(RequestContext httpContext)
|
internal Response(RequestContext requestContext)
|
||||||
{
|
{
|
||||||
// TODO: Verbose log
|
// TODO: Verbose log
|
||||||
_requestContext = httpContext;
|
_requestContext = requestContext;
|
||||||
_nativeResponse = new UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_V2();
|
|
||||||
_headers = new HeaderCollection();
|
_headers = new HeaderCollection();
|
||||||
|
Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
if (_responseState >= ResponseState.StartedSending)
|
||||||
|
{
|
||||||
|
_requestContext.Abort();
|
||||||
|
throw new InvalidOperationException("The response has already been sent. Request Aborted.");
|
||||||
|
}
|
||||||
|
// 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;
|
_boundaryType = BoundaryType.None;
|
||||||
_nativeResponse.Response_V1.StatusCode = (ushort)HttpStatusCode.OK;
|
_nativeResponse.Response_V1.StatusCode = (ushort)HttpStatusCode.OK;
|
||||||
_nativeResponse.Response_V1.Version.MajorVersion = 1;
|
_nativeResponse.Response_V1.Version.MajorVersion = 1;
|
||||||
|
|
@ -65,13 +82,17 @@ namespace Microsoft.Net.Http.Server
|
||||||
_responseState = ResponseState.Created;
|
_responseState = ResponseState.Created;
|
||||||
_onSendingHeadersActions = new List<Tuple<Action<object>, object>>();
|
_onSendingHeadersActions = new List<Tuple<Action<object>, object>>();
|
||||||
_onResponseCompletedActions = new List<Tuple<Action<object>, object>>();
|
_onResponseCompletedActions = new List<Tuple<Action<object>, object>>();
|
||||||
|
_bufferingEnabled = _requestContext.Server.BufferResponses;
|
||||||
|
_expectedBodyLength = 0;
|
||||||
|
_nativeStream = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum ResponseState
|
private enum ResponseState
|
||||||
{
|
{
|
||||||
Created,
|
Created,
|
||||||
|
Started,
|
||||||
ComputedHeaders,
|
ComputedHeaders,
|
||||||
SentHeaders,
|
StartedSending,
|
||||||
Closed,
|
Closed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,14 +126,6 @@ namespace Microsoft.Net.Http.Server
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CheckResponseStarted()
|
|
||||||
{
|
|
||||||
if (_responseState >= ResponseState.SentHeaders)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Headers already sent.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ReasonPhrase
|
public string ReasonPhrase
|
||||||
{
|
{
|
||||||
get { return _reasonPhrase; }
|
get { return _reasonPhrase; }
|
||||||
|
|
@ -124,6 +137,16 @@ namespace Microsoft.Net.Http.Server
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool ShouldBuffer
|
||||||
|
{
|
||||||
|
get { return _bufferingEnabled; }
|
||||||
|
set
|
||||||
|
{
|
||||||
|
CheckResponseStarted();
|
||||||
|
_bufferingEnabled = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Stream Body
|
public Stream Body
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
@ -168,10 +191,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
|
|
||||||
internal long ExpectedBodyLength
|
internal long ExpectedBodyLength
|
||||||
{
|
{
|
||||||
get
|
get { return _expectedBodyLength; }
|
||||||
{
|
|
||||||
return _expectedBodyLength;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header accessors
|
// Header accessors
|
||||||
|
|
@ -248,6 +268,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Start();
|
||||||
NotifyOnResponseCompleted();
|
NotifyOnResponseCompleted();
|
||||||
// TODO: Verbose log
|
// TODO: Verbose log
|
||||||
EnsureResponseStream();
|
EnsureResponseStream();
|
||||||
|
|
@ -259,26 +280,30 @@ namespace Microsoft.Net.Http.Server
|
||||||
|
|
||||||
internal BoundaryType BoundaryType
|
internal BoundaryType BoundaryType
|
||||||
{
|
{
|
||||||
get
|
get { return _boundaryType; }
|
||||||
{
|
|
||||||
return _boundaryType;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool HeadersSent
|
public bool HasStarted
|
||||||
{
|
{
|
||||||
get
|
get { return _responseState >= ResponseState.Started; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckResponseStarted()
|
||||||
|
{
|
||||||
|
if (HasStarted)
|
||||||
{
|
{
|
||||||
return _responseState >= ResponseState.SentHeaders;
|
throw new InvalidOperationException("Headers already sent.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal bool ComputedHeaders
|
internal bool ComputedHeaders
|
||||||
{
|
{
|
||||||
get
|
get { return _responseState >= ResponseState.ComputedHeaders; }
|
||||||
{
|
}
|
||||||
return _responseState >= ResponseState.ComputedHeaders;
|
|
||||||
}
|
public bool HasStartedSending
|
||||||
|
{
|
||||||
|
get { return _responseState >= ResponseState.StartedSending; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnsureResponseStream()
|
private void EnsureResponseStream()
|
||||||
|
|
@ -319,14 +344,14 @@ namespace Microsoft.Net.Http.Server
|
||||||
// What would we loose by bypassing HttpSendHttpResponse?
|
// 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.
|
// TODO: Consider using the HTTP_SEND_RESPONSE_FLAG_BUFFER_DATA flag for most/all responses rather than just Opaque.
|
||||||
internal unsafe uint SendHeaders(UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK* pDataChunk,
|
internal unsafe uint SendHeaders(HttpApi.HTTP_DATA_CHUNK[] dataChunks,
|
||||||
ResponseStreamAsyncResult asyncResult,
|
ResponseStreamAsyncResult asyncResult,
|
||||||
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags,
|
HttpApi.HTTP_FLAGS flags,
|
||||||
bool isOpaqueUpgrade)
|
bool isOpaqueUpgrade)
|
||||||
{
|
{
|
||||||
Debug.Assert(!HeadersSent, "HttpListenerResponse::SendHeaders()|SentHeaders is true.");
|
Debug.Assert(!HasStartedSending, "HttpListenerResponse::SendHeaders()|SentHeaders is true.");
|
||||||
|
|
||||||
_responseState = ResponseState.SentHeaders;
|
_responseState = ResponseState.StartedSending;
|
||||||
var reasonPhrase = GetReasonPhrase(StatusCode);
|
var reasonPhrase = GetReasonPhrase(StatusCode);
|
||||||
|
|
||||||
if (RequestContext.Logger.IsEnabled(LogLevel.Verbose))
|
if (RequestContext.Logger.IsEnabled(LogLevel.Verbose))
|
||||||
|
|
@ -344,10 +369,16 @@ namespace Microsoft.Net.Http.Server
|
||||||
List<GCHandle> pinnedHeaders = SerializeHeaders(isOpaqueUpgrade);
|
List<GCHandle> pinnedHeaders = SerializeHeaders(isOpaqueUpgrade);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (pDataChunk != null)
|
if (dataChunks != null)
|
||||||
{
|
{
|
||||||
_nativeResponse.Response_V1.EntityChunkCount = 1;
|
if (pinnedHeaders == null)
|
||||||
_nativeResponse.Response_V1.pEntityChunks = pDataChunk;
|
{
|
||||||
|
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)
|
else if (asyncResult != null && asyncResult.DataChunks != null)
|
||||||
{
|
{
|
||||||
|
|
@ -360,45 +391,16 @@ namespace Microsoft.Net.Http.Server
|
||||||
_nativeResponse.Response_V1.pEntityChunks = null;
|
_nativeResponse.Response_V1.pEntityChunks = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reasonPhrase.Length > 0)
|
byte[] reasonPhraseBytes = new byte[HeaderEncoding.GetByteCount(reasonPhrase)];
|
||||||
|
fixed (byte* pReasonPhrase = reasonPhraseBytes)
|
||||||
{
|
{
|
||||||
byte[] reasonPhraseBytes = new byte[HeaderEncoding.GetByteCount(reasonPhrase)];
|
_nativeResponse.Response_V1.ReasonLength = (ushort)reasonPhraseBytes.Length;
|
||||||
fixed (byte* pReasonPhrase = reasonPhraseBytes)
|
HeaderEncoding.GetBytes(reasonPhrase, 0, reasonPhraseBytes.Length, reasonPhraseBytes, 0);
|
||||||
{
|
_nativeResponse.Response_V1.pReason = (sbyte*)pReasonPhrase;
|
||||||
_nativeResponse.Response_V1.ReasonLength = (ushort)reasonPhraseBytes.Length;
|
fixed (HttpApi.HTTP_RESPONSE_V2* pResponse = &_nativeResponse)
|
||||||
HeaderEncoding.GetBytes(reasonPhrase, 0, reasonPhraseBytes.Length, reasonPhraseBytes, 0);
|
|
||||||
_nativeResponse.Response_V1.pReason = (sbyte*)pReasonPhrase;
|
|
||||||
fixed (UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_V2* pResponse = &_nativeResponse)
|
|
||||||
{
|
|
||||||
statusCode =
|
|
||||||
UnsafeNclNativeMethods.HttpApi.HttpSendHttpResponse(
|
|
||||||
RequestContext.RequestQueueHandle,
|
|
||||||
Request.RequestId,
|
|
||||||
(uint)flags,
|
|
||||||
pResponse,
|
|
||||||
null,
|
|
||||||
&bytesSent,
|
|
||||||
SafeLocalFree.Zero,
|
|
||||||
0,
|
|
||||||
asyncResult == null ? SafeNativeOverlapped.Zero : asyncResult.NativeOverlapped,
|
|
||||||
IntPtr.Zero);
|
|
||||||
|
|
||||||
if (asyncResult != null &&
|
|
||||||
statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS &&
|
|
||||||
WebListener.SkipIOCPCallbackOnSuccess)
|
|
||||||
{
|
|
||||||
asyncResult.BytesSent = bytesSent;
|
|
||||||
// The caller will invoke IOCompleted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
fixed (UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_V2* pResponse = &_nativeResponse)
|
|
||||||
{
|
{
|
||||||
statusCode =
|
statusCode =
|
||||||
UnsafeNclNativeMethods.HttpApi.HttpSendHttpResponse(
|
HttpApi.HttpSendHttpResponse(
|
||||||
RequestContext.RequestQueueHandle,
|
RequestContext.RequestQueueHandle,
|
||||||
Request.RequestId,
|
Request.RequestId,
|
||||||
(uint)flags,
|
(uint)flags,
|
||||||
|
|
@ -411,7 +413,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
IntPtr.Zero);
|
IntPtr.Zero);
|
||||||
|
|
||||||
if (asyncResult != null &&
|
if (asyncResult != null &&
|
||||||
statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS &&
|
statusCode == ErrorCodes.ERROR_SUCCESS &&
|
||||||
WebListener.SkipIOCPCallbackOnSuccess)
|
WebListener.SkipIOCPCallbackOnSuccess)
|
||||||
{
|
{
|
||||||
asyncResult.BytesSent = bytesSent;
|
asyncResult.BytesSent = bytesSent;
|
||||||
|
|
@ -427,10 +429,20 @@ namespace Microsoft.Net.Http.Server
|
||||||
return statusCode;
|
return statusCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS ComputeHeaders(bool endOfRequest = false)
|
internal void Start()
|
||||||
{
|
{
|
||||||
// Notify that this is absolutely the last chance to make changes.
|
if (!HasStarted)
|
||||||
NotifyOnSendingHeaders();
|
{
|
||||||
|
// Notify that this is absolutely the last chance to make changes.
|
||||||
|
NotifyOnSendingHeaders();
|
||||||
|
Headers.IsReadOnly = true; // Prohibit further modifications.
|
||||||
|
_responseState = ResponseState.Started;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal HttpApi.HTTP_FLAGS ComputeHeaders(bool endOfRequest = false, int bufferedBytes = 0)
|
||||||
|
{
|
||||||
|
Headers.IsReadOnly = false; // Temporarily allow modification.
|
||||||
|
|
||||||
// 401
|
// 401
|
||||||
if (StatusCode == (ushort)HttpStatusCode.Unauthorized)
|
if (StatusCode == (ushort)HttpStatusCode.Unauthorized)
|
||||||
|
|
@ -438,7 +450,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
RequestContext.Server.AuthenticationManager.SetAuthenticationChallenge(RequestContext);
|
RequestContext.Server.AuthenticationManager.SetAuthenticationChallenge(RequestContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
var flags = UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE;
|
var flags = HttpApi.HTTP_FLAGS.NONE;
|
||||||
Debug.Assert(!ComputedHeaders, "HttpListenerResponse::ComputeHeaders()|ComputedHeaders is true.");
|
Debug.Assert(!ComputedHeaders, "HttpListenerResponse::ComputeHeaders()|ComputedHeaders is true.");
|
||||||
_responseState = ResponseState.ComputedHeaders;
|
_responseState = ResponseState.ComputedHeaders;
|
||||||
|
|
||||||
|
|
@ -478,14 +490,18 @@ namespace Microsoft.Net.Http.Server
|
||||||
// The application is performing it's own chunking.
|
// The application is performing it's own chunking.
|
||||||
_boundaryType = BoundaryType.PassThrough;
|
_boundaryType = BoundaryType.PassThrough;
|
||||||
}
|
}
|
||||||
else if (endOfRequest && !(isHeadRequest && statusCanHaveBody)) // HEAD requests always end without a body. Assume a GET response would have a body.
|
else if (endOfRequest && !(isHeadRequest && statusCanHaveBody)) // HEAD requests should always end without a body. Assume a GET response would have a body.
|
||||||
{
|
{
|
||||||
if (statusCanHaveBody)
|
if (bufferedBytes > 0)
|
||||||
|
{
|
||||||
|
Headers[HttpKnownHeaderNames.ContentLength] = bufferedBytes.ToString(CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
else if (statusCanHaveBody)
|
||||||
{
|
{
|
||||||
Headers[HttpKnownHeaderNames.ContentLength] = Constants.Zero;
|
Headers[HttpKnownHeaderNames.ContentLength] = Constants.Zero;
|
||||||
}
|
}
|
||||||
_boundaryType = BoundaryType.ContentLength;
|
_boundaryType = BoundaryType.ContentLength;
|
||||||
_expectedBodyLength = 0;
|
_expectedBodyLength = bufferedBytes;
|
||||||
}
|
}
|
||||||
else if (keepConnectionAlive && requestVersion == Constants.V1_1)
|
else if (keepConnectionAlive && requestVersion == Constants.V1_1)
|
||||||
{
|
{
|
||||||
|
|
@ -508,9 +524,10 @@ namespace Microsoft.Net.Http.Server
|
||||||
{
|
{
|
||||||
Headers.Append(HttpKnownHeaderNames.Connection, Constants.Close);
|
Headers.Append(HttpKnownHeaderNames.Connection, Constants.Close);
|
||||||
}
|
}
|
||||||
flags = UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_DISCONNECT;
|
flags = HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_DISCONNECT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Headers.IsReadOnly = true; // Prohibit further modifications.
|
||||||
return flags;
|
return flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -521,9 +538,9 @@ namespace Microsoft.Net.Http.Server
|
||||||
|
|
||||||
private List<GCHandle> SerializeHeaders(bool isOpaqueUpgrade)
|
private List<GCHandle> SerializeHeaders(bool isOpaqueUpgrade)
|
||||||
{
|
{
|
||||||
Headers.Sent = true; // Prohibit further modifications.
|
Headers.IsReadOnly = true; // Prohibit further modifications.
|
||||||
UnsafeNclNativeMethods.HttpApi.HTTP_UNKNOWN_HEADER[] unknownHeaders = null;
|
HttpApi.HTTP_UNKNOWN_HEADER[] unknownHeaders = null;
|
||||||
UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_INFO[] knownHeaderInfo = null;
|
HttpApi.HTTP_RESPONSE_INFO[] knownHeaderInfo = null;
|
||||||
List<GCHandle> pinnedHeaders;
|
List<GCHandle> pinnedHeaders;
|
||||||
GCHandle gcHandle;
|
GCHandle gcHandle;
|
||||||
/*
|
/*
|
||||||
|
|
@ -552,11 +569,11 @@ namespace Microsoft.Net.Http.Server
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// See if this is an unknown header
|
// See if this is an unknown header
|
||||||
lookup = UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_HEADER_ID.IndexOfKnownHeader(headerPair.Key);
|
lookup = HttpApi.HTTP_RESPONSE_HEADER_ID.IndexOfKnownHeader(headerPair.Key);
|
||||||
|
|
||||||
// Http.Sys doesn't let us send the Connection: Upgrade header as a Known header.
|
// Http.Sys doesn't let us send the Connection: Upgrade header as a Known header.
|
||||||
if (lookup == -1 ||
|
if (lookup == -1 ||
|
||||||
(isOpaqueUpgrade && lookup == (int)UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_HEADER_ID.Enum.HttpHeaderConnection))
|
(isOpaqueUpgrade && lookup == (int)HttpApi.HTTP_RESPONSE_HEADER_ID.Enum.HttpHeaderConnection))
|
||||||
{
|
{
|
||||||
numUnknownHeaders += headerPair.Value.Length;
|
numUnknownHeaders += headerPair.Value.Length;
|
||||||
}
|
}
|
||||||
|
|
@ -569,7 +586,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
fixed (UnsafeNclNativeMethods.HttpApi.HTTP_KNOWN_HEADER* pKnownHeaders = &_nativeResponse.Response_V1.Headers.KnownHeaders)
|
fixed (HttpApi.HTTP_KNOWN_HEADER* pKnownHeaders = &_nativeResponse.Response_V1.Headers.KnownHeaders)
|
||||||
{
|
{
|
||||||
foreach (KeyValuePair<string, string[]> headerPair in Headers)
|
foreach (KeyValuePair<string, string[]> headerPair in Headers)
|
||||||
{
|
{
|
||||||
|
|
@ -580,18 +597,18 @@ namespace Microsoft.Net.Http.Server
|
||||||
}
|
}
|
||||||
headerName = headerPair.Key;
|
headerName = headerPair.Key;
|
||||||
string[] headerValues = headerPair.Value;
|
string[] headerValues = headerPair.Value;
|
||||||
lookup = UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_HEADER_ID.IndexOfKnownHeader(headerName);
|
lookup = HttpApi.HTTP_RESPONSE_HEADER_ID.IndexOfKnownHeader(headerName);
|
||||||
|
|
||||||
// Http.Sys doesn't let us send the Connection: Upgrade header as a Known header.
|
// Http.Sys doesn't let us send the Connection: Upgrade header as a Known header.
|
||||||
if (lookup == -1 ||
|
if (lookup == -1 ||
|
||||||
(isOpaqueUpgrade && lookup == (int)UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_HEADER_ID.Enum.HttpHeaderConnection))
|
(isOpaqueUpgrade && lookup == (int)HttpApi.HTTP_RESPONSE_HEADER_ID.Enum.HttpHeaderConnection))
|
||||||
{
|
{
|
||||||
if (unknownHeaders == null)
|
if (unknownHeaders == null)
|
||||||
{
|
{
|
||||||
unknownHeaders = new UnsafeNclNativeMethods.HttpApi.HTTP_UNKNOWN_HEADER[numUnknownHeaders];
|
unknownHeaders = new HttpApi.HTTP_UNKNOWN_HEADER[numUnknownHeaders];
|
||||||
gcHandle = GCHandle.Alloc(unknownHeaders, GCHandleType.Pinned);
|
gcHandle = GCHandle.Alloc(unknownHeaders, GCHandleType.Pinned);
|
||||||
pinnedHeaders.Add(gcHandle);
|
pinnedHeaders.Add(gcHandle);
|
||||||
_nativeResponse.Response_V1.Headers.pUnknownHeaders = (UnsafeNclNativeMethods.HttpApi.HTTP_UNKNOWN_HEADER*)gcHandle.AddrOfPinnedObject();
|
_nativeResponse.Response_V1.Headers.pUnknownHeaders = (HttpApi.HTTP_UNKNOWN_HEADER*)gcHandle.AddrOfPinnedObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int headerValueIndex = 0; headerValueIndex < headerValues.Length; headerValueIndex++)
|
for (int headerValueIndex = 0; headerValueIndex < headerValues.Length; headerValueIndex++)
|
||||||
|
|
@ -632,29 +649,29 @@ namespace Microsoft.Net.Http.Server
|
||||||
{
|
{
|
||||||
if (knownHeaderInfo == null)
|
if (knownHeaderInfo == null)
|
||||||
{
|
{
|
||||||
knownHeaderInfo = new UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_INFO[numKnownMultiHeaders];
|
knownHeaderInfo = new HttpApi.HTTP_RESPONSE_INFO[numKnownMultiHeaders];
|
||||||
gcHandle = GCHandle.Alloc(knownHeaderInfo, GCHandleType.Pinned);
|
gcHandle = GCHandle.Alloc(knownHeaderInfo, GCHandleType.Pinned);
|
||||||
pinnedHeaders.Add(gcHandle);
|
pinnedHeaders.Add(gcHandle);
|
||||||
_nativeResponse.pResponseInfo = (UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_INFO*)gcHandle.AddrOfPinnedObject();
|
_nativeResponse.pResponseInfo = (HttpApi.HTTP_RESPONSE_INFO*)gcHandle.AddrOfPinnedObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
knownHeaderInfo[_nativeResponse.ResponseInfoCount].Type = UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_INFO_TYPE.HttpResponseInfoTypeMultipleKnownHeaders;
|
knownHeaderInfo[_nativeResponse.ResponseInfoCount].Type = HttpApi.HTTP_RESPONSE_INFO_TYPE.HttpResponseInfoTypeMultipleKnownHeaders;
|
||||||
knownHeaderInfo[_nativeResponse.ResponseInfoCount].Length =
|
knownHeaderInfo[_nativeResponse.ResponseInfoCount].Length =
|
||||||
#if DNXCORE50
|
#if DNXCORE50
|
||||||
(uint)Marshal.SizeOf<UnsafeNclNativeMethods.HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS>();
|
(uint)Marshal.SizeOf<HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS>();
|
||||||
#else
|
#else
|
||||||
(uint)Marshal.SizeOf(typeof(UnsafeNclNativeMethods.HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS));
|
(uint)Marshal.SizeOf(typeof(HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS));
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
UnsafeNclNativeMethods.HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS header = new UnsafeNclNativeMethods.HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS();
|
HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS header = new HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS();
|
||||||
|
|
||||||
header.HeaderId = (UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_HEADER_ID.Enum)lookup;
|
header.HeaderId = (HttpApi.HTTP_RESPONSE_HEADER_ID.Enum)lookup;
|
||||||
header.Flags = UnsafeNclNativeMethods.HttpApi.HTTP_RESPONSE_INFO_FLAGS.PreserveOrder; // TODO: The docs say this is for www-auth only.
|
header.Flags = HttpApi.HTTP_RESPONSE_INFO_FLAGS.PreserveOrder; // TODO: The docs say this is for www-auth only.
|
||||||
|
|
||||||
UnsafeNclNativeMethods.HttpApi.HTTP_KNOWN_HEADER[] nativeHeaderValues = new UnsafeNclNativeMethods.HttpApi.HTTP_KNOWN_HEADER[headerValues.Length];
|
HttpApi.HTTP_KNOWN_HEADER[] nativeHeaderValues = new HttpApi.HTTP_KNOWN_HEADER[headerValues.Length];
|
||||||
gcHandle = GCHandle.Alloc(nativeHeaderValues, GCHandleType.Pinned);
|
gcHandle = GCHandle.Alloc(nativeHeaderValues, GCHandleType.Pinned);
|
||||||
pinnedHeaders.Add(gcHandle);
|
pinnedHeaders.Add(gcHandle);
|
||||||
header.KnownHeaders = (UnsafeNclNativeMethods.HttpApi.HTTP_KNOWN_HEADER*)gcHandle.AddrOfPinnedObject();
|
header.KnownHeaders = (HttpApi.HTTP_KNOWN_HEADER*)gcHandle.AddrOfPinnedObject();
|
||||||
|
|
||||||
for (int headerValueIndex = 0; headerValueIndex < headerValues.Length; headerValueIndex++)
|
for (int headerValueIndex = 0; headerValueIndex < headerValues.Length; headerValueIndex++)
|
||||||
{
|
{
|
||||||
|
|
@ -672,7 +689,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
// 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.
|
// 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);
|
gcHandle = GCHandle.Alloc(header, GCHandleType.Pinned);
|
||||||
pinnedHeaders.Add(gcHandle);
|
pinnedHeaders.Add(gcHandle);
|
||||||
knownHeaderInfo[_nativeResponse.ResponseInfoCount].pInfo = (UnsafeNclNativeMethods.HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS*)gcHandle.AddrOfPinnedObject();
|
knownHeaderInfo[_nativeResponse.ResponseInfoCount].pInfo = (HttpApi.HTTP_MULTIPLE_KNOWN_HEADERS*)gcHandle.AddrOfPinnedObject();
|
||||||
|
|
||||||
_nativeResponse.ResponseInfoCount++;
|
_nativeResponse.ResponseInfoCount++;
|
||||||
}
|
}
|
||||||
|
|
@ -704,18 +721,18 @@ namespace Microsoft.Net.Http.Server
|
||||||
// Subset of ComputeHeaders
|
// Subset of ComputeHeaders
|
||||||
internal void SendOpaqueUpgrade()
|
internal void SendOpaqueUpgrade()
|
||||||
{
|
{
|
||||||
// TODO: Should we do this notification earlier when you still have a chance to change the status code to avoid an upgrade?
|
|
||||||
// Notify that this is absolutely the last chance to make changes.
|
// Notify that this is absolutely the last chance to make changes.
|
||||||
NotifyOnSendingHeaders();
|
Start();
|
||||||
|
_boundaryType = BoundaryType.Close;
|
||||||
|
|
||||||
// TODO: Send headers async?
|
// TODO: Send headers async?
|
||||||
ulong errorCode = SendHeaders(null, null,
|
ulong errorCode = SendHeaders(null, null,
|
||||||
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_OPAQUE |
|
HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_OPAQUE |
|
||||||
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA |
|
HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA |
|
||||||
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_BUFFER_DATA,
|
HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_BUFFER_DATA,
|
||||||
true);
|
true);
|
||||||
|
|
||||||
if (errorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
|
if (errorCode != ErrorCodes.ERROR_SUCCESS)
|
||||||
{
|
{
|
||||||
throw new WebListenerException((int)errorCode);
|
throw new WebListenerException((int)errorCode);
|
||||||
}
|
}
|
||||||
|
|
@ -725,7 +742,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
{
|
{
|
||||||
if (_responseState >= ResponseState.Closed)
|
if (_responseState >= ResponseState.Closed)
|
||||||
{
|
{
|
||||||
throw new ObjectDisposedException(this.GetType().FullName);
|
throw new ObjectDisposedException(GetType().FullName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -746,6 +763,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
internal void SwitchToOpaqueMode()
|
internal void SwitchToOpaqueMode()
|
||||||
{
|
{
|
||||||
EnsureResponseStream();
|
EnsureResponseStream();
|
||||||
|
_bufferingEnabled = false;
|
||||||
_nativeStream.SwitchToOpaqueMode();
|
_nativeStream.SwitchToOpaqueMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,23 +22,27 @@
|
||||||
// ------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using static Microsoft.Net.Http.Server.UnsafeNclNativeMethods;
|
||||||
|
|
||||||
namespace Microsoft.Net.Http.Server
|
namespace Microsoft.Net.Http.Server
|
||||||
{
|
{
|
||||||
internal class ResponseStream : Stream
|
internal class ResponseStream : Stream
|
||||||
{
|
{
|
||||||
private static readonly byte[] ChunkTerminator = new byte[] { (byte)'0', (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n' };
|
private const int MaxBufferSize = 4 * 1024;
|
||||||
|
|
||||||
private RequestContext _requestContext;
|
private RequestContext _requestContext;
|
||||||
private long _leftToWrite = long.MinValue;
|
private long _leftToWrite = long.MinValue;
|
||||||
private bool _closed;
|
private bool _closed;
|
||||||
private bool _inOpaqueMode;
|
private bool _inOpaqueMode;
|
||||||
|
private BufferBuilder _buffer = new BufferBuilder();
|
||||||
|
|
||||||
// The last write needs special handling to cancel.
|
// The last write needs special handling to cancel.
|
||||||
private ResponseStreamAsyncResult _lastWrite;
|
private ResponseStreamAsyncResult _lastWrite;
|
||||||
|
|
||||||
|
|
@ -99,49 +103,185 @@ namespace Microsoft.Net.Http.Server
|
||||||
// Send headers
|
// Send headers
|
||||||
public override void Flush()
|
public override void Flush()
|
||||||
{
|
{
|
||||||
if (_closed || _requestContext.Response.HeadersSent)
|
if (_closed)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = ComputeLeftToWrite();
|
FlushInternal(endOfRequest: false);
|
||||||
// TODO: Verbose log
|
}
|
||||||
|
|
||||||
|
private unsafe void FlushInternal(bool endOfRequest)
|
||||||
|
{
|
||||||
|
bool startedSending = _requestContext.Response.HasStartedSending;
|
||||||
|
var byteCount = _buffer.TotalBytes;
|
||||||
|
if (byteCount == 0 && startedSending && !endOfRequest)
|
||||||
|
{
|
||||||
|
// Empty flush
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var flags = ComputeLeftToWrite(endOfRequest);
|
||||||
|
if (!_inOpaqueMode && endOfRequest && _leftToWrite > byteCount)
|
||||||
|
{
|
||||||
|
_requestContext.Abort();
|
||||||
|
// This is logged rather than thrown because it is too late for an exception to be visible in user code.
|
||||||
|
LogHelper.LogError(_requestContext.Logger, "ResponseStream::Dispose", "Fewer bytes were written than were specified in the Content-Length.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endOfRequest && _requestContext.Response.BoundaryType == BoundaryType.Close)
|
||||||
|
{
|
||||||
|
flags |= HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_DISCONNECT;
|
||||||
|
}
|
||||||
|
else if (!endOfRequest && _leftToWrite != byteCount)
|
||||||
|
{
|
||||||
|
flags |= HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateWritenCount((uint)byteCount);
|
||||||
|
uint statusCode = 0;
|
||||||
|
HttpApi.HTTP_DATA_CHUNK[] dataChunks;
|
||||||
|
var pinnedBuffers = PinDataBuffers(endOfRequest, out dataChunks);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
uint statusCode;
|
if (!startedSending)
|
||||||
unsafe
|
|
||||||
{
|
{
|
||||||
// TODO: Don't add MoreData flag if content-length == 0?
|
statusCode = _requestContext.Response.SendHeaders(dataChunks, null, flags, false);
|
||||||
flags |= UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA;
|
|
||||||
statusCode = _requestContext.Response.SendHeaders(null, null, flags, false);
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_HANDLE_EOF)
|
|
||||||
{
|
{
|
||||||
throw new IOException(string.Empty, new WebListenerException((int)statusCode));
|
fixed (HttpApi.HTTP_DATA_CHUNK* pDataChunks = dataChunks)
|
||||||
|
{
|
||||||
|
statusCode = HttpApi.HttpSendResponseEntityBody(
|
||||||
|
_requestContext.RequestQueueHandle,
|
||||||
|
_requestContext.RequestId,
|
||||||
|
(uint)flags,
|
||||||
|
(ushort)dataChunks.Length,
|
||||||
|
pDataChunks,
|
||||||
|
null,
|
||||||
|
SafeLocalFree.Zero,
|
||||||
|
0,
|
||||||
|
SafeNativeOverlapped.Zero,
|
||||||
|
IntPtr.Zero);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_requestContext.Server.IgnoreWriteExceptions)
|
||||||
|
{
|
||||||
|
statusCode = UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
finally
|
||||||
{
|
{
|
||||||
LogHelper.LogException(_requestContext.Logger, "Flush", e);
|
FreeDataBuffers(pinnedBuffers);
|
||||||
|
_buffer.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusCode != ErrorCodes.ERROR_SUCCESS && statusCode != ErrorCodes.ERROR_HANDLE_EOF
|
||||||
|
// Don't throw for disconnects, we were already finished with the response.
|
||||||
|
&& (!endOfRequest || (statusCode != ErrorCodes.ERROR_CONNECTION_INVALID && statusCode != ErrorCodes.ERROR_INVALID_PARAMETER)))
|
||||||
|
{
|
||||||
|
Exception exception = new IOException(string.Empty, new WebListenerException((int)statusCode));
|
||||||
|
LogHelper.LogException(_requestContext.Logger, "Flush", exception);
|
||||||
Abort();
|
Abort();
|
||||||
throw;
|
throw exception;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send headers
|
private List<GCHandle> PinDataBuffers(bool endOfRequest, out HttpApi.HTTP_DATA_CHUNK[] dataChunks)
|
||||||
public override Task FlushAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
if (_closed || _requestContext.Response.HeadersSent)
|
var pins = new List<GCHandle>();
|
||||||
|
var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked;
|
||||||
|
|
||||||
|
var currentChunk = 0;
|
||||||
|
// Figure out how many data chunks
|
||||||
|
if (chunked && _buffer.TotalBytes == 0 && endOfRequest)
|
||||||
|
{
|
||||||
|
dataChunks = new HttpApi.HTTP_DATA_CHUNK[1];
|
||||||
|
SetDataChunk(dataChunks, ref currentChunk, pins, new ArraySegment<byte>(Helpers.ChunkTerminator));
|
||||||
|
return pins;
|
||||||
|
}
|
||||||
|
else if (_buffer.TotalBytes == 0)
|
||||||
|
{
|
||||||
|
// No data
|
||||||
|
dataChunks = new HttpApi.HTTP_DATA_CHUNK[0];
|
||||||
|
return pins;
|
||||||
|
}
|
||||||
|
|
||||||
|
var chunkCount = _buffer.BufferCount;
|
||||||
|
if (chunked)
|
||||||
|
{
|
||||||
|
// Chunk framing
|
||||||
|
chunkCount += 2;
|
||||||
|
|
||||||
|
if (endOfRequest)
|
||||||
|
{
|
||||||
|
// Chunk terminator
|
||||||
|
chunkCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataChunks = new HttpApi.HTTP_DATA_CHUNK[chunkCount];
|
||||||
|
|
||||||
|
if (chunked)
|
||||||
|
{
|
||||||
|
var chunkHeaderBuffer = Helpers.GetChunkHeader(_buffer.TotalBytes);
|
||||||
|
SetDataChunk(dataChunks, ref currentChunk, pins, chunkHeaderBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var buffer in _buffer.Buffers)
|
||||||
|
{
|
||||||
|
SetDataChunk(dataChunks, ref currentChunk, pins, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunked)
|
||||||
|
{
|
||||||
|
SetDataChunk(dataChunks, ref currentChunk, pins, new ArraySegment<byte>(Helpers.CRLF));
|
||||||
|
|
||||||
|
if (endOfRequest)
|
||||||
|
{
|
||||||
|
SetDataChunk(dataChunks, ref currentChunk, pins, new ArraySegment<byte>(Helpers.ChunkTerminator));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pins;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetDataChunk(HttpApi.HTTP_DATA_CHUNK[] chunks, ref int chunkIndex, List<GCHandle> pins, ArraySegment<byte> buffer)
|
||||||
|
{
|
||||||
|
var handle = GCHandle.Alloc(buffer.Array, GCHandleType.Pinned);
|
||||||
|
pins.Add(handle);
|
||||||
|
chunks[chunkIndex].DataChunkType = HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
|
||||||
|
chunks[chunkIndex].fromMemory.pBuffer = handle.AddrOfPinnedObject() + buffer.Offset;
|
||||||
|
chunks[chunkIndex].fromMemory.BufferLength = (uint)buffer.Count;
|
||||||
|
chunkIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FreeDataBuffers(List<GCHandle> pinnedBuffers)
|
||||||
|
{
|
||||||
|
foreach (var pin in pinnedBuffers)
|
||||||
|
{
|
||||||
|
if (pin.IsAllocated)
|
||||||
|
{
|
||||||
|
pin.Free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Simpler than Flush because it will never be called at the end of the request from Dispose.
|
||||||
|
public unsafe override Task FlushAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (_closed)
|
||||||
{
|
{
|
||||||
return Helpers.CompletedTask();
|
return Helpers.CompletedTask();
|
||||||
}
|
}
|
||||||
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = ComputeLeftToWrite();
|
|
||||||
// TODO: Verbose log
|
|
||||||
|
|
||||||
if (cancellationToken.IsCancellationRequested)
|
bool startedSending = _requestContext.Response.HasStartedSending;
|
||||||
|
var byteCount = _buffer.TotalBytes;
|
||||||
|
if (byteCount == 0 && startedSending)
|
||||||
{
|
{
|
||||||
return Helpers.CanceledTask<int>();
|
// Empty flush
|
||||||
|
return Helpers.CompletedTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
var cancellationRegistration = default(CancellationTokenRegistration);
|
var cancellationRegistration = default(CancellationTokenRegistration);
|
||||||
|
|
@ -150,26 +290,37 @@ namespace Microsoft.Net.Http.Server
|
||||||
cancellationRegistration = cancellationToken.Register(RequestContext.AbortDelegate, _requestContext);
|
cancellationRegistration = cancellationToken.Register(RequestContext.AbortDelegate, _requestContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Don't add MoreData flag if content-length == 0?
|
var flags = ComputeLeftToWrite();
|
||||||
flags |= UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA;
|
if (_leftToWrite != byteCount)
|
||||||
ResponseStreamAsyncResult asyncResult = new ResponseStreamAsyncResult(this, null, null, null, 0, 0, _requestContext.Response.BoundaryType == BoundaryType.Chunked, false, cancellationRegistration);
|
{
|
||||||
|
flags |= HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateWritenCount((uint)byteCount);
|
||||||
|
uint statusCode = 0;
|
||||||
|
var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked;
|
||||||
|
var asyncResult = new ResponseStreamAsyncResult(this, _buffer, chunked, cancellationRegistration);
|
||||||
|
uint bytesSent = 0;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
uint statusCode;
|
if (!startedSending)
|
||||||
unsafe
|
|
||||||
{
|
{
|
||||||
statusCode = _requestContext.Response.SendHeaders(null, asyncResult, flags, false);
|
statusCode = _requestContext.Response.SendHeaders(null, asyncResult, flags, false);
|
||||||
|
bytesSent = asyncResult.BytesSent;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && WebListener.SkipIOCPCallbackOnSuccess)
|
|
||||||
{
|
{
|
||||||
// IO operation completed synchronously - callback won't be called to signal completion.
|
statusCode = HttpApi.HttpSendResponseEntityBody(
|
||||||
asyncResult.IOCompleted(statusCode);
|
_requestContext.RequestQueueHandle,
|
||||||
}
|
_requestContext.RequestId,
|
||||||
else if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
|
(uint)flags,
|
||||||
{
|
asyncResult.DataChunkCount,
|
||||||
throw new IOException(string.Empty, new WebListenerException((int)statusCode));
|
asyncResult.DataChunks,
|
||||||
|
&bytesSent,
|
||||||
|
SafeLocalFree.Zero,
|
||||||
|
0,
|
||||||
|
asyncResult.NativeOverlapped,
|
||||||
|
IntPtr.Zero);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
|
|
@ -180,6 +331,34 @@ namespace Microsoft.Net.Http.Server
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (statusCode != ErrorCodes.ERROR_SUCCESS && statusCode != ErrorCodes.ERROR_IO_PENDING)
|
||||||
|
{
|
||||||
|
asyncResult.Dispose();
|
||||||
|
if (_requestContext.Server.IgnoreWriteExceptions && startedSending)
|
||||||
|
{
|
||||||
|
asyncResult.Complete();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Exception exception = new IOException(string.Empty, new WebListenerException((int)statusCode));
|
||||||
|
LogHelper.LogException(_requestContext.Logger, "FlushAsync", exception);
|
||||||
|
Abort();
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusCode == ErrorCodes.ERROR_SUCCESS && WebListener.SkipIOCPCallbackOnSuccess)
|
||||||
|
{
|
||||||
|
// IO operation completed synchronously - callback won't be called to signal completion.
|
||||||
|
asyncResult.IOCompleted(statusCode, bytesSent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last write, cache it for special cancellation handling.
|
||||||
|
if ((flags & HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA) == 0)
|
||||||
|
{
|
||||||
|
_lastWrite = asyncResult;
|
||||||
|
}
|
||||||
|
|
||||||
return asyncResult.Task;
|
return asyncResult.Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -195,13 +374,13 @@ namespace Microsoft.Net.Http.Server
|
||||||
throw new NotSupportedException(Resources.Exception_NoSeek);
|
throw new NotSupportedException(Resources.Exception_NoSeek);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override int Read([In, Out] byte[] buffer, int offset, int size)
|
public override int Read([In, Out] byte[] buffer, int offset, int count)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(Resources.Exception_WriteOnlyStream);
|
throw new InvalidOperationException(Resources.Exception_WriteOnlyStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !DNXCORE50
|
#if !DNXCORE50
|
||||||
public override IAsyncResult BeginRead(byte[] buffer, int offset, int size, AsyncCallback callback, object state)
|
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(Resources.Exception_WriteOnlyStream);
|
throw new InvalidOperationException(Resources.Exception_WriteOnlyStream);
|
||||||
}
|
}
|
||||||
|
|
@ -225,7 +404,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE;
|
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE;
|
||||||
if (!_requestContext.Response.ComputedHeaders)
|
if (!_requestContext.Response.ComputedHeaders)
|
||||||
{
|
{
|
||||||
flags = _requestContext.Response.ComputeHeaders(endOfRequest: endOfRequest);
|
flags = _requestContext.Response.ComputeHeaders(endOfRequest, _buffer.TotalBytes);
|
||||||
}
|
}
|
||||||
if (_leftToWrite == long.MinValue)
|
if (_leftToWrite == long.MinValue)
|
||||||
{
|
{
|
||||||
|
|
@ -246,229 +425,56 @@ namespace Microsoft.Net.Http.Server
|
||||||
return flags;
|
return flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override unsafe void Write(byte[] buffer, int offset, int size)
|
public override void Write(byte[] buffer, int offset, int count)
|
||||||
{
|
{
|
||||||
if (buffer == null)
|
// Validates for null and bounds. Allows count == 0.
|
||||||
{
|
var data = new ArraySegment<byte>(buffer, offset, count);
|
||||||
throw new ArgumentNullException("buffer");
|
CheckDisposed();
|
||||||
}
|
|
||||||
if (offset < 0 || offset > buffer.Length)
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException("offset");
|
|
||||||
}
|
|
||||||
if (size < 0 || size > buffer.Length - offset)
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException("size");
|
|
||||||
}
|
|
||||||
if (_closed)
|
|
||||||
{
|
|
||||||
throw new ObjectDisposedException(GetType().FullName);
|
|
||||||
}
|
|
||||||
// TODO: Verbose log parameters
|
// TODO: Verbose log parameters
|
||||||
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = ComputeLeftToWrite();
|
// Officially starts the response and fires OnSendingHeaders
|
||||||
if (size == 0 && _leftToWrite != 0)
|
_requestContext.Response.Start();
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (_leftToWrite >= 0 && size > _leftToWrite)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(Resources.Exception_TooMuchWritten);
|
|
||||||
}
|
|
||||||
// TODO: Verbose log
|
|
||||||
|
|
||||||
uint statusCode;
|
var currentBytes = _buffer.TotalBytes + data.Count;
|
||||||
uint dataToWrite = (uint)size;
|
var contentLength = _requestContext.Response.ContentLength;
|
||||||
SafeLocalFree bufferAsIntPtr = null;
|
if (contentLength.HasValue && !_requestContext.Response.ComputedHeaders && contentLength.Value <= currentBytes)
|
||||||
IntPtr pBufferAsIntPtr = IntPtr.Zero;
|
|
||||||
bool sentHeaders = _requestContext.Response.HeadersSent;
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
if (size == 0)
|
if (contentLength.Value < currentBytes)
|
||||||
{
|
{
|
||||||
// TODO: Is this code path accessible? Is this like a Flush?
|
throw new InvalidOperationException("More bytes written than specified in the Content-Length header.");
|
||||||
statusCode = _requestContext.Response.SendHeaders(null, null, flags, false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
fixed (byte* pDataBuffer = buffer)
|
|
||||||
{
|
|
||||||
byte* pBuffer = pDataBuffer;
|
|
||||||
if (_requestContext.Response.BoundaryType == BoundaryType.Chunked)
|
|
||||||
{
|
|
||||||
// TODO:
|
|
||||||
// here we need some heuristics, some time it is definitely better to split this in 3 write calls
|
|
||||||
// but for small writes it is probably good enough to just copy the data internally.
|
|
||||||
string chunkHeader = size.ToString("x", CultureInfo.InvariantCulture);
|
|
||||||
dataToWrite = dataToWrite + (uint)(chunkHeader.Length + 4);
|
|
||||||
bufferAsIntPtr = SafeLocalFree.LocalAlloc((int)dataToWrite);
|
|
||||||
pBufferAsIntPtr = bufferAsIntPtr.DangerousGetHandle();
|
|
||||||
for (int i = 0; i < chunkHeader.Length; i++)
|
|
||||||
{
|
|
||||||
Marshal.WriteByte(pBufferAsIntPtr, i, (byte)chunkHeader[i]);
|
|
||||||
}
|
|
||||||
Marshal.WriteInt16(pBufferAsIntPtr, chunkHeader.Length, 0x0A0D);
|
|
||||||
Marshal.Copy(buffer, offset, IntPtrHelper.Add(pBufferAsIntPtr, chunkHeader.Length + 2), size);
|
|
||||||
Marshal.WriteInt16(pBufferAsIntPtr, (int)(dataToWrite - 2), 0x0A0D);
|
|
||||||
pBuffer = (byte*)pBufferAsIntPtr;
|
|
||||||
offset = 0;
|
|
||||||
}
|
|
||||||
UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK dataChunk = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK();
|
|
||||||
dataChunk.DataChunkType = UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
|
|
||||||
dataChunk.fromMemory.pBuffer = (IntPtr)(pBuffer + offset);
|
|
||||||
dataChunk.fromMemory.BufferLength = dataToWrite;
|
|
||||||
|
|
||||||
flags |= _leftToWrite == size ? UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE : UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA;
|
|
||||||
if (!sentHeaders)
|
|
||||||
{
|
|
||||||
statusCode = _requestContext.Response.SendHeaders(&dataChunk, null, flags, false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
statusCode =
|
|
||||||
UnsafeNclNativeMethods.HttpApi.HttpSendResponseEntityBody(
|
|
||||||
_requestContext.RequestQueueHandle,
|
|
||||||
_requestContext.RequestId,
|
|
||||||
(uint)flags,
|
|
||||||
1,
|
|
||||||
&dataChunk,
|
|
||||||
null,
|
|
||||||
SafeLocalFree.Zero,
|
|
||||||
0,
|
|
||||||
SafeNativeOverlapped.Zero,
|
|
||||||
IntPtr.Zero);
|
|
||||||
|
|
||||||
if (_requestContext.Server.IgnoreWriteExceptions)
|
|
||||||
{
|
|
||||||
statusCode = UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// or the last write in a response that hasn't started yet, flush immideately
|
||||||
|
_buffer.Add(data);
|
||||||
|
Flush();
|
||||||
}
|
}
|
||||||
finally
|
// The last write in a response that has already started, flush immidately
|
||||||
|
else if (_requestContext.Response.ComputedHeaders && _leftToWrite >= 0 && _leftToWrite <= currentBytes)
|
||||||
{
|
{
|
||||||
if (bufferAsIntPtr != null)
|
if (_leftToWrite < currentBytes)
|
||||||
{
|
{
|
||||||
// free unmanaged buffer
|
throw new InvalidOperationException("More bytes written than specified in the Content-Length header.");
|
||||||
bufferAsIntPtr.Dispose();
|
|
||||||
}
|
}
|
||||||
|
_buffer.Add(data);
|
||||||
|
Flush();
|
||||||
}
|
}
|
||||||
|
else if (_requestContext.Response.ShouldBuffer && currentBytes < MaxBufferSize)
|
||||||
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_HANDLE_EOF)
|
|
||||||
{
|
{
|
||||||
Exception exception = new IOException(string.Empty, new WebListenerException((int)statusCode));
|
_buffer.CopyAndAdd(data);
|
||||||
LogHelper.LogException(_requestContext.Logger, "Write", exception);
|
}
|
||||||
Abort();
|
else
|
||||||
throw exception;
|
{
|
||||||
|
// Append to existing data without a copy, and then flush immidately
|
||||||
|
_buffer.Add(data);
|
||||||
|
Flush();
|
||||||
}
|
}
|
||||||
UpdateWritenCount(dataToWrite);
|
|
||||||
|
|
||||||
// TODO: Verbose log data written
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DNXCORE50
|
#if DNXCORE50
|
||||||
public unsafe IAsyncResult BeginWrite(byte[] buffer, int offset, int size, AsyncCallback callback, object state)
|
public unsafe IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
||||||
#else
|
#else
|
||||||
public override unsafe IAsyncResult BeginWrite(byte[] buffer, int offset, int size, AsyncCallback callback, object state)
|
public override unsafe IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
||||||
#endif
|
#endif
|
||||||
{
|
{
|
||||||
if (buffer == null)
|
return WriteAsync(buffer, offset, count).ToIAsyncResult(callback, state);
|
||||||
{
|
|
||||||
throw new ArgumentNullException("buffer");
|
|
||||||
}
|
|
||||||
if (offset < 0 || offset > buffer.Length)
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException("offset");
|
|
||||||
}
|
|
||||||
if (size < 0 || size > buffer.Length - offset)
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException("size");
|
|
||||||
}
|
|
||||||
if (_closed)
|
|
||||||
{
|
|
||||||
throw new ObjectDisposedException(GetType().FullName);
|
|
||||||
}
|
|
||||||
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = ComputeLeftToWrite();
|
|
||||||
if (size == 0 && _leftToWrite != 0)
|
|
||||||
{
|
|
||||||
ResponseStreamAsyncResult result = new ResponseStreamAsyncResult(this, state, callback);
|
|
||||||
result.Complete();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
if (_leftToWrite >= 0 && size > _leftToWrite)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(Resources.Exception_TooMuchWritten);
|
|
||||||
}
|
|
||||||
// TODO: Verbose log parameters
|
|
||||||
|
|
||||||
uint statusCode;
|
|
||||||
uint bytesSent = 0;
|
|
||||||
flags |= _leftToWrite == size ? UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE : UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA;
|
|
||||||
bool sentHeaders = _requestContext.Response.HeadersSent;
|
|
||||||
ResponseStreamAsyncResult asyncResult = new ResponseStreamAsyncResult(this, state, callback, buffer, offset, size, _requestContext.Response.BoundaryType == BoundaryType.Chunked, sentHeaders);
|
|
||||||
|
|
||||||
// Update m_LeftToWrite now so we can queue up additional BeginWrite's without waiting for EndWrite.
|
|
||||||
UpdateWritenCount((uint)((_requestContext.Response.BoundaryType == BoundaryType.Chunked) ? 0 : size));
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!sentHeaders)
|
|
||||||
{
|
|
||||||
statusCode = _requestContext.Response.SendHeaders(null, asyncResult, flags, false);
|
|
||||||
bytesSent = asyncResult.BytesSent;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
statusCode =
|
|
||||||
UnsafeNclNativeMethods.HttpApi.HttpSendResponseEntityBody(
|
|
||||||
_requestContext.RequestQueueHandle,
|
|
||||||
_requestContext.RequestId,
|
|
||||||
(uint)flags,
|
|
||||||
asyncResult.DataChunkCount,
|
|
||||||
asyncResult.DataChunks,
|
|
||||||
&bytesSent,
|
|
||||||
SafeLocalFree.Zero,
|
|
||||||
0,
|
|
||||||
asyncResult.NativeOverlapped,
|
|
||||||
IntPtr.Zero);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
LogHelper.LogException(_requestContext.Logger, "BeginWrite", e);
|
|
||||||
asyncResult.Dispose();
|
|
||||||
Abort();
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
|
|
||||||
{
|
|
||||||
asyncResult.Dispose();
|
|
||||||
if (_requestContext.Server.IgnoreWriteExceptions && sentHeaders)
|
|
||||||
{
|
|
||||||
asyncResult.Complete();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Exception exception = new IOException(string.Empty, new WebListenerException((int)statusCode));
|
|
||||||
LogHelper.LogException(_requestContext.Logger, "BeginWrite", exception);
|
|
||||||
Abort();
|
|
||||||
throw exception;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && WebListener.SkipIOCPCallbackOnSuccess)
|
|
||||||
{
|
|
||||||
// IO operation completed synchronously - callback won't be called to signal completion.
|
|
||||||
asyncResult.IOCompleted(statusCode, bytesSent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last write, cache it for special cancellation handling.
|
|
||||||
if ((flags & UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA) == 0)
|
|
||||||
{
|
|
||||||
_lastWrite = asyncResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
return asyncResult;
|
|
||||||
}
|
}
|
||||||
#if DNXCORE50
|
#if DNXCORE50
|
||||||
public void EndWrite(IAsyncResult asyncResult)
|
public void EndWrite(IAsyncResult asyncResult)
|
||||||
|
|
@ -480,143 +486,58 @@ namespace Microsoft.Net.Http.Server
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException("asyncResult");
|
throw new ArgumentNullException("asyncResult");
|
||||||
}
|
}
|
||||||
ResponseStreamAsyncResult castedAsyncResult = asyncResult as ResponseStreamAsyncResult;
|
((Task)asyncResult).GetAwaiter().GetResult();
|
||||||
if (castedAsyncResult == null || castedAsyncResult.ResponseStream != this)
|
|
||||||
{
|
|
||||||
throw new ArgumentException(Resources.Exception_WrongIAsyncResult, "asyncResult");
|
|
||||||
}
|
|
||||||
if (castedAsyncResult.EndCalled)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(Resources.Exception_EndCalledMultipleTimes);
|
|
||||||
}
|
|
||||||
castedAsyncResult.EndCalled = true;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// wait & then check for errors
|
|
||||||
// TODO: Graceful re-throw
|
|
||||||
castedAsyncResult.Task.Wait();
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
LogHelper.LogException(_requestContext.Logger, "EndWrite", exception);
|
|
||||||
Abort();
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override unsafe Task WriteAsync(byte[] buffer, int offset, int size, CancellationToken cancellationToken)
|
public override unsafe Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (buffer == null)
|
// Validates for null and bounds. Allows count == 0.
|
||||||
{
|
var data = new ArraySegment<byte>(buffer, offset, count);
|
||||||
throw new ArgumentNullException("buffer");
|
|
||||||
}
|
|
||||||
if (offset < 0 || offset > buffer.Length)
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException("offset");
|
|
||||||
}
|
|
||||||
if (size < 0 || size > buffer.Length - offset)
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException("size");
|
|
||||||
}
|
|
||||||
if (_closed)
|
|
||||||
{
|
|
||||||
throw new ObjectDisposedException(GetType().FullName);
|
|
||||||
}
|
|
||||||
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = ComputeLeftToWrite();
|
|
||||||
if (size == 0 && _leftToWrite != 0)
|
|
||||||
{
|
|
||||||
return Helpers.CompletedTask();
|
|
||||||
}
|
|
||||||
if (_leftToWrite >= 0 && size > _leftToWrite)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(Resources.Exception_TooMuchWritten);
|
|
||||||
}
|
|
||||||
// TODO: Verbose log
|
|
||||||
|
|
||||||
if (cancellationToken.IsCancellationRequested)
|
if (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
return Helpers.CanceledTask<int>();
|
return Helpers.CanceledTask<int>();
|
||||||
}
|
}
|
||||||
|
CheckDisposed();
|
||||||
|
// TODO: Verbose log parameters
|
||||||
|
// Officially starts the response and fires OnSendingHeaders
|
||||||
|
_requestContext.Response.Start();
|
||||||
|
|
||||||
var cancellationRegistration = default(CancellationTokenRegistration);
|
var currentBytes = _buffer.TotalBytes + data.Count;
|
||||||
if (cancellationToken.CanBeCanceled)
|
var contentLength = _requestContext.Response.ContentLength;
|
||||||
|
if (contentLength.HasValue && !_requestContext.Response.ComputedHeaders && contentLength.Value <= currentBytes)
|
||||||
{
|
{
|
||||||
cancellationRegistration = cancellationToken.Register(RequestContext.AbortDelegate, _requestContext);
|
if (contentLength.Value < currentBytes)
|
||||||
}
|
|
||||||
|
|
||||||
uint statusCode;
|
|
||||||
uint bytesSent = 0;
|
|
||||||
flags |= _leftToWrite == size ? UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE : UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA;
|
|
||||||
bool sentHeaders = _requestContext.Response.HeadersSent;
|
|
||||||
ResponseStreamAsyncResult asyncResult = new ResponseStreamAsyncResult(this, null, null, buffer, offset, size, _requestContext.Response.BoundaryType == BoundaryType.Chunked, sentHeaders, cancellationRegistration);
|
|
||||||
|
|
||||||
// Update m_LeftToWrite now so we can queue up additional BeginWrite's without waiting for EndWrite.
|
|
||||||
UpdateWritenCount((uint)((_requestContext.Response.BoundaryType == BoundaryType.Chunked) ? 0 : size));
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!sentHeaders)
|
|
||||||
{
|
{
|
||||||
statusCode = _requestContext.Response.SendHeaders(null, asyncResult, flags, false);
|
throw new InvalidOperationException("More bytes written than specified in the Content-Length header.");
|
||||||
bytesSent = asyncResult.BytesSent;
|
|
||||||
}
|
}
|
||||||
else
|
// The last write in a response that hasn't started yet, flush immideately
|
||||||
|
_buffer.Add(data);
|
||||||
|
return FlushAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
// The last write in a response that has already started, flush immidately
|
||||||
|
else if (_requestContext.Response.ComputedHeaders && _leftToWrite > 0 && _leftToWrite <= currentBytes)
|
||||||
|
{
|
||||||
|
if (_leftToWrite < currentBytes)
|
||||||
{
|
{
|
||||||
statusCode =
|
throw new InvalidOperationException("More bytes written than specified in the Content-Length header.");
|
||||||
UnsafeNclNativeMethods.HttpApi.HttpSendResponseEntityBody(
|
|
||||||
_requestContext.RequestQueueHandle,
|
|
||||||
_requestContext.RequestId,
|
|
||||||
(uint)flags,
|
|
||||||
asyncResult.DataChunkCount,
|
|
||||||
asyncResult.DataChunks,
|
|
||||||
&bytesSent,
|
|
||||||
SafeLocalFree.Zero,
|
|
||||||
0,
|
|
||||||
asyncResult.NativeOverlapped,
|
|
||||||
IntPtr.Zero);
|
|
||||||
}
|
}
|
||||||
|
_buffer.Add(data);
|
||||||
|
return FlushAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
else if (_requestContext.Response.ShouldBuffer && currentBytes < MaxBufferSize)
|
||||||
{
|
{
|
||||||
LogHelper.LogException(_requestContext.Logger, "WriteAsync", e);
|
_buffer.CopyAndAdd(data);
|
||||||
asyncResult.Dispose();
|
return Helpers.CompletedTask();
|
||||||
Abort();
|
|
||||||
throw;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
|
|
||||||
{
|
{
|
||||||
asyncResult.Dispose();
|
// Append to existing data without a copy, and then flush immidately
|
||||||
if (_requestContext.Server.IgnoreWriteExceptions && sentHeaders)
|
_buffer.Add(data);
|
||||||
{
|
return FlushAsync(cancellationToken);
|
||||||
asyncResult.Complete();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Exception exception = new IOException(string.Empty, new WebListenerException((int)statusCode));
|
|
||||||
LogHelper.LogException(_requestContext.Logger, "WriteAsync", exception);
|
|
||||||
Abort();
|
|
||||||
throw exception;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && WebListener.SkipIOCPCallbackOnSuccess)
|
|
||||||
{
|
|
||||||
// IO operation completed synchronously - callback won't be called to signal completion.
|
|
||||||
asyncResult.IOCompleted(statusCode, bytesSent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last write, cache it for special cancellation handling.
|
|
||||||
if ((flags & UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA) == 0)
|
|
||||||
{
|
|
||||||
_lastWrite = asyncResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
return asyncResult.Task;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal unsafe Task SendFileAsync(string fileName, long offset, long? size, CancellationToken cancellationToken)
|
internal async Task SendFileAsync(string fileName, long offset, long? count, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// It's too expensive to validate the file attributes before opening the file. Open the file and then check the lengths.
|
// It's too expensive to validate the file attributes before opening the file. Open the file and then check the lengths.
|
||||||
// This all happens inside of ResponseStreamAsyncResult.
|
// This all happens inside of ResponseStreamAsyncResult.
|
||||||
|
|
@ -624,17 +545,26 @@ namespace Microsoft.Net.Http.Server
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException("fileName");
|
throw new ArgumentNullException("fileName");
|
||||||
}
|
}
|
||||||
if (_closed)
|
CheckDisposed();
|
||||||
|
if (_buffer.TotalBytes > 0)
|
||||||
{
|
{
|
||||||
throw new ObjectDisposedException(GetType().FullName);
|
// SendFileAsync is primarly used for full responses so we don't optimize this partialy buffered scenario.
|
||||||
|
// In theory we could merge SendFileAsyncCore into FlushAsync[Internal] and send the buffered data in the same call as the file.
|
||||||
|
await FlushAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
// We can't mix await and unsafe so seperate the unsafe code into another method.
|
||||||
|
await SendFileAsyncCore(fileName, offset, count, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal unsafe Task SendFileAsyncCore(string fileName, long offset, long? count, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_requestContext.Response.Start();
|
||||||
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = ComputeLeftToWrite();
|
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = ComputeLeftToWrite();
|
||||||
if (size == 0 && _leftToWrite != 0)
|
if (count == 0 && _leftToWrite != 0)
|
||||||
{
|
{
|
||||||
return Helpers.CompletedTask();
|
return Helpers.CompletedTask();
|
||||||
}
|
}
|
||||||
if (_leftToWrite >= 0 && size > _leftToWrite)
|
if (_leftToWrite >= 0 && count > _leftToWrite)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(Resources.Exception_TooMuchWritten);
|
throw new InvalidOperationException(Resources.Exception_TooMuchWritten);
|
||||||
}
|
}
|
||||||
|
|
@ -653,30 +583,30 @@ namespace Microsoft.Net.Http.Server
|
||||||
|
|
||||||
uint statusCode;
|
uint statusCode;
|
||||||
uint bytesSent = 0;
|
uint bytesSent = 0;
|
||||||
flags |= _leftToWrite == size ? UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE : UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA;
|
flags |= _leftToWrite == count ? HttpApi.HTTP_FLAGS.NONE : HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA;
|
||||||
bool sentHeaders = _requestContext.Response.HeadersSent;
|
bool startedSending = _requestContext.Response.HasStartedSending;
|
||||||
ResponseStreamAsyncResult asyncResult = new ResponseStreamAsyncResult(this, null, null, fileName, offset, size,
|
var chunked = _requestContext.Response.BoundaryType == BoundaryType.Chunked;
|
||||||
_requestContext.Response.BoundaryType == BoundaryType.Chunked, sentHeaders, cancellationRegistration);
|
ResponseStreamAsyncResult asyncResult = new ResponseStreamAsyncResult(this, fileName, offset, count, chunked, cancellationRegistration);
|
||||||
|
|
||||||
long bytesWritten;
|
long bytesWritten;
|
||||||
if (_requestContext.Response.BoundaryType == BoundaryType.Chunked)
|
if (chunked)
|
||||||
{
|
{
|
||||||
bytesWritten = 0;
|
bytesWritten = 0;
|
||||||
}
|
}
|
||||||
else if (size.HasValue)
|
else if (count.HasValue)
|
||||||
{
|
{
|
||||||
bytesWritten = size.Value;
|
bytesWritten = count.Value;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
bytesWritten = asyncResult.FileLength - offset;
|
bytesWritten = asyncResult.FileLength - offset;
|
||||||
}
|
}
|
||||||
// Update m_LeftToWrite now so we can queue up additional calls to SendFileAsync.
|
// Update _leftToWrite now so we can queue up additional calls to SendFileAsync.
|
||||||
UpdateWritenCount((uint)bytesWritten);
|
UpdateWritenCount((uint)bytesWritten);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!sentHeaders)
|
if (!startedSending)
|
||||||
{
|
{
|
||||||
statusCode = _requestContext.Response.SendHeaders(null, asyncResult, flags, false);
|
statusCode = _requestContext.Response.SendHeaders(null, asyncResult, flags, false);
|
||||||
bytesSent = asyncResult.BytesSent;
|
bytesSent = asyncResult.BytesSent;
|
||||||
|
|
@ -684,8 +614,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// TODO: If opaque then include the buffer data flag.
|
// TODO: If opaque then include the buffer data flag.
|
||||||
statusCode =
|
statusCode = HttpApi.HttpSendResponseEntityBody(
|
||||||
UnsafeNclNativeMethods.HttpApi.HttpSendResponseEntityBody(
|
|
||||||
_requestContext.RequestQueueHandle,
|
_requestContext.RequestQueueHandle,
|
||||||
_requestContext.RequestId,
|
_requestContext.RequestId,
|
||||||
(uint)flags,
|
(uint)flags,
|
||||||
|
|
@ -709,7 +638,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
|
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
|
||||||
{
|
{
|
||||||
asyncResult.Dispose();
|
asyncResult.Dispose();
|
||||||
if (_requestContext.Server.IgnoreWriteExceptions && sentHeaders)
|
if (_requestContext.Server.IgnoreWriteExceptions && startedSending)
|
||||||
{
|
{
|
||||||
asyncResult.Complete();
|
asyncResult.Complete();
|
||||||
}
|
}
|
||||||
|
|
@ -722,14 +651,14 @@ namespace Microsoft.Net.Http.Server
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && WebListener.SkipIOCPCallbackOnSuccess)
|
if (statusCode == ErrorCodes.ERROR_SUCCESS && WebListener.SkipIOCPCallbackOnSuccess)
|
||||||
{
|
{
|
||||||
// IO operation completed synchronously - callback won't be called to signal completion.
|
// IO operation completed synchronously - callback won't be called to signal completion.
|
||||||
asyncResult.IOCompleted(statusCode, bytesSent);
|
asyncResult.IOCompleted(statusCode, bytesSent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Last write, cache it for special cancellation handling.
|
// Last write, cache it for special cancellation handling.
|
||||||
if ((flags & UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA) == 0)
|
if ((flags & HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_MORE_DATA) == 0)
|
||||||
{
|
{
|
||||||
_lastWrite = asyncResult;
|
_lastWrite = asyncResult;
|
||||||
}
|
}
|
||||||
|
|
@ -765,85 +694,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_closed = true;
|
_closed = true;
|
||||||
UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS flags = ComputeLeftToWrite(endOfRequest: true);
|
FlushInternal(endOfRequest: true);
|
||||||
if (_leftToWrite > 0 && !_inOpaqueMode)
|
|
||||||
{
|
|
||||||
_requestContext.Abort();
|
|
||||||
// This is logged rather than thrown because it is too late for an exception to be visible in user code.
|
|
||||||
LogHelper.LogError(_requestContext.Logger, "ResponseStream::Dispose", "Fewer bytes were written than were specified in the Content-Length.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
bool sentHeaders = _requestContext.Response.HeadersSent;
|
|
||||||
if (sentHeaders && _leftToWrite == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint statusCode = 0;
|
|
||||||
if ((_requestContext.Response.BoundaryType == BoundaryType.Chunked
|
|
||||||
|| _requestContext.Response.BoundaryType == BoundaryType.Close
|
|
||||||
|| _requestContext.Response.BoundaryType == BoundaryType.PassThrough)
|
|
||||||
&& !_requestContext.Request.IsHeadMethod)
|
|
||||||
{
|
|
||||||
if (_requestContext.Response.BoundaryType == BoundaryType.Close)
|
|
||||||
{
|
|
||||||
flags |= UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_DISCONNECT;
|
|
||||||
}
|
|
||||||
fixed (void* pBuffer = ChunkTerminator)
|
|
||||||
{
|
|
||||||
UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK* pDataChunk = null;
|
|
||||||
if (_requestContext.Response.BoundaryType == BoundaryType.Chunked)
|
|
||||||
{
|
|
||||||
UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK dataChunk = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK();
|
|
||||||
dataChunk.DataChunkType = UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
|
|
||||||
dataChunk.fromMemory.pBuffer = (IntPtr)pBuffer;
|
|
||||||
dataChunk.fromMemory.BufferLength = (uint)ChunkTerminator.Length;
|
|
||||||
pDataChunk = &dataChunk;
|
|
||||||
}
|
|
||||||
if (!sentHeaders)
|
|
||||||
{
|
|
||||||
statusCode = _requestContext.Response.SendHeaders(pDataChunk, null, flags, false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
statusCode =
|
|
||||||
UnsafeNclNativeMethods.HttpApi.HttpSendResponseEntityBody(
|
|
||||||
_requestContext.RequestQueueHandle,
|
|
||||||
_requestContext.RequestId,
|
|
||||||
(uint)flags,
|
|
||||||
pDataChunk != null ? (ushort)1 : (ushort)0,
|
|
||||||
pDataChunk,
|
|
||||||
null,
|
|
||||||
SafeLocalFree.Zero,
|
|
||||||
0,
|
|
||||||
SafeNativeOverlapped.Zero,
|
|
||||||
IntPtr.Zero);
|
|
||||||
|
|
||||||
if (_requestContext.Server.IgnoreWriteExceptions)
|
|
||||||
{
|
|
||||||
statusCode = UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!sentHeaders)
|
|
||||||
{
|
|
||||||
statusCode = _requestContext.Response.SendHeaders(null, null, flags, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_HANDLE_EOF
|
|
||||||
// Don't throw for disconnects, we were already finished with the response.
|
|
||||||
&& statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_CONNECTION_INVALID
|
|
||||||
&& statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_INVALID_PARAMETER)
|
|
||||||
{
|
|
||||||
Exception exception = new IOException(string.Empty, new WebListenerException((int)statusCode));
|
|
||||||
LogHelper.LogException(_requestContext.Logger, "Dispose", exception);
|
|
||||||
_requestContext.Abort();
|
|
||||||
throw exception;
|
|
||||||
}
|
|
||||||
_leftToWrite = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
@ -870,5 +721,13 @@ namespace Microsoft.Net.Http.Server
|
||||||
UnsafeNclNativeMethods.CancelIoEx(requestQueueHandle, asyncState.NativeOverlapped);
|
UnsafeNclNativeMethods.CancelIoEx(requestQueueHandle, asyncState.NativeOverlapped);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CheckDisposed()
|
||||||
|
{
|
||||||
|
if (_closed)
|
||||||
|
{
|
||||||
|
throw new ObjectDisposedException(GetType().FullName);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,117 +27,97 @@ using System.IO;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using static Microsoft.Net.Http.Server.UnsafeNclNativeMethods;
|
||||||
|
|
||||||
namespace Microsoft.Net.Http.Server
|
namespace Microsoft.Net.Http.Server
|
||||||
{
|
{
|
||||||
internal unsafe class ResponseStreamAsyncResult : IAsyncResult, IDisposable
|
internal unsafe class ResponseStreamAsyncResult : IAsyncResult, IDisposable
|
||||||
{
|
{
|
||||||
private static readonly byte[] CRLF = new byte[] { (byte)'\r', (byte)'\n' };
|
|
||||||
private static readonly IOCompletionCallback IOCallback = new IOCompletionCallback(Callback);
|
private static readonly IOCompletionCallback IOCallback = new IOCompletionCallback(Callback);
|
||||||
|
|
||||||
private SafeNativeOverlapped _overlapped;
|
private SafeNativeOverlapped _overlapped;
|
||||||
private UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK[] _dataChunks;
|
private HttpApi.HTTP_DATA_CHUNK[] _dataChunks;
|
||||||
private bool _sentHeaders;
|
|
||||||
private FileStream _fileStream;
|
private FileStream _fileStream;
|
||||||
private ResponseStream _responseStream;
|
private ResponseStream _responseStream;
|
||||||
private TaskCompletionSource<object> _tcs;
|
private TaskCompletionSource<object> _tcs;
|
||||||
private AsyncCallback _callback;
|
|
||||||
private uint _bytesSent;
|
private uint _bytesSent;
|
||||||
private CancellationTokenRegistration _cancellationRegistration;
|
private CancellationTokenRegistration _cancellationRegistration;
|
||||||
|
|
||||||
internal ResponseStreamAsyncResult(ResponseStream responseStream, object userState, AsyncCallback callback)
|
internal ResponseStreamAsyncResult(ResponseStream responseStream, CancellationTokenRegistration cancellationRegistration)
|
||||||
{
|
{
|
||||||
_responseStream = responseStream;
|
_responseStream = responseStream;
|
||||||
_tcs = new TaskCompletionSource<object>(userState);
|
_tcs = new TaskCompletionSource<object>();
|
||||||
_callback = callback;
|
|
||||||
}
|
|
||||||
internal ResponseStreamAsyncResult(ResponseStream responseStream, object userState, AsyncCallback callback,
|
|
||||||
byte[] buffer, int offset, int size, bool chunked, bool sentHeaders)
|
|
||||||
: this(responseStream, userState, callback, buffer, offset, size, chunked, sentHeaders,
|
|
||||||
new CancellationTokenRegistration())
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
internal ResponseStreamAsyncResult(ResponseStream responseStream, object userState, AsyncCallback callback,
|
|
||||||
byte[] buffer, int offset, int size, bool chunked, bool sentHeaders,
|
|
||||||
CancellationTokenRegistration cancellationRegistration)
|
|
||||||
: this(responseStream, userState, callback)
|
|
||||||
{
|
|
||||||
_sentHeaders = sentHeaders;
|
|
||||||
_cancellationRegistration = cancellationRegistration;
|
_cancellationRegistration = cancellationRegistration;
|
||||||
var boundHandle = _responseStream.RequestContext.Server.BoundHandle;
|
}
|
||||||
|
|
||||||
if (size == 0)
|
internal ResponseStreamAsyncResult(ResponseStream responseStream, BufferBuilder buffer, bool chunked,
|
||||||
|
CancellationTokenRegistration cancellationRegistration)
|
||||||
|
: this(responseStream, cancellationRegistration)
|
||||||
|
{
|
||||||
|
var boundHandle = _responseStream.RequestContext.Server.BoundHandle;
|
||||||
|
object[] objectsToPin;
|
||||||
|
|
||||||
|
if (buffer.TotalBytes == 0)
|
||||||
{
|
{
|
||||||
_dataChunks = null;
|
_dataChunks = null;
|
||||||
_overlapped = new SafeNativeOverlapped(boundHandle,
|
_overlapped = new SafeNativeOverlapped(boundHandle,
|
||||||
boundHandle.AllocateNativeOverlapped(IOCallback, this, null));
|
boundHandle.AllocateNativeOverlapped(IOCallback, this, null));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
_dataChunks = new HttpApi.HTTP_DATA_CHUNK[buffer.BufferCount + (chunked ? 2 : 0)];
|
||||||
|
objectsToPin = new object[_dataChunks.Length + 1];
|
||||||
|
objectsToPin[0] = _dataChunks;
|
||||||
|
var currentChunk = 0;
|
||||||
|
var currentPin = 1;
|
||||||
|
|
||||||
|
var chunkHeaderBuffer = new ArraySegment<byte>();
|
||||||
|
if (chunked)
|
||||||
{
|
{
|
||||||
_dataChunks = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK[chunked ? 3 : 1];
|
chunkHeaderBuffer = Helpers.GetChunkHeader(buffer.TotalBytes);
|
||||||
|
SetDataChunk(_dataChunks, ref currentChunk, objectsToPin, ref currentPin, chunkHeaderBuffer);
|
||||||
object[] objectsToPin = new object[1 + _dataChunks.Length];
|
|
||||||
objectsToPin[_dataChunks.Length] = _dataChunks;
|
|
||||||
|
|
||||||
int chunkHeaderOffset = 0;
|
|
||||||
byte[] chunkHeaderBuffer = null;
|
|
||||||
if (chunked)
|
|
||||||
{
|
|
||||||
chunkHeaderBuffer = GetChunkHeader(size, out chunkHeaderOffset);
|
|
||||||
|
|
||||||
_dataChunks[0] = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK();
|
|
||||||
_dataChunks[0].DataChunkType = UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
|
|
||||||
_dataChunks[0].fromMemory.BufferLength = (uint)(chunkHeaderBuffer.Length - chunkHeaderOffset);
|
|
||||||
|
|
||||||
objectsToPin[0] = chunkHeaderBuffer;
|
|
||||||
|
|
||||||
_dataChunks[1] = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK();
|
|
||||||
_dataChunks[1].DataChunkType = UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
|
|
||||||
_dataChunks[1].fromMemory.BufferLength = (uint)size;
|
|
||||||
|
|
||||||
objectsToPin[1] = buffer;
|
|
||||||
|
|
||||||
_dataChunks[2] = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK();
|
|
||||||
_dataChunks[2].DataChunkType = UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
|
|
||||||
_dataChunks[2].fromMemory.BufferLength = (uint)CRLF.Length;
|
|
||||||
|
|
||||||
objectsToPin[2] = CRLF;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_dataChunks[0] = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK();
|
|
||||||
_dataChunks[0].DataChunkType = UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
|
|
||||||
_dataChunks[0].fromMemory.BufferLength = (uint)size;
|
|
||||||
|
|
||||||
objectsToPin[0] = buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This call will pin needed memory
|
|
||||||
_overlapped = new SafeNativeOverlapped(boundHandle,
|
|
||||||
boundHandle.AllocateNativeOverlapped(IOCallback, this, objectsToPin));
|
|
||||||
|
|
||||||
if (chunked)
|
|
||||||
{
|
|
||||||
_dataChunks[0].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(chunkHeaderBuffer, chunkHeaderOffset);
|
|
||||||
_dataChunks[1].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, offset);
|
|
||||||
_dataChunks[2].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(CRLF, 0);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_dataChunks[0].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, offset);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var segment in buffer.Buffers)
|
||||||
|
{
|
||||||
|
SetDataChunk(_dataChunks, ref currentChunk, objectsToPin, ref currentPin, segment);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunked)
|
||||||
|
{
|
||||||
|
SetDataChunk(_dataChunks, ref currentChunk, objectsToPin, ref currentPin, new ArraySegment<byte>(Helpers.CRLF));
|
||||||
|
}
|
||||||
|
|
||||||
|
// This call will pin needed memory
|
||||||
|
_overlapped = new SafeNativeOverlapped(boundHandle,
|
||||||
|
boundHandle.AllocateNativeOverlapped(IOCallback, this, objectsToPin));
|
||||||
|
|
||||||
|
currentChunk = 0;
|
||||||
|
if (chunked)
|
||||||
|
{
|
||||||
|
_dataChunks[currentChunk].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(chunkHeaderBuffer.Array, chunkHeaderBuffer.Offset);
|
||||||
|
currentChunk++;
|
||||||
|
}
|
||||||
|
foreach (var segment in buffer.Buffers)
|
||||||
|
{
|
||||||
|
_dataChunks[currentChunk].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(segment.Array, segment.Offset);
|
||||||
|
currentChunk++;
|
||||||
|
}
|
||||||
|
if (chunked)
|
||||||
|
{
|
||||||
|
_dataChunks[currentChunk].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(Helpers.CRLF, 0);
|
||||||
|
currentChunk++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We've captured a reference to all the buffers, clear the buffer so that it can be used to queue overlapped writes.
|
||||||
|
buffer.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal ResponseStreamAsyncResult(ResponseStream responseStream, object userState, AsyncCallback callback,
|
internal ResponseStreamAsyncResult(ResponseStream responseStream, string fileName, long offset,
|
||||||
string fileName, long offset, long? size, bool chunked, bool sentHeaders,
|
long? count, bool chunked, CancellationTokenRegistration cancellationRegistration)
|
||||||
CancellationTokenRegistration cancellationRegistration)
|
: this(responseStream, cancellationRegistration)
|
||||||
: this(responseStream, userState, callback)
|
|
||||||
{
|
{
|
||||||
_sentHeaders = sentHeaders;
|
var boundHandle = responseStream.RequestContext.Server.BoundHandle;
|
||||||
_cancellationRegistration = cancellationRegistration;
|
|
||||||
var boundHandle = ResponseStream.RequestContext.Server.BoundHandle;
|
|
||||||
|
|
||||||
int bufferSize = 1024 * 64; // TODO: Validate buffer size choice.
|
int bufferSize = 1024 * 64; // TODO: Validate buffer size choice.
|
||||||
#if DNXCORE50
|
#if DNXCORE50
|
||||||
|
|
@ -153,13 +133,13 @@ namespace Microsoft.Net.Http.Server
|
||||||
_fileStream.Dispose();
|
_fileStream.Dispose();
|
||||||
throw new ArgumentOutOfRangeException("offset", offset, string.Empty);
|
throw new ArgumentOutOfRangeException("offset", offset, string.Empty);
|
||||||
}
|
}
|
||||||
if (size.HasValue && (size < 0 || size > length - offset))
|
if (count.HasValue && (count < 0 || count > length - offset))
|
||||||
{
|
{
|
||||||
_fileStream.Dispose();
|
_fileStream.Dispose();
|
||||||
throw new ArgumentOutOfRangeException("size", size, string.Empty);
|
throw new ArgumentOutOfRangeException("count", count, string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (size == 0 || (!size.HasValue && _fileStream.Length == 0))
|
if (count == 0 || (!count.HasValue && _fileStream.Length == 0))
|
||||||
{
|
{
|
||||||
_dataChunks = null;
|
_dataChunks = null;
|
||||||
_overlapped = new SafeNativeOverlapped(boundHandle,
|
_overlapped = new SafeNativeOverlapped(boundHandle,
|
||||||
|
|
@ -167,42 +147,34 @@ namespace Microsoft.Net.Http.Server
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_dataChunks = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK[chunked ? 3 : 1];
|
_dataChunks = new HttpApi.HTTP_DATA_CHUNK[chunked ? 3 : 1];
|
||||||
|
|
||||||
object[] objectsToPin = new object[_dataChunks.Length];
|
object[] objectsToPin = new object[_dataChunks.Length];
|
||||||
objectsToPin[_dataChunks.Length - 1] = _dataChunks;
|
objectsToPin[_dataChunks.Length - 1] = _dataChunks;
|
||||||
|
|
||||||
int chunkHeaderOffset = 0;
|
var chunkHeaderBuffer = new ArraySegment<byte>();
|
||||||
byte[] chunkHeaderBuffer = null;
|
|
||||||
if (chunked)
|
if (chunked)
|
||||||
{
|
{
|
||||||
chunkHeaderBuffer = GetChunkHeader((int)(size ?? _fileStream.Length - offset), out chunkHeaderOffset);
|
chunkHeaderBuffer = Helpers.GetChunkHeader((int)(count ?? _fileStream.Length - offset));
|
||||||
|
_dataChunks[0].DataChunkType = HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
|
||||||
|
_dataChunks[0].fromMemory.BufferLength = (uint)chunkHeaderBuffer.Count;
|
||||||
|
objectsToPin[0] = chunkHeaderBuffer.Array;
|
||||||
|
|
||||||
_dataChunks[0] = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK();
|
_dataChunks[1].DataChunkType = HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromFileHandle;
|
||||||
_dataChunks[0].DataChunkType = UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
|
|
||||||
_dataChunks[0].fromMemory.BufferLength = (uint)(chunkHeaderBuffer.Length - chunkHeaderOffset);
|
|
||||||
|
|
||||||
objectsToPin[0] = chunkHeaderBuffer;
|
|
||||||
|
|
||||||
_dataChunks[1] = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK();
|
|
||||||
_dataChunks[1].DataChunkType = UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromFileHandle;
|
|
||||||
_dataChunks[1].fromFile.offset = (ulong)offset;
|
_dataChunks[1].fromFile.offset = (ulong)offset;
|
||||||
_dataChunks[1].fromFile.count = (ulong)(size ?? -1);
|
_dataChunks[1].fromFile.count = (ulong)(count ?? -1);
|
||||||
_dataChunks[1].fromFile.fileHandle = _fileStream.SafeFileHandle.DangerousGetHandle();
|
_dataChunks[1].fromFile.fileHandle = _fileStream.SafeFileHandle.DangerousGetHandle();
|
||||||
// Nothing to pin for the file handle.
|
// Nothing to pin for the file handle.
|
||||||
|
|
||||||
_dataChunks[2] = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK();
|
_dataChunks[2].DataChunkType = HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
|
||||||
_dataChunks[2].DataChunkType = UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
|
_dataChunks[2].fromMemory.BufferLength = (uint)Helpers.CRLF.Length;
|
||||||
_dataChunks[2].fromMemory.BufferLength = (uint)CRLF.Length;
|
objectsToPin[1] = Helpers.CRLF;
|
||||||
|
|
||||||
objectsToPin[1] = CRLF;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_dataChunks[0] = new UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK();
|
_dataChunks[0].DataChunkType = HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromFileHandle;
|
||||||
_dataChunks[0].DataChunkType = UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromFileHandle;
|
|
||||||
_dataChunks[0].fromFile.offset = (ulong)offset;
|
_dataChunks[0].fromFile.offset = (ulong)offset;
|
||||||
_dataChunks[0].fromFile.count = (ulong)(size ?? -1);
|
_dataChunks[0].fromFile.count = (ulong)(count ?? -1);
|
||||||
_dataChunks[0].fromFile.fileHandle = _fileStream.SafeFileHandle.DangerousGetHandle();
|
_dataChunks[0].fromFile.fileHandle = _fileStream.SafeFileHandle.DangerousGetHandle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -212,15 +184,21 @@ namespace Microsoft.Net.Http.Server
|
||||||
|
|
||||||
if (chunked)
|
if (chunked)
|
||||||
{
|
{
|
||||||
_dataChunks[0].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(chunkHeaderBuffer, chunkHeaderOffset);
|
// These must be set after pinning with Overlapped.
|
||||||
_dataChunks[2].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(CRLF, 0);
|
_dataChunks[0].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(chunkHeaderBuffer.Array, chunkHeaderBuffer.Offset);
|
||||||
|
_dataChunks[2].fromMemory.pBuffer = Marshal.UnsafeAddrOfPinnedArrayElement(Helpers.CRLF, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal ResponseStream ResponseStream
|
private static void SetDataChunk(HttpApi.HTTP_DATA_CHUNK[] chunks, ref int chunkIndex, object[] objectsToPin, ref int pinIndex, ArraySegment<byte> segment)
|
||||||
{
|
{
|
||||||
get { return _responseStream; }
|
objectsToPin[pinIndex] = segment.Array;
|
||||||
|
pinIndex++;
|
||||||
|
chunks[chunkIndex].DataChunkType = HttpApi.HTTP_DATA_CHUNK_TYPE.HttpDataChunkFromMemory;
|
||||||
|
// The address is not set until after we pin it with Overlapped
|
||||||
|
chunks[chunkIndex].fromMemory.BufferLength = (uint)segment.Count;
|
||||||
|
chunkIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal SafeNativeOverlapped NativeOverlapped
|
internal SafeNativeOverlapped NativeOverlapped
|
||||||
|
|
@ -254,7 +232,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK* DataChunks
|
internal HttpApi.HTTP_DATA_CHUNK* DataChunks
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
|
|
@ -264,7 +242,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return (UnsafeNclNativeMethods.HttpApi.HTTP_DATA_CHUNK*)(Marshal.UnsafeAddrOfPinnedArrayElement(_dataChunks, 0));
|
return (HttpApi.HTTP_DATA_CHUNK*)(Marshal.UnsafeAddrOfPinnedArrayElement(_dataChunks, 0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -326,111 +304,17 @@ namespace Microsoft.Net.Http.Server
|
||||||
|
|
||||||
internal void Complete()
|
internal void Complete()
|
||||||
{
|
{
|
||||||
if (_tcs.TrySetResult(null) && _callback != null)
|
_tcs.TrySetResult(null);
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_callback(this);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
// TODO: Exception handling? This may be an IO callback thread and throwing here could crash the app.
|
|
||||||
// TODO: Log
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Dispose();
|
Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void Fail(Exception ex)
|
internal void Fail(Exception ex)
|
||||||
{
|
{
|
||||||
if (_tcs.TrySetException(ex) && _callback != null)
|
_tcs.TrySetException(ex);
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_callback(this);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
// TODO: Exception handling? This may be an IO callback thread and throwing here could crash the app.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Dispose();
|
Dispose();
|
||||||
_responseStream.Abort();
|
_responseStream.Abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*++
|
|
||||||
|
|
||||||
GetChunkHeader
|
|
||||||
|
|
||||||
A private utility routine to convert an integer to a chunk header,
|
|
||||||
which is an ASCII hex number followed by a CRLF. The header is returned
|
|
||||||
as a byte array.
|
|
||||||
|
|
||||||
Input:
|
|
||||||
|
|
||||||
size - Chunk size to be encoded
|
|
||||||
offset - Out parameter where we store offset into buffer.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
A byte array with the header in int.
|
|
||||||
|
|
||||||
--*/
|
|
||||||
|
|
||||||
private static byte[] GetChunkHeader(int size, out int offset)
|
|
||||||
{
|
|
||||||
uint mask = 0xf0000000;
|
|
||||||
byte[] header = new byte[10];
|
|
||||||
int i;
|
|
||||||
offset = -1;
|
|
||||||
|
|
||||||
// Loop through the size, looking at each nibble. If it's not 0
|
|
||||||
// convert it to hex. Save the index of the first non-zero
|
|
||||||
// byte.
|
|
||||||
|
|
||||||
for (i = 0; i < 8; i++, size <<= 4)
|
|
||||||
{
|
|
||||||
// offset == -1 means that we haven't found a non-zero nibble
|
|
||||||
// yet. If we haven't found one, and the current one is zero,
|
|
||||||
// don't do anything.
|
|
||||||
|
|
||||||
if (offset == -1)
|
|
||||||
{
|
|
||||||
if ((size & mask) == 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Either we have a non-zero nibble or we're no longer skipping
|
|
||||||
// leading zeros. Convert this nibble to ASCII and save it.
|
|
||||||
|
|
||||||
uint temp = (uint)size >> 28;
|
|
||||||
|
|
||||||
if (temp < 10)
|
|
||||||
{
|
|
||||||
header[i] = (byte)(temp + '0');
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
header[i] = (byte)((temp - 10) + 'A');
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we haven't found a non-zero nibble yet, we've found one
|
|
||||||
// now, so remember that.
|
|
||||||
|
|
||||||
if (offset == -1)
|
|
||||||
{
|
|
||||||
offset = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
header[8] = (byte)'\r';
|
|
||||||
header[9] = (byte)'\n';
|
|
||||||
|
|
||||||
return header;
|
|
||||||
}
|
|
||||||
|
|
||||||
public object AsyncState
|
public object AsyncState
|
||||||
{
|
{
|
||||||
get { return _tcs.Task.AsyncState; }
|
get { return _tcs.Task.AsyncState; }
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,8 @@ namespace Microsoft.Net.Http.Server
|
||||||
// The native request queue
|
// The native request queue
|
||||||
private long? _requestQueueLength;
|
private long? _requestQueueLength;
|
||||||
|
|
||||||
|
private bool _bufferResponses = true;
|
||||||
|
|
||||||
public WebListener()
|
public WebListener()
|
||||||
: this(null)
|
: this(null)
|
||||||
{
|
{
|
||||||
|
|
@ -134,6 +136,12 @@ namespace Microsoft.Net.Http.Server
|
||||||
get { return _urlPrefixes; }
|
get { return _urlPrefixes; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool BufferResponses
|
||||||
|
{
|
||||||
|
get { return _bufferResponses; }
|
||||||
|
set { _bufferResponses = value; }
|
||||||
|
}
|
||||||
|
|
||||||
internal SafeHandle RequestQueueHandle
|
internal SafeHandle RequestQueueHandle
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,8 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<SchemaVersion>2.0</SchemaVersion>
|
<SchemaVersion>2.0</SchemaVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
|
||||||
|
</ItemGroup>
|
||||||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
|
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||||
</Project>
|
</Project>
|
||||||
|
|
@ -71,6 +71,7 @@ namespace Microsoft.AspNet.Server.WebListener
|
||||||
{
|
{
|
||||||
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
|
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
|
||||||
await httpContext.Response.WriteAsync("Hello World");
|
await httpContext.Response.WriteAsync("Hello World");
|
||||||
|
await httpContext.Response.Body.FlushAsync();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var opaqueFeature = httpContext.GetFeature<IHttpUpgradeFeature>();
|
var opaqueFeature = httpContext.GetFeature<IHttpUpgradeFeature>();
|
||||||
|
|
@ -171,8 +172,8 @@ namespace Microsoft.AspNet.Server.WebListener
|
||||||
using (Stream stream = await SendOpaqueRequestAsync(method, address, extraHeader))
|
using (Stream stream = await SendOpaqueRequestAsync(method, address, extraHeader))
|
||||||
{
|
{
|
||||||
byte[] data = new byte[100];
|
byte[] data = new byte[100];
|
||||||
stream.WriteAsync(data, 0, 49).Wait();
|
await stream.WriteAsync(data, 0, 49);
|
||||||
int read = stream.ReadAsync(data, 0, data.Length).Result;
|
int read = await stream.ReadAsync(data, 0, data.Length);
|
||||||
Assert.Equal(49, read);
|
Assert.Equal(49, read);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.FeatureModel;
|
using Microsoft.AspNet.FeatureModel;
|
||||||
|
|
@ -31,7 +32,7 @@ namespace Microsoft.AspNet.Server.WebListener
|
||||||
public class ResponseBodyTests
|
public class ResponseBodyTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ResponseBody_WriteNoHeaders_DefaultsToChunked()
|
public async Task ResponseBody_WriteNoHeaders_BuffersAndSetsContentLength()
|
||||||
{
|
{
|
||||||
string address;
|
string address;
|
||||||
using (Utilities.CreateHttpServer(out address, env =>
|
using (Utilities.CreateHttpServer(out address, env =>
|
||||||
|
|
@ -45,24 +46,22 @@ namespace Microsoft.AspNet.Server.WebListener
|
||||||
Assert.Equal(200, (int)response.StatusCode);
|
Assert.Equal(200, (int)response.StatusCode);
|
||||||
Assert.Equal(new Version(1, 1), response.Version);
|
Assert.Equal(new Version(1, 1), response.Version);
|
||||||
IEnumerable<string> ignored;
|
IEnumerable<string> ignored;
|
||||||
Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
|
Assert.True(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
|
||||||
Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
|
Assert.False(response.Headers.TransferEncodingChunked.HasValue, "Chunked");
|
||||||
Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync());
|
Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ResponseBody_WriteChunked_Chunked()
|
public async Task ResponseBody_WriteNoHeadersAndFlush_DefaultsToChunked()
|
||||||
{
|
{
|
||||||
string address;
|
string address;
|
||||||
using (Utilities.CreateHttpServer(out address, env =>
|
using (Utilities.CreateHttpServer(out address, async env =>
|
||||||
{
|
{
|
||||||
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
|
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
|
||||||
httpContext.Request.Headers["transfeR-Encoding"] = " CHunked ";
|
httpContext.Response.Body.Write(new byte[10], 0, 10);
|
||||||
Stream stream = httpContext.Response.Body;
|
await httpContext.Response.Body.WriteAsync(new byte[10], 0, 10);
|
||||||
stream.EndWrite(stream.BeginWrite(new byte[10], 0, 10, null, null));
|
await httpContext.Response.Body.FlushAsync();
|
||||||
stream.Write(new byte[10], 0, 10);
|
|
||||||
return stream.WriteAsync(new byte[10], 0, 10);
|
|
||||||
}))
|
}))
|
||||||
{
|
{
|
||||||
HttpResponseMessage response = await SendRequestAsync(address);
|
HttpResponseMessage response = await SendRequestAsync(address);
|
||||||
|
|
@ -71,7 +70,30 @@ namespace Microsoft.AspNet.Server.WebListener
|
||||||
IEnumerable<string> ignored;
|
IEnumerable<string> ignored;
|
||||||
Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
|
Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
|
||||||
Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
|
Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
|
||||||
Assert.Equal(new byte[30], await response.Content.ReadAsByteArrayAsync());
|
Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResponseBody_WriteChunked_ManuallyChunked()
|
||||||
|
{
|
||||||
|
string address;
|
||||||
|
using (Utilities.CreateHttpServer(out address, async env =>
|
||||||
|
{
|
||||||
|
var httpContext = new DefaultHttpContext((IFeatureCollection)env);
|
||||||
|
httpContext.Response.Headers["transfeR-Encoding"] = " CHunked ";
|
||||||
|
Stream stream = httpContext.Response.Body;
|
||||||
|
var responseBytes = Encoding.ASCII.GetBytes("10\r\nManually Chunked\r\n0\r\n\r\n");
|
||||||
|
await stream.WriteAsync(responseBytes, 0, responseBytes.Length);
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
HttpResponseMessage response = await SendRequestAsync(address);
|
||||||
|
Assert.Equal(200, (int)response.StatusCode);
|
||||||
|
Assert.Equal(new Version(1, 1), response.Version);
|
||||||
|
IEnumerable<string> ignored;
|
||||||
|
Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
|
||||||
|
Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
|
||||||
|
Assert.Equal("Manually Chunked", await response.Content.ReadAsStringAsync());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -132,7 +154,7 @@ namespace Microsoft.AspNet.Server.WebListener
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResponseBody_WriteContentLengthTooMuchWritten_Throws()
|
public async Task ResponseBody_WriteContentLengthTooMuchWritten_Throws()
|
||||||
{
|
{
|
||||||
string address;
|
string address;
|
||||||
using (Utilities.CreateHttpServer(out address, env =>
|
using (Utilities.CreateHttpServer(out address, env =>
|
||||||
|
|
@ -144,7 +166,8 @@ namespace Microsoft.AspNet.Server.WebListener
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}))
|
}))
|
||||||
{
|
{
|
||||||
Assert.Throws<AggregateException>(() => SendRequestAsync(address).Result);
|
var response = await SendRequestAsync(address);
|
||||||
|
Assert.Equal(500, (int)response.StatusCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,6 @@ namespace Microsoft.AspNet.Server.WebListener
|
||||||
{
|
{
|
||||||
HttpResponseMessage response = await SendRequestAsync(address);
|
HttpResponseMessage response = await SendRequestAsync(address);
|
||||||
Assert.Equal(200, (int)response.StatusCode);
|
Assert.Equal(200, (int)response.StatusCode);
|
||||||
Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
|
|
||||||
Assert.True(upgradeThrew.Value);
|
Assert.True(upgradeThrew.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ namespace Microsoft.Net.Http.Server
|
||||||
context.Dispose();
|
context.Dispose();
|
||||||
HttpResponseMessage response = await clientTask;
|
HttpResponseMessage response = await clientTask;
|
||||||
Assert.Equal(200, (int)response.StatusCode);
|
Assert.Equal(200, (int)response.StatusCode);
|
||||||
Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
|
|
||||||
Assert.Equal("Hello World", await response.Content.ReadAsStringAsync());
|
Assert.Equal("Hello World", await response.Content.ReadAsStringAsync());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
public class ResponseBodyTests
|
public class ResponseBodyTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ResponseBody_WriteNoHeaders_DefaultsToChunked()
|
public async Task ResponseBody_BufferWriteNoHeaders_DefaultsToContentLength()
|
||||||
{
|
{
|
||||||
string address;
|
string address;
|
||||||
using (var server = Utilities.CreateHttpServer(out address))
|
using (var server = Utilities.CreateHttpServer(out address))
|
||||||
|
|
@ -23,6 +23,31 @@ namespace Microsoft.Net.Http.Server
|
||||||
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
|
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
|
||||||
|
|
||||||
var context = await server.GetContextAsync();
|
var context = await server.GetContextAsync();
|
||||||
|
context.Response.ShouldBuffer = true;
|
||||||
|
context.Response.Body.Write(new byte[10], 0, 10);
|
||||||
|
await context.Response.Body.WriteAsync(new byte[10], 0, 10);
|
||||||
|
context.Dispose();
|
||||||
|
|
||||||
|
HttpResponseMessage response = await responseTask;
|
||||||
|
Assert.Equal(200, (int)response.StatusCode);
|
||||||
|
Assert.Equal(new Version(1, 1), response.Version);
|
||||||
|
IEnumerable<string> ignored;
|
||||||
|
Assert.True(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
|
||||||
|
Assert.False(response.Headers.TransferEncodingChunked.HasValue, "Chunked");
|
||||||
|
Assert.Equal(new byte[20], await response.Content.ReadAsByteArrayAsync());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResponseBody_NoBufferWriteNoHeaders_DefaultsToChunked()
|
||||||
|
{
|
||||||
|
string address;
|
||||||
|
using (var server = Utilities.CreateHttpServer(out address))
|
||||||
|
{
|
||||||
|
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
|
||||||
|
|
||||||
|
var context = await server.GetContextAsync();
|
||||||
|
context.Response.ShouldBuffer = false;
|
||||||
context.Response.Body.Write(new byte[10], 0, 10);
|
context.Response.Body.Write(new byte[10], 0, 10);
|
||||||
await context.Response.Body.WriteAsync(new byte[10], 0, 10);
|
await context.Response.Body.WriteAsync(new byte[10], 0, 10);
|
||||||
context.Dispose();
|
context.Dispose();
|
||||||
|
|
@ -37,6 +62,29 @@ namespace Microsoft.Net.Http.Server
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResponseBody_FlushThenBuffer_DefaultsToChunkedAndTerminates()
|
||||||
|
{
|
||||||
|
string address;
|
||||||
|
using (var server = Utilities.CreateHttpServer(out address))
|
||||||
|
{
|
||||||
|
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
|
||||||
|
|
||||||
|
var context = await server.GetContextAsync();
|
||||||
|
context.Response.Body.Write(new byte[10], 0, 10);
|
||||||
|
context.Response.Body.Flush();
|
||||||
|
await context.Response.Body.WriteAsync(new byte[10], 0, 10);
|
||||||
|
context.Dispose();
|
||||||
|
|
||||||
|
HttpResponseMessage response = await responseTask;
|
||||||
|
Assert.Equal(200, (int)response.StatusCode);
|
||||||
|
IEnumerable<string> contentLength;
|
||||||
|
Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
|
||||||
|
Assert.True(response.Headers.TransferEncodingChunked.HasValue);
|
||||||
|
Assert.Equal(20, (await response.Content.ReadAsByteArrayAsync()).Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ResponseBody_WriteChunked_ManuallyChunked()
|
public async Task ResponseBody_WriteChunked_ManuallyChunked()
|
||||||
{
|
{
|
||||||
|
|
@ -111,7 +159,7 @@ namespace Microsoft.Net.Http.Server
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ResponseBody_WriteContentLengthNotEnoughWritten_Throws()
|
public async Task ResponseBody_WriteContentLengthNotEnoughWritten_Aborts()
|
||||||
{
|
{
|
||||||
string address;
|
string address;
|
||||||
using (var server = Utilities.CreateHttpServer(out address))
|
using (var server = Utilities.CreateHttpServer(out address))
|
||||||
|
|
@ -123,6 +171,12 @@ namespace Microsoft.Net.Http.Server
|
||||||
context.Response.Body.Write(new byte[5], 0, 5);
|
context.Response.Body.Write(new byte[5], 0, 5);
|
||||||
context.Dispose();
|
context.Dispose();
|
||||||
|
|
||||||
|
// HttpClient retries the request because it didn't get a response.
|
||||||
|
context = await server.GetContextAsync();
|
||||||
|
context.Response.Headers["Content-lenGth"] = " 20 ";
|
||||||
|
context.Response.Body.Write(new byte[5], 0, 5);
|
||||||
|
context.Dispose();
|
||||||
|
|
||||||
await Assert.ThrowsAsync<HttpRequestException>(() => responseTask);
|
await Assert.ThrowsAsync<HttpRequestException>(() => responseTask);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -141,6 +195,13 @@ namespace Microsoft.Net.Http.Server
|
||||||
Assert.Throws<InvalidOperationException>(() => context.Response.Body.Write(new byte[6], 0, 6));
|
Assert.Throws<InvalidOperationException>(() => context.Response.Body.Write(new byte[6], 0, 6));
|
||||||
context.Dispose();
|
context.Dispose();
|
||||||
|
|
||||||
|
// HttpClient retries the request because it didn't get a response.
|
||||||
|
context = await server.GetContextAsync();
|
||||||
|
context.Response.Headers["Content-lenGth"] = " 10 ";
|
||||||
|
context.Response.Body.Write(new byte[5], 0, 5);
|
||||||
|
Assert.Throws<InvalidOperationException>(() => context.Response.Body.Write(new byte[6], 0, 6));
|
||||||
|
context.Dispose();
|
||||||
|
|
||||||
await Assert.ThrowsAsync<HttpRequestException>(() => responseTask);
|
await Assert.ThrowsAsync<HttpRequestException>(() => responseTask);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -170,6 +231,92 @@ namespace Microsoft.Net.Http.Server
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResponseBody_WriteZeroCount_StartsResponse()
|
||||||
|
{
|
||||||
|
string address;
|
||||||
|
using (var server = Utilities.CreateHttpServer(out address))
|
||||||
|
{
|
||||||
|
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
|
||||||
|
|
||||||
|
var context = await server.GetContextAsync();
|
||||||
|
context.Response.Body.Write(new byte[10], 0, 0);
|
||||||
|
Assert.True(context.Response.HasStarted);
|
||||||
|
await context.Response.Body.WriteAsync(new byte[10], 0, 0);
|
||||||
|
context.Dispose();
|
||||||
|
|
||||||
|
HttpResponseMessage response = await responseTask;
|
||||||
|
Assert.Equal(200, (int)response.StatusCode);
|
||||||
|
Assert.Equal(new Version(1, 1), response.Version);
|
||||||
|
IEnumerable<string> ignored;
|
||||||
|
Assert.True(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
|
||||||
|
Assert.False(response.Headers.TransferEncodingChunked.HasValue, "Chunked");
|
||||||
|
Assert.Equal(new byte[0], await response.Content.ReadAsByteArrayAsync());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResponseBody_WriteMoreThanBufferLimitBufferWithNoHeaders_DefaultsToChunkedAndFlushes()
|
||||||
|
{
|
||||||
|
string address;
|
||||||
|
using (var server = Utilities.CreateHttpServer(out address))
|
||||||
|
{
|
||||||
|
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
|
||||||
|
|
||||||
|
var context = await server.GetContextAsync();
|
||||||
|
context.Response.ShouldBuffer = true;
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
context.Response.Body.Write(new byte[1020], 0, 1020);
|
||||||
|
Assert.True(context.Response.HasStarted);
|
||||||
|
Assert.False(context.Response.HasStartedSending);
|
||||||
|
}
|
||||||
|
context.Response.Body.Write(new byte[1020], 0, 1020);
|
||||||
|
Assert.True(context.Response.HasStartedSending);
|
||||||
|
context.Response.Body.Write(new byte[1020], 0, 1020);
|
||||||
|
context.Dispose();
|
||||||
|
|
||||||
|
HttpResponseMessage response = await responseTask;
|
||||||
|
Assert.Equal(200, (int)response.StatusCode);
|
||||||
|
Assert.Equal(new Version(1, 1), response.Version);
|
||||||
|
IEnumerable<string> ignored;
|
||||||
|
Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
|
||||||
|
Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
|
||||||
|
Assert.Equal(new byte[1020*6], await response.Content.ReadAsByteArrayAsync());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResponseBody_WriteAsyncMoreThanBufferLimitBufferWithNoHeaders_DefaultsToChunkedAndFlushes()
|
||||||
|
{
|
||||||
|
string address;
|
||||||
|
using (var server = Utilities.CreateHttpServer(out address))
|
||||||
|
{
|
||||||
|
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
|
||||||
|
|
||||||
|
var context = await server.GetContextAsync();
|
||||||
|
context.Response.ShouldBuffer = true;
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
await context.Response.Body.WriteAsync(new byte[1020], 0, 1020);
|
||||||
|
Assert.True(context.Response.HasStarted);
|
||||||
|
Assert.False(context.Response.HasStartedSending);
|
||||||
|
}
|
||||||
|
await context.Response.Body.WriteAsync(new byte[1020], 0, 1020);
|
||||||
|
Assert.True(context.Response.HasStartedSending);
|
||||||
|
await context.Response.Body.WriteAsync(new byte[1020], 0, 1020);
|
||||||
|
context.Dispose();
|
||||||
|
|
||||||
|
HttpResponseMessage response = await responseTask;
|
||||||
|
Assert.Equal(200, (int)response.StatusCode);
|
||||||
|
Assert.Equal(new Version(1, 1), response.Version);
|
||||||
|
IEnumerable<string> ignored;
|
||||||
|
Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
|
||||||
|
Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
|
||||||
|
Assert.Equal(new byte[1020 * 6], await response.Content.ReadAsByteArrayAsync());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ResponseBody_WriteAsyncWithActiveCancellationToken_Success()
|
public async Task ResponseBody_WriteAsyncWithActiveCancellationToken_Success()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -349,13 +349,13 @@ namespace Microsoft.Net.Http.Server
|
||||||
responseHeaders.SetValues("Custom1", "value1a", "value1b");
|
responseHeaders.SetValues("Custom1", "value1a", "value1b");
|
||||||
responseHeaders.SetValues("Custom2", "value2a, value2b");
|
responseHeaders.SetValues("Custom2", "value2a, value2b");
|
||||||
var body = context.Response.Body;
|
var body = context.Response.Body;
|
||||||
Assert.False(context.Response.HeadersSent);
|
Assert.False(context.Response.HasStarted);
|
||||||
body.Flush();
|
body.Flush();
|
||||||
Assert.True(context.Response.HeadersSent);
|
Assert.True(context.Response.HasStarted);
|
||||||
var ex = Assert.Throws<InvalidOperationException>(() => context.Response.StatusCode = 404);
|
var ex = Assert.Throws<InvalidOperationException>(() => context.Response.StatusCode = 404);
|
||||||
Assert.Equal("Headers already sent.", ex.Message);
|
Assert.Equal("Headers already sent.", ex.Message);
|
||||||
ex = Assert.Throws<InvalidOperationException>(() => responseHeaders.Add("Custom3", new string[] { "value3a, value3b", "value3c" }));
|
ex = Assert.Throws<InvalidOperationException>(() => responseHeaders.Add("Custom3", new string[] { "value3a, value3b", "value3c" }));
|
||||||
Assert.Equal("The response headers cannot be modified because they have already been sent.", ex.Message);
|
Assert.Equal("The response headers cannot be modified because the response has already started.", ex.Message);
|
||||||
|
|
||||||
context.Dispose();
|
context.Dispose();
|
||||||
|
|
||||||
|
|
@ -385,13 +385,13 @@ namespace Microsoft.Net.Http.Server
|
||||||
responseHeaders.SetValues("Custom1", "value1a", "value1b");
|
responseHeaders.SetValues("Custom1", "value1a", "value1b");
|
||||||
responseHeaders.SetValues("Custom2", "value2a, value2b");
|
responseHeaders.SetValues("Custom2", "value2a, value2b");
|
||||||
var body = context.Response.Body;
|
var body = context.Response.Body;
|
||||||
Assert.False(context.Response.HeadersSent);
|
Assert.False(context.Response.HasStarted);
|
||||||
await body.FlushAsync();
|
await body.FlushAsync();
|
||||||
Assert.True(context.Response.HeadersSent);
|
Assert.True(context.Response.HasStarted);
|
||||||
var ex = Assert.Throws<InvalidOperationException>(() => context.Response.StatusCode = 404);
|
var ex = Assert.Throws<InvalidOperationException>(() => context.Response.StatusCode = 404);
|
||||||
Assert.Equal("Headers already sent.", ex.Message);
|
Assert.Equal("Headers already sent.", ex.Message);
|
||||||
ex = Assert.Throws<InvalidOperationException>(() => responseHeaders.Add("Custom3", new string[] { "value3a, value3b", "value3c" }));
|
ex = Assert.Throws<InvalidOperationException>(() => responseHeaders.Add("Custom3", new string[] { "value3a, value3b", "value3c" }));
|
||||||
Assert.Equal("The response headers cannot be modified because they have already been sent.", ex.Message);
|
Assert.Equal("The response headers cannot be modified because the response has already started.", ex.Message);
|
||||||
|
|
||||||
context.Dispose();
|
context.Dispose();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -202,6 +202,34 @@ namespace Microsoft.Net.Http.Server
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResponseSendFile_EmptyFileCountUnspecified_SetsChunkedAndFlushesHeaders()
|
||||||
|
{
|
||||||
|
var emptyFilePath = Path.Combine(Environment.CurrentDirectory, "zz_" + Guid.NewGuid().ToString() + "EmptyTestFile.txt");
|
||||||
|
var emptyFile = File.Create(emptyFilePath, 1024);
|
||||||
|
emptyFile.Close();
|
||||||
|
|
||||||
|
string address;
|
||||||
|
using (var server = Utilities.CreateHttpServer(out address))
|
||||||
|
{
|
||||||
|
Task<HttpResponseMessage> responseTask = SendRequestAsync(address);
|
||||||
|
|
||||||
|
var context = await server.GetContextAsync();
|
||||||
|
await context.Response.SendFileAsync(emptyFilePath, 0, null, CancellationToken.None);
|
||||||
|
Assert.True(context.Response.HasStartedSending);
|
||||||
|
await context.Response.Body.WriteAsync(new byte[10], 0, 10, CancellationToken.None);
|
||||||
|
context.Dispose();
|
||||||
|
File.Delete(emptyFilePath);
|
||||||
|
|
||||||
|
HttpResponseMessage response = await responseTask;
|
||||||
|
Assert.Equal(200, (int)response.StatusCode);
|
||||||
|
IEnumerable<string> contentLength;
|
||||||
|
Assert.False(response.Content.Headers.TryGetValues("content-length", out contentLength), "Content-Length");
|
||||||
|
Assert.True(response.Headers.TransferEncodingChunked.HasValue);
|
||||||
|
Assert.Equal(10, (await response.Content.ReadAsByteArrayAsync()).Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ResponseSendFile_ContentLength_PassedThrough()
|
public async Task ResponseSendFile_ContentLength_PassedThrough()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ namespace Microsoft.Net.Http.Server
|
||||||
context.Dispose();
|
context.Dispose();
|
||||||
HttpResponseMessage response = await clientTask;
|
HttpResponseMessage response = await clientTask;
|
||||||
Assert.Equal(200, (int)response.StatusCode);
|
Assert.Equal(200, (int)response.StatusCode);
|
||||||
Assert.True(response.Headers.TransferEncodingChunked.Value, "Chunked");
|
|
||||||
Assert.Equal("Hello World", await response.Content.ReadAsStringAsync());
|
Assert.Equal("Hello World", await response.Content.ReadAsStringAsync());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue