533 lines
17 KiB
C#
533 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="HttpListenerRequest.cs" company="Microsoft">
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// </copyright>
|
|
//------------------------------------------------------------------------------
|
|
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Runtime.InteropServices;
|
|
using System.Security.Claims;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Microsoft.Net.Http.Server
|
|
{
|
|
public sealed class Request
|
|
{
|
|
private RequestContext _requestContext;
|
|
private NativeRequestContext _nativeRequestContext;
|
|
|
|
private ulong _requestId;
|
|
private ulong _connectionId;
|
|
private ulong _contextId;
|
|
|
|
private SslStatus _sslStatus;
|
|
|
|
private string _httpMethod;
|
|
private Version _httpVersion;
|
|
|
|
// private Uri _requestUri;
|
|
private string _rawUrl;
|
|
private string _cookedUrlHost;
|
|
private string _cookedUrlPath;
|
|
private string _cookedUrlQuery;
|
|
private string _pathBase;
|
|
private string _path;
|
|
|
|
private X509Certificate2 _clientCert;
|
|
private byte[] _providedTokenBindingId;
|
|
private byte[] _referredTokenBindingId;
|
|
|
|
private HeaderCollection _headers;
|
|
private BoundaryType _contentBoundaryType;
|
|
private long? _contentLength;
|
|
private Stream _nativeStream;
|
|
|
|
private SocketAddress _localEndPoint;
|
|
private SocketAddress _remoteEndPoint;
|
|
|
|
private ClaimsPrincipal _user;
|
|
|
|
private bool _isDisposed = false;
|
|
|
|
internal unsafe Request(RequestContext httpContext, NativeRequestContext memoryBlob)
|
|
{
|
|
// TODO: Verbose log
|
|
_requestContext = httpContext;
|
|
_nativeRequestContext = memoryBlob;
|
|
_contentBoundaryType = BoundaryType.None;
|
|
|
|
// Set up some of these now to avoid refcounting on memory blob later.
|
|
_requestId = memoryBlob.RequestBlob->RequestId;
|
|
_connectionId = memoryBlob.RequestBlob->ConnectionId;
|
|
_contextId = memoryBlob.RequestBlob->UrlContext;
|
|
_sslStatus = memoryBlob.RequestBlob->pSslInfo == null ? SslStatus.Insecure :
|
|
memoryBlob.RequestBlob->pSslInfo->SslClientCertNegotiated == 0 ? SslStatus.NoClientCert :
|
|
SslStatus.ClientCert;
|
|
if (memoryBlob.RequestBlob->pRawUrl != null && memoryBlob.RequestBlob->RawUrlLength > 0)
|
|
{
|
|
_rawUrl = Marshal.PtrToStringAnsi((IntPtr)memoryBlob.RequestBlob->pRawUrl, memoryBlob.RequestBlob->RawUrlLength);
|
|
}
|
|
|
|
UnsafeNclNativeMethods.HttpApi.HTTP_COOKED_URL cookedUrl = memoryBlob.RequestBlob->CookedUrl;
|
|
if (cookedUrl.pHost != null && cookedUrl.HostLength > 0)
|
|
{
|
|
_cookedUrlHost = Marshal.PtrToStringUni((IntPtr)cookedUrl.pHost, cookedUrl.HostLength / 2);
|
|
}
|
|
if (cookedUrl.pAbsPath != null && cookedUrl.AbsPathLength > 0)
|
|
{
|
|
_cookedUrlPath = Marshal.PtrToStringUni((IntPtr)cookedUrl.pAbsPath, cookedUrl.AbsPathLength / 2);
|
|
}
|
|
if (cookedUrl.pQueryString != null && cookedUrl.QueryStringLength > 0)
|
|
{
|
|
_cookedUrlQuery = Marshal.PtrToStringUni((IntPtr)cookedUrl.pQueryString, cookedUrl.QueryStringLength / 2);
|
|
}
|
|
|
|
UrlPrefix prefix = httpContext.Server.UrlPrefixes.GetPrefix((int)_contextId);
|
|
string originalPath = RequestPath;
|
|
|
|
// These paths are both unescaped already.
|
|
if (originalPath.Length == prefix.Path.Length - 1)
|
|
{
|
|
// They matched exactly except for the trailing slash.
|
|
_pathBase = originalPath;
|
|
_path = string.Empty;
|
|
}
|
|
else
|
|
{
|
|
// url: /base/path, prefix: /base/, base: /base, path: /path
|
|
// url: /, prefix: /, base: , path: /
|
|
_pathBase = originalPath.Substring(0, prefix.Path.Length - 1);
|
|
_path = originalPath.Substring(prefix.Path.Length - 1);
|
|
}
|
|
|
|
int major = memoryBlob.RequestBlob->Version.MajorVersion;
|
|
int minor = memoryBlob.RequestBlob->Version.MinorVersion;
|
|
if (major == 1 && minor == 1)
|
|
{
|
|
_httpVersion = Constants.V1_1;
|
|
}
|
|
else if (major == 1 && minor == 0)
|
|
{
|
|
_httpVersion = Constants.V1_0;
|
|
}
|
|
else
|
|
{
|
|
_httpVersion = new Version(major, minor);
|
|
}
|
|
|
|
_httpMethod = UnsafeNclNativeMethods.HttpApi.GetVerb(RequestBuffer, OriginalBlobAddress);
|
|
_headers = new HeaderCollection(new RequestHeaders(_nativeRequestContext));
|
|
|
|
var requestV2 = (UnsafeNclNativeMethods.HttpApi.HTTP_REQUEST_V2*)memoryBlob.RequestBlob;
|
|
_user = AuthenticationManager.GetUser(requestV2->pRequestInfo, requestV2->RequestInfoCount);
|
|
|
|
GetTlsTokenBindingInfo();
|
|
|
|
// TODO: Verbose log parameters
|
|
}
|
|
|
|
internal SslStatus SslStatus
|
|
{
|
|
get
|
|
{
|
|
return _sslStatus;
|
|
}
|
|
}
|
|
|
|
internal ulong ConnectionId
|
|
{
|
|
get
|
|
{
|
|
return _connectionId;
|
|
}
|
|
}
|
|
|
|
internal ulong ContextId
|
|
{
|
|
get { return _contextId; }
|
|
}
|
|
|
|
internal RequestContext RequestContext
|
|
{
|
|
get
|
|
{
|
|
return _requestContext;
|
|
}
|
|
}
|
|
|
|
internal byte[] RequestBuffer
|
|
{
|
|
get
|
|
{
|
|
CheckDisposed();
|
|
return _nativeRequestContext.RequestBuffer;
|
|
}
|
|
}
|
|
|
|
internal IntPtr OriginalBlobAddress
|
|
{
|
|
get
|
|
{
|
|
CheckDisposed();
|
|
return _nativeRequestContext.OriginalBlobAddress;
|
|
}
|
|
}
|
|
|
|
// With the leading ?, if any
|
|
public string QueryString
|
|
{
|
|
get
|
|
{
|
|
return _cookedUrlQuery ?? string.Empty;
|
|
}
|
|
set
|
|
{
|
|
_cookedUrlQuery = value;
|
|
}
|
|
}
|
|
|
|
internal ulong RequestId
|
|
{
|
|
get
|
|
{
|
|
return _requestId;
|
|
}
|
|
}
|
|
|
|
public long? ContentLength
|
|
{
|
|
get
|
|
{
|
|
if (_contentBoundaryType == BoundaryType.None)
|
|
{
|
|
string transferEncoding = Headers[HttpKnownHeaderNames.TransferEncoding];
|
|
if (string.Equals("chunked", transferEncoding?.Trim(), StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
_contentBoundaryType = BoundaryType.Chunked;
|
|
}
|
|
else
|
|
{
|
|
string length = Headers[HttpKnownHeaderNames.ContentLength];
|
|
long value;
|
|
if (length != null && long.TryParse(length.Trim(), NumberStyles.None,
|
|
CultureInfo.InvariantCulture.NumberFormat, out value))
|
|
{
|
|
_contentBoundaryType = BoundaryType.ContentLength;
|
|
_contentLength = value;
|
|
}
|
|
else
|
|
{
|
|
_contentBoundaryType = BoundaryType.Invalid;
|
|
}
|
|
}
|
|
}
|
|
|
|
return _contentLength;
|
|
}
|
|
}
|
|
|
|
public HeaderCollection Headers
|
|
{
|
|
get { return _headers; }
|
|
}
|
|
|
|
public string Method
|
|
{
|
|
get { return _httpMethod; }
|
|
}
|
|
|
|
public bool IsHeadMethod
|
|
{
|
|
get { return string.Equals(_httpMethod, "HEAD", StringComparison.OrdinalIgnoreCase); }
|
|
}
|
|
|
|
public Stream Body
|
|
{
|
|
get
|
|
{
|
|
if (_nativeStream == null)
|
|
{
|
|
_nativeStream = HasEntityBody ? new RequestStream(RequestContext) : Stream.Null;
|
|
}
|
|
return _nativeStream;
|
|
}
|
|
}
|
|
|
|
public string PathBase
|
|
{
|
|
get { return _pathBase; }
|
|
}
|
|
|
|
public string Path
|
|
{
|
|
get { return _path; }
|
|
}
|
|
|
|
public bool IsSecureConnection
|
|
{
|
|
get
|
|
{
|
|
return _sslStatus != SslStatus.Insecure;
|
|
}
|
|
}
|
|
/*
|
|
internal string RawUrl
|
|
{
|
|
get
|
|
{
|
|
return _rawUrl;
|
|
}
|
|
}
|
|
*/
|
|
public Version ProtocolVersion
|
|
{
|
|
get
|
|
{
|
|
return _httpVersion;
|
|
}
|
|
}
|
|
|
|
public bool HasEntityBody
|
|
{
|
|
get
|
|
{
|
|
// accessing the ContentLength property delay creates _contentBoundaryType
|
|
return (ContentLength.HasValue && ContentLength.Value > 0 && _contentBoundaryType == BoundaryType.ContentLength)
|
|
|| _contentBoundaryType == BoundaryType.Chunked;
|
|
}
|
|
}
|
|
|
|
private SocketAddress RemoteEndPoint
|
|
{
|
|
get
|
|
{
|
|
if (_remoteEndPoint == null)
|
|
{
|
|
_remoteEndPoint = UnsafeNclNativeMethods.HttpApi.GetRemoteEndPoint(RequestBuffer, OriginalBlobAddress);
|
|
}
|
|
|
|
return _remoteEndPoint;
|
|
}
|
|
}
|
|
|
|
private SocketAddress LocalEndPoint
|
|
{
|
|
get
|
|
{
|
|
if (_localEndPoint == null)
|
|
{
|
|
_localEndPoint = UnsafeNclNativeMethods.HttpApi.GetLocalEndPoint(RequestBuffer, OriginalBlobAddress);
|
|
}
|
|
|
|
return _localEndPoint;
|
|
}
|
|
}
|
|
|
|
public IPAddress RemoteIpAddress
|
|
{
|
|
get { return RemoteEndPoint.GetIPAddress(); }
|
|
}
|
|
|
|
public IPAddress LocalIpAddress
|
|
{
|
|
get { return LocalEndPoint.GetIPAddress(); }
|
|
}
|
|
|
|
public int RemotePort
|
|
{
|
|
get { return RemoteEndPoint.GetPort(); }
|
|
}
|
|
|
|
public int LocalPort
|
|
{
|
|
get { return LocalEndPoint.GetPort(); }
|
|
}
|
|
|
|
public string Scheme
|
|
{
|
|
get { return IsSecureConnection ? Constants.HttpsScheme : Constants.HttpScheme; }
|
|
}
|
|
|
|
internal string RequestPath
|
|
{
|
|
get
|
|
{
|
|
return RequestUriBuilder.GetRequestPath(_rawUrl, _cookedUrlPath, RequestContext.Logger);
|
|
}
|
|
}
|
|
|
|
internal bool IsUpgradable
|
|
{
|
|
get
|
|
{
|
|
// HTTP.Sys allows you to upgrade anything to opaque unless content-length > 0 or chunked are specified.
|
|
return !HasEntityBody;
|
|
}
|
|
}
|
|
|
|
public string ContentType
|
|
{
|
|
get
|
|
{
|
|
return Headers[HttpKnownHeaderNames.ContentType];
|
|
}
|
|
}
|
|
|
|
internal ClaimsPrincipal User
|
|
{
|
|
get { return _user; }
|
|
}
|
|
|
|
internal UnsafeNclNativeMethods.HttpApi.HTTP_VERB GetKnownMethod()
|
|
{
|
|
return UnsafeNclNativeMethods.HttpApi.GetKnownVerb(RequestBuffer, OriginalBlobAddress);
|
|
}
|
|
|
|
// Populates the client certificate. The result may be null if there is no client cert.
|
|
// TODO: Does it make sense for this to be invoked multiple times (e.g. renegotiate)? Client and server code appear to
|
|
// enable this, but it's unclear what Http.Sys would do.
|
|
public async Task<X509Certificate2> GetClientCertificateAsync(CancellationToken cancellationToken = default(CancellationToken))
|
|
{
|
|
if (SslStatus == SslStatus.Insecure)
|
|
{
|
|
// Non-SSL
|
|
return null;
|
|
}
|
|
// TODO: Verbose log
|
|
if (_clientCert != null)
|
|
{
|
|
return _clientCert;
|
|
}
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
ClientCertLoader certLoader = new ClientCertLoader(RequestContext, cancellationToken);
|
|
try
|
|
{
|
|
await certLoader.LoadClientCertificateAsync().SupressContext();
|
|
// Populate the environment.
|
|
if (certLoader.ClientCert != null)
|
|
{
|
|
_clientCert = certLoader.ClientCert;
|
|
}
|
|
// TODO: Expose errors and exceptions?
|
|
}
|
|
catch (Exception)
|
|
{
|
|
if (certLoader != null)
|
|
{
|
|
certLoader.Dispose();
|
|
}
|
|
throw;
|
|
}
|
|
return _clientCert;
|
|
}
|
|
|
|
public byte[] GetProvidedTokenBindingId()
|
|
{
|
|
return _providedTokenBindingId;
|
|
}
|
|
|
|
public byte[] GetReferredTokenBindingId()
|
|
{
|
|
return _referredTokenBindingId;
|
|
}
|
|
|
|
// Only call from the constructor so we can directly access the native request blob.
|
|
// This requires Windows 10 and the following reg key:
|
|
// Set Key: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\HTTP\Parameters to Value: EnableSslTokenBinding = 1 [DWORD]
|
|
// Then for IE to work you need to set these:
|
|
// Key: HKLM\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ENABLE_TOKEN_BINDING
|
|
// Value: "iexplore.exe"=dword:0x00000001
|
|
// Key: HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ENABLE_TOKEN_BINDING
|
|
// Value: "iexplore.exe"=dword:00000001
|
|
private unsafe void GetTlsTokenBindingInfo()
|
|
{
|
|
var nativeRequest = (UnsafeNclNativeMethods.HttpApi.HTTP_REQUEST_V2*)_nativeRequestContext.RequestBlob;
|
|
for (int i = 0; i < nativeRequest->RequestInfoCount; i++)
|
|
{
|
|
var pThisInfo = &nativeRequest->pRequestInfo[i];
|
|
if (pThisInfo->InfoType == UnsafeNclNativeMethods.HttpApi.HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeSslTokenBinding)
|
|
{
|
|
var pTokenBindingInfo = (UnsafeNclNativeMethods.HttpApi.HTTP_REQUEST_TOKEN_BINDING_INFO*)pThisInfo->pInfo;
|
|
_providedTokenBindingId = TokenBindingUtil.GetProvidedTokenIdFromBindingInfo(pTokenBindingInfo, out _referredTokenBindingId);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Use this to save the blob from dispose if this object was never used (never given to a user) and is about to be
|
|
// disposed.
|
|
internal void DetachBlob(NativeRequestContext memoryBlob)
|
|
{
|
|
if (memoryBlob != null && (object)memoryBlob == (object)_nativeRequestContext)
|
|
{
|
|
_nativeRequestContext = null;
|
|
}
|
|
}
|
|
|
|
// Finalizes ownership of the memory blob. DetachBlob can't be called after this.
|
|
internal void ReleasePins()
|
|
{
|
|
_nativeRequestContext.ReleasePins();
|
|
}
|
|
|
|
// should only be called from RequestContext
|
|
internal void Dispose()
|
|
{
|
|
// TODO: Verbose log
|
|
_isDisposed = true;
|
|
NativeRequestContext memoryBlob = _nativeRequestContext;
|
|
if (memoryBlob != null)
|
|
{
|
|
memoryBlob.Dispose();
|
|
_nativeRequestContext = null;
|
|
}
|
|
if (_nativeStream != null)
|
|
{
|
|
_nativeStream.Dispose();
|
|
}
|
|
}
|
|
|
|
internal void CheckDisposed()
|
|
{
|
|
if (_isDisposed)
|
|
{
|
|
throw new ObjectDisposedException(this.GetType().FullName);
|
|
}
|
|
}
|
|
|
|
internal void SwitchToOpaqueMode()
|
|
{
|
|
if (_nativeStream == null || _nativeStream == Stream.Null)
|
|
{
|
|
_nativeStream = new RequestStream(RequestContext);
|
|
}
|
|
}
|
|
}
|
|
}
|