From 830471f4b7a91c67cd8cc1a5b23f793509e0a468 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Wed, 25 Jul 2018 12:07:24 -0400 Subject: [PATCH] The enitre fucking administration controller --- .../Models/Administration.cs | 7 +- .../Rights/AdministrationRights.cs | 2 +- .../Configuration/UpdatesConfiguration.cs | 28 +++ .../Controllers/AdministrationController.cs | 160 ++++++++++++++++++ src/Tgstation.Server.Host/Core/Application.cs | 14 +- src/Tgstation.Server.Host/appsettings.json | 5 + 6 files changed, 211 insertions(+), 5 deletions(-) create mode 100644 src/Tgstation.Server.Host/Configuration/UpdatesConfiguration.cs create mode 100644 src/Tgstation.Server.Host/Controllers/AdministrationController.cs diff --git a/src/Tgstation.Server.Api/Models/Administration.cs b/src/Tgstation.Server.Api/Models/Administration.cs index 9e51bd7d3c..99421efdbe 100644 --- a/src/Tgstation.Server.Api/Models/Administration.cs +++ b/src/Tgstation.Server.Api/Models/Administration.cs @@ -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 /// public sealed class Administration { + /// + /// The GitHub repository the server is built to recieve updates from + /// + [Permissions(DenyWrite = true)] + public Uri TrackedRepositoryUrl { get; set; } + /// /// The latest available version of the Tgstation.Server.Host assembly from the upstream repository. If is higher than 's the update cannot be applied due to API changes /// diff --git a/src/Tgstation.Server.Api/Rights/AdministrationRights.cs b/src/Tgstation.Server.Api/Rights/AdministrationRights.cs index 58f208711c..3aaad4d552 100644 --- a/src/Tgstation.Server.Api/Rights/AdministrationRights.cs +++ b/src/Tgstation.Server.Api/Rights/AdministrationRights.cs @@ -19,7 +19,7 @@ namespace Tgstation.Server.Api.Rights /// /// User can gracefully restart the host /// - SoftStop = 2, + RestartHost = 2, /// /// User can change /// diff --git a/src/Tgstation.Server.Host/Configuration/UpdatesConfiguration.cs b/src/Tgstation.Server.Host/Configuration/UpdatesConfiguration.cs new file mode 100644 index 0000000000..3a5aff123a --- /dev/null +++ b/src/Tgstation.Server.Host/Configuration/UpdatesConfiguration.cs @@ -0,0 +1,28 @@ +namespace Tgstation.Server.Host.Configuration +{ + /// + /// Configuration for the automatic update system + /// + sealed class UpdatesConfiguration + { + /// + /// The key for the the resides in + /// + public const string Section = "Updates"; + + /// + /// The of the tgstation-server fork to recieve updates from + /// + public long GitHubRepositoryId { get; set; } + + /// + /// Prefix before the of TGS published in git tags + /// + public string GitTagPrefix { get; set; } + + /// + /// Asset package containing the new assembly in zip form + /// + public string UpdatePackageAssetName { get; set; } + } +} diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs new file mode 100644 index 0000000000..a7819ba632 --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -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 +{ + /// + /// for + /// + [Route("/" + nameof(Administration))] + sealed class AdministrationController : ModelController + { + /// + /// HTTP 429 status code + /// + const int RateLimitHttpStatusCode = 429; + + /// + /// The for the + /// + readonly IGitHubClient gitHubClient; + + /// + /// The for the + /// + readonly IServerUpdater serverUpdater; + + /// + /// The for the + /// + readonly IApplication application; + + /// + /// The for the + /// + readonly IIOManager ioManager; + + /// + /// The for the + /// + readonly ILogger logger; + + /// + /// The for the + /// + readonly UpdatesConfiguration updatesConfiguration; + + /// + /// Construct an + /// + /// The for the + /// The for the + /// The value of + /// The value of + /// The value of + /// The value of + /// The value of + /// The containing value of + public AdministrationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IGitHubClient gitHubClient, IServerUpdater serverUpdater, IApplication application, IIOManager ioManager, ILogger logger, IOptions 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); + } + + /// + [TgsAuthorize] + public override async Task 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); + } + + /// + [TgsAuthorize(AdministrationRights.ChangeVersion)] + public override async Task 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 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); + } + } +} diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 689ee11fba..48b5affc83 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -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 /// sealed class Application : IApplication { + /// + /// Prefix for string version names + /// + const string VersionPrefix = "/tg/station server"; + /// 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); } /// @@ -77,7 +83,8 @@ namespace Tgstation.Server.Host.Core { if (services == null) throw new ArgumentNullException(nameof(services)); - var workingDir = Environment.CurrentDirectory; + + services.Configure(configuration.GetSection(UpdatesConfiguration.Section)); var databaseConfigurationSection = configuration.GetSection(DatabaseConfiguration.Section); services.Configure(databaseConfigurationSection); @@ -146,9 +153,10 @@ namespace Tgstation.Server.Host.Core services.AddSingleton(); services.AddSingleton(); - services.AddSingleton, PasswordHasher>(); + services.AddSingleton, PasswordHasher>(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(x => new GitHubClient(new ProductHeaderValue(VersionPrefix, Version.ToString()))); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { diff --git a/src/Tgstation.Server.Host/appsettings.json b/src/Tgstation.Server.Host/appsettings.json index 8abbd1eb6a..3ea279ea3c 100644 --- a/src/Tgstation.Server.Host/appsettings.json +++ b/src/Tgstation.Server.Host/appsettings.json @@ -18,6 +18,11 @@ } } }, + "Updates": { + "GitHubRepositoryId": 92952846, + "GitTagPrefix": "tgstation-server-v", + "UpdatePackageAssetName": "ServerUpdatePackage.zip" + }, "Database": { "DatabaseType": "SqlServer", "ResetAdminPassword": false,