Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Some swarm mode improvements #1929

Merged
merged 6 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build/Version.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<TgsDmapiVersion>7.3.0</TgsDmapiVersion>
<TgsInteropVersion>5.10.0</TgsInteropVersion>
<TgsHostWatchdogVersion>1.5.0</TgsHostWatchdogVersion>
<TgsSwarmProtocolVersion>7.0.0</TgsSwarmProtocolVersion>
<TgsContainerScriptVersion>1.2.1</TgsContainerScriptVersion>
<TgsMigratorVersion>2.0.0</TgsMigratorVersion>
<TgsNugetNetFramework>netstandard2.0</TgsNugetNetFramework>
Expand Down
21 changes: 9 additions & 12 deletions src/Tgstation.Server.Host/Controllers/SwarmController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@

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;

Expand Down Expand Up @@ -44,11 +44,6 @@ public sealed class SwarmController : ApiControllerBase
/// </summary>
readonly IFileTransferStreamHandler transferService;

/// <summary>
/// The <see cref="IAssemblyInformationProvider"/> for the <see cref="SwarmController"/>.
/// </summary>
readonly IAssemblyInformationProvider assemblyInformationProvider;

/// <summary>
/// The <see cref="ILogger"/> for the <see cref="SwarmController"/>.
/// </summary>
Expand All @@ -63,19 +58,16 @@ public sealed class SwarmController : ApiControllerBase
/// Initializes a new instance of the <see cref="SwarmController"/> class.
/// </summary>
/// <param name="swarmOperations">The value of <see cref="swarmOperations"/>.</param>
/// <param name="assemblyInformationProvider">The value of <see cref="assemblyInformationProvider"/>.</param>
/// <param name="transferService">The value of <see cref="transferService"/>.</param>
/// <param name="swarmConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="swarmConfiguration"/>.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
public SwarmController(
ISwarmOperations swarmOperations,
IAssemblyInformationProvider assemblyInformationProvider,
IFileTransferStreamHandler transferService,
IOptions<SwarmConfiguration> swarmConfigurationOptions,
ILogger<SwarmController> 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;
Expand All @@ -92,13 +84,18 @@ public async ValueTask<IActionResult> Register([FromBody] SwarmRegistrationReque
{
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);
}

/// <summary>
Expand Down
2 changes: 2 additions & 0 deletions src/Tgstation.Server.Host/Database/DatabaseContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
// 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

/// <summary>
Expand Down Expand Up @@ -480,6 +481,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)

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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ sealed class MasterVersionsAttribute : Attribute
/// </summary>
public string RawMariaDBRedistVersion { get; }

/// <summary>
/// The <see cref="Version"/> <see cref="string"/> of the MariaDB server bundled with TGS installs.
/// </summary>
public string RawSwarmProtocolVersion { get; }

/// <summary>
/// Initializes a new instance of the <see cref="MasterVersionsAttribute"/> class.
/// </summary>
Expand All @@ -49,18 +54,21 @@ sealed class MasterVersionsAttribute : Attribute
/// <param name="rawWebpanelVersion">The value of <see cref="RawWebpanelVersion"/>.</param>
/// <param name="rawHostWatchdogVersion">The value of <see cref="RawHostWatchdogVersion"/>.</param>
/// <param name="rawMariaDBRedistVersion">The value of <see cref="RawMariaDBRedistVersion"/>.</param>
/// <param name="rawSwarmProtocolVersion">The value of <see cref="RawSwarmProtocolVersion"/>.</param>
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));
}
}
}
9 changes: 8 additions & 1 deletion src/Tgstation.Server.Host/Security/ITokenFactory.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Microsoft.IdentityModel.Tokens;
using System;

using Microsoft.IdentityModel.Tokens;

using Tgstation.Server.Api.Models.Response;

