mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-27 23:17:20 +01:00
Merge branch 'dev' into 934-ActuallyImplement
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user