Skip to content

Commit

Permalink
(Extremely shitty) engine version redirection.
Browse files Browse the repository at this point in the history
This is necessary to backport engine security fixes to old client versions. The robust engine manifest can now specify a "redirect" for a version, which the launcher will use instead of the engine version advertised by the game server.

I've already gone through and made ourselves backend infra to backport fixes to engine versions.

This change is EXTREMELY SHITTILY hacked together. The problem is basically that engine (module) versions are selected when CONTENT is downloaded (remember that the launcher basically has a 2-stage download, content then engine). I made it do redirect when the engine is subsequently downloaded and then pass up the modified engine version, but...

This introduces two problems:

* The code to cull old engine versions uses that info, which means ugly hacks are needed to make that not immediately delete the new engine version.
* Module versions are not re-resolved, which is probably gonna bite me in the ass sooner or later.

I really need to rewrite the launcher structure here, so that there's one table for "the declared versions content wants" and then have a separate database table for resolved versions. Ugh.

There's other icks. I obviously had to make the engine manifest checked even if the existing engine version is already present, but the code for caching the HTTP responses could use work. Ideally they'd be cached to disk too.
  • Loading branch information
PJB3005 committed Mar 10, 2024
1 parent 7349e42 commit 8ef2bd2
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 96 deletions.
4 changes: 4 additions & 0 deletions SS14.Launcher/ConfigConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ public static class ConfigConstants
public const string RobustBuildsManifest = "https://central.spacestation14.io/builds/robust/manifest.json";
public const string RobustModulesManifest = "https://central.spacestation14.io/builds/robust/modules.json";

// How long to keep cached copies of Robust manifests.
// TODO: Take this from Cache-Control header responses instead.
public static readonly TimeSpan RobustManifestCacheTime = TimeSpan.FromMinutes(15);

public const string UrlOverrideAssets = "https://central.spacestation14.io/launcher/override_assets.json";
public const string UrlAssetsBase = "https://central.spacestation14.io/launcher/assets/";

Expand Down
117 changes: 117 additions & 0 deletions SS14.Launcher/Models/EngineManager/EngineManagerDynamic.Manifest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Serilog;

namespace SS14.Launcher.Models.EngineManager;

