diff --git a/src/Tgstation.Server.Api/ApiHeaders.cs b/src/Tgstation.Server.Api/ApiHeaders.cs
index 3758ec9b34..9308ad9976 100644
--- a/src/Tgstation.Server.Api/ApiHeaders.cs
+++ b/src/Tgstation.Server.Api/ApiHeaders.cs
@@ -3,9 +3,9 @@ using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.Linq;
using System.Net.Http.Headers;
+using System.Net.Mime;
using System.Reflection;
using System.Text;
@@ -16,20 +16,15 @@ namespace Tgstation.Server.Api
///
public sealed class ApiHeaders
{
- ///
- /// TODO: Remove this when we upgrade to .NET Standard 2.1
- ///
- public const string ApplicationJson = "application/json";
-
///
/// The header key
///
- public const string ApiVersionHeader = "api";
+ public const string ApiVersionHeader = "Api";
///
/// The header key
///
- public const string InstanceIdHeader = "instance";
+ public const string InstanceIdHeader = "Instance";
///
/// The JWT authentication header scheme
@@ -41,16 +36,6 @@ namespace Tgstation.Server.Api
///
public const string BasicAuthenticationScheme = "basic";
- ///
- /// The header key
- ///
- const string UsernameHeader = "username";
-
- ///
- /// The basic authentication header scheme
- ///
- const string PasswordAuthenticationScheme = "password";
-
///
/// The current
///
@@ -141,92 +126,105 @@ namespace Tgstation.Server.Api
/// Construct and validates from a set of
///
/// The containing the
+ /// Thrown if the constitue invalid .
public ApiHeaders(RequestHeaders requestHeaders)
{
if (requestHeaders == null)
throw new ArgumentNullException(nameof(requestHeaders));
- var jsonAccept = new Microsoft.Net.Http.Headers.MediaTypeHeaderValue(ApplicationJson);
- if (!requestHeaders.Accept.Any(x => x.MediaType == jsonAccept.MediaType))
- throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Client does not accept {0}!", ApplicationJson));
+ var badHeaders = HeaderTypes.None;
+ var errorBuilder = new StringBuilder();
+
+ void AddError(HeaderTypes headerType, string message)
+ {
+ if (badHeaders != HeaderTypes.None)
+ errorBuilder.Append(Environment.NewLine);
+ badHeaders |= headerType;
+ errorBuilder.Append(message);
+ }
+
+ var jsonAccept = new Microsoft.Net.Http.Headers.MediaTypeHeaderValue(MediaTypeNames.Application.Json);
+ if (!requestHeaders.Accept.Any(x => jsonAccept.IsSubsetOf(x)))
+ AddError(HeaderTypes.Accept, $"Client does not accept {MediaTypeNames.Application.Json}!");
if (!requestHeaders.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgentValues) || userAgentValues.Count == 0)
- throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Missing {0} headers!", HeaderNames.UserAgent));
-
- RawUserAgent = userAgentValues.First();
- if (String.IsNullOrWhiteSpace(RawUserAgent))
- throw new InvalidOperationException("Malformed client User-Agent!");
+ AddError(HeaderTypes.UserAgent, $"Missing {HeaderNames.UserAgent} header!");
+ else
+ {
+ RawUserAgent = userAgentValues.First();
+ if (String.IsNullOrWhiteSpace(RawUserAgent))
+ AddError(HeaderTypes.UserAgent, $"Malformed {HeaderNames.UserAgent} header!");
+ }
// make sure the api header matches ours
+ Version? apiVersion = null;
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 version!");
-
- if (!Version.TryParse(apiUserAgent.Product.Version, out var apiVersion))
- throw new InvalidOperationException("Malformed API version!");
-
- ApiVersion = apiVersion.Semver();
+ AddError(HeaderTypes.Api, $"Missing {ApiVersionHeader} header!");
+ else if (!Version.TryParse(apiUserAgent.Product.Version, out apiVersion))
+ AddError(HeaderTypes.Api, $"Malformed {ApiVersionHeader} header!");
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();
- var splits = new List(auth.Split(' '));
- var scheme = splits.First();
- if (String.IsNullOrWhiteSpace(scheme))
- throw new InvalidOperationException("Missing authentication scheme!");
-
- splits.RemoveAt(0);
- var parameter = String.Concat(splits);
- if (String.IsNullOrEmpty(parameter))
- throw new InvalidOperationException("Missing authentication parameter!");
-
- if (requestHeaders.Headers.TryGetValue(InstanceIdHeader, out var instanceIdValues))
+ AddError(HeaderTypes.Authorization, $"Missing {HeaderNames.Authorization} header!");
+ else
{
- var instanceIdString = instanceIdValues.FirstOrDefault();
- if (instanceIdString != default && Int64.TryParse(instanceIdString, out var instanceId))
- InstanceId = instanceId;
- }
+ var auth = authorization.First();
+ var splits = new List(auth.Split(' '));
+ var scheme = splits.First();
+ if (String.IsNullOrWhiteSpace(scheme))
+ AddError(HeaderTypes.Authorization, "Missing authentication scheme!");
+ else
+ {
+ splits.RemoveAt(0);
+ var parameter = String.Concat(splits);
+ if (String.IsNullOrEmpty(parameter))
+ AddError(HeaderTypes.Authorization, "Missing authentication parameter!");
+ else
+ {
+ if (requestHeaders.Headers.TryGetValue(InstanceIdHeader, out var instanceIdValues))
+ {
+ var instanceIdString = instanceIdValues.FirstOrDefault();
+ if (instanceIdString != default && Int64.TryParse(instanceIdString, out var instanceId))
+ InstanceId = instanceId;
+ }
#pragma warning disable CA1308 // Normalize strings to uppercase
- switch (scheme.ToLowerInvariant())
+ switch (scheme.ToLowerInvariant())
#pragma warning restore CA1308 // Normalize strings to uppercase
- {
- case JwtAuthenticationScheme:
- Token = parameter;
- break;
- case PasswordAuthenticationScheme:
- Password = parameter;
- var fail = !requestHeaders.Headers.TryGetValue(UsernameHeader, out var values);
- if (!fail)
- {
- Username = values.FirstOrDefault();
- fail = String.IsNullOrWhiteSpace(Username);
- }
+ {
+ case JwtAuthenticationScheme:
+ Token = parameter;
+ break;
+ case BasicAuthenticationScheme:
+ string joinedString;
+ try
+ {
+ var base64Bytes = Convert.FromBase64String(parameter);
+ joinedString = Encoding.UTF8.GetString(base64Bytes);
+ }
+ catch
+ {
+ throw new InvalidOperationException("Invalid basic Authorization header!");
+ }
- if (fail)
- throw new InvalidOperationException("Missing Username header!");
- break;
- case BasicAuthenticationScheme:
- string joinedString;
- try
- {
- var base64Bytes = Convert.FromBase64String(parameter);
- joinedString = Encoding.UTF8.GetString(base64Bytes);
- }
- catch
- {
- throw new InvalidOperationException("Invalid basic Authorization header!");
- }
+ var basicAuthSplits = joinedString.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
+ if (basicAuthSplits.Length < 2)
+ throw new InvalidOperationException("Invalid basic Authorization header!");
- var basicAuthSplits = joinedString.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
- if (basicAuthSplits.Length < 2)
- throw new InvalidOperationException("Invalid basic Authorization header!");
-
- Username = basicAuthSplits.First();
- Password = String.Concat(basicAuthSplits.Skip(1));
- break;
- default:
- throw new InvalidOperationException("Invalid authentication scheme!");
+ Username = basicAuthSplits.First();
+ Password = String.Concat(basicAuthSplits.Skip(1));
+ break;
+ default:
+ AddError(HeaderTypes.Authorization, "Invalid authentication scheme!");
+ break;
+ }
+ }
+ }
}
+
+ if (badHeaders != HeaderTypes.None)
+ throw new HeadersException(badHeaders, errorBuilder.ToString());
+
+ ApiVersion = apiVersion!.Semver();
}
///
@@ -261,10 +259,10 @@ namespace Tgstation.Server.Api
if (headers == null)
throw new ArgumentNullException(nameof(headers));
if (instanceId.HasValue && InstanceId.HasValue && instanceId != InstanceId)
- throw new InvalidOperationException("Specified instance ID in constructor and SetRequestHeaders!");
+ throw new InvalidOperationException("Specified different instance IDs in constructor and SetRequestHeaders!");
headers.Clear();
- headers.Accept.Add(new MediaTypeWithQualityHeaderValue(ApplicationJson));
+ headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
if (IsTokenAuthentication)
headers.Authorization = new AuthenticationHeaderValue(JwtAuthenticationScheme, Token);
else
diff --git a/src/Tgstation.Server.Api/HeaderTypes.cs b/src/Tgstation.Server.Api/HeaderTypes.cs
new file mode 100644
index 0000000000..a1d0a6e17e
--- /dev/null
+++ b/src/Tgstation.Server.Api/HeaderTypes.cs
@@ -0,0 +1,36 @@
+using System;
+
+namespace Tgstation.Server.Api
+{
+ ///
+ /// Types of individual .
+ ///
+ [Flags]
+ public enum HeaderTypes
+ {
+ ///
+ /// No headers.
+ ///
+ None = 0,
+
+ ///
+ /// header.
+ ///
+ UserAgent = 1,
+
+ ///
+ /// header.
+ ///
+ Accept = 2,
+
+ ///
+ /// Api header.
+ ///
+ Api = 4,
+
+ ///
+ ///
+ ///
+ Authorization = 8
+ }
+}
\ No newline at end of file
diff --git a/src/Tgstation.Server.Api/HeadersException.cs b/src/Tgstation.Server.Api/HeadersException.cs
new file mode 100644
index 0000000000..e13ef45598
--- /dev/null
+++ b/src/Tgstation.Server.Api/HeadersException.cs
@@ -0,0 +1,49 @@
+using System;
+
+namespace Tgstation.Server.Api
+{
+ ///
+ /// Thrown when trying to generate from fails.
+ ///
+ public sealed class HeadersException : Exception
+ {
+ ///
+ /// The s that are missing or malformed.
+ ///
+ public HeaderTypes MissingOrMalformedHeaders { get; }
+
+ ///
+ /// Initializes a new instance of the .
+ ///
+ /// The value of .
+ /// The error message.
+ public HeadersException(HeaderTypes missingOrMalformedHeaders, string message) : base(message)
+ {
+ MissingOrMalformedHeaders = missingOrMalformedHeaders;
+ }
+
+ ///
+ /// Initializes a new instance of the .
+ ///
+ public HeadersException()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the .
+ ///
+ /// The error message.
+ public HeadersException(string message) : base(message)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the .
+ ///
+ /// The error message.
+ /// The inner for the base
+ public HeadersException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Client/ApiClient.cs b/src/Tgstation.Server.Client/ApiClient.cs
index cf1041fc86..0e107e91f8 100644
--- a/src/Tgstation.Server.Client/ApiClient.cs
+++ b/src/Tgstation.Server.Client/ApiClient.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Net.Mime;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -157,7 +158,10 @@ namespace Tgstation.Server.Client
using (var request = new HttpRequestMessage(method, fullUri))
{
if (body != null)
- request.Content = new StringContent(JsonConvert.SerializeObject(body, serializerSettings), Encoding.UTF8, ApiHeaders.ApplicationJson);
+ request.Content = new StringContent(
+ JsonConvert.SerializeObject(body, serializerSettings),
+ Encoding.UTF8,
+ MediaTypeNames.Application.Json);
var headersToUse = tokenRefresh ? tokenRefreshHeaders! : headers;
headersToUse.SetRequestHeaders(request.Headers, instanceId);
diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs
index f8321e4ec3..f17c625e12 100644
--- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs
+++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs
@@ -17,6 +17,7 @@ using Tgstation.Server.Host.Components.Repository;
using Tgstation.Server.Host.Components.Session;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Database;
+using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Models;
@@ -629,7 +630,8 @@ namespace Tgstation.Server.Host.Components.Deployment
activeCompileJob?.RevisionInformation,
repositorySettings,
repoOwner,
- repoName);
+ repoName,
+ cancellationToken);
var eventTask = eventConsumer.HandleEvent(EventType.DeploymentComplete, null, cancellationToken);
@@ -755,13 +757,15 @@ namespace Tgstation.Server.Host.Components.Deployment
/// The .
/// The GitHub repostiory owner.
/// The GitHub repostiory name.
+ /// The for the operation.
/// A representing the running operation.
async Task PostDeploymentComments(
Models.RevisionInformation deployedRevisionInformation,
Models.RevisionInformation previousRevisionInformation,
Models.RepositorySettings repositorySettings,
string repoOwner,
- string repoName)
+ string repoName,
+ CancellationToken cancellationToken)
{
if (repositorySettings?.AccessToken == null)
return;
@@ -781,7 +785,9 @@ namespace Tgstation.Server.Host.Components.Deployment
{
try
{
- await gitHubClient.Issue.Comment.Create(repoOwner, repoName, prNumber, comment).ConfigureAwait(false);
+ await gitHubClient.Issue.Comment.Create(repoOwner, repoName, prNumber, comment)
+ .WithToken(cancellationToken)
+ .ConfigureAwait(false);
}
catch (ApiException e)
{
diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs
index 8c470d4f59..7a0219d18f 100644
--- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs
+++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs
@@ -102,8 +102,7 @@ namespace Tgstation.Server.Host.Controllers
databaseContext,
authenticationContextFactory,
logger,
- false,
- true)
+ false)
{
this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
this.serverUpdater = serverUpdater ?? throw new ArgumentNullException(nameof(serverUpdater));
@@ -120,7 +119,7 @@ namespace Tgstation.Server.Host.Controllers
Logger.LogWarning("Exceeded GitHub rate limit! Exception {0}", exception);
var secondsString = Math.Ceiling((exception.Reset - DateTimeOffset.Now).TotalSeconds).ToString(CultureInfo.InvariantCulture);
Response.Headers.Add("Retry-After", new StringValues(secondsString));
- return StatusCode(429, new ErrorMessage(ErrorCode.GitHubApiRateLimit));
+ return StatusCode(HttpStatusCode.TooManyRequests, new ErrorMessage(ErrorCode.GitHubApiRateLimit));
}
///
@@ -150,7 +149,7 @@ namespace Tgstation.Server.Host.Controllers
catch (ApiException e)
{
Logger.LogWarning(OctokitException, e);
- return StatusCode((int)HttpStatusCode.FailedDependency);
+ return StatusCode(HttpStatusCode.FailedDependency);
}
releases = releases.Where(x => x.TagName.StartsWith(updatesConfiguration.GitTagPrefix, StringComparison.InvariantCulture));
@@ -158,7 +157,11 @@ namespace Tgstation.Server.Host.Controllers
Logger.LogTrace("Release query complete!");
foreach (var release in releases)
- if (Version.TryParse(release.TagName.Replace(updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal), out var version) && version == newVersion)
+ if (Version.TryParse(
+ release.TagName.Replace(
+ updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal),
+ out var version)
+ && version == newVersion)
{
var asset = release.Assets.Where(x => x.Name.Equals(updatesConfiguration.UpdatePackageAssetName, StringComparison.Ordinal)).FirstOrDefault();
if (asset == default)
@@ -181,6 +184,7 @@ namespace Tgstation.Server.Host.Controllers
///
/// Get server information.
///
+ /// The for the operation.
/// A resulting in the for the operation.
/// Retrieved data successfully.
/// The GitHub API rate limit was hit. See response header Retry-After.
@@ -190,7 +194,7 @@ namespace Tgstation.Server.Host.Controllers
[ProducesResponseType(typeof(Administration), 200)]
[ProducesResponseType(typeof(ErrorMessage), 424)]
[ProducesResponseType(typeof(ErrorMessage), 429)]
- public async Task Read()
+ public async Task Read(CancellationToken cancellationToken)
{
try
{
@@ -199,8 +203,19 @@ namespace Tgstation.Server.Host.Controllers
try
{
var gitHubClient = GetGitHubClient();
- 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));
+ var repositoryTask = gitHubClient
+ .Repository
+ .Get(updatesConfiguration.GitHubRepositoryId)
+ .WithToken(cancellationToken);
+ var releases = (await gitHubClient
+ .Repository
+ .Release
+ .GetAll(updatesConfiguration.GitHubRepositoryId)
+ .WithToken(cancellationToken)
+ .ConfigureAwait(false))
+ .Where(x => x.TagName.StartsWith(
+ updatesConfiguration.GitTagPrefix,
+ StringComparison.InvariantCulture));
foreach (var I in releases)
if (Version.TryParse(I.TagName.Replace(updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal), out var version)
@@ -228,7 +243,7 @@ namespace Tgstation.Server.Host.Controllers
catch (ApiException e)
{
Logger.LogWarning(OctokitException, e);
- return StatusCode((int)HttpStatusCode.FailedDependency, new ErrorMessage(ErrorCode.GitHubApiError)
+ return StatusCode(HttpStatusCode.FailedDependency, new ErrorMessage(ErrorCode.GitHubApiError)
{
AdditionalData = e.Message
});
@@ -298,7 +313,7 @@ namespace Tgstation.Server.Host.Controllers
}
catch (InvalidOperationException)
{
- return StatusCode((int)HttpStatusCode.ServiceUnavailable);
+ return StatusCode(HttpStatusCode.ServiceUnavailable);
}
}
diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs
index 5cd3865257..88309c993a 100644
--- a/src/Tgstation.Server.Host/Controllers/ApiController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs
@@ -6,6 +6,7 @@ using Serilog.Context;
using System;
using System.Linq;
using System.Net;
+using System.Net.Mime;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
@@ -17,7 +18,7 @@ namespace Tgstation.Server.Host.Controllers
///
/// A for API functions
///
- [Produces(ApiHeaders.ApplicationJson)]
+ [Produces(MediaTypeNames.Application.Json)]
[ApiController]
public abstract class ApiController : Controller
{
@@ -64,7 +65,12 @@ namespace Tgstation.Server.Host.Controllers
/// The value of
/// The value of
/// The value of
- public ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance, bool requireHeaders)
+ public ApiController(
+ IDatabaseContext databaseContext,
+ IAuthenticationContextFactory authenticationContextFactory,
+ ILogger logger,
+ bool requireInstance,
+ bool requireHeaders = true)
{
DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
if (authenticationContextFactory == null)
@@ -80,22 +86,75 @@ namespace Tgstation.Server.Host.Controllers
/// Generic 410 response.
///
/// An with .
- protected ObjectResult Gone() => StatusCode((int)HttpStatusCode.Gone, new ErrorMessage(ErrorCode.ResourceNotPresent));
+ protected ObjectResult Gone() => StatusCode(HttpStatusCode.Gone, new ErrorMessage(ErrorCode.ResourceNotPresent));
///
/// Generic 404 response.
///
/// An with .
- protected new ObjectResult NotFound() => NotFound(new ErrorMessage(ErrorCode.ResourceNeverPresent));
+ protected new NotFoundObjectResult NotFound() => NotFound(new ErrorMessage(ErrorCode.ResourceNeverPresent));
///
/// Generic 501 response.
///
/// An with .
- protected ObjectResult RequiresPosixSystemIdentity() => StatusCode((int)HttpStatusCode.NotImplemented, new ErrorMessage(ErrorCode.RequiresPosixSystemIdentity));
+ protected ObjectResult RequiresPosixSystemIdentity() => StatusCode(HttpStatusCode.NotImplemented, new ErrorMessage(ErrorCode.RequiresPosixSystemIdentity));
+
+ ///
+ /// Strongly type calls to .
+ ///
+ /// The .
+ /// A with the given .
+ protected StatusCodeResult StatusCode(HttpStatusCode statusCode) => StatusCode((int)statusCode);
+
+ ///
+ /// Strongly type calls to .
+ ///
+ /// The .
+ /// The accompanying payload.
+ /// A with the given .
+ protected ObjectResult StatusCode(HttpStatusCode statusCode, object errorMessage) => StatusCode((int)statusCode, errorMessage);
+
+ ///
+ /// Generic 201 response with a given .
+ ///
+ /// The accompanying API payload.
+ /// A with the given .
+ protected ObjectResult Created(object payload) => StatusCode((int)HttpStatusCode.Created, payload);
+
+ ///
+ /// Response for missing/Invalid headers.
+ ///
+ /// The appropriate .
+ protected IActionResult HeadersIssue()
+ {
+ HeadersException headersException;
+ try
+ {
+ var _ = new ApiHeaders(Request.GetTypedHeaders());
+ throw new InvalidOperationException("Expected a header parse exception!");
+ }
+ catch (HeadersException ex)
+ {
+ headersException = ex;
+ }
+
+ var errorMessage = new ErrorMessage(ErrorCode.BadHeaders)
+ {
+ AdditionalData = headersException.Message
+ };
+
+ if (headersException.MissingOrMalformedHeaders.HasFlag(HeaderTypes.Accept))
+ return StatusCode(HttpStatusCode.NotAcceptable, errorMessage);
+
+ if (headersException.MissingOrMalformedHeaders == HeaderTypes.Authorization)
+ return Unauthorized(errorMessage);
+
+ return BadRequest(errorMessage);
+ }
///
- #pragma warning disable CA1506 // TODO: Decomplexify
+#pragma warning disable CA1506 // TODO: Decomplexify
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// ALL valid token and login requests that match a route go through this function
@@ -115,7 +174,7 @@ namespace Tgstation.Server.Host.Controllers
if (!ApiHeaders.Compatible())
{
await StatusCode(
- (int)HttpStatusCode.UpgradeRequired,
+ HttpStatusCode.UpgradeRequired,
new ErrorMessage(ErrorCode.ApiMismatch))
.ExecuteResultAsync(context)
.ConfigureAwait(false);
@@ -138,15 +197,11 @@ namespace Tgstation.Server.Host.Controllers
}
}
}
- catch (InvalidOperationException e)
+ catch (HeadersException)
{
if (requireHeaders)
{
- await BadRequest(
- new ErrorMessage(ErrorCode.BadHeaders)
- {
- AdditionalData = e.Message
- })
+ await HeadersIssue()
.ExecuteResultAsync(context)
.ConfigureAwait(false);
return;
diff --git a/src/Tgstation.Server.Host/Controllers/BridgeController.cs b/src/Tgstation.Server.Host/Controllers/BridgeController.cs
index 3ee3155cd8..1a0e794e9a 100644
--- a/src/Tgstation.Server.Host/Controllers/BridgeController.cs
+++ b/src/Tgstation.Server.Host/Controllers/BridgeController.cs
@@ -5,9 +5,9 @@ using Newtonsoft.Json;
using Serilog.Context;
using System;
using System.Net;
+using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
-using Tgstation.Server.Api;
using Tgstation.Server.Host.Components.Interop;
using Tgstation.Server.Host.Components.Interop.Bridge;
@@ -17,7 +17,7 @@ namespace Tgstation.Server.Host.Controllers
/// for recieving DMAPI requests from DreamDaemon.
///
[Route("Bridge")]
- [Produces(ApiHeaders.ApplicationJson)]
+ [Produces(MediaTypeNames.Application.Json)]
public class BridgeController : Controller
{
///
@@ -86,7 +86,7 @@ namespace Tgstation.Server.Host.Controllers
var responseJson = JsonConvert.SerializeObject(response, DMApiConstants.SerializerSettings);
logger.LogTrace("Bridge Response: {0}", responseJson);
- return Content(responseJson, ApiHeaders.ApplicationJson);
+ return Content(responseJson, MediaTypeNames.Application.Json);
}
}
}
diff --git a/src/Tgstation.Server.Host/Controllers/ByondController.cs b/src/Tgstation.Server.Host/Controllers/ByondController.cs
index c6f9dbd0d7..0e27ba918c 100644
--- a/src/Tgstation.Server.Host/Controllers/ByondController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ByondController.cs
@@ -40,7 +40,17 @@ namespace Tgstation.Server.Host.Controllers
/// The value of
/// The value of
/// The for the
- public ByondController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IJobManager jobManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
+ public ByondController(
+ IDatabaseContext databaseContext,
+ IAuthenticationContextFactory authenticationContextFactory,
+ IInstanceManager instanceManager,
+ IJobManager jobManager,
+ ILogger logger)
+ : base(
+ databaseContext,
+ authenticationContextFactory,
+ logger,
+ true)
{
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs
index 5c47fbd619..f642156e0f 100644
--- a/src/Tgstation.Server.Host/Controllers/ChatController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs
@@ -39,7 +39,16 @@ namespace Tgstation.Server.Host.Controllers
/// The for the
/// The value of
/// The for the
- public ChatController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
+ public ChatController(
+ IDatabaseContext databaseContext,
+ IAuthenticationContextFactory authenticationContextFactory,
+ IInstanceManager instanceManager,
+ ILogger logger)
+ : base(
+ databaseContext,
+ authenticationContextFactory,
+ logger,
+ true)
{
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
}
@@ -125,7 +134,7 @@ namespace Tgstation.Server.Host.Controllers
throw;
}
- return StatusCode((int)HttpStatusCode.Created, dbModel.ToApi());
+ return StatusCode(HttpStatusCode.Created, dbModel.ToApi());
}
///
diff --git a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs
index 4cebfdbc8a..a697f36a12 100644
--- a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs
@@ -3,7 +3,6 @@ using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
-using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
@@ -41,7 +40,17 @@ namespace Tgstation.Server.Host.Controllers
/// The value of
/// The value of
/// The for the
- public ConfigurationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IIOManager ioManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
+ public ConfigurationController(
+ IDatabaseContext databaseContext,
+ IAuthenticationContextFactory authenticationContextFactory,
+ IInstanceManager instanceManager,
+ IIOManager ioManager,
+ ILogger logger)
+ : base(
+ databaseContext,
+ authenticationContextFactory,
+ logger,
+ true)
{
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
@@ -95,7 +104,7 @@ namespace Tgstation.Server.Host.Controllers
newFile.Content = null;
- return model.LastReadHash == null ? (IActionResult)StatusCode((int)HttpStatusCode.Created, newFile) : Json(newFile);
+ return model.LastReadHash == null ? (IActionResult)Created(newFile) : Json(newFile);
}
catch(IOException e)
{
@@ -218,7 +227,13 @@ namespace Tgstation.Server.Host.Controllers
try
{
model.IsDirectory = true;
- return await instanceManager.GetInstance(Instance).Configuration.CreateDirectory(model.Path, systemIdentity, cancellationToken).ConfigureAwait(false) ? (IActionResult)Json(model) : StatusCode((int)HttpStatusCode.Created, model);
+ return await instanceManager
+ .GetInstance(Instance)
+ .Configuration
+ .CreateDirectory(model.Path, systemIdentity, cancellationToken)
+ .ConfigureAwait(false)
+ ? (IActionResult)Json(model)
+ : Created(model);
}
catch (IOException e)
{
diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
index aa66421cc8..9ae5df0a0e 100644
--- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
+++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
@@ -43,7 +43,17 @@ namespace Tgstation.Server.Host.Controllers
/// The value of
/// The value of
/// The for the
- public DreamDaemonController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
+ public DreamDaemonController(
+ IDatabaseContext databaseContext,
+ IAuthenticationContextFactory authenticationContextFactory,
+ IJobManager jobManager,
+ IInstanceManager instanceManager,
+ ILogger logger)
+ : base(
+ databaseContext,
+ authenticationContextFactory,
+ logger,
+ true)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs
index b40053100b..8839a3af5c 100644
--- a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs
+++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs
@@ -42,7 +42,17 @@ namespace Tgstation.Server.Host.Controllers
/// The value of
/// The value of
/// The for the
- public DreamMakerController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
+ public DreamMakerController(
+ IDatabaseContext databaseContext,
+ IAuthenticationContextFactory authenticationContextFactory,
+ IJobManager jobManager,
+ IInstanceManager instanceManager,
+ ILogger logger)
+ : base(
+ databaseContext,
+ authenticationContextFactory,
+ logger,
+ true)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs
index 5b0fa18a95..f8c5195413 100644
--- a/src/Tgstation.Server.Host/Controllers/HomeController.cs
+++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs
@@ -1,5 +1,4 @@
using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@@ -12,6 +11,7 @@ using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
+using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.Components.Interop;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Core;
@@ -95,14 +95,20 @@ namespace Tgstation.Server.Host.Controllers
IOptions generalConfigurationOptions,
IOptions controlPanelConfigurationOptions,
ILogger logger)
- : base(databaseContext, authenticationContextFactory, logger, false, false)
+ : base(
+ databaseContext,
+ authenticationContextFactory,
+ logger,
+ false,
+ (browserResolver ?? throw new ArgumentNullException(nameof(browserResolver))).Browser.Type != BrowserType.Generic
+ && (controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions))).Enable)
{
this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
- this.browserResolver = browserResolver ?? throw new ArgumentNullException(nameof(browserResolver));
+ this.browserResolver = browserResolver;
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions));
}
@@ -111,17 +117,17 @@ namespace Tgstation.Server.Host.Controllers
/// Main page of the
///
///
- /// The of the if a properly authenticated API request, the web control panel if on a browser and enabled, otherwise.
+ /// The of the if a properly authenticated API request, the web control panel if on a browser and enabled, otherwise.
///
- /// retrieved successfully.
+ /// retrieved successfully.
[HttpGet]
[TgsAuthorize]
[AllowAnonymous]
- [ProducesResponseType(typeof(Api.Models.ServerInformation), 200)]
+ [ProducesResponseType(typeof(ServerInformation), 200)]
public IActionResult Home()
{
if (AuthenticationContext != null)
- return Json(new Api.Models.ServerInformation
+ return Json(new ServerInformation
{
Version = assemblyInformationProvider.Version,
ApiVersion = ApiHeaders.Version,
@@ -139,7 +145,7 @@ namespace Tgstation.Server.Host.Controllers
return File("~/index.html", MediaTypeNames.Text.Html);
}
- return Unauthorized();
+ return ApiHeaders == null ? HeadersIssue() : Unauthorized();
}
///
@@ -147,38 +153,22 @@ namespace Tgstation.Server.Host.Controllers
///
/// The for the operation
/// A resulting in the of the operation
- /// User logged in and generated successfully.
+ /// User logged in and generated successfully.
/// User authentication failed.
/// User authenticated but is disabled by an administrator.
[HttpPost]
- [ProducesResponseType(typeof(Api.Models.Token), 200)]
+ [ProducesResponseType(typeof(Token), 200)]
#pragma warning disable CA1506 // TODO: Decomplexify
public async Task CreateToken(CancellationToken cancellationToken)
{
if (ApiHeaders == null)
{
- // Get the exact error
- var errorMessage = "Missing API headers!";
- try
- {
- var _ = new ApiHeaders(Request.GetTypedHeaders());
- }
- catch (InvalidOperationException ex)
- {
- errorMessage = ex.Message;
- }
-
Response.Headers.Add(HeaderNames.WWWAuthenticate, new StringValues("basic realm=\"Create TGS4 bearer token\""));
-
- return BadRequest(
- new Api.Models.ErrorMessage(Api.Models.ErrorCode.BadHeaders)
- {
- AdditionalData = errorMessage
- });
+ return HeadersIssue();
}
if (ApiHeaders.IsTokenAuthentication)
- return BadRequest(new Api.Models.ErrorMessage(Api.Models.ErrorCode.TokenWithToken));
+ return BadRequest(new ErrorMessage(ErrorCode.TokenWithToken));
ISystemIdentity systemIdentity;
try
@@ -194,13 +184,13 @@ namespace Tgstation.Server.Host.Controllers
using (systemIdentity)
{
// Get the user from the database
- IQueryable query = DatabaseContext.Users.AsQueryable();
+ IQueryable query = DatabaseContext.Users.AsQueryable();
string canonicalName = Models.User.CanonicalizeName(ApiHeaders.Username);
if (systemIdentity == null)
query = query.Where(x => x.CanonicalName == canonicalName);
else
query = query.Where(x => x.CanonicalName == canonicalName || x.SystemIdentifier == systemIdentity.Uid);
- var users = await query.Select(x => new User
+ var users = await query.Select(x => new Models.User
{
Id = x.Id,
PasswordHash = x.PasswordHash,
@@ -233,7 +223,7 @@ namespace Tgstation.Server.Host.Controllers
if (user.PasswordHash != originalHash)
{
Logger.LogDebug("User ID {0}'s password hash needs a refresh, updating database.", user.Id);
- var updatedUser = new User
+ var updatedUser = new Models.User
{
Id = user.Id
};
diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs
index db738f96c6..ec5ca653d9 100644
--- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs
+++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs
@@ -8,7 +8,6 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
-using System.Net;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
@@ -92,7 +91,11 @@ namespace Tgstation.Server.Host.Controllers
IPlatformIdentifier platformIdentifier,
IOptions generalConfigurationOptions,
ILogger logger)
- : base(databaseContext, authenticationContextFactory, logger, false, true)
+ : base(
+ databaseContext,
+ authenticationContextFactory,
+ logger,
+ false)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
@@ -307,7 +310,7 @@ namespace Tgstation.Server.Host.Controllers
Logger.LogInformation("{0} {1} instance {2}: {3} ({4})", AuthenticationContext.User.Name, attached ? "attached" : "created", newInstance.Name, newInstance.Id, newInstance.Path);
var api = newInstance.ToApi();
- return attached ? (IActionResult)Json(api) : StatusCode((int)HttpStatusCode.Created, api);
+ return attached ? (IActionResult)Json(api) : Created(api);
}
///
diff --git a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs
index a6c4bef4f5..dbb3ff8dae 100644
--- a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs
+++ b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs
@@ -4,7 +4,6 @@ using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
@@ -29,7 +28,15 @@ namespace Tgstation.Server.Host.Controllers
/// The for the
/// The for the
/// The for the
- public InstanceUserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
+ public InstanceUserController(
+ IDatabaseContext databaseContext,
+ IAuthenticationContextFactory authenticationContextFactory,
+ ILogger logger)
+ : base(
+ databaseContext,
+ authenticationContextFactory,
+ logger,
+ true)
{ }
///
@@ -79,7 +86,7 @@ namespace Tgstation.Server.Host.Controllers
DatabaseContext.InstanceUsers.Add(dbUser);
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
- return StatusCode((int)HttpStatusCode.Created, dbUser.ToApi());
+ return Created(dbUser.ToApi());
}
///
diff --git a/src/Tgstation.Server.Host/Controllers/JobController.cs b/src/Tgstation.Server.Host/Controllers/JobController.cs
index e0d972805f..e6bca24a46 100644
--- a/src/Tgstation.Server.Host/Controllers/JobController.cs
+++ b/src/Tgstation.Server.Host/Controllers/JobController.cs
@@ -33,7 +33,16 @@ namespace Tgstation.Server.Host.Controllers
/// The for the
/// The value of
/// The for the
- public JobController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
+ public JobController(
+ IDatabaseContext databaseContext,
+ IAuthenticationContextFactory authenticationContextFactory,
+ IJobManager jobManager,
+ ILogger logger)
+ : base(
+ databaseContext,
+ authenticationContextFactory,
+ logger,
+ true)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
}
diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs
index 8cd72e95f9..9b14db234b 100644
--- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs
+++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs
@@ -7,7 +7,6 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
-using System.Net;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
@@ -18,6 +17,7 @@ using Tgstation.Server.Host.Components;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Database;
+using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
@@ -61,7 +61,20 @@ namespace Tgstation.Server.Host.Controllers
/// The value of
/// The for the
/// The containing value of
- public RepositoryController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IGitHubClientFactory gitHubClientFactory, IJobManager jobManager, ILogger logger, IOptions generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, true, true)
+ public RepositoryController(
+ IDatabaseContext databaseContext,
+ IAuthenticationContextFactory authenticationContextFactory,
+ IInstanceManager instanceManager,
+ IGitHubClientFactory gitHubClientFactory,
+ IJobManager jobManager,
+ ILogger logger,
+ IOptions generalConfigurationOptions)
+ : base(
+ databaseContext,
+ authenticationContextFactory,
+ logger,
+ true,
+ true)
{
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
@@ -233,7 +246,7 @@ namespace Tgstation.Server.Host.Controllers
api.Reference = model.Reference;
api.ActiveJob = job.ToApi();
- return StatusCode((int)HttpStatusCode.Created, api);
+ return Created(api);
}
///
@@ -317,7 +330,7 @@ namespace Tgstation.Server.Host.Controllers
{
// user may have fucked with the repo manually, do what we can
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
- return StatusCode((int)HttpStatusCode.Created, api);
+ return Created(api);
}
return Json(api);
@@ -659,7 +672,9 @@ namespace Tgstation.Server.Host.Controllers
try
{
// retrieve the latest sha
- var pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number).ConfigureAwait(false);
+ var pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number)
+ .WithToken(ct)
+ .ConfigureAwait(false);
prMap.Add(I.Number, pr);
I.PullRequestRevision = pr.Head.Sha;
}
@@ -762,7 +777,11 @@ namespace Tgstation.Server.Host.Controllers
{
// load from cache if possible
if (prMap == null || !prMap.TryGetValue(I.Number, out pr))
- pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number).ConfigureAwait(false);
+ pr = await gitHubClient
+ .PullRequest
+ .Get(repoOwner, repoName, I.Number)
+ .WithToken(ct)
+ .ConfigureAwait(false);
}
catch (Octokit.RateLimitExceededException ex)
{
diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs
index 7bac3e59e8..dc7a1a2b71 100644
--- a/src/Tgstation.Server.Host/Controllers/UserController.cs
+++ b/src/Tgstation.Server.Host/Controllers/UserController.cs
@@ -5,7 +5,6 @@ using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
@@ -48,7 +47,18 @@ namespace Tgstation.Server.Host.Controllers
/// The value of
/// The for the .
/// The containing the value of
- public UserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, ILogger logger, IOptions generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, false, true)
+ public UserController(
+ IDatabaseContext databaseContext,
+ IAuthenticationContextFactory authenticationContextFactory,
+ ISystemIdentityFactory systemIdentityFactory,
+ ICryptographySuite cryptographySuite,
+ ILogger logger,
+ IOptions generalConfigurationOptions)
+ : base(
+ databaseContext,
+ authenticationContextFactory,
+ logger,
+ false)
{
this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
@@ -160,7 +170,7 @@ namespace Tgstation.Server.Host.Controllers
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
- return StatusCode((int)HttpStatusCode.Created, dbUser.ToApi(true));
+ return Created(dbUser.ToApi(true));
}
///
diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs
index 0b21fb8cc5..f07cdee7fa 100644
--- a/src/Tgstation.Server.Host/Core/Application.cs
+++ b/src/Tgstation.Server.Host/Core/Application.cs
@@ -173,6 +173,8 @@ namespace Tgstation.Server.Host.Core
.AddMvc(options =>
{
options.EnableEndpointRouting = false;
+ options.ReturnHttpNotAcceptable = true;
+ options.RespectBrowserAcceptHeader = true;
})
.AddNewtonsoftJson(options =>
{
diff --git a/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs b/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs
index 218740c51b..7bd6d01449 100644
--- a/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs
+++ b/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs
@@ -7,6 +7,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
+using System.Net.Mime;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Rights;
@@ -34,7 +35,7 @@ namespace Tgstation.Server.Host.Core
var errorMessageContent = new Dictionary
{
{
- ApiHeaders.ApplicationJson,
+ MediaTypeNames.Application.Json,
new OpenApiMediaType
{
Schema = new OpenApiSchema
@@ -90,6 +91,12 @@ namespace Tgstation.Server.Host.Core
Content = errorMessageContent
});
+ AddDefaultResponse(HttpStatusCode.NotAcceptable, new OpenApiResponse
+ {
+ Description = "Invalid Accept header, TGS requires `Accept: application/json`.",
+ Content = errorMessageContent
+ });
+
AddDefaultResponse(HttpStatusCode.InternalServerError, new OpenApiResponse
{
Description = ErrorCode.InternalServerError.Describe(),
diff --git a/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs b/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs
index f155aff190..fe3356d981 100644
--- a/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs
+++ b/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs
@@ -40,6 +40,12 @@ namespace Tgstation.Server.Host.Extensions
}
catch (DbUpdateException e)
{
+ if (e.InnerException is OperationCanceledException)
+ {
+ logger.LogTrace("Rethrowing DbUpdateException as OperationCanceledException: {0}", e);
+ throw e.InnerException;
+ }
+
logger.LogDebug("Database conflict: {0}", e.Message);
await new ConflictObjectResult(new ErrorMessage(ErrorCode.DatabaseIntegrityConflict)
{
diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs
index aa019be91f..45935ac098 100644
--- a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs
+++ b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs
@@ -64,6 +64,7 @@ namespace Tgstation.Server.Host.Security
.ConfigureAwait(false);
if (user == default)
{
+ logger.LogWarning("Unable to find user with ID {0}!", userId);
CurrentAuthenticationContext = new AuthenticationContext();
return;
}
@@ -75,6 +76,7 @@ namespace Tgstation.Server.Host.Security
{
if (user.LastPasswordUpdate.HasValue && user.LastPasswordUpdate > validAfter)
{
+ logger.LogDebug("Rejecting token for user {0} created before last password update: {1}", userId, user.LastPasswordUpdate.Value);
CurrentAuthenticationContext = new AuthenticationContext();
return;
}
diff --git a/src/Tgstation.Server.Host/Security/ClaimsInjector.cs b/src/Tgstation.Server.Host/Security/ClaimsInjector.cs
index 7fd31958de..00c3ce2bed 100644
--- a/src/Tgstation.Server.Host/Security/ClaimsInjector.cs
+++ b/src/Tgstation.Server.Host/Security/ClaimsInjector.cs
@@ -55,7 +55,7 @@ namespace Tgstation.Server.Host.Security
{
apiHeaders = new ApiHeaders(tokenValidatedContext.HttpContext.Request.GetTypedHeaders());
}
- catch (InvalidOperationException)
+ catch (HeadersException)
{
// we are not responsible for handling header validation issues
return;
@@ -78,9 +78,12 @@ namespace Tgstation.Server.Host.Security
// if there's no instance user, do a weird thing and add all the instance roles
// we need it so we can get to OnActionExecutionAsync where we can properly decide between BadRequest and Forbid
// if user is null that means they got the token with an expired password
- var rightInt = authenticationContext.User == null || (RightsHelper.IsInstanceRight(I) && authenticationContext.InstanceUser == null) ? ~0U : authenticationContext.GetRight(I);
+ var rightAsULong = authenticationContext.User == null
+ || (RightsHelper.IsInstanceRight(I) && authenticationContext.InstanceUser == null)
+ ? ~0UL
+ : authenticationContext.GetRight(I);
var rightEnum = RightsHelper.RightToType(I);
- var right = (Enum)Enum.ToObject(rightEnum, rightInt);
+ var right = (Enum)Enum.ToObject(rightEnum, rightAsULong);
foreach (Enum J in Enum.GetValues(rightEnum))
if (right.HasFlag(J))
claims.Add(new Claim(ClaimTypes.Role, RightsHelper.RoleName(I, J)));
diff --git a/src/Tgstation.Server.Host/Server.cs b/src/Tgstation.Server.Host/Server.cs
index 45b824bd39..45b37f2c74 100644
--- a/src/Tgstation.Server.Host/Server.cs
+++ b/src/Tgstation.Server.Host/Server.cs
@@ -21,7 +21,12 @@ namespace Tgstation.Server.Host
public bool RestartRequested { get; private set; }
///
- public bool WatchdogPresent => updatePath != null;
+ public bool WatchdogPresent =>
+#if WATCHDOG_FREE_RESTART
+ true;
+#else
+ updatePath != null;
+#endif
///
/// The for the
diff --git a/tests/Tgstation.Server.Api.Tests/TestApiHeaders.cs b/tests/Tgstation.Server.Api.Tests/TestApiHeaders.cs
index 5420a0fb29..0f78162c50 100644
--- a/tests/Tgstation.Server.Api.Tests/TestApiHeaders.cs
+++ b/tests/Tgstation.Server.Api.Tests/TestApiHeaders.cs
@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Http.Headers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Net.Http.Headers;
+using System.Net.Mime;
namespace Tgstation.Server.Api.Tests
{
@@ -28,11 +29,11 @@ namespace Tgstation.Server.Api.Tests
const string BrowserHeader = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36.";
const string ConformantHeader = "TGSClient/3.2.1.4";
- ApiHeaders TestHeader(string userAgent)
+ static ApiHeaders TestHeader(string userAgent)
{
var headers = new HeaderDictionary
{
- { "Accept", ApiHeaders.ApplicationJson },
+ { "Accept", MediaTypeNames.Application.Json },
{ "Api", "Tgstation.Server.Api/4.0.0.0" },
{ "Authorization", "Bearer asdfasdf" },
{ "User-Agent", userAgent }
@@ -49,7 +50,7 @@ namespace Tgstation.Server.Api.Tests
Assert.AreEqual(ConformantHeader, header.RawUserAgent);
Assert.IsNotNull(header.UserAgent);
- Assert.ThrowsException(() => TestHeader(String.Empty));
+ Assert.ThrowsException(() => TestHeader(String.Empty));
}
}
}
diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs
index 637a16ad9c..c73ed0f635 100644
--- a/tests/Tgstation.Server.Tests/IntegrationTest.cs
+++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs
@@ -14,7 +14,6 @@ using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Client;
-using Tgstation.Server.Host;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.System;
using Tgstation.Server.Tests.Instance;
@@ -36,34 +35,8 @@ namespace Tgstation.Server.Tests
var serverTask = server.Run(cancellationToken);
try
{
- IServerClient adminClient;
-
- var giveUpAt = DateTimeOffset.Now.AddSeconds(60);
- do
- {
- try
- {
- adminClient = await clientFactory.CreateFromLogin(server.Url, User.AdminName, User.DefaultAdminPassword).ConfigureAwait(false);
- break;
- }
- catch (HttpRequestException)
- {
- //migrating, to be expected
- if (DateTimeOffset.Now > giveUpAt)
- throw;
- await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
- }
- catch (ServiceUnavailableException)
- {
- //migrating, to be expected
- if (DateTimeOffset.Now > giveUpAt)
- throw;
- await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
- }
- } while (true);
-
var testUpdateVersion = new Version(4, 3, 0);
- using (adminClient)
+ using (var adminClient = await CreateAdminClient(server.Url, cancellationToken))
//attempt to update to stable
await adminClient.Administration.Update(new Administration
{
@@ -107,6 +80,32 @@ namespace Tgstation.Server.Tests
proc.Kill();
}
+ async Task CreateAdminClient(Uri url, CancellationToken cancellationToken)
+ {
+ var giveUpAt = DateTimeOffset.Now.AddSeconds(60);
+ do
+ {
+ try
+ {
+ return await clientFactory.CreateFromLogin(url, User.AdminName, User.DefaultAdminPassword, attemptLoginRefresh: false).ConfigureAwait(false);
+ }
+ catch (HttpRequestException)
+ {
+ //migrating, to be expected
+ if (DateTimeOffset.Now > giveUpAt)
+ throw;
+ await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
+ }
+ catch (ServiceUnavailableException)
+ {
+ // migrating, to be expected
+ if (DateTimeOffset.Now > giveUpAt)
+ throw;
+ await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
+ }
+ } while (true);
+ }
+
[TestMethod]
public async Task TestServer()
{
@@ -140,36 +139,11 @@ namespace Tgstation.Server.Tests
TerminateAllDDs();
var serverTask = server.Run(cancellationToken);
+
try
{
- async Task CreateAdminClient()
- {
- var giveUpAt = DateTimeOffset.Now.AddSeconds(60);
- do
- {
- try
- {
- return await clientFactory.CreateFromLogin(server.Url, User.AdminName, User.DefaultAdminPassword, attemptLoginRefresh: false).ConfigureAwait(false);
- }
- catch (HttpRequestException)
- {
- //migrating, to be expected
- if (DateTimeOffset.Now > giveUpAt)
- throw;
- await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
- }
- catch (ServiceUnavailableException)
- {
- // migrating, to be expected
- if (DateTimeOffset.Now > giveUpAt)
- throw;
- await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
- }
- } while (true);
- }
-
Api.Models.Instance instance;
- using (var adminClient = await CreateAdminClient())
+ using (var adminClient = await CreateAdminClient(server.Url, cancellationToken))
{
if (server.DumpOpenApiSpecpath)
{
@@ -182,34 +156,34 @@ namespace Tgstation.Server.Tests
await content.CopyToAsync(output);
}
- var serverInfo = await adminClient.Version(default).ConfigureAwait(false);
-
- Assert.AreEqual(ApiHeaders.Version, serverInfo.ApiVersion);
- var assemblyVersion = typeof(IServer).Assembly.GetName().Version.Semver();
- Assert.AreEqual(assemblyVersion, serverInfo.Version);
- Assert.AreEqual(10U, serverInfo.MinimumPasswordLength);
- Assert.AreEqual(11U, serverInfo.InstanceLimit);
- Assert.AreEqual(150U, serverInfo.UserLimit);
-
- //check that modifying the token even slightly fucks up the auth
- var newToken = new Token
+ async Task FailFast(Task task)
{
- ExpiresAt = adminClient.Token.ExpiresAt,
- Bearer = adminClient.Token.Bearer + '0'
- };
+ try
+ {
+ await task;
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[{DateTimeOffset.Now}] TEST ERROR: {ex}");
+ serverCts.Cancel();
+ throw;
+ }
+ }
- var badClient = clientFactory.CreateFromToken(server.Url, newToken);
- await Assert.ThrowsExceptionAsync(() => badClient.Version(cancellationToken)).ConfigureAwait(false);
-
- var adminTest = new AdministrationTest(adminClient.Administration).Run(cancellationToken);
- var usersTest = new UsersTest(adminClient.Users).Run(cancellationToken);
- instance = await new InstanceManagerTest(adminClient.Instances, adminClient.Users, server.Directory).RunPreInstanceTest(cancellationToken).ConfigureAwait(false);
+ var rootTest = FailFast(new RootTest().Run(clientFactory, adminClient, cancellationToken));
+ var adminTest = FailFast(new AdministrationTest(adminClient.Administration).Run(cancellationToken));
+ var usersTest = FailFast(new UsersTest(adminClient.Users).Run(cancellationToken));
+ instance = await new InstanceManagerTest(adminClient.Instances, adminClient.Users, server.Directory).RunPreInstanceTest(cancellationToken);
var instanceClient = adminClient.Instances.CreateClient(instance);
- var instanceTests = new InstanceTest(instanceClient, adminClient.Instances).RunTests(cancellationToken);
+ var instanceTests = FailFast(new InstanceTest(instanceClient, adminClient.Instances).RunTests(cancellationToken));
- await Task.WhenAll(adminTest, instanceTests, usersTest);
+ await Task.WhenAll(rootTest, adminTest, instanceTests, usersTest);
await adminClient.Administration.Restart(cancellationToken);
}
@@ -220,7 +194,7 @@ namespace Tgstation.Server.Tests
var preStartupTime = DateTimeOffset.Now;
serverTask = server.Run(cancellationToken);
- using (var adminClient = await CreateAdminClient())
+ using (var adminClient = await CreateAdminClient(server.Url, cancellationToken))
{
var instanceClient = adminClient.Instances.CreateClient(instance);
@@ -263,7 +237,7 @@ namespace Tgstation.Server.Tests
preStartupTime = DateTimeOffset.Now;
serverTask = server.Run(cancellationToken);
- using (var adminClient = await CreateAdminClient())
+ using (var adminClient = await CreateAdminClient(server.Url, cancellationToken))
{
var instanceClient = adminClient.Instances.CreateClient(instance);
diff --git a/tests/Tgstation.Server.Tests/RootTest.cs b/tests/Tgstation.Server.Tests/RootTest.cs
new file mode 100644
index 0000000000..b94e2c3dc7
--- /dev/null
+++ b/tests/Tgstation.Server.Tests/RootTest.cs
@@ -0,0 +1,151 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Newtonsoft.Json;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Net.Mime;
+using System.Text;
+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
+{
+ class RootTest
+ {
+ async Task TestRequestValidation(IServerClient serverClient, CancellationToken cancellationToken)
+ {
+ var url = serverClient.Url;
+ 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()))
+ {
+ request.Headers.Accept.Clear();
+ request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0"));
+ using var response = await httpClient.SendAsync(request, cancellationToken);
+ Assert.AreEqual(HttpStatusCode.NotAcceptable, response.StatusCode);
+ }
+
+ using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString()))
+ {
+ request.Headers.Accept.Clear();
+ request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0"));
+ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Xml));
+ using var response = await httpClient.SendAsync(request, cancellationToken);
+ Assert.AreEqual(HttpStatusCode.NotAcceptable, response.StatusCode);
+ }
+
+ using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString()))
+ {
+ request.Headers.Accept.Clear();
+ request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0"));
+ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
+ using var response = await httpClient.SendAsync(request, cancellationToken);
+ Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode);
+ var content = await response.Content.ReadAsStringAsync();
+ var message = JsonConvert.DeserializeObject(content);
+ Assert.AreEqual(ErrorCode.BadHeaders, message.ErrorCode);
+ }
+
+ using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString()))
+ {
+ request.Headers.Accept.Clear();
+ request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0"));
+ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
+ request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/6.0.0");
+ using var response = await httpClient.SendAsync(request, cancellationToken);
+ Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode);
+ var content = await response.Content.ReadAsStringAsync();
+ var message = JsonConvert.DeserializeObject(content);
+ Assert.AreEqual(ErrorCode.BadHeaders, message.ErrorCode);
+ }
+
+ using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString()))
+ {
+ request.Headers.Accept.Clear();
+ request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0"));
+ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*"));
+ request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/6.0.0");
+ request.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
+ using var response = await httpClient.SendAsync(request, cancellationToken);
+ Assert.AreEqual(HttpStatusCode.UpgradeRequired, response.StatusCode);
+ var content = await response.Content.ReadAsStringAsync();
+ var message = JsonConvert.DeserializeObject(content);
+ Assert.AreEqual(ErrorCode.ApiMismatch, message.ErrorCode);
+ }
+
+ using (var request = new HttpRequestMessage(HttpMethod.Post, url.ToString() + Routes.Administration.Substring(1)))
+ {
+ request.Headers.Accept.Clear();
+ request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0"));
+ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
+ request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/7.0.0");
+ request.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
+ request.Content = new StringContent(
+ "{ newVersion: 1234 }",
+ Encoding.UTF8,
+ MediaTypeNames.Application.Json);
+ using var response = await httpClient.SendAsync(request, cancellationToken);
+ Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode);
+ var content = await response.Content.ReadAsStringAsync();
+ var message = JsonConvert.DeserializeObject(content);
+ Assert.AreEqual(ErrorCode.ModelValidationFailure, message.ErrorCode);
+ }
+
+ using (var request = new HttpRequestMessage(HttpMethod.Post, url.ToString() + Routes.DreamDaemon.Substring(1)))
+ {
+ request.Headers.Accept.Clear();
+ request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0"));
+ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
+ request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/7.0.0");
+ using var response = await httpClient.SendAsync(request, cancellationToken);
+ Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+
+ using (var request = new HttpRequestMessage(HttpMethod.Post, url.ToString() + Routes.DreamDaemon.Substring(1)))
+ {
+ request.Headers.Accept.Clear();
+ request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0"));
+ request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
+ request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/7.0.0");
+ request.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
+ using var response = await httpClient.SendAsync(request, cancellationToken);
+ Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode);
+ var content = await response.Content.ReadAsStringAsync();
+ var message = JsonConvert.DeserializeObject(content);
+ Assert.AreEqual(ErrorCode.InstanceHeaderRequired, message.ErrorCode);
+ }
+ }
+
+ async Task TestServerInformation(IServerClientFactory clientFactory, IServerClient serverClient, CancellationToken cancellationToken)
+ {
+ var serverInfo = await serverClient.Version(default).ConfigureAwait(false);
+
+ Assert.AreEqual(ApiHeaders.Version, serverInfo.ApiVersion);
+ var assemblyVersion = typeof(IServer).Assembly.GetName().Version.Semver();
+ Assert.AreEqual(assemblyVersion, serverInfo.Version);
+ Assert.AreEqual(10U, serverInfo.MinimumPasswordLength);
+ Assert.AreEqual(11U, serverInfo.InstanceLimit);
+ Assert.AreEqual(150U, serverInfo.UserLimit);
+
+ //check that modifying the token even slightly fucks up the auth
+ var newToken = new Token
+ {
+ ExpiresAt = serverClient.Token.ExpiresAt,
+ Bearer = serverClient.Token.Bearer + '0'
+ };
+
+ var badClient = clientFactory.CreateFromToken(serverClient.Url, newToken);
+ await Assert.ThrowsExceptionAsync(() => badClient.Version(cancellationToken)).ConfigureAwait(false);
+ }
+
+ public Task Run(IServerClientFactory clientFactory, IServerClient serverClient, CancellationToken cancellationToken)
+ => Task.WhenAll(
+ TestRequestValidation(serverClient, cancellationToken),
+ TestServerInformation(clientFactory, serverClient, cancellationToken));
+ }
+}