Skip to content

Commit

Permalink
Merge pull request #1685 from tgstation/SignalR [APIDeploy][NugetDepl…
Browse files Browse the repository at this point in the history
…oy][DMDeploy]

Add SignalR/SSE endpoint for job updates and `JobCode`s
  • Loading branch information
Cyberboss authored Nov 5, 2023
2 parents ddda3c8 + 32d1f0a commit 3b97530
Show file tree
Hide file tree
Showing 141 changed files with 7,781 additions and 986 deletions.
4 changes: 2 additions & 2 deletions build/TestCommon.props
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
<!-- Usage: Hard to say what exactly this is for, but not including it removes the test icon and breaks vstest.console.exe for some reason -->
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<!-- Usage: Dependency mocking for tests -->
<!-- Pinned: Moq is OVER https://github.com/moq/moq/issues/1372 -->
<PackageReference Include="Moq" Version="4.20.2" />
<!-- Pinned: Be VERY careful about updating https://github.com/moq/moq/issues/1372 -->
<PackageReference Include="Moq" Version="4.20.69" />
<!-- Usage: MSTest execution -->
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
<!-- Usage: MSTest asserts etc... -->
Expand Down
10 changes: 5 additions & 5 deletions build/Version.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
<!-- Integration tests will ensure they match across the board -->
<Import Project="ControlPanelVersion.props" />
<PropertyGroup>
<TgsCoreVersion>5.16.4</TgsCoreVersion>
<TgsCoreVersion>5.17.0</TgsCoreVersion>
<TgsConfigVersion>4.7.1</TgsConfigVersion>
<TgsApiVersion>9.12.0</TgsApiVersion>
<TgsApiVersion>9.13.0</TgsApiVersion>
<TgsCommonLibraryVersion>7.0.0</TgsCommonLibraryVersion>
<TgsApiLibraryVersion>11.1.2</TgsApiLibraryVersion>
<TgsClientVersion>13.0.0</TgsClientVersion>
<TgsDmapiVersion>6.6.1</TgsDmapiVersion>
<TgsApiLibraryVersion>12.0.0</TgsApiLibraryVersion>
<TgsClientVersion>14.0.0</TgsClientVersion>
<TgsDmapiVersion>6.6.2</TgsDmapiVersion>
<TgsInteropVersion>5.6.2</TgsInteropVersion>
<TgsHostWatchdogVersion>1.4.0</TgsHostWatchdogVersion>
<TgsContainerScriptVersion>1.2.1</TgsContainerScriptVersion>
Expand Down
8 changes: 4 additions & 4 deletions build/analyzers.ruleset
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="myrules" Description="My rule set" ToolsVersion="17.0">
<Rules AnalyzerId="AsyncUsageAnalyzers" RuleNamespace="AsyncUsageAnalyzers">
<Rule Id="UseConfigureAwait" Action="Warning" />
Expand Down Expand Up @@ -88,7 +88,7 @@
<Rule Id="CA1414" Action="Warning" />
<Rule Id="CA1415" Action="Warning" />
<Rule Id="CA1500" Action="Warning" />
<Rule Id="CA1501" Action="Warning" />
<Rule Id="CA1501" Action="None" />
<Rule Id="CA1502" Action="Warning" />
<Rule Id="CA1504" Action="Warning" />
<Rule Id="CA1505" Action="Warning" />
Expand Down Expand Up @@ -971,7 +971,7 @@
<Rule Id="CA1033" Action="Warning" />
<Rule Id="CA1050" Action="Warning" />
<Rule Id="CA1060" Action="Warning" />
<Rule Id="CA1501" Action="Warning" />
<Rule Id="CA1501" Action="None" />
<Rule Id="CA1502" Action="Warning" />
<Rule Id="CA1505" Action="Warning" />
<Rule Id="CA1506" Action="Warning" />
Expand Down Expand Up @@ -1043,4 +1043,4 @@
<Rules AnalyzerId="Text.CSharp.Analyzers" RuleNamespace="Text.CSharp.Analyzers">
<Rule Id="CA1704" Action="Warning" />
</Rules>
</RuleSet>
</RuleSet>
2 changes: 1 addition & 1 deletion src/DMAPI/tgs.dm
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// tgstation-server DMAPI

#define TGS_DMAPI_VERSION "6.6.1"
#define TGS_DMAPI_VERSION "6.6.2"

// All functions and datums outside this document are subject to change with any version and should not be relied on.

Expand Down
9 changes: 7 additions & 2 deletions src/DMAPI/tgs/core/datum.dm
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@ TGS_DEFINE_AND_SET_GLOBAL(tgs, null)
src.version = version

/datum/tgs_api/proc/TerminateWorld()
del(world)
sleep(1) // https://www.byond.com/forum/post/2894866
while(TRUE)
TGS_DEBUG_LOG("About to terminate world. Tick: [world.time], sleep_offline: [world.sleep_offline]")
world.sleep_offline = FALSE // https://www.byond.com/forum/post/2894866
del(world)
world.sleep_offline = FALSE // just in case, this is BYOND after all...
sleep(1)
TGS_DEBUG_LOG("BYOND DIDN'T TERMINATE THE WORLD!!! TICK IS: [world.time], sleep_offline: [world.sleep_offline]")

