/* * Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials * File: HttpEntity.cs * * HttpEntity.cs is part of VNLib.Plugins.Essentials which is part of the larger * VNLib collection of libraries and utilities. * * VNLib.Plugins.Essentials is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * VNLib.Plugins.Essentials is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ using System; using System.IO; using System.Net; using System.Threading; using System.Diagnostics; using System.Collections.Generic; using System.Runtime.CompilerServices; using VNLib.Utils.IO; using VNLib.Net.Http; using VNLib.Plugins.Essentials.Content; using VNLib.Plugins.Essentials.Sessions; using VNLib.Plugins.Essentials.Extensions; /* * HttpEntity was converted to an object as during profiling * it was almost always heap allcated due to async opertaions * or other object tracking issues. So to reduce the number of * allocations (at the cost of larger objects) basic profiling * showed less GC load and less collections when SessionInfo * remained a value type */ #pragma warning disable CA1051 // Do not declare visible instance fields namespace VNLib.Plugins.Essentials { /// /// A container for an with its attached session. /// This class cannot be inherited. /// public sealed class HttpEntity : IHttpEvent, IDisposable { /// /// The connection event entity /// private readonly IHttpEvent Entity; private readonly CancellationTokenSource EventCts; /// /// Creates a new instance with the optional /// session handle. If the session handle is set, the session will be /// attached to the entity /// /// The event to parse and wrap /// The processor the connection has originated from /// An optional session handle to attach to the entity public HttpEntity(IHttpEvent evnt, IWebProcessor root, ref readonly SessionHandle session) :this(evnt, root) { //Assign optional session and attempt to attach it EventSessionHandle = session; AttachSession(); } internal HttpEntity(IHttpEvent entity, IWebProcessor root) { Entity = entity; RequestedRoot = root; //Init event cts EventCts = new(root.Options.ExecutionTimeout); //See if the connection is coming from an downstream server IsBehindDownStreamServer = root.Options.DownStreamServers.Contains(entity.Server.RemoteEndpoint.Address); /* * If the connection was behind a trusted downstream server, * we can trust the x-forwarded-for header, * otherwise use the remote ep ip address */ TrustedRemoteIp = entity.Server.GetTrustedIp(IsBehindDownStreamServer); //Local connection IsLocalConnection = entity.Server.LocalEndpoint.Address.IsLocalSubnet(TrustedRemoteIp); //Cache value IsSecure = entity.Server.IsSecure(IsBehindDownStreamServer); //Cache current time RequestedTimeUtc = DateTimeOffset.UtcNow; } private SessionInfo _session; internal FileProcessArgs EventArgs; internal SessionHandle EventSessionHandle; /// /// Internal call to attach a new session to the entity from the /// internal session handle /// internal void AttachSession() { if (EventSessionHandle.IsSet) { _session = new(EventSessionHandle.SessionData!, Entity.Server, TrustedRemoteIp); } } /// /// Cleans up internal resources /// public void Dispose() => EventCts.Dispose(); /// /// A token that has a scheduled timeout to signal the cancellation of the entity event /// public CancellationToken EventCancellation => EventCts.Token; /// /// The session associated with the event /// public ref readonly SessionInfo Session => ref _session; /// /// A value that indicates if the connecion came from a trusted downstream server /// public readonly bool IsBehindDownStreamServer; /// /// Determines if the connection came from the local network to the current server /// public readonly bool IsLocalConnection; /// /// Gets a value that determines if the connection is using tls, locally /// or behind a trusted downstream server that is using tls. /// public readonly bool IsSecure; /// /// Caches a that was created when the connection was created. /// The approximate current UTC time /// public readonly DateTimeOffset RequestedTimeUtc; /// /// The connection info object assocated with the entity /// public IConnectionInfo Server => Entity.Server; /// /// User's ip. If the connection is behind a local proxy, returns the users actual IP. Otherwise returns the connection ip. /// public readonly IPAddress TrustedRemoteIp; /// /// The requested web root. Provides additional site information /// public readonly IWebProcessor RequestedRoot; /// /// If the request has query arguments they are stored in key value format /// public IReadOnlyDictionary QueryArgs => Entity.QueryArgs; /// /// If the request body has form data or url encoded arguments they are stored in key value format /// public IReadOnlyDictionary RequestArgs => Entity.RequestArgs; /// /// Contains all files upladed with current request /// public IReadOnlyList Files => Entity.Files; /// IHttpServer IHttpEvent.OriginServer => Entity.OriginServer; /// /// Complete the session and respond to user /// /// Status code of operation /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void CloseResponse(HttpStatusCode code) => Entity.CloseResponse(code); /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void CloseResponse(HttpStatusCode code, ContentType type, Stream stream, long length) { //Verify content type matches if (!Server.Accepts(type)) { throw new ContentTypeUnacceptableException("The client does not accept the content type of the response"); } /* * If the underlying stream is actaully a memory stream, * create a wrapper for it to read as a memory response. * This is done to avoid a user-space copy since we can * get access to access the internal buffer * * Stream length also should not cause an integer overflow, * which also mean position is assumed not to overflow * or cause an overflow during reading * * Finally not all memory streams allow fetching the internal * buffer, so check that it can be aquired. */ if (stream is MemoryStream ms && length < int.MaxValue && ms.TryGetBuffer(out ArraySegment arrSeg) ) { Entity.CloseResponse( code, type, entity: new MemStreamWrapper(in arrSeg, ms, (int)length) ); return; } /* * Readonly vn streams can also use a shortcut to avoid http buffer allocation and * async streaming. This is done by wrapping the stream in a memory response reader * * Allocating a memory manager requires that the stream is readonly */ if (stream is VnMemoryStream vms && length < int.MaxValue) { Entity.CloseResponse( code, type, entity: new VnStreamWrapper(vms, (int)length) ); return; } /* * Files can have a bit more performance using the RandomAccess library when reading * sequential segments without buffering. It avoids a user-space copy and async reading * performance without the file being opened as async. */ if(stream is FileStream fs) { Entity.CloseResponse( code, type, entity: new DirectFileStream(fs.SafeFileHandle), length ); return; } Entity.CloseResponse(code, type, stream, length); } /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void CloseResponse(HttpStatusCode code, ContentType type, IMemoryResponseReader entity) { //Verify content type matches if (!Server.Accepts(type)) { throw new ContentTypeUnacceptableException("The client does not accept the content type of the response"); } Entity.CloseResponse(code, type, entity); } /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void CloseResponse(HttpStatusCode code, ContentType type, IHttpStreamResponse stream, long length) { //Verify content type matches if (!Server.Accepts(type)) { throw new ContentTypeUnacceptableException("The client does not accept the content type of the response"); } Entity.CloseResponse(code, type, stream, length); } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetControlFlag(ulong mask) => Entity.SetControlFlag(mask); /* * Do not directly expose dangerous methods, but allow them to be called */ /// [MethodImpl(MethodImplOptions.AggressiveInlining)] void IHttpEvent.DangerousChangeProtocol(IAlternateProtocol protocolHandler) => Entity.DangerousChangeProtocol(protocolHandler); private sealed class VnStreamWrapper(VnMemoryStream memStream, int length) : IMemoryResponseReader { //Store memory buffer, causes an internal allocation, so avoid calling mutliple times readonly ReadOnlyMemory _memory = memStream.AsMemory(); readonly int length = length; /* * Stream may be offset by the caller, it needs * to be respected during streaming. */ int read = (int)memStream.Position; /// public int Remaining { get { Debug.Assert(length - read >= 0); return length - read; } } /// public void Advance(int written) => read += written; /// public void Close() => memStream.Dispose(); /// public ReadOnlyMemory GetMemory() => _memory.Slice(read, Remaining); } private sealed class MemStreamWrapper(ref readonly ArraySegment data, MemoryStream stream, int length) : IMemoryResponseReader { readonly ArraySegment _data = data; readonly int length = length; /* * Stream may be offset by the caller, it needs * to be respected during streaming. */ int read = (int)stream.Position; /// public int Remaining { get { Debug.Assert(length - read >= 0); return length - read; } } /// public void Advance(int written) => read += written; /// public void Close() => stream.Dispose(); /// public ReadOnlyMemory GetMemory() => _data.AsMemory(read, Remaining); } } }