Merge branch 'dev' into 934-ActuallyImplement

This commit is contained in:
Jordan Brown
2020-07-05 12:01:27 -04:00
28 changed files with 666 additions and 260 deletions
+85 -87
View File
@@ -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
/// </summary>
public sealed class ApiHeaders
{
/// <summary>
/// TODO: Remove this when we upgrade to .NET Standard 2.1
/// </summary>
public const string ApplicationJson = "application/json";
/// <summary>
/// The <see cref="ApiVersion"/> header key
/// </summary>
public const string ApiVersionHeader = "api";
public const string ApiVersionHeader = "Api";
/// <summary>
/// The <see cref="InstanceId"/> header key
/// </summary>
public const string InstanceIdHeader = "instance";
public const string InstanceIdHeader = "Instance";
/// <summary>
/// The JWT authentication header scheme
@@ -41,16 +36,6 @@ namespace Tgstation.Server.Api
/// </summary>
public const string BasicAuthenticationScheme = "basic";
/// <summary>
/// The <see cref="Username"/> header key
/// </summary>
const string UsernameHeader = "username";
/// <summary>
/// The basic authentication header scheme
/// </summary>
const string PasswordAuthenticationScheme = "password";
/// <summary>
/// The current <see cref="System.Reflection.AssemblyName"/>
/// </summary>
@@ -141,92 +126,105 @@ namespace Tgstation.Server.Api
/// Construct and validates <see cref="ApiHeaders"/> from a set of <paramref name="requestHeaders"/>
/// </summary>
/// <param name="requestHeaders">The <see cref="RequestHeaders"/> containing the <see cref="ApiHeaders"/></param>
/// <exception cref="HeadersException">Thrown if the <paramref name="requestHeaders"/> constitue invalid <see cref="ApiHeaders"/>.</exception>
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<string>(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<string>(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();
}
/// <summary>
@@ -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
+36
View File
@@ -0,0 +1,36 @@
using System;
namespace Tgstation.Server.Api
{
/// <summary>
/// Types of individual <see cref="ApiHeaders"/>.
/// </summary>
[Flags]
public enum HeaderTypes
{
/// <summary>
/// No headers.
/// </summary>
None = 0,
/// <summary>
/// <see cref="Microsoft.Net.Http.Headers.HeaderNames.UserAgent"/> header.
/// </summary>
UserAgent = 1,
/// <summary>
/// <see cref="Microsoft.Net.Http.Headers.HeaderNames.Accept"/> header.
/// </summary>
Accept = 2,
/// <summary>
/// Api header.
/// </summary>
Api = 4,
/// <summary>
/// <see cref="Microsoft.Net.Http.Headers.HeaderNames.Authorization"/>
/// </summary>
Authorization = 8
}
}
@@ -0,0 +1,49 @@
using System;
namespace Tgstation.Server.Api
{
/// <summary>
/// Thrown when trying to generate <see cref="ApiHeaders"/> from <see cref="Microsoft.AspNetCore.Http.Headers.RequestHeaders"/> fails.
/// </summary>
public sealed class HeadersException : Exception
{
/// <summary>
/// The <see cref="HeaderTypes"/>s that are missing or malformed.
/// </summary>
public HeaderTypes MissingOrMalformedHeaders { get; }
/// <summary>
/// Initializes a new instance of the <see cref="HeadersException"/> <see langword="class"/>.
/// </summary>
/// <param name="missingOrMalformedHeaders">The value of <see cref="MissingOrMalformedHeaders"/>.</param>
/// <param name="message">The error message.</param>
public HeadersException(HeaderTypes missingOrMalformedHeaders, string message) : base(message)
{
MissingOrMalformedHeaders = missingOrMalformedHeaders;
}
/// <summary>
/// Initializes a new instance of the <see cref="HeadersException"/> <see langword="class"/>.
/// </summary>
public HeadersException()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="HeadersException"/> <see langword="class"/>.
/// </summary>
/// <param name="message">The error message.</param>
public HeadersException(string message) : base(message)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="HeadersException"/> <see langword="class"/>.
/// </summary>
/// <param name="message">The error message.</param>
/// <param name="innerException">The inner <see cref="Exception"/> for the base <see cref="Exception"/></param>
public HeadersException(string message, Exception innerException) : base(message, innerException)
{
}
}
}
+5 -1
View File
@@ -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);
@@ -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
/// <param name="repositorySettings">The <see cref="RepositorySettings"/>.</param>
/// <param name="repoOwner">The GitHub repostiory owner.</param>
/// <param name="repoName">The GitHub repostiory name.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
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)
{
@@ -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));
}
/// <summary>
@@ -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
/// <summary>
/// Get <see cref="Administration"/> server information.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
/// <response code="200">Retrieved <see cref="Administration"/> data successfully.</response>
/// <response code="424">The GitHub API rate limit was hit. See response header Retry-After.</response>
@@ -190,7 +194,7 @@ namespace Tgstation.Server.Host.Controllers
[ProducesResponseType(typeof(Administration), 200)]
[ProducesResponseType(typeof(ErrorMessage), 424)]
[ProducesResponseType(typeof(ErrorMessage), 429)]
public async Task<IActionResult> Read()
public async Task<IActionResult> 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);
}
}
@@ -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
/// <summary>
/// A <see cref="Controller"/> for API functions
/// </summary>
[Produces(ApiHeaders.ApplicationJson)]
[Produces(MediaTypeNames.Application.Json)]
[ApiController]
public abstract class ApiController : Controller
{
@@ -64,7 +65,12 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="logger">The value of <see cref="Logger"/></param>
/// <param name="requireInstance">The value of <see cref="requireInstance"/></param>
/// <param name="requireHeaders">The value of <see cref="requireHeaders"/></param>
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.
/// </summary>
/// <returns>An <see cref="ObjectResult"/> with <see cref="HttpStatusCode.Gone"/>.</returns>
protected ObjectResult Gone() => StatusCode((int)HttpStatusCode.Gone, new ErrorMessage(ErrorCode.ResourceNotPresent));
protected ObjectResult Gone() => StatusCode(HttpStatusCode.Gone, new ErrorMessage(ErrorCode.ResourceNotPresent));
/// <summary>
/// Generic 404 response.
/// </summary>
/// <returns>An <see cref="ObjectResult"/> with <see cref="HttpStatusCode.NotFound"/>.</returns>
protected new ObjectResult NotFound() => NotFound(new ErrorMessage(ErrorCode.ResourceNeverPresent));
protected new NotFoundObjectResult NotFound() => NotFound(new ErrorMessage(ErrorCode.ResourceNeverPresent));
/// <summary>
/// Generic 501 response.
/// </summary>
/// <returns>An <see cref="ObjectResult"/> with <see cref="HttpStatusCode.NotImplemented"/>.</returns>
protected ObjectResult RequiresPosixSystemIdentity() => StatusCode((int)HttpStatusCode.NotImplemented, new ErrorMessage(ErrorCode.RequiresPosixSystemIdentity));
protected ObjectResult RequiresPosixSystemIdentity() => StatusCode(HttpStatusCode.NotImplemented, new ErrorMessage(ErrorCode.RequiresPosixSystemIdentity));
/// <summary>
/// Strongly type calls to <see cref="ControllerBase.StatusCode(int)"/>.
/// </summary>
/// <param name="statusCode">The <see cref="HttpStatusCode"/>.</param>
/// <returns>A <see cref="StatusCodeResult"/> with the given <paramref name="statusCode"/>.</returns>
protected StatusCodeResult StatusCode(HttpStatusCode statusCode) => StatusCode((int)statusCode);
/// <summary>
/// Strongly type calls to <see cref="ControllerBase.StatusCode(int, object)"/>.
/// </summary>
/// <param name="statusCode">The <see cref="HttpStatusCode"/>.</param>
/// <param name="errorMessage">The accompanying <see cref="ErrorMessage"/> payload.</param>
/// <returns>A <see cref="StatusCodeResult"/> with the given <paramref name="statusCode"/>.</returns>
protected ObjectResult StatusCode(HttpStatusCode statusCode, object errorMessage) => StatusCode((int)statusCode, errorMessage);
/// <summary>
/// Generic 201 response with a given <paramref name="payload"/>.
/// </summary>
/// <param name="payload">The accompanying API payload.</param>
/// <returns>A <see cref="HttpStatusCode.Created"/> <see cref="ObjectResult"/> with the given <paramref name="payload"/>.</returns>
protected ObjectResult Created(object payload) => StatusCode((int)HttpStatusCode.Created, payload);
/// <summary>
/// Response for missing/Invalid headers.
/// </summary>
/// <returns>The appropriate <see cref="IActionResult"/>.</returns>
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);
}
/// <inheritdoc />
#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;
@@ -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
/// <see cref="Controller"/> for recieving DMAPI requests from DreamDaemon.
/// </summary>
[Route("Bridge")]
[Produces(ApiHeaders.ApplicationJson)]
[Produces(MediaTypeNames.Application.Json)]
public class BridgeController : Controller
{
/// <summary>
@@ -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);
}
}
}
@@ -40,7 +40,17 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="instanceManager">The value of <see cref="instanceManager"/></param>
/// <param name="jobManager">The value of <see cref="jobManager"/></param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/></param>
public ByondController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IJobManager jobManager, ILogger<ByondController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
public ByondController(
IDatabaseContext databaseContext,
IAuthenticationContextFactory authenticationContextFactory,
IInstanceManager instanceManager,
IJobManager jobManager,
ILogger<ByondController> logger)
: base(
databaseContext,
authenticationContextFactory,
logger,
true)
{
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
@@ -39,7 +39,16 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
/// <param name="instanceManager">The value of <see cref="instanceManager"/></param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/></param>
public ChatController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, ILogger<ChatController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
public ChatController(
IDatabaseContext databaseContext,
IAuthenticationContextFactory authenticationContextFactory,
IInstanceManager instanceManager,
ILogger<ChatController> 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());
}
/// <summary>
@@ -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
/// <param name="instanceManager">The value of <see cref="instanceManager"/></param>
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/></param>
public ConfigurationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IIOManager ioManager, ILogger<ConfigurationController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
public ConfigurationController(
IDatabaseContext databaseContext,
IAuthenticationContextFactory authenticationContextFactory,
IInstanceManager instanceManager,
IIOManager ioManager,
ILogger<ConfigurationController> 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)
{
@@ -43,7 +43,17 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="jobManager">The value of <see cref="jobManager"/></param>
/// <param name="instanceManager">The value of <see cref="instanceManager"/></param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/></param>
public DreamDaemonController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger<DreamDaemonController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
public DreamDaemonController(
IDatabaseContext databaseContext,
IAuthenticationContextFactory authenticationContextFactory,
IJobManager jobManager,
IInstanceManager instanceManager,
ILogger<DreamDaemonController> logger)
: base(
databaseContext,
authenticationContextFactory,
logger,
true)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
@@ -42,7 +42,17 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="jobManager">The value of <see cref="jobManager"/></param>
/// <param name="instanceManager">The value of <see cref="instanceManager"/></param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/></param>
public DreamMakerController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger<DreamMakerController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
public DreamMakerController(
IDatabaseContext databaseContext,
IAuthenticationContextFactory authenticationContextFactory,
IJobManager jobManager,
IInstanceManager instanceManager,
ILogger<DreamMakerController> logger)
: base(
databaseContext,
authenticationContextFactory,
logger,
true)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
@@ -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<GeneralConfiguration> generalConfigurationOptions,
IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions,
ILogger<HomeController> 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 <see cref="Application"/>
/// </summary>
/// <returns>
/// The <see cref="Api.Models.ServerInformation"/> of the <see cref="Application"/> if a properly authenticated API request, the web control panel if on a browser and enabled, <see cref="UnauthorizedResult"/> otherwise.
/// The <see cref="ServerInformation"/> of the <see cref="Application"/> if a properly authenticated API request, the web control panel if on a browser and enabled, <see cref="UnauthorizedResult"/> otherwise.
/// </returns>
/// <response code="200"><see cref="Api.Models.ServerInformation"/> retrieved successfully.</response>
/// <response code="200"><see cref="ServerInformation"/> retrieved successfully.</response>
[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();
}
/// <summary>
@@ -147,38 +153,22 @@ namespace Tgstation.Server.Host.Controllers
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation</returns>
/// <response code="200">User logged in and <see cref="Api.Models.Token"/> generated successfully.</response>
/// <response code="200">User logged in and <see cref="Token"/> generated successfully.</response>
/// <response code="401">User authentication failed.</response>
/// <response code="403">User authenticated but is disabled by an administrator.</response>
[HttpPost]
[ProducesResponseType(typeof(Api.Models.Token), 200)]
[ProducesResponseType(typeof(Token), 200)]
#pragma warning disable CA1506 // TODO: Decomplexify
public async Task<IActionResult> 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<User> query = DatabaseContext.Users.AsQueryable();
IQueryable<Models.User> 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
};
@@ -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<GeneralConfiguration> generalConfigurationOptions,
ILogger<InstanceController> 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);
}
/// <summary>
@@ -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
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/></param>
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/></param>
public InstanceUserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger<InstanceUserController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
public InstanceUserController(
IDatabaseContext databaseContext,
IAuthenticationContextFactory authenticationContextFactory,
ILogger<InstanceUserController> logger)
: base(
databaseContext,
authenticationContextFactory,
logger,
true)
{ }
/// <summary>
@@ -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());
}
/// <summary>
@@ -33,7 +33,16 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
/// <param name="jobManager">The value of <see cref="jobManager"/></param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/></param>
public JobController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, ILogger<JobController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
public JobController(
IDatabaseContext databaseContext,
IAuthenticationContextFactory authenticationContextFactory,
IJobManager jobManager,
ILogger<JobController> logger)
: base(
databaseContext,
authenticationContextFactory,
logger,
true)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
}
@@ -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
/// <param name="jobManager">The value of <see cref="jobManager"/></param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/></param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing value of <see cref="generalConfiguration"/></param>
public RepositoryController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IGitHubClientFactory gitHubClientFactory, IJobManager jobManager, ILogger<RepositoryController> logger, IOptions<GeneralConfiguration> generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, true, true)
public RepositoryController(
IDatabaseContext databaseContext,
IAuthenticationContextFactory authenticationContextFactory,
IInstanceManager instanceManager,
IGitHubClientFactory gitHubClientFactory,
IJobManager jobManager,
ILogger<RepositoryController> logger,
IOptions<GeneralConfiguration> 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);
}
/// <summary>
@@ -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)
{
@@ -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
/// <param name="cryptographySuite">The value of <see cref="cryptographySuite"/></param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/></param>
public UserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, ILogger<UserController> logger, IOptions<GeneralConfiguration> generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, false, true)
public UserController(
IDatabaseContext databaseContext,
IAuthenticationContextFactory authenticationContextFactory,
ISystemIdentityFactory systemIdentityFactory,
ICryptographySuite cryptographySuite,
ILogger<UserController> logger,
IOptions<GeneralConfiguration> 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));
}
/// <summary>
@@ -173,6 +173,8 @@ namespace Tgstation.Server.Host.Core
.AddMvc(options =>
{
options.EnableEndpointRouting = false;
options.ReturnHttpNotAcceptable = true;
options.RespectBrowserAcceptHeader = true;
})
.AddNewtonsoftJson(options =>
{
@@ -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<string, OpenApiMediaType>
{
{
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(),
@@ -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)
{
@@ -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;
}
@@ -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)));
+6 -1
View File
@@ -21,7 +21,12 @@ namespace Tgstation.Server.Host
public bool RestartRequested { get; private set; }
/// <inheritdoc />
public bool WatchdogPresent => updatePath != null;
public bool WatchdogPresent =>
#if WATCHDOG_FREE_RESTART
true;
#else
updatePath != null;
#endif
/// <summary>
/// The <see cref="IHostBuilder"/> for the <see cref="Server"/>
@@ -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<InvalidOperationException>(() => TestHeader(String.Empty));
Assert.ThrowsException<HeadersException>(() => TestHeader(String.Empty));
}
}
}
+53 -79
View File
@@ -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<IServerClient> 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<IServerClient> 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<UnauthorizedException>(() => 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);
+151
View File
@@ -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<ErrorMessage>(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<ErrorMessage>(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<ErrorMessage>(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<ErrorMessage>(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<ErrorMessage>(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<UnauthorizedException>(() => badClient.Version(cancellationToken)).ConfigureAwait(false);
}
public Task Run(IServerClientFactory clientFactory, IServerClient serverClient, CancellationToken cancellationToken)
=> Task.WhenAll(
TestRequestValidation(serverClient, cancellationToken),
TestServerInformation(clientFactory, serverClient, cancellationToken));
}
}