Automatic Client Zipping (#2225)

This commit is contained in:
20kdc
2021-11-15 01:42:20 +00:00
committed by GitHub
parent b406526592
commit 5443f77526
5 changed files with 263 additions and 17 deletions

View File

@@ -28,6 +28,16 @@ namespace Robust.Server.ServerStatus
int code = 200,
string contentType = "text/plain");
void Respond(
byte[] data,
HttpStatusCode code = HttpStatusCode.OK,
string contentType = "text/plain");
void Respond(
byte[] data,
int code = 200,
string contentType = "text/plain");
void RespondError(HttpStatusCode code);
void RespondJson(object jsonData, HttpStatusCode code = HttpStatusCode.OK);

View File

@@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Security.Cryptography;
using Newtonsoft.Json.Linq;
using Robust.Shared;
using Robust.Shared.ContentPack;
using Robust.Shared.Utility;
namespace Robust.Server.ServerStatus
{
internal sealed partial class StatusHost
{
// Lock used while working on the ACZ.
private readonly object _aczLock = new();
// If an attempt has been made to prepare the ACZ.
private bool _aczPrepareAttempted = false;
// Automatic Client Zip
private byte[]? _aczData;
private string _aczHash = "";
private bool HandleAutomaticClientZip(IStatusHandlerContext context)
{
if (!context.IsGetLike || context.Url!.AbsolutePath != "/client.zip")
{
return false;
}
if (!string.IsNullOrEmpty(_configurationManager.GetCVar(CVars.BuildDownloadUrl)))
{
context.Respond("This server has a build download URL.", HttpStatusCode.NotFound);
return true;
}
var result = PrepareACZ();
if (result == null)
{
context.Respond("Automatic Client Zip was not preparable.", HttpStatusCode.InternalServerError);
return true;
}
context.Respond(result, HttpStatusCode.OK, "application/zip");
return true;
}
private byte[]? PrepareACZ()
{
lock (_aczLock)
{
if (_aczPrepareAttempted) return _aczData;
_aczPrepareAttempted = true;
byte[] data;
try
{
var maybeData = PrepareACZInnards();
if (maybeData == null)
{
return null;
}
data = maybeData;
}
catch (Exception e)
{
_httpSawmill.Error($"Exception in StatusHost PrepareACZ: {e}");
return null;
}
_aczData = data;
using var sha = SHA256.Create();
_aczHash = Convert.ToHexString(sha.ComputeHash(data));
return data;
}
}
private byte[]? PrepareACZInnards()
{
return PrepareACZViaFile() ?? PrepareACZViaMagic();
}
private byte[]? PrepareACZViaFile()
{
var path = PathHelpers.ExecutableRelativeFile("Content.Client.zip");
if (!File.Exists(path)) return null;
return File.ReadAllBytes(path);
}
private byte[]? PrepareACZViaMagic()
{
var paths = new Dictionary<string, byte[]>();
bool AttemptPullFromDisk(string pathTo, string pathFrom)
{
// _httpSawmill.Debug($"StatusHost PrepareACZMagic: {pathFrom} -> {pathTo}");
var res = PathHelpers.ExecutableRelativeFile(pathFrom);
if (!File.Exists(res)) return false;
paths[pathTo] = File.ReadAllBytes(res);
return true;
}
AttemptPullFromDisk("Assemblies/Content.Shared.dll", "../../bin/Content.Client/Content.Shared.dll");
AttemptPullFromDisk("Assemblies/Content.Shared.pdb", "../../bin/Content.Client/Content.Shared.pdb");
if (!AttemptPullFromDisk("Assemblies/Content.Client.dll", "../../bin/Content.Client/Content.Client.dll"))
{
_httpSawmill.Error($"StatusHost PrepareACZMagic couldn't get client assembly - not continuing");
return null;
}
AttemptPullFromDisk("Assemblies/Content.Client.pdb", "../../bin/Content.Client/Content.Client.pdb");
var prefix = PathHelpers.ExecutableRelativeFile("../../Resources");
foreach (var path in PathHelpers.GetFiles(prefix))
{
var relPath = Path.GetRelativePath(prefix, path);
AttemptPullFromDisk(relPath, path);
}
var outStream = new MemoryStream();
var archive = new ZipArchive(outStream, ZipArchiveMode.Create);
foreach (var kvp in paths)
{
var entry = archive.CreateEntry(kvp.Key);
using (var entryStream = entry.Open())
{
entryStream.Write(kvp.Value);
}
}
archive.Dispose();
return outStream.ToArray();
}
}
}

