From 06dda10ac2a5106cbf793a926909d4b7d8652dc4 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Fri, 10 Nov 2023 10:04:09 -0500 Subject: [PATCH 1/8] Change the swagger documentation path to `/doc/tgs_api.json` Change hosted site path to `/documentation` Closes #1586 --- .github/workflows/ci-pipeline.yml | 8 ++++---- .../Configuration/GeneralConfiguration.cs | 2 +- src/Tgstation.Server.Host/Core/Application.cs | 11 +++++++++-- .../Utils/SwaggerConfiguration.cs | 7 ++++++- src/Tgstation.Server.Host/appsettings.yml | 2 +- tests/Tgstation.Server.Tests/Live/TestLiveServer.cs | 4 ++-- 6 files changed, 23 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 3a5e143e6b..123c939f48 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -478,7 +478,7 @@ jobs: uses: actions/upload-artifact@v3 with: name: openapi-spec - path: C:/swagger.json + path: C:/tgs_api.json - name: Package Server Service if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Basic' }} @@ -706,7 +706,7 @@ jobs: path: ./swagger - name: Lint OpenAPI Spec - run: npx lint-openapi -v -p -c build/OpenApiValidationSettings.json ./swagger/swagger.json + run: npx lint-openapi -v -p -c build/OpenApiValidationSettings.json ./swagger/tgs_api.json upload-code-coverage: name: Upload Code Coverage @@ -1311,7 +1311,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.DEV_PUSH_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./swagger/swagger.json + asset_path: ./swagger/tgs_api.json asset_name: swagger.json asset_content_type: application/json @@ -1615,7 +1615,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.DEV_PUSH_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./swagger/swagger.json + asset_path: ./swagger/tgs_api.json asset_name: swagger.json asset_content_type: application/json diff --git a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs index 7b02f6d177..1bb02a86e6 100644 --- a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs @@ -108,7 +108,7 @@ namespace Tgstation.Server.Host.Configuration public bool UseBasicWatchdog { get; set; } /// - /// If the swagger UI should be made avaiable. + /// If the swagger documentation and UI should be made avaiable. /// public bool HostApiDocumentation { get; set; } diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index d730b9de10..4d54a42b01 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -471,8 +471,15 @@ namespace Tgstation.Server.Host.Core if (generalConfiguration.HostApiDocumentation) { - applicationBuilder.UseSwagger(); - applicationBuilder.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "TGS API")); + applicationBuilder.UseSwagger(options => + { + options.RouteTemplate = Routes.Root + "doc/{documentName}.{json|yaml}"; + }); + applicationBuilder.UseSwaggerUI(options => + { + options.RoutePrefix = "documentation"; + options.SwaggerEndpoint(Routes.Root + $"doc/{SwaggerConfiguration.DocumentName}.json", "TGS API"); + }); logger.LogTrace("Swagger API generation enabled"); } diff --git a/src/Tgstation.Server.Host/Utils/SwaggerConfiguration.cs b/src/Tgstation.Server.Host/Utils/SwaggerConfiguration.cs index 89f7716732..927fc4f4fa 100644 --- a/src/Tgstation.Server.Host/Utils/SwaggerConfiguration.cs +++ b/src/Tgstation.Server.Host/Utils/SwaggerConfiguration.cs @@ -27,6 +27,11 @@ namespace Tgstation.Server.Host.Utils /// sealed class SwaggerConfiguration : IOperationFilter, IDocumentFilter, ISchemaFilter, IRequestBodyFilter { + /// + /// The name of the swagger document. + /// + public const string DocumentName = "tgs_api"; + /// /// The name for password authentication. /// @@ -51,7 +56,7 @@ namespace Tgstation.Server.Host.Utils public static void Configure(SwaggerGenOptions swaggerGenOptions, string assemblyDocumentationPath, string apiDocumentationPath) { swaggerGenOptions.SwaggerDoc( - "v1", + DocumentName, new OpenApiInfo { Title = "TGS API", diff --git a/src/Tgstation.Server.Host/appsettings.yml b/src/Tgstation.Server.Host/appsettings.yml index bebeb79c04..680c347e3d 100644 --- a/src/Tgstation.Server.Host/appsettings.yml +++ b/src/Tgstation.Server.Host/appsettings.yml @@ -13,7 +13,7 @@ General: UserGroupLimit: 25 # Maximum number of allowed groups InstanceLimit: 10 # Maximum number of allowed instances ValidInstancePaths: # An array of directories instances may be created in (either directly or as a subdirectory). null removes the restriction - HostApiDocumentation: false # Make HTTP API documentation available at /swagger/v1/swagger.json + HostApiDocumentation: false # Make HTTP API documentation available at /doc/tgs_api.json SkipAddingByondFirewallException: false # Windows Only: Prevent running netsh.exe to add a firewall exception for installed DreamDaemon binaries DeploymentDirectoryCopyTasksPerCore: 100 # Maximum number of concurrent file copy operations PER available CPU core Session: diff --git a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs index 0ef060c2f3..f8cf0c7802 100644 --- a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs +++ b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs @@ -1302,11 +1302,11 @@ namespace Tgstation.Server.Tests.Live // Dump swagger to disk // This is purely for CI using var httpClient = new HttpClient(); - var webRequestTask = httpClient.GetAsync(server.Url.ToString() + "swagger/v1/swagger.json", cancellationToken); + var webRequestTask = httpClient.GetAsync(server.Url.ToString() + "doc/tgs_api.json", cancellationToken); using var response = await webRequestTask; response.EnsureSuccessStatusCode(); await using var content = await response.Content.ReadAsStreamAsync(cancellationToken); - await using var output = new FileStream(@"C:\swagger.json", FileMode.Create); + await using var output = new FileStream(@"C:\tgs_api.json", FileMode.Create); await content.CopyToAsync(output, cancellationToken); } From 9e1806a2b4551b9369c80da0235b66e5774b3890 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Fri, 10 Nov 2023 12:11:28 -0500 Subject: [PATCH 2/8] Move all API functionality to `/api`. Move `BridgeController` route but BYOND forces us to support the legacy one as well. Add a basic homepage if it's not obvious where to redirect the user. Closes #1689 --- build/Version.props | 2 +- src/DMAPI/tgs/core/core.dm | 4 +- src/DMAPI/tgs/v5/__interop_version.dm | 2 +- src/DMAPI/tgs/v5/api.dm | 12 +- src/DMAPI/tgs/v5/bridge.dm | 4 +- src/Tgstation.Server.Api/Routes.cs | 32 ++--- src/Tgstation.Server.Client/ApiClient.cs | 2 +- src/Tgstation.Server.Client/IServerClient.cs | 2 +- src/Tgstation.Server.Client/ServerClient.cs | 2 +- .../ServerClientFactory.cs | 4 +- ...HomeController.cs => ApiRootController.cs} | 52 +++------ .../Controllers/BridgeController.cs | 11 +- .../Controllers/RootController.cs | 109 ++++++++++++++++++ src/Tgstation.Server.Host/Core/Application.cs | 6 +- .../Swarm/SwarmConstants.cs | 2 +- .../Utils/SwaggerConfiguration.cs | 7 +- .../Views/Root/Index.cshtml | 28 +++++ src/Tgstation.Server.Host/appsettings.yml | 2 +- tests/DMAPI/LongRunning/Test.dm | 30 +++++ .../Live/Instance/TestBridgeHandler.cs | 2 +- .../Live/Instance/WatchdogTest.cs | 10 ++ .../Live/LiveTestingServer.cs | 8 +- .../Live/RawRequestTests.cs | 18 +-- .../Live/TestLiveServer.cs | 57 ++++----- tests/Tgstation.Server.Tests/TestVersions.cs | 27 ++++- 25 files changed, 322 insertions(+), 113 deletions(-) rename src/Tgstation.Server.Host/Controllers/{HomeController.cs => ApiRootController.cs} (90%) create mode 100644 src/Tgstation.Server.Host/Controllers/RootController.cs create mode 100644 src/Tgstation.Server.Host/Views/Root/Index.cshtml diff --git a/build/Version.props b/build/Version.props index e3a0b04745..5bba820f55 100644 --- a/build/Version.props +++ b/build/Version.props @@ -10,7 +10,7 @@ 13.0.0 15.0.0 6.6.2 - 5.6.2 + 5.7.0 1.4.0 1.2.1 2.0.0 diff --git a/src/DMAPI/tgs/core/core.dm b/src/DMAPI/tgs/core/core.dm index b9a9f27a28..4a408e89a2 100644 --- a/src/DMAPI/tgs/core/core.dm +++ b/src/DMAPI/tgs/core/core.dm @@ -42,11 +42,11 @@ var/datum/tgs_version/max_api_version = TgsMaximumApiVersion(); if(version.suite != null && version.minor != null && version.patch != null && version.deprecated_patch != null && version.deprefixed_parameter > max_api_version.deprefixed_parameter) - TGS_ERROR_LOG("Detected unknown API version! Defaulting to latest. Update the DMAPI to fix this problem.") + TGS_ERROR_LOG("Detected unknown Interop API version! Defaulting to latest. Update the DMAPI to fix this problem.") api_datum = /datum/tgs_api/latest if(!api_datum) - TGS_ERROR_LOG("Found unsupported API version: [raw_parameter]. If this is a valid version please report this, backporting is done on demand.") + TGS_ERROR_LOG("Found unsupported Interop API version: [raw_parameter]. If this is a valid version please report this, backporting is done on demand.") return TGS_INFO_LOG("Activating API for version [version.deprefixed_parameter]") diff --git a/src/DMAPI/tgs/v5/__interop_version.dm b/src/DMAPI/tgs/v5/__interop_version.dm index 1b52b31d6a..83420d130a 100644 --- a/src/DMAPI/tgs/v5/__interop_version.dm +++ b/src/DMAPI/tgs/v5/__interop_version.dm @@ -1 +1 @@ -"5.6.2" +"5.7.0" diff --git a/src/DMAPI/tgs/v5/api.dm b/src/DMAPI/tgs/v5/api.dm index 7226f29bba..4a101d58dc 100644 --- a/src/DMAPI/tgs/v5/api.dm +++ b/src/DMAPI/tgs/v5/api.dm @@ -17,6 +17,8 @@ var/list/chat_channels var/initialized = FALSE + var/initial_bridge_request_received = FALSE + var/datum/tgs_version/interop_version var/chunked_requests = 0 var/list/chunked_topics = list() @@ -25,7 +27,8 @@ /datum/tgs_api/v5/New() . = ..() - TGS_DEBUG_LOG("V5 API created") + interop_version = version + TGS_DEBUG_LOG("V5 API created: [json_encode(args)]") /datum/tgs_api/v5/ApiVersion() return new /datum/tgs_version( @@ -38,7 +41,7 @@ access_identifier = world.params[DMAPI5_PARAM_ACCESS_IDENTIFIER] var/datum/tgs_version/api_version = ApiVersion() - version = null + version = null // we want this to be the TGS version, not the interop version var/list/bridge_response = Bridge(DMAPI5_BRIDGE_COMMAND_STARTUP, list(DMAPI5_BRIDGE_PARAMETER_MINIMUM_SECURITY_LEVEL = minimum_required_security_level, DMAPI5_BRIDGE_PARAMETER_VERSION = api_version.raw_parameter, DMAPI5_PARAMETER_CUSTOM_COMMANDS = ListCustomCommands())) if(!istype(bridge_response)) TGS_ERROR_LOG("Failed initial bridge request!") @@ -53,7 +56,8 @@ TGS_INFO_LOG("DMAPI validation, exiting...") TerminateWorld() - version = new /datum/tgs_version(runtime_information[DMAPI5_RUNTIME_INFORMATION_SERVER_VERSION]) + initial_bridge_request_received = TRUE + version = new /datum/tgs_version(runtime_information[DMAPI5_RUNTIME_INFORMATION_SERVER_VERSION]) // reassigning this because it can change if TGS updates security_level = runtime_information[DMAPI5_RUNTIME_INFORMATION_SECURITY_LEVEL] visibility = runtime_information[DMAPI5_RUNTIME_INFORMATION_VISIBILITY] instance_name = runtime_information[DMAPI5_RUNTIME_INFORMATION_INSTANCE_NAME] @@ -105,7 +109,7 @@ /datum/tgs_api/v5/proc/RequireInitialBridgeResponse() TGS_DEBUG_LOG("RequireInitialBridgeResponse()") var/logged = FALSE - while(!version) + while(!initial_bridge_request_received) if(!logged) TGS_DEBUG_LOG("RequireInitialBridgeResponse: Starting sleep") logged = TRUE diff --git a/src/DMAPI/tgs/v5/bridge.dm b/src/DMAPI/tgs/v5/bridge.dm index 37f58bcdf6..8e35dc3b1e 100644 --- a/src/DMAPI/tgs/v5/bridge.dm +++ b/src/DMAPI/tgs/v5/bridge.dm @@ -48,7 +48,9 @@ var/json = CreateBridgeData(command, data, TRUE) var/encoded_json = url_encode(json) - var/url = "http://127.0.0.1:[server_port]/Bridge?[DMAPI5_BRIDGE_DATA]=[encoded_json]" + var/api_prefix = interop_version.minor >= 7 ? "api/" : "" + + var/url = "http://127.0.0.1:[server_port]/[api_prefix]Bridge?[DMAPI5_BRIDGE_DATA]=[encoded_json]" return url /datum/tgs_api/v5/proc/CreateBridgeData(command, list/data, needs_auth) diff --git a/src/Tgstation.Server.Api/Routes.cs b/src/Tgstation.Server.Api/Routes.cs index b62d781604..862dd3994e 100644 --- a/src/Tgstation.Server.Api/Routes.cs +++ b/src/Tgstation.Server.Api/Routes.cs @@ -8,19 +8,19 @@ namespace Tgstation.Server.Api public static class Routes { /// - /// The root controller. + /// The root of API methods. /// - public const string Root = "/"; + public const string ApiRoot = "/api/"; /// /// The root route of all hubs. /// - public const string HubsRoot = Root + "hubs"; + public const string HubsRoot = ApiRoot + "hubs"; /// /// The server administration controller. /// - public const string Administration = Root + "Administration"; + public const string Administration = ApiRoot + "Administration"; /// /// The endpoint to download server logs. @@ -30,32 +30,32 @@ namespace Tgstation.Server.Api /// /// The user controller. /// - public const string User = Root + "User"; + public const string User = ApiRoot + "User"; /// /// The user group controller. /// - public const string UserGroup = Root + "UserGroup"; + public const string UserGroup = ApiRoot + "UserGroup"; /// /// The controller. /// - public const string InstanceManager = Root + "Instance"; + public const string InstanceManager = ApiRoot + "Instance"; /// /// The BYOND controller. /// - public const string Byond = Root + "Byond"; + public const string Byond = ApiRoot + "Byond"; /// /// The git repository controller. /// - public const string Repository = Root + "Repository"; + public const string Repository = ApiRoot + "Repository"; /// /// The DreamDaemon controller. /// - public const string DreamDaemon = Root + "DreamDaemon"; + public const string DreamDaemon = ApiRoot + "DreamDaemon"; /// /// For accessing DD diagnostics. @@ -65,7 +65,7 @@ namespace Tgstation.Server.Api /// /// The configuration controller. /// - public const string Configuration = Root + "Config"; + public const string Configuration = ApiRoot + "Config"; /// /// To be paired with for accessing s. @@ -80,27 +80,27 @@ namespace Tgstation.Server.Api /// /// The instance permission set controller. /// - public const string InstancePermissionSet = Root + "InstancePermissionSet"; + public const string InstancePermissionSet = ApiRoot + "InstancePermissionSet"; /// /// The chat bot controller. /// - public const string Chat = Root + "Chat"; + public const string Chat = ApiRoot + "Chat"; /// /// The deployment controller. /// - public const string DreamMaker = Root + "DreamMaker"; + public const string DreamMaker = ApiRoot + "DreamMaker"; /// /// The jobs controller. /// - public const string Jobs = Root + "Job"; + public const string Jobs = ApiRoot + "Job"; /// /// The transfer controller. /// - public const string Transfer = Root + "Transfer"; + public const string Transfer = ApiRoot + "Transfer"; /// /// The postfix for list operations. diff --git a/src/Tgstation.Server.Client/ApiClient.cs b/src/Tgstation.Server.Client/ApiClient.cs index d759ccbbdd..f691dd0e90 100644 --- a/src/Tgstation.Server.Client/ApiClient.cs +++ b/src/Tgstation.Server.Client/ApiClient.cs @@ -349,7 +349,7 @@ namespace Tgstation.Server.Client if (startingToken != headers.Token) return true; - var token = await RunRequest(Routes.Root, new object(), HttpMethod.Post, null, true, cancellationToken).ConfigureAwait(false); + var token = await RunRequest(Routes.ApiRoot, new object(), HttpMethod.Post, null, true, cancellationToken).ConfigureAwait(false); headers = new ApiHeaders(headers.UserAgent!, token); } finally diff --git a/src/Tgstation.Server.Client/IServerClient.cs b/src/Tgstation.Server.Client/IServerClient.cs index 65014f66b1..317e012ed0 100644 --- a/src/Tgstation.Server.Client/IServerClient.cs +++ b/src/Tgstation.Server.Client/IServerClient.cs @@ -16,7 +16,7 @@ namespace Tgstation.Server.Client public interface IServerClient : IAsyncDisposable { /// - /// The connected server . + /// The connected server's root . /// Uri Url { get; } diff --git a/src/Tgstation.Server.Client/ServerClient.cs b/src/Tgstation.Server.Client/ServerClient.cs index 186e32745b..6fce35fcf9 100644 --- a/src/Tgstation.Server.Client/ServerClient.cs +++ b/src/Tgstation.Server.Client/ServerClient.cs @@ -66,7 +66,7 @@ namespace Tgstation.Server.Client public ValueTask DisposeAsync() => apiClient.DisposeAsync(); /// - public ValueTask ServerInformation(CancellationToken cancellationToken) => apiClient.Read(Routes.Root, cancellationToken); + public ValueTask ServerInformation(CancellationToken cancellationToken) => apiClient.Read(Routes.ApiRoot, cancellationToken); /// public void AddRequestLogger(IRequestLogger requestLogger) => apiClient.AddRequestLogger(requestLogger); diff --git a/src/Tgstation.Server.Client/ServerClientFactory.cs b/src/Tgstation.Server.Client/ServerClientFactory.cs index 155198996a..98a51e8dc5 100644 --- a/src/Tgstation.Server.Client/ServerClientFactory.cs +++ b/src/Tgstation.Server.Client/ServerClientFactory.cs @@ -138,7 +138,7 @@ namespace Tgstation.Server.Client if (timeout.HasValue) api.Timeout = timeout.Value; - return await api.Read(Routes.Root, cancellationToken).ConfigureAwait(false); + return await api.Read(Routes.ApiRoot, cancellationToken).ConfigureAwait(false); } /// @@ -169,7 +169,7 @@ namespace Tgstation.Server.Client if (timeout.HasValue) api.Timeout = timeout.Value; - token = await api.Update(Routes.Root, cancellationToken).ConfigureAwait(false); + token = await api.Update(Routes.ApiRoot, cancellationToken).ConfigureAwait(false); } var apiHeaders = new ApiHeaders(productHeaderValue, token); diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/ApiRootController.cs similarity index 90% rename from src/Tgstation.Server.Host/Controllers/HomeController.cs rename to src/Tgstation.Server.Host/Controllers/ApiRootController.cs index 659a379591..fbfc1d4465 100644 --- a/src/Tgstation.Server.Host/Controllers/HomeController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiRootController.cs @@ -32,66 +32,66 @@ namespace Tgstation.Server.Host.Controllers /// /// Root for the . /// - [Route(Routes.Root)] - public sealed class HomeController : ApiController + [Route(Routes.ApiRoot)] + public sealed class ApiRootController : ApiController { /// - /// The for the . + /// The for the . /// readonly ITokenFactory tokenFactory; /// - /// The for the . + /// The for the . /// readonly ISystemIdentityFactory systemIdentityFactory; /// - /// The for the . + /// The for the . /// readonly ICryptographySuite cryptographySuite; /// - /// The for the . + /// The for the . /// readonly IAssemblyInformationProvider assemblyInformationProvider; /// - /// The for the . + /// The for the . /// readonly IIdentityCache identityCache; /// - /// The for the . + /// The for the . /// readonly IOAuthProviders oAuthProviders; /// - /// The for the . + /// The for the . /// readonly IPlatformIdentifier platformIdentifier; /// - /// The for the . + /// The for the . /// readonly ISwarmService swarmService; /// - /// The for the . + /// The for the . /// readonly IServerControl serverControl; /// - /// The for the . + /// The for the . /// readonly GeneralConfiguration generalConfiguration; /// - /// The for the . + /// The for the . /// readonly ControlPanelConfiguration controlPanelConfiguration; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The for the . /// The for the . @@ -108,7 +108,7 @@ namespace Tgstation.Server.Host.Controllers /// The containing the value of . /// The for the . /// The for the . - public HomeController( + public ApiRootController( IDatabaseContext databaseContext, IAuthenticationContext authenticationContext, ITokenFactory tokenFactory, @@ -122,7 +122,7 @@ namespace Tgstation.Server.Host.Controllers IServerControl serverControl, IOptions generalConfigurationOptions, IOptions controlPanelConfigurationOptions, - ILogger logger, + ILogger logger, IApiHeadersProvider apiHeadersProvider) : base( databaseContext, @@ -154,29 +154,12 @@ namespace Tgstation.Server.Host.Controllers [HttpGet] [AllowAnonymous] [ProducesResponseType(typeof(ServerInformationResponse), 200)] -#pragma warning disable CA1506 - public IActionResult Home() + public IActionResult ServerInfo() { - if (controlPanelConfiguration.Enable) - Response.Headers.Add( - HeaderNames.Vary, - new StringValues(ApiHeaders.ApiVersionHeader)); - // if they tried to authenticate in any form and failed, let them know immediately bool failIfUnauthed; if (ApiHeaders == null) { - if (controlPanelConfiguration.Enable && !Request.Headers.TryGetValue(ApiHeaders.ApiVersionHeader, out _)) - { - Logger.LogDebug("No API headers on request, redirecting to control panel..."); - - var controlPanelRoute = controlPanelConfiguration.PublicPath; - if (String.IsNullOrWhiteSpace(controlPanelRoute)) - controlPanelRoute = ControlPanelController.ControlPanelRoute; - - return Redirect(controlPanelRoute); - } - try { // we only allow authorization header issues @@ -211,7 +194,6 @@ namespace Tgstation.Server.Host.Controllers UpdateInProgress = serverControl.UpdateInProgress, }); } -#pragma warning restore CA1506 /// /// Attempt to authenticate a using . diff --git a/src/Tgstation.Server.Host/Controllers/BridgeController.cs b/src/Tgstation.Server.Host/Controllers/BridgeController.cs index 4ae2e0497c..86d67d19c0 100644 --- a/src/Tgstation.Server.Host/Controllers/BridgeController.cs +++ b/src/Tgstation.Server.Host/Controllers/BridgeController.cs @@ -8,9 +8,12 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; + using Newtonsoft.Json; + using Serilog.Context; +using Tgstation.Server.Api; using Tgstation.Server.Host.Components.Interop; using Tgstation.Server.Host.Components.Interop.Bridge; using Tgstation.Server.Host.Utils; @@ -20,10 +23,16 @@ namespace Tgstation.Server.Host.Controllers /// /// for recieving DMAPI requests from DreamDaemon. /// - [Route("/Bridge")] + [Route("/" + RouteExtension)] // obsolete route, but BYOND can't handle a simple fucking 301 + [Route(Routes.ApiRoot + RouteExtension)] [ApiExplorerSettings(IgnoreApi = true)] public sealed class BridgeController : ApiControllerBase { + /// + /// The route to the . + /// + const string RouteExtension = "Bridge"; + /// /// If the content of bridge requests and responses should be logged. /// diff --git a/src/Tgstation.Server.Host/Controllers/RootController.cs b/src/Tgstation.Server.Host/Controllers/RootController.cs new file mode 100644 index 0000000000..fd1bf36b11 --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/RootController.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; + +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.System; +using Tgstation.Server.Host.Utils; + +namespace Tgstation.Server.Host.Controllers +{ + /// + /// The root path . + /// + [Route("/")] + public sealed class RootController : Controller + { + /// + /// The route to the TGS logo .svg in the on Windows. + /// + public const string ProjectLogoSvgRouteWindows = "/0176d5d8b7d307f158e0.svg"; + + /// + /// The route to the TGS logo .svg in the on Linux. + /// + public const string ProjectLogoSvgRouteLinux = "/b5616c99bf2052a6bbd7.svg"; + + /// + /// The for the . + /// + readonly IAssemblyInformationProvider assemblyInformationProvider; + + /// + /// The for the . + /// + readonly IPlatformIdentifier platformIdentifier; + + /// + /// The for the . + /// + readonly GeneralConfiguration generalConfiguration; + + /// + /// The for the . + /// + readonly ControlPanelConfiguration controlPanelConfiguration; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The containing the value of . + /// The containing the value of . + public RootController( + IAssemblyInformationProvider assemblyInformationProvider, + IPlatformIdentifier platformIdentifier, + IOptions generalConfigurationOptions, + IOptions controlPanelConfigurationOptions) + { + this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); + this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); + generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); + controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions)); + } + + /// + /// Gets the server's homepage. + /// + /// The appropriate . + [HttpGet] + [AllowAnonymous] + public IActionResult Index() + { + const string ApiDocumentationRoute = "/" + SwaggerConfiguration.DocumentationSiteRouteExtension; + var panelEnabled = controlPanelConfiguration.Enable; + var apiDocsEnabled = generalConfiguration.HostApiDocumentation; + + if (panelEnabled ^ apiDocsEnabled) + if (panelEnabled) + return Redirect(ControlPanelController.ControlPanelRoute); + else + return Redirect(ApiDocumentationRoute); + + Dictionary links; + if (panelEnabled) + links = new Dictionary() + { + { "Web Control Panel", ControlPanelController.ControlPanelRoute.TrimStart('/') }, + { "API Documentation", SwaggerConfiguration.DocumentationSiteRouteExtension }, + }; + else + links = null; + + var model = new + { + Links = links, + Svg = platformIdentifier.IsWindows // these are different because of motherfucking line endings -_- + ? ProjectLogoSvgRouteWindows + : ProjectLogoSvgRouteLinux, + Title = assemblyInformationProvider.VersionString, + }; + + return View(model); + } + } +} diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 4d54a42b01..c3a4907f28 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -473,12 +473,12 @@ namespace Tgstation.Server.Host.Core { applicationBuilder.UseSwagger(options => { - options.RouteTemplate = Routes.Root + "doc/{documentName}.{json|yaml}"; + options.RouteTemplate = Routes.ApiRoot + "doc/{documentName}.{json|yaml}"; }); applicationBuilder.UseSwaggerUI(options => { - options.RoutePrefix = "documentation"; - options.SwaggerEndpoint(Routes.Root + $"doc/{SwaggerConfiguration.DocumentName}.json", "TGS API"); + options.RoutePrefix = SwaggerConfiguration.DocumentationSiteRouteExtension; + options.SwaggerEndpoint(Routes.ApiRoot + $"doc/{SwaggerConfiguration.DocumentName}.json", "TGS API"); }); logger.LogTrace("Swagger API generation enabled"); } diff --git a/src/Tgstation.Server.Host/Swarm/SwarmConstants.cs b/src/Tgstation.Server.Host/Swarm/SwarmConstants.cs index fe2b892c32..f5485cc280 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmConstants.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmConstants.cs @@ -14,7 +14,7 @@ namespace Tgstation.Server.Host.Swarm /// /// The base route for . /// - public const string ControllerRoute = Routes.Root + "Swarm"; + public const string ControllerRoute = Routes.ApiRoot + "Swarm"; /// /// The header used to pass in the . diff --git a/src/Tgstation.Server.Host/Utils/SwaggerConfiguration.cs b/src/Tgstation.Server.Host/Utils/SwaggerConfiguration.cs index 927fc4f4fa..8f8b0195bd 100644 --- a/src/Tgstation.Server.Host/Utils/SwaggerConfiguration.cs +++ b/src/Tgstation.Server.Host/Utils/SwaggerConfiguration.cs @@ -32,6 +32,11 @@ namespace Tgstation.Server.Host.Utils /// public const string DocumentName = "tgs_api"; + /// + /// The path to the hosted documentation site. + /// + public const string DocumentationSiteRouteExtension = "documentation"; + /// /// The name for password authentication. /// @@ -410,7 +415,7 @@ namespace Tgstation.Server.Host.Utils twoHundredResponseContents.Add(MediaTypeNames.Application.Octet, fileContent); } } - else if (context.MethodInfo.Name == nameof(HomeController.CreateToken)) + else if (context.MethodInfo.Name == nameof(ApiRootController.CreateToken)) { var passwordScheme = new OpenApiSecurityScheme { diff --git a/src/Tgstation.Server.Host/Views/Root/Index.cshtml b/src/Tgstation.Server.Host/Views/Root/Index.cshtml new file mode 100644 index 0000000000..209a6ad5ac --- /dev/null +++ b/src/Tgstation.Server.Host/Views/Root/Index.cshtml @@ -0,0 +1,28 @@ +@{ + var svgPath = Model.Svg; + var title = Model.Title; + + + + @title + + + + + @{ + if (Model.Links != null) + foreach (KeyValuePair kvp in Model.Links) + { +

