Merge branch 'develop' of https://github.com/sp-tarkov/server-csharp into develop

This commit is contained in:
clodan
2025-02-20 15:21:19 +00:00
5 changed files with 211 additions and 116 deletions
+174
View File
@@ -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<CertificateHelper> _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;
}
}
/// <summary>
/// Get a certificate from provided path and return
/// </summary>
/// <param name="pfxPath">Path to pfx file</param>
/// <param name="certPassword">Optional password for certificate</param>
/// <returns>X509Certificate2</returns>
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;
}
}
/// <summary>
/// Generate and return a self-signed certificate
/// </summary>
/// <param name="subjectName">e.g. localhost</param>
/// <returns>X509Certificate2</returns>
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)));
}
/// <summary>
/// Save a certificate as a file to disk
/// </summary>
/// <param name="certificate">Certificate to save</param>
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}");
}
}
/// <summary>
/// Save a certificate as a file to disk
/// </summary>
/// <param name="certificate">Certificate to save</param>
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}");
}
}
}
}
+7 -92
View File
@@ -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<HttpServer> _logger,
LocalisationService _localisationService,
ConfigServer _configServer,
CertificateHelper _certificateHelper,
ApplicationContext _applicationContext,
WebSocketServer _webSocketServer,
IEnumerable<IHttpListener> _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);
}
/// <summary>
/// Get a certificate from provided path and return
/// </summary>
/// <param name="pfxPath">Path to pfx file</param>
/// <param name="certPassword">Optional password for certificate</param>
/// <returns>X509Certificate2</returns>
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;
}
/// <summary>
/// Generate and return a self-signed certificate
/// </summary>
/// <param name="subjectName">e.g. localhost</param>
/// <returns>X509Certificate2</returns>
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)));
}
/// <summary>
/// Save a certificate as a file to disk
/// </summary>
/// <param name="certificate">Certificate to save</param>
/// <param name="pfxPath">Path to destination</param>
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)
+10
View File
@@ -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);
+19 -23
View File
@@ -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;
}
/// <summary>
/// Create a 24 character MongoId
/// </summary>
/// <returns>24 character objectId</returns>
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<byte> 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
/// <returns>True when string is a valid mongo id</returns>
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
+1 -1
View File
@@ -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<WebApplication>().Run(httpServerHelper.GetBackendUrl());
appContext?.GetLatestValue(ContextVariableType.WEB_APPLICATION)?.GetValue<WebApplication>().Run();
}
}
catch (Exception ex)