tgstation-server 6.12.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
SwarmController.cs
Go to the documentation of this file.
1using System;
2using System.Linq;
3using System.Net;
4using System.Net.Mime;
5using System.Threading;
6using System.Threading.Tasks;
7
8using Microsoft.AspNetCore.Authorization;
9using Microsoft.AspNetCore.Mvc;
10using Microsoft.Extensions.Logging;
11using Microsoft.Extensions.Options;
12
13using Serilog.Context;
14
21
23{
28 [ApiExplorerSettings(IgnoreApi = true)]
29 [AllowAnonymous] // We have custom private key auth
30 public sealed class SwarmController : ApiControllerBase
31 {
35 internal Guid RequestRegistrationId => Guid.Parse(Request.Headers[SwarmConstants.RegistrationIdHeader].First()!);
36
41
46
50 readonly ILogger<SwarmController> logger;
51
56
67 IOptions<SwarmConfiguration> swarmConfigurationOptions,
68 ILogger<SwarmController> logger)
69 {
70 this.swarmOperations = swarmOperations ?? throw new ArgumentNullException(nameof(swarmOperations));
71 this.transferService = transferService ?? throw new ArgumentNullException(nameof(transferService));
72 swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions));
73 this.logger = logger;
74 }
75
83 public async ValueTask<IActionResult> Register([FromBody] SwarmRegistrationRequest registrationRequest, CancellationToken cancellationToken)
84 {
85 ArgumentNullException.ThrowIfNull(registrationRequest);
86
87 var swarmProtocolVersion = Version.Parse(MasterVersionsAttribute.Instance.RawSwarmProtocolVersion);
88 if (registrationRequest.ServerVersion?.Major != swarmProtocolVersion.Major)
89 return StatusCode((int)HttpStatusCode.UpgradeRequired);
90
91 var registrationResult = await swarmOperations.RegisterNode(registrationRequest, RequestRegistrationId, cancellationToken);
92 if (registrationResult == null)
93 return Conflict();
94
95 if (registrationRequest.ServerVersion != swarmProtocolVersion)
96 logger.LogWarning("Allowed node {identifier} to register despite having a slightly different swarm protocol version!", registrationRequest.Identifier);
97
98 return Json(registrationResult);
99 }
100
106 [HttpDelete(SwarmConstants.RegisterRoute)]
107 public async ValueTask<IActionResult> UnregisterNode(CancellationToken cancellationToken)
108 {
110 return Forbid();
111
112 await swarmOperations.UnregisterNode(RequestRegistrationId, cancellationToken);
113 return NoContent();
114 }
115
120 [HttpGet]
121 public IActionResult HealthCheck()
122 {
124 return Forbid();
125
126 return NoContent();
127 }
128
135 [HttpGet(SwarmConstants.UpdateRoute)]
136 [Produces(MediaTypeNames.Application.Octet)]
137 public ValueTask<IActionResult> GetUpdatePackage([FromQuery] string ticket, CancellationToken cancellationToken)
138 => transferService.GenerateDownloadResponse(this, ticket, cancellationToken);
139
145 [HttpPost]
146 public IActionResult UpdateNodeList([FromBody] SwarmServersUpdateRequest serversUpdateRequest)
147 {
148 ArgumentNullException.ThrowIfNull(serversUpdateRequest);
149
150 if (serversUpdateRequest.SwarmServers == null)
151 return BadRequest();
152
154 return Forbid();
155
156 swarmOperations.UpdateSwarmServersList(serversUpdateRequest.SwarmServers);
157 return NoContent();
158 }
159
166 [HttpPut(SwarmConstants.UpdateRoute)]
167 public async ValueTask<IActionResult> PrepareUpdate([FromBody] SwarmUpdateRequest updateRequest, CancellationToken cancellationToken)
168 {
169 ArgumentNullException.ThrowIfNull(updateRequest);
170
172 return Forbid();
173
174 var prepareResult = await swarmOperations.PrepareUpdateFromController(updateRequest, cancellationToken);
175 if (!prepareResult)
176 return Conflict();
177
178 return NoContent();
179 }
180
186 [HttpPost(SwarmConstants.UpdateRoute)]
187 public async ValueTask<IActionResult> CommitUpdate(CancellationToken cancellationToken)
188 {
190 return Forbid();
191
192 var result = await swarmOperations.RemoteCommitReceived(RequestRegistrationId, cancellationToken);
193 if (!result)
194 return Conflict();
195 return NoContent();
196 }
197
202 [HttpDelete(SwarmConstants.UpdateRoute)]
203 public async ValueTask<IActionResult> AbortUpdate()
204 {
206 return Forbid();
207
209 return NoContent();
210 }
211
213 protected override async ValueTask<IActionResult?> HookExecuteAction(Func<Task> executeAction, CancellationToken cancellationToken)
214 {
215 using (LogContext.PushProperty(SerilogContextHelper.RequestPathContextProperty, $"{Request.Method} {Request.Path}"))
216 {
217 logger.LogTrace("Swarm request from {remoteIP}...", Request.HttpContext.Connection.RemoteIpAddress);
218 if (swarmConfiguration.PrivateKey == null)
219 {
220 logger.LogDebug("Attempted swarm request without private key configured!");
221 return Forbid();
222 }
223
224 if (!(Request.Headers.TryGetValue(SwarmConstants.ApiKeyHeader, out var apiKeyHeaderValues)
225 && apiKeyHeaderValues.Count == 1
226 && apiKeyHeaderValues.First() == swarmConfiguration.PrivateKey))
227 {
228 logger.LogDebug("Unauthorized swarm request!");
229 return Unauthorized();
230 }
231
232 if (!(Request.Headers.TryGetValue(SwarmConstants.RegistrationIdHeader, out var registrationHeaderValues)
233 && registrationHeaderValues.Count == 1
234 && Guid.TryParse(registrationHeaderValues.First(), out var registrationId)))
235 {
236 logger.LogDebug("Swarm request without registration ID!");
237 return BadRequest();
238 }
239
240 // we validate the registration itself on a case-by-case basis
241 if (ModelState?.IsValid == false)
242 {
243 var errorMessages = ModelState
244 .SelectMany(x => x.Value!.Errors)
245 .Select(x => x.ErrorMessage);
246
247 logger.LogDebug(
248 "Swarm request model validation failed!{newLine}{messages}",
249 Environment.NewLine,
250 String.Join(Environment.NewLine, errorMessages));
251 return BadRequest();
252 }
253
254 logger.LogTrace("Starting swarm request processing...");
255 await executeAction();
256 return null;
257 }
258 }
259
265 }
266}
Configuration for the server swarm system.
string? PrivateKey
The private key used for swarm communication.
Base class for all API style controllers.
readonly ISwarmOperations swarmOperations
The ISwarmOperations for the SwarmController.
bool ValidateRegistration()
Check that the RequestRegistrationId is valid.
ValueTask< IActionResult > GetUpdatePackage([FromQuery] string ticket, CancellationToken cancellationToken)
Endpoint to retrieve server update packages.
async ValueTask< IActionResult > Register([FromBody] SwarmRegistrationRequest registrationRequest, CancellationToken cancellationToken)
Registration endpoint.
IActionResult HealthCheck()
Health check endpoint.
override async ValueTask< IActionResult?> HookExecuteAction(Func< Task > executeAction, CancellationToken cancellationToken)
Hook for executing a request.A ValueTask<TResult> resulting in an IActionResult that,...
IActionResult UpdateNodeList([FromBody] SwarmServersUpdateRequest serversUpdateRequest)
Node list update endpoint.
readonly SwarmConfiguration swarmConfiguration
The SwarmConfiguration for the SwarmController.
SwarmController(ISwarmOperations swarmOperations, IFileTransferStreamHandler transferService, IOptions< SwarmConfiguration > swarmConfigurationOptions, ILogger< SwarmController > logger)
Initializes a new instance of the SwarmController class.
readonly ILogger< SwarmController > logger
The ILogger for the SwarmController.
async ValueTask< IActionResult > PrepareUpdate([FromBody] SwarmUpdateRequest updateRequest, CancellationToken cancellationToken)
Update initiation endpoint.
async ValueTask< IActionResult > AbortUpdate()
Update abort endpoint.
readonly IFileTransferStreamHandler transferService
The IFileTransferStreamHandler for the SwarmController.
async ValueTask< IActionResult > CommitUpdate(CancellationToken cancellationToken)
Update commit endpoint.
async ValueTask< IActionResult > UnregisterNode(CancellationToken cancellationToken)
Deregistration endpoint.
Attribute for bringing in the master versions list from MSBuild that aren't embedded into assemblies ...
string RawSwarmProtocolVersion
The Version string of the TGS swarm protocol.
static MasterVersionsAttribute Instance
Return the Assembly's instance of the MasterVersionsAttribute.
Constants used by the swarm system.
const string ApiKeyHeader
The header used to pass in the Configuration.SwarmConfiguration.PrivateKey.
const string UpdateRoute
The route used for swarm updates.
const string ControllerRoute
The base route for Controllers.SwarmController.
const string RegisterRoute
The route used for swarm registration.
const string RegistrationIdHeader
The header used to pass in swarm registration IDs.
A request to register with a swarm controller.
A request to update a nodes list of SwarmServers.
A request to update the swarm's TGS version.
Helpers for manipulating the Serilog.Context.LogContext.
const string RequestPathContextProperty
The Serilog.Context.LogContext property name for Models.User Api.Models.EntityId.Ids.
Swarm service operations for the Controllers.SwarmController.
ValueTask UnregisterNode(Guid registrationId, CancellationToken cancellationToken)
Attempt to unregister a node with a given registrationId with the controller.
ValueTask< bool > RemoteCommitReceived(Guid registrationId, CancellationToken cancellationToken)
Notify the controller that the node with the given registrationId is ready to commit or notify the n...
ValueTask< bool > PrepareUpdateFromController(SwarmUpdateRequest updateRequest, CancellationToken cancellationToken)
Notify the node of an update request from the controller.
bool ValidateRegistration(Guid registrationId)
Validate a given registrationId .
ValueTask< SwarmRegistrationResponse?> RegisterNode(SwarmServer node, Guid registrationId, CancellationToken cancellationToken)
Attempt to register a given node with the controller.
void UpdateSwarmServersList(IEnumerable< SwarmServerInformation > swarmServers)
Pass in an updated list of swarmServers to the node.
ValueTask AbortUpdate()
Attempt to abort an uncommitted update.
Reads and writes to Streams associated with FileTicketResponses.
@ Unauthorized
The swarm private keys didn't match.