// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Net;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using HordeServer.Utilities;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace HordeServer.Server
{
///
/// Accessor for the global singleton
///
public class GlobalsService
{
///
/// Global server settings
///
[SingletonDocument("globals", "5e3981cb28b8ec59cd07184a")]
class Globals : SingletonBase, IGlobals
{
[BsonIgnore]
public GlobalsService _owner = null!;
public ObjectId InstanceId { get; set; }
public string ConfigRevision { get; set; } = String.Empty;
public byte[]? JwtSigningKey { get; set; }
public RSAParameters? RsaParameters { get; set; }
public int? SchemaVersion { get; set; }
[BsonIgnore]
string IGlobals.JwtIssuer => _owner._jwtIssuer;
[BsonIgnore]
SecurityKey IGlobals.JwtSigningKey => new SymmetricSecurityKey(JwtSigningKey!);
[BsonIgnore]
RsaSecurityKey IGlobals.RsaSigningKey => new RsaSecurityKey(RsaParameters!.Value) { KeyId = InstanceId.ToString() };
public Globals()
{
InstanceId = ObjectId.GenerateNewId();
}
public Globals Clone()
{
return (Globals)MemberwiseClone();
}
public void RotateSigningKey()
{
JwtSigningKey = RandomNumberGenerator.GetBytes(128);
}
public void RotateRsaParameters()
{
using RSACryptoServiceProvider rsaProvider = new RSACryptoServiceProvider(2048);
rsaProvider.PersistKeyInCsp = false;
RsaParameters = rsaProvider.ExportParameters(true);
}
}
readonly IMongoService _mongoService;
readonly string _jwtIssuer;
///
/// Constructor
///
///
/// Global settings instance
public GlobalsService(IMongoService mongoService, IOptions settings)
{
_mongoService = mongoService;
if (String.IsNullOrEmpty(settings.Value.JwtIssuer))
{
_jwtIssuer = Dns.GetHostName();
}
else
{
_jwtIssuer = settings.Value.JwtIssuer;
}
}
///
/// Gets the current globals instance
///
/// Globals instance
public async ValueTask GetAsync(CancellationToken cancellationToken)
{
for (; ; )
{
Globals globals = await _mongoService.GetSingletonAsync(() => CreateGlobals(), cancellationToken);
globals._owner = this;
if (globals.RsaParameters != null)
{
return globals;
}
globals.RotateRsaParameters();
if (await _mongoService.TryUpdateSingletonAsync(globals, cancellationToken))
{
return globals;
}
}
}
static Globals CreateGlobals()
{
Globals globals = new Globals();
globals.RotateSigningKey();
globals.RotateRsaParameters();
return globals;
}
///
/// Try to update the current globals object
///
/// The current options value
///
/// Cancellation token for the operation
///
public async ValueTask TryUpdateAsync(IGlobals globals, string? configRevision, CancellationToken cancellationToken)
{
Globals concreteGlobals = ((Globals)globals).Clone();
if (configRevision != null)
{
concreteGlobals.ConfigRevision = configRevision;
}
if (!await _mongoService.TryUpdateSingletonAsync(concreteGlobals, cancellationToken))
{
return null;
}
return concreteGlobals;
}
}
}