mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
Radicade Public Code
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user