View File

@@ -14,6 +14,7 @@ namespace Robust.Server.ServerStatus
AddHandler(HandleTeapot);
AddHandler(HandleStatus);
AddHandler(HandleInfo);
AddHandler(HandleAutomaticClientZip);
}
private static bool HandleTeapot(IStatusHandlerContext context)
@@ -62,7 +63,7 @@ namespace Robust.Server.ServerStatus
if (string.IsNullOrEmpty(downloadUrl))
{
buildInfo = null;
buildInfo = PrepareACZBuildInfo();
}
else
{
@@ -103,6 +104,34 @@ namespace Robust.Server.ServerStatus
return true;
}
private JObject? PrepareACZBuildInfo()
{
if (PrepareACZ() == null)
{
return null;
}
// Automatic - pass to ACZ
// Unfortunately, we still can't divine engine version.
var engineVersion = _configurationManager.GetCVar(CVars.BuildEngineVersion);
// Fork ID is an interesting case, we don't want to cause too many redownloads but we also don't want to pollute disk.
// Call the fork "custom" if there's no explicit ID given.
var fork = _configurationManager.GetCVar(CVars.BuildForkId);
if (string.IsNullOrEmpty(fork))
{
fork = "custom";
}
return new JObject
{
["engine_version"] = engineVersion,
["fork_id"] = fork,
["version"] = _aczHash,
// Don't supply a download URL - like supplying an empty self-address
["download_url"] = "",
["hash"] = _aczHash,
};
}
}
}

View File

