Skip to content
This repository was archived by the owner on Apr 7, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions CortexRemote.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,23 @@
<TargetFramework>net9.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Deterministic>true</Deterministic>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<UseWindowsForms>true</UseWindowsForms>
<UserSecretsId>9e3a541b-7d5a-407c-b1e6-96c24aa9c872</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2025.2.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.9" />
<PackageReference Include="QRCoder" Version="1.6.0" />
<PackageReference Include="Scalar.AspNetCore" Version="2.8.7" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3" />
<PackageReference Include="System.Management" Version="9.0.8" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="9.0.8" />
<PackageReference Include="System.Threading.RateLimiting" Version="9.0.9" />
</ItemGroup>

<ItemGroup>
Expand All @@ -28,9 +31,4 @@
</Content>
</ItemGroup>

<ItemGroup>
<Folder Include="logs\" />
<Folder Include="WindowsRemoteControl\WindowsApi\" />
</ItemGroup>

</Project>
3 changes: 3 additions & 0 deletions Endpoints/ErrorResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace CortexRemote.Endpoints;

public abstract record ErrorResponse(int StatusCode, string Message);
106 changes: 106 additions & 0 deletions Endpoints/MapRemoteControlEndpoints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using System.Runtime.InteropServices;
using CortexRemote.Windows.Enums;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Routing;
using Serilog;

namespace CortexRemote.Endpoints;

public static partial class MapRemoteControlEndpoints
{
[LibraryImport("user32.dll", SetLastError = true)]
private static partial void LockWorkStation();

[LibraryImport("user32.dll", SetLastError = true)]
private static partial void ExitWindowsEx(
ShutdownTypes uExitCode,
uint dwReason = SHTDN_REASON_FLAG_PLANNED);

// https://learn.microsoft.com/en-us/windows/win32/shutdown/system-shutdown-reason-codes
private const uint SHTDN_REASON_FLAG_PLANNED = 0x80000000;

public static IEndpointConventionBuilder MapCommands(this IEndpointRouteBuilder endpoints)
{
ArgumentNullException.ThrowIfNull(endpoints);

var routeGroup = endpoints.MapGroup("/command");

routeGroup.MapPost("/shutdown", Results<Ok<string>, InternalServerError>() =>
{
Log.Information("Command: System Shutdown Command Received at {time}",
DateTime.Now);

try
{
ExitWindowsEx(ShutdownTypes.EWX_HYBRID_SHUTDOWN
| ShutdownTypes.EWX_SHUTDOWN);
return TypedResults.Ok(
"System Shutdown Command Received");
}
catch (Exception exception)
{
Log.Error("Shutdown Error: {exception}", exception);
return TypedResults.InternalServerError();
}
})
.Produces<string>(StatusCodes.Status200OK)
.Produces<ErrorResponse>(StatusCodes.Status401Unauthorized)
.Produces<ErrorResponse>(StatusCodes.Status403Forbidden)
.WithName("Shutdown System")
.WithTags("Commands")
.WithSummary("Turn your computer off");

routeGroup.MapPost("/restart", Results<Ok<string>, InternalServerError> () =>
{
Log.Information("Command: System Restart Command Received at {time}",
DateTime.Now);

try
{
ExitWindowsEx(ShutdownTypes.EWX_REBOOT);
return TypedResults.Ok(
"System Restart Command Received");
}
catch (Exception exception)
{
Log.Error("Restart Error: {exception}", exception);
return TypedResults.InternalServerError();
}
})
.Produces<string>(StatusCodes.Status200OK)
.Produces<ErrorResponse>(StatusCodes.Status401Unauthorized)
.Produces<ErrorResponse>(StatusCodes.Status403Forbidden)
.WithName("Restarts System")
.WithTags("Commands")
.WithSummary("Restart your computer");


routeGroup.MapPost("/lock", Results<Ok<string>, InternalServerError> () =>
{
Log.Information("Command: System Lock Command Received at {time}",
DateTime.Now);

try
{
LockWorkStation();
return TypedResults.Ok(
"System Lock Command Received");
}
catch (Exception exception)
{
Log.Error("Lock Error: {exception}", exception);
return TypedResults.InternalServerError();
}
})
.Produces<string>(StatusCodes.Status200OK)
.Produces<ErrorResponse>(StatusCodes.Status401Unauthorized)
.Produces<ErrorResponse>(StatusCodes.Status403Forbidden)
.WithName("Lock System")
.WithTags("Commands")
.WithSummary("Lock your computer");

return routeGroup;
}
}
60 changes: 60 additions & 0 deletions Endpoints/MapSecurityEndpoints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System.IdentityModel.Tokens.Jwt;
using CortexRemote.Endpoints.TokenModel;
using CortexRemote.Windows;
using CortexRemote.Windows.Security;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Routing;
using Microsoft.IdentityModel.Tokens;

