mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-30 00:22:40 +01:00
Fix windows system identities
This commit is contained in:
@@ -115,6 +115,13 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <inheritdoc />
|
||||
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
|
||||
{
|
||||
if (AuthenticationContext == null)
|
||||
{
|
||||
//accessing an instance they don't have access to
|
||||
await Forbid().ExecuteResultAsync(context).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
//validate the headers
|
||||
try
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api;
|
||||
@@ -33,6 +34,10 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// The <see cref="IApplication"/> for the <see cref="HomeController"/>
|
||||
/// </summary>
|
||||
readonly IApplication application;
|
||||
/// <summary>
|
||||
/// The <see cref="IAuthenticationContextFactory"/> for the <see cref="HomeController"/>
|
||||
/// </summary>
|
||||
readonly IAuthenticationContextFactory authenticationContextFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="HomeController"/>
|
||||
@@ -49,6 +54,8 @@ namespace Tgstation.Server.Host.Controllers
|
||||
this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
|
||||
this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
|
||||
this.application = application ?? throw new ArgumentNullException(nameof(application));
|
||||
//base checks not null
|
||||
this.authenticationContextFactory = authenticationContextFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -81,6 +88,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
if (user == null)
|
||||
return Unauthorized();
|
||||
|
||||
ISystemIdentity identity = null;
|
||||
if (user.PasswordHash != null)
|
||||
{
|
||||
var originalHash = user.PasswordHash;
|
||||
@@ -101,17 +109,23 @@ namespace Tgstation.Server.Host.Controllers
|
||||
else
|
||||
try
|
||||
{
|
||||
using (await systemIdentityFactory.CreateSystemIdentity(user.Name, ApiHeaders.Password, cancellationToken).ConfigureAwait(false)) { }
|
||||
identity = await systemIdentityFactory.CreateSystemIdentity(user.Name, ApiHeaders.Password, cancellationToken).ConfigureAwait(false);
|
||||
if (identity == null || identity.Uid != user.SystemIdentifier)
|
||||
return Unauthorized();
|
||||
}
|
||||
catch
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
return Unauthorized();
|
||||
return StatusCode((int)HttpStatusCode.NotImplemented);
|
||||
}
|
||||
using (identity) {
|
||||
if (!user.Enabled.Value)
|
||||
return Forbid();
|
||||
|
||||
if (!user.Enabled.Value)
|
||||
return Forbid();
|
||||
|
||||
return Json(tokenFactory.CreateToken(user));
|
||||
var token = tokenFactory.CreateToken(user, out var expiry);
|
||||
if (identity != null)
|
||||
authenticationContextFactory.CacheSystemIdentity(user, identity, expiry.AddSeconds(10)); //expire the identity slightly after the auth token in case of lag
|
||||
return Json(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,8 @@ namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
using (var sysIdentity = await systemIdentityFactory.CreateSystemIdentity(dbUser, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (sysIdentity == null)
|
||||
return StatusCode((int)HttpStatusCode.Gone);
|
||||
dbUser.Name = sysIdentity.Username;
|
||||
dbUser.SystemIdentifier = sysIdentity.Uid;
|
||||
}
|
||||
@@ -90,11 +92,6 @@ namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
return StatusCode((int)HttpStatusCode.NotImplemented);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
logger.LogInformation("System identifier user creation failure for {0}. Exception: {1}", model.SystemIdentifier, e);
|
||||
return Forbid();
|
||||
}
|
||||
else
|
||||
cryptographySuite.SetUserPassword(dbUser, model.Password);
|
||||
|
||||
|
||||
@@ -149,7 +149,8 @@ namespace Tgstation.Server.Host.Core
|
||||
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}!", nameof(DatabaseType)));
|
||||
}
|
||||
|
||||
services.AddScoped<IAuthenticationContextFactory, AuthenticationContextFactory>();
|
||||
//very important this remains a singleton
|
||||
services.AddSingleton<IAuthenticationContextFactory, AuthenticationContextFactory>();
|
||||
|
||||
services.AddSingleton<ICryptographySuite, CryptographySuite>();
|
||||
services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Security
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class AuthenticationContextFactory : IAuthenticationContextFactory
|
||||
sealed class AuthenticationContextFactory : IAuthenticationContextFactory, IDisposable
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IAuthenticationContext CurrentAuthenticationContext { get; private set; }
|
||||
@@ -19,19 +21,53 @@ namespace Tgstation.Server.Host.Security
|
||||
readonly ISystemIdentityFactory systemIdentityFactory;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IDatabaseContext"/> for the <see cref="AuthenticationContextFactory"/>
|
||||
/// The <see cref="IDatabaseContextFactory"/> for the <see cref="AuthenticationContextFactory"/>
|
||||
/// </summary>
|
||||
readonly IDatabaseContext databaseContext;
|
||||
readonly IDatabaseContextFactory databaseContextFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Map of <see cref="Api.Models.Internal.User.Id"/>s to <see cref="IdentityCache"/>s for that user
|
||||
/// </summary>
|
||||
readonly Dictionary<long, IdentityCache> identityCache;
|
||||
|
||||
/// <summary>
|
||||
/// Construct an <see cref="AuthenticationContextFactory"/>
|
||||
/// </summary>
|
||||
/// <param name="systemIdentityFactory">The value of <see cref="systemIdentityFactory"/></param>
|
||||
/// <param name="databaseContext">The value of <see cref="databaseContext"/></param>
|
||||
public AuthenticationContextFactory(ISystemIdentityFactory systemIdentityFactory, IDatabaseContext databaseContext)
|
||||
/// <param name="databaseContextFactory">The value of <see cref="databaseContextFactory"/></param>
|
||||
public AuthenticationContextFactory(ISystemIdentityFactory systemIdentityFactory, IDatabaseContextFactory databaseContextFactory)
|
||||
{
|
||||
this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
|
||||
this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
|
||||
this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
|
||||
|
||||
identityCache = new Dictionary<long, IdentityCache>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var I in identityCache)
|
||||
I.Value.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CacheSystemIdentity(User user, ISystemIdentity systemIdentity, DateTimeOffset expiry)
|
||||
{
|
||||
if (user == null)
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
if (systemIdentity == null)
|
||||
throw new ArgumentNullException(nameof(systemIdentity));
|
||||
lock (identityCache)
|
||||
{
|
||||
if (identityCache.TryGetValue(user.Id, out var identCache))
|
||||
identCache.Dispose(); //also clears it out
|
||||
identCache = new IdentityCache(systemIdentity.Clone(), expiry, () =>
|
||||
{
|
||||
lock (identityCache)
|
||||
identityCache.Remove(user.Id);
|
||||
});
|
||||
identityCache.Add(user.Id, identCache);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -40,18 +76,35 @@ namespace Tgstation.Server.Host.Security
|
||||
if (CurrentAuthenticationContext != null)
|
||||
throw new InvalidOperationException("Authentication context has already been loaded");
|
||||
|
||||
var userQuery = databaseContext.Users.Where(x => x.Id == userId);
|
||||
User user = null;
|
||||
await databaseContextFactory.UseContext(async db =>
|
||||
{
|
||||
var userQuery = db.Users.Where(x => x.Id == userId);
|
||||
|
||||
if (instanceId.HasValue)
|
||||
userQuery = userQuery.Include(x => x.InstanceUsers.Where(y => y.Id == instanceId));
|
||||
if (instanceId.HasValue)
|
||||
userQuery = userQuery.Include(x => x.InstanceUsers.Where(y => y.Id == instanceId));
|
||||
|
||||
var user = await userQuery.Include(x => x.InstanceUsers).FirstAsync(cancellationToken).ConfigureAwait(false);
|
||||
user = await userQuery.Include(x => x.InstanceUsers).FirstAsync(cancellationToken).ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
InstanceUser instanceUser = null;
|
||||
if (instanceId.HasValue)
|
||||
instanceUser = user.InstanceUsers.Where(x => x.InstanceId == instanceId).First();
|
||||
instanceUser = user.InstanceUsers.Where(x => x.InstanceId == instanceId).FirstOrDefault();
|
||||
|
||||
if (instanceUser == default)
|
||||
return;
|
||||
|
||||
ISystemIdentity systemIdentity;
|
||||
if (user.SystemIdentifier != null)
|
||||
lock (identityCache)
|
||||
{
|
||||
if (!identityCache.TryGetValue(userId, out var identCache))
|
||||
throw new InvalidOperationException("Cached system identity has expired!");
|
||||
systemIdentity = identCache.SystemIdentity.Clone();
|
||||
}
|
||||
else
|
||||
systemIdentity = null;
|
||||
|
||||
var systemIdentity = user.SystemIdentifier != null ? await systemIdentityFactory.CreateSystemIdentity(user, cancellationToken).ConfigureAwait(false) : null;
|
||||
CurrentAuthenticationContext = new AuthenticationContext(systemIdentity, user, instanceUser);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Threading;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Host.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Security
|
||||
{
|
||||
@@ -13,6 +15,14 @@ namespace Tgstation.Server.Host.Security
|
||||
/// </summary>
|
||||
IAuthenticationContext CurrentAuthenticationContext { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Keep a <paramref name="user"/>'s <paramref name="systemIdentity"/> alive until an <paramref name="expiry"/> time
|
||||
/// </summary>
|
||||
/// <param name="user">The <see cref="User"/> the <paramref name="systemIdentity"/> belongs to</param>
|
||||
/// <param name="systemIdentity">The <see cref="ISystemIdentity"/> to cache</param>
|
||||
/// <param name="expiry">When the <paramref name="systemIdentity"/> should expire</param>
|
||||
void CacheSystemIdentity(User user, ISystemIdentity systemIdentity, DateTimeOffset expiry);
|
||||
|
||||
/// <summary>
|
||||
/// Create an <see cref="IAuthenticationContext"/> to populate <see cref="CurrentAuthenticationContext"/>
|
||||
/// </summary>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using Tgstation.Server.Api.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Security
|
||||
@@ -13,7 +12,8 @@ namespace Tgstation.Server.Host.Security
|
||||
/// Create a <see cref="Token"/> for a given <paramref name="user"/>
|
||||
/// </summary>
|
||||
/// <param name="user">The <see cref="Models.User"/> to create the token for. Must have the <see cref="Api.Models.Internal.User.Id"/> field available</param>
|
||||
/// <param name="expiry">The <see cref="DateTimeOffset"/> representing the time the token expires</param>
|
||||
/// <returns>A new <see cref="Token"/></returns>
|
||||
Token CreateToken(Models.User user);
|
||||
Token CreateToken(Models.User user, out DateTimeOffset expiry);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// For keeping a specific <see cref="ISystemIdentity"/> alive for a period of time
|
||||
/// </summary>
|
||||
sealed class IdentityCache : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="ISystemIdentity"/> the <see cref="IdentityCache"/> manages
|
||||
/// </summary>
|
||||
public ISystemIdentity SystemIdentity { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="cancellationTokenSource"/> for the <see cref="IdentityCache"/>
|
||||
/// </summary>
|
||||
readonly CancellationTokenSource cancellationTokenSource;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Task"/> to clean up <see cref="SystemIdentity"/>
|
||||
/// </summary>
|
||||
readonly Task task;
|
||||
|
||||
/// <summary>
|
||||
/// Construct an <see cref="IdentityCache"/>
|
||||
/// </summary>
|
||||
/// <param name="systemIdentity">The value of <see cref="SystemIdentity"/></param>
|
||||
/// <param name="expiry">The <see cref="DateTimeOffset"/></param>
|
||||
/// <param name="onExpiry">An optional <see cref="Action"/> to take on expiry</param>
|
||||
public IdentityCache(ISystemIdentity systemIdentity, DateTimeOffset expiry, Action onExpiry)
|
||||
{
|
||||
SystemIdentity = systemIdentity ?? throw new ArgumentNullException(nameof(systemIdentity));
|
||||
|
||||
cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
async Task DisposeOnExipiry(CancellationToken cancellationToken)
|
||||
{
|
||||
using (SystemIdentity)
|
||||
try
|
||||
{
|
||||
await Task.Delay(expiry - DateTimeOffset.Now, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
onExpiry?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
task = DisposeOnExipiry(cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
cancellationTokenSource.Cancel();
|
||||
task.Wait();
|
||||
cancellationTokenSource.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ using System.Globalization;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Tgstation.Server.Api.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Security
|
||||
@@ -12,20 +11,26 @@ namespace Tgstation.Server.Host.Security
|
||||
/// <inheritdoc />
|
||||
sealed class TokenFactory : ITokenFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Amount of hours until generated <see cref="Token"/>s expire
|
||||
/// </summary>
|
||||
const int TokenExpiryHours = 1;
|
||||
|
||||
public static readonly string TokenAudience = typeof(Token).Assembly.GetName().Name;
|
||||
public static readonly string TokenIssuer = Assembly.GetExecutingAssembly().GetName().Name;
|
||||
public static readonly byte[] TokenSigningKey = CryptographySuite.GetSecureBytes(256);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Token CreateToken(Models.User user)
|
||||
public Token CreateToken(Models.User user, out DateTimeOffset expiry)
|
||||
{
|
||||
if (user == null)
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
|
||||
expiry = DateTimeOffset.Now.AddHours(TokenExpiryHours);
|
||||
var claims = new Claim[]
|
||||
{
|
||||
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString(CultureInfo.InvariantCulture)),
|
||||
new Claim(JwtRegisteredClaimNames.Exp, $"{DateTimeOffset.Now.AddHours(1).ToUnixTimeSeconds()}"),
|
||||
new Claim(JwtRegisteredClaimNames.Exp, $"{expiry.ToUnixTimeSeconds()}"),
|
||||
new Claim(JwtRegisteredClaimNames.Nbf, $"{DateTimeOffset.Now.ToUnixTimeSeconds()}"),
|
||||
new Claim(JwtRegisteredClaimNames.Iss, TokenIssuer),
|
||||
new Claim(JwtRegisteredClaimNames.Aud, TokenAudience)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.DirectoryServices.AccountManagement;
|
||||
using System.Security.Principal;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -16,7 +17,12 @@ namespace Tgstation.Server.Host.Security
|
||||
readonly WindowsIdentity identity;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="WindowsSystemIdentity"/>
|
||||
/// The <see cref="UserPrincipal"/> for the <see cref="WindowsSystemIdentity"/>
|
||||
/// </summary>
|
||||
readonly UserPrincipal userPrincipal;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="WindowsSystemIdentity"/> using a <see cref="WindowsIdentity"/>
|
||||
/// </summary>
|
||||
/// <param name="identity">The value of <see cref="identity"/></param>
|
||||
public WindowsSystemIdentity(WindowsIdentity identity)
|
||||
@@ -24,23 +30,40 @@ namespace Tgstation.Server.Host.Security
|
||||
this.identity = identity ?? throw new ArgumentNullException(nameof(identity));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="WindowsSystemIdentity"/> using a <see cref="UserPrincipal"/>
|
||||
/// </summary>
|
||||
/// <param name="userPrincipal">The value of <see cref="userPrincipal"/></param>
|
||||
public WindowsSystemIdentity(UserPrincipal userPrincipal)
|
||||
{
|
||||
this.userPrincipal = userPrincipal ?? throw new ArgumentNullException(nameof(userPrincipal));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => identity.Dispose();
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Uid => identity.User.ToString();
|
||||
public string Uid => (userPrincipal?.Sid ?? identity.User).ToString();
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Username => identity.Name;
|
||||
public string Username => userPrincipal?.Name ?? identity.Name;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ISystemIdentity Clone() => new WindowsSystemIdentity((WindowsIdentity)identity.Clone());
|
||||
public ISystemIdentity Clone()
|
||||
{
|
||||
if (identity != null)
|
||||
return new WindowsSystemIdentity((WindowsIdentity)identity.Clone());
|
||||
//can't clone a UP, shouldn't be trying to anyway, cloning is for impersonation
|
||||
throw new InvalidOperationException("Cannot clone a UserPrincipal based WindowsSystemIdentity!");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task RunImpersonated(Action action, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
|
||||
{
|
||||
if (action == null)
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
if (identity == null)
|
||||
throw new InvalidOperationException("Impersonate using a UserPrincipal based WindowsSystemIdentity!");
|
||||
WindowsIdentity.RunImpersonated(identity.AccessToken, action);
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.DirectoryServices.AccountManagement;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Principal;
|
||||
using System.Threading;
|
||||
@@ -24,15 +24,19 @@ namespace Tgstation.Server.Host.Security
|
||||
if (user.SystemIdentifier == null)
|
||||
throw new InvalidOperationException("User's SystemIdentifier must not be null!");
|
||||
|
||||
//System identity at this point will always be in the form DOMAIN\\USER or USER
|
||||
var splits = user.SystemIdentifier.Split('\\');
|
||||
string identity;
|
||||
if (splits.Length > 1)
|
||||
identity = String.Format(CultureInfo.InvariantCulture, "{0}@{1}", splits[0], splits[1]);
|
||||
else
|
||||
identity = user.SystemIdentifier;
|
||||
UserPrincipal principal = null;
|
||||
//machine logon first cause it's faster
|
||||
using (var pc = new PrincipalContext(ContextType.Machine, Environment.UserDomainName))
|
||||
principal = UserPrincipal.FindByIdentity(pc, user.SystemIdentifier);
|
||||
|
||||
return (ISystemIdentity)new WindowsSystemIdentity(new WindowsIdentity(identity));
|
||||
if(principal == null)
|
||||
using (var pc = new PrincipalContext(ContextType.Domain, Environment.UserDomainName))
|
||||
principal = UserPrincipal.FindByIdentity(pc, user.SystemIdentifier);
|
||||
|
||||
if (principal == null)
|
||||
return null;
|
||||
|
||||
return (ISystemIdentity)new WindowsSystemIdentity(principal);
|
||||
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -87,9 +87,19 @@ namespace Tgstation.Server.Host
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RegisterForUpdate(Action action) => cancellationTokenSource.Token.Register(action);
|
||||
public void RegisterForUpdate(Action action)
|
||||
{
|
||||
if (cancellationTokenSource == null)
|
||||
throw new InvalidOperationException("Tried to register an update action on a non-running Server!");
|
||||
cancellationTokenSource.Token.Register(action);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Restart() => cancellationTokenSource.Cancel();
|
||||
public void Restart()
|
||||
{
|
||||
if (cancellationTokenSource == null)
|
||||
throw new InvalidOperationException("Tried to restart a non-running Server!");
|
||||
cancellationTokenSource.Cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user