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
+222
View File
@@ -0,0 +1,222 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Net.Http.Headers;
using RadicadeServer.RecNet.CDN;
namespace RadicadeServer.Storage
{
[Route("storage")]
public class StorageAPI : Controller
{
private const long MaxUploadBytes = 256L * 1024 * 1024;
private const int MaxMultipartPreambleBytes = 1024 * 1024;
private static async Task<long?> FindFirstBoundaryAsync(Stream body, string boundary, CancellationToken cancellationToken)
{
var marker = System.Text.Encoding.ASCII.GetBytes("--" + boundary);
if (marker.Length > MaxMultipartPreambleBytes) return null;
var failure = new int[marker.Length];
for (var i = 1; i < marker.Length; i++)
{
var matched = failure[i - 1];
while (matched > 0 && marker[i] != marker[matched]) matched = failure[matched - 1];
if (marker[i] == marker[matched]) matched++;
failure[i] = matched;
}
var buffer = new byte[8192];
var matchedBytes = 0;
long scanned = 0;
while (scanned <= MaxMultipartPreambleBytes)
{
var read = await body.ReadAsync(buffer.AsMemory(), cancellationToken);
if (read == 0) break;
var endOfBuffer = scanned + read;
for (var i = 0; i < read; i++)
{
while (matchedBytes > 0 && buffer[i] != marker[matchedBytes])
matchedBytes = failure[matchedBytes - 1];
if (buffer[i] == marker[matchedBytes]) matchedBytes++;
if (matchedBytes != marker.Length) continue;
var markerStart = scanned + i + 1 - marker.Length;
body.Position = markerStart + marker.Length;
var nextByte = body.ReadByte();
var secondByte = body.ReadByte();
body.Position = endOfBuffer;
if (markerStart <= MaxMultipartPreambleBytes &&
(nextByte == 13 && secondByte == 10 || nextByte == 45 && secondByte == 45) &&
(markerStart == 0 || ReadByteAt(body, markerStart - 1) == 10))
{
body.Position = markerStart;
return markerStart;
}
matchedBytes = failure[matchedBytes - 1];
}
scanned = endOfBuffer;
}
return null;
}
private static int ReadByteAt(Stream body, long position)
{
var currentPosition = body.Position;
body.Position = position;
var value = body.ReadByte();
body.Position = currentPosition;
return value;
}
[HttpPost("upload/unityassets"), Authorize]
[RequestSizeLimit(MaxUploadBytes)]
public async Task<IActionResult> UploadUnityAssets()
{
if (!Request.HasFormContentType) return BadRequest(new { error = "multipart files required" });
var contentType = MediaTypeHeaderValue.Parse(Request.ContentType!);
var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;
if (string.IsNullOrWhiteSpace(boundary)) return BadRequest(new { error = "multipart boundary required" });
if (boundary.Length > 200) return BadRequest(new { error = "multipart boundary too long" });
var contentEncoding = Request.Headers["Content-Encoding"].ToString().Trim();
var isGzip = string.Equals(contentEncoding, "gzip", StringComparison.OrdinalIgnoreCase);
if (!string.IsNullOrEmpty(contentEncoding) && !isGzip)
return StatusCode(StatusCodes.Status415UnsupportedMediaType, new { error = "unsupported content encoding" });
Stream multipartBody;
await using var decompressedBody = isGzip
? new FileStream(Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".studio-upload"), FileMode.CreateNew,
FileAccess.ReadWrite, FileShare.None, 81920, FileOptions.Asynchronous | FileOptions.DeleteOnClose)
: null;
if (decompressedBody != null)
{
await using (var gzip = new System.IO.Compression.GZipStream(Request.Body, System.IO.Compression.CompressionMode.Decompress, leaveOpen: true))
{
var buffer = new byte[81920];
long total = 0;
int read;
while ((read = await gzip.ReadAsync(buffer.AsMemory(), HttpContext.RequestAborted)) > 0)
{
total += read;
if (total > MaxUploadBytes) return StatusCode(StatusCodes.Status413PayloadTooLarge);
await decompressedBody.WriteAsync(buffer.AsMemory(0, read), HttpContext.RequestAborted);
}
}
decompressedBody.Position = 0;
multipartBody = decompressedBody;
}
else
{
Request.EnableBuffering();
multipartBody = Request.Body;
}
var preambleLength = await FindFirstBoundaryAsync(multipartBody, boundary, HttpContext.RequestAborted);
if (preambleLength is null)
return BadRequest(new { error = "multipart boundary not found within the preamble limit" });
if (preambleLength > 0)
Console.WriteLine($"[StudioUpload] skipped {preambleLength} multipart preamble bytes");
var reader = new MultipartReader(boundary, multipartBody)
{
BodyLengthLimit = null,
HeadersLengthLimit = 64 * 1024
};
var responses = new List<object>();
MultipartSection? section;
while ((section = await reader.ReadNextSectionAsync(HttpContext.RequestAborted)) != null)
{
if (!ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var disposition) ||
!string.Equals(disposition.DispositionType.Value, "form-data", StringComparison.OrdinalIgnoreCase))
continue;
var fileName = HeaderUtilities.RemoveQuotes(disposition.FileNameStar.HasValue
? disposition.FileNameStar
: disposition.FileName).Value;
if (string.IsNullOrWhiteSpace(fileName)) continue;
responses.Add(await StudioAssetStorage.Save(section.Body, HttpContext.RequestAborted));
}
if (responses.Count == 0) return BadRequest(new { error = "no files supplied" });
return Ok(responses);
}
[HttpPost("upload"), Authorize]
[RequestSizeLimit(MaxUploadBytes)]
public async Task<ActionResult<UploadResponse?>> Upload()
{
var boundaryHeader = Request.ContentType == null ? null : MediaTypeHeaderValue.Parse(Request.ContentType).Boundary;
var boundary = HeaderUtilities.RemoveQuotes(boundaryHeader).Value;
if (string.IsNullOrWhiteSpace(boundary)) return BadRequest(new { error = "multipart boundary required" });
var contentEncoding = Request.Headers["Content-Encoding"].ToString().Trim();
var isGzip = string.Equals(contentEncoding, "gzip", StringComparison.OrdinalIgnoreCase);
if (!string.IsNullOrEmpty(contentEncoding) && !isGzip)
return StatusCode(StatusCodes.Status415UnsupportedMediaType, new { error = "unsupported content encoding" });
Stream multipartBody;
await using var decompressedBody = isGzip
? new FileStream(Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".studio-upload"), FileMode.CreateNew,
FileAccess.ReadWrite, FileShare.None, 81920, FileOptions.Asynchronous | FileOptions.DeleteOnClose)
: null;
if (decompressedBody != null)
{
await using (var gzip = new System.IO.Compression.GZipStream(Request.Body, System.IO.Compression.CompressionMode.Decompress, leaveOpen: true))
{
var buffer = new byte[81920];
long total = 0;
int read;
while ((read = await gzip.ReadAsync(buffer.AsMemory(), HttpContext.RequestAborted)) > 0)
{
total += read;
if (total > MaxUploadBytes) return StatusCode(StatusCodes.Status413PayloadTooLarge);
await decompressedBody.WriteAsync(buffer.AsMemory(0, read), HttpContext.RequestAborted);
}
}
decompressedBody.Position = 0;
multipartBody = decompressedBody;
}
else
{
Request.EnableBuffering();
multipartBody = Request.Body;
}
var preambleLength = await FindFirstBoundaryAsync(multipartBody, boundary, HttpContext.RequestAborted);
if (preambleLength is null) return BadRequest(new { error = "multipart boundary not found within the preamble limit" });
var reader = new MultipartReader(boundary, multipartBody)
{
BodyLengthLimit = null,
HeadersLengthLimit = 64 * 1024
};
MultipartSection? section;
while ((section = await reader.ReadNextSectionAsync(HttpContext.RequestAborted)) != null)
{
if (!ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var disposition) ||
!string.Equals(disposition.DispositionType.Value, "form-data", StringComparison.OrdinalIgnoreCase)) continue;
var fileName = HeaderUtilities.RemoveQuotes(disposition.FileNameStar.HasValue
? disposition.FileNameStar
: disposition.FileName).Value;
if (string.IsNullOrWhiteSpace(fileName)) continue;
var stored = await StudioAssetStorage.Save(section.Body, HttpContext.RequestAborted);
if (stored.Length <= 0) return BadRequest(new { error = "empty upload" });
return new UploadResponse(stored.Filename);
}
return BadRequest(new { error = "no file supplied" });
}
}
}