From 3ecda02630a2fa07d0527f9c9bd53e72311daac7 Mon Sep 17 00:00:00 2001 From: chylex Date: Sun, 27 Apr 2025 08:24:22 +0200 Subject: [PATCH] [WIP] Replace ASP.NET Core with Sisk library --- .../Database/Import/LegacyArchiveImport.cs | 5 +- app/Server/Endpoints/BaseEndpoint.cs | 43 +- .../Endpoints/GetDownloadedFileEndpoint.cs | 33 +- .../Endpoints/GetTrackingScriptEndpoint.cs | 12 +- app/Server/Endpoints/GetUserscriptEndpoint.cs | 10 +- .../Endpoints/GetViewerMessagesEndpoint.cs | 16 +- .../Endpoints/GetViewerMetadataEndpoint.cs | 16 +- app/Server/Endpoints/TrackChannelEndpoint.cs | 7 +- app/Server/Endpoints/TrackMessagesEndpoint.cs | 6 +- app/Server/Endpoints/TrackUsersEndpoint.cs | 7 +- app/Server/Endpoints/ViewerEndpoint.cs | 13 +- app/Server/Server.csproj | 5 +- .../ServerAuthorizationMiddleware.cs | 90 ++-- .../Middlewares/ServerLoggingMiddleware.cs | 80 ++-- app/Server/Service/ServerManager.cs | 71 +++- app/Server/Service/ServerStartup.cs | 56 --- app/Utils/Http/HttpExtensions.cs | 34 -- app/Utils/Http/MimeTypes.cs | 399 ++++++++++++++++++ app/Utils/Utils.csproj | 4 - 19 files changed, 629 insertions(+), 278 deletions(-) delete mode 100644 app/Server/Service/ServerStartup.cs delete mode 100644 app/Utils/Http/HttpExtensions.cs create mode 100644 app/Utils/Http/MimeTypes.cs diff --git a/app/Server/Database/Import/LegacyArchiveImport.cs b/app/Server/Database/Import/LegacyArchiveImport.cs index 3d8d187..18240ee 100644 --- a/app/Server/Database/Import/LegacyArchiveImport.cs +++ b/app/Server/Database/Import/LegacyArchiveImport.cs @@ -10,15 +10,12 @@ using DHT.Server.Data; using DHT.Utils.Collections; using DHT.Utils.Http; using DHT.Utils.Logging; -using Microsoft.AspNetCore.StaticFiles; namespace DHT.Server.Database.Import; public static class LegacyArchiveImport { private static readonly Log Log = Log.ForType(typeof(LegacyArchiveImport)); - private static readonly FileExtensionContentTypeProvider ContentTypeProvider = new (); - public static async Task Read(Stream stream, IDatabaseFile db, FakeSnowflake fakeSnowflake, Func?>> askForServerIds) { Perf perf = Log.Start(); JsonElement root = await JsonSerializer.DeserializeAsync(stream, JsonElementContext.Default.JsonElement); @@ -189,7 +186,7 @@ public static class LegacyArchiveImport { return attachmentsArray.Select(attachmentObj => { string url = attachmentObj.RequireString("url", path); string name = url[(url.LastIndexOf('/') + 1)..]; - string? type = ContentTypeProvider.TryGetContentType(name, out string? contentType) ? contentType : null; + string? type = MimeTypes.TryGetByFileExtension(name, out var mimeType) ? mimeType : null; return new Attachment { Id = fakeSnowflake.Next(), diff --git a/app/Server/Endpoints/BaseEndpoint.cs b/app/Server/Endpoints/BaseEndpoint.cs index 42c44b9..e560cc2 100644 --- a/app/Server/Endpoints/BaseEndpoint.cs +++ b/app/Server/Endpoints/BaseEndpoint.cs @@ -1,65 +1,58 @@ using System; using System.Net; +using System.Net.Http; using System.Text.Json; -using System.Threading; using System.Threading.Tasks; using DHT.Utils.Http; using DHT.Utils.Logging; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.StaticFiles; -using Microsoft.Extensions.Primitives; +using Sisk.Core.Entity; +using Sisk.Core.Http; namespace DHT.Server.Endpoints; abstract class BaseEndpoint { private static readonly Log Log = Log.ForType(); - private static readonly FileExtensionContentTypeProvider ContentTypeProvider = new (); - public async Task Handle(HttpContext ctx) { - HttpResponse response = ctx.Response; - + public async Task Handle(HttpRequest request) { try { - response.StatusCode = (int) HttpStatusCode.OK; - await Respond(ctx.Request, response, ctx.RequestAborted); + return await Respond(request); } catch (OperationCanceledException) { throw; } catch (HttpException e) { Log.Error(e); - response.StatusCode = (int) e.StatusCode; - if (response.HasStarted) { - Log.Warn("Response has already started, cannot write status message: " + e.Message); - } - else { - await response.WriteAsync(e.Message); - } + return new HttpResponse(e.StatusCode).WithContent(e.Message); } catch (Exception e) { Log.Error(e); - response.StatusCode = (int) HttpStatusCode.InternalServerError; + return new HttpResponse(HttpStatusCode.InternalServerError); } } - protected abstract Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken); + protected abstract Task Respond(HttpRequest request); protected static async Task ReadJson(HttpRequest request) { try { - return await request.ReadFromJsonAsync(JsonElementContext.Default.JsonElement); + return await request.GetJsonContentAsync(JsonElementContext.Default.JsonElement); } catch (JsonException) { throw new HttpException(HttpStatusCode.UnsupportedMediaType, "This endpoint only accepts JSON."); } } - protected static async Task WriteFileIfFound(HttpResponse response, string relativeFilePath, byte[]? bytes, CancellationToken cancellationToken) { + protected static Task WriteFileIfFound(string relativeFilePath, byte[]? bytes) { if (bytes == null) { throw new HttpException(HttpStatusCode.NotFound, "File not found: " + relativeFilePath); } - else { - string? contentType = ContentTypeProvider.TryGetContentType(relativeFilePath, out string? type) ? type : null; - await response.WriteFileAsync(contentType, bytes, cancellationToken); + + HttpResponse response = new HttpResponse(new ByteArrayContent(bytes)); + + if (MimeTypes.TryGetByFileExtension(relativeFilePath, out var mimeType)) { + response.Headers.Set(HttpKnownHeaderNames.ContentType, mimeType); } + + return Task.FromResult(response); } protected static Guid GetSessionId(HttpRequest request) { - if (request.Query.TryGetValue("session", out StringValues sessionIdValue) && sessionIdValue.Count == 1 && Guid.TryParse(sessionIdValue[0], out Guid sessionId)) { + if (request.Query.TryGetValue("session", out StringValue sessionIdValue) && !sessionIdValue.IsNull && Guid.TryParse(sessionIdValue, out Guid sessionId)) { return sessionId; } else { diff --git a/app/Server/Endpoints/GetDownloadedFileEndpoint.cs b/app/Server/Endpoints/GetDownloadedFileEndpoint.cs index d40e265..fa83fc0 100644 --- a/app/Server/Endpoints/GetDownloadedFileEndpoint.cs +++ b/app/Server/Endpoints/GetDownloadedFileEndpoint.cs @@ -5,22 +5,39 @@ using System.Threading; using System.Threading.Tasks; using DHT.Server.Database; using DHT.Server.Download; -using DHT.Utils.Http; -using Microsoft.AspNetCore.Http; +using Sisk.Core.Http; +using Sisk.Core.Http.Streams; namespace DHT.Server.Endpoints; sealed class GetDownloadedFileEndpoint(IDatabaseFile db) : BaseEndpoint { - protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) { - string url = WebUtility.UrlDecode((string) request.RouteValues["url"]!); + protected override async Task Respond(HttpRequest request) { + string url = WebUtility.UrlDecode(request.RouteParameters.GetItem("url")); string normalizedUrl = DiscordCdn.NormalizeUrl(url); - if (!await db.Downloads.GetSuccessfulDownloadWithData(normalizedUrl, WriteDataTo(response), cancellationToken)) { - response.Redirect(url, permanent: false); + HttpResponseStreamManager response = request.GetResponseStream(); + + if (!await db.Downloads.GetSuccessfulDownloadWithData(normalizedUrl, WriteDataTo(response), CancellationToken.None)) { + response.SetStatus(HttpStatusCode.Redirect); + response.SetHeader(HttpKnownHeaderNames.Location, url); } + + return response.Close(); } - private static Func WriteDataTo(HttpResponse response) { - return (download, stream, cancellationToken) => response.WriteStreamAsync(download.Type, download.Size, stream, cancellationToken); + private static Func WriteDataTo(HttpResponseStreamManager response) { + return (download, stream, cancellationToken) => { + response.SetStatus(HttpStatusCode.OK); + response.SetHeader(HttpKnownHeaderNames.ContentType, download.Type); + + if (download.Size is {} size) { + response.SetContentLength((long) size); + } + else { + response.SendChunked = true; + } + + return stream.CopyToAsync(response.ResponseStream, cancellationToken); + }; } } diff --git a/app/Server/Endpoints/GetTrackingScriptEndpoint.cs b/app/Server/Endpoints/GetTrackingScriptEndpoint.cs index 98df2d6..74ddeda 100644 --- a/app/Server/Endpoints/GetTrackingScriptEndpoint.cs +++ b/app/Server/Endpoints/GetTrackingScriptEndpoint.cs @@ -1,16 +1,15 @@ using System.Net.Mime; -using System.Threading; +using System.Text; using System.Threading.Tasks; using System.Web; using DHT.Server.Service; -using DHT.Utils.Http; using DHT.Utils.Resources; -using Microsoft.AspNetCore.Http; +using Sisk.Core.Http; namespace DHT.Server.Endpoints; sealed class GetTrackingScriptEndpoint(ServerParameters parameters, ResourceLoader resources) : BaseEndpoint { - protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) { + protected override async Task Respond(HttpRequest request) { string bootstrap = await resources.ReadTextAsync("Tracker/bootstrap.js"); string script = bootstrap.Replace("= 0; /*[PORT]*/", "= " + parameters.Port + ";") .Replace("/*[TOKEN]*/", HttpUtility.JavaScriptStringEncode(parameters.Token)) @@ -19,7 +18,8 @@ sealed class GetTrackingScriptEndpoint(ServerParameters parameters, ResourceLoad .Replace("/*[CSS-SETTINGS]*/", await resources.ReadTextAsync("Tracker/styles/settings.css")) .Replace("/*[DEBUGGER]*/", request.Query.ContainsKey("debug") ? "debugger;" : ""); - response.Headers.Append("X-DHT", "1"); - await response.WriteTextAsync(MediaTypeNames.Text.JavaScript, script, cancellationToken); + return new HttpResponse() + .WithHeader("X-DHT", "1") + .WithContent(script, Encoding.UTF8, MediaTypeNames.Text.JavaScript); } } diff --git a/app/Server/Endpoints/GetUserscriptEndpoint.cs b/app/Server/Endpoints/GetUserscriptEndpoint.cs index e755b04..3862652 100644 --- a/app/Server/Endpoints/GetUserscriptEndpoint.cs +++ b/app/Server/Endpoints/GetUserscriptEndpoint.cs @@ -1,18 +1,16 @@ -using System.Threading; using System.Threading.Tasks; -using DHT.Server.Service.Middlewares; using DHT.Utils.Resources; -using Microsoft.AspNetCore.Http; +using Sisk.Core.Http; namespace DHT.Server.Endpoints; -[ServerAuthorizationMiddleware.NoAuthorization] +// [ServerAuthorizationMiddleware.NoAuthorization] sealed class GetUserscriptEndpoint(ResourceLoader resources) : BaseEndpoint { - protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) { + protected override async Task Respond(HttpRequest request) { const string FileName = "dht.user.js"; const string ResourcePath = "Tracker/loader/" + FileName; byte[]? resourceBytes = await resources.ReadBytesAsyncIfExists(ResourcePath); - await WriteFileIfFound(response, FileName, resourceBytes, cancellationToken); + return await WriteFileIfFound(FileName, resourceBytes); } } diff --git a/app/Server/Endpoints/GetViewerMessagesEndpoint.cs b/app/Server/Endpoints/GetViewerMessagesEndpoint.cs index 17f1ec4..897fa3f 100644 --- a/app/Server/Endpoints/GetViewerMessagesEndpoint.cs +++ b/app/Server/Endpoints/GetViewerMessagesEndpoint.cs @@ -1,19 +1,27 @@ using System; +using System.Net; using System.Threading; using System.Threading.Tasks; using DHT.Server.Database; using DHT.Server.Database.Export; using DHT.Server.Service.Viewer; -using Microsoft.AspNetCore.Http; +using Sisk.Core.Http; +using Sisk.Core.Http.Streams; namespace DHT.Server.Endpoints; sealed class GetViewerMessagesEndpoint(IDatabaseFile db, ViewerSessions viewerSessions) : BaseEndpoint { - protected override Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) { + protected override async Task Respond(HttpRequest request) { Guid sessionId = GetSessionId(request); ViewerSession session = viewerSessions.Get(sessionId); - response.ContentType = "application/x-ndjson"; - return ViewerJsonExport.GetMessages(response.Body, db, session.MessageFilter, cancellationToken); + HttpResponseStreamManager response = request.GetResponseStream(); + response.SendChunked = true; + response.SetStatus(HttpStatusCode.OK); + response.SetHeader(HttpKnownHeaderNames.ContentType, "application/x-ndjson"); + + await ViewerJsonExport.GetMessages(response.ResponseStream, db, session.MessageFilter, CancellationToken.None); + + return response.Close(); } } diff --git a/app/Server/Endpoints/GetViewerMetadataEndpoint.cs b/app/Server/Endpoints/GetViewerMetadataEndpoint.cs index eb52731..68d1bea 100644 --- a/app/Server/Endpoints/GetViewerMetadataEndpoint.cs +++ b/app/Server/Endpoints/GetViewerMetadataEndpoint.cs @@ -1,20 +1,28 @@ using System; +using System.Net; using System.Net.Mime; using System.Threading; using System.Threading.Tasks; using DHT.Server.Database; using DHT.Server.Database.Export; using DHT.Server.Service.Viewer; -using Microsoft.AspNetCore.Http; +using Sisk.Core.Http; +using Sisk.Core.Http.Streams; namespace DHT.Server.Endpoints; sealed class GetViewerMetadataEndpoint(IDatabaseFile db, ViewerSessions viewerSessions) : BaseEndpoint { - protected override Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) { + protected override async Task Respond(HttpRequest request) { Guid sessionId = GetSessionId(request); ViewerSession session = viewerSessions.Get(sessionId); - response.ContentType = MediaTypeNames.Application.Json; - return ViewerJsonExport.GetMetadata(response.Body, db, session.MessageFilter, cancellationToken); + HttpResponseStreamManager response = request.GetResponseStream(); + response.SendChunked = true; + response.SetStatus(HttpStatusCode.OK); + response.SetHeader(HttpKnownHeaderNames.ContentType, MediaTypeNames.Application.Json); + + await ViewerJsonExport.GetMetadata(response.ResponseStream, db, session.MessageFilter, CancellationToken.None); + + return response.Close(); } } diff --git a/app/Server/Endpoints/TrackChannelEndpoint.cs b/app/Server/Endpoints/TrackChannelEndpoint.cs index ab50bde..9b72ba8 100644 --- a/app/Server/Endpoints/TrackChannelEndpoint.cs +++ b/app/Server/Endpoints/TrackChannelEndpoint.cs @@ -1,22 +1,23 @@ using System.Net; using System.Text.Json; -using System.Threading; using System.Threading.Tasks; using DHT.Server.Data; using DHT.Server.Database; using DHT.Utils.Http; -using Microsoft.AspNetCore.Http; +using Sisk.Core.Http; namespace DHT.Server.Endpoints; sealed class TrackChannelEndpoint(IDatabaseFile db) : BaseEndpoint { - protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) { + protected override async Task Respond(HttpRequest request) { JsonElement root = await ReadJson(request); Data.Server server = ReadServer(root.RequireObject("server"), "server"); Channel channel = ReadChannel(root.RequireObject("channel"), "channel", server.Id); await db.Servers.Add([server]); await db.Channels.Add([channel]); + + return new HttpResponse(); } private static Data.Server ReadServer(JsonElement json, string path) { diff --git a/app/Server/Endpoints/TrackMessagesEndpoint.cs b/app/Server/Endpoints/TrackMessagesEndpoint.cs index e40380e..3e06050 100644 --- a/app/Server/Endpoints/TrackMessagesEndpoint.cs +++ b/app/Server/Endpoints/TrackMessagesEndpoint.cs @@ -12,7 +12,7 @@ using DHT.Server.Database; using DHT.Server.Download; using DHT.Utils.Collections; using DHT.Utils.Http; -using Microsoft.AspNetCore.Http; +using Sisk.Core.Http; namespace DHT.Server.Endpoints; @@ -20,7 +20,7 @@ sealed class TrackMessagesEndpoint(IDatabaseFile db) : BaseEndpoint { private const string HasNewMessages = "1"; private const string NoNewMessages = "0"; - protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) { + protected override async Task Respond(HttpRequest request) { JsonElement root = await ReadJson(request); if (root.ValueKind != JsonValueKind.Array) { @@ -42,7 +42,7 @@ sealed class TrackMessagesEndpoint(IDatabaseFile db) : BaseEndpoint { await db.Messages.Add(messages); - await response.WriteTextAsync(anyNewMessages ? HasNewMessages : NoNewMessages, cancellationToken); + return new HttpResponse().WithContent(anyNewMessages ? HasNewMessages : NoNewMessages); } private static Message ReadMessage(JsonElement json, string path) { diff --git a/app/Server/Endpoints/TrackUsersEndpoint.cs b/app/Server/Endpoints/TrackUsersEndpoint.cs index aa5dfb5..d7ebf1e 100644 --- a/app/Server/Endpoints/TrackUsersEndpoint.cs +++ b/app/Server/Endpoints/TrackUsersEndpoint.cs @@ -1,16 +1,15 @@ using System.Net; using System.Text.Json; -using System.Threading; using System.Threading.Tasks; using DHT.Server.Data; using DHT.Server.Database; using DHT.Utils.Http; -using Microsoft.AspNetCore.Http; +using Sisk.Core.Http; namespace DHT.Server.Endpoints; sealed class TrackUsersEndpoint(IDatabaseFile db) : BaseEndpoint { - protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) { + protected override async Task Respond(HttpRequest request) { JsonElement root = await ReadJson(request); if (root.ValueKind != JsonValueKind.Array) { @@ -25,6 +24,8 @@ sealed class TrackUsersEndpoint(IDatabaseFile db) : BaseEndpoint { } await db.Users.Add(users); + + return new HttpResponse(); } private static User ReadUser(JsonElement json, string path) { diff --git a/app/Server/Endpoints/ViewerEndpoint.cs b/app/Server/Endpoints/ViewerEndpoint.cs index 7e255d7..bd24c7c 100644 --- a/app/Server/Endpoints/ViewerEndpoint.cs +++ b/app/Server/Endpoints/ViewerEndpoint.cs @@ -1,24 +1,23 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using DHT.Server.Service.Middlewares; using DHT.Utils.Resources; -using Microsoft.AspNetCore.Http; +using Sisk.Core.Http; namespace DHT.Server.Endpoints; -[ServerAuthorizationMiddleware.NoAuthorization] +// [ServerAuthorizationMiddleware.NoAuthorization] sealed class ViewerEndpoint(ResourceLoader resources) : BaseEndpoint { private readonly Dictionary cache = new (); private readonly SemaphoreSlim cacheSemaphore = new (1); - protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) { - string path = (string?) request.RouteValues["path"] ?? "index.html"; + protected override async Task Respond(HttpRequest request) { + string path = request.RouteParameters.GetValue("path") ?? "index.html"; string resourcePath = "Viewer/" + path; byte[]? resourceBytes; - await cacheSemaphore.WaitAsync(cancellationToken); + await cacheSemaphore.WaitAsync(); try { if (!cache.TryGetValue(resourcePath, out resourceBytes)) { cache[resourcePath] = resourceBytes = await resources.ReadBytesAsyncIfExists(resourcePath); @@ -27,6 +26,6 @@ sealed class ViewerEndpoint(ResourceLoader resources) : BaseEndpoint { cacheSemaphore.Release(); } - await WriteFileIfFound(response, path, resourceBytes, cancellationToken); + return await WriteFileIfFound(path, resourceBytes); } } diff --git a/app/Server/Server.csproj b/app/Server/Server.csproj index 37d902f..1ff83c1 100644 --- a/app/Server/Server.csproj +++ b/app/Server/Server.csproj @@ -6,12 +6,9 @@ DiscordHistoryTrackerServer - - - - + diff --git a/app/Server/Service/Middlewares/ServerAuthorizationMiddleware.cs b/app/Server/Service/Middlewares/ServerAuthorizationMiddleware.cs index 9809a47..0aa44da 100644 --- a/app/Server/Service/Middlewares/ServerAuthorizationMiddleware.cs +++ b/app/Server/Service/Middlewares/ServerAuthorizationMiddleware.cs @@ -1,45 +1,45 @@ -using System; -using System.Net; -using System.Reflection; -using System.Threading.Tasks; -using DHT.Utils.Logging; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Primitives; - -namespace DHT.Server.Service.Middlewares; - -sealed class ServerAuthorizationMiddleware(RequestDelegate next, ServerParameters serverParameters) { - private static readonly Log Log = Log.ForType(); - - public async Task InvokeAsync(HttpContext context) { - if (SkipAuthorization(context) || CheckToken(context.Request)) { - await next(context); - } - else { - context.Response.StatusCode = (int) HttpStatusCode.Forbidden; - } - } - - private static bool SkipAuthorization(HttpContext context) { - return context.GetEndpoint()?.RequestDelegate?.Target?.GetType().GetCustomAttribute() != null; - } - - private bool CheckToken(HttpRequest request) { - return HttpMethods.IsGet(request.Method) - ? CheckToken(request.Query["token"]) - : CheckToken(request.Headers["X-DHT-Token"]); - } - - private bool CheckToken(StringValues token) { - if (token.Count == 1 && token[0] == serverParameters.Token) { - return true; - } - else { - Log.Error("Invalid token: " + (token.Count == 1 ? token[0] : "")); - return false; - } - } - - [AttributeUsage(AttributeTargets.Class)] - public sealed class NoAuthorization : Attribute; -} +// using System; +// using System.Net; +// using System.Reflection; +// using System.Threading.Tasks; +// using DHT.Utils.Logging; +// using Microsoft.AspNetCore.Http; +// using Microsoft.Extensions.Primitives; +// +// namespace DHT.Server.Service.Middlewares; +// +// sealed class ServerAuthorizationMiddleware(RequestDelegate next, ServerParameters serverParameters) { +// private static readonly Log Log = Log.ForType(); +// +// public async Task InvokeAsync(HttpContext context) { +// if (SkipAuthorization(context) || CheckToken(context.Request)) { +// await next(context); +// } +// else { +// context.Response.StatusCode = (int) HttpStatusCode.Forbidden; +// } +// } +// +// private static bool SkipAuthorization(HttpContext context) { +// return context.GetEndpoint()?.RequestDelegate?.Target?.GetType().GetCustomAttribute() != null; +// } +// +// private bool CheckToken(HttpRequest request) { +// return HttpMethods.IsGet(request.Method) +// ? CheckToken(request.Query["token"]) +// : CheckToken(request.Headers["X-DHT-Token"]); +// } +// +// private bool CheckToken(StringValues token) { +// if (token.Count == 1 && token[0] == serverParameters.Token) { +// return true; +// } +// else { +// Log.Error("Invalid token: " + (token.Count == 1 ? token[0] : "")); +// return false; +// } +// } +// +// [AttributeUsage(AttributeTargets.Class)] +// public sealed class NoAuthorization : Attribute; +// } diff --git a/app/Server/Service/Middlewares/ServerLoggingMiddleware.cs b/app/Server/Service/Middlewares/ServerLoggingMiddleware.cs index 4a5b241..04dbe0e 100644 --- a/app/Server/Service/Middlewares/ServerLoggingMiddleware.cs +++ b/app/Server/Service/Middlewares/ServerLoggingMiddleware.cs @@ -1,40 +1,40 @@ -using System; -using System.Diagnostics; -using System.Threading.Tasks; -using DHT.Utils.Logging; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; - -namespace DHT.Server.Service.Middlewares; - -sealed class ServerLoggingMiddleware(RequestDelegate next) { - private static readonly Log Log = Log.ForType(); - - public async Task InvokeAsync(HttpContext context) { - var stopwatch = Stopwatch.StartNew(); - try { - await next(context); - } catch (OperationCanceledException) { - OnFinished(stopwatch, context); - throw; - } - - OnFinished(stopwatch, context); - } - - private static void OnFinished(Stopwatch stopwatch, HttpContext context) { - stopwatch.Stop(); - - HttpRequest request = context.Request; - long requestLength = request.ContentLength ?? 0L; - long elapsedMs = stopwatch.ElapsedMilliseconds; - - if (context.RequestAborted.IsCancellationRequested) { - Log.Debug("Request to " + request.GetEncodedPathAndQuery() + " (" + requestLength + " B) was cancelled after " + elapsedMs + " ms"); - } - else { - int responseStatus = context.Response.StatusCode; - Log.Debug("Request to " + request.GetEncodedPathAndQuery() + " (" + requestLength + " B) returned " + responseStatus + ", took " + elapsedMs + " ms"); - } - } -} +// using System; +// using System.Diagnostics; +// using System.Threading.Tasks; +// using DHT.Utils.Logging; +// using Microsoft.AspNetCore.Http; +// using Microsoft.AspNetCore.Http.Extensions; +// +// namespace DHT.Server.Service.Middlewares; +// +// sealed class ServerLoggingMiddleware(RequestDelegate next) { +// private static readonly Log Log = Log.ForType(); +// +// public async Task InvokeAsync(HttpContext context) { +// var stopwatch = Stopwatch.StartNew(); +// try { +// await next(context); +// } catch (OperationCanceledException) { +// OnFinished(stopwatch, context); +// throw; +// } +// +// OnFinished(stopwatch, context); +// } +// +// private static void OnFinished(Stopwatch stopwatch, HttpContext context) { +// stopwatch.Stop(); +// +// HttpRequest request = context.Request; +// long requestLength = request.ContentLength ?? 0L; +// long elapsedMs = stopwatch.ElapsedMilliseconds; +// +// if (context.RequestAborted.IsCancellationRequested) { +// Log.Debug("Request to " + request.GetEncodedPathAndQuery() + " (" + requestLength + " B) was cancelled after " + elapsedMs + " ms"); +// } +// else { +// int responseStatus = context.Response.StatusCode; +// Log.Debug("Request to " + request.GetEncodedPathAndQuery() + " (" + requestLength + " B) returned " + responseStatus + ", took " + elapsedMs + " ms"); +// } +// } +// } diff --git a/app/Server/Service/ServerManager.cs b/app/Server/Service/ServerManager.cs index 84de064..bf027ef 100644 --- a/app/Server/Service/ServerManager.cs +++ b/app/Server/Service/ServerManager.cs @@ -1,21 +1,24 @@ using System; +using System.Net; using System.Reflection; using System.Threading; using System.Threading.Tasks; using DHT.Server.Database; +using DHT.Server.Endpoints; using DHT.Server.Service.Viewer; using DHT.Utils.Logging; using DHT.Utils.Resources; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Server.Kestrel.Core; -using Microsoft.Extensions.DependencyInjection; +using Sisk.Core.Entity; +using Sisk.Core.Http; +using Sisk.Core.Http.Hosting; +using Router = Sisk.Core.Routing.Router; namespace DHT.Server.Service; public sealed class ServerManager { private static readonly Log Log = Log.ForType(typeof(ServerManager)); - private IWebHost? server; + private HttpServerHostContext? server; public bool IsRunning => server != null; public event EventHandler? StatusChanged; @@ -48,35 +51,59 @@ public sealed class ServerManager { public async Task Stop() { await semaphore.WaitAsync(); try { - await StopInternal(); + StopInternal(); } finally { semaphore.Release(); } } private async Task StartInternal(ushort port, string token) { - await StopInternal(); + StopInternal(); StatusChanged?.Invoke(this, Status.Starting); - void AddServices(IServiceCollection services) { - services.AddSingleton(typeof(IDatabaseFile), db); - services.AddSingleton(typeof(ServerParameters), new ServerParameters(port, token)); - services.AddSingleton(typeof(ResourceLoader), new ResourceLoader(Assembly.GetExecutingAssembly())); - services.AddSingleton(typeof(ViewerSessions), viewerSessions); + ServerParameters parameters = new ServerParameters(port, token); + ResourceLoader resources = new ResourceLoader(Assembly.GetExecutingAssembly()); + + static void ConfigureServer(HttpServerConfiguration config) { + config.AsyncRequestProcessing = true; + config.IncludeRequestIdHeader = false; + config.MaximumContentLength = 0; + config.SendSiskHeader = false; + config.ThrowExceptions = false; } - void SetKestrelOptions(KestrelServerOptions options) { - options.Limits.MaxRequestBodySize = null; - options.Limits.MinResponseDataRate = null; - options.ListenLocalhost(port, static listenOptions => listenOptions.Protocols = HttpProtocols.Http1); + void ConfigureRoutes(Router router) { + router.CallbackErrorHandler = static (exception, _) => { + Log.Error(exception); + return new HttpResponse(HttpStatusCode.InternalServerError).WithContent("An error occurred."); + }; + + router.MapGet("/get-downloaded-file/", new GetDownloadedFileEndpoint(db).Handle); + router.MapGet("/get-tracking-script", new GetTrackingScriptEndpoint(parameters, resources).Handle); + router.MapGet("/get-userscript/", new GetUserscriptEndpoint(resources).Handle); + router.MapGet("/get-viewer-messages", new GetViewerMessagesEndpoint(db, viewerSessions).Handle); + router.MapGet("/get-viewer-metadata", new GetViewerMetadataEndpoint(db, viewerSessions).Handle); + router.MapGet("/viewer/<>", new ViewerEndpoint(resources).Handle); + + router.MapPost("/track-channel", new TrackChannelEndpoint(db).Handle); + router.MapPost("/track-messages", new TrackMessagesEndpoint(db).Handle); + router.MapPost("/track-users", new TrackUsersEndpoint(db).Handle); } - IWebHost newServer = new WebHostBuilder() - .ConfigureServices(AddServices) - .UseKestrel(SetKestrelOptions) - .UseStartup() - .Build(); + string[] allowedOrigins = [ + "https://discord.com", + "https://ptb.discord.com", + "https://canary.discord.com", + "https://discordapp.com", + ]; + + HttpServerHostContext newServer = HttpServer.CreateBuilder() + .UseListeningPort(new ListeningPort(secure: false, "127.0.0.1", port)) + .UseCors(new CrossOriginResourceSharingHeaders(allowOrigins: allowedOrigins, exposeHeaders: [ "X-DHT" ])) + .UseConfiguration(ConfigureServer) + .UseRouter(ConfigureRoutes) + .Build(); Log.Info("Starting server on port " + port + "..."); @@ -95,7 +122,7 @@ public sealed class ServerManager { StatusChanged?.Invoke(this, Status.Started); } - private async Task StopInternal() { + private void StopInternal() { if (server == null) { return; } @@ -103,7 +130,7 @@ public sealed class ServerManager { StatusChanged?.Invoke(this, Status.Stopping); Log.Info("Stopping server..."); - await server.StopAsync(); + server.Dispose(); Log.Info("Server stopped"); server.Dispose(); diff --git a/app/Server/Service/ServerStartup.cs b/app/Server/Service/ServerStartup.cs deleted file mode 100644 index 99b7a04..0000000 --- a/app/Server/Service/ServerStartup.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; -using DHT.Server.Database; -using DHT.Server.Endpoints; -using DHT.Server.Service.Middlewares; -using DHT.Server.Service.Viewer; -using DHT.Utils.Resources; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http.Json; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace DHT.Server.Service; - -sealed class Startup { - private static readonly string[] AllowedOrigins = [ - "https://discord.com", - "https://ptb.discord.com", - "https://canary.discord.com", - "https://discordapp.com", - ]; - - public void ConfigureServices(IServiceCollection services) { - services.Configure(static options => { - options.SerializerOptions.NumberHandling = JsonNumberHandling.Strict; - }); - - services.AddCors(static cors => { - cors.AddDefaultPolicy(static builder => { - builder.WithOrigins(AllowedOrigins).AllowCredentials().AllowAnyMethod().AllowAnyHeader().WithExposedHeaders("X-DHT"); - }); - }); - - services.AddRoutingCore(); - } - - [SuppressMessage("ReSharper", "UnusedMember.Global")] - public void Configure(IApplicationBuilder app, IHostApplicationLifetime lifetime, IDatabaseFile db, ServerParameters parameters, ResourceLoader resources, ViewerSessions viewerSessions) { - app.UseMiddleware(); - app.UseCors(); - app.UseRouting(); - app.UseMiddleware(); - app.UseEndpoints(endpoints => { - endpoints.MapGet("/get-downloaded-file/{url}", new GetDownloadedFileEndpoint(db).Handle); - endpoints.MapGet("/get-tracking-script", new GetTrackingScriptEndpoint(parameters, resources).Handle); - endpoints.MapGet("/get-userscript/{**ignored}", new GetUserscriptEndpoint(resources).Handle); - endpoints.MapGet("/get-viewer-messages", new GetViewerMessagesEndpoint(db, viewerSessions).Handle); - endpoints.MapGet("/get-viewer-metadata", new GetViewerMetadataEndpoint(db, viewerSessions).Handle); - endpoints.MapGet("/viewer/{**path}", new ViewerEndpoint(resources).Handle); - - endpoints.MapPost("/track-channel", new TrackChannelEndpoint(db).Handle); - endpoints.MapPost("/track-messages", new TrackMessagesEndpoint(db).Handle); - endpoints.MapPost("/track-users", new TrackUsersEndpoint(db).Handle); - }); - } -} diff --git a/app/Utils/Http/HttpExtensions.cs b/app/Utils/Http/HttpExtensions.cs deleted file mode 100644 index 7c429a0..0000000 --- a/app/Utils/Http/HttpExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.IO; -using System.Net.Mime; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; - -namespace DHT.Utils.Http; - -public static class HttpExtensions { - public static Task WriteTextAsync(this HttpResponse response, string text, CancellationToken cancellationToken) { - return WriteTextAsync(response, MediaTypeNames.Text.Plain, text, cancellationToken); - } - - public static async Task WriteTextAsync(this HttpResponse response, string contentType, string text, CancellationToken cancellationToken) { - response.ContentType = contentType; - await response.StartAsync(cancellationToken); - await response.WriteAsync(text, Encoding.UTF8, cancellationToken); - } - - public static async Task WriteFileAsync(this HttpResponse response, string? contentType, byte[] bytes, CancellationToken cancellationToken) { - response.ContentType = contentType ?? string.Empty; - response.ContentLength = bytes.Length; - await response.StartAsync(cancellationToken); - await response.Body.WriteAsync(bytes, cancellationToken); - } - - public static async Task WriteStreamAsync(this HttpResponse response, string? contentType, ulong? contentLength, Stream source, CancellationToken cancellationToken) { - response.ContentType = contentType ?? string.Empty; - response.ContentLength = (long?) contentLength; - await response.StartAsync(cancellationToken); - await source.CopyToAsync(response.Body, cancellationToken); - } -} diff --git a/app/Utils/Http/MimeTypes.cs b/app/Utils/Http/MimeTypes.cs new file mode 100644 index 0000000..a302051 --- /dev/null +++ b/app/Utils/Http/MimeTypes.cs @@ -0,0 +1,399 @@ +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; + +namespace DHT.Utils.Http; + +public static class MimeTypes { + // Copied from https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/StaticFiles/src/FileExtensionContentTypeProvider.cs + // Original belongs to the .NET Foundation, licensed under the MIT license. + private static readonly FrozenDictionary ByFileExtension = new Dictionary { + { ".323", "text/h323" }, + { ".3g2", "video/3gpp2" }, + { ".3gp2", "video/3gpp2" }, + { ".3gp", "video/3gpp" }, + { ".3gpp", "video/3gpp" }, + { ".aac", "audio/aac" }, + { ".aaf", "application/octet-stream" }, + { ".aca", "application/octet-stream" }, + { ".accdb", "application/msaccess" }, + { ".accde", "application/msaccess" }, + { ".accdt", "application/msaccess" }, + { ".acx", "application/internet-property-stream" }, + { ".adt", "audio/vnd.dlna.adts" }, + { ".adts", "audio/vnd.dlna.adts" }, + { ".afm", "application/octet-stream" }, + { ".ai", "application/postscript" }, + { ".aif", "audio/x-aiff" }, + { ".aifc", "audio/aiff" }, + { ".aiff", "audio/aiff" }, + { ".appcache", "text/cache-manifest" }, + { ".application", "application/x-ms-application" }, + { ".art", "image/x-jg" }, + { ".asd", "application/octet-stream" }, + { ".asf", "video/x-ms-asf" }, + { ".asi", "application/octet-stream" }, + { ".asm", "text/plain" }, + { ".asr", "video/x-ms-asf" }, + { ".asx", "video/x-ms-asf" }, + { ".atom", "application/atom+xml" }, + { ".au", "audio/basic" }, + { ".avi", "video/x-msvideo" }, + { ".avif", "image/avif" }, + { ".axs", "application/olescript" }, + { ".bas", "text/plain" }, + { ".bcpio", "application/x-bcpio" }, + { ".bin", "application/octet-stream" }, + { ".bmp", "image/bmp" }, + { ".c", "text/plain" }, + { ".cab", "application/vnd.ms-cab-compressed" }, + { ".calx", "application/vnd.ms-office.calx" }, + { ".cat", "application/vnd.ms-pki.seccat" }, + { ".cdf", "application/x-cdf" }, + { ".chm", "application/octet-stream" }, + { ".class", "application/x-java-applet" }, + { ".clp", "application/x-msclip" }, + { ".cmx", "image/x-cmx" }, + { ".cnf", "text/plain" }, + { ".cod", "image/cis-cod" }, + { ".cpio", "application/x-cpio" }, + { ".cpp", "text/plain" }, + { ".crd", "application/x-mscardfile" }, + { ".crl", "application/pkix-crl" }, + { ".crt", "application/x-x509-ca-cert" }, + { ".csh", "application/x-csh" }, + { ".css", "text/css" }, + { ".csv", "text/csv" }, // https://tools.ietf.org/html/rfc7111#section-5.1 + { ".cur", "application/octet-stream" }, + { ".dcr", "application/x-director" }, + { ".deploy", "application/octet-stream" }, + { ".der", "application/x-x509-ca-cert" }, + { ".dib", "image/bmp" }, + { ".dir", "application/x-director" }, + { ".disco", "text/xml" }, + { ".dlm", "text/dlm" }, + { ".doc", "application/msword" }, + { ".docm", "application/vnd.ms-word.document.macroEnabled.12" }, + { ".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }, + { ".dot", "application/msword" }, + { ".dotm", "application/vnd.ms-word.template.macroEnabled.12" }, + { ".dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template" }, + { ".dsp", "application/octet-stream" }, + { ".dtd", "text/xml" }, + { ".dvi", "application/x-dvi" }, + { ".dvr-ms", "video/x-ms-dvr" }, + { ".dwf", "drawing/x-dwf" }, + { ".dwp", "application/octet-stream" }, + { ".dxr", "application/x-director" }, + { ".eml", "message/rfc822" }, + { ".emz", "application/octet-stream" }, + { ".eot", "application/vnd.ms-fontobject" }, + { ".eps", "application/postscript" }, + { ".etx", "text/x-setext" }, + { ".evy", "application/envoy" }, + { ".exe", "application/vnd.microsoft.portable-executable" }, // https://www.iana.org/assignments/media-types/application/vnd.microsoft.portable-executable + { ".fdf", "application/vnd.fdf" }, + { ".fif", "application/fractals" }, + { ".fla", "application/octet-stream" }, + { ".flr", "x-world/x-vrml" }, + { ".flv", "video/x-flv" }, + { ".gif", "image/gif" }, + { ".gtar", "application/x-gtar" }, + { ".gz", "application/x-gzip" }, + { ".h", "text/plain" }, + { ".hdf", "application/x-hdf" }, + { ".hdml", "text/x-hdml" }, + { ".hhc", "application/x-oleobject" }, + { ".hhk", "application/octet-stream" }, + { ".hhp", "application/octet-stream" }, + { ".hlp", "application/winhlp" }, + { ".hqx", "application/mac-binhex40" }, + { ".hta", "application/hta" }, + { ".htc", "text/x-component" }, + { ".htm", "text/html" }, + { ".html", "text/html" }, + { ".htt", "text/webviewhtml" }, + { ".hxt", "text/html" }, + { ".ical", "text/calendar" }, + { ".icalendar", "text/calendar" }, + { ".ico", "image/x-icon" }, + { ".ics", "text/calendar" }, + { ".ief", "image/ief" }, + { ".ifb", "text/calendar" }, + { ".iii", "application/x-iphone" }, + { ".inf", "application/octet-stream" }, + { ".ins", "application/x-internet-signup" }, + { ".isp", "application/x-internet-signup" }, + { ".IVF", "video/x-ivf" }, + { ".jar", "application/java-archive" }, + { ".java", "application/octet-stream" }, + { ".jck", "application/liquidmotion" }, + { ".jcz", "application/liquidmotion" }, + { ".jfif", "image/pjpeg" }, + { ".jpb", "application/octet-stream" }, + { ".jpe", "image/jpeg" }, + { ".jpeg", "image/jpeg" }, + { ".jpg", "image/jpeg" }, + { ".js", "text/javascript" }, + { ".json", "application/json" }, + { ".jsx", "text/jscript" }, + { ".latex", "application/x-latex" }, + { ".lit", "application/x-ms-reader" }, + { ".lpk", "application/octet-stream" }, + { ".lsf", "video/x-la-asf" }, + { ".lsx", "video/x-la-asf" }, + { ".lzh", "application/octet-stream" }, + { ".m13", "application/x-msmediaview" }, + { ".m14", "application/x-msmediaview" }, + { ".m1v", "video/mpeg" }, + { ".m2ts", "video/vnd.dlna.mpeg-tts" }, + { ".m3u", "audio/x-mpegurl" }, + { ".m4a", "audio/mp4" }, + { ".m4v", "video/mp4" }, + { ".man", "application/x-troff-man" }, + { ".manifest", "application/x-ms-manifest" }, + { ".map", "text/plain" }, + { ".markdown", "text/markdown" }, + { ".md", "text/markdown" }, + { ".mdb", "application/x-msaccess" }, + { ".mdp", "application/octet-stream" }, + { ".me", "application/x-troff-me" }, + { ".mht", "message/rfc822" }, + { ".mhtml", "message/rfc822" }, + { ".mid", "audio/mid" }, + { ".midi", "audio/mid" }, + { ".mix", "application/octet-stream" }, + { ".mjs", "text/javascript" }, + { ".mmf", "application/x-smaf" }, + { ".mno", "text/xml" }, + { ".mny", "application/x-msmoney" }, + { ".mov", "video/quicktime" }, + { ".movie", "video/x-sgi-movie" }, + { ".mp2", "video/mpeg" }, + { ".mp3", "audio/mpeg" }, + { ".mp4", "video/mp4" }, + { ".mp4v", "video/mp4" }, + { ".mpa", "video/mpeg" }, + { ".mpe", "video/mpeg" }, + { ".mpeg", "video/mpeg" }, + { ".mpg", "video/mpeg" }, + { ".mpp", "application/vnd.ms-project" }, + { ".mpv2", "video/mpeg" }, + { ".ms", "application/x-troff-ms" }, + { ".msi", "application/octet-stream" }, + { ".mso", "application/octet-stream" }, + { ".mvb", "application/x-msmediaview" }, + { ".mvc", "application/x-miva-compiled" }, + { ".nc", "application/x-netcdf" }, + { ".nsc", "video/x-ms-asf" }, + { ".nws", "message/rfc822" }, + { ".ocx", "application/octet-stream" }, + { ".oda", "application/oda" }, + { ".odc", "text/x-ms-odc" }, + { ".ods", "application/oleobject" }, + { ".oga", "audio/ogg" }, + { ".ogg", "video/ogg" }, + { ".ogv", "video/ogg" }, + { ".ogx", "application/ogg" }, + { ".one", "application/onenote" }, + { ".onea", "application/onenote" }, + { ".onetoc", "application/onenote" }, + { ".onetoc2", "application/onenote" }, + { ".onetmp", "application/onenote" }, + { ".onepkg", "application/onenote" }, + { ".osdx", "application/opensearchdescription+xml" }, + { ".otf", "font/otf" }, + { ".p10", "application/pkcs10" }, + { ".p12", "application/x-pkcs12" }, + { ".p7b", "application/x-pkcs7-certificates" }, + { ".p7c", "application/pkcs7-mime" }, + { ".p7m", "application/pkcs7-mime" }, + { ".p7r", "application/x-pkcs7-certreqresp" }, + { ".p7s", "application/pkcs7-signature" }, + { ".pbm", "image/x-portable-bitmap" }, + { ".pcx", "application/octet-stream" }, + { ".pcz", "application/octet-stream" }, + { ".pdf", "application/pdf" }, + { ".pfb", "application/octet-stream" }, + { ".pfm", "application/octet-stream" }, + { ".pfx", "application/x-pkcs12" }, + { ".pgm", "image/x-portable-graymap" }, + { ".pko", "application/vnd.ms-pki.pko" }, + { ".pma", "application/x-perfmon" }, + { ".pmc", "application/x-perfmon" }, + { ".pml", "application/x-perfmon" }, + { ".pmr", "application/x-perfmon" }, + { ".pmw", "application/x-perfmon" }, + { ".png", "image/png" }, + { ".pnm", "image/x-portable-anymap" }, + { ".pnz", "image/png" }, + { ".pot", "application/vnd.ms-powerpoint" }, + { ".potm", "application/vnd.ms-powerpoint.template.macroEnabled.12" }, + { ".potx", "application/vnd.openxmlformats-officedocument.presentationml.template" }, + { ".ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12" }, + { ".ppm", "image/x-portable-pixmap" }, + { ".pps", "application/vnd.ms-powerpoint" }, + { ".ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12" }, + { ".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow" }, + { ".ppt", "application/vnd.ms-powerpoint" }, + { ".pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12" }, + { ".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" }, + { ".prf", "application/pics-rules" }, + { ".prm", "application/octet-stream" }, + { ".prx", "application/octet-stream" }, + { ".ps", "application/postscript" }, + { ".psd", "application/octet-stream" }, + { ".psm", "application/octet-stream" }, + { ".psp", "application/octet-stream" }, + { ".pub", "application/x-mspublisher" }, + { ".qt", "video/quicktime" }, + { ".qtl", "application/x-quicktimeplayer" }, + { ".qxd", "application/octet-stream" }, + { ".ra", "audio/x-pn-realaudio" }, + { ".ram", "audio/x-pn-realaudio" }, + { ".rar", "application/octet-stream" }, + { ".ras", "image/x-cmu-raster" }, + { ".rf", "image/vnd.rn-realflash" }, + { ".rgb", "image/x-rgb" }, + { ".rm", "application/vnd.rn-realmedia" }, + { ".rmi", "audio/mid" }, + { ".roff", "application/x-troff" }, + { ".rpm", "audio/x-pn-realaudio-plugin" }, + { ".rtf", "application/rtf" }, + { ".rtx", "text/richtext" }, + { ".scd", "application/x-msschedule" }, + { ".sct", "text/scriptlet" }, + { ".sea", "application/octet-stream" }, + { ".setpay", "application/set-payment-initiation" }, + { ".setreg", "application/set-registration-initiation" }, + { ".sgml", "text/sgml" }, + { ".sh", "application/x-sh" }, + { ".shar", "application/x-shar" }, + { ".sit", "application/x-stuffit" }, + { ".sldm", "application/vnd.ms-powerpoint.slide.macroEnabled.12" }, + { ".sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide" }, + { ".smd", "audio/x-smd" }, + { ".smi", "application/octet-stream" }, + { ".smx", "audio/x-smd" }, + { ".smz", "audio/x-smd" }, + { ".snd", "audio/basic" }, + { ".snp", "application/octet-stream" }, + { ".spc", "application/x-pkcs7-certificates" }, + { ".spl", "application/futuresplash" }, + { ".spx", "audio/ogg" }, + { ".src", "application/x-wais-source" }, + { ".ssm", "application/streamingmedia" }, + { ".sst", "application/vnd.ms-pki.certstore" }, + { ".stl", "application/vnd.ms-pki.stl" }, + { ".sv4cpio", "application/x-sv4cpio" }, + { ".sv4crc", "application/x-sv4crc" }, + { ".svg", "image/svg+xml" }, + { ".svgz", "image/svg+xml" }, + { ".swf", "application/x-shockwave-flash" }, + { ".t", "application/x-troff" }, + { ".tar", "application/x-tar" }, + { ".tcl", "application/x-tcl" }, + { ".tex", "application/x-tex" }, + { ".texi", "application/x-texinfo" }, + { ".texinfo", "application/x-texinfo" }, + { ".tgz", "application/x-compressed" }, + { ".thmx", "application/vnd.ms-officetheme" }, + { ".thn", "application/octet-stream" }, + { ".tif", "image/tiff" }, + { ".tiff", "image/tiff" }, + { ".toc", "application/octet-stream" }, + { ".tr", "application/x-troff" }, + { ".trm", "application/x-msterminal" }, + { ".ts", "video/vnd.dlna.mpeg-tts" }, + { ".tsv", "text/tab-separated-values" }, + { ".ttc", "application/x-font-ttf" }, + { ".ttf", "application/x-font-ttf" }, + { ".tts", "video/vnd.dlna.mpeg-tts" }, + { ".txt", "text/plain" }, + { ".u32", "application/octet-stream" }, + { ".uls", "text/iuls" }, + { ".ustar", "application/x-ustar" }, + { ".vbs", "text/vbscript" }, + { ".vcf", "text/x-vcard" }, + { ".vcs", "text/plain" }, + { ".vdx", "application/vnd.ms-visio.viewer" }, + { ".vml", "text/xml" }, + { ".vsd", "application/vnd.visio" }, + { ".vss", "application/vnd.visio" }, + { ".vst", "application/vnd.visio" }, + { ".vsto", "application/x-ms-vsto" }, + { ".vsw", "application/vnd.visio" }, + { ".vsx", "application/vnd.visio" }, + { ".vtx", "application/vnd.visio" }, + { ".wasm", "application/wasm" }, + { ".wav", "audio/wav" }, + { ".wax", "audio/x-ms-wax" }, + { ".wbmp", "image/vnd.wap.wbmp" }, + { ".wcm", "application/vnd.ms-works" }, + { ".wdb", "application/vnd.ms-works" }, + { ".webm", "video/webm" }, + { ".webmanifest", "application/manifest+json" }, // https://w3c.github.io/manifest/#media-type-registration + { ".webp", "image/webp" }, + { ".wks", "application/vnd.ms-works" }, + { ".wm", "video/x-ms-wm" }, + { ".wma", "audio/x-ms-wma" }, + { ".wmd", "application/x-ms-wmd" }, + { ".wmf", "application/x-msmetafile" }, + { ".wml", "text/vnd.wap.wml" }, + { ".wmlc", "application/vnd.wap.wmlc" }, + { ".wmls", "text/vnd.wap.wmlscript" }, + { ".wmlsc", "application/vnd.wap.wmlscriptc" }, + { ".wmp", "video/x-ms-wmp" }, + { ".wmv", "video/x-ms-wmv" }, + { ".wmx", "video/x-ms-wmx" }, + { ".wmz", "application/x-ms-wmz" }, + { ".woff", "application/font-woff" }, // https://www.w3.org/TR/WOFF/#appendix-b + { ".woff2", "font/woff2" }, // https://www.w3.org/TR/WOFF2/#IMT + { ".wps", "application/vnd.ms-works" }, + { ".wri", "application/x-mswrite" }, + { ".wrl", "x-world/x-vrml" }, + { ".wrz", "x-world/x-vrml" }, + { ".wsdl", "text/xml" }, + { ".wtv", "video/x-ms-wtv" }, + { ".wvx", "video/x-ms-wvx" }, + { ".x", "application/directx" }, + { ".xaf", "x-world/x-vrml" }, + { ".xaml", "application/xaml+xml" }, + { ".xap", "application/x-silverlight-app" }, + { ".xbap", "application/x-ms-xbap" }, + { ".xbm", "image/x-xbitmap" }, + { ".xdr", "text/plain" }, + { ".xht", "application/xhtml+xml" }, + { ".xhtml", "application/xhtml+xml" }, + { ".xla", "application/vnd.ms-excel" }, + { ".xlam", "application/vnd.ms-excel.addin.macroEnabled.12" }, + { ".xlc", "application/vnd.ms-excel" }, + { ".xlm", "application/vnd.ms-excel" }, + { ".xls", "application/vnd.ms-excel" }, + { ".xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12" }, + { ".xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12" }, + { ".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }, + { ".xlt", "application/vnd.ms-excel" }, + { ".xltm", "application/vnd.ms-excel.template.macroEnabled.12" }, + { ".xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template" }, + { ".xlw", "application/vnd.ms-excel" }, + { ".xml", "text/xml" }, + { ".xof", "x-world/x-vrml" }, + { ".xpm", "image/x-xpixmap" }, + { ".xps", "application/vnd.ms-xpsdocument" }, + { ".xsd", "text/xml" }, + { ".xsf", "text/xml" }, + { ".xsl", "text/xml" }, + { ".xslt", "text/xml" }, + { ".xsn", "application/octet-stream" }, + { ".xtp", "application/octet-stream" }, + { ".xwd", "image/x-xwindowdump" }, + { ".z", "application/x-compress" }, + { ".zip", "application/x-zip-compressed" }, + }.ToFrozenDictionary(); + + public static bool TryGetByFileExtension(string filePath, [NotNullWhen(true)] out string? mimeType) { + return ByFileExtension.TryGetValue(Path.GetExtension(filePath), out mimeType); + } +} diff --git a/app/Utils/Utils.csproj b/app/Utils/Utils.csproj index f521f5d..6851244 100644 --- a/app/Utils/Utils.csproj +++ b/app/Utils/Utils.csproj @@ -10,10 +10,6 @@ true - - - -