diff --git a/appveyor.yml b/appveyor.yml
index 0397e3682f..b5a69eab3e 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -53,6 +53,9 @@ test_script:
- vstest.console /logger:trx;LogFileName=results.trx "tests\Tgstation.Server.Host.Watchdog.Tests\bin\%CONFIGURATION%\netcoreapp2.0\Tgstation.Server.Host.Watchdog.Tests.dll" /Enablecodecoverage /inIsolation /Platform:x64
- ps: $wc = New-Object 'System.Net.WebClient'
- ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\TestResults\results.trx))
+ - vstest.console /logger:trx;LogFileName=results.trx "tests\Tgstation.Server.Tests\bin\%CONFIGURATION%\netcoreapp2.0\Tgstation.Server.Tests.dll" /Enablecodecoverage /inIsolation /Platform:x64
+ - ps: $wc = New-Object 'System.Net.WebClient'
+ - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\TestResults\results.trx))
after_test:
- ps: build/UploadCoverage.ps1
- ps: build/BuildDox.ps1
diff --git a/build/Dockerfile b/build/Dockerfile
index af174c8317..a0da9591bd 100644
--- a/build/Dockerfile
+++ b/build/Dockerfile
@@ -29,6 +29,9 @@ RUN dotnet test -c Release
WORKDIR /src/tests/Tgstation.Server.Host.Console.Tests
RUN dotnet test -c Release
+WORKDIR /src/tests/Tgstation.Server.Tests
+RUN dotnet test -c Release
+
WORKDIR /src/src/Tgstation.Server.Host.Console
RUN dotnet publish -c Release -o /app
diff --git a/docs/API.dox b/docs/API.dox
index 18c5ec8258..4d5c8a51fc 100644
--- a/docs/API.dox
+++ b/docs/API.dox
@@ -52,7 +52,9 @@ An Authentication header is also required. See @ref api_auth
TGS will only every return the response codes listed here
- 200: General OK status. Unless the HTTP DELETE verb was used to make the request (In which case the response body will be empty), the response body will contain a json model or array depending on the API called.
-- 400: Bad request made. The response body will contain a @ref Tgstation.Server.Api.Models.ErrorMessage model detailing the error
+- 201: Created: Returned when the request created an entity, 202 trumps this
+- 202: Accepted. Used when a response triggers a long running operation such as a @ref Tgstation.Server.Api.Models.Job or server restart
+- 400: Bad request made. The response body will contain an @ref Tgstation.Server.Api.Models.ErrorMessage model detailing the error
- 401: User unauthorized. Invalid or expired credentials were provided. Check rights APIs for updates. See @ref api_auth for details
- 403: Usage forbidden. User tried to make a request they were not allowed to perform.
- 404: Not found. A resource was requested that had never existed. In the case of retrieving a resource by ID, it could potentially exist in the future
@@ -60,9 +62,11 @@ TGS will only every return the response codes listed here
- 408: Request timeout. The client took to long to continue a request
- 409: Conflict. Documented in the requests that use them
- 410: Gone. Attempted to access/modify a resource that ideally should have been ready, but isn't or no longer is
+- 422: Unprocessable Entity: Used specifically when an operation that requires a server restart is unable to be performed due to the @ref Tgstation.Server.Host.Watchdog not being present in the deployment. Blame MSO. Response body contains an @ref Tgstation.Server.Api.Models.ErrorMessage
+- 426: Upgrade required: Used when the client's API version is not compatible with the server's. Response body contains an @ref Tgstation.Server.Api.Models.ErrorMessage
- 429: Rate limited. Used with operations that rely on GitHub.com. If a rate limit is hit for an operation this will be returned. Response will contain a Retry-After header
- 500: Server error. Please report the request and response body to the code repository
-- 501: Not implemented. Currently used in two places: 1. Endpoints that trigger a server restart but the server is not running in a restartable configuration. 2.
+- 501: Not implemented. Functionality not available in the current server version
- 503: Service unavailable. The server is either starting up or shutting down and isn't ready to respond to requests. You can try again soon and a response/lack thereof will indicate which of the two events it was
@section api_auth Authentication
@@ -141,11 +145,11 @@ Users with the permission to modify @ref Tgstation.Server.Api.Models.Instance ob
@section api_ver Version
-The version of TGS running can be retireved with this request
+The versions of the TGS host can be retireved with this request
-GET "/" => Version
+GET "/" => @ref Tgstation.Server.Api.Models.ServerInformation
-The Version model is based on the C# one and looks like this:
+The Version model fields are based on the C# one and looks like this:
@code{.json}
{
@@ -156,7 +160,7 @@ The Version model is based on the C# one and looks like this:
}
@endcode
-Other fields may be present but should be ignored. See a description of these version numbers here.
+Other fields may be present in the Version model but should be ignored. See a description of these version numbers here.
@section api_admin Server-wide Administrative Actions
@@ -168,6 +172,8 @@ If you want to perform a live update of the server use
POST "/Administration" @ref Tgstation.Server.Api.Models.Administration => OK
+With the @ref Tgstation.Server.Api.Models.Administration.NewVersion field set
+
Any DreamDaemon servers running will persist while the server installs the new version from the official tgstation-server GitHub (If it exists)
If the server is otherwise acting funky and you wish to restart it, use this request:
@@ -390,6 +396,10 @@ If the path is empty, the root directory will be retrieved. The @ref Tgstation.S
If you do not have access to list the requested directory, a 403 response will be returned. If the path actually represents a file or the directory no longer exists a 410 response will be returned.
+To create an empty config directory use the following request
+
+I PUT "/Config" @ref Tgstation.Server.Api.Models.ConfigurationFile => @ref Tgstation.Server.Api.Models.ConfigurationFile
+
To get the content of a static file use the following method
I GET "/Config/File/" => @ref Tgstation.Server.Api.Models.ConfigurationFile
diff --git a/src/DMAPI/tgs/v4/api.dm b/src/DMAPI/tgs/v4/api.dm
index 1736130b67..c24691e150 100644
--- a/src/DMAPI/tgs/v4/api.dm
+++ b/src/DMAPI/tgs/v4/api.dm
@@ -135,8 +135,8 @@
switch(command)
if(TGS4_TOPIC_CHAT_COMMAND)
var/result = HandleCustomCommand(params[TGS4_PARAMETER_DATA])
- if(!result)
- return json_encode(list("error" = "Error running chat command!"))
+ if(result == null)
+ result = "Error running chat command!"
return result
if(TGS4_TOPIC_EVENT)
intercepted_message_queue = list()
@@ -294,21 +294,6 @@
channel.is_private_channel = channel_json["isPrivateChannel"] || FALSE
return channel
-#undef TGS4_TOPIC_COMMAND
-#undef TGS4_TOPIC_TOKEN
-#undef TGS4_TOPIC_SUCCESS
-#undef TGS4_TOPIC_SWAP
-#undef TGS4_TOPIC_SWAP_DELAYED
-#undef TGS4_TOPIC_CHAT_COMMAND
-#undef TGS4_TOPIC_EVENT
-
-#undef TGS4_COMM_SERVER_PRIMED
-#undef TGS4_COMM_SERVER_REBOOT
-#undef TGS4_COMM_END_PROCESS
-#undef TGS4_COMM_CHAT
-
-#undef TGS4_COMM_VALIDATE
-
/*
The MIT License
diff --git a/src/DMAPI/tgs/v4/commands.dm b/src/DMAPI/tgs/v4/commands.dm
index 88ee61e882..1d9951bc04 100644
--- a/src/DMAPI/tgs/v4/commands.dm
+++ b/src/DMAPI/tgs/v4/commands.dm
@@ -12,7 +12,7 @@
var/datum/other = custom_commands[command_name]
TGS_ERROR_LOG("Custom commands [other.type] and [I] have the same name (\"[command_name]\"), only [other.type] will be available!")
continue
- results[command_name] = list("help_text" = stc.help_text, "admin_only" = stc.admin_only)
+ results += list(list("name" = command_name, "help_text" = stc.help_text, "admin_only" = stc.admin_only))
custom_commands[command_name] = stc
var/commands_file = chat_commands_json_path
@@ -33,8 +33,12 @@
u.channel = DecodeChannel(user["channel"])
var/datum/tgs_chat_command/sc = custom_commands[command]
- var/result = sc.Run(u, params)
- return json_encode(list("result" = result))
+ if(sc)
+ var/result = sc.Run(u, params)
+ if(result == null)
+ result = ""
+ return result
+ return "Unknown command: [command]!"
/*
diff --git a/src/Tgstation.Server.Api/ApiHeaders.cs b/src/Tgstation.Server.Api/ApiHeaders.cs
index 8208b922c8..1160e258ca 100644
--- a/src/Tgstation.Server.Api/ApiHeaders.cs
+++ b/src/Tgstation.Server.Api/ApiHeaders.cs
@@ -48,7 +48,12 @@ namespace Tgstation.Server.Api
///
/// The current
///
- internal static readonly AssemblyName assemblyName = Assembly.GetExecutingAssembly().GetName();
+ static readonly AssemblyName assemblyName = Assembly.GetExecutingAssembly().GetName();
+
+ ///
+ /// Get the version of the the caller is using
+ ///
+ public static Version Version => assemblyName.Version;
///
/// The being accessed
@@ -90,11 +95,7 @@ namespace Tgstation.Server.Api
///
/// The to test
/// if the given version is compatible with the API. otherwise
- public static bool CheckCompatibility(Version otherVersion)
- {
- var ourVersion = assemblyName.Version;
- return !(ourVersion.Major != otherVersion.Major || ourVersion.Minor != otherVersion.Minor || ourVersion.Build > otherVersion.Build);
- }
+ public static bool CheckCompatibility(Version otherVersion) => !(Version.Major != otherVersion.Major || Version.Minor != otherVersion.Minor || Version.Build > otherVersion.Build);
///
/// Construct for JWT authentication
@@ -144,7 +145,7 @@ namespace Tgstation.Server.Api
//make sure the api header matches ours
if (!requestHeaders.Headers.TryGetValue(ApiVersionHeader, out var apiUserAgentHeaderValues) || !ProductInfoHeaderValue.TryParse(apiUserAgentHeaderValues.FirstOrDefault(), out var apiUserAgent) || apiUserAgent.Product.Name != assemblyName.Name)
- throw new InvalidOperationException("Missing API user agent!");
+ throw new InvalidOperationException("Missing API version!");
if (!Version.TryParse(apiUserAgent.Product.Version, out var apiVersion))
throw new InvalidOperationException("Malformed API version!");
@@ -152,9 +153,6 @@ namespace Tgstation.Server.Api
ApiVersion = apiVersion;
UserAgent = clientUserAgent.Product;
- if(!CheckCompatibility(ApiVersion))
- throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Given API version is incompatible with version {0}!", ApiVersion));
-
if (!requestHeaders.Headers.TryGetValue(HeaderNames.Authorization, out StringValues authorization))
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Missing {0} header!", HeaderNames.Authorization));
var auth = authorization.First();
@@ -209,9 +207,15 @@ namespace Tgstation.Server.Api
Token = token;
Username = username;
Password = password;
- ApiVersion = assemblyName.Version;
+ ApiVersion = Version;
}
+ ///
+ /// Checks if the is compatible with
+ ///
+ /// if the API is compatible, otherwise
+ public bool Compatible() => CheckCompatibility(ApiVersion);
+
///
/// Set using the . This initially clears
///
@@ -234,7 +238,7 @@ namespace Tgstation.Server.Api
headers.Add(usernameHeader, Username);
}
headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent));
- headers.Add(ApiVersionHeader, ApiVersion.ToString());
+ headers.Add(ApiVersionHeader, new ProductHeaderValue(assemblyName.Name, ApiVersion.ToString()).ToString());
instanceId = instanceId ?? InstanceId;
if (instanceId.HasValue)
headers.Add(instanceIdHeader, instanceId.ToString());
diff --git a/src/Tgstation.Server.Api/Models/Administration.cs b/src/Tgstation.Server.Api/Models/Administration.cs
index 7c8873d04b..33f7aa5e5f 100644
--- a/src/Tgstation.Server.Api/Models/Administration.cs
+++ b/src/Tgstation.Server.Api/Models/Administration.cs
@@ -19,7 +19,7 @@ namespace Tgstation.Server.Api.Models
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
+ /// 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
///
[Permissions(DenyWrite = true)]
public Version LatestVersion { get; set; }
@@ -28,6 +28,6 @@ namespace Tgstation.Server.Api.Models
/// Changes the version of Tgstation.Server.Host to the given version from the upstream repository
///
[Permissions(WriteRight = AdministrationRights.ChangeVersion)]
- public Version CurrentVersion { get; set; }
+ public Version NewVersion { get; set; }
}
}
diff --git a/src/Tgstation.Server.Api/Models/ErrorMessage.cs b/src/Tgstation.Server.Api/Models/ErrorMessage.cs
index 05f5b03d86..033ff8b9ad 100644
--- a/src/Tgstation.Server.Api/Models/ErrorMessage.cs
+++ b/src/Tgstation.Server.Api/Models/ErrorMessage.cs
@@ -15,6 +15,6 @@ namespace Tgstation.Server.Api.Models
///
/// The version of the API the server is using
///
- public Version SeverApiVersion { get; set; } = ApiHeaders.assemblyName.Version;
+ public Version SeverApiVersion { get; set; } = ApiHeaders.Version;
}
}
diff --git a/src/Tgstation.Server.Api/Models/ServerInformation.cs b/src/Tgstation.Server.Api/Models/ServerInformation.cs
new file mode 100644
index 0000000000..2bfbf9b6b6
--- /dev/null
+++ b/src/Tgstation.Server.Api/Models/ServerInformation.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace Tgstation.Server.Api.Models
+{
+ ///
+ /// Represents basic server information
+ ///
+ public sealed class ServerInformation
+ {
+ ///
+ /// The version of the host
+ ///
+ public Version Version { get; set; }
+
+ ///
+ /// The version of the host
+ ///
+ public Version ApiVersion { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Api/Models/User.cs b/src/Tgstation.Server.Api/Models/User.cs
index a18856a366..1431dd2db9 100644
--- a/src/Tgstation.Server.Api/Models/User.cs
+++ b/src/Tgstation.Server.Api/Models/User.cs
@@ -3,6 +3,16 @@
///
public class User : Internal.User
{
+ ///
+ /// The name of the default admin user
+ ///
+ public const string AdminName = "Admin";
+
+ ///
+ /// The default admin password
+ ///
+ public const string DefaultAdminPassword = "ISolemlySwearToDeleteTheDataDirectory";
+
///
/// The who created this
///
diff --git a/src/Tgstation.Server.Api/Rights/AdministrationRights.cs b/src/Tgstation.Server.Api/Rights/AdministrationRights.cs
index dc8c6f9dfa..6f546020aa 100644
--- a/src/Tgstation.Server.Api/Rights/AdministrationRights.cs
+++ b/src/Tgstation.Server.Api/Rights/AdministrationRights.cs
@@ -21,7 +21,7 @@ namespace Tgstation.Server.Api.Rights
///
RestartHost = 2,
///
- /// User can change
+ /// User can change
///
ChangeVersion = 4,
///
diff --git a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj
index 7eed75c01d..9f9426a642 100644
--- a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj
+++ b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj
@@ -17,7 +17,7 @@
4.0.0.0
json web api tgstation-server tgstation ss13 byond
Prototype release
- 4.0.0.0-preview1
+ 4.0.0.0-preview2
diff --git a/src/Tgstation.Server.Client/ApiClient.cs b/src/Tgstation.Server.Client/ApiClient.cs
index c9e16bdca2..176253e06d 100644
--- a/src/Tgstation.Server.Client/ApiClient.cs
+++ b/src/Tgstation.Server.Client/ApiClient.cs
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
@@ -83,7 +84,7 @@ namespace Tgstation.Server.Client
};
if (body != null)
- message.Content = new StringContent(JsonConvert.SerializeObject(body, serializerSettings));
+ message.Content = new StringContent(JsonConvert.SerializeObject(body, serializerSettings), Encoding.UTF8, ApiHeaders.ApplicationJson);
Headers.SetRequestHeaders(message.Headers, instanceId);
@@ -95,22 +96,20 @@ namespace Tgstation.Server.Client
var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
- if (!response.IsSuccessStatusCode) {
+ if (!response.IsSuccessStatusCode)
+ {
ErrorMessage errorMessage = null;
try
{
//check if json serializes to an error message
errorMessage = JsonConvert.DeserializeObject(json, serializerSettings);
}
- catch (JsonSerializationException) { }
+ catch (JsonException) { }
switch (response.StatusCode)
{
- case HttpStatusCode.BadRequest:
- //validate our api version is compatible
- if(errorMessage != null && ApiHeaders.CheckCompatibility(errorMessage.SeverApiVersion))
- throw new ApiMismatchException(errorMessage);
- goto default;
+ case HttpStatusCode.UpgradeRequired:
+ throw new ApiMismatchException(errorMessage);
case HttpStatusCode.Unauthorized:
throw new UnauthorizedException();
case HttpStatusCode.RequestTimeout:
@@ -124,10 +123,11 @@ namespace Tgstation.Server.Client
case HttpStatusCode.Conflict:
throw new ConflictException(errorMessage, response.StatusCode);
case HttpStatusCode.NotImplemented:
+ case (HttpStatusCode)422: //unprocessable entity
throw new MethodNotSupportedException();
case HttpStatusCode.InternalServerError:
//response
- throw new ServerErrorException(json); //json is html
+ throw new ServerErrorException(json); //json is html
case (HttpStatusCode)429: //rate limited
response.Headers.TryGetValues("Retry-After", out var values);
throw new RateLimitException(values?.FirstOrDefault());
diff --git a/src/Tgstation.Server.Client/ApiConflictException.cs b/src/Tgstation.Server.Client/ApiConflictException.cs
index ba3eaa90c6..189fd92009 100644
--- a/src/Tgstation.Server.Client/ApiConflictException.cs
+++ b/src/Tgstation.Server.Client/ApiConflictException.cs
@@ -14,7 +14,11 @@ namespace Tgstation.Server.Client
///
/// The for the
/// The for the
- public ApiConflictException(ErrorMessage errorMessage, HttpStatusCode statusCode) : base(errorMessage, statusCode) { }
+ public ApiConflictException(ErrorMessage errorMessage, HttpStatusCode statusCode) : base(errorMessage ?? new ErrorMessage
+ {
+ Message = "An unknown API error occurred!",
+ SeverApiVersion = null
+ }, statusCode) { }
///
/// Construct an
diff --git a/src/Tgstation.Server.Client/IServerClient.cs b/src/Tgstation.Server.Client/IServerClient.cs
index 3bd57d287e..23e44a36b2 100644
--- a/src/Tgstation.Server.Client/IServerClient.cs
+++ b/src/Tgstation.Server.Client/IServerClient.cs
@@ -36,9 +36,9 @@ namespace Tgstation.Server.Client
IUsersClient Users { get; }
///
- /// The of the
+ /// The of the
///
- Task Version(CancellationToken cancellationToken);
+ Task Version(CancellationToken cancellationToken);
///
/// Adds a to the request pipeline
diff --git a/src/Tgstation.Server.Client/ServerClient.cs b/src/Tgstation.Server.Client/ServerClient.cs
index 7e34062840..afc19af371 100644
--- a/src/Tgstation.Server.Client/ServerClient.cs
+++ b/src/Tgstation.Server.Client/ServerClient.cs
@@ -55,7 +55,7 @@ namespace Tgstation.Server.Client
public void Dispose() => apiClient.Dispose();
///
- public Task Version(CancellationToken cancellationToken) => apiClient.Read(Routes.Root, cancellationToken);
+ public Task Version(CancellationToken cancellationToken) => apiClient.Read(Routes.Root, cancellationToken);
///
public void AddRequestLogger(IRequestLogger requestLogger) => apiClient.AddRequestLogger(requestLogger);
diff --git a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj
index 9701d04cd4..d0957a24a9 100644
--- a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj
+++ b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj
@@ -3,7 +3,7 @@
netstandard2.0
Full
- 4.0.0.0-preview1
+ 4.0.0.0-preview2
true
Cyberboss
/tg/station 13
diff --git a/src/Tgstation.Server.Host/Components/Chat/JsonTrackingContext.cs b/src/Tgstation.Server.Host/Components/Chat/JsonTrackingContext.cs
index c7482f38ed..f5bbcd558f 100644
--- a/src/Tgstation.Server.Host/Components/Chat/JsonTrackingContext.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/JsonTrackingContext.cs
@@ -44,7 +44,13 @@ namespace Tgstation.Server.Host.Components.Chat
{
var resultBytes = await ioManager.ReadAllBytes(commandsPath, cancellationToken).ConfigureAwait(false);
var resultJson = Encoding.UTF8.GetString(resultBytes);
- var result = JsonConvert.DeserializeObject>(resultJson);
+ var result = JsonConvert.DeserializeObject>(resultJson, new JsonSerializerSettings
+ {
+ ContractResolver = new DefaultContractResolver
+ {
+ NamingStrategy = new SnakeCaseNamingStrategy()
+ }
+ });
foreach (var I in result)
I.SetHandler(customCommandHandler);
return result;
@@ -61,10 +67,7 @@ namespace Tgstation.Server.Host.Components.Chat
using (await SemaphoreSlimContext.Lock(channelsSemaphore, cancellationToken).ConfigureAwait(false))
await ioManager.WriteAllBytes(channelsPath, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(channels, Formatting.Indented, new JsonSerializerSettings
{
- ContractResolver = new DefaultContractResolver
- {
- NamingStrategy = new CamelCaseNamingStrategy()
- }
+ ContractResolver = new CamelCasePropertyNamesContractResolver()
})), cancellationToken).ConfigureAwait(false);
}
}
diff --git a/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs
index c88bc67e91..514b574631 100644
--- a/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs
+++ b/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs
@@ -364,7 +364,7 @@ namespace Tgstation.Server.Host.Components.Compiler
{
//server never validated or compile failed
await eventConsumer.HandleEvent(EventType.CompileFailure, new List { resolvedGameDirectory, exitCode == 0 ? "1" : "0" }, cancellationToken).ConfigureAwait(false);
- throw new JobException(exitCode == 0 ? "Validation of the TGS api failed!" : String.Format(CultureInfo.InvariantCulture, "DM exited with a non-zero code: {0}{1}", exitCode, job.Output));
+ throw new JobException(exitCode == 0 ? "Validation of the TGS api failed!" : String.Format(CultureInfo.InvariantCulture, "DM exited with a non-zero code: {0}{1}{2}", exitCode, Environment.NewLine, job.Output));
}
logger.LogTrace("Running post compile event...");
diff --git a/src/Tgstation.Server.Host/Components/Interop/ChatCommand.cs b/src/Tgstation.Server.Host/Components/Interop/ChatCommand.cs
new file mode 100644
index 0000000000..1c49476b35
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Interop/ChatCommand.cs
@@ -0,0 +1,11 @@
+using Tgstation.Server.Host.Components.Chat;
+
+namespace Tgstation.Server.Host.Components.Interop
+{
+ sealed class ChatCommand
+ {
+ public string Command { get; set; }
+ public string Parameters { get; set; }
+ public User User { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs
index 260e3447fe..0f431bea96 100644
--- a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs
+++ b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs
@@ -347,6 +347,22 @@ namespace Tgstation.Server.Host.Components.StaticFiles
return result;
}
+ ///
+ public async Task CreateDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
+ {
+ await EnsureDirectories(cancellationToken).ConfigureAwait(false);
+ var path = ValidateConfigRelativePath(configurationRelativePath);
+
+ bool? result = null;
+ void DoCreate() => result = synchronousIOManager.CreateDirectory(path, cancellationToken);
+ if (systemIdentity == null)
+ await Task.Factory.StartNew(DoCreate, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
+ else
+ await systemIdentity.RunImpersonated(DoCreate, cancellationToken).ConfigureAwait(false);
+
+ return result.Value;
+ }
+
///
public Task StartAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs
index 3fdce2aa9b..ebccc9c24e 100644
--- a/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs
+++ b/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs
@@ -48,6 +48,15 @@ namespace Tgstation.Server.Host.Components.StaticFiles
/// A resulting in the of the file
Task Read(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken);
+ ///
+ /// Create an empty directory at
+ ///
+ /// The relative path in the Configuration directory
+ /// The for the operation. If , the operation will be performed as the user of the
+ /// The for the operation. Usage may result in partial writes
+ /// A resulting in if the directory already existed, otherwise
+ Task CreateDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken);
+
///
/// Writes to a given
///
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs
index 69fbe5cd85..fd7209bb41 100644
--- a/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs
+++ b/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs
@@ -153,10 +153,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
var interopJson = JsonConvert.SerializeObject(interopInfo, Formatting.Indented, new JsonSerializerSettings
{
- ContractResolver = new DefaultContractResolver
- {
- NamingStrategy = new CamelCaseNamingStrategy()
- },
+ ContractResolver = new CamelCasePropertyNamesContractResolver(),
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs
index dcda48d43c..bec6c74822 100644
--- a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs
+++ b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs
@@ -1,6 +1,7 @@
using Byond.TopicSender;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -858,7 +859,19 @@ namespace Tgstation.Server.Host.Components.Watchdog
if (!Running)
return "ERROR: Server offline!";
- var command = String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicChatCommand), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(JsonConvert.SerializeObject(arguments)));
+ var commandObject = new ChatCommand
+ {
+ Command = commandName,
+ Parameters = arguments,
+ User = sender
+ };
+
+ var json = JsonConvert.SerializeObject(arguments, new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver()
+ });
+
+ var command = String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicChatCommand), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(json));
var activeServer = AlphaIsActive ? alphaServer : bravoServer;
return await activeServer.SendCommand(command, cancellationToken).ConfigureAwait(false) ?? "ERROR: Bad topic exchange!";
diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs
index 45aadcca9a..24ace9d9b9 100644
--- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs
+++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs
@@ -28,10 +28,7 @@ namespace Tgstation.Server.Host.Controllers
[Route(Routes.Administration)]
public sealed class AdministrationController : ModelController
{
- ///
- /// HTTP 429 status code
- ///
- const int RateLimitHttpStatusCode = 429;
+ const string RestartNotSupportedException = "This deployment of tgstation-server is lacking the Tgstation.Server.Host.Watchdog component. Restarts and version changes cannot be completed!";
///
/// The for the
@@ -83,38 +80,44 @@ namespace Tgstation.Server.Host.Controllers
Logger.LogWarning("Exceeded GitHub rate limit!");
var secondsString = Math.Ceiling((exception.Reset - DateTimeOffset.Now).TotalSeconds).ToString(CultureInfo.InvariantCulture);
Response.Headers.Add("Retry-After", new StringValues(secondsString));
- return StatusCode(RateLimitHttpStatusCode);
+ return StatusCode(429);
}
///
[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, StringComparison.Ordinal), out var version)
- && version.Major == application.Version.Major
- && (greatestVersion == null || version > greatestVersion))
- greatestVersion = version;
+ Uri repoUrl = null;
+ 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));
- model.LatestVersion = greatestVersion;
- model.TrackedRepositoryUrl = new Uri((await repositoryTask.ConfigureAwait(false)).HtmlUrl);
- model.WindowsHost = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+ foreach (var I in releases)
+ if (Version.TryParse(I.TagName.Replace(updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal), out var version)
+ && version.Major == application.Version.Major
+ && (greatestVersion == null || version > greatestVersion))
+ greatestVersion = version;
+ repoUrl = new Uri((await repositoryTask.ConfigureAwait(false)).HtmlUrl);
+ }
+ catch (NotFoundException e)
+ {
+ Logger.LogWarning("Not found exception while retrieving upstream repository info: {0}", e);
+ }
+ return Json(new Administration
+ {
+ LatestVersion = greatestVersion,
+ TrackedRepositoryUrl = repoUrl,
+ WindowsHost = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
+ });
}
catch (RateLimitExceededException e)
{
return RateLimit(e);
}
- return Json(model);
}
///
@@ -124,10 +127,10 @@ namespace Tgstation.Server.Host.Controllers
if (model == null)
throw new ArgumentNullException(nameof(model));
- if (model.CurrentVersion == null)
+ if (model.NewVersion == null)
return BadRequest(new ErrorMessage { Message = "Missing new version!" });
- if (model.CurrentVersion.Major != application.Version.Major)
+ if (model.NewVersion.Major != application.Version.Major)
return BadRequest(new ErrorMessage { Message = "Cannot update to a different suite version!" });
IEnumerable releases;
@@ -141,7 +144,7 @@ namespace Tgstation.Server.Host.Controllers
}
foreach (var release in releases)
- if (Version.TryParse(release.TagName.Replace(updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal), out var version) && version == model.CurrentVersion)
+ if (Version.TryParse(release.TagName.Replace(updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal), out var version) && version == model.NewVersion)
{
var asset = release.Assets.Where(x => x.Name == updatesConfiguration.UpdatePackageAssetName).FirstOrDefault();
if (asset == default)
@@ -151,10 +154,16 @@ namespace Tgstation.Server.Host.Controllers
try
{
if (!await serverUpdater.ApplyUpdate(assetBytes, ioManager, cancellationToken).ConfigureAwait(false))
- return StatusCode((int)HttpStatusCode.NotImplemented);
+ return UnprocessableEntity(new ErrorMessage
+ {
+ Message = RestartNotSupportedException
+ }); //unprocessable entity
}
- catch (InvalidOperationException) { } //we were beat to the punch
- return Ok(); //gtfo of here before all the cancellation tokens fire
+ catch (InvalidOperationException)
+ {
+ return StatusCode((int)HttpStatusCode.ServiceUnavailable); //we were beat to the punch, really shouldn't happen but heat death of the universe and what not
+ }
+ return Accepted(); //gtfo of here before all the cancellation tokens fire
}
return StatusCode((int)HttpStatusCode.Gone);
@@ -163,6 +172,18 @@ namespace Tgstation.Server.Host.Controllers
///
[HttpDelete]
[TgsAuthorize(AdministrationRights.RestartHost)]
- public Task Delete() => Task.FromResult(serverUpdater.Restart() ? (IActionResult)Ok() : StatusCode((int)HttpStatusCode.NotImplemented));
+ public Task Delete() {
+ try
+ {
+ return Task.FromResult(serverUpdater.Restart() ? (IActionResult)Ok() : UnprocessableEntity(new ErrorMessage
+ {
+ Message = RestartNotSupportedException
+ }));
+ }
+ catch (InvalidOperationException)
+ {
+ return Task.FromResult(StatusCode((int)HttpStatusCode.ServiceUnavailable));
+ }
+ }
}
}
diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs
index ffa84c64a3..faf1cad40e 100644
--- a/src/Tgstation.Server.Host/Controllers/ApiController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs
@@ -9,6 +9,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
+using System.Net;
using System.Security.Claims;
using System.Threading.Tasks;
using Tgstation.Server.Api;
@@ -150,6 +151,15 @@ namespace Tgstation.Server.Host.Controllers
{
ApiHeaders = new ApiHeaders(Request.GetTypedHeaders());
+ if(!ApiHeaders.Compatible())
+ {
+ await StatusCode((int)HttpStatusCode.UpgradeRequired, new ErrorMessage
+ {
+ Message = "Provided API version is incompatible with server version!"
+ }).ExecuteResultAsync(context).ConfigureAwait(false);
+ return;
+ }
+
if (requireInstance)
{
if(!ApiHeaders.InstanceId.HasValue)
diff --git a/src/Tgstation.Server.Host/Controllers/ByondController.cs b/src/Tgstation.Server.Host/Controllers/ByondController.cs
index 11c0191d24..566aaae2b0 100644
--- a/src/Tgstation.Server.Host/Controllers/ByondController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ByondController.cs
@@ -99,7 +99,7 @@ namespace Tgstation.Server.Host.Controllers
result.InstallJob = job.ToApi();
}
result.Version = byondManager.ActiveVersion;
- return Json(result);
+ return result.InstallJob != null ? (IActionResult)Accepted(result) : Json(result);
}
}
}
diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs
index f1cb99d06c..297bfb0bb0 100644
--- a/src/Tgstation.Server.Host/Controllers/ChatController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs
@@ -100,14 +100,7 @@ namespace Tgstation.Server.Host.Controllers
DatabaseContext.ChatBots.Add(dbModel);
- try
- {
- await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
- }
- catch (DbUpdateException)
- {
- return Conflict();
- }
+ await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
try
{
@@ -132,7 +125,7 @@ namespace Tgstation.Server.Host.Controllers
{
return BadRequest(new ErrorMessage { Message = e.Message });
}
- return Json(dbModel.ToApi());
+ return StatusCode((int)HttpStatusCode.Created, dbModel.ToApi());
}
///
diff --git a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs
index e724238caa..8823ee4de5 100644
--- a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs
@@ -58,7 +58,7 @@ namespace Tgstation.Server.Host.Controllers
newFile.Content = null;
- return Json(newFile);
+ return model.LastReadHash == null ? (IActionResult)StatusCode((int)HttpStatusCode.Created, newFile) : Json(newFile);
}
catch(NotImplementedException)
{
@@ -127,5 +127,26 @@ namespace Tgstation.Server.Host.Controllers
///
[TgsAuthorize(ConfigurationRights.List)]
public override Task List(CancellationToken cancellationToken) => Directory(null, cancellationToken);
+
+ ///
+ [TgsAuthorize(ConfigurationRights.Write)]
+ public override async Task Create([FromBody] ConfigurationFile model, CancellationToken cancellationToken)
+ {
+ if (ForbidDueToModeConflicts())
+ return Forbid();
+
+ try
+ {
+ return await instanceManager.GetInstance(Instance).Configuration.CreateDirectory(model.Path, AuthenticationContext.SystemIdentity, cancellationToken).ConfigureAwait(false) ? (IActionResult)Json(model) : StatusCode((int)HttpStatusCode.Created, model);
+ }
+ catch (NotImplementedException)
+ {
+ return StatusCode((int)HttpStatusCode.NotImplemented);
+ }
+ catch (UnauthorizedAccessException)
+ {
+ return Forbid();
+ }
+ }
}
}
diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
index 2872f37787..c61672848f 100644
--- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
+++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
@@ -74,7 +74,7 @@ namespace Tgstation.Server.Host.Controllers
throw new InvalidOperationException("Watchdog already running!");
},
cancellationToken).ConfigureAwait(false);
- return Json(job.ToApi());
+ return Accepted(job.ToApi());
}
///
diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs
index 657e7fe3dc..bcabfd014e 100644
--- a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs
+++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs
@@ -93,7 +93,7 @@ namespace Tgstation.Server.Host.Controllers
Instance = Instance
};
await jobManager.RegisterOperation(job, (paramJob, serviceProvider, progressReporter, ct) => RunCompile(paramJob, serviceProvider, Instance, ct), cancellationToken).ConfigureAwait(false);
- return Json(job.ToApi());
+ return Accepted(job.ToApi());
}
///
diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs
index 9619335070..ca67ed2d09 100644
--- a/src/Tgstation.Server.Host/Controllers/HomeController.cs
+++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs
@@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
+using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
@@ -65,7 +66,10 @@ namespace Tgstation.Server.Host.Controllers
///
[TgsAuthorize]
[HttpGet]
- public JsonResult Home() => Json(new { application.Version });
+ public JsonResult Home() => Json(new Api.Models.ServerInformation {
+ Version = application.Version,
+ ApiVersion = ApiHeaders.Version
+ });
///
/// Attempt to authenticate a using
diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs
index 902d1c54c6..a417dbe86c 100644
--- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs
+++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs
@@ -183,7 +183,7 @@ namespace Tgstation.Server.Host.Controllers
Logger.LogInformation("{0} created instance {1}: {2}", AuthenticationContext.User.Name, newInstance.Name, newInstance.Id);
- return Json(newInstance.ToApi());
+ return StatusCode((int)HttpStatusCode.Created, newInstance.ToApi());
}
///
diff --git a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs
index 5a37a37c3f..26412f7509 100644
--- a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs
+++ b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs
@@ -70,7 +70,7 @@ namespace Tgstation.Server.Host.Controllers
DatabaseContext.InstanceUsers.Add(dbUser);
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
- return Json(dbUser.ToApi());
+ return StatusCode((int)HttpStatusCode.Created, dbUser.ToApi());
}
///
diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs
index 77e7cfb95e..91b1a36a7c 100644
--- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs
+++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs
@@ -174,7 +174,7 @@ namespace Tgstation.Server.Host.Controllers
api.IsGitHub = model.Origin.ToUpperInvariant().Contains(uiGitHub);
api.ActiveJob = job.ToApi();
- return Json(api);
+ return StatusCode((int)HttpStatusCode.Created, api);
}
}
@@ -207,7 +207,7 @@ namespace Tgstation.Server.Host.Controllers
var api = currentModel.ToApi();
await jobManager.RegisterOperation(job, (paramJob, serviceProvider, progressReporter, ct) => instanceManager.GetInstance(Instance).RepositoryManager.DeleteRepository(cancellationToken), cancellationToken).ConfigureAwait(false);
api.ActiveJob = job.ToApi();
- return Ok();
+ return Accepted(api);
}
///
@@ -595,7 +595,7 @@ namespace Tgstation.Server.Host.Controllers
}
}, cancellationToken).ConfigureAwait(false);
- return Json(job.ToApi());
+ return Accepted(job.ToApi());
}
}
}
diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs
index 961e9a99e7..8e0b574398 100644
--- a/src/Tgstation.Server.Host/Controllers/UserController.cs
+++ b/src/Tgstation.Server.Host/Controllers/UserController.cs
@@ -118,7 +118,7 @@ namespace Tgstation.Server.Host.Controllers
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
- return Json(dbUser.ToApi());
+ return StatusCode((int)HttpStatusCode.Created, dbUser.ToApi());
}
///
diff --git a/src/Tgstation.Server.Host/IO/ISynchronousIOManager.cs b/src/Tgstation.Server.Host/IO/ISynchronousIOManager.cs
index 72b241645e..f361004dfe 100644
--- a/src/Tgstation.Server.Host/IO/ISynchronousIOManager.cs
+++ b/src/Tgstation.Server.Host/IO/ISynchronousIOManager.cs
@@ -24,6 +24,14 @@ namespace Tgstation.Server.Host.IO
/// A of directory names in
IEnumerable GetDirectories(string path, CancellationToken cancellationToken);
+ ///
+ /// Create an empty directory at
+ ///
+ /// The path to create
+ /// The for the operation. Usage may result in partial writes
+ /// if the directory already existed, otherwise
+ bool CreateDirectory(string path, CancellationToken cancellationToken);
+
///
/// Read the s of a file at a given
///
diff --git a/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs b/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs
index 4cd4b175a0..9a60da2313 100644
--- a/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs
+++ b/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs
@@ -11,6 +11,16 @@ namespace Tgstation.Server.Host.IO
///
sealed class SynchronousIOManager : ISynchronousIOManager
{
+ ///
+ public bool CreateDirectory(string path, CancellationToken cancellationToken)
+ {
+ if (IsDirectory(path))
+ return true;
+ cancellationToken.ThrowIfCancellationRequested();
+ Directory.CreateDirectory(path);
+ return false;
+ }
+
///
public IEnumerable GetDirectories(string path, CancellationToken cancellationToken)
{
diff --git a/src/Tgstation.Server.Host/Models/DatabaseSeeder.cs b/src/Tgstation.Server.Host/Models/DatabaseSeeder.cs
index 4603c3e09d..5076d67cb8 100644
--- a/src/Tgstation.Server.Host/Models/DatabaseSeeder.cs
+++ b/src/Tgstation.Server.Host/Models/DatabaseSeeder.cs
@@ -11,16 +11,6 @@ namespace Tgstation.Server.Host.Models
///
sealed class DatabaseSeeder : IDatabaseSeeder
{
- ///
- /// The name of the default admin user
- ///
- const string AdminName = "Admin";
-
- ///
- /// The default admin password
- ///
- const string DefaultAdminPassword = "ISolemlySwearToDeleteTheDataDirectory";
-
///
/// The for the
///
@@ -43,11 +33,11 @@ namespace Tgstation.Server.Host.Models
AdministrationRights = (AdministrationRights)~0U,
CreatedAt = DateTimeOffset.Now,
InstanceManagerRights = (InstanceManagerRights)~0U,
- Name = AdminName,
- CanonicalName = AdminName.ToUpperInvariant(),
+ Name = Api.Models.User.AdminName,
+ CanonicalName = Api.Models.User.AdminName.ToUpperInvariant(),
Enabled = true,
};
- cryptographySuite.SetUserPassword(admin, DefaultAdminPassword);
+ cryptographySuite.SetUserPassword(admin, Api.Models.User.DefaultAdminPassword);
databaseContext.Users.Add(admin);
}
@@ -61,13 +51,13 @@ namespace Tgstation.Server.Host.Models
///
public async Task ResetAdminPassword(IDatabaseContext databaseContext, CancellationToken cancellationToken)
{
- var admin = await databaseContext.Users.Where(x => x.CanonicalName == AdminName.ToUpperInvariant()).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
+ var admin = await databaseContext.Users.Where(x => x.CanonicalName == Api.Models.User.AdminName.ToUpperInvariant()).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
if (admin == default)
SeedAdminUser(databaseContext);
else
{
admin.Enabled = true;
- cryptographySuite.SetUserPassword(admin, DefaultAdminPassword);
+ cryptographySuite.SetUserPassword(admin, Api.Models.User.DefaultAdminPassword);
}
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
diff --git a/src/Tgstation.Server.Host/ServerFactory.cs b/src/Tgstation.Server.Host/ServerFactory.cs
index 61f2984323..b8b34e0553 100644
--- a/src/Tgstation.Server.Host/ServerFactory.cs
+++ b/src/Tgstation.Server.Host/ServerFactory.cs
@@ -3,7 +3,7 @@
namespace Tgstation.Server.Host
{
///
- sealed class ServerFactory : IServerFactory
+ public sealed class ServerFactory : IServerFactory
{
///
public IServer CreateServer(string[] args, string updatePath) => new Server(WebHost.CreateDefaultBuilder(args), updatePath);
diff --git a/tests/Tgstation.Server.Tests/AdministrationTest.cs b/tests/Tgstation.Server.Tests/AdministrationTest.cs
new file mode 100644
index 0000000000..ef20efe158
--- /dev/null
+++ b/tests/Tgstation.Server.Tests/AdministrationTest.cs
@@ -0,0 +1,31 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Tgstation.Server.Client;
+
+namespace Tgstation.Server.Tests
+{
+ sealed class AdministrationTest
+ {
+ readonly IAdministrationClient client;
+
+ public AdministrationTest(IAdministrationClient client)
+ {
+ this.client = client ?? throw new ArgumentNullException(nameof(client));
+ }
+
+ public async Task Run()
+ {
+ await TestRead().ConfigureAwait(false);
+ }
+
+ async Task TestRead()
+ {
+ var model = await client.Read(default).ConfigureAwait(false);
+ Assert.AreEqual(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), model.WindowsHost);
+
+ //uhh not much else to do
+ }
+ }
+}
diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs
new file mode 100644
index 0000000000..e630207f2b
--- /dev/null
+++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs
@@ -0,0 +1,50 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System;
+using System.Net.Http.Headers;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using Tgstation.Server.Api;
+using Tgstation.Server.Api.Models;
+using Tgstation.Server.Client;
+using Tgstation.Server.Host;
+
+namespace Tgstation.Server.Tests
+{
+ [TestClass]
+ public sealed class IntegrationTest
+ {
+ readonly IServerClientFactory clientFactory = new ServerClientFactory(new ProductHeaderValue(Assembly.GetExecutingAssembly().GetName().Name, Assembly.GetExecutingAssembly().GetName().Version.ToString()));
+
+ [TestMethod]
+ public async Task FullMonty()
+ {
+ using (var server = new TestingServer())
+ using (var serverCts = new CancellationTokenSource())
+ {
+ var serverTask = server.RunAsync(serverCts.Token);
+ try
+ {
+ using (var adminClient = await clientFactory.CreateServerClient(server.Url, User.AdminName, User.DefaultAdminPassword).ConfigureAwait(false))
+ {
+ var serverInfo = await adminClient.Version(default).ConfigureAwait(false);
+
+ Assert.AreEqual(ApiHeaders.Version, serverInfo.ApiVersion);
+ Assert.AreEqual(typeof(IServer).Assembly.GetName().Version, serverInfo.Version);
+
+ await new AdministrationTest(adminClient.Administration).Run().ConfigureAwait(false);
+ }
+ }
+ finally
+ {
+ serverCts.Cancel();
+ try
+ {
+ await serverTask.ConfigureAwait(false);
+ }
+ catch (OperationCanceledException) { }
+ }
+ }
+ }
+ }
+}
diff --git a/tests/Tgstation.Server.Tests/TestingServer.cs b/tests/Tgstation.Server.Tests/TestingServer.cs
new file mode 100644
index 0000000000..c943f1d821
--- /dev/null
+++ b/tests/Tgstation.Server.Tests/TestingServer.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Tgstation.Server.Host;
+
+namespace Tgstation.Server.Tests
+{
+ sealed class TestingServer : IServer
+ {
+ public Uri Url { get; }
+ public bool RestartRequested => realServer.RestartRequested;
+
+ readonly IServer realServer;
+ readonly string databasePath;
+
+ public TestingServer()
+ {
+ databasePath = Path.GetTempFileName();
+ File.Delete(databasePath);
+ Url = new Uri("http://localhost:5001");
+ realServer = new ServerFactory().CreateServer(new string[]
+ {
+ "--urls",
+ Url.ToString(),
+ "Database:DatabaseType=Sqlite",
+ String.Format(CultureInfo.InvariantCulture, "Database:ConnectionString=Data Source={0}", databasePath)
+ ,"Database:NoMigrations=true" //TODO: remove this when migrations are added
+ }, null);
+ }
+
+ public void Dispose()
+ {
+ realServer.Dispose();
+ File.Delete(databasePath);
+ }
+
+ public Task RunAsync(CancellationToken cancellationToken) => realServer.RunAsync(cancellationToken);
+ }
+}
diff --git a/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj b/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj
new file mode 100644
index 0000000000..1a89f581ca
--- /dev/null
+++ b/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj
@@ -0,0 +1,28 @@
+
+
+
+ netcoreapp2.0
+
+ false
+
+
+
+ 7.1
+
+
+
+ 7.1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tgstation-server.sln b/tgstation-server.sln
index 41c53999f5..bf94dec0a7 100644
--- a/tgstation-server.sln
+++ b/tgstation-server.sln
@@ -101,8 +101,8 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{DCC75431-7913-4306-9FAB-70998D440BCE}"
ProjectSection(SolutionItems) = preProject
docs\API.dox = docs\API.dox
- docs\ArchitectureOverview.png = docs\ArchitectureOverview.png
docs\Architecture.dox = docs\Architecture.dox
+ docs\ArchitectureOverview.png = docs\ArchitectureOverview.png
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{E82104F4-F5C4-4786-ACD4-B635166CDB21}"
@@ -112,85 +112,66 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{E821
.github\ISSUE_TEMPLATE.md = .github\ISSUE_TEMPLATE.md
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tgstation.Server.Tests", "tests\Tgstation.Server.Tests\Tgstation.Server.Tests.csproj", "{09056964-1C74-445A-96EC-33F6DFC07916}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
- Docker|Any CPU = Docker|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A09C947F-4B9B-4AEE-AB50-47055EA05F18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A09C947F-4B9B-4AEE-AB50-47055EA05F18}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {A09C947F-4B9B-4AEE-AB50-47055EA05F18}.Docker|Any CPU.ActiveCfg = Release|Any CPU
- {A09C947F-4B9B-4AEE-AB50-47055EA05F18}.Docker|Any CPU.Build.0 = Release|Any CPU
{A09C947F-4B9B-4AEE-AB50-47055EA05F18}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A09C947F-4B9B-4AEE-AB50-47055EA05F18}.Release|Any CPU.Build.0 = Release|Any CPU
{D36B99C4-E771-42D6-A95F-1102B3E236DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D36B99C4-E771-42D6-A95F-1102B3E236DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D36B99C4-E771-42D6-A95F-1102B3E236DF}.Docker|Any CPU.ActiveCfg = Release|Any CPU
- {D36B99C4-E771-42D6-A95F-1102B3E236DF}.Docker|Any CPU.Build.0 = Release|Any CPU
{D36B99C4-E771-42D6-A95F-1102B3E236DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D36B99C4-E771-42D6-A95F-1102B3E236DF}.Release|Any CPU.Build.0 = Release|Any CPU
{2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.Docker|Any CPU.ActiveCfg = Release|Any CPU
- {2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.Docker|Any CPU.Build.0 = Release|Any CPU
{2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.Release|Any CPU.Build.0 = Release|Any CPU
{8B4A208D-A48A-4A5D-8B94-E2661138865D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8B4A208D-A48A-4A5D-8B94-E2661138865D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {8B4A208D-A48A-4A5D-8B94-E2661138865D}.Docker|Any CPU.ActiveCfg = Release|Any CPU
- {8B4A208D-A48A-4A5D-8B94-E2661138865D}.Docker|Any CPU.Build.0 = Release|Any CPU
{8B4A208D-A48A-4A5D-8B94-E2661138865D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8B4A208D-A48A-4A5D-8B94-E2661138865D}.Release|Any CPU.Build.0 = Release|Any CPU
{29927416-3B78-49A7-A560-5CCAA638B6B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{29927416-3B78-49A7-A560-5CCAA638B6B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {29927416-3B78-49A7-A560-5CCAA638B6B4}.Docker|Any CPU.ActiveCfg = Release|Any CPU
- {29927416-3B78-49A7-A560-5CCAA638B6B4}.Docker|Any CPU.Build.0 = Release|Any CPU
{29927416-3B78-49A7-A560-5CCAA638B6B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{29927416-3B78-49A7-A560-5CCAA638B6B4}.Release|Any CPU.Build.0 = Release|Any CPU
{E0AC911F-7675-4A91-9499-D8A2E2390AAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E0AC911F-7675-4A91-9499-D8A2E2390AAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {E0AC911F-7675-4A91-9499-D8A2E2390AAD}.Docker|Any CPU.ActiveCfg = Release|Any CPU
- {E0AC911F-7675-4A91-9499-D8A2E2390AAD}.Docker|Any CPU.Build.0 = Release|Any CPU
{E0AC911F-7675-4A91-9499-D8A2E2390AAD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E0AC911F-7675-4A91-9499-D8A2E2390AAD}.Release|Any CPU.Build.0 = Release|Any CPU
{BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.Docker|Any CPU.ActiveCfg = Release|Any CPU
- {BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.Docker|Any CPU.Build.0 = Release|Any CPU
{BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.Release|Any CPU.Build.0 = Release|Any CPU
{E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.Docker|Any CPU.ActiveCfg = Release|Any CPU
- {E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.Docker|Any CPU.Build.0 = Release|Any CPU
{E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.Release|Any CPU.Build.0 = Release|Any CPU
{A3362FF6-550F-480F-859E-8EC1EB6EAB31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A3362FF6-550F-480F-859E-8EC1EB6EAB31}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {A3362FF6-550F-480F-859E-8EC1EB6EAB31}.Docker|Any CPU.ActiveCfg = Release|Any CPU
- {A3362FF6-550F-480F-859E-8EC1EB6EAB31}.Docker|Any CPU.Build.0 = Release|Any CPU
{A3362FF6-550F-480F-859E-8EC1EB6EAB31}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A3362FF6-550F-480F-859E-8EC1EB6EAB31}.Release|Any CPU.Build.0 = Release|Any CPU
{5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.Docker|Any CPU.ActiveCfg = Release|Any CPU
- {5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.Docker|Any CPU.Build.0 = Release|Any CPU
{5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.Release|Any CPU.Build.0 = Release|Any CPU
{7500F776-4384-4B5F-A8D8-22461CAD108B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7500F776-4384-4B5F-A8D8-22461CAD108B}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {7500F776-4384-4B5F-A8D8-22461CAD108B}.Docker|Any CPU.ActiveCfg = Release|Any CPU
- {7500F776-4384-4B5F-A8D8-22461CAD108B}.Docker|Any CPU.Build.0 = Release|Any CPU
{7500F776-4384-4B5F-A8D8-22461CAD108B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7500F776-4384-4B5F-A8D8-22461CAD108B}.Release|Any CPU.Build.0 = Release|Any CPU
{AA80A190-52E2-4BE3-BFEB-1F148D9E9007}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AA80A190-52E2-4BE3-BFEB-1F148D9E9007}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {AA80A190-52E2-4BE3-BFEB-1F148D9E9007}.Docker|Any CPU.ActiveCfg = Release|Any CPU
- {AA80A190-52E2-4BE3-BFEB-1F148D9E9007}.Docker|Any CPU.Build.0 = Release|Any CPU
{AA80A190-52E2-4BE3-BFEB-1F148D9E9007}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AA80A190-52E2-4BE3-BFEB-1F148D9E9007}.Release|Any CPU.Build.0 = Release|Any CPU
+ {09056964-1C74-445A-96EC-33F6DFC07916}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {09056964-1C74-445A-96EC-33F6DFC07916}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {09056964-1C74-445A-96EC-33F6DFC07916}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {09056964-1C74-445A-96EC-33F6DFC07916}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/tools/TGS.postman_collection.json b/tools/TGS.postman_collection.json
index c7f7374670..635c67dc90 100644
--- a/tools/TGS.postman_collection.json
+++ b/tools/TGS.postman_collection.json
@@ -1,6 +1,6 @@
{
"info": {
- "_postman_id": "b7e61e38-6891-473e-8ab4-869e5cf41a96",
+ "_postman_id": "bb44d5ca-de20-4da3-880d-1ceacd54fe4a",
"name": "TGS",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
@@ -2624,6 +2624,49 @@
}
},
"response": []
+ },
+ {
+ "name": "Create NewDir",
+ "request": {
+ "method": "PUT",
+ "header": [
+ {
+ "key": "Accept",
+ "value": "application/json"
+ },
+ {
+ "key": "User-Agent",
+ "value": "Postman/1.0"
+ },
+ {
+ "key": "Api",
+ "value": "Tgstation.Server.Api/4.0.0.0"
+ },
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ },
+ {
+ "key": "Instance",
+ "value": "1"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n\t\"path\": \"NewDir\"\n}"
+ },
+ "url": {
+ "raw": "localhost:5000/Config",
+ "host": [
+ "localhost"
+ ],
+ "port": "5000",
+ "path": [
+ "Config"
+ ]
+ }
+ },
+ "response": []
}
],
"_postman_isSubFolder": true
diff --git a/v4_prototype_TODO.txt b/v4_prototype_TODO.txt
index 641f358e0e..3b97a1ae4d 100644
--- a/v4_prototype_TODO.txt
+++ b/v4_prototype_TODO.txt
@@ -3,4 +3,6 @@ Verify the byond cache folder location on linux
Test watchdog
Only show user name and ID when serializing to API
-In fact remove IApiConvertable<> altogether, it's not required by anything
\ No newline at end of file
+In fact remove IApiConvertable<> altogether, it's not required by anything
+
+Chat channel tagging