diff --git a/Libraries/Core/Helpers/CertificateHelper.cs b/Libraries/Core/Helpers/CertificateHelper.cs new file mode 100644 index 00000000..825b4b33 --- /dev/null +++ b/Libraries/Core/Helpers/CertificateHelper.cs @@ -0,0 +1,174 @@ +using System.Net; +using System.Security.Cryptography.X509Certificates; +using System.Security.Cryptography; +using SptCommon.Annotations; +using Core.Models.Utils; +using Core.Utils; + +namespace Core.Helpers +{ + [Injectable] + public class CertificateHelper(ISptLogger _logger, FileUtil _fileUtil) + { + private const string certificatePath = "./user/certs/server.crt"; + private const string certificateKeyPath = "./user/certs/server.key"; + private const string certificatePfxPath = "./user/certs/certificate.pfx"; + + //Todo: Finish off to match TS server + public X509Certificate2 LoadOrGenerateCertificate() + { + if (!Directory.Exists("./user/certs")) + { + Directory.CreateDirectory("./user/certs"); + } + + var certificate = LoadCertificate(); + + if (certificate == null) + { + // Generate self-signed certificate + certificate = GenerateSelfSignedCertificate("localhost"); + SaveCertificate(certificate); // Save cert and new key + + _logger.Success($"Generated and stored self-signed certificate ({certificatePath})"); + } + + return certificate; + } + + //Todo: When the above is finished off, remove any method with Pfx in the name + public X509Certificate2 LoadOrGenerateCertificatePfx() + { + if (!Directory.Exists("./user/certs")) + { + Directory.CreateDirectory("./user/certs"); + } + + var certificate = LoadCertificatePfx(); + + if (certificate == null) + { + // Generate self-signed certificate + certificate = GenerateSelfSignedCertificate("localhost"); + SaveCertificatePfx(certificate); // Save cert + + _logger.Success($"Generated and stored self-signed certificate ({certificatePath})"); + } + + return certificate; + } + + private X509Certificate2? LoadCertificate() + { + try + { + return X509Certificate2.CreateFromPemFile(certificatePath, certificateKeyPath); + } + catch (Exception) + { + return null; + } + } + + /// + /// Get a certificate from provided path and return + /// + /// Path to pfx file + /// Optional password for certificate + /// X509Certificate2 + private X509Certificate2? LoadCertificatePfx() + { + try + { + //Archangel: For some reason despite this being deprecated this is the only way to load a certificate file + //No idea why, I want to eventually switch over to the other format so it lines up with the TS server + //But for now this works fine + return new(certificatePfxPath); + } + catch (Exception) + { + return null; + } + } + + /// + /// Generate and return a self-signed certificate + /// + /// e.g. localhost + /// X509Certificate2 + private X509Certificate2 GenerateSelfSignedCertificate(string subjectName) + { + var sanBuilder = new SubjectAlternativeNameBuilder(); + sanBuilder.AddIpAddress(IPAddress.Loopback); + sanBuilder.AddDnsName("localhost"); + sanBuilder.AddDnsName(Environment.MachineName); + + var distinguishedName = new X500DistinguishedName($"CN={subjectName}"); + + using var rsa = RSA.Create(2048); + var request = new CertificateRequest(distinguishedName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + request.CertificateExtensions.Add(sanBuilder.Build()); + + //Todo: Enable when Pfx methods can be removed + //SavePrivateKey(rsa); + + return request.CreateSelfSigned(new DateTimeOffset(DateTime.UtcNow.AddDays(-1)), new DateTimeOffset(DateTime.UtcNow.AddDays(3650))); + } + + /// + /// Save a certificate as a file to disk + /// + /// Certificate to save + private void SaveCertificate(X509Certificate2 certificate) + { + try + { + // Save as PEM (ensure the certificate is in PEM format) + var certPem = "-----BEGIN CERTIFICATE-----\n" + + Convert.ToBase64String(certificate.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks) + + "\n-----END CERTIFICATE-----"; + _fileUtil.WriteFile(certificatePath, certPem); + } + catch (Exception ex) + { + _logger.Error($"Error saving certificate: {ex.Message}"); + } + } + + /// + /// Save a certificate as a file to disk + /// + /// Certificate to save + private void SaveCertificatePfx(X509Certificate2 certificate) + { + try + { + _fileUtil.WriteFile(certificatePfxPath, certificate.Export(X509ContentType.Pfx)); + } + catch (Exception ex) + { + _logger.Error($"Error saving certificate: {ex.Message}"); + } + } + + private void SavePrivateKey(RSA privateKey) + { + try + { + var privateKeyBytes = privateKey.ExportPkcs8PrivateKey(); + + // Convert the private key to PEM format (Base64 encoded) + var privateKeyString = "-----BEGIN PRIVATE KEY-----\n" + + Convert.ToBase64String(privateKeyBytes, Base64FormattingOptions.InsertLineBreaks) + + "\n-----END PRIVATE KEY-----"; + + _fileUtil.WriteFile(certificateKeyPath, privateKeyString); + } + catch (Exception ex) + { + _logger.Error($"Error saving certificate key: {ex.Message}"); + } + } + + } +} diff --git a/Libraries/Core/Servers/HttpServer.cs b/Libraries/Core/Servers/HttpServer.cs index 4198eb31..9147d998 100644 --- a/Libraries/Core/Servers/HttpServer.cs +++ b/Libraries/Core/Servers/HttpServer.cs @@ -1,13 +1,11 @@ using System.Net; using System.Security.Authentication; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; using Core.Context; +using Core.Helpers; using Core.Models.Spt.Config; using Core.Models.Utils; using Core.Servers.Http; using Core.Services; -using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.Extensions.Primitives; using SptCommon.Annotations; @@ -19,6 +17,7 @@ public class HttpServer( ISptLogger _logger, LocalisationService _localisationService, ConfigServer _configServer, + CertificateHelper _certificateHelper, ApplicationContext _applicationContext, WebSocketServer _webSocketServer, IEnumerable _httpListeners @@ -33,38 +32,23 @@ public class HttpServer( { throw new Exception("WebApplicationBuilder is null in HttpServer.Load()"); } - builder.Services.AddHttpsRedirection(conf => - { - conf.HttpsPort = _httpConfig.Port; - }); + builder.WebHost.ConfigureKestrel( options => { - const string certFileName = "certificate.pfx"; - var certificate = LoadCertificate(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, certFileName, _httpConfig.CertificatePassword)); - if (certificate == null) - { - // Generate self-signed certificate - certificate = GenerateSelfSignedCertificate("localhost"); - SaveCertificate(certificate, certFileName); // Save cert - - _logger.Success($"Generated and stored self-signed certificate ({certFileName}) in {AppDomain.CurrentDomain.BaseDirectory}"); - } - - options.ListenAnyIP(_httpConfig.Port, listenOptions => + options.Listen(IPAddress.Parse(_httpConfig.Ip), _httpConfig.Port, listenOptions => { listenOptions.UseHttps(opts => { - opts.SslProtocols = SslProtocols.Tls12; - opts.AllowAnyClientCertificate(); - opts.ServerCertificate = certificate; + opts.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13; + opts.ServerCertificate = _certificateHelper.LoadOrGenerateCertificatePfx(); opts.ClientCertificateMode = ClientCertificateMode.NoCertificate; }); }); }); var app = builder.Build(); - app.UseHttpsRedirection(); + if (app is null) { throw new Exception("WebApplication is null in HttpServer.Load()"); @@ -84,75 +68,6 @@ public class HttpServer( _applicationContext.AddValue(ContextVariableType.WEB_APPLICATION, app); } - /// - /// Get a certificate from provided path and return - /// - /// Path to pfx file - /// Optional password for certificate - /// X509Certificate2 - private X509Certificate2? LoadCertificate(string pfxPath, string? certPassword = null) - { - if (File.Exists(pfxPath)) - { - try - { - //TODO: use this - //return X509CertificateLoader.LoadCertificateFromFile(pfxPath); - return string.IsNullOrEmpty(certPassword) - ? new X509Certificate2(pfxPath) - : new X509Certificate2(pfxPath, certPassword); - } - catch (Exception ex) - { - _logger.Error($"Error loading certificate from path: {pfxPath} error: {ex.Message}"); - - return null; - } - } - - return null; - } - - /// - /// Generate and return a self-signed certificate - /// - /// e.g. localhost - /// X509Certificate2 - private X509Certificate2 GenerateSelfSignedCertificate(string subjectName) - { - var sanBuilder = new SubjectAlternativeNameBuilder(); - sanBuilder.AddIpAddress(IPAddress.Loopback); - sanBuilder.AddIpAddress(IPAddress.IPv6Loopback); - sanBuilder.AddIpAddress(new IPAddress(new byte[] { 127, 0, 0, 1 })); - sanBuilder.AddDnsName("localhost"); - sanBuilder.AddDnsName(Environment.MachineName); - - var distinguishedName = new X500DistinguishedName($"CN={subjectName}"); - - using var rsa = RSA.Create(2048); - var request = new CertificateRequest(distinguishedName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - request.CertificateExtensions.Add(sanBuilder.Build()); - - return request.CreateSelfSigned(new DateTimeOffset(DateTime.UtcNow.AddDays(-1)), new DateTimeOffset(DateTime.UtcNow.AddDays(3650))); - } - - /// - /// Save a certificate as a file to disk - /// - /// Certificate to save - /// Path to destination - private void SaveCertificate(X509Certificate2 certificate, string pfxPath) - { - try - { - File.WriteAllBytes(pfxPath, certificate.Export(X509ContentType.Pfx)); - } - catch (Exception ex) - { - _logger.Error($"Error saving certificate: {ex.Message}"); - } - } - private async Task HandleFallback(HttpContext context) { if (context.WebSockets.IsWebSocketRequest) diff --git a/Libraries/Core/Utils/FileUtil.cs b/Libraries/Core/Utils/FileUtil.cs index 84781a89..0b51301f 100644 --- a/Libraries/Core/Utils/FileUtil.cs +++ b/Libraries/Core/Utils/FileUtil.cs @@ -77,6 +77,16 @@ public class FileUtil( File.WriteAllText(filePath, fileContent); } + public void WriteFile(string filePath, byte[] fileContent) + { + if (!FileExists(filePath)) + { + CreateFile(filePath); + } + + File.WriteAllBytes(filePath, fileContent); + } + private void CreateFile(string filePath) { var stream = File.Create(filePath); diff --git a/Libraries/Core/Utils/HashUtil.cs b/Libraries/Core/Utils/HashUtil.cs index b6687f28..2ecc7e1a 100644 --- a/Libraries/Core/Utils/HashUtil.cs +++ b/Libraries/Core/Utils/HashUtil.cs @@ -7,40 +7,35 @@ using SptCommon.Annotations; namespace Core.Utils; [Injectable(InjectionType.Singleton)] -public class HashUtil +public partial class HashUtil(RandomUtil _randomUtil) { - protected RandomUtil _randomUtil; - protected Regex MongoIdRegex = new("^[a-fA-F0-9]{24}$"); - - public HashUtil(RandomUtil randomUtil) - { - _randomUtil = randomUtil; - } - /// /// Create a 24 character MongoId /// /// 24 character objectId public string Generate() { - var objectId = new byte[12]; + // Allocate a span directly onto the stack, will dispose whenever we finished running + // Span is recommended to work with stackalloc and we can use stackalloc here because we don't do anything with this afterwards + Span objectId = stackalloc byte[12]; // Time stamp (4 bytes) - var timestamp = BitConverter.GetBytes((int) DateTimeOffset.UtcNow.ToUnixTimeSeconds()); + var timestamp = (int) DateTimeOffset.UtcNow.ToUnixTimeSeconds(); // Convert to big-endian - Array.Reverse(timestamp); - Array.Copy(timestamp, 0, objectId, 0, 4); + objectId[0] = (byte) (timestamp >> 24); + objectId[1] = (byte) (timestamp >> 16); + objectId[2] = (byte) (timestamp >> 8); + objectId[3] = (byte) timestamp; // Random value (5 bytes) - var randomValue = new byte[5]; - _randomUtil.Random.NextBytes(randomValue); - Array.Copy(randomValue, 0, objectId, 4, 5); + _randomUtil.Random.NextBytes(objectId.Slice(4, 5)); // Incrementing counter (3 bytes) // 24-bit counter - var counter = BitConverter.GetBytes(_randomUtil.GetInt(0, 16777215)); - Array.Reverse(counter); - Array.Copy(counter, 0, objectId, 9, 3); + var counter = _randomUtil.GetInt(0, 16777215); + objectId[9] = (byte) (counter >> 16); + objectId[10] = (byte) (counter >> 8); + objectId[11] = (byte) counter; return Convert.ToHexStringLower(objectId); } @@ -52,7 +47,7 @@ public class HashUtil /// True when string is a valid mongo id public bool IsValidMongoId(string stringToCheck) { - return MongoIdRegex.IsMatch(stringToCheck); + return MongoIdRegex().IsMatch(stringToCheck); } public string GenerateMd5ForData(string data) @@ -103,10 +98,11 @@ public class HashUtil const int min = 1000000; const int max = 1999999; - var random = new Random(); - - return random.Next(min, max + 1); + return _randomUtil.Random.Next(min, max + 1); } + + [GeneratedRegex("^[a-fA-F0-9]{24}$")] + private static partial Regex MongoIdRegex(); } public enum HashingAlgorithm diff --git a/Server/Program.cs b/Server/Program.cs index 9f35ff80..48179831 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -63,7 +63,7 @@ public static class Program // object, which we can use here to start the webapp. if (httpServerHelper != null) { - appContext?.GetLatestValue(ContextVariableType.WEB_APPLICATION)?.GetValue().Run(httpServerHelper.GetBackendUrl()); + appContext?.GetLatestValue(ContextVariableType.WEB_APPLICATION)?.GetValue().Run(); } } catch (Exception ex)