public sealed partial class EngineManagerDynamic
{
// This part of the code is responsible for downloading and caching the Robust build manifest.

private readonly SemaphoreSlim _manifestSemaphore = new(1);
private readonly Stopwatch _manifestStopwatch = Stopwatch.StartNew();

private Dictionary<string, VersionInfo>? _cachedRobustVersionInfo;
private TimeSpan _robustCacheValidUntil;

/// <summary>
/// Look up information about an engine version.
/// </summary>
/// <param name="version">The version number to look up.</param>
/// <param name="followRedirects">Follow redirections in version info.</param>
/// <param name="cancel">Cancellation token.</param>
/// <returns>
/// Information about the version, or null if it could not be found.
/// The returned version may be different than what was requested if redirects were followed.
/// </returns>
private async ValueTask<FoundVersionInfo?> GetVersionInfo(
string version,
bool followRedirects = true,
CancellationToken cancel = default)
{
await _manifestSemaphore.WaitAsync(cancel);
try
{
return await GetVersionInfoCore(version, followRedirects, cancel);
}
finally
{
_manifestSemaphore.Release();
}
}

private async ValueTask<FoundVersionInfo?> GetVersionInfoCore(
string version,
bool followRedirects,
CancellationToken cancel)
{
// If we have a cached copy, and it's not expired, we check it.
if (_cachedRobustVersionInfo != null && _robustCacheValidUntil > _manifestStopwatch.Elapsed)
{
// Check the version. If this fails, we immediately re-request the manifest as it may have changed.
// (Connecting to a freshly-updated server with a new Robust version, within the cache window.)
if (FindVersionInfoInCached(version, followRedirects) is { } foundVersionInfo)
return foundVersionInfo;
}

await UpdateBuildManifest(cancel);

return FindVersionInfoInCached(version, followRedirects);
}

private async Task UpdateBuildManifest(CancellationToken cancel)
{
// TODO: If-Modified-Since and If-None-Match request conditions.

Log.Debug("Loading manifest from {manifestUrl}...", ConfigConstants.RobustBuildsManifest);
_cachedRobustVersionInfo =
await _http.GetFromJsonAsync<Dictionary<string, VersionInfo>>(
ConfigConstants.RobustBuildsManifest, cancellationToken: cancel);

_robustCacheValidUntil = _manifestStopwatch.Elapsed + ConfigConstants.RobustManifestCacheTime;
}

private FoundVersionInfo? FindVersionInfoInCached(string version, bool followRedirects)
{
Debug.Assert(_cachedRobustVersionInfo != null);

if (!_cachedRobustVersionInfo.TryGetValue(version, out var versionInfo))
return null;

if (followRedirects)
{
while (versionInfo.RedirectVersion != null)
{
version = versionInfo.RedirectVersion;
versionInfo = _cachedRobustVersionInfo[versionInfo.RedirectVersion];
}
}

return new FoundVersionInfo(version, versionInfo);
}

private sealed record FoundVersionInfo(string Version, VersionInfo Info);

private sealed record VersionInfo(
bool Insecure,
[property: JsonPropertyName("redirect")]
string? RedirectVersion,
Dictionary<string, BuildInfo> Platforms);

private sealed class BuildInfo
{
[JsonInclude] [JsonPropertyName("url")]
public string Url = default!;

[JsonInclude] [JsonPropertyName("sha256")]
public string Sha256 = default!;

[JsonInclude] [JsonPropertyName("sig")]
public string Signature = default!;
}
}
89 changes: 39 additions & 50 deletions SS14.Launcher/Models/EngineManager/EngineManagerDynamic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -22,7 +21,7 @@ namespace SS14.Launcher.Models.EngineManager;
/// <summary>
/// Downloads engine versions from the website.
/// </summary>
public sealed class EngineManagerDynamic : IEngineManager
public sealed partial class EngineManagerDynamic : IEngineManager
{
public const string OverrideVersionName = "_OVERRIDE_";

Expand Down Expand Up @@ -72,7 +71,7 @@ public string GetEngineSignature(string engineVersion)
return _cfg.EngineInstallations.Lookup(engineVersion).Value.Signature;
}

