aboutsummaryrefslogtreecommitdiff
path: root/lib/Plugins.Essentials
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-10-14 15:41:17 -0400
committerLibravatar vnugent <public@vaughnnugent.com>2023-10-14 15:41:17 -0400
commit62f9e126912fa9a620a361fb5b88d33506e096fb (patch)
tree78665fe8516c559821aa4358ca9e2734e475415a /lib/Plugins.Essentials
parent0f0c991891b6be076a9a367627201eceeb6d354e (diff)
some refactoring and tests
Diffstat (limited to 'lib/Plugins.Essentials')
-rw-r--r--lib/Plugins.Essentials/src/Accounts/AccountUtils.cs19
-rw-r--r--lib/Plugins.Essentials/src/Accounts/FailedLoginLockout.cs137
-rw-r--r--lib/Plugins.Essentials/src/Endpoints/ResourceEndpointBase.cs141
-rw-r--r--lib/Plugins.Essentials/src/Extensions/ConnectionInfoExtensions.cs36
-rw-r--r--lib/Plugins.Essentials/src/Middleware/HttpMiddlewareResult.cs42
-rw-r--r--lib/Plugins.Essentials/src/Middleware/IHttpMiddleware.cs4
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>