diff options
author | vnugent <public@vaughnnugent.com> | 2023-10-14 15:41:17 -0400 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2023-10-14 15:41:17 -0400 |
commit | 62f9e126912fa9a620a361fb5b88d33506e096fb (patch) | |
tree | 78665fe8516c559821aa4358ca9e2734e475415a /lib/Plugins.Essentials | |
parent | 0f0c991891b6be076a9a367627201eceeb6d354e (diff) |
some refactoring and tests
Diffstat (limited to 'lib/Plugins.Essentials')
6 files changed, 264 insertions, 115 deletions
diff --git a/lib/Plugins.Essentials/src/Accounts/AccountUtils.cs b/lib/Plugins.Essentials/src/Accounts/AccountUtils.cs index 7f3ae40..54deb8c 100644 --- a/lib/Plugins.Essentials/src/Accounts/AccountUtils.cs +++ b/lib/Plugins.Essentials/src/Accounts/AccountUtils.cs @@ -595,25 +595,6 @@ namespace VNLib.Plugins.Essentials.Accounts user.SetValueType(FAILED_LOGIN_ENTRY, value.ToUInt64()); } - /// <summary> - /// Increments the failed login attempt count - /// </summary> - /// <param name="user"></param> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void FailedLoginIncrement(this IUser user) => FailedLoginIncrement(user, DateTimeOffset.UtcNow); - - /// <summary> - /// Increments the failed login attempt count - /// </summary> - /// <param name="user"></param> - /// <param name="time">Explicitly set the time of the counter</param> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void FailedLoginIncrement(this IUser user, DateTimeOffset time) - { - TimestampedCounter current = user.FailedLoginCount(); - user.FailedLoginCount(current.Count + 1, time); - } - #endregion } }
\ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Accounts/FailedLoginLockout.cs b/lib/Plugins.Essentials/src/Accounts/FailedLoginLockout.cs new file mode 100644 index 0000000..a67eee2 --- /dev/null +++ b/lib/Plugins.Essentials/src/Accounts/FailedLoginLockout.cs @@ -0,0 +1,137 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: FailedLoginLockout.cs +* +* FailedLoginLockout.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 VNLib.Plugins.Essentials.Users; + +#nullable enable + +namespace VNLib.Plugins.Essentials.Accounts +{ + /// <summary> + /// Allows tracking of failed login attempts and lockout of accounts + /// </summary> + public class FailedLoginLockout + { + private readonly uint _maxCounts; + private readonly TimeSpan _duration; + + /// <summary> + /// Initializes a new instance of the <see cref="FailedLoginLockout"/> class. + /// </summary> + /// <param name="maxCounts">The max number of failed login attempts before a lockout occurs</param> + /// <param name="maxTimeout">The max duration for a lockout to last</param> + public FailedLoginLockout(uint maxCounts, TimeSpan maxTimeout) + { + _maxCounts = maxCounts; + _duration = maxTimeout; + } + + /// <summary> + /// Increments the lockout counter for the supplied user. If the lockout count + /// has been exceeded, it is not incremented. If the lockout count has expired, + /// it is reset and 0 is returned. + /// </summary> + /// <param name="user">The user to increment the failed login count</param> + /// <param name="now">The current time</param> + /// <returns>The new lockout count after incrementing or 0 if the count was cleared</returns> + public bool IncrementOrClear(IUser user, DateTimeOffset now) + { + //Recover last counter value + TimestampedCounter current = user.FailedLoginCount(); + + //See if the flc timeout period has expired + if (current.LastModified.Add(_duration) < now) + { + //clear flc flag + user.ClearFailedLoginCount(); + return false; + } + + if (current.Count <= _maxCounts) + { + //Increment counter + user.FailedLoginCount(current.Count + 1, now); + return false; + } + + return true; + } + + /// <summary> + /// Checks if the current user's lockout count has been exceeded, or + /// clears a previous lockout if the timeout period has expired. + /// </summary> + /// <param name="user">The user to increment the failed login count</param> + /// <param name="now">The current time</param> + /// <returns>A value that indicates if the count has been exceeded</returns> + public bool CheckOrClear(IUser user, DateTimeOffset now) + { + //Recover last counter value + TimestampedCounter flc = user.FailedLoginCount(); + + //See if the flc timeout period has expired + if (flc.LastModified.Add(_duration) < now) + { + //clear flc flag + user.ClearFailedLoginCount(); + return false; + } + + return flc.Count >= _maxCounts; + } + + /// <summary> + /// Checks if the current user's lockout count has been exceeded + /// </summary> + /// <param name="user">The user to check the counter for</param> + /// <returns>True if the lockout has been exceeded</returns> + public bool IsCountExceeded(IUser user) + { + //Recover last counter value + TimestampedCounter flc = user.FailedLoginCount(); + //Count has been exceeded, and has not timed out yet + return flc.Count >= _maxCounts; + } + + /// <summary> + /// Increments the lockout counter for the supplied user. + /// </summary> + /// <param name="user">The user to increment the count on</param> + /// <param name="now"></param> + public void Increment(IUser user, DateTimeOffset now) + { + //Recover last counter value + TimestampedCounter current = user.FailedLoginCount(); + + //Only increment if the count is less than max counts + if (current.Count <= _maxCounts) + { + //Increment counter + user.FailedLoginCount(current.Count + 1, now); + } + } + } +}
\ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Endpoints/ResourceEndpointBase.cs b/lib/Plugins.Essentials/src/Endpoints/ResourceEndpointBase.cs index dfb94f7..92643d9 100644 --- a/lib/Plugins.Essentials/src/Endpoints/ResourceEndpointBase.cs +++ b/lib/Plugins.Essentials/src/Endpoints/ResourceEndpointBase.cs @@ -199,51 +199,33 @@ namespace VNLib.Plugins.Essentials.Endpoints /// </summary> /// <param name="entity">The entity to be processed</param> /// <returns>The result of the operation to return to the file processor</returns> - protected virtual ValueTask<VfReturnType> PostAsync(HttpEntity entity) - { - return ValueTask.FromResult(Post(entity)); - } + protected virtual ValueTask<VfReturnType> PostAsync(HttpEntity entity) => ValueTask.FromResult(Post(entity)); /// <summary> /// This method gets invoked when an incoming GET request to the endpoint has been requested. /// </summary> /// <param name="entity">The entity to be processed</param> /// <returns>The result of the operation to return to the file processor</returns> - protected virtual ValueTask<VfReturnType> GetAsync(HttpEntity entity) - { - return ValueTask.FromResult(Get(entity)); - } + protected virtual ValueTask<VfReturnType> GetAsync(HttpEntity entity) => ValueTask.FromResult(Get(entity)); /// <summary> /// This method gets invoked when an incoming DELETE request to the endpoint has been requested. /// </summary> /// <param name="entity">The entity to be processed</param> /// <returns>The result of the operation to return to the file processor</returns> - protected virtual ValueTask<VfReturnType> DeleteAsync(HttpEntity entity) - { - return ValueTask.FromResult(Delete(entity)); - } + protected virtual ValueTask<VfReturnType> DeleteAsync(HttpEntity entity) => ValueTask.FromResult(Delete(entity)); /// <summary> /// This method gets invoked when an incoming PUT request to the endpoint has been requested. /// </summary> /// <param name="entity">The entity to be processed</param> /// <returns>The result of the operation to return to the file processor</returns> - protected virtual ValueTask<VfReturnType> PutAsync(HttpEntity entity) - { - return ValueTask.FromResult(Put(entity)); - } + protected virtual ValueTask<VfReturnType> PutAsync(HttpEntity entity) => ValueTask.FromResult(Put(entity)); /// <summary> /// This method gets invoked when an incoming PATCH request to the endpoint has been requested. /// </summary> /// <param name="entity">The entity to be processed</param> /// <returns>The result of the operation to return to the file processor</returns> - protected virtual ValueTask<VfReturnType> PatchAsync(HttpEntity entity) - { - return ValueTask.FromResult(Patch(entity)); - } + protected virtual ValueTask<VfReturnType> PatchAsync(HttpEntity entity) => ValueTask.FromResult(Patch(entity)); - protected virtual ValueTask<VfReturnType> OptionsAsync(HttpEntity entity) - { - return ValueTask.FromResult(Options(entity)); - } + protected virtual ValueTask<VfReturnType> OptionsAsync(HttpEntity entity) => ValueTask.FromResult(Options(entity)); /// <summary> /// Invoked when a request is received for a method other than GET, POST, DELETE, or PUT; @@ -251,20 +233,14 @@ namespace VNLib.Plugins.Essentials.Endpoints /// <param name="entity">The entity that </param> /// <param name="method">The request method</param> /// <returns>The results of the processing</returns> - protected virtual ValueTask<VfReturnType> AlternateMethodAsync(HttpEntity entity, HttpMethod method) - { - return ValueTask.FromResult(AlternateMethod(entity, method)); - } + protected virtual ValueTask<VfReturnType> AlternateMethodAsync(HttpEntity entity, HttpMethod method) => ValueTask.FromResult(AlternateMethod(entity, method)); /// <summary> /// Invoked when the current endpoint received a websocket request /// </summary> /// <param name="entity">The entity that requested the websocket</param> /// <returns>The results of the operation</returns> - protected virtual ValueTask<VfReturnType> WebsocketRequestedAsync(HttpEntity entity) - { - return ValueTask.FromResult(WebsocketRequested(entity)); - } + protected virtual ValueTask<VfReturnType> WebsocketRequestedAsync(HttpEntity entity) => ValueTask.FromResult(WebsocketRequested(entity)); /// <summary> /// This method gets invoked when an incoming POST request to the endpoint has been requested. @@ -329,10 +305,7 @@ namespace VNLib.Plugins.Essentials.Endpoints return VfReturnType.VirtualSkip; } - protected virtual VfReturnType Options(HttpEntity entity) - { - return VfReturnType.Forbidden; - } + protected virtual VfReturnType Options(HttpEntity entity) => VfReturnType.Forbidden; /// <summary> /// Invoked when the current endpoint received a websocket request @@ -344,5 +317,101 @@ namespace VNLib.Plugins.Essentials.Endpoints entity.CloseResponse(HttpStatusCode.Forbidden); return VfReturnType.VirtualSkip; } + + /// <summary> + /// Shortcut helper methods to a virtual skip response with a 200 OK status code + /// </summary> + /// <param name="entity">The entity to close the connection for</param> + /// <returns>The <see cref="VfReturnType.VirtualSkip"/> operation result</returns> + public static VfReturnType VirtualOk(HttpEntity entity) => VirtualClose(entity, HttpStatusCode.OK); + + /// <summary> + /// Shortcut helper methods to a virtual skip response with a 200 OK status code + /// that returns a <see cref="WebMessage"/> json response + /// </summary> + /// <param name="entity">The entity to close the connection for</param> + /// <param name="webm">The <see cref="WebMessage"/> json response</param> + /// <returns>The <see cref="VfReturnType.VirtualSkip"/> operation result</returns> + public static VfReturnType VirtualOk<T>(HttpEntity entity, T webm) where T: WebMessage + { + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + + /// <summary> + /// Shortcut helper methods to a virtual skip response with a 200 OK status code + /// that returns a <see cref="WebMessage"/> json response + /// </summary> + /// <param name="entity">The entity to close the connection for</param> + /// <param name="jsonValue">A object that will be serialized and returned to the client as JSON</param> + /// <returns>The <see cref="VfReturnType.VirtualSkip"/> operation result</returns> + public static VfReturnType VirtualOkJson<T>(HttpEntity entity, T jsonValue) => VirtualCloseJson(entity, jsonValue, HttpStatusCode.OK); + + /// <summary> + /// Shortcut helper methods to a virtual skip response with a 200 OK status code + /// that returns a <see cref="WebMessage"/> json response + /// </summary> + /// <param name="entity">The entity to close the connection for</param> + /// <param name="webm">The <see cref="WebMessage"/> json response</param> + /// <param name="code">The status code to return to the client</param> + /// <returns>The <see cref="VfReturnType.VirtualSkip"/> operation result</returns> + public static VfReturnType VirtualClose<T>(HttpEntity entity, T webm, HttpStatusCode code) where T: WebMessage => VirtualCloseJson(entity, webm, code); + + /// <summary> + /// Shortcut helper methods to a virtual skip response with a given status code + /// </summary> + /// <param name="entity">The entity to close the connection for</param> + /// <param name="code">The status code to return to the client</param> + /// <returns>The <see cref="VfReturnType.VirtualSkip"/> operation result</returns> + public static VfReturnType VirtualClose(HttpEntity entity, HttpStatusCode code) + { + entity.CloseResponse(code); + return VfReturnType.VirtualSkip; + } + + /// <summary> + /// Shortcut helper methods to a virtual skip response with a given status code, + /// and memory content to return to the client + /// </summary> + /// <param name="entity">The entity to close the connection for</param> + /// <param name="code">The status code to return to the client</param> + /// <param name="ct">The response content type</param> + /// <param name="response">The memory response to return to the user</param> + /// <returns>The <see cref="VfReturnType.VirtualSkip"/> operation result</returns> + public static VfReturnType VirtualClose(HttpEntity entity, HttpStatusCode code, ContentType ct, IMemoryResponseReader response) + { + entity.CloseResponse(code, ct, response); + return VfReturnType.VirtualSkip; + } + + /// <summary> + /// Shortcut helper methods to a virtual skip response with a given status code, + /// and memory content to return to the client + /// </summary> + /// <param name="entity">The entity to close the connection for</param> + /// <param name="code">The status code to return to the client</param> + /// <param name="ct">The response content type</param> + /// <param name="response">The stream response to return to the user</param> + /// <returns>The <see cref="VfReturnType.VirtualSkip"/> operation result</returns> + public static VfReturnType VirtualClose(HttpEntity entity, HttpStatusCode code, ContentType ct, Stream response) + { + entity.CloseResponse(code, ct, response); + return VfReturnType.VirtualSkip; + } + + /// <summary> + /// Shortcut helper methods to a virtual skip response with a given status code, and + /// a json payload to return to the client + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="entity">The entity to close the connection for</param> + /// <param name="code">The status code to return to the client</param> + /// <param name="jsonValue">A object that will be serialized and returned to the client as JSON</param> + /// <returns>The <see cref="VfReturnType.VirtualSkip"/> operation result</returns> + public static VfReturnType VirtualCloseJson<T>(HttpEntity entity, T jsonValue, HttpStatusCode code) + { + entity.CloseResponseJson(code, jsonValue); + return VfReturnType.VirtualSkip; + } } }
\ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs b/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs index f0cc46a..393c838 100644 --- a/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs +++ b/lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs @@ -112,10 +112,23 @@ namespace VNLib.Plugins.Essentials.Extensions /// Determines if the connection accepts any content type /// </summary> /// <returns>true if the connection accepts any content typ, false otherwise</returns> - private static bool AcceptsAny(this IConnectionInfo server) + private static bool AcceptsAny(IConnectionInfo server) { - //Accept any if no accept header was present, or accept all value */* - return server.Accept.Count == 0 || server.Accept.Where(static t => t.StartsWith("*/*", StringComparison.OrdinalIgnoreCase)).Any(); + if(server.Accept.Count == 0) + { + return true; + } + + //Search list for accept any + foreach(string accept in server.Accept) + { + if(accept.StartsWith("*/*", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; } /// <summary> @@ -222,10 +235,7 @@ namespace VNLib.Plugins.Essentials.Extensions /// code relies on the port number of the <see cref="ConnectionInfo.RequestUri"/> /// </remarks> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool EnpointPortsMatch(this IConnectionInfo server) - { - return server.RequestUri.Port == server.LocalEndpoint.Port; - } + public static bool EnpointPortsMatch(this IConnectionInfo server) => server.RequestUri.Port == server.LocalEndpoint.Port; /// <summary> /// Determines if the host of the current request URI matches the referer header host /// </summary> @@ -384,7 +394,6 @@ namespace VNLib.Plugins.Essentials.Extensions /// <param name="server"></param> /// <param name="isTrusted"></param> /// <returns>The real ip of the connection</returns> - [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static IPAddress GetTrustedIp(this IConnectionInfo server, bool isTrusted) { //If the connection is not trusted, then ignore header parsing @@ -400,7 +409,7 @@ namespace VNLib.Plugins.Essentials.Extensions return server.RemoteEndpoint.Address; } } - + /// <summary> /// Gets a value that determines if the connection is using tls, locally /// or behind a trusted downstream server that is using tls. @@ -408,13 +417,8 @@ namespace VNLib.Plugins.Essentials.Extensions /// <param name="server"></param> /// <returns>True if the connection is secure, false otherwise</returns> [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsSecure(this IConnectionInfo server) - { - //Get value of the trusted downstream server - return IsSecure(server, server.IsBehindDownStreamServer()); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsSecure(this IConnectionInfo server) => IsSecure(server, server.IsBehindDownStreamServer()); + internal static bool IsSecure(this IConnectionInfo server, bool isTrusted) { //If the connection is not trusted, then ignore header parsing diff --git a/lib/Plugins.Essentials/src/Middleware/HttpMiddlewareResult.cs b/lib/Plugins.Essentials/src/Middleware/HttpMiddlewareResult.cs deleted file mode 100644 index 6054a6e..0000000 --- a/lib/Plugins.Essentials/src/Middleware/HttpMiddlewareResult.cs +++ /dev/null @@ -1,42 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials -* File: HttpMiddlewareResult.cs -* -* HttpMiddlewareResult.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/. -*/ - -namespace VNLib.Plugins.Essentials.Middleware -{ - /// <summary> - /// The result of a <see cref="IHttpMiddleware"/> process. - /// </summary> - public enum HttpMiddlewareResult - { - /// <summary> - /// The request has not been completed and should continue to be processed. - /// </summary> - Continue, - - /// <summary> - /// The request has been handled and no further processing should occur. - /// </summary> - Complete - } -} diff --git a/lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs b/lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs index 4485c55..3c56866 100644 --- a/lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs +++ b/lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs @@ -34,8 +34,8 @@ namespace VNLib.Plugins.Essentials.Middleware public interface IHttpMiddleware { /// <summary> - /// Processes the <see cref="HttpEntity"/> and returns a <see cref="HttpMiddlewareResult"/> - /// indicating whether the request should continue to be processed. + /// Processes the <see cref="HttpEntity"/> and returns a <see cref="FileProcessArgs"/> + /// indicating the result of the process operation /// </summary> /// <param name="entity">The entity to process</param> /// <returns>The result of the operation</returns> |