namespace CortexRemote.Endpoints;

public static class MapSecurityEndpoints
{
public static void MapAuthentication(this IEndpointRouteBuilder endpoints, IKeyHandler keyHandler)
{
ArgumentNullException.ThrowIfNull(endpoints);

var routeGroup = endpoints.MapGroup("/auth");

routeGroup.MapPost("/refresh",
Results<Ok<TokenResponse>, UnauthorizedHttpResult> (AuthenticationRequest authenticationRequest) =>
{
var storedToken = Convert.ToBase64String(
keyHandler.ImportRefreshToken());

if (!storedToken.Equals(authenticationRequest.RefreshToken))
return TypedResults.Unauthorized();

var key = new RsaSecurityKey(
keyHandler.ImportAsymmetricKey(KeyHandler.SigningKeyFileName));
var credentials = new SigningCredentials(
key: key,
algorithm: SecurityAlgorithms.RsaSha512);

var token = new JwtSecurityToken(
issuer: DirectoryManager.APPLICATION_NAME_FOLDER,
audience: DirectoryManager.APPLICATION_NAME_FOLDER +
Environment.MachineName,
expires: DateTime.UtcNow.AddSeconds(30),
signingCredentials: credentials);

var JwtTokenBody = new JwtSecurityTokenHandler()
.WriteToken(token);

return TypedResults.Ok(
new TokenResponse
{
AccessToken = JwtTokenBody,
Expiration = token.ValidTo.ToLocalTime(),
Type = "Bearer"
});
})
.Produces<TokenResponse>(StatusCodes.Status200OK)
.Produces<ErrorResponse>(StatusCodes.Status401Unauthorized)
.WithName("Authorization")
.WithTags("Validation")
.WithSummary("Request new access token");
}
}
9 changes: 9 additions & 0 deletions Endpoints/TokenModel/AuthenticationRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using JetBrains.Annotations;

namespace CortexRemote.Endpoints.TokenModel;

[UsedImplicitly]
public class AuthenticationRequest
{
public required string RefreshToken { get; init; }
}
8 changes: 8 additions & 0 deletions Endpoints/TokenModel/TokenResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace CortexRemote.Endpoints.TokenModel;

public class TokenResponse
{
public string AccessToken { get; set; } = null!;
public string Type { get; set; } = null!;
public DateTime Expiration { get; set; }
}
39 changes: 0 additions & 39 deletions Extensions/ApiEndpointExtensionMethod/RemoteEndpointExtension.cs

This file was deleted.

10 changes: 0 additions & 10 deletions Interfaces/IExecutionCommands.cs

This file was deleted.

33 changes: 33 additions & 0 deletions Middleware/BearerSecuritySchemeTransformer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi.Models;

namespace CortexRemote.Middleware;

internal sealed class BearerSecuritySchemeTransformer(
IAuthenticationSchemeProvider authenticationSchemeProvider
) : IOpenApiDocumentTransformer
{
public async Task TransformAsync(
OpenApiDocument document,
OpenApiDocumentTransformerContext context,
CancellationToken cancellationToken)
{
var authenticationSchemes = await authenticationSchemeProvider.GetAllSchemesAsync();
if (authenticationSchemes.Any(authScheme => authScheme.Name == "Bearer"))
{
var requirements = new Dictionary<string, OpenApiSecurityScheme>
{
["Bearer"] = new OpenApiSecurityScheme
{
Type = SecuritySchemeType.Http,
Scheme = "Bearer",
In = ParameterLocation.Header,
BearerFormat = "Json Web Token"
}
};
document.Components ??= new OpenApiComponents();
document.Components.SecuritySchemes = requirements;
}
}
}
15 changes: 15 additions & 0 deletions Middleware/JsonDateTimeConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace CortexRemote.Middleware;

public class JsonDateTimeConverter : JsonConverter<DateTime>
{
private const string TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";

public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> DateTime.Parse(reader.GetString() ?? "");

public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToLocalTime().ToString(TIME_FORMAT));
}
31 changes: 31 additions & 0 deletions Middleware/StartupConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using CortexRemote.Windows.Security;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace CortexRemote.Middleware;

internal sealed class StartupConfiguration : IHostedService
{
private readonly ILogger<StartupConfiguration> _logger;
private readonly IKeyHandler _keyHandler;

public StartupConfiguration(
ILogger<StartupConfiguration> logger,
IKeyHandler keyHandler
)
{
_logger = logger;
_keyHandler = keyHandler;
}

public Task StartAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}

public Task StopAsync(CancellationToken cancellationToken)
{
throw new NotImplementedException();
}

}
Loading