diff --git a/src/Tgstation.Server.Api/ApiHeaders.cs b/src/Tgstation.Server.Api/ApiHeaders.cs
index c912e58265..b23d890abe 100644
--- a/src/Tgstation.Server.Api/ApiHeaders.cs
+++ b/src/Tgstation.Server.Api/ApiHeaders.cs
@@ -190,17 +190,17 @@ namespace Tgstation.Server.Api
/// If a missing should be ignored.
/// Thrown if the constitue invalid .
#pragma warning disable CA1502 // TODO: Decomplexify
- public ApiHeaders(RequestHeaders requestHeaders, bool ignoreMissingAuth = false)
+ public ApiHeaders(RequestHeaders requestHeaders, bool ignoreMissingAuth)
{
if (requestHeaders == null)
throw new ArgumentNullException(nameof(requestHeaders));
- var badHeaders = HeaderTypes.None;
+ var badHeaders = HeaderErrorTypes.None;
var errorBuilder = new StringBuilder();
var multipleErrors = false;
- void AddError(HeaderTypes headerType, string message)
+ void AddError(HeaderErrorTypes headerType, string message)
{
- if (badHeaders != HeaderTypes.None)
+ if (badHeaders != HeaderErrorTypes.None)
{
multipleErrors = true;
errorBuilder.AppendLine();
@@ -212,28 +212,28 @@ namespace Tgstation.Server.Api
var jsonAccept = new Microsoft.Net.Http.Headers.MediaTypeHeaderValue(ApplicationJsonMime);
if (!requestHeaders.Accept.Any(x => jsonAccept.IsSubsetOf(x)))
- AddError(HeaderTypes.Accept, $"Client does not accept {ApplicationJsonMime}!");
+ AddError(HeaderErrorTypes.Accept, $"Client does not accept {ApplicationJsonMime}!");
if (!requestHeaders.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgentValues) || userAgentValues.Count == 0)
- AddError(HeaderTypes.UserAgent, $"Missing {HeaderNames.UserAgent} header!");
+ AddError(HeaderErrorTypes.UserAgent, $"Missing {HeaderNames.UserAgent} header!");
else
{
RawUserAgent = userAgentValues.First();
if (String.IsNullOrWhiteSpace(RawUserAgent))
- AddError(HeaderTypes.UserAgent, $"Malformed {HeaderNames.UserAgent} header!");
+ AddError(HeaderErrorTypes.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)
- AddError(HeaderTypes.Api, $"Missing {ApiVersionHeader} header!");
+ AddError(HeaderErrorTypes.Api, $"Missing {ApiVersionHeader} header!");
else if (!Version.TryParse(apiUserAgent.Product.Version, out apiVersion))
- AddError(HeaderTypes.Api, $"Malformed {ApiVersionHeader} header!");
+ AddError(HeaderErrorTypes.Api, $"Malformed {ApiVersionHeader} header!");
if (!requestHeaders.Headers.TryGetValue(HeaderNames.Authorization, out StringValues authorization))
{
if (!ignoreMissingAuth)
- AddError(HeaderTypes.Authorization, $"Missing {HeaderNames.Authorization} header!");
+ AddError(HeaderErrorTypes.AuthorizationMissing, $"Missing {HeaderNames.Authorization} header!");
}
else
{
@@ -241,13 +241,13 @@ namespace Tgstation.Server.Api
var splits = new List(auth.Split(' '));
var scheme = splits.First();
if (String.IsNullOrWhiteSpace(scheme))
- AddError(HeaderTypes.Authorization, "Missing authentication scheme!");
+ AddError(HeaderErrorTypes.AuthorizationInvalid, "Missing authentication scheme!");
else
{
splits.RemoveAt(0);
var parameter = String.Concat(splits);
if (String.IsNullOrEmpty(parameter))
- AddError(HeaderTypes.Authorization, "Missing authentication parameter!");
+ AddError(HeaderErrorTypes.AuthorizationInvalid, "Missing authentication parameter!");
else
{
if (requestHeaders.Headers.TryGetValue(InstanceIdHeader, out var instanceIdValues))
@@ -266,10 +266,10 @@ namespace Tgstation.Server.Api
if (Enum.TryParse(oauthProviderString, out var oauthProvider))
OAuthProvider = oauthProvider;
else
- AddError(HeaderTypes.OAuthProvider, "Invalid OAuth provider!");
+ AddError(HeaderErrorTypes.OAuthProvider, "Invalid OAuth provider!");
}
else
- AddError(HeaderTypes.OAuthProvider, $"Missing {OAuthProviderHeader} header!");
+ AddError(HeaderErrorTypes.OAuthProvider, $"Missing {OAuthProviderHeader} header!");
OAuthCode = parameter;
break;
@@ -277,7 +277,7 @@ namespace Tgstation.Server.Api
var tokenSplits = parameter.Split('.');
DateTimeOffset? expiresAt = null;
if (tokenSplits.Length != 3)
- AddError(HeaderTypes.Authorization, "Invalid JWT!");
+ AddError(HeaderErrorTypes.AuthorizationInvalid, "Invalid JWT!");
else
try
{
@@ -290,13 +290,13 @@ namespace Tgstation.Server.Api
if (Int64.TryParse(nbf, out var unixTimeSeconds))
expiresAt = DateTimeOffset.FromUnixTimeSeconds(unixTimeSeconds);
else
- AddError(HeaderTypes.Authorization, "'nbf' in JWT could not be parsed!");
+ AddError(HeaderErrorTypes.AuthorizationInvalid, "'nbf' in JWT could not be parsed!");
else
- AddError(HeaderTypes.Authorization, "Missing 'nbf' in JWT payload!");
+ AddError(HeaderErrorTypes.AuthorizationInvalid, "Missing 'nbf' in JWT payload!");
}
catch
{
- AddError(HeaderTypes.Authorization, "Invalid JWT payload!");
+ AddError(HeaderErrorTypes.AuthorizationInvalid, "Invalid JWT payload!");
}
Token = new TokenResponse
@@ -315,14 +315,14 @@ namespace Tgstation.Server.Api
}
catch
{
- AddError(HeaderTypes.Authorization, badBasicAuthHeaderMessage);
+ AddError(HeaderErrorTypes.AuthorizationInvalid, badBasicAuthHeaderMessage);
break;
}
var basicAuthSplits = joinedString.Split(ColonSeparator, StringSplitOptions.RemoveEmptyEntries);
if (basicAuthSplits.Length < 2)
{
- AddError(HeaderTypes.Authorization, badBasicAuthHeaderMessage);
+ AddError(HeaderErrorTypes.AuthorizationInvalid, badBasicAuthHeaderMessage);
break;
}
@@ -330,14 +330,14 @@ namespace Tgstation.Server.Api
Password = String.Concat(basicAuthSplits.Skip(1));
break;
default:
- AddError(HeaderTypes.Authorization, "Invalid authentication scheme!");
+ AddError(HeaderErrorTypes.AuthorizationInvalid, "Invalid authentication scheme!");
break;
}
}
}
}
- if (badHeaders != HeaderTypes.None)
+ if (badHeaders != HeaderErrorTypes.None)
{
if (multipleErrors)
errorBuilder.Insert(0, $"Multiple header validation errors occurred:{Environment.NewLine}");
diff --git a/src/Tgstation.Server.Api/HeaderErrorTypes.cs b/src/Tgstation.Server.Api/HeaderErrorTypes.cs
new file mode 100644
index 0000000000..414b1e22d3
--- /dev/null
+++ b/src/Tgstation.Server.Api/HeaderErrorTypes.cs
@@ -0,0 +1,46 @@
+using System;
+
+namespace Tgstation.Server.Api
+{
+ ///
+ /// Types of individual errors.
+ ///
+ [Flags]
+ public enum HeaderErrorTypes
+ {
+ ///
+ /// No header errors.
+ ///
+ None = 0,
+
+ ///
+ /// The header is missing or invalid.
+ ///
+ UserAgent = 1 << 0,
+
+ ///
+ /// The header is missing or invalid.
+ ///
+ Accept = 1 << 1,
+
+ ///
+ /// The header is missing or invalid.
+ ///
+ Api = 1 << 2,
+
+ ///
+ /// The header is invalid.
+ ///
+ AuthorizationInvalid = 1 << 3,
+
+ ///
+ /// The header is missing or invalid.
+ ///
+ OAuthProvider = 1 << 4,
+
+ ///
+ /// The header is missing.
+ ///
+ AuthorizationMissing = 1 << 5,
+ }
+}
diff --git a/src/Tgstation.Server.Api/HeaderTypes.cs b/src/Tgstation.Server.Api/HeaderTypes.cs
deleted file mode 100644
index a5fdca325d..0000000000
--- a/src/Tgstation.Server.Api/HeaderTypes.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using System;
-
-namespace Tgstation.Server.Api
-{
- ///
- /// Types of individual .
- ///
- [Flags]
- public enum HeaderTypes
- {
- ///
- /// No headers.
- ///
- None = 0,
-
- ///
- /// header.
- ///
- UserAgent = 1 << 0,
-
- ///
- /// header.
- ///
- Accept = 1 << 1,
-
- ///
- /// .
- ///
- Api = 1 << 2,
-
- ///
- ///
- ///
- Authorization = 1 << 3,
-
- ///
- /// .
- ///
- OAuthProvider = 1 << 4,
- }
-}
diff --git a/src/Tgstation.Server.Api/HeadersException.cs b/src/Tgstation.Server.Api/HeadersException.cs
index 6287ee1c88..352d2ce9d8 100644
--- a/src/Tgstation.Server.Api/HeadersException.cs
+++ b/src/Tgstation.Server.Api/HeadersException.cs
@@ -8,19 +8,19 @@ namespace Tgstation.Server.Api
public sealed class HeadersException : Exception
{
///
- /// The s that are missing or malformed.
+ /// The s that are missing or malformed.
///
- public HeaderTypes MissingOrMalformedHeaders { get; }
+ public HeaderErrorTypes ParseErrors { get; }
///
/// Initializes a new instance of the class.
///
- /// The value of .
+ /// The value of .
/// The error message.
- public HeadersException(HeaderTypes missingOrMalformedHeaders, string message)
+ public HeadersException(HeaderErrorTypes parseErrors, string message)
: base(message)
{
- MissingOrMalformedHeaders = missingOrMalformedHeaders;
+ ParseErrors = parseErrors;
}
///
diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs
index 9fd4b6154a..d0b7213427 100644
--- a/src/Tgstation.Server.Host/Controllers/ApiController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs
@@ -48,7 +48,12 @@ namespace Tgstation.Server.Host.Controllers
///
/// The for the operation.
///
- protected ApiHeaders ApiHeaders { get; }
+ protected ApiHeaders ApiHeaders => ApiHeadersProvider.ApiHeaders;
+
+ ///
+ /// The containing value of .
+ ///
+ protected IApiHeadersProvider ApiHeadersProvider { get; }
///
/// The for the operation.
@@ -81,7 +86,7 @@ namespace Tgstation.Server.Host.Controllers
/// The value of .
/// The for the .
/// The value of .
- /// The containing value of .
+ /// The value of ..
/// The value of .
protected ApiController(
IDatabaseContext databaseContext,
@@ -92,11 +97,10 @@ namespace Tgstation.Server.Host.Controllers
{
DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
AuthenticationContext = authenticationContext ?? throw new ArgumentNullException(nameof(authenticationContext));
- ArgumentNullException.ThrowIfNull(apiHeadersProvider);
+ ApiHeadersProvider = apiHeadersProvider ?? throw new ArgumentNullException(nameof(apiHeadersProvider));
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
Instance = AuthenticationContext?.InstancePermissionSet?.Instance;
- ApiHeaders = apiHeadersProvider.ApiHeaders;
this.requireHeaders = requireHeaders;
}
@@ -110,7 +114,7 @@ namespace Tgstation.Server.Host.Controllers
if (ApiHeaders == null)
{
if (requireHeaders)
- return HeadersIssue(false);
+ return HeadersIssue(ApiHeadersProvider.HeadersException);
}
else if (!ApiHeaders.Compatible())
return this.StatusCode(
@@ -239,28 +243,19 @@ namespace Tgstation.Server.Host.Controllers
///
/// Response for missing/Invalid headers.
///
- /// Whether or not errors due to missing should be thrown.
+ /// The that occurred while trying to parse the .
/// The appropriate .
- protected IActionResult HeadersIssue(bool ignoreMissingAuth)
+ protected IActionResult HeadersIssue(HeadersException headersException)
{
- HeadersException headersException;
- try
- {
- // TODO: Move this somewhere saner?
- _ = new ApiHeaders(Request.GetTypedHeaders(), ignoreMissingAuth);
+ if (headersException == null)
throw new InvalidOperationException("Expected a header parse exception!");
- }
- catch (HeadersException ex)
- {
- headersException = ex;
- }
var errorMessage = new ErrorMessageResponse(ErrorCode.BadHeaders)
{
AdditionalData = headersException.Message,
};
- if (headersException.MissingOrMalformedHeaders.HasFlag(HeaderTypes.Accept))
+ if (headersException.ParseErrors.HasFlag(HeaderErrorTypes.Accept))
return this.StatusCode(HttpStatusCode.NotAcceptable, errorMessage);
return BadRequest(errorMessage);
diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs
index 76598ef4f1..290049ca52 100644
--- a/src/Tgstation.Server.Host/Controllers/HomeController.cs
+++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs
@@ -180,15 +180,15 @@ namespace Tgstation.Server.Host.Controllers
try
{
// we only allow authorization header issues
- var headers = new ApiHeaders(Request.GetTypedHeaders(), true);
+ var headers = ApiHeadersProvider.CreateAuthlessHeaders();
if (!headers.Compatible())
return this.StatusCode(
HttpStatusCode.UpgradeRequired,
new ErrorMessageResponse(ErrorCode.ApiMismatch));
}
- catch (HeadersException)
+ catch (HeadersException ex)
{
- return HeadersIssue(true);
+ return HeadersIssue(ex);
}
}
@@ -228,7 +228,7 @@ namespace Tgstation.Server.Host.Controllers
if (ApiHeaders == null)
{
Response.Headers.Add(HeaderNames.WWWAuthenticate, new StringValues($"basic realm=\"Create TGS {ApiHeaders.BearerAuthenticationScheme} token\""));
- return HeadersIssue(false);
+ return HeadersIssue(ApiHeadersProvider.HeadersException);
}
if (ApiHeaders.IsTokenAuthentication)
diff --git a/src/Tgstation.Server.Host/Utils/ApiHeadersProvider.cs b/src/Tgstation.Server.Host/Utils/ApiHeadersProvider.cs
index c59d2d67d0..868a7683c7 100644
--- a/src/Tgstation.Server.Host/Utils/ApiHeadersProvider.cs
+++ b/src/Tgstation.Server.Host/Utils/ApiHeadersProvider.cs
@@ -2,6 +2,7 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
+
using Tgstation.Server.Api;
namespace Tgstation.Server.Host.Utils
@@ -10,29 +11,76 @@ namespace Tgstation.Server.Host.Utils
sealed class ApiHeadersProvider : IApiHeadersProvider
{
///
- public ApiHeaders ApiHeaders { get; }
+ public ApiHeaders ApiHeaders => attemptedApiHeadersCreation
+ ? apiHeaders
+ : CreateApiHeaders(true);
+
+ ///
+ public HeadersException HeadersException { get; private set; }
+
+ ///
+ /// The for the .
+ ///
+ readonly IHttpContextAccessor httpContextAccessor;
+
+ ///
+ /// The for the .
+ ///
+ readonly ILogger logger;
+
+ ///
+ /// Backing field for .
+ ///
+ ApiHeaders apiHeaders;
+
+ ///
+ /// If populating was previously attempted.
+ ///
+ bool attemptedApiHeadersCreation;
///
/// Initializes a new instance of the class.
///
- /// The for accessing the .
- /// The to write to.
+ /// The value of .
+ /// The value of .
public ApiHeadersProvider(IHttpContextAccessor httpContextAccessor, ILogger logger)
{
- ArgumentNullException.ThrowIfNull(httpContextAccessor);
+ this.httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
+ this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+ ///
+ public ApiHeaders CreateAuthlessHeaders() => CreateApiHeaders(false);
+
+ ///
+ /// Attempt to parse from the , optionally populating the properties.
+ ///
+ /// If the error should be ignored and / should be populated.
+ /// A newly parsed or if was set and the parse failed.
+ ApiHeaders CreateApiHeaders(bool includeAuthAndSetProperties)
+ {
if (httpContextAccessor.HttpContext == null)
throw new InvalidOperationException("httpContextAccessor has no HttpContext!");
var request = httpContextAccessor.HttpContext.Request;
+ var ignoreMissingAuth = !includeAuthAndSetProperties;
+
+ if (includeAuthAndSetProperties)
+ attemptedApiHeadersCreation = true;
+
try
{
- ApiHeaders = new ApiHeaders(request.GetTypedHeaders());
+ var headers = new ApiHeaders(request.GetTypedHeaders(), ignoreMissingAuth);
+ if (includeAuthAndSetProperties)
+ apiHeaders = headers;
+
+ return headers;
}
- catch (HeadersException ex)
+ catch (HeadersException ex) when (includeAuthAndSetProperties)
{
- // we are not responsible for handling header validation issues
- logger.LogTrace(ex, "Failed to validated API request headers!");
+ logger.LogTrace(ex, "Failed to parse API headers!");
+ HeadersException = ex;
+ return null;
}
}
}
diff --git a/src/Tgstation.Server.Host/Utils/IApiHeadersProvider.cs b/src/Tgstation.Server.Host/Utils/IApiHeadersProvider.cs
index 31747862be..7de65834d3 100644
--- a/src/Tgstation.Server.Host/Utils/IApiHeadersProvider.cs
+++ b/src/Tgstation.Server.Host/Utils/IApiHeadersProvider.cs
@@ -11,5 +11,18 @@ namespace Tgstation.Server.Host.Utils
/// The created , if any.
///
ApiHeaders ApiHeaders { get; }
+
+ ///
+ /// The thrown when attempting to parse the if any.
+ ///
+ HeadersException HeadersException { get; }
+
+ ///
+ /// Attempt to create without checking for the presence of an header.
+ ///
+ /// A new .
+ /// This does not populate the property.
+ /// Thrown if the requested contain errors other than .
+ ApiHeaders CreateAuthlessHeaders();
}
}
diff --git a/tests/Tgstation.Server.Api.Tests/TestApiHeaders.cs b/tests/Tgstation.Server.Api.Tests/TestApiHeaders.cs
index 5261d8719a..27329a1af4 100644
--- a/tests/Tgstation.Server.Api.Tests/TestApiHeaders.cs
+++ b/tests/Tgstation.Server.Api.Tests/TestApiHeaders.cs
@@ -43,7 +43,7 @@ namespace Tgstation.Server.Api.Tests
{ "User-Agent", userAgent }
};
- return new ApiHeaders(new RequestHeaders(headers));
+ return new ApiHeaders(new RequestHeaders(headers), false);
};
var header = TestHeader(BrowserHeader);