Expand All @@ -9,6 +11,11 @@ namespace Tgstation.Server.Host.Security
/// </summary>
public interface ITokenFactory
{
/// <summary>
/// Gets or sets the <see cref="ITokenFactory"/>'s signing key <see cref="byte"/>s.
/// </summary>
ReadOnlySpan<byte> SigningKeyBytes { get; set; }

/// <summary>
/// The <see cref="TokenValidationParameters"/> for the <see cref="ITokenFactory"/>.
/// </summary>
Expand Down
38 changes: 28 additions & 10 deletions src/Tgstation.Server.Host/Security/TokenFactory.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -21,20 +22,41 @@ sealed class TokenFactory : ITokenFactory
/// <inheritdoc />
public TokenValidationParameters ValidationParameters { get; }

/// <inheritdoc />
public ReadOnlySpan<byte> SigningKeyBytes
{
get => signingKey.Key;
[MemberNotNull(nameof(signingKey))]
[MemberNotNull(nameof(tokenHeader))]
set
{
signingKey = new SymmetricSecurityKey(value.ToArray());
tokenHeader = new JwtHeader(
new SigningCredentials(
signingKey,
SecurityAlgorithms.HmacSha256));
}
}

/// <summary>
/// The <see cref="SecurityConfiguration"/> for the <see cref="TokenFactory"/>.
/// </summary>
readonly SecurityConfiguration securityConfiguration;

/// <summary>
/// The <see cref="JwtHeader"/> for generating tokens.
/// The <see cref="JwtSecurityTokenHandler"/> used to generate <see cref="TokenResponse.Bearer"/> <see cref="string"/>s.
/// </summary>
readonly JwtHeader tokenHeader;
readonly JwtSecurityTokenHandler tokenHandler;

/// <summary>
/// The <see cref="JwtSecurityTokenHandler"/> used to generate <see cref="TokenResponse.Bearer"/> <see cref="string"/>s.
/// Backing field for <see cref="SigningKeyBytes"/>.
/// </summary>
readonly JwtSecurityTokenHandler tokenHandler;
SymmetricSecurityKey signingKey;

/// <summary>
/// The <see cref="JwtHeader"/> for generating tokens.
/// </summary>
JwtHeader tokenHeader;

/// <summary>
/// Initializes a new instance of the <see cref="TokenFactory"/> class.
Expand All @@ -52,14 +74,14 @@ public TokenFactory(

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,
Expand All @@ -75,10 +97,6 @@ public TokenFactory(
RequireExpirationTime = true,
};

tokenHeader = new JwtHeader(
new SigningCredentials(
ValidationParameters.IssuerSigningKey,
SecurityAlgorithms.HmacSha256));
tokenHandler = new JwtSecurityTokenHandler();
}

Expand Down
4 changes: 2 additions & 2 deletions src/Tgstation.Server.Host/Swarm/ISwarmOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ public interface ISwarmOperations : ISwarmUpdateAborter
/// <param name="node">The <see cref="SwarmServer"/> that is registering.</param>
/// <param name="registrationId">The registration <see cref="Guid"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in <see langword="true"/> if the registration was successful, <see langword="false"/> otherwise.</returns>
ValueTask<bool> RegisterNode(SwarmServer node, Guid registrationId, CancellationToken cancellationToken);
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="SwarmRegistrationResponse"/> if the registration was successful, <see langword="null"/> otherwise.</returns>
ValueTask<SwarmRegistrationResponse?> RegisterNode(SwarmServer node, Guid registrationId, CancellationToken cancellationToken);

/// <summary>
/// Attempt to unregister a node with a given <paramref name="registrationId"/> with the controller.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Tgstation.Server.Host.Swarm
public sealed class SwarmRegistrationRequest : SwarmServer
{
/// <summary>
/// The TGS <see cref="Version"/> of the sending server.
/// The swarm protocol <see cref="Version"/> of the sending server. Named this way due to legacy reasons.
/// </summary>
[Required]
public Version ServerVersion { get; }
Expand Down
13 changes: 13 additions & 0 deletions src/Tgstation.Server.Host/Swarm/SwarmRegistrationResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Tgstation.Server.Host.Swarm
{
/// <summary>
/// Response for a <see cref="SwarmRegistrationRequest"/>.
/// </summary>
public sealed class SwarmRegistrationResponse
{
/// <summary>
/// The base64 encoded token signing key.
/// </summary>
public required string TokenSigningKeyBase64 { get; init; }
}
}
5 changes: 5 additions & 0 deletions src/Tgstation.Server.Host/Swarm/SwarmRegistrationResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,10 @@ public enum SwarmRegistrationResult
/// A communication error occurred.
/// </summary>
CommunicationFailure,

/// <summary>
/// Response could not be deserialized.
/// </summary>
PayloadFailure,
}
}
Loading
Loading