460 lines
17 KiB
C#
460 lines
17 KiB
C#
// Copyright (c) Microsoft Open Technologies, Inc.
|
|
// All Rights Reserved
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR
|
|
// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING
|
|
// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF
|
|
// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR
|
|
// NON-INFRINGEMENT.
|
|
// See the Apache 2 License for the specific language governing
|
|
// permissions and limitations under the License.
|
|
|
|
//------------------------------------------------------------------------------
|
|
// <copyright file="HttpListenerContext.cs" company="Microsoft">
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// </copyright>
|
|
//------------------------------------------------------------------------------
|
|
|
|
using System;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net.WebSockets;
|
|
using System.Runtime.InteropServices;
|
|
using System.Security.Claims;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Framework.Logging;
|
|
using Microsoft.Net.WebSockets;
|
|
|
|
namespace Microsoft.Net.Http.Server
|
|
{
|
|
public sealed class RequestContext : IDisposable
|
|
{
|
|
internal static readonly Action<object> AbortDelegate = Abort;
|
|
|
|
private WebListener _server;
|
|
private Request _request;
|
|
private Response _response;
|
|
private NativeRequestContext _memoryBlob;
|
|
private bool _disposed;
|
|
private CancellationTokenSource _requestAbortSource;
|
|
private CancellationToken? _disconnectToken;
|
|
|
|
internal RequestContext(WebListener server, NativeRequestContext memoryBlob)
|
|
{
|
|
// TODO: Verbose log
|
|
_server = server;
|
|
_memoryBlob = memoryBlob;
|
|
_request = new Request(this, _memoryBlob);
|
|
_response = new Response(this);
|
|
_request.ReleasePins();
|
|
AuthenticationChallenges = server.AuthenticationManager.AuthenticationSchemes & ~AuthenticationSchemes.AllowAnonymous;
|
|
}
|
|
|
|
public Request Request
|
|
{
|
|
get
|
|
{
|
|
return _request;
|
|
}
|
|
}
|
|
|
|
public Response Response
|
|
{
|
|
get
|
|
{
|
|
return _response;
|
|
}
|
|
}
|
|
|
|
public ClaimsPrincipal User
|
|
{
|
|
get { return _request.User; }
|
|
}
|
|
|
|
public CancellationToken DisconnectToken
|
|
{
|
|
get
|
|
{
|
|
// Create a new token per request, but link it to a single connection token.
|
|
// We need to be able to dispose of the registrations each request to prevent leaks.
|
|
if (!_disconnectToken.HasValue)
|
|
{
|
|
var connectionDisconnectToken = _server.RegisterForDisconnectNotification(this);
|
|
|
|
if (connectionDisconnectToken.CanBeCanceled)
|
|
{
|
|
_requestAbortSource = CancellationTokenSource.CreateLinkedTokenSource(connectionDisconnectToken);
|
|
_disconnectToken = _requestAbortSource.Token;
|
|
}
|
|
else
|
|
{
|
|
_disconnectToken = CancellationToken.None;
|
|
}
|
|
}
|
|
return _disconnectToken.Value;
|
|
}
|
|
}
|
|
|
|
internal WebListener Server
|
|
{
|
|
get
|
|
{
|
|
return _server;
|
|
}
|
|
}
|
|
|
|
internal ILogger Logger
|
|
{
|
|
get { return Server.Logger; }
|
|
}
|
|
|
|
internal SafeHandle RequestQueueHandle
|
|
{
|
|
get
|
|
{
|
|
return _server.RequestQueueHandle;
|
|
}
|
|
}
|
|
|
|
internal ulong RequestId
|
|
{
|
|
get
|
|
{
|
|
return Request.RequestId;
|
|
}
|
|
}
|
|
|
|
public unsafe Guid TraceIdentifier
|
|
{
|
|
get
|
|
{
|
|
// This is the base GUID used by HTTP.SYS for generating the activity ID.
|
|
// HTTP.SYS overwrites the first 8 bytes of the base GUID with RequestId to generate ETW activity ID.
|
|
|
|
var guid = new Guid(0xffcb4c93, 0xa57f, 0x453c, 0xb6, 0x3f, 0x84, 0x71, 0xc, 0x79, 0x67, 0xbb);
|
|
*((ulong*)&guid) = Request.RequestId;
|
|
return guid;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The authentication challengest that will be added to the response if the status code is 401.
|
|
/// This must be a subset of the AuthenticationSchemes enabled on the server.
|
|
/// </summary>
|
|
public AuthenticationSchemes AuthenticationChallenges { get; set; }
|
|
|
|
public bool IsUpgradableRequest
|
|
{
|
|
get { return _request.IsUpgradable; }
|
|
}
|
|
|
|
public Task<Stream> UpgradeAsync()
|
|
{
|
|
if (!IsUpgradableRequest || _response.HeadersSent)
|
|
{
|
|
throw new InvalidOperationException();
|
|
}
|
|
|
|
// Set the status code and reason phrase
|
|
Response.StatusCode = (int)HttpStatusCode.SwitchingProtocols;
|
|
Response.ReasonPhrase = HttpReasonPhrase.Get(HttpStatusCode.SwitchingProtocols);
|
|
|
|
Response.SendOpaqueUpgrade(); // TODO: Async
|
|
Request.SwitchToOpaqueMode();
|
|
Response.SwitchToOpaqueMode();
|
|
Stream opaqueStream = new OpaqueStream(Request.Body, Response.Body);
|
|
return Task.FromResult(opaqueStream);
|
|
}
|
|
|
|
public bool IsWebSocketRequest
|
|
{
|
|
get
|
|
{
|
|
if (!WebSocketHelpers.AreWebSocketsSupported)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!IsUpgradableRequest)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!string.Equals("GET", Request.Method, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Connection: Upgrade (some odd clients send Upgrade,KeepAlive)
|
|
string connection = Request.Headers[HttpKnownHeaderNames.Connection] ?? string.Empty;
|
|
if (connection.IndexOf(HttpKnownHeaderNames.Upgrade, StringComparison.OrdinalIgnoreCase) < 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Upgrade: websocket
|
|
string upgrade = Request.Headers[HttpKnownHeaderNames.Upgrade];
|
|
if (!string.Equals(WebSocketHelpers.WebSocketUpgradeToken, upgrade, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Sec-WebSocket-Version: 13
|
|
string version = Request.Headers[HttpKnownHeaderNames.SecWebSocketVersion];
|
|
if (!string.Equals(WebSocketConstants.SupportedProtocolVersion, version, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Sec-WebSocket-Key: {base64string}
|
|
string key = Request.Headers[HttpKnownHeaderNames.SecWebSocketKey];
|
|
if (!WebSocketHelpers.IsValidWebSocketKey(key))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Compare IsWebSocketRequest
|
|
private void ValidateWebSocketRequest()
|
|
{
|
|
if (!WebSocketHelpers.AreWebSocketsSupported)
|
|
{
|
|
throw new NotSupportedException("WebSockets are not supported on this platform.");
|
|
}
|
|
|
|
if (!IsUpgradableRequest)
|
|
{
|
|
throw new InvalidOperationException("This request is not a valid upgrade request.");
|
|
}
|
|
|
|
if (!string.Equals("GET", Request.Method, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
throw new InvalidOperationException("This request is not a valid upgrade request; invalid verb: " + Request.Method);
|
|
}
|
|
|
|
// Connection: Upgrade (some odd clients send Upgrade,KeepAlive)
|
|
string connection = Request.Headers[HttpKnownHeaderNames.Connection] ?? string.Empty;
|
|
if (connection.IndexOf(HttpKnownHeaderNames.Upgrade, StringComparison.OrdinalIgnoreCase) < 0)
|
|
{
|
|
throw new InvalidOperationException("The Connection header is invalid: " + connection);
|
|
}
|
|
|
|
// Upgrade: websocket
|
|
string upgrade = Request.Headers[HttpKnownHeaderNames.Upgrade];
|
|
if (!string.Equals(WebSocketHelpers.WebSocketUpgradeToken, upgrade, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
throw new InvalidOperationException("The Upgrade header is invalid: " + upgrade);
|
|
}
|
|
|
|
// Sec-WebSocket-Version: 13
|
|
string version = Request.Headers[HttpKnownHeaderNames.SecWebSocketVersion];
|
|
if (!string.Equals(WebSocketConstants.SupportedProtocolVersion, version, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
throw new InvalidOperationException("The Sec-WebSocket-Version header is invalid or not supported: " + version);
|
|
}
|
|
|
|
// Sec-WebSocket-Key: {base64string}
|
|
string key = Request.Headers[HttpKnownHeaderNames.SecWebSocketKey];
|
|
if (!WebSocketHelpers.IsValidWebSocketKey(key))
|
|
{
|
|
throw new InvalidOperationException("The Sec-WebSocket-Key header is invalid: " + upgrade);
|
|
}
|
|
}
|
|
|
|
public Task<WebSocket> AcceptWebSocketAsync()
|
|
{
|
|
return AcceptWebSocketAsync(null,
|
|
WebSocketHelpers.DefaultReceiveBufferSize,
|
|
WebSocketHelpers.DefaultKeepAliveInterval);
|
|
}
|
|
|
|
public Task<WebSocket> AcceptWebSocketAsync(string subProtocol)
|
|
{
|
|
return AcceptWebSocketAsync(subProtocol,
|
|
WebSocketHelpers.DefaultReceiveBufferSize,
|
|
WebSocketHelpers.DefaultKeepAliveInterval);
|
|
}
|
|
|
|
public Task<WebSocket> AcceptWebSocketAsync(string subProtocol, TimeSpan keepAliveInterval)
|
|
{
|
|
return AcceptWebSocketAsync(subProtocol,
|
|
WebSocketHelpers.DefaultReceiveBufferSize,
|
|
keepAliveInterval);
|
|
}
|
|
|
|
public Task<WebSocket> AcceptWebSocketAsync(
|
|
string subProtocol,
|
|
int receiveBufferSize,
|
|
TimeSpan keepAliveInterval)
|
|
{
|
|
WebSocketHelpers.ValidateOptions(subProtocol, receiveBufferSize, WebSocketBuffer.MinSendBufferSize, keepAliveInterval);
|
|
|
|
ArraySegment<byte> internalBuffer = WebSocketBuffer.CreateInternalBufferArraySegment(receiveBufferSize, WebSocketBuffer.MinSendBufferSize, true);
|
|
return this.AcceptWebSocketAsync(subProtocol,
|
|
receiveBufferSize,
|
|
keepAliveInterval,
|
|
internalBuffer);
|
|
}
|
|
|
|
public Task<WebSocket> AcceptWebSocketAsync(
|
|
string subProtocol,
|
|
int receiveBufferSize,
|
|
TimeSpan keepAliveInterval,
|
|
ArraySegment<byte> internalBuffer)
|
|
{
|
|
if (!IsUpgradableRequest)
|
|
{
|
|
throw new InvalidOperationException("This request is cannot be upgraded.");
|
|
}
|
|
WebSocketHelpers.ValidateOptions(subProtocol, receiveBufferSize, WebSocketBuffer.MinSendBufferSize, keepAliveInterval);
|
|
WebSocketHelpers.ValidateArraySegment<byte>(internalBuffer, "internalBuffer");
|
|
WebSocketBuffer.Validate(internalBuffer.Count, receiveBufferSize, WebSocketBuffer.MinSendBufferSize, true);
|
|
|
|
return AcceptWebSocketAsyncCore(subProtocol, receiveBufferSize, keepAliveInterval, internalBuffer);
|
|
}
|
|
|
|
private async Task<WebSocket> AcceptWebSocketAsyncCore(
|
|
string subProtocol,
|
|
int receiveBufferSize,
|
|
TimeSpan keepAliveInterval,
|
|
ArraySegment<byte> internalBuffer)
|
|
{
|
|
try
|
|
{
|
|
ValidateWebSocketRequest();
|
|
|
|
var subProtocols = Request.Headers.GetValues(HttpKnownHeaderNames.SecWebSocketProtocol);
|
|
bool shouldSendSecWebSocketProtocolHeader = WebSocketHelpers.ProcessWebSocketProtocolHeader(subProtocols, subProtocol);
|
|
if (shouldSendSecWebSocketProtocolHeader)
|
|
{
|
|
Response.Headers[HttpKnownHeaderNames.SecWebSocketProtocol] = subProtocol;
|
|
}
|
|
|
|
// negotiate the websocket key return value
|
|
string secWebSocketKey = Request.Headers[HttpKnownHeaderNames.SecWebSocketKey];
|
|
string secWebSocketAccept = WebSocketHelpers.GetSecWebSocketAcceptString(secWebSocketKey);
|
|
|
|
Response.Headers.AppendValues(HttpKnownHeaderNames.Connection, HttpKnownHeaderNames.Upgrade);
|
|
Response.Headers.AppendValues(HttpKnownHeaderNames.Upgrade, WebSocketHelpers.WebSocketUpgradeToken);
|
|
Response.Headers.AppendValues(HttpKnownHeaderNames.SecWebSocketAccept, secWebSocketAccept);
|
|
|
|
Stream opaqueStream = await UpgradeAsync();
|
|
|
|
return WebSocketHelpers.CreateServerWebSocket(
|
|
opaqueStream,
|
|
subProtocol,
|
|
receiveBufferSize,
|
|
keepAliveInterval,
|
|
internalBuffer);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogHelper.LogException(Logger, "AcceptWebSocketAsync", ex);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
public bool TryGetChannelBinding(ref ChannelBinding value)
|
|
{
|
|
value = Server.GetChannelBinding(Request.ConnectionId, Request.IsSecureConnection);
|
|
return value != null;
|
|
}
|
|
*/
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
{
|
|
return;
|
|
}
|
|
_disposed = true;
|
|
|
|
// TODO: Verbose log
|
|
try
|
|
{
|
|
if (_requestAbortSource != null)
|
|
{
|
|
_requestAbortSource.Dispose();
|
|
}
|
|
_response.Dispose();
|
|
}
|
|
finally
|
|
{
|
|
_request.Dispose();
|
|
}
|
|
}
|
|
|
|
public void Abort()
|
|
{
|
|
// May be called from Dispose() code path, don't check _disposed.
|
|
// TODO: Verbose log
|
|
_disposed = true;
|
|
if (_requestAbortSource != null)
|
|
{
|
|
try
|
|
{
|
|
_requestAbortSource.Cancel();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogHelper.LogException(Logger, "Abort", ex);
|
|
}
|
|
_requestAbortSource.Dispose();
|
|
}
|
|
ForceCancelRequest(RequestQueueHandle, _request.RequestId);
|
|
_request.Dispose();
|
|
}
|
|
|
|
private static void Abort(object state)
|
|
{
|
|
var context = (RequestContext)state;
|
|
context.Abort();
|
|
}
|
|
|
|
// This is only called while processing incoming requests. We don't have to worry about canceling
|
|
// any response writes.
|
|
[SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", Justification =
|
|
"It is safe to ignore the return value on a cancel operation because the connection is being closed")]
|
|
internal static void CancelRequest(SafeHandle requestQueueHandle, ulong requestId)
|
|
{
|
|
UnsafeNclNativeMethods.HttpApi.HttpCancelHttpRequest(requestQueueHandle, requestId,
|
|
IntPtr.Zero);
|
|
}
|
|
|
|
// The request is being aborted, but large writes may be in progress. Cancel them.
|
|
internal void ForceCancelRequest(SafeHandle requestQueueHandle, ulong requestId)
|
|
{
|
|
try
|
|
{
|
|
uint statusCode = UnsafeNclNativeMethods.HttpApi.HttpCancelHttpRequest(requestQueueHandle, requestId,
|
|
IntPtr.Zero);
|
|
|
|
// Either the connection has already dropped, or the last write is in progress.
|
|
// The requestId becomes invalid as soon as the last Content-Length write starts.
|
|
// The only way to cancel now is with CancelIoEx.
|
|
if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_CONNECTION_INVALID)
|
|
{
|
|
_response.CancelLastWrite(requestQueueHandle);
|
|
}
|
|
}
|
|
catch (ObjectDisposedException)
|
|
{
|
|
// RequestQueueHandle may have been closed
|
|
}
|
|
}
|
|
}
|
|
}
|