// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Buffers; using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.ObjectModel; using System.Diagnostics; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; using System.Security.Principal; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.HttpSys.Internal { internal unsafe class NativeRequestContext : IDisposable { private const int AlignmentPadding = 8; private const int DefaultBufferSize = 4096 - AlignmentPadding; private IntPtr _originalBufferAddress; private HttpApiTypes.HTTP_REQUEST* _nativeRequest; private IMemoryOwner _backingBuffer; private MemoryHandle _memoryHandle; private int _bufferAlignment; private SafeNativeOverlapped _nativeOverlapped; private bool _permanentlyPinned; private bool _disposed; // To be used by HttpSys internal NativeRequestContext(SafeNativeOverlapped nativeOverlapped, MemoryPool memoryPool, uint? bufferSize, ulong requestId) { _nativeOverlapped = nativeOverlapped; // TODO: // Apparently the HttpReceiveHttpRequest memory alignment requirements for non - ARM processors // are different than for ARM processors. We have seen 4 - byte - aligned buffers allocated on // virtual x64/x86 machines which were accepted by HttpReceiveHttpRequest without errors. In // these cases the buffer alignment may cause reading values at invalid offset. Setting buffer // alignment to 0 for now. // // _bufferAlignment = (int)(requestAddress.ToInt64() & 0x07); _bufferAlignment = 0; var newSize = (int)(bufferSize ?? DefaultBufferSize) + AlignmentPadding; if (newSize <= memoryPool.MaxBufferSize) { _backingBuffer = memoryPool.Rent(newSize); } else { // No size limit _backingBuffer = MemoryPool.Shared.Rent(newSize); } _backingBuffer.Memory.Span.Fill(0);// Zero the buffer _memoryHandle = _backingBuffer.Memory.Pin(); _nativeRequest = (HttpApiTypes.HTTP_REQUEST*)((long)_memoryHandle.Pointer + _bufferAlignment); RequestId = requestId; } // To be used by IIS Integration. internal NativeRequestContext(HttpApiTypes.HTTP_REQUEST* request) { _nativeRequest = request; _bufferAlignment = 0; _permanentlyPinned = true; } internal SafeNativeOverlapped NativeOverlapped => _nativeOverlapped; internal HttpApiTypes.HTTP_REQUEST* NativeRequest { get { Debug.Assert(_nativeRequest != null || _backingBuffer == null, "native request accessed after ReleasePins()."); return _nativeRequest; } } internal HttpApiTypes.HTTP_REQUEST_V2* NativeRequestV2 { get { Debug.Assert(_nativeRequest != null || _backingBuffer == null, "native request accessed after ReleasePins()."); return (HttpApiTypes.HTTP_REQUEST_V2*)_nativeRequest; } } internal ulong RequestId { get { return NativeRequest->RequestId; } set { NativeRequest->RequestId = value; } } internal ulong ConnectionId => NativeRequest->ConnectionId; internal HttpApiTypes.HTTP_VERB VerbId => NativeRequest->Verb; internal ulong UrlContext => NativeRequest->UrlContext; internal ushort UnknownHeaderCount => NativeRequest->Headers.UnknownHeaderCount; internal SslStatus SslStatus { get { return NativeRequest->pSslInfo == null ? SslStatus.Insecure : NativeRequest->pSslInfo->SslClientCertNegotiated == 0 ? SslStatus.NoClientCert : SslStatus.ClientCert; } } internal bool IsHttp2 => NativeRequest->Flags.HasFlag(HttpApiTypes.HTTP_REQUEST_FLAGS.Http2); internal uint Size { get { return (uint)_backingBuffer.Memory.Length - AlignmentPadding; } } // ReleasePins() should be called exactly once. It must be called before Dispose() is called, which means it must be called // before an object (Request) which closes the RequestContext on demand is returned to the application. internal void ReleasePins() { Debug.Assert(_nativeRequest != null, "RequestContextBase::ReleasePins()|ReleasePins() called twice."); _originalBufferAddress = (IntPtr)_nativeRequest; _memoryHandle.Dispose(); _memoryHandle = default; _nativeRequest = null; _nativeOverlapped?.Dispose(); _nativeOverlapped = null; } public virtual void Dispose() { if (!_disposed) { _disposed = true; Debug.Assert(_nativeRequest == null, "RequestContextBase::Dispose()|Dispose() called before ReleasePins()."); _nativeOverlapped?.Dispose(); _memoryHandle.Dispose(); _backingBuffer.Dispose(); } } // These methods require the HTTP_REQUEST to still be pinned in its original location. internal string GetVerb() { var verb = NativeRequest->Verb; if (verb > HttpApiTypes.HTTP_VERB.HttpVerbUnknown && verb < HttpApiTypes.HTTP_VERB.HttpVerbMaximum) { return HttpApiTypes.HttpVerbs[(int)verb]; } else if (verb == HttpApiTypes.HTTP_VERB.HttpVerbUnknown && NativeRequest->pUnknownVerb != null) { return HeaderEncoding.GetString(NativeRequest->pUnknownVerb, NativeRequest->UnknownVerbLength); } return null; } internal string GetRawUrl() { if (NativeRequest->pRawUrl != null && NativeRequest->RawUrlLength > 0) { return Marshal.PtrToStringAnsi((IntPtr)NativeRequest->pRawUrl, NativeRequest->RawUrlLength); } return null; } internal Span GetRawUrlInBytes() { if (NativeRequest->pRawUrl != null && NativeRequest->RawUrlLength > 0) { return new Span(NativeRequest->pRawUrl, NativeRequest->RawUrlLength); } return default; } internal CookedUrl GetCookedUrl() { return new CookedUrl(NativeRequest->CookedUrl); } internal Version GetVersion() { if (IsHttp2) { return Constants.V2; } var major = NativeRequest->Version.MajorVersion; var minor = NativeRequest->Version.MinorVersion; if (major == 1 && minor == 1) { return Constants.V1_1; } else if (major == 1 && minor == 0) { return Constants.V1_0; } return new Version(major, minor); } internal bool CheckAuthenticated() { var requestInfo = NativeRequestV2->pRequestInfo; var infoCount = NativeRequestV2->RequestInfoCount; for (int i = 0; i < infoCount; i++) { var info = &requestInfo[i]; if (info != null && info->InfoType == HttpApiTypes.HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeAuth) { var authInfo = (HttpApiTypes.HTTP_REQUEST_AUTH_INFO*)info->pInfo; if (authInfo->AuthStatus == HttpApiTypes.HTTP_AUTH_STATUS.HttpAuthStatusSuccess) { return true; } } } return false; } internal WindowsPrincipal GetUser() { var requestInfo = NativeRequestV2->pRequestInfo; var infoCount = NativeRequestV2->RequestInfoCount; for (int i = 0; i < infoCount; i++) { var info = &requestInfo[i]; if (info != null && info->InfoType == HttpApiTypes.HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeAuth) { var authInfo = (HttpApiTypes.HTTP_REQUEST_AUTH_INFO*)info->pInfo; if (authInfo->AuthStatus == HttpApiTypes.HTTP_AUTH_STATUS.HttpAuthStatusSuccess) { // Duplicates AccessToken var identity = new WindowsIdentity(authInfo->AccessToken, GetAuthTypeFromRequest(authInfo->AuthType)); // Close the original UnsafeNclNativeMethods.SafeNetHandles.CloseHandle(authInfo->AccessToken); return new WindowsPrincipal(identity); } } } return new WindowsPrincipal(WindowsIdentity.GetAnonymous()); // Anonymous / !IsAuthenticated } internal HttpApiTypes.HTTP_SSL_PROTOCOL_INFO GetTlsHandshake() { var requestInfo = NativeRequestV2->pRequestInfo; var infoCount = NativeRequestV2->RequestInfoCount; for (int i = 0; i < infoCount; i++) { var info = &requestInfo[i]; if (info != null && info->InfoType == HttpApiTypes.HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeSslProtocol) { var authInfo = (HttpApiTypes.HTTP_SSL_PROTOCOL_INFO*)info->pInfo; return *authInfo; } } return default; } private static string GetAuthTypeFromRequest(HttpApiTypes.HTTP_REQUEST_AUTH_TYPE input) { switch (input) { case HttpApiTypes.HTTP_REQUEST_AUTH_TYPE.HttpRequestAuthTypeBasic: return "Basic"; case HttpApiTypes.HTTP_REQUEST_AUTH_TYPE.HttpRequestAuthTypeNTLM: return "NTLM"; // case HttpApi.HTTP_REQUEST_AUTH_TYPE.HttpRequestAuthTypeDigest: // return "Digest"; case HttpApiTypes.HTTP_REQUEST_AUTH_TYPE.HttpRequestAuthTypeNegotiate: return "Negotiate"; case HttpApiTypes.HTTP_REQUEST_AUTH_TYPE.HttpRequestAuthTypeKerberos: return "Kerberos"; default: throw new NotImplementedException(input.ToString()); } } // These methods are for accessing the request structure after it has been unpinned. They need to adjust addresses // in case GC has moved the original object. internal string GetKnownHeader(HttpSysRequestHeader header) { if (_permanentlyPinned) { return GetKnowHeaderHelper(header, 0, _nativeRequest); } else { fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span) { var request = (HttpApiTypes.HTTP_REQUEST*)(pMemoryBlob + _bufferAlignment); long fixup = pMemoryBlob - (byte*)_originalBufferAddress; return GetKnowHeaderHelper(header, fixup, request); } } } private string GetKnowHeaderHelper(HttpSysRequestHeader header, long fixup, HttpApiTypes.HTTP_REQUEST* request) { int headerIndex = (int)header; string value = null; HttpApiTypes.HTTP_KNOWN_HEADER* pKnownHeader = (&request->Headers.KnownHeaders) + headerIndex; // For known headers, when header value is empty, RawValueLength will be 0 and // pRawValue will point to empty string ("\0") if (pKnownHeader->RawValueLength > 0) { value = HeaderEncoding.GetString(pKnownHeader->pRawValue + fixup, pKnownHeader->RawValueLength); } return value; } internal void GetUnknownHeaders(IDictionary unknownHeaders) { if (_permanentlyPinned) { GetUnknownHeadersHelper(unknownHeaders, 0, _nativeRequest); } else { // Return value. fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span) { var request = (HttpApiTypes.HTTP_REQUEST*)(pMemoryBlob + _bufferAlignment); long fixup = pMemoryBlob - (byte*)_originalBufferAddress; GetUnknownHeadersHelper(unknownHeaders, fixup, request); } } } private void GetUnknownHeadersHelper(IDictionary unknownHeaders, long fixup, HttpApiTypes.HTTP_REQUEST* request) { int index; // unknown headers if (request->Headers.UnknownHeaderCount != 0) { var pUnknownHeader = (HttpApiTypes.HTTP_UNKNOWN_HEADER*)(fixup + (byte*)request->Headers.pUnknownHeaders); for (index = 0; index < request->Headers.UnknownHeaderCount; index++) { // For unknown headers, when header value is empty, RawValueLength will be 0 and // pRawValue will be null. if (pUnknownHeader->pName != null && pUnknownHeader->NameLength > 0) { var headerName = HeaderEncoding.GetString(pUnknownHeader->pName + fixup, pUnknownHeader->NameLength); string headerValue; if (pUnknownHeader->pRawValue != null && pUnknownHeader->RawValueLength > 0) { headerValue = HeaderEncoding.GetString(pUnknownHeader->pRawValue + fixup, pUnknownHeader->RawValueLength); } else { headerValue = string.Empty; } // Note that Http.Sys currently collapses all headers of the same name to a single coma separated string, // so we can just call Set. unknownHeaders[headerName] = headerValue; } pUnknownHeader++; } } } internal SocketAddress GetRemoteEndPoint() { return GetEndPoint(localEndpoint: false); } internal SocketAddress GetLocalEndPoint() { return GetEndPoint(localEndpoint: true); } private SocketAddress GetEndPoint(bool localEndpoint) { if (_permanentlyPinned) { return GetEndPointHelper(localEndpoint, _nativeRequest, (byte *)0); } else { fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span) { var request = (HttpApiTypes.HTTP_REQUEST*)(pMemoryBlob + _bufferAlignment); return GetEndPointHelper(localEndpoint, request, pMemoryBlob); } } } private SocketAddress GetEndPointHelper(bool localEndpoint, HttpApiTypes.HTTP_REQUEST* request, byte* pMemoryBlob) { var source = localEndpoint ? (byte*)request->Address.pLocalAddress : (byte*)request->Address.pRemoteAddress; if (source == null) { return null; } var address = (IntPtr)(pMemoryBlob + _bufferAlignment - (byte*)_originalBufferAddress + source); return CopyOutAddress(address); } private static SocketAddress CopyOutAddress(IntPtr address) { ushort addressFamily = *((ushort*)address); if (addressFamily == (ushort)AddressFamily.InterNetwork) { var v4address = new SocketAddress(AddressFamily.InterNetwork, SocketAddress.IPv4AddressSize); fixed (byte* pBuffer = v4address.Buffer) { for (int index = 2; index < SocketAddress.IPv4AddressSize; index++) { pBuffer[index] = ((byte*)address)[index]; } } return v4address; } if (addressFamily == (ushort)AddressFamily.InterNetworkV6) { var v6address = new SocketAddress(AddressFamily.InterNetworkV6, SocketAddress.IPv6AddressSize); fixed (byte* pBuffer = v6address.Buffer) { for (int index = 2; index < SocketAddress.IPv6AddressSize; index++) { pBuffer[index] = ((byte*)address)[index]; } } return v6address; } return null; } internal uint GetChunks(ref int dataChunkIndex, ref uint dataChunkOffset, byte[] buffer, int offset, int size) { // Return value. if (_permanentlyPinned) { return GetChunksHelper(ref dataChunkIndex, ref dataChunkOffset, buffer, offset, size, 0, _nativeRequest); } else { fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span) { var request = (HttpApiTypes.HTTP_REQUEST*)(pMemoryBlob + _bufferAlignment); long fixup = pMemoryBlob - (byte*)_originalBufferAddress; return GetChunksHelper(ref dataChunkIndex, ref dataChunkOffset, buffer, offset, size, fixup, request); } } } private uint GetChunksHelper(ref int dataChunkIndex, ref uint dataChunkOffset, byte[] buffer, int offset, int size, long fixup, HttpApiTypes.HTTP_REQUEST* request) { uint dataRead = 0; if (request->EntityChunkCount > 0 && dataChunkIndex < request->EntityChunkCount && dataChunkIndex != -1) { var pDataChunk = (HttpApiTypes.HTTP_DATA_CHUNK*)(fixup + (byte*)&request->pEntityChunks[dataChunkIndex]); fixed (byte* pReadBuffer = buffer) { byte* pTo = &pReadBuffer[offset]; while (dataChunkIndex < request->EntityChunkCount && dataRead < size) { if (dataChunkOffset >= pDataChunk->fromMemory.BufferLength) { dataChunkOffset = 0; dataChunkIndex++; pDataChunk++; } else { byte* pFrom = (byte*)pDataChunk->fromMemory.pBuffer + dataChunkOffset + fixup; uint bytesToRead = pDataChunk->fromMemory.BufferLength - (uint)dataChunkOffset; if (bytesToRead > (uint)size) { bytesToRead = (uint)size; } for (uint i = 0; i < bytesToRead; i++) { *(pTo++) = *(pFrom++); } dataRead += bytesToRead; dataChunkOffset += bytesToRead; } } } } // we're finished. if (dataChunkIndex == request->EntityChunkCount) { dataChunkIndex = -1; } return dataRead; } internal IReadOnlyDictionary> GetRequestInfo() { if (_permanentlyPinned) { return GetRequestInfo((IntPtr)_nativeRequest, (HttpApiTypes.HTTP_REQUEST_V2*)_nativeRequest); } else { fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span) { var request = (HttpApiTypes.HTTP_REQUEST_V2*)(pMemoryBlob + _bufferAlignment); return GetRequestInfo(_originalBufferAddress, request); } } } private IReadOnlyDictionary> GetRequestInfo(IntPtr baseAddress, HttpApiTypes.HTTP_REQUEST_V2* nativeRequest) { var count = nativeRequest->RequestInfoCount; if (count == 0) { return ImmutableDictionary>.Empty; } var info = new Dictionary>(count); for (var i = 0; i < count; i++) { var requestInfo = nativeRequest->pRequestInfo[i]; var offset = (long)requestInfo.pInfo - (long)baseAddress; info.Add( (int)requestInfo.InfoType, _backingBuffer.Memory.Slice((int)offset, (int)requestInfo.InfoLength)); } return new ReadOnlyDictionary>(info); } internal X509Certificate2 GetClientCertificate() { if (_permanentlyPinned) { return GetClientCertificate((IntPtr)_nativeRequest, (HttpApiTypes.HTTP_REQUEST_V2*)_nativeRequest); } else { fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span) { var request = (HttpApiTypes.HTTP_REQUEST_V2*)(pMemoryBlob + _bufferAlignment); return GetClientCertificate(_originalBufferAddress, request); } } } // Throws CryptographicException private X509Certificate2 GetClientCertificate(IntPtr baseAddress, HttpApiTypes.HTTP_REQUEST_V2* nativeRequest) { var request = nativeRequest->Request; long fixup = (byte*)nativeRequest - (byte*)baseAddress; if (request.pSslInfo == null) { return null; } var sslInfo = (HttpApiTypes.HTTP_SSL_INFO*)((byte*)request.pSslInfo + fixup); if (sslInfo->SslClientCertNegotiated == 0 || sslInfo->pClientCertInfo == null) { return null; } var clientCertInfo = (HttpApiTypes.HTTP_SSL_CLIENT_CERT_INFO*)((byte*)sslInfo->pClientCertInfo + fixup); if (clientCertInfo->pCertEncoded == null) { return null; } var clientCert = clientCertInfo->pCertEncoded + fixup; byte[] certEncoded = new byte[clientCertInfo->CertEncodedSize]; Marshal.Copy((IntPtr)clientCert, certEncoded, 0, certEncoded.Length); return new X509Certificate2(certEncoded); } } }