public async Task<bool> DownloadEngineIfNecessary(
public async Task<EngineInstallationResult> DownloadEngineIfNecessary(
string engineVersion,
Helpers.DownloadProgressCallback? progress = null,
CancellationToken cancel = default)
Expand All @@ -82,48 +81,45 @@ public async Task<bool> DownloadEngineIfNecessary(
{
// Engine override means we don't need to download anything, we have it locally!
// At least, if we don't, we'll just blame the developer that enabled it.
return false;
return new EngineInstallationResult(engineVersion, false);
}
#endif

if (_cfg.EngineInstallations.Lookup(engineVersion).HasValue)
{
// Already have the engine version, we're good.
return false;
}
var foundVersion = await GetVersionInfo(engineVersion, cancel: cancel);
if (foundVersion == null)
throw new UpdateException("Unable to find engine version in manifest!");

Log.Information("Installing engine version {version}...", engineVersion);
if (foundVersion.Info.Insecure)
throw new UpdateException("Specified engine version is insecure!");

Log.Debug("Loading manifest from {manifestUrl}...", ConfigConstants.RobustBuildsManifest);
var manifest =
await _http.GetFromJsonAsync<Dictionary<string, VersionInfo>>(
ConfigConstants.RobustBuildsManifest, cancellationToken: cancel);
Log.Debug(
"Requested engine version was {RequestedEngien}, redirected to {FoundVersion}",
engineVersion,
foundVersion.Version);

if (!manifest!.TryGetValue(engineVersion, out var versionInfo))
if (_cfg.EngineInstallations.Lookup(foundVersion.Version).HasValue)
{
throw new UpdateException("Unable to find engine version in manifest!");
// Already have the engine version, we're good.
return new EngineInstallationResult(foundVersion.Version, false);
}

if (versionInfo.Insecure)
{
throw new UpdateException("Specified engine version is insecure!");
}
Log.Information("Installing engine version {version}...", foundVersion.Version);

var bestRid = RidUtility.FindBestRid(versionInfo.Platforms.Keys);
var bestRid = RidUtility.FindBestRid(foundVersion.Info.Platforms.Keys);
if (bestRid == null)
{
throw new UpdateException("No engine version available for our platform!");
}

Log.Debug("Selecting RID {rid}", bestRid);

var buildInfo = versionInfo.Platforms[bestRid];
var buildInfo = foundVersion.Info.Platforms[bestRid];

Log.Debug("Downloading engine: {EngineDownloadUrl}", buildInfo.Url);

Helpers.EnsureDirectoryExists(LauncherPaths.DirEngineInstallations);

var downloadTarget = Path.Combine(LauncherPaths.DirEngineInstallations, $"{engineVersion}.zip");
var downloadTarget = Path.Combine(LauncherPaths.DirEngineInstallations, $"{foundVersion.Version}.zip");
await using var file = File.Create(downloadTarget, 4096, FileOptions.Asynchronous);

try
Expand All @@ -139,9 +135,9 @@ await _http.GetFromJsonAsync<Dictionary<string, VersionInfo>>(
throw;
}

_cfg.AddEngineInstallation(new InstalledEngineVersion(engineVersion, buildInfo.Signature));
_cfg.AddEngineInstallation(new InstalledEngineVersion(foundVersion.Version, buildInfo.Signature));
_cfg.CommitConfig();
return true;
return new EngineInstallationResult(foundVersion.Version, true);
}

public async Task<bool> DownloadModuleIfNecessary(
Expand Down Expand Up @@ -344,9 +340,25 @@ public async Task DoEngineCullMaybeAsync(SqliteConnection contenCon)

// Cull main engine installations.

var modulesUsed = contenCon
var origModulesUsed = contenCon
.Query<(string, string)>("SELECT DISTINCT ModuleName, ModuleVersion FROM ContentEngineDependency")
.ToHashSet();
.ToList();

// GOD DAMNIT more bodging everything together.
// The code sucks.
// My shitty hacks to do engine version redirection fall apart here as well.
var modulesUsed = new HashSet<(string, string)>();
foreach (var (name, version) in origModulesUsed)
{
if (name == "Robust" && await GetVersionInfo(version) is { } redirect)
{
modulesUsed.Add(("Robust", redirect.Version));
}
else
{
modulesUsed.Add((name, version));
}
}

var toCull = _cfg.EngineInstallations.Items.Where(i => !modulesUsed.Contains(("Robust", i.Version))).ToArray();

Expand Down Expand Up @@ -424,27 +436,4 @@ private static string FindOverrideZip(string name, string dir)
Log.Warning("Using override for {Name}: {Path}", name, path);
return path;
}

private sealed class VersionInfo
{
[JsonInclude] [JsonPropertyName("insecure")]
#pragma warning disable CS0649
public bool Insecure;
#pragma warning restore CS0649

[JsonInclude] [JsonPropertyName("platforms")]
public Dictionary<string, BuildInfo> Platforms = default!;
}

private sealed class BuildInfo
{
[JsonInclude] [JsonPropertyName("url")]
public string Url = default!;

[JsonInclude] [JsonPropertyName("sha256")]
public string Sha256 = default!;

[JsonInclude] [JsonPropertyName("sig")]
public string Signature = default!;
}
}
5 changes: 3 additions & 2 deletions SS14.Launcher/Models/EngineManager/IEngineManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ public interface IEngineManager

Task<EngineModuleManifest> GetEngineModuleManifest(CancellationToken cancel = default);

/// <returns>True if something new had to be installed.</returns>
Task<bool> DownloadEngineIfNecessary(
Task<EngineInstallationResult> DownloadEngineIfNecessary(
string engineVersion,
Helpers.DownloadProgressCallback? progress = null,
CancellationToken cancel = default);
Expand Down Expand Up @@ -57,6 +56,8 @@ static string ResolveEngineModuleVersion(EngineModuleManifest manifest, string m
}
}

public record struct EngineInstallationResult(string Version, bool Changed);

public sealed record EngineModuleManifest(
Dictionary<string, EngineModuleData> Modules
);
Expand Down
Loading

0 comments on commit 8ef2bd2

Please sign in to comment.