mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Automatic Client Zipping (#2225)
This commit is contained in:
@@ -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);
|
||||
|
||||
131
Robust.Server/ServerStatus/StatusHost.ClientZip.cs
Normal file
131
Robust.Server/ServerStatus/StatusHost.ClientZip.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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/
|
||||
|
||||
|
||||
Reference in New Issue
Block a user