diff --git a/build/Version.props b/build/Version.props
index d0a850c6de..9c784ed4d7 100644
--- a/build/Version.props
+++ b/build/Version.props
@@ -12,6 +12,7 @@
7.3.0
5.10.0
1.5.0
+ 7.0.0
1.2.1
2.0.0
netstandard2.0
diff --git a/src/Tgstation.Server.Host/Controllers/SwarmController.cs b/src/Tgstation.Server.Host/Controllers/SwarmController.cs
index be171b19d8..dd57e45138 100644
--- a/src/Tgstation.Server.Host/Controllers/SwarmController.cs
+++ b/src/Tgstation.Server.Host/Controllers/SwarmController.cs
@@ -14,8 +14,8 @@ using Serilog.Context;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Extensions;
+using Tgstation.Server.Host.Properties;
using Tgstation.Server.Host.Swarm;
-using Tgstation.Server.Host.System;
using Tgstation.Server.Host.Transfer;
using Tgstation.Server.Host.Utils;
@@ -44,11 +44,6 @@ namespace Tgstation.Server.Host.Controllers
///
readonly IFileTransferStreamHandler transferService;
- ///
- /// The for the .
- ///
- readonly IAssemblyInformationProvider assemblyInformationProvider;
-
///
/// The for the .
///
@@ -63,19 +58,16 @@ namespace Tgstation.Server.Host.Controllers
/// Initializes a new instance of the class.
///
/// The value of .
- /// The value of .
/// The value of .
/// The containing the value of .
/// The value of .
public SwarmController(
ISwarmOperations swarmOperations,
- IAssemblyInformationProvider assemblyInformationProvider,
IFileTransferStreamHandler transferService,
IOptions swarmConfigurationOptions,
ILogger logger)
{
this.swarmOperations = swarmOperations ?? throw new ArgumentNullException(nameof(swarmOperations));
- this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
this.transferService = transferService ?? throw new ArgumentNullException(nameof(transferService));
swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions));
this.logger = logger;
@@ -92,13 +84,18 @@ namespace Tgstation.Server.Host.Controllers
{
ArgumentNullException.ThrowIfNull(registrationRequest);
- if (registrationRequest.ServerVersion != assemblyInformationProvider.Version)
+ var swarmProtocolVersion = Version.Parse(MasterVersionsAttribute.Instance.RawSwarmProtocolVersion);
+ if (registrationRequest.ServerVersion?.Major != swarmProtocolVersion.Major)
return StatusCode((int)HttpStatusCode.UpgradeRequired);
var registrationResult = await swarmOperations.RegisterNode(registrationRequest, RequestRegistrationId, cancellationToken);
- if (!registrationResult)
+ if (registrationResult == null)
return Conflict();
- return NoContent();
+
+ if (registrationRequest.ServerVersion != swarmProtocolVersion)
+ logger.LogWarning("Allowed node {identifier} to register despite having a slightly different swarm protocol version!", registrationRequest.Identifier);
+
+ return Json(registrationResult);
}
///
diff --git a/src/Tgstation.Server.Host/Database/DatabaseContext.cs b/src/Tgstation.Server.Host/Database/DatabaseContext.cs
index dbf724e45d..d79bb709f9 100644
--- a/src/Tgstation.Server.Host/Database/DatabaseContext.cs
+++ b/src/Tgstation.Server.Host/Database/DatabaseContext.cs
@@ -445,6 +445,7 @@ namespace Tgstation.Server.Host.Database
// HEY YOU
// IF YOU HAVE A TEST THAT'S CREATING ERRORS BECAUSE THESE VALUES AREN'T SET CORRECTLY THERE'S MORE TO FIXING IT THAN JUST UPDATING THEM
// IN THE FUNCTION BELOW YOU ALSO NEED TO CORRECTLY SET THE RIGHT MIGRATION TO DOWNGRADE TO FOR THE LAST TGS VERSION
+ // YOU ALSO NEED TO UPDATE THE SWARM PROTOCOL MAJOR VERSION
// IF THIS BREAKS AGAIN I WILL PERSONALLY HAUNT YOUR ASS WHEN I DIE
///
@@ -480,6 +481,7 @@ namespace Tgstation.Server.Host.Database
string BadDatabaseType() => throw new ArgumentException($"Invalid DatabaseType: {currentDatabaseType}", nameof(currentDatabaseType));
+ // !!! DON'T FORGET TO UPDATE THE SWARM PROTOCOL MAJOR VERSION !!!
if (targetVersion < new Version(6, 7, 0))
targetMigration = currentDatabaseType switch
{
diff --git a/src/Tgstation.Server.Host/Properties/MasterVersionsAttribute.cs b/src/Tgstation.Server.Host/Properties/MasterVersionsAttribute.cs
index d77bcd349b..8212c4ffee 100644
--- a/src/Tgstation.Server.Host/Properties/MasterVersionsAttribute.cs
+++ b/src/Tgstation.Server.Host/Properties/MasterVersionsAttribute.cs
@@ -41,6 +41,11 @@ namespace Tgstation.Server.Host.Properties
///
public string RawMariaDBRedistVersion { get; }
+ ///
+ /// The of the MariaDB server bundled with TGS installs.
+ ///
+ public string RawSwarmProtocolVersion { get; }
+
///
/// Initializes a new instance of the class.
///
@@ -49,18 +54,21 @@ namespace Tgstation.Server.Host.Properties
/// The value of .
/// The value of .
/// The value of .
+ /// The value of .
public MasterVersionsAttribute(
string rawConfigurationVersion,
string rawInteropVersion,
string rawWebpanelVersion,
string rawHostWatchdogVersion,
- string rawMariaDBRedistVersion)
+ string rawMariaDBRedistVersion,
+ string rawSwarmProtocolVersion)
{
RawConfigurationVersion = rawConfigurationVersion ?? throw new ArgumentNullException(nameof(rawConfigurationVersion));
RawInteropVersion = rawInteropVersion ?? throw new ArgumentNullException(nameof(rawInteropVersion));
RawWebpanelVersion = rawWebpanelVersion ?? throw new ArgumentNullException(nameof(rawWebpanelVersion));
RawHostWatchdogVersion = rawHostWatchdogVersion ?? throw new ArgumentNullException(nameof(rawHostWatchdogVersion));
RawMariaDBRedistVersion = rawMariaDBRedistVersion ?? throw new ArgumentNullException(nameof(rawMariaDBRedistVersion));
+ RawSwarmProtocolVersion = rawSwarmProtocolVersion ?? throw new ArgumentNullException(nameof(rawSwarmProtocolVersion));
}
}
}
diff --git a/src/Tgstation.Server.Host/Security/ITokenFactory.cs b/src/Tgstation.Server.Host/Security/ITokenFactory.cs
index bf9a70fab1..707c5d78ff 100644
--- a/src/Tgstation.Server.Host/Security/ITokenFactory.cs
+++ b/src/Tgstation.Server.Host/Security/ITokenFactory.cs
@@ -1,4 +1,6 @@
-using Microsoft.IdentityModel.Tokens;
+using System;
+
+using Microsoft.IdentityModel.Tokens;
using Tgstation.Server.Api.Models.Response;
@@ -9,6 +11,11 @@ namespace Tgstation.Server.Host.Security
///
public interface ITokenFactory
{
+ ///
+ /// Gets or sets the 's signing key s.
+ ///
+ ReadOnlySpan SigningKeyBytes { get; set; }
+
///
/// The for the .
///
diff --git a/src/Tgstation.Server.Host/Security/TokenFactory.cs b/src/Tgstation.Server.Host/Security/TokenFactory.cs
index fcc5990c2f..d2ab3c0527 100644
--- a/src/Tgstation.Server.Host/Security/TokenFactory.cs
+++ b/src/Tgstation.Server.Host/Security/TokenFactory.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
@@ -21,21 +22,42 @@ namespace Tgstation.Server.Host.Security
///
public TokenValidationParameters ValidationParameters { get; }
+ ///
+ public ReadOnlySpan SigningKeyBytes
+ {
+ get => signingKey.Key;
+ [MemberNotNull(nameof(signingKey))]
+ [MemberNotNull(nameof(tokenHeader))]
+ set
+ {
+ signingKey = new SymmetricSecurityKey(value.ToArray());
+ tokenHeader = new JwtHeader(
+ new SigningCredentials(
+ signingKey,
+ SecurityAlgorithms.HmacSha256));
+ }
+ }
+
///
/// The for the .
///
readonly SecurityConfiguration securityConfiguration;
- ///
- /// The for generating tokens.
- ///
- readonly JwtHeader tokenHeader;
-
///
/// The used to generate s.
///
readonly JwtSecurityTokenHandler tokenHandler;
+ ///
+ /// Backing field for .
+ ///
+ SymmetricSecurityKey signingKey;
+
+ ///
+ /// The for generating tokens.
+ ///
+ JwtHeader tokenHeader;
+
///
/// Initializes a new instance of the class.
///
@@ -52,14 +74,14 @@ namespace Tgstation.Server.Host.Security
securityConfiguration = securityConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(securityConfigurationOptions));
- var signingKeyBytes = String.IsNullOrWhiteSpace(securityConfiguration.CustomTokenSigningKeyBase64)
+ SigningKeyBytes = String.IsNullOrWhiteSpace(securityConfiguration.CustomTokenSigningKeyBase64)
? cryptographySuite.GetSecureBytes(securityConfiguration.TokenSigningKeyByteCount)
: Convert.FromBase64String(securityConfiguration.CustomTokenSigningKeyBase64);
ValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
- IssuerSigningKey = new SymmetricSecurityKey(signingKeyBytes),
+ IssuerSigningKeyResolver = (_, _, _, _) => Enumerable.Repeat(signingKey, 1),
ValidateIssuer = true,
ValidIssuer = assemblyInformationProvider.AssemblyName.Name,
@@ -75,10 +97,6 @@ namespace Tgstation.Server.Host.Security
RequireExpirationTime = true,
};
- tokenHeader = new JwtHeader(
- new SigningCredentials(
- ValidationParameters.IssuerSigningKey,
- SecurityAlgorithms.HmacSha256));
tokenHandler = new JwtSecurityTokenHandler();
}
diff --git a/src/Tgstation.Server.Host/Swarm/ISwarmOperations.cs b/src/Tgstation.Server.Host/Swarm/ISwarmOperations.cs
index 5a0889b7e9..c8ae31031a 100644
--- a/src/Tgstation.Server.Host/Swarm/ISwarmOperations.cs
+++ b/src/Tgstation.Server.Host/Swarm/ISwarmOperations.cs
@@ -39,8 +39,8 @@ namespace Tgstation.Server.Host.Swarm
/// The that is registering.
/// The registration .
/// The for the operation.
- /// A resulting in if the registration was successful, otherwise.
- ValueTask RegisterNode(SwarmServer node, Guid registrationId, CancellationToken cancellationToken);
+ /// A resulting in a if the registration was successful, otherwise.
+ ValueTask RegisterNode(SwarmServer node, Guid registrationId, CancellationToken cancellationToken);
///
/// Attempt to unregister a node with a given with the controller.
diff --git a/src/Tgstation.Server.Host/Swarm/SwarmRegistrationRequest.cs b/src/Tgstation.Server.Host/Swarm/SwarmRegistrationRequest.cs
index 229d4d28d2..f6bda1ac47 100644
--- a/src/Tgstation.Server.Host/Swarm/SwarmRegistrationRequest.cs
+++ b/src/Tgstation.Server.Host/Swarm/SwarmRegistrationRequest.cs
@@ -11,7 +11,7 @@ namespace Tgstation.Server.Host.Swarm
public sealed class SwarmRegistrationRequest : SwarmServer
{
///
- /// The TGS of the sending server.
+ /// The swarm protocol of the sending server. Named this way due to legacy reasons.
///
[Required]
public Version ServerVersion { get; }
diff --git a/src/Tgstation.Server.Host/Swarm/SwarmRegistrationResponse.cs b/src/Tgstation.Server.Host/Swarm/SwarmRegistrationResponse.cs
new file mode 100644
index 0000000000..a31858fa29
--- /dev/null
+++ b/src/Tgstation.Server.Host/Swarm/SwarmRegistrationResponse.cs
@@ -0,0 +1,13 @@
+namespace Tgstation.Server.Host.Swarm
+{
+ ///
+ /// Response for a .
+ ///
+ public sealed class SwarmRegistrationResponse
+ {
+ ///
+ /// The base64 encoded token signing key.
+ ///
+ public required string TokenSigningKeyBase64 { get; init; }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Swarm/SwarmRegistrationResult.cs b/src/Tgstation.Server.Host/Swarm/SwarmRegistrationResult.cs
index 87508af508..a754aa52cd 100644
--- a/src/Tgstation.Server.Host/Swarm/SwarmRegistrationResult.cs
+++ b/src/Tgstation.Server.Host/Swarm/SwarmRegistrationResult.cs
@@ -24,5 +24,10 @@
/// A communication error occurred.
///
CommunicationFailure,
+
+ ///
+ /// Response could not be deserialized.
+ ///
+ PayloadFailure,
}
}
diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs
index ea77de6a3c..9884ac7193 100644
--- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs
+++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs
@@ -24,6 +24,8 @@ using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.IO;
+using Tgstation.Server.Host.Properties;
+using Tgstation.Server.Host.Security;
using Tgstation.Server.Host.System;
using Tgstation.Server.Host.Transfer;
using Tgstation.Server.Host.Utils;
@@ -33,7 +35,9 @@ namespace Tgstation.Server.Host.Swarm
///
/// Helps keep servers connected to the same database in sync by coordinating updates.
///
+#pragma warning disable CA1506 // TODO: Decomplexify
sealed class SwarmService : ISwarmService, ISwarmServiceController, ISwarmOperations, IDisposable
+#pragma warning restore CA1506
{
///
public bool ExpectedNumberOfNodesConnected
@@ -89,6 +93,11 @@ namespace Tgstation.Server.Host.Swarm
///
readonly IFileTransferTicketProvider transferService;
+ ///
+ /// The for the .
+ ///
+ readonly ITokenFactory tokenFactory;
+
///
/// The for the .
///
@@ -159,6 +168,7 @@ namespace Tgstation.Server.Host.Swarm
/// The value of .
/// The value of .
/// The value of .
+ /// The value of .
/// The containing the value of .
/// The value of .
public SwarmService(
@@ -169,6 +179,7 @@ namespace Tgstation.Server.Host.Swarm
IAsyncDelayer asyncDelayer,
IServerUpdater serverUpdater,
IFileTransferTicketProvider transferService,
+ ITokenFactory tokenFactory,
IOptions swarmConfigurationOptions,
ILogger logger)
{
@@ -179,6 +190,7 @@ namespace Tgstation.Server.Host.Swarm
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
this.serverUpdater = serverUpdater ?? throw new ArgumentNullException(nameof(serverUpdater));
this.transferService = transferService ?? throw new ArgumentNullException(nameof(transferService));
+ this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
@@ -540,7 +552,7 @@ namespace Tgstation.Server.Host.Swarm
}
///
- public async ValueTask RegisterNode(SwarmServer node, Guid registrationId, CancellationToken cancellationToken)
+ public async ValueTask RegisterNode(SwarmServer node, Guid registrationId, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(node);
@@ -560,6 +572,11 @@ namespace Tgstation.Server.Host.Swarm
await AbortUpdate();
+ SwarmRegistrationResponse CreateResponse() => new()
+ {
+ TokenSigningKeyBase64 = Convert.ToBase64String(tokenFactory.SigningKeyBytes),
+ };
+
var registrationIdsAndTimes = this.registrationIdsAndTimes!;
lock (swarmServers)
{
@@ -569,7 +586,7 @@ namespace Tgstation.Server.Host.Swarm
if (preExistingRegistrationKvp.Key == node.Identifier)
{
logger.LogWarning("Node {nodeId} has already registered!", node.Identifier);
- return true;
+ return CreateResponse();
}
logger.LogWarning(
@@ -577,7 +594,7 @@ namespace Tgstation.Server.Host.Swarm
node.Identifier,
preExistingRegistrationKvp.Key,
registrationId);
- return false;
+ return null;
}
if (registrationIdsAndTimes.TryGetValue(node.Identifier, out var oldRegistration))
@@ -599,7 +616,7 @@ namespace Tgstation.Server.Host.Swarm
logger.LogInformation("Registered node {nodeId} ({nodeIP}) with ID {registrationId}", node.Identifier, node.Address, registrationId);
MarkServersDirty();
- return true;
+ return CreateResponse();
}
///
@@ -1268,7 +1285,7 @@ namespace Tgstation.Server.Host.Swarm
null,
HttpMethod.Post,
SwarmConstants.RegisterRoute,
- new SwarmRegistrationRequest(assemblyInformationProvider.Version)
+ new SwarmRegistrationRequest(Version.Parse(MasterVersionsAttribute.Instance.RawSwarmProtocolVersion))
{
Identifier = swarmConfiguration.Identifier,
Address = swarmConfiguration.Address,
@@ -1281,6 +1298,36 @@ namespace Tgstation.Server.Host.Swarm
using var response = await httpClient.SendAsync(registrationRequest, HttpCompletionOption.ResponseContentRead, cancellationToken);
if (response.IsSuccessStatusCode)
{
+ try
+ {
+ var json = await response.Content.ReadAsStringAsync(cancellationToken);
+ if (json == null)
+ {
+ logger.LogDebug("Error reading registration response content stream! Text was null!");
+ return SwarmRegistrationResult.PayloadFailure;
+ }
+
+ var registrationResponse = JsonConvert.DeserializeObject(json);
+ if (registrationResponse == null)
+ {
+ logger.LogDebug("Error reading registration response content stream! Payload was null!");
+ return SwarmRegistrationResult.PayloadFailure;
+ }
+
+ if (registrationResponse.TokenSigningKeyBase64 == null)
+ {
+ logger.LogDebug("Error reading registration response content stream! SigningKey was null!");
+ return SwarmRegistrationResult.PayloadFailure;
+ }
+
+ tokenFactory.SigningKeyBytes = Convert.FromBase64String(registrationResponse.TokenSigningKeyBase64);
+ }
+ catch (Exception ex)
+ {
+ logger.LogDebug(ex, "Error reading registration response content stream!");
+ return SwarmRegistrationResult.PayloadFailure;
+ }
+
logger.LogInformation("Sucessfully registered with ID {registrationId}", requestedRegistrationId);
controllerRegistration = requestedRegistrationId;
lastControllerHealthCheck = DateTimeOffset.UtcNow;
diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj
index 5510316328..8317ed2e64 100644
--- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj
+++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj
@@ -56,6 +56,7 @@
<_Parameter3>$(TgsWebpanelVersion)
<_Parameter4>$(TgsHostWatchdogVersion)
<_Parameter5>$(TgsMariaDBRedistVersion)
+ <_Parameter6>$(TgsSwarmProtocolVersion)
diff --git a/tests/Tgstation.Server.Host.Tests/Swarm/TestableSwarmNode.cs b/tests/Tgstation.Server.Host.Tests/Swarm/TestableSwarmNode.cs
index c293318381..2fbc888b1b 100644
--- a/tests/Tgstation.Server.Host.Tests/Swarm/TestableSwarmNode.cs
+++ b/tests/Tgstation.Server.Host.Tests/Swarm/TestableSwarmNode.cs
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Tokens;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
@@ -20,6 +21,7 @@ using Tgstation.Server.Host.Controllers;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.IO;
+using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
using Tgstation.Server.Host.System;
using Tgstation.Server.Host.Transfer;
@@ -75,6 +77,24 @@ namespace Tgstation.Server.Host.Swarm.Tests
}
}
+ private class MockTokenFactory : ITokenFactory
+ {
+ public ReadOnlySpan SigningKeyBytes
+ {
+ get => [0, 1, 2, 3, 4];
+ set
+ {
+ }
+ }
+
+ public TokenValidationParameters ValidationParameters => throw new NotSupportedException();
+
+ public TokenResponse CreateToken(User user, bool oAuth)
+ {
+ throw new NotSupportedException();
+ }
+ }
+
public TestableSwarmNode(
ILoggerFactory loggerFactory,
SwarmConfiguration swarmConfiguration,
@@ -138,7 +158,6 @@ namespace Tgstation.Server.Host.Swarm.Tests
RpcMapper = new SwarmRpcMapper(
(targetService, targetTransfer) => new SwarmController(
targetService,
- mockAssemblyInformationProvider.Object,
targetTransfer,
mockOptions.Object,
loggerFactory.CreateLogger()),
@@ -154,6 +173,8 @@ namespace Tgstation.Server.Host.Swarm.Tests
logger = loggerFactory.CreateLogger($"TestableSwarmNode-{swarmConfiguration.Identifier}");
+ var mockTokenFactory = new MockTokenFactory();
+
var runCount = 0;
void RecreateControllerAndService()
{
@@ -180,6 +201,7 @@ namespace Tgstation.Server.Host.Swarm.Tests
mockAsyncDelayer.Object,
mockServerUpdater.Object,
TransferService,
+ mockTokenFactory,
mockOptions.Object,
serviceLogger);
}
diff --git a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs
index c0063a1a1e..bc55b33077 100644
--- a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs
+++ b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs
@@ -670,8 +670,8 @@ namespace Tgstation.Server.Tests.Live
newUser.Name,
"asdfasdfasdfasdf");
- await using var node1BadClient = clientFactory.CreateFromToken(node1.RootUrl, controllerUserClient.Token);
- await ApiAssert.ThrowsException(() => node1BadClient.Administration.Read(false, cancellationToken));
+ await using var node1TokenCopiedClient = clientFactory.CreateFromToken(node1.RootUrl, controllerUserClient.Token);
+ await node1TokenCopiedClient.Administration.Read(false, cancellationToken);
// check instance info is not shared
var controllerInstance = await controllerClient.Instances.CreateOrAttach(