mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-27 23:17:20 +01:00
The enitre fucking administration controller
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Tgstation.Server.Api.Rights;
|
||||
|
||||
namespace Tgstation.Server.Api.Models
|
||||
@@ -7,6 +6,12 @@ namespace Tgstation.Server.Api.Models
|
||||
/// <inheritdoc />
|
||||
public sealed class Administration
|
||||
{
|
||||
/// <summary>
|
||||
/// The GitHub repository the server is built to recieve updates from
|
||||
/// </summary>
|
||||
[Permissions(DenyWrite = true)]
|
||||
public Uri TrackedRepositoryUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The latest available version of the Tgstation.Server.Host assembly from the upstream repository. If <see cref="Version.Minor"/> is higher than <see cref="CurrentVersion"/>'s the update cannot be applied due to API changes
|
||||
/// </summary>
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Tgstation.Server.Api.Rights
|
||||
/// <summary>
|
||||
/// User can gracefully restart the host
|
||||
/// </summary>
|
||||
SoftStop = 2,
|
||||
RestartHost = 2,
|
||||
/// <summary>
|
||||
/// User can change <see cref="Models.Administration.CurrentVersion"/>
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace Tgstation.Server.Host.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration for the automatic update system
|
||||
/// </summary>
|
||||
sealed class UpdatesConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// The key for the <see cref="Microsoft.Extensions.Configuration.IConfigurationSection"/> the <see cref="UpdatesConfiguration"/> resides in
|
||||
/// </summary>
|
||||
public const string Section = "Updates";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Octokit.Repository.Id"/> of the tgstation-server fork to recieve updates from
|
||||
/// </summary>
|
||||
public long GitHubRepositoryId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Prefix before the <see cref="System.Version"/> of TGS published in git tags
|
||||
/// </summary>
|
||||
public string GitTagPrefix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Asset package containing the new <see cref="Host"/> assembly in zip form
|
||||
/// </summary>
|
||||
public string UpdatePackageAssetName { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Octokit;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Rights;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Security;
|
||||
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="ModelController{TModel}"/> for <see cref="Administration"/>
|
||||
/// </summary>
|
||||
[Route("/" + nameof(Administration))]
|
||||
sealed class AdministrationController : ModelController<Administration>
|
||||
{
|
||||
/// <summary>
|
||||
/// HTTP 429 status code
|
||||
/// </summary>
|
||||
const int RateLimitHttpStatusCode = 429;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IGitHubClient"/> for the <see cref="AdministrationController"/>
|
||||
/// </summary>
|
||||
readonly IGitHubClient gitHubClient;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IServerUpdater"/> for the <see cref="AdministrationController"/>
|
||||
/// </summary>
|
||||
readonly IServerUpdater serverUpdater;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IApplication"/> for the <see cref="AdministrationController"/>
|
||||
/// </summary>
|
||||
readonly IApplication application;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IIOManager"/> for the <see cref="AdministrationController"/>
|
||||
/// </summary>
|
||||
readonly IIOManager ioManager;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="AdministrationController"/>
|
||||
/// </summary>
|
||||
readonly ILogger<AdministrationController> logger;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="UpdatesConfiguration"/> for the <see cref="AdministrationController"/>
|
||||
/// </summary>
|
||||
readonly UpdatesConfiguration updatesConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// Construct an <see cref="AdministrationController"/>
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/></param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
|
||||
/// <param name="gitHubClient">The value of <see cref="gitHubClient"/></param>
|
||||
/// <param name="serverUpdater">The value of <see cref="serverUpdater"/></param>
|
||||
/// <param name="application">The value of <see cref="application"/></param>
|
||||
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
/// <param name="updatesConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing value of <see cref="updatesConfiguration"/></param>
|
||||
public AdministrationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IGitHubClient gitHubClient, IServerUpdater serverUpdater, IApplication application, IIOManager ioManager, ILogger<AdministrationController> logger, IOptions<UpdatesConfiguration> updatesConfigurationOptions) : base(databaseContext, authenticationContextFactory)
|
||||
{
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
this.gitHubClient = gitHubClient ?? throw new ArgumentNullException(nameof(gitHubClient));
|
||||
this.serverUpdater = serverUpdater ?? throw new ArgumentNullException(nameof(serverUpdater));
|
||||
this.application = application ?? throw new ArgumentNullException(nameof(application));
|
||||
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
|
||||
updatesConfiguration = updatesConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(updatesConfigurationOptions));
|
||||
}
|
||||
|
||||
StatusCodeResult RateLimit(RateLimitExceededException exception)
|
||||
{
|
||||
logger.LogWarning("Exceeded GitHub rate limit!");
|
||||
var secondsString = Math.Ceiling((exception.Reset - DateTimeOffset.Now).TotalSeconds).ToString(CultureInfo.InvariantCulture);
|
||||
Response.Headers.Add("Retry-After", new Microsoft.Extensions.Primitives.StringValues { });
|
||||
return StatusCode(RateLimitHttpStatusCode);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[TgsAuthorize]
|
||||
public override async Task<IActionResult> Read(CancellationToken cancellationToken)
|
||||
{
|
||||
var model = new Administration
|
||||
{
|
||||
CurrentVersion = application.Version
|
||||
};
|
||||
try
|
||||
{
|
||||
var repositoryTask = gitHubClient.Repository.Get(updatesConfiguration.GitHubRepositoryId);
|
||||
var releases = (await gitHubClient.Repository.Release.GetAll(updatesConfiguration.GitHubRepositoryId).ConfigureAwait(false)).Where(x => x.TagName.StartsWith(updatesConfiguration.GitTagPrefix, StringComparison.InvariantCulture));
|
||||
|
||||
Version greatestVersion = null;
|
||||
foreach (var I in releases)
|
||||
if (Version.TryParse(I.TagName.Replace(updatesConfiguration.GitTagPrefix, String.Empty), out var version)
|
||||
&& version.Major == application.Version.Major
|
||||
&& (greatestVersion == null || version > greatestVersion))
|
||||
greatestVersion = version;
|
||||
|
||||
model.LatestVersion = greatestVersion;
|
||||
model.TrackedRepositoryUrl = new Uri((await repositoryTask.ConfigureAwait(false)).HtmlUrl);
|
||||
}
|
||||
catch (RateLimitExceededException e)
|
||||
{
|
||||
return RateLimit(e);
|
||||
}
|
||||
return Json(model);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[TgsAuthorize(AdministrationRights.ChangeVersion)]
|
||||
public override async Task<IActionResult> Update([FromBody] Administration model, CancellationToken cancellationToken)
|
||||
{
|
||||
if (model == null)
|
||||
throw new ArgumentNullException(nameof(model));
|
||||
|
||||
if (model.CurrentVersion == null)
|
||||
return BadRequest(new { message = "Missing new version!" });
|
||||
|
||||
if (model.CurrentVersion.Major != application.Version.Major)
|
||||
return BadRequest(new { message = "Cannot update to a different suite version!" });
|
||||
|
||||
IEnumerable<Release> releases;
|
||||
try
|
||||
{
|
||||
releases = (await gitHubClient.Repository.Release.GetAll(updatesConfiguration.GitHubRepositoryId).ConfigureAwait(false)).Where(x => x.TagName.StartsWith(updatesConfiguration.GitTagPrefix, StringComparison.InvariantCulture));
|
||||
}
|
||||
catch (RateLimitExceededException e)
|
||||
{
|
||||
return RateLimit(e);
|
||||
}
|
||||
|
||||
foreach (var release in releases)
|
||||
if (Version.TryParse(release.TagName.Replace(updatesConfiguration.GitTagPrefix, String.Empty), out var version) && version == model.CurrentVersion)
|
||||
{
|
||||
var asset = release.Assets.Where(x => x.Name == updatesConfiguration.UpdatePackageAssetName).FirstOrDefault();
|
||||
if (asset == default)
|
||||
continue;
|
||||
|
||||
var assetBytes = await ioManager.DownloadFile(new Uri(asset.Url), cancellationToken).ConfigureAwait(false);
|
||||
await serverUpdater.ApplyUpdate(assetBytes, ioManager, cancellationToken).ConfigureAwait(false);
|
||||
return Ok(); //gtfo of here before all the cancellation tokens fire
|
||||
}
|
||||
|
||||
return StatusCode((int)HttpStatusCode.Gone);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Octokit;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
@@ -29,6 +30,11 @@ namespace Tgstation.Server.Host.Core
|
||||
/// <inheritdoc />
|
||||
sealed class Application : IApplication
|
||||
{
|
||||
/// <summary>
|
||||
/// Prefix for string version names
|
||||
/// </summary>
|
||||
const string VersionPrefix = "/tg/station server";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string HostingPath => serverAddresses.Addresses.First();
|
||||
|
||||
@@ -64,7 +70,7 @@ namespace Tgstation.Server.Host.Core
|
||||
this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
|
||||
|
||||
Version = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
VersionString = String.Format(CultureInfo.InvariantCulture, "/tg/station server v{0}", Version);
|
||||
VersionString = String.Format(CultureInfo.InvariantCulture, "{0} v{1}", VersionPrefix, Version);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -77,7 +83,8 @@ namespace Tgstation.Server.Host.Core
|
||||
{
|
||||
if (services == null)
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
var workingDir = Environment.CurrentDirectory;
|
||||
|
||||
services.Configure<UpdatesConfiguration>(configuration.GetSection(UpdatesConfiguration.Section));
|
||||
var databaseConfigurationSection = configuration.GetSection(DatabaseConfiguration.Section);
|
||||
services.Configure<DatabaseConfiguration>(databaseConfigurationSection);
|
||||
|
||||
@@ -146,9 +153,10 @@ namespace Tgstation.Server.Host.Core
|
||||
|
||||
services.AddSingleton<ICryptographySuite, CryptographySuite>();
|
||||
services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
|
||||
services.AddSingleton<IPasswordHasher<User>, PasswordHasher<User>>();
|
||||
services.AddSingleton<IPasswordHasher<Models.User>, PasswordHasher<Models.User>>();
|
||||
services.AddSingleton<ITokenFactory, TokenFactory>();
|
||||
services.AddSingleton<ISynchronousIOManager, SynchronousIOManager>();
|
||||
services.AddSingleton<IGitHubClient>(x => new GitHubClient(new ProductHeaderValue(VersionPrefix, Version.ToString())));
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Updates": {
|
||||
"GitHubRepositoryId": 92952846,
|
||||
"GitTagPrefix": "tgstation-server-v",
|
||||
"UpdatePackageAssetName": "ServerUpdatePackage.zip"
|
||||
},
|
||||
"Database": {
|
||||
"DatabaseType": "SqlServer",
|
||||
"ResetAdminPassword": false,
|
||||
|
||||
Reference in New Issue
Block a user