/datum/tgs_api/latest
parent_type = /datum/tgs_api/v5
Expand Down
141 changes: 109 additions & 32 deletions src/Tgstation.Server.Api/ApiHeaders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.Net.Http.Headers;

using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Api.Properties;
using Tgstation.Server.Common.Extensions;

Expand Down Expand Up @@ -57,6 +58,11 @@ public sealed class ApiHeaders
/// </summary>
public const string ApplicationJsonMime = "application/json";

/// <summary>
/// Added to <see cref="MediaTypeNames.Application"/> in netstandard2.1. Can't use because of lack of .NET Framework support.
/// </summary>
const string TextEventStreamMime = "text/event-stream";

/// <summary>
/// Get the version of the <see cref="Api"/> the caller is using.
/// </summary>
Expand Down Expand Up @@ -93,9 +99,9 @@ public sealed class ApiHeaders
public Version ApiVersion { get; }

/// <summary>
/// The client's JWT.
/// The client's <see cref="TokenResponse"/>.
/// </summary>
public string? Token { get; }
public TokenResponse? Token { get; }

/// <summary>
/// The client's username.
Expand All @@ -107,13 +113,18 @@ public sealed class ApiHeaders
/// </summary>
public string? Password { get; }

/// <summary>
/// The OAuth code in use.
/// </summary>
public string? OAuthCode { get; }

/// <summary>
/// The <see cref="Models.OAuthProvider"/> the <see cref="Token"/> is for, if any.
/// </summary>
public OAuthProvider? OAuthProvider { get; }

/// <summary>
/// If the header uses password or TGS JWT authentication.
/// If the header uses OAuth or TGS JWT authentication.
/// </summary>
public bool IsTokenAuthentication => Token != null && !OAuthProvider.HasValue;

Expand All @@ -129,16 +140,31 @@ public sealed class ApiHeaders
/// </summary>
/// <param name="userAgent">The value of <see cref="UserAgent"/>.</param>
/// <param name="token">The value of <see cref="Token"/>.</param>
/// <param name="oauthProvider">The value of <see cref="OAuthProvider"/>.</param>
public ApiHeaders(ProductHeaderValue userAgent, string token, OAuthProvider? oauthProvider = null)
public ApiHeaders(ProductHeaderValue userAgent, TokenResponse token)
: this(userAgent, token, null, null)
{
if (userAgent == null)
throw new ArgumentNullException(nameof(userAgent));
if (token == null)
throw new ArgumentNullException(nameof(token));
if (token.Bearer == null)
throw new InvalidOperationException("token.Bearer must be set!");
}

/// <summary>
/// Initializes a new instance of the <see cref="ApiHeaders"/> class. Used for token authentication.
/// </summary>
/// <param name="userAgent">The value of <see cref="UserAgent"/>.</param>
/// <param name="oAuthCode">The value of <see cref="OAuthCode"/>.</param>
/// <param name="oAuthProvider">The value of <see cref="OAuthProvider"/>.</param>
public ApiHeaders(ProductHeaderValue userAgent, string oAuthCode, OAuthProvider oAuthProvider)
: this(userAgent, null, null, null)
{
if (userAgent == null)
throw new ArgumentNullException(nameof(userAgent));

OAuthProvider = oauthProvider;
OAuthCode = oAuthCode ?? throw new ArgumentNullException(nameof(oAuthCode));
OAuthProvider = oAuthProvider;
}