+ @kvp.Key +

+ } + } + + +} diff --git a/src/Tgstation.Server.Host/appsettings.yml b/src/Tgstation.Server.Host/appsettings.yml index 680c347e3d..4ea862d7da 100644 --- a/src/Tgstation.Server.Host/appsettings.yml +++ b/src/Tgstation.Server.Host/appsettings.yml @@ -13,7 +13,7 @@ General: UserGroupLimit: 25 # Maximum number of allowed groups InstanceLimit: 10 # Maximum number of allowed instances ValidInstancePaths: # An array of directories instances may be created in (either directly or as a subdirectory). null removes the restriction - HostApiDocumentation: false # Make HTTP API documentation available at /doc/tgs_api.json + HostApiDocumentation: false # Make HTTP API documentation available at /api/doc/tgs_api.json SkipAddingByondFirewallException: false # Windows Only: Prevent running netsh.exe to add a firewall exception for installed DreamDaemon binaries DeploymentDirectoryCopyTasksPerCore: 100 # Maximum number of concurrent file copy operations PER available CPU core Session: diff --git a/tests/DMAPI/LongRunning/Test.dm b/tests/DMAPI/LongRunning/Test.dm index c2e274f4d4..c9cbf5cab1 100644 --- a/tests/DMAPI/LongRunning/Test.dm +++ b/tests/DMAPI/LongRunning/Test.dm @@ -180,6 +180,11 @@ var/run_bridge_test kajigger_test = TRUE return "we love casting spells" + var/its_sad = data["im_out_of_memes"] + if(its_sad) + TestLegacyBridge() + return "yeah gimmie a sec" + TgsChatBroadcast(new /datum/tgs_message_content("Recieved non-tgs topic: `[T]`")) return "feck" @@ -349,3 +354,28 @@ var/suppress_bridge_spam = FALSE FailTest("Failed to end bridge limit test! [(istype(final_result) ? json_encode(final_result): (final_result || "null"))]") api.access_identifier = old_ai + +/proc/TestLegacyBridge() + set waitfor = FALSE + + sleep(10) + + var/datum/tgs_api/v5/api = TGS_READ_GLOBAL(tgs) + if(api.interop_version.suite != 5) + FailTest("Legacy bridge test not required anymore?") + + var/old_minor_version = api.interop_version.minor + api.interop_version.minor = 6 // before api repath + + var/result + var/bridge_request = api.CreateBridgeRequest(5, list("chatMessage" = list("text" = "legacy bridge test", "channelIds" = list()))) + try + result = api.PerformBridgeRequest(bridge_request) + catch(var/exception/e2) + world.log << "Caught exception: [e2]" + result = null + + if(!result || lastTgsError) + FailTest("Failed bridge request redirect test!") + + api.interop_version.minor = old_minor_version diff --git a/tests/Tgstation.Server.Tests/Live/Instance/TestBridgeHandler.cs b/tests/Tgstation.Server.Tests/Live/Instance/TestBridgeHandler.cs index 53e0c43a51..4276067b19 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/TestBridgeHandler.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/TestBridgeHandler.cs @@ -88,7 +88,7 @@ namespace Tgstation.Server.Tests.Live.Instance Assert.AreEqual("payload", coreMessage); var serializedRequest = JsonConvert.SerializeObject(parameters, DMApiConstants.SerializerSettings); - var actualLastRequest = $"http://127.0.0.1:{serverPort}/Bridge?data=" + HttpUtility.UrlEncode(serializedRequest); + var actualLastRequest = $"http://127.0.0.1:{serverPort}/api/Bridge?data=" + HttpUtility.UrlEncode(serializedRequest); lastBridgeRequestSize = actualLastRequest.Length; return new BridgeResponseHack { diff --git a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs index 0a67dc8c02..7c64451fd1 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs @@ -219,6 +219,8 @@ namespace Tgstation.Server.Tests.Live.Instance await RegressionTest1550(cancellationToken); + await TestLegacyBridgeEndpoint(cancellationToken); + var deleteJobTask = TestDeleteByondInstallErrorCasesAndQueing(cancellationToken); SessionController.LogTopicRequests = false; @@ -1299,5 +1301,13 @@ namespace Tgstation.Server.Tests.Live.Instance var logtext = await File.ReadAllTextAsync(logfile.FullName, cancellationToken); Assert.IsFalse(String.IsNullOrWhiteSpace(logtext)); } + + async ValueTask TestLegacyBridgeEndpoint(CancellationToken cancellationToken) + { + var result = await topicClient.SendTopic(IPAddress.Loopback, "im_out_of_memes=1", ddPort, cancellationToken); + Assert.AreEqual("yeah gimmie a sec", result.StringData); + await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); + await CheckDMApiFail((await instanceClient.DreamDaemon.Read(cancellationToken)).ActiveCompileJob, cancellationToken); + } } } diff --git a/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs b/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs index 5f78b526b1..f43798005f 100644 --- a/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs +++ b/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Api; using Tgstation.Server.Api.Models; using Tgstation.Server.Host; using Tgstation.Server.Host.Configuration; @@ -50,7 +51,9 @@ namespace Tgstation.Server.Tests.Live } } - public Uri Url { get; } + public Uri ApiUrl { get; } + + public Uri RootUrl { get; } public string Directory { get; } @@ -82,7 +85,8 @@ namespace Tgstation.Server.Tests.Live Directory = Path.Combine(Directory, Guid.NewGuid().ToString()); System.IO.Directory.CreateDirectory(Directory); string urlString = $"http://localhost:{port}"; - Url = new Uri(urlString); + RootUrl = new Uri(urlString); + ApiUrl = new Uri(urlString + Routes.ApiRoot); //so we need a db //we have to rely on env vars diff --git a/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs b/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs index bf0d409e61..50e9b21169 100644 --- a/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs +++ b/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs @@ -37,7 +37,7 @@ namespace Tgstation.Server.Tests.Live var token = serverClient.Token.Bearer; // check that 400s are returned appropriately using var httpClient = new HttpClient(); - using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString())) + using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString() + Routes.ApiRoot.TrimStart('/'))) { request.Headers.Accept.Clear(); request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); @@ -45,7 +45,7 @@ namespace Tgstation.Server.Tests.Live Assert.AreEqual(HttpStatusCode.NotAcceptable, response.StatusCode); } - using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString())) + using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString() + Routes.ApiRoot.TrimStart('/'))) { request.Headers.Accept.Clear(); request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); @@ -54,7 +54,7 @@ namespace Tgstation.Server.Tests.Live Assert.AreEqual(HttpStatusCode.NotAcceptable, response.StatusCode); } - using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString())) + using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString() + Routes.ApiRoot.TrimStart('/'))) { request.Headers.Accept.Clear(); request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); @@ -66,7 +66,7 @@ namespace Tgstation.Server.Tests.Live Assert.AreEqual(ErrorCode.BadHeaders, message.ErrorCode); } - using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString())) + using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString() + Routes.ApiRoot.TrimStart('/'))) { request.Headers.Accept.Clear(); request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); @@ -79,7 +79,7 @@ namespace Tgstation.Server.Tests.Live Assert.AreEqual(ApiHeaders.Version, message.ApiVersion); } - using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString())) + using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString() + Routes.ApiRoot.TrimStart('/'))) { request.Headers.Accept.Clear(); request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); @@ -149,7 +149,7 @@ namespace Tgstation.Server.Tests.Live Assert.AreEqual(ErrorCode.InstanceHeaderRequired, message.ErrorCode); } - using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString())) + using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString() + Routes.ApiRoot.TrimStart('/'))) { request.Headers.Accept.Clear(); request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); @@ -163,7 +163,7 @@ namespace Tgstation.Server.Tests.Live Assert.AreEqual(ErrorCode.BadHeaders, message.ErrorCode); } - using (var request = new HttpRequestMessage(HttpMethod.Post, url.ToString())) + using (var request = new HttpRequestMessage(HttpMethod.Post, url.ToString() + Routes.ApiRoot.TrimStart('/'))) { request.Headers.Accept.Clear(); request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); @@ -178,7 +178,7 @@ namespace Tgstation.Server.Tests.Live Assert.AreEqual(ErrorCode.BadHeaders, message.ErrorCode); } - using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString())) + using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString() + Routes.ApiRoot.TrimStart('/'))) { request.Headers.Accept.Clear(); request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); @@ -232,7 +232,7 @@ namespace Tgstation.Server.Tests.Live // just hitting each type of oauth provider for coverage foreach (var I in Enum.GetValues(typeof(OAuthProvider))) - using (var request = new HttpRequestMessage(HttpMethod.Post, url.ToString())) + using (var request = new HttpRequestMessage(HttpMethod.Post, url.ToString() + Routes.ApiRoot.TrimStart('/'))) { request.Headers.Accept.Clear(); request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); diff --git a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs index f8cf0c7802..3daea816f7 100644 --- a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs +++ b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs @@ -255,11 +255,11 @@ namespace Tgstation.Server.Tests.Live return await action(); } - await using (var adminClient = await CreateAdminClient(server.Url, cancellationToken)) + await using (var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken)) { // Disabled OAuth test using (var httpClient = new HttpClient()) - using (var request = new HttpRequestMessage(HttpMethod.Post, server.Url.ToString())) + using (var request = new HttpRequestMessage(HttpMethod.Post, server.ApiUrl.ToString())) { request.Headers.Accept.Clear(); request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); @@ -336,7 +336,7 @@ namespace Tgstation.Server.Tests.Live await new Host.IO.DefaultIOManager().DeleteDirectory(server.UpdatePath, cancellationToken); serverTask = server.Run(cancellationToken).AsTask(); - await using (var adminClient = await CreateAdminClient(server.Url, cancellationToken)) + await using (var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken)) { // test we can't do this without the correct permission @@ -392,7 +392,7 @@ namespace Tgstation.Server.Tests.Live try { var testUpdateVersion = new Version(5, 11, 20); - await using var adminClient = await CreateAdminClient(server.Url, cancellationToken); + await using var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken); await ApiAssert.ThrowsException( () => adminClient.Administration.Update( new ServerUpdateRequest @@ -442,7 +442,7 @@ namespace Tgstation.Server.Tests.Live try { - await using var controllerClient = await CreateAdminClient(controller.Url, cancellationToken); + await using var controllerClient = await CreateAdminClient(controller.ApiUrl, cancellationToken); var controllerInfo = await controllerClient.ServerInformation(cancellationToken); @@ -544,9 +544,9 @@ namespace Tgstation.Server.Tests.Live try { - await using var controllerClient = await CreateAdminClient(controller.Url, cancellationToken); - await using var node1Client = await CreateAdminClient(node1.Url, cancellationToken); - await using var node2Client = await CreateAdminClient(node2.Url, cancellationToken); + await using var controllerClient = await CreateAdminClient(controller.ApiUrl, cancellationToken); + await using var node1Client = await CreateAdminClient(node1.ApiUrl, cancellationToken); + await using var node2Client = await CreateAdminClient(node2.ApiUrl, cancellationToken); var controllerInfo = await controllerClient.ServerInformation(cancellationToken); @@ -615,7 +615,7 @@ namespace Tgstation.Server.Tests.Live newUser.Name, "asdfasdfasdfasdf"); - await using var node1BadClient = clientFactory.CreateFromToken(node1.Url, controllerUserClient.Token); + await using var node1BadClient = clientFactory.CreateFromToken(node1.RootUrl, controllerUserClient.Token); await ApiAssert.ThrowsException(() => node1BadClient.Administration.Read(cancellationToken)); // check instance info is not shared @@ -685,8 +685,8 @@ namespace Tgstation.Server.Tests.Live controller.Run(cancellationToken).AsTask(), node1.Run(cancellationToken).AsTask()); - await using var controllerClient2 = await CreateAdminClient(controller.Url, cancellationToken); - await using var node1Client2 = await CreateAdminClient(node1.Url, cancellationToken); + await using var controllerClient2 = await CreateAdminClient(controller.ApiUrl, cancellationToken); + await using var node1Client2 = await CreateAdminClient(node1.ApiUrl, cancellationToken); await ApiAssert.ThrowsException(() => controllerClient2.Administration.Update( new ServerUpdateRequest @@ -701,7 +701,7 @@ namespace Tgstation.Server.Tests.Live serverTask, node2.Run(cancellationToken).AsTask()); - await using var node2Client2 = await CreateAdminClient(node2.Url, cancellationToken); + await using var node2Client2 = await CreateAdminClient(node2.ApiUrl, cancellationToken); async Task WaitForSwarmServerUpdate2() { @@ -815,9 +815,9 @@ namespace Tgstation.Server.Tests.Live try { - await using var controllerClient = await CreateAdminClient(controller.Url, cancellationToken); - await using var node1Client = await CreateAdminClient(node1.Url, cancellationToken); - await using var node2Client = await CreateAdminClient(node2.Url, cancellationToken); + await using var controllerClient = await CreateAdminClient(controller.ApiUrl, cancellationToken); + await using var node1Client = await CreateAdminClient(node1.ApiUrl, cancellationToken); + await using var node2Client = await CreateAdminClient(node2.ApiUrl, cancellationToken); var controllerInfo = await controllerClient.ServerInformation(cancellationToken); @@ -897,7 +897,7 @@ namespace Tgstation.Server.Tests.Live Assert.IsTrue(controllerTask.IsCompleted); controllerTask = controller.Run(cancellationToken).AsTask(); - await using var controllerClient2 = await CreateAdminClient(controller.Url, cancellationToken); + await using var controllerClient2 = await CreateAdminClient(controller.ApiUrl, cancellationToken); // node 2 should reconnect once it's health check triggers await Task.WhenAny( @@ -934,7 +934,7 @@ namespace Tgstation.Server.Tests.Live ErrorCode.SwarmIntegrityCheckFailed); node2Task = node2.Run(cancellationToken).AsTask(); - await using var node2Client2 = await CreateAdminClient(node2.Url, cancellationToken); + await using var node2Client2 = await CreateAdminClient(node2.ApiUrl, cancellationToken); // should re-register await Task.WhenAny( @@ -991,7 +991,7 @@ namespace Tgstation.Server.Tests.Live var serverTask = server.Run(cancellationToken); try { - await using var adminClient = await CreateAdminClient(server.Url, cancellationToken); + await using var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken); var instanceManagerTest = new InstanceManagerTest(adminClient, server.Directory); var instance = await instanceManagerTest.CreateTestInstance("TgTestInstance", cancellationToken); @@ -1273,7 +1273,7 @@ namespace Tgstation.Server.Tests.Live { Api.Models.Instance instance; long initialStaged, initialActive; - await using var firstAdminClient = await CreateAdminClient(server.Url, cancellationToken); + await using var firstAdminClient = await CreateAdminClient(server.ApiUrl, cancellationToken); async ValueTask CreateUserWithNoInstancePerms() { @@ -1291,7 +1291,7 @@ namespace Tgstation.Server.Tests.Live var user = await firstAdminClient.Users.Create(createRequest, cancellationToken); Assert.IsTrue(user.Enabled); - return await clientFactory.CreateFromLogin(server.Url, createRequest.Name, createRequest.Password, cancellationToken: cancellationToken); + return await clientFactory.CreateFromLogin(server.RootUrl, createRequest.Name, createRequest.Password, cancellationToken: cancellationToken); } var jobsHubTest = new JobsHubTests(firstAdminClient, await CreateUserWithNoInstancePerms()); @@ -1302,7 +1302,7 @@ namespace Tgstation.Server.Tests.Live // Dump swagger to disk // This is purely for CI using var httpClient = new HttpClient(); - var webRequestTask = httpClient.GetAsync(server.Url.ToString() + "doc/tgs_api.json", cancellationToken); + var webRequestTask = httpClient.GetAsync(server.ApiUrl.ToString() + "doc/tgs_api.json", cancellationToken); using var response = await webRequestTask; response.EnsureSuccessStatusCode(); await using var content = await response.Content.ReadAsStreamAsync(cancellationToken); @@ -1343,7 +1343,7 @@ namespace Tgstation.Server.Tests.Live firstAdminClient.Instances, fileDownloader, GetInstanceManager(), - (ushort)server.Url.Port); + (ushort)server.ApiUrl.Port); async Task RunInstanceTests() { @@ -1415,7 +1415,7 @@ namespace Tgstation.Server.Tests.Live using var blockingSocket = new Socket(SocketType.Stream, ProtocolType.Tcp); blockingSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, true); blockingSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, false); - blockingSocket.Bind(new IPEndPoint(IPAddress.Any, server.Url.Port)); + blockingSocket.Bind(new IPEndPoint(IPAddress.Any, server.ApiUrl.Port)); // bind test run await server.Run(cancellationToken); Assert.Fail("Expected server task to end with a SocketException"); @@ -1437,7 +1437,7 @@ namespace Tgstation.Server.Tests.Live // chat bot start and DD reattach test serverTask = server.Run(cancellationToken).AsTask(); - await using (var adminClient = await CreateAdminClient(server.Url, cancellationToken)) + await using (var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken)) { await jobsHubTest.WaitForReconnect(cancellationToken); var instanceClient = adminClient.Instances.CreateClient(instance); @@ -1541,7 +1541,7 @@ namespace Tgstation.Server.Tests.Live serverTask = server.Run(cancellationToken).AsTask(); long expectedCompileJobId, expectedStaged; var edgeByond = await ByondTest.GetEdgeVersion(fileDownloader, cancellationToken); - await using (var adminClient = await CreateAdminClient(server.Url, cancellationToken)) + await using (var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken)) { var instanceClient = adminClient.Instances.CreateClient(instance); await WaitForInitialJobs(instanceClient); @@ -1552,7 +1552,7 @@ namespace Tgstation.Server.Tests.Live Assert.AreEqual(WatchdogStatus.Online, dd.Status.Value); var compileJob = await instanceClient.DreamMaker.Compile(cancellationToken); - var wdt = new WatchdogTest(edgeByond, instanceClient, GetInstanceManager(), (ushort)server.Url.Port, server.HighPriorityDreamDaemon, mainDDPort, server.UsingBasicWatchdog); + var wdt = new WatchdogTest(edgeByond, instanceClient, GetInstanceManager(), (ushort)server.ApiUrl.Port, server.HighPriorityDreamDaemon, mainDDPort, server.UsingBasicWatchdog); await wdt.WaitForJob(compileJob, 30, false, null, cancellationToken); dd = await instanceClient.DreamDaemon.Read(cancellationToken); @@ -1590,7 +1590,7 @@ namespace Tgstation.Server.Tests.Live // post/entity deletion tests serverTask = server.Run(cancellationToken).AsTask(); - await using (var adminClient = await CreateAdminClient(server.Url, cancellationToken)) + await using (var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken)) { var instanceClient = adminClient.Instances.CreateClient(instance); await WaitForInitialJobs(instanceClient); @@ -1601,7 +1601,7 @@ namespace Tgstation.Server.Tests.Live Assert.AreEqual(WatchdogStatus.Online, currentDD.Status); Assert.AreEqual(expectedStaged, currentDD.StagedCompileJob.Job.Id.Value); - var wdt = new WatchdogTest(edgeByond, instanceClient, GetInstanceManager(), (ushort)server.Url.Port, server.HighPriorityDreamDaemon, mainDDPort, server.UsingBasicWatchdog); + var wdt = new WatchdogTest(edgeByond, instanceClient, GetInstanceManager(), (ushort)server.ApiUrl.Port, server.HighPriorityDreamDaemon, mainDDPort, server.UsingBasicWatchdog); currentDD = await wdt.TellWorldToReboot(cancellationToken); Assert.AreEqual(expectedStaged, currentDD.ActiveCompileJob.Job.Id.Value); Assert.IsNull(currentDD.StagedCompileJob); @@ -1644,6 +1644,7 @@ namespace Tgstation.Server.Tests.Live async Task CreateAdminClient(Uri url, CancellationToken cancellationToken) { + url = new Uri(url.ToString().Replace(Routes.ApiRoot, String.Empty)); var giveUpAt = DateTimeOffset.UtcNow.AddMinutes(2); for (var I = 1; ; ++I) { diff --git a/tests/Tgstation.Server.Tests/TestVersions.cs b/tests/Tgstation.Server.Tests/TestVersions.cs index 41c48a1196..68ac407ef6 100644 --- a/tests/Tgstation.Server.Tests/TestVersions.cs +++ b/tests/Tgstation.Server.Tests/TestVersions.cs @@ -1,9 +1,11 @@ using System; using System.IO; using System.IO.Compression; +using System.Globalization; using System.Linq; using System.Net.Http; using System.Reflection; +using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; @@ -27,7 +29,7 @@ using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.System; -using System.Net; +using Tgstation.Server.Host.Controllers; namespace Tgstation.Server.Tests { @@ -380,6 +382,29 @@ namespace Tgstation.Server.Tests Assert.AreEqual(latestMigrationSL, DatabaseContext.SLLatestMigration); } + [TestMethod] + public async Task CheckWebRootPathForTgsLogo() + { + var directory = Path.GetFullPath("../../../../../src/Tgstation.Server.Host/wwwroot"); + if (!Directory.Exists(directory)) + Assert.Inconclusive("Webpanel not built?"); + + var logo = new PlatformIdentifier().IsWindows + ? RootController.ProjectLogoSvgRouteWindows + : RootController.ProjectLogoSvgRouteLinux; + + var path = $"../../../../../src/Tgstation.Server.Host/wwwroot{logo}"; + Assert.IsTrue(File.Exists(path)); + + var content = await File.ReadAllBytesAsync(path); + var hash = String.Join(String.Empty, SHA1.HashData(content).Select(b => b.ToString("x2", CultureInfo.InvariantCulture))); + Assert.AreEqual( + new PlatformIdentifier().IsWindows + ? "c5e4709774c14a6f376dbb5100bd80a0114a2287" + : "9eba2fac24c5c7e0008721690d07c3df575a00d6", + hash); + } + static async Task> GetByondVersionPriorTo(IByondInstaller byondInstaller, Version version) { var minusOneMinor = new Version(version.Major, version.Minor - 1); From 2413dc84f0d22ddecb46f93544b7cfce4d8e50e7 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Fri, 10 Nov 2023 23:21:06 -0500 Subject: [PATCH 3/8] Fix .deb `postinst` conditional. --- build/package/deb/debian/postinst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/package/deb/debian/postinst b/build/package/deb/debian/postinst index 9f71a1931f..a9ce7b9696 100755 --- a/build/package/deb/debian/postinst +++ b/build/package/deb/debian/postinst @@ -2,7 +2,7 @@ #DEBHELPER# -if [ "$1" = "configure" ]; then +if [ -z "$2" ]; then chmod 600 /etc/tgstation-server deb-systemd-helper stop 'tgstation-server.service' >/dev/null || true From 22bd831c8c89d027eac699db8945e2ca017119fe Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Fri, 10 Nov 2023 23:23:19 -0500 Subject: [PATCH 4/8] Only create `tgstation-server` user + other permission fixes on first install. --- build/package/deb/debian/postinst | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/build/package/deb/debian/postinst b/build/package/deb/debian/postinst index 9a1cb1b13c..664ce89a97 100755 --- a/build/package/deb/debian/postinst +++ b/build/package/deb/debian/postinst @@ -1,10 +1,12 @@ #!/bin/sh -e -adduser --system tgstation-server -mkdir -m 754 -p /var/log/tgstation-server -chown -R tgstation-server /etc/tgstation-server -chown -R tgstation-server /opt/tgstation-server -chown -R tgstation-server /var/log/tgstation-server +if [ -z "$2" ]; then + adduser --system tgstation-server + mkdir -m 754 -p /var/log/tgstation-server + chown -R tgstation-server /etc/tgstation-server + chown -R tgstation-server /opt/tgstation-server + chown -R tgstation-server /var/log/tgstation-server +fi #DEBHELPER# From 1527900571b25393a57c6dc28e81212cbf43f4c4 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Fri, 10 Nov 2023 23:23:46 -0500 Subject: [PATCH 5/8] Fix .deb installation directory ownership --- build/package/deb/debian/postinst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/package/deb/debian/postinst b/build/package/deb/debian/postinst index 664ce89a97..a40084e5eb 100755 --- a/build/package/deb/debian/postinst +++ b/build/package/deb/debian/postinst @@ -4,7 +4,7 @@ if [ -z "$2" ]; then adduser --system tgstation-server mkdir -m 754 -p /var/log/tgstation-server chown -R tgstation-server /etc/tgstation-server - chown -R tgstation-server /opt/tgstation-server + chown -R tgstation-server /opt/tgstation-server/lib chown -R tgstation-server /var/log/tgstation-server fi From 30293c3e39052ead17c1a67ee2b549f6409ad9eb Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sat, 11 Nov 2023 21:31:07 -0500 Subject: [PATCH 6/8] Fix repository fetching possibly not fetching all tags --- .../Components/Repository/Repository.cs | 1 + .../Tgstation.Server.Tests/TestRepository.cs | 80 ++++++++++++++++++- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs index 46cc06fd8e..ea7844c2a8 100644 --- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs @@ -450,6 +450,7 @@ namespace Tgstation.Server.Host.Components.Repository OnTransferProgress = TransferProgressHandler(progressReporter.CreateSection("Fetch Origin", 1.0), cancellationToken), OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested, CredentialsProvider = credentialsProvider.GenerateCredentialsHandler(username, password), + TagFetchMode = TagFetchMode.All, }, "Fetch origin commits"); } diff --git a/tests/Tgstation.Server.Tests/TestRepository.cs b/tests/Tgstation.Server.Tests/TestRepository.cs index e03fbe984a..d0549409f9 100644 --- a/tests/Tgstation.Server.Tests/TestRepository.cs +++ b/tests/Tgstation.Server.Tests/TestRepository.cs @@ -1,8 +1,11 @@ using System; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using LibGit2Sharp; + using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -28,7 +31,7 @@ namespace Tgstation.Server.Tests { LibGit2Sharp.Repository.Clone("https://github.com/Cyberboss/test", tempPath); var libGit2Repo = new LibGit2Sharp.Repository(tempPath); - using var repo = new Repository( + using var repo = new Host.Components.Repository.Repository( libGit2Repo, new LibGit2Commands(), Mock.Of(), @@ -36,7 +39,7 @@ namespace Tgstation.Server.Tests Mock.Of(), Mock.Of(), Mock.Of(), - Mock.Of>(), + Mock.Of>(), new GeneralConfiguration(), () => { }); @@ -56,5 +59,78 @@ namespace Tgstation.Server.Tests CancellationToken.None); } } + + [TestMethod] + public async Task TestFetchingAdditionalCommits() + { + var tempPath = Path.Combine(Path.GetTempPath(), "TGS-Repository-Integration-Test", Guid.NewGuid().ToString()); + var repoFac = + new LibGit2RepositoryFactory( + Mock.Of>()); + var commands = new LibGit2Commands(); + using var manager = new RepositoryManager( + repoFac, + commands, + new ResolvingIOManager( + new DefaultIOManager(), + tempPath), + Mock.Of(), + new WindowsPostWriteHandler(), + Mock.Of(), + Mock.Of>(), + Mock.Of>(), + new GeneralConfiguration()); + try + { + using (await manager.CloneRepository( + new Uri("https://github.com/Cyberboss/common_core"), + null, + null, + null, + new JobProgressReporter(Mock.Of>(), null, (_, _) => { }), + true, + default)) + { + } + + using (var repo = await repoFac.CreateFromPath(tempPath, default)) + { + repo.Network.Remotes.Update("origin", updater => + { + updater.Url = "https://github.com/tgstation/common_core"; + }); + + var targetCommit = repo.Lookup("5b0d0a38057a2c8306a852ccbd6cd6f4ae766a33"); + Assert.IsNull(targetCommit); + } + + using (var repo2 = await manager.LoadRepository(default)) + { + await repo2.FetchOrigin( + new JobProgressReporter(Mock.Of>(), null, (_, _) => { }), + null, + null, + false, + default); + } + + using var repo3 = await repoFac.CreateFromPath(tempPath, default); + var remote = repo3.Network.Remotes.First(); + commands.Fetch(repo3, remote.FetchRefSpecs.Select(x => x.Specification), remote, new FetchOptions + { + TagFetchMode = TagFetchMode.All, + Prune = true, + }, "test"); + + var targetCommit2 = repo3.Lookup("5b0d0a38057a2c8306a852ccbd6cd6f4ae766a33"); + Assert.IsNotNull(targetCommit2); + } + finally + { + await new DefaultIOManager().DeleteDirectory( + Path.GetDirectoryName(tempPath), + CancellationToken.None); + } + } } } From 9ea468b8244bda363ccff8c1934598318532da49 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sat, 11 Nov 2023 21:31:38 -0500 Subject: [PATCH 7/8] Version bump to 5.17.2 --- build/Version.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Version.props b/build/Version.props index ae1f43929d..c79cd90822 100644 --- a/build/Version.props +++ b/build/Version.props @@ -3,7 +3,7 @@ - 5.17.1 + 5.17.2 4.7.1 9.13.0 7.0.0 From a252c5f95a108a2244fd11ade9ec3d2abc96b1ef Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sat, 11 Nov 2023 21:50:38 -0500 Subject: [PATCH 8/8] Update dotnet redistributable to latest --- build/Version.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Version.props b/build/Version.props index c79cd90822..28f434af34 100644 --- a/build/Version.props +++ b/build/Version.props @@ -17,7 +17,7 @@ netstandard2.0 6 - https://dotnetcli.azureedge.net/dotnet/aspnetcore/Runtime/6.0.23/dotnet-hosting-6.0.23-win.exe + https://dotnetcli.azureedge.net/dotnet/aspnetcore/Runtime/6.0.24/dotnet-hosting-6.0.24-win.exe 10.11.5 https://ftp.osuosl.org/pub/mariadb//mariadb-10.11.5/winx64-packages/mariadb-10.11.5-winx64.msi