diff --git a/src/.dockerignore b/src/.dockerignore index fe1152b..80e10b6 100644 --- a/src/.dockerignore +++ b/src/.dockerignore @@ -10,6 +10,7 @@ **/.vscode **/*.*proj.user **/*.dbmdl +**/*.http **/*.jfm **/azds.yaml **/bin @@ -19,10 +20,11 @@ **/node_modules **/npm-debug.log **/obj +**/Properties +**/README.md **/secrets.dev.yaml **/values.dev.yaml LICENSE -README.md !**/.gitignore !.git/HEAD !.git/config diff --git a/src/CrackSharp.Api/Common/AppJsonSerializerContext.cs b/src/CrackSharp.Api/Common/AppJsonSerializerContext.cs new file mode 100644 index 0000000..f87c022 --- /dev/null +++ b/src/CrackSharp.Api/Common/AppJsonSerializerContext.cs @@ -0,0 +1,8 @@ +using System.Text.Json.Serialization; + +namespace CrackSharp.Api.Common; + +[JsonSerializable(typeof(HttpValidationProblemDetails))] +internal partial class AppJsonSerializerContext : JsonSerializerContext +{ +} diff --git a/src/CrackSharp.Api/Common/Services/AwaitableMemoryCache.cs b/src/CrackSharp.Api/Common/Services/AwaitableMemoryCache.cs index ef709f3..3074249 100644 --- a/src/CrackSharp.Api/Common/Services/AwaitableMemoryCache.cs +++ b/src/CrackSharp.Api/Common/Services/AwaitableMemoryCache.cs @@ -30,8 +30,7 @@ public Task AwaitValueAsync(TKey key, CancellationToken cancellationToke awaiter.Task.ContinueWith(task => awaiters.TryRemove(key, out _), TaskScheduler.Default); return new(awaiter, taskCompletionSource); }), - _awaiters) - .Value; + _awaiters).Value; if (TryGetValue(key, out value)) taskCompletionSource.TrySetResult(value); diff --git a/src/CrackSharp.Api/CrackSharp.Api.csproj b/src/CrackSharp.Api/CrackSharp.Api.csproj index e7934cd..c2f9b21 100644 --- a/src/CrackSharp.Api/CrackSharp.Api.csproj +++ b/src/CrackSharp.Api/CrackSharp.Api.csproj @@ -5,6 +5,8 @@ enable enable latest + true + true @@ -12,8 +14,8 @@ - - + + diff --git a/src/CrackSharp.Api/CrackSharp.Api.http b/src/CrackSharp.Api/CrackSharp.Api.http index 98bf5cc..d7544f1 100644 --- a/src/CrackSharp.Api/CrackSharp.Api.http +++ b/src/CrackSharp.Api/CrackSharp.Api.http @@ -1,17 +1,73 @@ -@CrackSharp.Api_HostAddress = https://localhost:5001 +@CrackSharp_Api_HostAddress = http://localhost:5000 -GET {{CrackSharp.Api_HostAddress}}/api/v1/des/decrypt/FAzlTwVAZ1NZ2 +GET {{CrackSharp_Api_HostAddress}}/api/v1/des/decrypt/FAzlTwVAZ1NZ2 ### 200 "LOL" -GET {{CrackSharp.Api_HostAddress}}/api/v1/des/decrypt/FAzlTwVAZ1NZ2?chars=LMNO&maxTextLength=4 +GET {{CrackSharp_Api_HostAddress}}/api/v1/des/decrypt/FAzlTwVAZ1NZ2?chars=LMNO&maxTextLength=4 ### 200 "LOL" -GET {{CrackSharp.Api_HostAddress}}/api/v1/des/encrypt/abc +GET {{CrackSharp_Api_HostAddress}}/api/v1/des/decrypt/FAzlTwVAZ1NZ2?chars=LMNO + +### 200 "LOL" + +GET {{CrackSharp_Api_HostAddress}}/api/v1/des/decrypt/FAzlTwVAZ1NZ2?maxTextLength=4 + +### 200 "LOL" + +GET {{CrackSharp_Api_HostAddress}}/api/v1/des/encrypt/abc ### 200 DES hash based on random salt -GET {{CrackSharp.Api_HostAddress}}/api/v1/des/encrypt/abc?salt=50 +GET {{CrackSharp_Api_HostAddress}}/api/v1/des/encrypt/abc?salt=50 ### 200 "50PaJ4.RO0YUo" + + + +GET {{CrackSharp_Api_HostAddress}}/api/v1/des/decrypt + +### 400 "Required parameter "string hash" was not provided from route or query string." + +GET {{CrackSharp_Api_HostAddress}}/api/v1/des/decrypt/FAzlTwVAZ1NZ + +### 400 "hash": [ "Value cannot be null or empty and must follow pattern ^[./0-9A-Za-z]{13}$" ] + +GET {{CrackSharp_Api_HostAddress}}/api/v1/des/decrypt/FAzlTwVAZ1NZ2?maxTextLength=9 + +### 400 "maxTextLength": [ "Value must be greater than 0 and less than 9." ] + +GET {{CrackSharp_Api_HostAddress}}/api/v1/des/decrypt/FAzlTwVAZ1NZ2?chars=-_ + +### 400 "chars": [ "Value cannot be null or empty and must follow pattern ^[./0-9A-Za-z]+$" ] + +GET {{CrackSharp_Api_HostAddress}}/api/v1/des/decrypt/FAzlTwVAZ1NZ?maxTextLength=9&chars=-_ + +### 400 Multiple error messages for parameters + +GET {{CrackSharp_Api_HostAddress}}/api/v1/des/encrypt + +### 400 Required parameter "string text" was not provided from route or query string. + +GET {{CrackSharp_Api_HostAddress}}/api/v1/des/encrypt/-_ + +### 400 "text": [ "Value cannot be null or empty and must follow pattern ^[./0-9A-Za-z]+$" ] + +GET {{CrackSharp_Api_HostAddress}}/api/v1/des/encrypt/abc?salt=_ + +### 400 "salt": [ "Value must follow pattern ^[./0-9A-Za-z]{2}$" ] + +GET {{CrackSharp_Api_HostAddress}}/api/v1/des/encrypt/abc?salt=def + +### 400 "salt": [ "Value must follow pattern ^[./0-9A-Za-z]{2}$" ] + + + +GET {{CrackSharp_Api_HostAddress}}/api/v1/des/decrypt/FAzlTwVAZ1NZ2?maxTextLength=2 + +### 404 Not Found + +GET {{CrackSharp_Api_HostAddress}}/api/v1/des/decrypt/FAzlTwVAZ1NZ2?chars=L + +### 404 Not Found \ No newline at end of file diff --git a/src/CrackSharp.Api/Des/Endpoints/DesApi.cs b/src/CrackSharp.Api/Des/Endpoints/DesApi.cs index 50e7b88..acd1559 100644 --- a/src/CrackSharp.Api/Des/Endpoints/DesApi.cs +++ b/src/CrackSharp.Api/Des/Endpoints/DesApi.cs @@ -2,6 +2,7 @@ using CrackSharp.Api.Des.Endpoints.Filters; using CrackSharp.Core; using Microsoft.AspNetCore.Http.HttpResults; +using System.ComponentModel.DataAnnotations; namespace CrackSharp.Api.Des.Endpoints; @@ -23,13 +24,14 @@ public static IEndpointRouteBuilder MapDesApi(this IEndpointRouteBuilder app) } private static async Task, NotFound, StatusCodeHttpResult>> Decrypt( - [AsParameters] DesDecryptRequest request, [AsParameters] DesDecryptServices services, - CancellationToken cancellationToken) + [RegularExpression("^[./0-9A-Za-z]{13}$")] string hash, + [Range(1, 8)] int maxTextLength = 8, + [RegularExpression("^[./0-9A-Za-z]+$")] string chars = DesConstants.DecryptDefaultChars, + CancellationToken cancellationToken = default) { var decryptionService = services.DecryptionService; var logger = services.Logger; - var (hash, maxTextLength, chars) = request; const string partialMessage = $"Decryption of the {nameof(hash)} '{{{nameof(hash)}}}' " + $"with {nameof(maxTextLength)} {{{nameof(maxTextLength)}}} and {nameof(chars)} '{{{nameof(chars)}}}'"; @@ -37,7 +39,7 @@ private static async Task, NotFound, StatusCodeHttpResult>> D try { logger.LogInformation($"{partialMessage} requested.", hash, maxTextLength, chars); - var decrypted = await decryptionService.DecryptAsync(request, cancellationToken); + var decrypted = await decryptionService.DecryptAsync(new(hash, maxTextLength, chars) , cancellationToken); logger.LogInformation($"{partialMessage} succeeded.", hash, maxTextLength, chars); return TypedResults.Ok(decrypted); @@ -60,12 +62,12 @@ private static async Task, NotFound, StatusCodeHttpResult>> D } private static Ok Encrypt( - [AsParameters] DesEncryptRequest request, - [AsParameters] DesEncryptServices services) + [AsParameters] DesEncryptServices services, + [RegularExpression("^[./0-9A-Za-z]+$")] string text, + [RegularExpression("^[./0-9A-Za-z]{2}$")] string? salt = null) { var encryptionService = services.EncryptionService; var logger = services.Logger; - var (text, salt) = request; const string partialMessage = $"Encryption of the {nameof(text)} '{{{nameof(text)}}}' " + $"with {nameof(salt)} '{{{nameof(salt)}}}'"; diff --git a/src/CrackSharp.Api/Des/Endpoints/Filters/DesValidationFilters.cs b/src/CrackSharp.Api/Des/Endpoints/Filters/DesValidationFilters.cs index 141a95e..ed236e3 100644 --- a/src/CrackSharp.Api/Des/Endpoints/Filters/DesValidationFilters.cs +++ b/src/CrackSharp.Api/Des/Endpoints/Filters/DesValidationFilters.cs @@ -26,11 +26,13 @@ internal static class DesValidationFilters EndpointFilterInvocationContext context, EndpointFilterDelegate next) { - var request = context.GetArgument(0); - request = request with { Hash = WebUtility.UrlDecode(request.Hash) }; - context.Arguments[0] = request; + var hash = context.GetArgument(1); + var maxTextLength = context.GetArgument(2); + var chars = context.GetArgument(3); + + hash = WebUtility.UrlDecode(hash); + context.Arguments[1] = hash; - var (hash, maxTextLength, chars) = request; Dictionary? errors = null; if (hash is null || !_hashValidator.IsMatch(hash)) @@ -49,11 +51,12 @@ internal static class DesValidationFilters EndpointFilterInvocationContext context, EndpointFilterDelegate next) { - var request = context.GetArgument(0); - request = request with { Text = WebUtility.UrlDecode(request.Text) }; - context.Arguments[0] = request; + var text = context.GetArgument(1); + var salt = context.GetArgument(2); + + text = WebUtility.UrlDecode(text); + context.Arguments[1] = text; - var (text, salt) = request; Dictionary? errors = null; if (text is null || !_charsValidator.IsMatch(text)) diff --git a/src/CrackSharp.Api/Des/Model/DesDecryptRequest.cs b/src/CrackSharp.Api/Des/Model/DesDecryptRequest.cs index bc6e3c3..dfed281 100644 --- a/src/CrackSharp.Api/Des/Model/DesDecryptRequest.cs +++ b/src/CrackSharp.Api/Des/Model/DesDecryptRequest.cs @@ -1,8 +1,6 @@ -using System.ComponentModel.DataAnnotations; - -namespace CrackSharp.Api.Des.Model; +namespace CrackSharp.Api.Des.Model; public readonly record struct DesDecryptRequest( - [RegularExpression("^[./0-9A-Za-z]{13}$")] string Hash, - [Range(1, 8)] int MaxTextLength = 8, - [RegularExpression("^[./0-9A-Za-z]+$")] string Chars = DesConstants.DecryptDefaultChars); + string Hash, + int MaxTextLength = 8, + string Chars = DesConstants.DecryptDefaultChars); diff --git a/src/CrackSharp.Api/Des/Model/DesDecryptServices.cs b/src/CrackSharp.Api/Des/Model/DesDecryptServices.cs index 8a7d144..7c3c522 100644 --- a/src/CrackSharp.Api/Des/Model/DesDecryptServices.cs +++ b/src/CrackSharp.Api/Des/Model/DesDecryptServices.cs @@ -1,10 +1,13 @@ using CrackSharp.Api.Des.Services; +using Microsoft.AspNetCore.Mvc; namespace CrackSharp.Api.Des.Model; public readonly struct DesDecryptServices { + [FromServices] public required DesBruteForceDecryptionService DecryptionService { get; init; } + [FromServices] public required ILogger Logger { get; init; } } diff --git a/src/CrackSharp.Api/Des/Model/DesEncryptRequest.cs b/src/CrackSharp.Api/Des/Model/DesEncryptRequest.cs index b31db5e..945f225 100644 --- a/src/CrackSharp.Api/Des/Model/DesEncryptRequest.cs +++ b/src/CrackSharp.Api/Des/Model/DesEncryptRequest.cs @@ -1,7 +1,5 @@ -using System.ComponentModel.DataAnnotations; - -namespace CrackSharp.Api.Des.Model; +namespace CrackSharp.Api.Des.Model; public readonly record struct DesEncryptRequest( - [RegularExpression("^[./0-9A-Za-z]+$")] string Text, - [RegularExpression("^[./0-9A-Za-z]{2}$")] string? Salt = null); + string Text, + string? Salt = null); diff --git a/src/CrackSharp.Api/Des/Model/DesEncryptServices.cs b/src/CrackSharp.Api/Des/Model/DesEncryptServices.cs index a718ba0..2157c9d 100644 --- a/src/CrackSharp.Api/Des/Model/DesEncryptServices.cs +++ b/src/CrackSharp.Api/Des/Model/DesEncryptServices.cs @@ -1,10 +1,13 @@ using CrackSharp.Api.Des.Services; +using Microsoft.AspNetCore.Mvc; namespace CrackSharp.Api.Des.Model; public readonly struct DesEncryptServices { + [FromServices] public required DesEncryptionService EncryptionService { get; init; } + [FromServices] public required ILogger Logger { get; init; } } diff --git a/src/CrackSharp.Api/Dockerfile b/src/CrackSharp.Api/Dockerfile index c858873..1a5c8bd 100644 --- a/src/CrackSharp.Api/Dockerfile +++ b/src/CrackSharp.Api/Dockerfile @@ -1,26 +1,19 @@ # Use SDK image -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-jammy AS build +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-noble AS build ARG TARGETARCH +ARG BINARY_VERSION WORKDIR /source -# Copy csproj and restore as distinct layers -COPY CrackSharp.Core/*.csproj CrackSharp.Core/ -COPY CrackSharp.Api/*.csproj CrackSharp.Api/ -RUN dotnet restore CrackSharp.Api -a $TARGETARCH - -# Copy everything and build the app +# Copy everything and publish the app COPY CrackSharp.Core/. CrackSharp.Core/ COPY CrackSharp.Api/. CrackSharp.Api/ -ARG BINARY_VERSION RUN (echo "$BINARY_VERSION" | grep -Eq "^[0-9]+\.[0-9]+\.[0-9]+(-[a-z0-9.]+)?$") && \ - dotnet publish CrackSharp.Api --no-restore -a $TARGETARCH -o /app -p:Version=$BINARY_VERSION || \ - dotnet publish CrackSharp.Api --no-restore -a $TARGETARCH -o /app + dotnet publish CrackSharp.Api -a $TARGETARCH -o /app -p:Version=$BINARY_VERSION || \ + dotnet publish CrackSharp.Api -a $TARGETARCH -o /app # Use runtime image -FROM mcr.microsoft.com/dotnet/aspnet:8.0-jammy-chiseled +FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-noble-chiseled WORKDIR /app - -# Copy build results and run the app COPY --from=build /app . USER $APP_UID ENTRYPOINT ["./CrackSharp.Api"] diff --git a/src/CrackSharp.Api/Program.cs b/src/CrackSharp.Api/Program.cs index 88d03ac..9f46a6f 100644 --- a/src/CrackSharp.Api/Program.cs +++ b/src/CrackSharp.Api/Program.cs @@ -2,10 +2,20 @@ using CrackSharp.Api.Common.Services; using CrackSharp.Api.Des.Endpoints; using CrackSharp.Api.Des.Services; +using Microsoft.AspNetCore.Routing.Constraints; +using Swashbuckle.AspNetCore.SwaggerGen; -var builder = WebApplication.CreateBuilder(args); +var builder = WebApplication.CreateSlimBuilder(args); + +builder.Services.ConfigureHttpJsonOptions( + options => options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default)); + +builder.Services.Configure( + options => options.SetParameterPolicy("regex")); + +builder.Services.AddSingleton( + _ => new(new() { TypeInfoResolver = AppJsonSerializerContext.Default })); -// Add services to the container. builder.Services.AddMemoryCache(options => options.SizeLimit = builder.Configuration.GetCacheSizeLimit()); builder.Services.AddSingleton(typeof(AwaitableMemoryCache<,>)); builder.Services.AddSingleton(); @@ -17,15 +27,12 @@ var app = builder.Build(); -// Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } -app.UseHttpsRedirection(); - app.MapDesApi(); app.Run(); diff --git a/src/CrackSharp.Api/Properties/launchSettings.json b/src/CrackSharp.Api/Properties/launchSettings.json index 9f578d3..b9c6155 100644 --- a/src/CrackSharp.Api/Properties/launchSettings.json +++ b/src/CrackSharp.Api/Properties/launchSettings.json @@ -1,30 +1,15 @@ { "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:55053", - "sslPort": 44367 - } - }, "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "swagger", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "CrackSharp.Api": { + "http": { "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "swagger", - "applicationUrl": "https://localhost:5001;http://localhost:5000", + "applicationUrl": "http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } -} +} \ No newline at end of file diff --git a/src/CrackSharp.Core/Common/BruteForce/BruteForceEnumerable.cs b/src/CrackSharp.Core/Common/BruteForce/BruteForceEnumerable.cs index ac2addb..dfdf759 100644 --- a/src/CrackSharp.Core/Common/BruteForce/BruteForceEnumerable.cs +++ b/src/CrackSharp.Core/Common/BruteForce/BruteForceEnumerable.cs @@ -8,41 +8,33 @@ public sealed class BruteForceEnumerable(IBruteForceParams parameters) : ISpanEn $"Brute-force enumerable combining characters '{_params.Characters}' " + $"into strings of up to {_params.MaxTextLength} characters long."; - public ISpanEnumerator GetEnumerator() => new Enumerator(_params); + public ISpanEnumerator GetEnumerator() => new Enumerator(_params.Characters, _params.MaxTextLength); - private class Enumerator : ISpanEnumerator + private class Enumerator(string characters, int maxTextLength) : ISpanEnumerator { - private readonly IBruteForceParams _params; - private readonly char[] _textBuffer; - private readonly int[] _indices; + private readonly char[] _textBuffer = new char[maxTextLength]; + private readonly int[] _indices = new int[maxTextLength]; private int _position = 0; - internal Enumerator(IBruteForceParams parameters) - { - _params = parameters; - _textBuffer = new char[_params.MaxTextLength]; - _indices = new int[_params.MaxTextLength]; - } - public ReadOnlySpan Current => _textBuffer.AsSpan(0, _position + 1); public bool MoveNext() => MoveNext(_position); private bool MoveNext(int position) { - if (_indices[position] < _params.Characters.Length) + if (_indices[position] < characters.Length) { - _textBuffer[position] = _params.Characters[_indices[position]]; + _textBuffer[position] = characters[_indices[position]]; _indices[position]++; return true; } - _textBuffer[position] = _params.Characters[0]; + _textBuffer[position] = characters[0]; _indices[position] = 1; return position > 0 ? MoveNext(position - 1) - : ++_position < _params.MaxTextLength && MoveNext(_position); + : ++_position < maxTextLength && MoveNext(_position); } } } diff --git a/src/CrackSharp.Core/Des/DesDecryptor.cs b/src/CrackSharp.Core/Des/DesDecryptor.cs index 7320d17..fa9c84b 100644 --- a/src/CrackSharp.Core/Des/DesDecryptor.cs +++ b/src/CrackSharp.Core/Des/DesDecryptor.cs @@ -11,9 +11,11 @@ public static Task DecryptAsync( where TEnumerable : ISpanEnumerable, IDescribable { if (!DesValidationUtils.GetHashValidator().IsMatch(hash)) + { throw new ArgumentException( - $"Value must consist of exactly 13 chars from the set {DesConstants.AllowedCharsPattern}.", + $"Value must consist of exactly 13 characters from the set {DesConstants.AllowedCharsPattern}.", nameof(hash)); + } return Task.Run( () => @@ -28,7 +30,7 @@ public static Task DecryptAsync( return text.ToString(); } - throw new DecryptionFailedException($"Decryption of the {nameof(hash)} '{hash}' " + + throw new DecryptionFailedException($"Decryption of the DES {nameof(hash)} '{hash}' " + $"using the following {nameof(enumerable)} has failed. {enumerable.Description}"); }, cancellationToken);