@@ -18,6 +18,7 @@ using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Utility;
using Robust.Shared.Exceptions;
using HttpListener = ManagedHttpListener.HttpListener;
using HttpListenerContext = ManagedHttpListener.HttpListenerContext;
@@ -152,27 +153,34 @@ namespace Robust.Server.ServerStatus
private void RegisterCVars()
{
// Check build.json
var path = PathHelpers.ExecutableRelativeFile("build.json");
if (!File.Exists(path))
if (File.Exists(path))
{
return;
var buildInfo = File.ReadAllText(path);
var info = JsonConvert.DeserializeObject<BuildInfo>(buildInfo)!;
// Don't replace cvars with contents of build.json if overriden by --cvar or such.
SetCVarIfUnmodified(CVars.BuildEngineVersion, info.EngineVersion);
SetCVarIfUnmodified(CVars.BuildForkId, info.ForkId);
SetCVarIfUnmodified(CVars.BuildVersion, info.Version);
SetCVarIfUnmodified(CVars.BuildDownloadUrl, info.Download ?? "");
SetCVarIfUnmodified(CVars.BuildHash, info.Hash ?? "");
}
var buildInfo = File.ReadAllText(path);
var info = JsonConvert.DeserializeObject<BuildInfo>(buildInfo)!;
// Don't replace cvars with contents of build.json if overriden by --cvar or such.
SetCVarIfUnmodified(CVars.BuildEngineVersion, info.EngineVersion);
SetCVarIfUnmodified(CVars.BuildForkId, info.ForkId);
SetCVarIfUnmodified(CVars.BuildVersion, info.Version);
SetCVarIfUnmodified(CVars.BuildDownloadUrl, info.Download ?? "");
SetCVarIfUnmodified(CVars.BuildHash, info.Hash ?? "");
// Automatically determine engine version if no other source has provided a result
var asmVer = typeof(StatusHost).Assembly.GetName().Version;
if (asmVer != null)
{
SetCVarIfUnmodified(CVars.BuildEngineVersion, asmVer.ToString(3));
}
void SetCVarIfUnmodified(CVarDef<string> cvar, string val)
{
if (_configurationManager.GetCVar(cvar) == "")
_configurationManager.SetCVar(cvar, val);
}
}
public void Dispose()
@@ -246,6 +254,7 @@ namespace Robust.Server.ServerStatus
if (RequestMethod == HttpMethod.Head)
{
_context.Response.Close();
return;
}
@@ -254,6 +263,26 @@ namespace Robust.Server.ServerStatus
writer.Write(text);
}
public void Respond(byte[] data, HttpStatusCode code = HttpStatusCode.OK, string contentType = MediaTypeNames.Text.Plain)
{
Respond(data, (int) code, contentType);
}
public void Respond(byte[] data, int code = 200, string contentType = MediaTypeNames.Text.Plain)
{
_context.Response.StatusCode = code;
_context.Response.ContentType = contentType;
_context.Response.ContentLength64 = data.Length;
if (RequestMethod == HttpMethod.Head)
{
_context.Response.Close();
return;
}
_context.Response.Close(data, false);
}
public void RespondError(HttpStatusCode code)
{
Respond(code.ToString(), code);

View File

@@ -10,23 +10,70 @@ tickrate = 60
port = 1212
bindto = "::,0.0.0.0"
# The status server is the TCP side, used by the launcher to determine engine version, etc.
[status]
enabled = true
bind = "*:1212"
# This is the address of the SS14 server as the launcher uses it.
# This is only needed if you're proxying the status HTTP server.
# connectaddress = "udp://localhost:1212"
[game]
hostname = "MyServer"
mapname = "stationstation"
# map = "maps/saltern.yml"
maxplayers = 64
type = 1
welcomemsg = "Welcome to the server!"
[console]
width = 120
height = 60
password = "honk"
hostpassword = "blah"
# If this is true, people connecting from this machine (loopback)
# will automatically be elevated to full admin privileges.
# This literally works by checking if address == 127.0.0.1 || address == ::1
loginlocal = true
[build]
# *Absolutely all of these can be supplied using a "build.json" file*
# For further information, see https://github.com/space-wizards/space-station-14/blob/master/Tools/gen_build_info.py
# The main reason you'd want to supply any of these manually is for a custom fork and if you have no tools.
# Useful to override if the existing version is bad.
# See https://github.com/space-wizards/RobustToolbox/tags for version values, remove the 'v'.
# The value listed here is almost certainly wrong - it is ONLY a demonstration of format.
# engine_version = "0.7.6"
# This one is optional, the launcher will delete other ZIPs of the same fork to save space.
# fork_id = "abacusstation"
# Automatically set if self-hosting client zip, but otherwise use this when updating client build.
# There is no required format, any change counts as a new version.
# version = "Example1"
# This is where the launcher will download the client ZIP from.
# If this isn't supplied, the server will check for a file called "Content.Client.zip",
# and will host it on the status server.
# If that isn't available, the server will attempt to find and use "../../Resources" and
# "../../bin/Content.Client" to automatically construct a client zip.
# It will then host this on the status server.
# Note that these paths do not work on "FULL_RELEASE" servers.
# FULL_RELEASE servers expect to be used with a specific "packaged" layout.
# As such, whatever script you're using to package them is expected to create the ZIP.
# download_url = "http://example.com/compass.zip"
# Build hash - this is a *capitalized* SHA256 hash of the client ZIP.
# Optional in any case and automatically set if hosting a client ZIP.
# build = "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855"
[auth]
# Authentication (accounts):
# 0 = Optional, 1 = Required, 2 = Disabled
# Presumably do require authentication on any public server.
# mode = 0
# If true, even if authentication is required, localhost is still allowed to login directly regardless.
# allowlocal = true
# You should probably never EVER need to touch this, but if you need a custom auth server,
# (the auth server being the one which manages Space Station 14 accounts), you change it here.
# server = https://central.spacestation14.io/auth/