Merge branch 'develop' of https://github.com/sp-tarkov/server-csharp into develop
This commit is contained in:
@@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user