/*
* 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;
}
}
}