/// <summary>
Expand All @@ -163,62 +189,71 @@ public ApiHeaders(ProductHeaderValue userAgent, string username, string password
/// </summary>
/// <param name="requestHeaders">The <see cref="RequestHeaders"/> containing the serialized <see cref="ApiHeaders"/>.</param>
/// <param name="ignoreMissingAuth">If a missing <see cref="HeaderNames.Authorization"/> should be ignored.</param>
/// <param name="allowEventStreamAccept">If <see cref="TextEventStreamMime"/> is a valid accept.</param>
/// <exception cref="HeadersException">Thrown if the <paramref name="requestHeaders"/> constitue invalid <see cref="ApiHeaders"/>.</exception>
#pragma warning disable CA1502 // TODO: Decomplexify
public ApiHeaders(RequestHeaders requestHeaders, bool ignoreMissingAuth = false)
public ApiHeaders(RequestHeaders requestHeaders, bool ignoreMissingAuth, bool allowEventStreamAccept)
{
if (requestHeaders == null)
throw new ArgumentNullException(nameof(requestHeaders));

var badHeaders = HeaderTypes.None;
var badHeaders = HeaderErrorTypes.None;
var errorBuilder = new StringBuilder();

void AddError(HeaderTypes headerType, string message)
var multipleErrors = false;
void AddError(HeaderErrorTypes headerType, string message)
{
if (badHeaders != HeaderTypes.None)
if (badHeaders != HeaderErrorTypes.None)
{
multipleErrors = true;
errorBuilder.AppendLine();
}

badHeaders |= headerType;
errorBuilder.Append(message);
}

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}!");
var eventStreamAccept = new Microsoft.Net.Http.Headers.MediaTypeHeaderValue(TextEventStreamMime);
if (!requestHeaders.Accept.Any(jsonAccept.IsSubsetOf))
if (!allowEventStreamAccept)
AddError(HeaderErrorTypes.Accept, $"Client does not accept {ApplicationJsonMime}!");
else if (!requestHeaders.Accept.Any(eventStreamAccept.IsSubsetOf))
AddError(HeaderErrorTypes.Accept, $"Client does not accept {ApplicationJsonMime} or {TextEventStreamMime}!");

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
{
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!");
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))
Expand All @@ -237,14 +272,30 @@ void AddError(HeaderTypes headerType, string message)
if (Enum.TryParse<OAuthProvider>(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!");

goto case BearerAuthenticationScheme;
OAuthCode = parameter;
break;
case BearerAuthenticationScheme:
Token = parameter;
Token = new TokenResponse
{
Bearer = parameter,
};

try
{
#pragma warning disable CS0618 // Type or member is obsolete
Token.ExpiresAt = Token.ParseJwt().ValidTo;
#pragma warning restore CS0618 // Type or member is obsolete
}
catch (ArgumentException ex) when (ex is not ArgumentNullException)
{
AddError(HeaderErrorTypes.AuthorizationInvalid, $"Invalid JWT: {ex.Message}");
}

break;
case BasicAuthenticationScheme:
string badBasicAuthHeaderMessage = $"Invalid basic {HeaderNames.Authorization} header!";
Expand All @@ -256,30 +307,35 @@ void AddError(HeaderTypes headerType, string message)
}
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;
}

Username = basicAuthSplits.First();
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}");

throw new HeadersException(badHeaders, errorBuilder.ToString());
}

ApiVersion = apiVersion!.Semver();
}
Expand All @@ -292,7 +348,7 @@ void AddError(HeaderTypes headerType, string message)
/// <param name="token">The value of <see cref="Token"/>.</param>
/// <param name="username">The value of <see cref="Username"/>.</param>
/// <param name="password">The value of <see cref="Password"/>.</param>
ApiHeaders(ProductHeaderValue userAgent, string? token, string? username, string? password)
ApiHeaders(ProductHeaderValue userAgent, TokenResponse? token, string? username, string? password)
{
RawUserAgent = userAgent?.ToString();
Token = token;
Expand Down Expand Up @@ -322,22 +378,43 @@ public void SetRequestHeaders(HttpRequestHeaders headers, long? instanceId = nul
headers.Clear();
headers.Accept.Add(new MediaTypeWithQualityHeaderValue(ApplicationJsonMime));
headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent));
headers.Add(ApiVersionHeader, new ProductHeaderValue(AssemblyName.Name, ApiVersion.ToString()).ToString());
headers.Add(ApiVersionHeader, CreateApiVersionHeader());
if (OAuthProvider.HasValue)
{
headers.Authorization = new AuthenticationHeaderValue(OAuthAuthenticationScheme, Token);
headers.Authorization = new AuthenticationHeaderValue(OAuthAuthenticationScheme, OAuthCode!);
headers.Add(OAuthProviderHeader, OAuthProvider.ToString());
}
else if (!IsTokenAuthentication)
headers.Authorization = new AuthenticationHeaderValue(
BasicAuthenticationScheme,
Convert.ToBase64String(Encoding.UTF8.GetBytes($"{Username}:{Password}")));
else
headers.Authorization = new AuthenticationHeaderValue(BearerAuthenticationScheme, Token);
headers.Authorization = new AuthenticationHeaderValue(BearerAuthenticationScheme, Token!.Bearer);

instanceId ??= InstanceId;
if (instanceId.HasValue)
headers.Add(InstanceIdHeader, instanceId.Value.ToString(CultureInfo.InvariantCulture));
}

/// <summary>
/// Adds the <paramref name="headers"/> necessary for a SignalR hub connection.
/// </summary>
/// <param name="headers">The headers <see cref="IDictionary{TKey, TValue}"/> to write to.</param>
public void SetHubConnectionHeaders(IDictionary<string, string> headers)
{
if (headers == null)
throw new ArgumentNullException(nameof(headers));

headers.Add(HeaderNames.UserAgent, RawUserAgent ?? throw new InvalidOperationException("Missing UserAgent!"));
headers.Add(HeaderNames.Accept, ApplicationJsonMime);
headers.Add(ApiVersionHeader, CreateApiVersionHeader());
}

/// <summary>
/// Create the <see cref="string"/>ified for of the <see cref="ApiVersionHeader"/>.
/// </summary>
/// <returns>A <see cref="string"/> representing the <see cref="ApiVersion"/>.</returns>
string CreateApiVersionHeader()
=> new ProductHeaderValue(AssemblyName.Name, ApiVersion.ToString()).ToString();
}
}
Loading

0 comments on commit 3b97530

Please sign in to comment.