Radicade Public Code

This commit is contained in:
JogmansonReal
2026-10-16 23:33:13 -07:00
commit 55175acdf6
832 changed files with 363261 additions and 0 deletions
+122
View File
@@ -0,0 +1,122 @@
using System.Security.Cryptography;
using System.Text;
using RadicadeServer.DB;
namespace RadicadeServer.Utils
{
public static class AdminUtils
{
/// <summary>
/// The ceiling every admin upload is held to, on the request and on the multipart form.
///
/// Kestrel's own default is 30 MB, and no room import fits under it: an .assetbundle or
/// a .room save routinely runs to tens or hundreds of megabytes. A route that does not
/// raise the limit has its connection killed part-way through the body, so the panel sees
/// a broken connection instead of an answer - which is what made importing a large room
/// look like an authentication failure. Named once here so the next upload route cannot
/// quietly forget it.
/// </summary>
public const long MaxUploadBytes = 256L * 1024 * 1024;
/// <summary>
/// A room file being assembled from parts lives beside its final name under this suffix
/// until the last part lands. Nothing serves or lists one, so a half-imported bundle can
/// never be fetched by the client, and a name ending in this suffix is refused outright.
/// </summary>
public const string PartSuffix = ".uploading";
/// The multipart ceiling this server answers with, in the words the panel shows an admin.
public const string MaxUploadLabel = "256 MiB";
public static bool IsUploadPart(string? name) =>
!string.IsNullOrEmpty(name) && name.EndsWith(PartSuffix, StringComparison.OrdinalIgnoreCase);
private static bool SecretMatches(string? given, string? stored)
{
if (string.IsNullOrEmpty(given) || string.IsNullOrEmpty(stored))
return false;
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(given),
Encoding.UTF8.GetBytes(stored));
}
public static bool IsAdminKey(this string? key)
{
return SecretMatches(key, DBConfig.AdminApiKey);
}
public static bool IsSupportKey(this string? key)
{
return SecretMatches(key, DBConfig.AdminApiKey)
|| SecretMatches(key, DBConfig.SupportApiKey);
}
public static bool TryResolveUploadName(string? requested, out string? name, out string? error)
{
name = null;
error = null;
if (string.IsNullOrWhiteSpace(requested)) return true;
var trimmed = requested.Trim();
if (trimmed.Length > 128)
{
error = "Name is too long.";
return false;
}
if (trimmed != Path.GetFileName(trimmed) || trimmed.Contains(".."))
{
error = "Name must be a bare file name.";
return false;
}
foreach (var c in trimmed)
{
if (!char.IsLetterOrDigit(c) && c != '-' && c != '_' && c != '.')
{
error = "Name may only contain letters, digits, '-', '_' and '.'.";
return false;
}
}
name = trimmed;
return true;
}
/// A subroom's Unity bundle is served under the name it is stored as
/// (RoomServerAPI's unity_assets route hands the client that file name, and
/// the client then fetches it from the CDN by name), so binding accepts any
/// uploaded '<name>.assetbundle'. That is deliberately wider than
/// StudioUnityAssetRegistry.IsValidAssetId, which only covers the
/// 40-character manifest ids the Studio pipeline mints.
public static bool TryResolveBundleName(string? requested, out string? name, out string? error)
{
if (!TryResolveUploadName(requested, out name, out error)) return false;
if (name == null)
{
error = "A bundle file name is required.";
return false;
}
if (!name.EndsWith(".assetbundle", StringComparison.OrdinalIgnoreCase))
{
error = $"'{name}' is not a .assetbundle file.";
name = null;
return false;
}
if (!System.IO.File.Exists(Path.Join(DBConfig.CDNStorage, name)))
{
error = $"No uploaded bundle named '{name}'.";
name = null;
return false;
}
return true;
}
}
}