/* * Copyright (c) 2022 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials * File: ResourceEndpointBase.cs * * ResourceEndpointBase.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.Net.Sockets; using System.Threading.Tasks; using VNLib.Net.Http; using VNLib.Utils; using VNLib.Utils.Logging; using VNLib.Plugins.Essentials.Extensions; namespace VNLib.Plugins.Essentials.Endpoints { /// /// Provides a base class for implementing un-authenticated resource endpoints /// with basic (configurable) security checks /// public abstract class ResourceEndpointBase : VirtualEndpoint { /// /// Default protection settings. Protection settings are the most /// secure by default, should be loosened an necessary /// protected virtual ProtectionSettings EndpointProtectionSettings { get; } /// public override async ValueTask Process(HttpEntity entity) { try { ERRNO preProc = PreProccess(entity); if (preProc == ERRNO.E_FAIL) { return VfReturnType.Forbidden; } //Entity was responded to by the pre-processor if (preProc < 0) { return VfReturnType.VirtualSkip; } //If websockets are quested allow them to be processed in a logged-in/secure context if (entity.Server.IsWebSocketRequest) { return await WebsocketRequestedAsync(entity); } ValueTask op = entity.Server.Method switch { //Get request to get account HttpMethod.GET => GetAsync(entity), HttpMethod.POST => PostAsync(entity), HttpMethod.DELETE => DeleteAsync(entity), HttpMethod.PUT => PutAsync(entity), HttpMethod.PATCH => PatchAsync(entity), HttpMethod.OPTIONS => OptionsAsync(entity), _ => AlternateMethodAsync(entity, entity.Server.Method), }; return await op; } catch (InvalidJsonRequestException ije) { //Write the je to debug log Log.Debug(ije, "Failed to de-serialize a request entity body"); //If the method is not POST/PUT/PATCH return a json message if ((entity.Server.Method & (HttpMethod.HEAD | HttpMethod.OPTIONS | HttpMethod.TRACE | HttpMethod.DELETE)) > 0) { return VfReturnType.BadRequest; } //Only allow if json is an accepted response type if (!entity.Server.Accepts(ContentType.Json)) { return VfReturnType.BadRequest; } //Build web-message WebMessage webm = new() { Result = "Request body is not valid json" }; //Set the response webm entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); //return virtual return VfReturnType.VirtualSkip; } catch (TerminateConnectionException) { //A TC exception is intentional and should always propagate to the runtime throw; } catch (ContentTypeUnacceptableException) { /* * The runtime will handle a 406 unaccetptable response * and invoke the proper error app handler */ throw; } //Re-throw exceptions that are cause by reading the transport layer catch (IOException ioe) when (ioe.InnerException is SocketException) { throw; } catch (Exception ex) { //Log an uncaught excetpion and return an error code (log may not be initialized) Log?.Error(ex); return VfReturnType.Error; } } /// /// Allows for synchronous Pre-Processing of an entity. The result /// will determine if the method processing methods will be invoked, or /// a error code will be returned /// /// The incomming request to process /// /// True if processing should continue, false if the response should be /// , less than 0 if entity was /// responded to. /// protected virtual ERRNO PreProccess(HttpEntity entity) { //Disable cache if requested if (!EndpointProtectionSettings.EnableCaching) { entity.Server.SetNoCache(); } //Enforce TLS if (!EndpointProtectionSettings.DisabledTlsRequired && !entity.IsSecure && !entity.IsLocalConnection) { return false; } //Enforce browser check if (!EndpointProtectionSettings.DisableBrowsersOnly && !entity.Server.IsBrowser()) { return false; } //Enforce refer check if (!EndpointProtectionSettings.DisableRefererMatch && entity.Server.Referer != null && !entity.Server.RefererMatch()) { return false; } //enforce session basic if (!EndpointProtectionSettings.DisableSessionsRequired && (!entity.Session.IsSet || entity.Session.IsNew)) { return false; } /* * If sessions are required, verify cors is set, and the client supplied an origin header, * verify that it matches the origin that was specified during session initialization */ if ((!EndpointProtectionSettings.DisableSessionsRequired & !EndpointProtectionSettings.DisableVerifySessionCors) && entity.Server.Origin != null && !entity.Session.CrossOriginMatch) { return false; } //Enforce cross-site if (!EndpointProtectionSettings.DisableCrossSiteDenied && entity.Server.IsCrossSite()) { return false; } return true; } /// /// This method gets invoked when an incoming POST request to the endpoint has been requested. /// /// The entity to be processed /// The result of the operation to return to the file processor protected virtual ValueTask PostAsync(HttpEntity entity) { return ValueTask.FromResult(Post(entity)); } /// /// This method gets invoked when an incoming GET request to the endpoint has been requested. /// /// The entity to be processed /// The result of the operation to return to the file processor protected virtual ValueTask GetAsync(HttpEntity entity) { return ValueTask.FromResult(Get(entity)); } /// /// This method gets invoked when an incoming DELETE request to the endpoint has been requested. /// /// The entity to be processed /// The result of the operation to return to the file processor protected virtual ValueTask DeleteAsync(HttpEntity entity) { return ValueTask.FromResult(Delete(entity)); } /// /// This method gets invoked when an incoming PUT request to the endpoint has been requested. /// /// The entity to be processed /// The result of the operation to return to the file processor protected virtual ValueTask PutAsync(HttpEntity entity) { return ValueTask.FromResult(Put(entity)); } /// /// This method gets invoked when an incoming PATCH request to the endpoint has been requested. /// /// The entity to be processed /// The result of the operation to return to the file processor protected virtual ValueTask PatchAsync(HttpEntity entity) { return ValueTask.FromResult(Patch(entity)); } protected virtual ValueTask OptionsAsync(HttpEntity entity) { return ValueTask.FromResult(Options(entity)); } /// /// Invoked when a request is received for a method other than GET, POST, DELETE, or PUT; /// /// The entity that /// The request method /// The results of the processing protected virtual ValueTask AlternateMethodAsync(HttpEntity entity, HttpMethod method) { return ValueTask.FromResult(AlternateMethod(entity, method)); } /// /// Invoked when the current endpoint received a websocket request /// /// The entity that requested the websocket /// The results of the operation protected virtual ValueTask WebsocketRequestedAsync(HttpEntity entity) { return ValueTask.FromResult(WebsocketRequested(entity)); } /// /// This method gets invoked when an incoming POST request to the endpoint has been requested. /// /// The entity to be processed /// The result of the operation to return to the file processor protected virtual VfReturnType Post(HttpEntity entity) { //Return method not allowed entity.CloseResponse(HttpStatusCode.MethodNotAllowed); return VfReturnType.VirtualSkip; } /// /// This method gets invoked when an incoming GET request to the endpoint has been requested. /// /// The entity to be processed /// The result of the operation to return to the file processor protected virtual VfReturnType Get(HttpEntity entity) { return VfReturnType.ProcessAsFile; } /// /// This method gets invoked when an incoming DELETE request to the endpoint has been requested. /// /// The entity to be processed /// The result of the operation to return to the file processor protected virtual VfReturnType Delete(HttpEntity entity) { entity.CloseResponse(HttpStatusCode.MethodNotAllowed); return VfReturnType.VirtualSkip; } /// /// This method gets invoked when an incoming PUT request to the endpoint has been requested. /// /// The entity to be processed /// The result of the operation to return to the file processor protected virtual VfReturnType Put(HttpEntity entity) { entity.CloseResponse(HttpStatusCode.MethodNotAllowed); return VfReturnType.VirtualSkip; } /// /// This method gets invoked when an incoming PATCH request to the endpoint has been requested. /// /// The entity to be processed /// The result of the operation to return to the file processor protected virtual VfReturnType Patch(HttpEntity entity) { entity.CloseResponse(HttpStatusCode.MethodNotAllowed); return VfReturnType.VirtualSkip; } /// /// Invoked when a request is received for a method other than GET, POST, DELETE, or PUT; /// /// The entity that /// The request method /// The results of the processing protected virtual VfReturnType AlternateMethod(HttpEntity entity, HttpMethod method) { //Return method not allowed entity.CloseResponse(HttpStatusCode.MethodNotAllowed); return VfReturnType.VirtualSkip; } protected virtual VfReturnType Options(HttpEntity entity) { return VfReturnType.Forbidden; } /// /// Invoked when the current endpoint received a websocket request /// /// The entity that requested the websocket /// The results of the operation protected virtual VfReturnType WebsocketRequested(HttpEntity entity) { entity.CloseResponse(HttpStatusCode.Forbidden); return VfReturnType.VirtualSkip; } } }