mirror of
				https://github.com/chylex/Discord-History-Tracker.git
				synced 2025-10-31 02:17:15 +01:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			c3bf7d5dc3
			...
			http-serve
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 3ecda02630 | 
| @@ -10,15 +10,12 @@ using DHT.Server.Data; | |||||||
| using DHT.Utils.Collections; | using DHT.Utils.Collections; | ||||||
| using DHT.Utils.Http; | using DHT.Utils.Http; | ||||||
| using DHT.Utils.Logging; | using DHT.Utils.Logging; | ||||||
| using Microsoft.AspNetCore.StaticFiles; |  | ||||||
|  |  | ||||||
| namespace DHT.Server.Database.Import; | namespace DHT.Server.Database.Import; | ||||||
|  |  | ||||||
| public static class LegacyArchiveImport { | public static class LegacyArchiveImport { | ||||||
| 	private static readonly Log Log = Log.ForType(typeof(LegacyArchiveImport)); | 	private static readonly Log Log = Log.ForType(typeof(LegacyArchiveImport)); | ||||||
| 	 | 	 | ||||||
| 	private static readonly FileExtensionContentTypeProvider ContentTypeProvider = new (); |  | ||||||
| 	 |  | ||||||
| 	public static async Task<bool> Read(Stream stream, IDatabaseFile db, FakeSnowflake fakeSnowflake, Func<Data.Server[], Task<Dictionary<Data.Server, ulong>?>> askForServerIds) { | 	public static async Task<bool> Read(Stream stream, IDatabaseFile db, FakeSnowflake fakeSnowflake, Func<Data.Server[], Task<Dictionary<Data.Server, ulong>?>> askForServerIds) { | ||||||
| 		Perf perf = Log.Start(); | 		Perf perf = Log.Start(); | ||||||
| 		JsonElement root = await JsonSerializer.DeserializeAsync(stream, JsonElementContext.Default.JsonElement); | 		JsonElement root = await JsonSerializer.DeserializeAsync(stream, JsonElementContext.Default.JsonElement); | ||||||
| @@ -189,7 +186,7 @@ public static class LegacyArchiveImport { | |||||||
| 		return attachmentsArray.Select(attachmentObj => { | 		return attachmentsArray.Select(attachmentObj => { | ||||||
| 			string url = attachmentObj.RequireString("url", path); | 			string url = attachmentObj.RequireString("url", path); | ||||||
| 			string name = url[(url.LastIndexOf('/') + 1)..]; | 			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 { | 			return new Attachment { | ||||||
| 				Id = fakeSnowflake.Next(), | 				Id = fakeSnowflake.Next(), | ||||||
|   | |||||||
| @@ -1,65 +1,58 @@ | |||||||
| using System; | using System; | ||||||
| using System.Net; | using System.Net; | ||||||
|  | using System.Net.Http; | ||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
| using System.Threading; |  | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
| using DHT.Utils.Http; | using DHT.Utils.Http; | ||||||
| using DHT.Utils.Logging; | using DHT.Utils.Logging; | ||||||
| using Microsoft.AspNetCore.Http; | using Sisk.Core.Entity; | ||||||
| using Microsoft.AspNetCore.StaticFiles; | using Sisk.Core.Http; | ||||||
| using Microsoft.Extensions.Primitives; |  | ||||||
|  |  | ||||||
| namespace DHT.Server.Endpoints; | namespace DHT.Server.Endpoints; | ||||||
|  |  | ||||||
| abstract class BaseEndpoint { | abstract class BaseEndpoint { | ||||||
| 	private static readonly Log Log = Log.ForType<BaseEndpoint>(); | 	private static readonly Log Log = Log.ForType<BaseEndpoint>(); | ||||||
| 	private static readonly FileExtensionContentTypeProvider ContentTypeProvider = new (); |  | ||||||
| 	 |  | ||||||
| 	public async Task Handle(HttpContext ctx) { |  | ||||||
| 		HttpResponse response = ctx.Response; |  | ||||||
| 	 | 	 | ||||||
|  | 	public async Task<HttpResponse> Handle(HttpRequest request) { | ||||||
| 		try { | 		try { | ||||||
| 			response.StatusCode = (int) HttpStatusCode.OK; | 			return await Respond(request); | ||||||
| 			await Respond(ctx.Request, response, ctx.RequestAborted); |  | ||||||
| 		} catch (OperationCanceledException) { | 		} catch (OperationCanceledException) { | ||||||
| 			throw; | 			throw; | ||||||
| 		} catch (HttpException e) { | 		} catch (HttpException e) { | ||||||
| 			Log.Error(e); | 			Log.Error(e); | ||||||
| 			response.StatusCode = (int) e.StatusCode; | 			return new HttpResponse(e.StatusCode).WithContent(e.Message); | ||||||
| 			if (response.HasStarted) { |  | ||||||
| 				Log.Warn("Response has already started, cannot write status message: " + e.Message); |  | ||||||
| 			} |  | ||||||
| 			else { |  | ||||||
| 				await response.WriteAsync(e.Message); |  | ||||||
| 			} |  | ||||||
| 		} catch (Exception e) { | 		} catch (Exception e) { | ||||||
| 			Log.Error(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<HttpResponse> Respond(HttpRequest request); | ||||||
| 	 | 	 | ||||||
| 	protected static async Task<JsonElement> ReadJson(HttpRequest request) { | 	protected static async Task<JsonElement> ReadJson(HttpRequest request) { | ||||||
| 		try { | 		try { | ||||||
| 			return await request.ReadFromJsonAsync(JsonElementContext.Default.JsonElement); | 			return await request.GetJsonContentAsync(JsonElementContext.Default.JsonElement); | ||||||
| 		} catch (JsonException) { | 		} catch (JsonException) { | ||||||
| 			throw new HttpException(HttpStatusCode.UnsupportedMediaType, "This endpoint only accepts JSON."); | 			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<HttpResponse> WriteFileIfFound(string relativeFilePath, byte[]? bytes) { | ||||||
| 		if (bytes == null) { | 		if (bytes == null) { | ||||||
| 			throw new HttpException(HttpStatusCode.NotFound, "File not found: " + relativeFilePath); | 			throw new HttpException(HttpStatusCode.NotFound, "File not found: " + relativeFilePath); | ||||||
| 		} | 		} | ||||||
| 		else { | 		 | ||||||
| 			string? contentType = ContentTypeProvider.TryGetContentType(relativeFilePath, out string? type) ? type : null; | 		HttpResponse response = new HttpResponse(new ByteArrayContent(bytes)); | ||||||
| 			await response.WriteFileAsync(contentType, bytes, cancellationToken); | 		 | ||||||
|  | 		if (MimeTypes.TryGetByFileExtension(relativeFilePath, out var mimeType)) { | ||||||
|  | 			response.Headers.Set(HttpKnownHeaderNames.ContentType, mimeType); | ||||||
| 		} | 		} | ||||||
|  | 		 | ||||||
|  | 		return Task.FromResult(response); | ||||||
| 	} | 	} | ||||||
| 	 | 	 | ||||||
| 	protected static Guid GetSessionId(HttpRequest request) { | 	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; | 			return sessionId; | ||||||
| 		} | 		} | ||||||
| 		else { | 		else { | ||||||
|   | |||||||
| @@ -5,22 +5,39 @@ using System.Threading; | |||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
| using DHT.Server.Database; | using DHT.Server.Database; | ||||||
| using DHT.Server.Download; | using DHT.Server.Download; | ||||||
| using DHT.Utils.Http; | using Sisk.Core.Http; | ||||||
| using Microsoft.AspNetCore.Http; | using Sisk.Core.Http.Streams; | ||||||
|  |  | ||||||
| namespace DHT.Server.Endpoints; | namespace DHT.Server.Endpoints; | ||||||
|  |  | ||||||
| sealed class GetDownloadedFileEndpoint(IDatabaseFile db) : BaseEndpoint { | sealed class GetDownloadedFileEndpoint(IDatabaseFile db) : BaseEndpoint { | ||||||
| 	protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) { | 	protected override async Task<HttpResponse> Respond(HttpRequest request) { | ||||||
| 		string url = WebUtility.UrlDecode((string) request.RouteValues["url"]!); | 		string url = WebUtility.UrlDecode(request.RouteParameters.GetItem("url")); | ||||||
| 		string normalizedUrl = DiscordCdn.NormalizeUrl(url); | 		string normalizedUrl = DiscordCdn.NormalizeUrl(url); | ||||||
| 		 | 		 | ||||||
| 		if (!await db.Downloads.GetSuccessfulDownloadWithData(normalizedUrl, WriteDataTo(response), cancellationToken)) { | 		HttpResponseStreamManager response = request.GetResponseStream(); | ||||||
| 			response.Redirect(url, permanent: false); | 		 | ||||||
|  | 		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<Data.Download, Stream, CancellationToken, Task> WriteDataTo(HttpResponse response) { | 	private static Func<Data.Download, Stream, CancellationToken, Task> WriteDataTo(HttpResponseStreamManager response) { | ||||||
| 		return (download, stream, cancellationToken) => response.WriteStreamAsync(download.Type, download.Size, stream, cancellationToken); | 		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); | ||||||
|  | 		}; | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,16 +1,15 @@ | |||||||
| using System.Net.Mime; | using System.Net.Mime; | ||||||
| using System.Threading; | using System.Text; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
| using System.Web; | using System.Web; | ||||||
| using DHT.Server.Service; | using DHT.Server.Service; | ||||||
| using DHT.Utils.Http; |  | ||||||
| using DHT.Utils.Resources; | using DHT.Utils.Resources; | ||||||
| using Microsoft.AspNetCore.Http; | using Sisk.Core.Http; | ||||||
|  |  | ||||||
| namespace DHT.Server.Endpoints; | namespace DHT.Server.Endpoints; | ||||||
|  |  | ||||||
| sealed class GetTrackingScriptEndpoint(ServerParameters parameters, ResourceLoader resources) : BaseEndpoint { | sealed class GetTrackingScriptEndpoint(ServerParameters parameters, ResourceLoader resources) : BaseEndpoint { | ||||||
| 	protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) { | 	protected override async Task<HttpResponse> Respond(HttpRequest request) { | ||||||
| 		string bootstrap = await resources.ReadTextAsync("Tracker/bootstrap.js"); | 		string bootstrap = await resources.ReadTextAsync("Tracker/bootstrap.js"); | ||||||
| 		string script = bootstrap.Replace("= 0; /*[PORT]*/", "= " + parameters.Port + ";") | 		string script = bootstrap.Replace("= 0; /*[PORT]*/", "= " + parameters.Port + ";") | ||||||
| 		                         .Replace("/*[TOKEN]*/", HttpUtility.JavaScriptStringEncode(parameters.Token)) | 		                         .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("/*[CSS-SETTINGS]*/", await resources.ReadTextAsync("Tracker/styles/settings.css")) | ||||||
| 		                         .Replace("/*[DEBUGGER]*/", request.Query.ContainsKey("debug") ? "debugger;" : ""); | 		                         .Replace("/*[DEBUGGER]*/", request.Query.ContainsKey("debug") ? "debugger;" : ""); | ||||||
| 		 | 		 | ||||||
| 		response.Headers.Append("X-DHT", "1"); | 		return new HttpResponse() | ||||||
| 		await response.WriteTextAsync(MediaTypeNames.Text.JavaScript, script, cancellationToken); | 		       .WithHeader("X-DHT", "1") | ||||||
|  | 		       .WithContent(script, Encoding.UTF8, MediaTypeNames.Text.JavaScript); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,18 +1,16 @@ | |||||||
| using System.Threading; |  | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
| using DHT.Server.Service.Middlewares; |  | ||||||
| using DHT.Utils.Resources; | using DHT.Utils.Resources; | ||||||
| using Microsoft.AspNetCore.Http; | using Sisk.Core.Http; | ||||||
|  |  | ||||||
| namespace DHT.Server.Endpoints; | namespace DHT.Server.Endpoints; | ||||||
|  |  | ||||||
| [ServerAuthorizationMiddleware.NoAuthorization] | // [ServerAuthorizationMiddleware.NoAuthorization] | ||||||
| sealed class GetUserscriptEndpoint(ResourceLoader resources) : BaseEndpoint { | sealed class GetUserscriptEndpoint(ResourceLoader resources) : BaseEndpoint { | ||||||
| 	protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) { | 	protected override async Task<HttpResponse> Respond(HttpRequest request) { | ||||||
| 		const string FileName = "dht.user.js"; | 		const string FileName = "dht.user.js"; | ||||||
| 		const string ResourcePath = "Tracker/loader/" + FileName; | 		const string ResourcePath = "Tracker/loader/" + FileName; | ||||||
| 		 | 		 | ||||||
| 		byte[]? resourceBytes = await resources.ReadBytesAsyncIfExists(ResourcePath); | 		byte[]? resourceBytes = await resources.ReadBytesAsyncIfExists(ResourcePath); | ||||||
| 		await WriteFileIfFound(response, FileName, resourceBytes, cancellationToken); | 		return await WriteFileIfFound(FileName, resourceBytes); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,19 +1,27 @@ | |||||||
| using System; | using System; | ||||||
|  | using System.Net; | ||||||
| using System.Threading; | using System.Threading; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
| using DHT.Server.Database; | using DHT.Server.Database; | ||||||
| using DHT.Server.Database.Export; | using DHT.Server.Database.Export; | ||||||
| using DHT.Server.Service.Viewer; | using DHT.Server.Service.Viewer; | ||||||
| using Microsoft.AspNetCore.Http; | using Sisk.Core.Http; | ||||||
|  | using Sisk.Core.Http.Streams; | ||||||
|  |  | ||||||
| namespace DHT.Server.Endpoints; | namespace DHT.Server.Endpoints; | ||||||
|  |  | ||||||
| sealed class GetViewerMessagesEndpoint(IDatabaseFile db, ViewerSessions viewerSessions) : BaseEndpoint { | sealed class GetViewerMessagesEndpoint(IDatabaseFile db, ViewerSessions viewerSessions) : BaseEndpoint { | ||||||
| 	protected override Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) { | 	protected override async Task<HttpResponse> Respond(HttpRequest request) { | ||||||
| 		Guid sessionId = GetSessionId(request); | 		Guid sessionId = GetSessionId(request); | ||||||
| 		ViewerSession session = viewerSessions.Get(sessionId); | 		ViewerSession session = viewerSessions.Get(sessionId); | ||||||
| 		 | 		 | ||||||
| 		response.ContentType = "application/x-ndjson"; | 		HttpResponseStreamManager response = request.GetResponseStream(); | ||||||
| 		return ViewerJsonExport.GetMessages(response.Body, db, session.MessageFilter, cancellationToken); | 		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(); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,20 +1,28 @@ | |||||||
| using System; | using System; | ||||||
|  | using System.Net; | ||||||
| using System.Net.Mime; | using System.Net.Mime; | ||||||
| using System.Threading; | using System.Threading; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
| using DHT.Server.Database; | using DHT.Server.Database; | ||||||
| using DHT.Server.Database.Export; | using DHT.Server.Database.Export; | ||||||
| using DHT.Server.Service.Viewer; | using DHT.Server.Service.Viewer; | ||||||
| using Microsoft.AspNetCore.Http; | using Sisk.Core.Http; | ||||||
|  | using Sisk.Core.Http.Streams; | ||||||
|  |  | ||||||
| namespace DHT.Server.Endpoints; | namespace DHT.Server.Endpoints; | ||||||
|  |  | ||||||
| sealed class GetViewerMetadataEndpoint(IDatabaseFile db, ViewerSessions viewerSessions) : BaseEndpoint { | sealed class GetViewerMetadataEndpoint(IDatabaseFile db, ViewerSessions viewerSessions) : BaseEndpoint { | ||||||
| 	protected override Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) { | 	protected override async Task<HttpResponse> Respond(HttpRequest request) { | ||||||
| 		Guid sessionId = GetSessionId(request); | 		Guid sessionId = GetSessionId(request); | ||||||
| 		ViewerSession session = viewerSessions.Get(sessionId); | 		ViewerSession session = viewerSessions.Get(sessionId); | ||||||
| 		 | 		 | ||||||
| 		response.ContentType = MediaTypeNames.Application.Json; | 		HttpResponseStreamManager response = request.GetResponseStream(); | ||||||
| 		return ViewerJsonExport.GetMetadata(response.Body, db, session.MessageFilter, cancellationToken); | 		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(); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,22 +1,23 @@ | |||||||
| using System.Net; | using System.Net; | ||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
| using System.Threading; |  | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
| using DHT.Server.Data; | using DHT.Server.Data; | ||||||
| using DHT.Server.Database; | using DHT.Server.Database; | ||||||
| using DHT.Utils.Http; | using DHT.Utils.Http; | ||||||
| using Microsoft.AspNetCore.Http; | using Sisk.Core.Http; | ||||||
|  |  | ||||||
| namespace DHT.Server.Endpoints; | namespace DHT.Server.Endpoints; | ||||||
|  |  | ||||||
| sealed class TrackChannelEndpoint(IDatabaseFile db) : BaseEndpoint { | sealed class TrackChannelEndpoint(IDatabaseFile db) : BaseEndpoint { | ||||||
| 	protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) { | 	protected override async Task<HttpResponse> Respond(HttpRequest request) { | ||||||
| 		JsonElement root = await ReadJson(request); | 		JsonElement root = await ReadJson(request); | ||||||
| 		Data.Server server = ReadServer(root.RequireObject("server"), "server"); | 		Data.Server server = ReadServer(root.RequireObject("server"), "server"); | ||||||
| 		Channel channel = ReadChannel(root.RequireObject("channel"), "channel", server.Id); | 		Channel channel = ReadChannel(root.RequireObject("channel"), "channel", server.Id); | ||||||
| 		 | 		 | ||||||
| 		await db.Servers.Add([server]); | 		await db.Servers.Add([server]); | ||||||
| 		await db.Channels.Add([channel]); | 		await db.Channels.Add([channel]); | ||||||
|  | 		 | ||||||
|  | 		return new HttpResponse(); | ||||||
| 	} | 	} | ||||||
| 	 | 	 | ||||||
| 	private static Data.Server ReadServer(JsonElement json, string path) { | 	private static Data.Server ReadServer(JsonElement json, string path) { | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ using DHT.Server.Database; | |||||||
| using DHT.Server.Download; | using DHT.Server.Download; | ||||||
| using DHT.Utils.Collections; | using DHT.Utils.Collections; | ||||||
| using DHT.Utils.Http; | using DHT.Utils.Http; | ||||||
| using Microsoft.AspNetCore.Http; | using Sisk.Core.Http; | ||||||
|  |  | ||||||
| namespace DHT.Server.Endpoints; | namespace DHT.Server.Endpoints; | ||||||
|  |  | ||||||
| @@ -20,7 +20,7 @@ sealed class TrackMessagesEndpoint(IDatabaseFile db) : BaseEndpoint { | |||||||
| 	private const string HasNewMessages = "1"; | 	private const string HasNewMessages = "1"; | ||||||
| 	private const string NoNewMessages = "0"; | 	private const string NoNewMessages = "0"; | ||||||
| 	 | 	 | ||||||
| 	protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) { | 	protected override async Task<HttpResponse> Respond(HttpRequest request) { | ||||||
| 		JsonElement root = await ReadJson(request); | 		JsonElement root = await ReadJson(request); | ||||||
| 		 | 		 | ||||||
| 		if (root.ValueKind != JsonValueKind.Array) { | 		if (root.ValueKind != JsonValueKind.Array) { | ||||||
| @@ -42,7 +42,7 @@ sealed class TrackMessagesEndpoint(IDatabaseFile db) : BaseEndpoint { | |||||||
| 		 | 		 | ||||||
| 		await db.Messages.Add(messages); | 		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) { | 	private static Message ReadMessage(JsonElement json, string path) { | ||||||
|   | |||||||
| @@ -1,16 +1,15 @@ | |||||||
| using System.Net; | using System.Net; | ||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
| using System.Threading; |  | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
| using DHT.Server.Data; | using DHT.Server.Data; | ||||||
| using DHT.Server.Database; | using DHT.Server.Database; | ||||||
| using DHT.Utils.Http; | using DHT.Utils.Http; | ||||||
| using Microsoft.AspNetCore.Http; | using Sisk.Core.Http; | ||||||
|  |  | ||||||
| namespace DHT.Server.Endpoints; | namespace DHT.Server.Endpoints; | ||||||
|  |  | ||||||
| sealed class TrackUsersEndpoint(IDatabaseFile db) : BaseEndpoint { | sealed class TrackUsersEndpoint(IDatabaseFile db) : BaseEndpoint { | ||||||
| 	protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) { | 	protected override async Task<HttpResponse> Respond(HttpRequest request) { | ||||||
| 		JsonElement root = await ReadJson(request); | 		JsonElement root = await ReadJson(request); | ||||||
| 		 | 		 | ||||||
| 		if (root.ValueKind != JsonValueKind.Array) { | 		if (root.ValueKind != JsonValueKind.Array) { | ||||||
| @@ -25,6 +24,8 @@ sealed class TrackUsersEndpoint(IDatabaseFile db) : BaseEndpoint { | |||||||
| 		} | 		} | ||||||
| 		 | 		 | ||||||
| 		await db.Users.Add(users); | 		await db.Users.Add(users); | ||||||
|  | 		 | ||||||
|  | 		return new HttpResponse(); | ||||||
| 	} | 	} | ||||||
| 	 | 	 | ||||||
| 	private static User ReadUser(JsonElement json, string path) { | 	private static User ReadUser(JsonElement json, string path) { | ||||||
|   | |||||||
| @@ -1,24 +1,23 @@ | |||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
| using System.Threading; | using System.Threading; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
| using DHT.Server.Service.Middlewares; |  | ||||||
| using DHT.Utils.Resources; | using DHT.Utils.Resources; | ||||||
| using Microsoft.AspNetCore.Http; | using Sisk.Core.Http; | ||||||
|  |  | ||||||
| namespace DHT.Server.Endpoints; | namespace DHT.Server.Endpoints; | ||||||
|  |  | ||||||
| [ServerAuthorizationMiddleware.NoAuthorization] | // [ServerAuthorizationMiddleware.NoAuthorization] | ||||||
| sealed class ViewerEndpoint(ResourceLoader resources) : BaseEndpoint { | sealed class ViewerEndpoint(ResourceLoader resources) : BaseEndpoint { | ||||||
| 	private readonly Dictionary<string, byte[]?> cache = new (); | 	private readonly Dictionary<string, byte[]?> cache = new (); | ||||||
| 	private readonly SemaphoreSlim cacheSemaphore = new (1); | 	private readonly SemaphoreSlim cacheSemaphore = new (1); | ||||||
| 	 | 	 | ||||||
| 	protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) { | 	protected override async Task<HttpResponse> Respond(HttpRequest request) { | ||||||
| 		string path = (string?) request.RouteValues["path"] ?? "index.html"; | 		string path = request.RouteParameters.GetValue("path") ?? "index.html"; | ||||||
| 		string resourcePath = "Viewer/" + path; | 		string resourcePath = "Viewer/" + path; | ||||||
| 		 | 		 | ||||||
| 		byte[]? resourceBytes; | 		byte[]? resourceBytes; | ||||||
| 		 | 		 | ||||||
| 		await cacheSemaphore.WaitAsync(cancellationToken); | 		await cacheSemaphore.WaitAsync(); | ||||||
| 		try { | 		try { | ||||||
| 			if (!cache.TryGetValue(resourcePath, out resourceBytes)) { | 			if (!cache.TryGetValue(resourcePath, out resourceBytes)) { | ||||||
| 				cache[resourcePath] = resourceBytes = await resources.ReadBytesAsyncIfExists(resourcePath); | 				cache[resourcePath] = resourceBytes = await resources.ReadBytesAsyncIfExists(resourcePath); | ||||||
| @@ -27,6 +26,6 @@ sealed class ViewerEndpoint(ResourceLoader resources) : BaseEndpoint { | |||||||
| 			cacheSemaphore.Release(); | 			cacheSemaphore.Release(); | ||||||
| 		} | 		} | ||||||
| 		 | 		 | ||||||
| 		await WriteFileIfFound(response, path, resourceBytes, cancellationToken); | 		return await WriteFileIfFound(path, resourceBytes); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,12 +6,9 @@ | |||||||
|     <PackageId>DiscordHistoryTrackerServer</PackageId> |     <PackageId>DiscordHistoryTrackerServer</PackageId> | ||||||
|   </PropertyGroup> |   </PropertyGroup> | ||||||
|    |    | ||||||
|   <ItemGroup> |  | ||||||
|     <FrameworkReference Include="Microsoft.AspNetCore.App" /> |  | ||||||
|   </ItemGroup> |  | ||||||
|    |  | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|     <PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.7" /> |     <PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.7" /> | ||||||
|  |     <PackageReference Include="Sisk.HttpServer" Version="1.4.2" /> | ||||||
|     <PackageReference Include="System.Linq.Async" Version="6.0.1" /> |     <PackageReference Include="System.Linq.Async" Version="6.0.1" /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
|    |    | ||||||
|   | |||||||
| @@ -1,45 +1,45 @@ | |||||||
| using System; | // using System; | ||||||
| using System.Net; | // using System.Net; | ||||||
| using System.Reflection; | // using System.Reflection; | ||||||
| using System.Threading.Tasks; | // using System.Threading.Tasks; | ||||||
| using DHT.Utils.Logging; | // using DHT.Utils.Logging; | ||||||
| using Microsoft.AspNetCore.Http; | // using Microsoft.AspNetCore.Http; | ||||||
| using Microsoft.Extensions.Primitives; | // using Microsoft.Extensions.Primitives; | ||||||
|  | // | ||||||
| namespace DHT.Server.Service.Middlewares; | // namespace DHT.Server.Service.Middlewares; | ||||||
|  | // | ||||||
| sealed class ServerAuthorizationMiddleware(RequestDelegate next, ServerParameters serverParameters) { | // sealed class ServerAuthorizationMiddleware(RequestDelegate next, ServerParameters serverParameters) { | ||||||
| 	private static readonly Log Log = Log.ForType<ServerAuthorizationMiddleware>(); | // 	private static readonly Log Log = Log.ForType<ServerAuthorizationMiddleware>(); | ||||||
| 	 | // 	 | ||||||
| 	public async Task InvokeAsync(HttpContext context) { | // 	public async Task InvokeAsync(HttpContext context) { | ||||||
| 		if (SkipAuthorization(context) || CheckToken(context.Request)) { | // 		if (SkipAuthorization(context) || CheckToken(context.Request)) { | ||||||
| 			await next(context); | // 			await next(context); | ||||||
| 		} | // 		} | ||||||
| 		else { | // 		else { | ||||||
| 			context.Response.StatusCode = (int) HttpStatusCode.Forbidden; | // 			context.Response.StatusCode = (int) HttpStatusCode.Forbidden; | ||||||
| 		} | // 		} | ||||||
| 	} | // 	} | ||||||
| 	 | // 	 | ||||||
| 	private static bool SkipAuthorization(HttpContext context) { | // 	private static bool SkipAuthorization(HttpContext context) { | ||||||
| 		return context.GetEndpoint()?.RequestDelegate?.Target?.GetType().GetCustomAttribute<NoAuthorization>() != null; | // 		return context.GetEndpoint()?.RequestDelegate?.Target?.GetType().GetCustomAttribute<NoAuthorization>() != null; | ||||||
| 	} | // 	} | ||||||
| 	 | // 	 | ||||||
| 	private bool CheckToken(HttpRequest request) { | // 	private bool CheckToken(HttpRequest request) { | ||||||
| 		return HttpMethods.IsGet(request.Method) | // 		return HttpMethods.IsGet(request.Method) | ||||||
| 			       ? CheckToken(request.Query["token"]) | // 			       ? CheckToken(request.Query["token"]) | ||||||
| 			       : CheckToken(request.Headers["X-DHT-Token"]); | // 			       : CheckToken(request.Headers["X-DHT-Token"]); | ||||||
| 	} | // 	} | ||||||
| 	 | // 	 | ||||||
| 	private bool CheckToken(StringValues token) { | // 	private bool CheckToken(StringValues token) { | ||||||
| 		if (token.Count == 1 && token[0] == serverParameters.Token) { | // 		if (token.Count == 1 && token[0] == serverParameters.Token) { | ||||||
| 			return true; | // 			return true; | ||||||
| 		} | // 		} | ||||||
| 		else { | // 		else { | ||||||
| 			Log.Error("Invalid token: " + (token.Count == 1 ? token[0] : "<missing>")); | // 			Log.Error("Invalid token: " + (token.Count == 1 ? token[0] : "<missing>")); | ||||||
| 			return false; | // 			return false; | ||||||
| 		} | // 		} | ||||||
| 	} | // 	} | ||||||
| 	 | // 	 | ||||||
| 	[AttributeUsage(AttributeTargets.Class)] | // 	[AttributeUsage(AttributeTargets.Class)] | ||||||
| 	public sealed class NoAuthorization : Attribute; | // 	public sealed class NoAuthorization : Attribute; | ||||||
| } | // } | ||||||
|   | |||||||
| @@ -1,40 +1,40 @@ | |||||||
| using System; | // using System; | ||||||
| using System.Diagnostics; | // using System.Diagnostics; | ||||||
| using System.Threading.Tasks; | // using System.Threading.Tasks; | ||||||
| using DHT.Utils.Logging; | // using DHT.Utils.Logging; | ||||||
| using Microsoft.AspNetCore.Http; | // using Microsoft.AspNetCore.Http; | ||||||
| using Microsoft.AspNetCore.Http.Extensions; | // using Microsoft.AspNetCore.Http.Extensions; | ||||||
|  | // | ||||||
| namespace DHT.Server.Service.Middlewares; | // namespace DHT.Server.Service.Middlewares; | ||||||
|  | // | ||||||
| sealed class ServerLoggingMiddleware(RequestDelegate next) { | // sealed class ServerLoggingMiddleware(RequestDelegate next) { | ||||||
| 	private static readonly Log Log = Log.ForType<ServerLoggingMiddleware>(); | // 	private static readonly Log Log = Log.ForType<ServerLoggingMiddleware>(); | ||||||
| 	 | // 	 | ||||||
| 	public async Task InvokeAsync(HttpContext context) { | // 	public async Task InvokeAsync(HttpContext context) { | ||||||
| 		var stopwatch = Stopwatch.StartNew(); | // 		var stopwatch = Stopwatch.StartNew(); | ||||||
| 		try { | // 		try { | ||||||
| 			await next(context); | // 			await next(context); | ||||||
| 		} catch (OperationCanceledException) { | // 		} catch (OperationCanceledException) { | ||||||
| 			OnFinished(stopwatch, context); | // 			OnFinished(stopwatch, context); | ||||||
| 			throw; | // 			throw; | ||||||
| 		} | // 		} | ||||||
| 		 | // 		 | ||||||
| 		OnFinished(stopwatch, context); | // 		OnFinished(stopwatch, context); | ||||||
| 	} | // 	} | ||||||
| 	 | // 	 | ||||||
| 	private static void OnFinished(Stopwatch stopwatch, HttpContext context) { | // 	private static void OnFinished(Stopwatch stopwatch, HttpContext context) { | ||||||
| 		stopwatch.Stop(); | // 		stopwatch.Stop(); | ||||||
| 		 | // 		 | ||||||
| 		HttpRequest request = context.Request; | // 		HttpRequest request = context.Request; | ||||||
| 		long requestLength = request.ContentLength ?? 0L; | // 		long requestLength = request.ContentLength ?? 0L; | ||||||
| 		long elapsedMs = stopwatch.ElapsedMilliseconds; | // 		long elapsedMs = stopwatch.ElapsedMilliseconds; | ||||||
| 		 | // 		 | ||||||
| 		if (context.RequestAborted.IsCancellationRequested) { | // 		if (context.RequestAborted.IsCancellationRequested) { | ||||||
| 			Log.Debug("Request to " + request.GetEncodedPathAndQuery() + " (" + requestLength + " B) was cancelled after " + elapsedMs + " ms"); | // 			Log.Debug("Request to " + request.GetEncodedPathAndQuery() + " (" + requestLength + " B) was cancelled after " + elapsedMs + " ms"); | ||||||
| 		} | // 		} | ||||||
| 		else { | // 		else { | ||||||
| 			int responseStatus = context.Response.StatusCode; | // 			int responseStatus = context.Response.StatusCode; | ||||||
| 			Log.Debug("Request to " + request.GetEncodedPathAndQuery() + " (" + requestLength + " B) returned " + responseStatus + ", took " + elapsedMs + " ms"); | // 			Log.Debug("Request to " + request.GetEncodedPathAndQuery() + " (" + requestLength + " B) returned " + responseStatus + ", took " + elapsedMs + " ms"); | ||||||
| 		} | // 		} | ||||||
| 	} | // 	} | ||||||
| } | // } | ||||||
|   | |||||||
| @@ -1,21 +1,24 @@ | |||||||
| using System; | using System; | ||||||
|  | using System.Net; | ||||||
| using System.Reflection; | using System.Reflection; | ||||||
| using System.Threading; | using System.Threading; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
| using DHT.Server.Database; | using DHT.Server.Database; | ||||||
|  | using DHT.Server.Endpoints; | ||||||
| using DHT.Server.Service.Viewer; | using DHT.Server.Service.Viewer; | ||||||
| using DHT.Utils.Logging; | using DHT.Utils.Logging; | ||||||
| using DHT.Utils.Resources; | using DHT.Utils.Resources; | ||||||
| using Microsoft.AspNetCore.Hosting; | using Sisk.Core.Entity; | ||||||
| using Microsoft.AspNetCore.Server.Kestrel.Core; | using Sisk.Core.Http; | ||||||
| using Microsoft.Extensions.DependencyInjection; | using Sisk.Core.Http.Hosting; | ||||||
|  | using Router = Sisk.Core.Routing.Router; | ||||||
|  |  | ||||||
| namespace DHT.Server.Service; | namespace DHT.Server.Service; | ||||||
|  |  | ||||||
| public sealed class ServerManager { | public sealed class ServerManager { | ||||||
| 	private static readonly Log Log = Log.ForType(typeof(ServerManager)); | 	private static readonly Log Log = Log.ForType(typeof(ServerManager)); | ||||||
| 	 | 	 | ||||||
| 	private IWebHost? server; | 	private HttpServerHostContext? server; | ||||||
| 	public bool IsRunning => server != null; | 	public bool IsRunning => server != null; | ||||||
| 	 | 	 | ||||||
| 	public event EventHandler<Status>? StatusChanged; | 	public event EventHandler<Status>? StatusChanged; | ||||||
| @@ -48,35 +51,59 @@ public sealed class ServerManager { | |||||||
| 	public async Task Stop() { | 	public async Task Stop() { | ||||||
| 		await semaphore.WaitAsync(); | 		await semaphore.WaitAsync(); | ||||||
| 		try { | 		try { | ||||||
| 			await StopInternal(); | 			StopInternal(); | ||||||
| 		} finally { | 		} finally { | ||||||
| 			semaphore.Release(); | 			semaphore.Release(); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	 | 	 | ||||||
| 	private async Task StartInternal(ushort port, string token) { | 	private async Task StartInternal(ushort port, string token) { | ||||||
| 		await StopInternal(); | 		StopInternal(); | ||||||
| 		 | 		 | ||||||
| 		StatusChanged?.Invoke(this, Status.Starting); | 		StatusChanged?.Invoke(this, Status.Starting); | ||||||
| 		 | 		 | ||||||
| 		void AddServices(IServiceCollection services) { | 		ServerParameters parameters = new ServerParameters(port, token); | ||||||
| 			services.AddSingleton(typeof(IDatabaseFile), db); | 		ResourceLoader resources = new ResourceLoader(Assembly.GetExecutingAssembly()); | ||||||
| 			services.AddSingleton(typeof(ServerParameters), new ServerParameters(port, token)); | 		 | ||||||
| 			services.AddSingleton(typeof(ResourceLoader), new ResourceLoader(Assembly.GetExecutingAssembly())); | 		static void ConfigureServer(HttpServerConfiguration config) { | ||||||
| 			services.AddSingleton(typeof(ViewerSessions), viewerSessions); | 			config.AsyncRequestProcessing = true; | ||||||
|  | 			config.IncludeRequestIdHeader = false; | ||||||
|  | 			config.MaximumContentLength = 0; | ||||||
|  | 			config.SendSiskHeader = false; | ||||||
|  | 			config.ThrowExceptions = false; | ||||||
| 		} | 		} | ||||||
| 		 | 		 | ||||||
| 		void SetKestrelOptions(KestrelServerOptions options) { | 		void ConfigureRoutes(Router router) { | ||||||
| 			options.Limits.MaxRequestBodySize = null; | 			router.CallbackErrorHandler = static (exception, _) => { | ||||||
| 			options.Limits.MinResponseDataRate = null; | 				Log.Error(exception); | ||||||
| 			options.ListenLocalhost(port, static listenOptions => listenOptions.Protocols = HttpProtocols.Http1); | 				return new HttpResponse(HttpStatusCode.InternalServerError).WithContent("An error occurred."); | ||||||
|  | 			}; | ||||||
|  | 			 | ||||||
|  | 			router.MapGet("/get-downloaded-file/<url>", new GetDownloadedFileEndpoint(db).Handle); | ||||||
|  | 			router.MapGet("/get-tracking-script", new GetTrackingScriptEndpoint(parameters, resources).Handle); | ||||||
|  | 			router.MapGet("/get-userscript/<ignored>", 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/<<path>>", 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() | 		string[] allowedOrigins = [ | ||||||
| 		                     .ConfigureServices(AddServices) | 			"https://discord.com", | ||||||
| 		                     .UseKestrel(SetKestrelOptions) | 			"https://ptb.discord.com", | ||||||
| 		                     .UseStartup<Startup>() | 			"https://canary.discord.com", | ||||||
| 		                     .Build(); | 			"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 + "..."); | 		Log.Info("Starting server on port " + port + "..."); | ||||||
| 		 | 		 | ||||||
| @@ -95,7 +122,7 @@ public sealed class ServerManager { | |||||||
| 		StatusChanged?.Invoke(this, Status.Started); | 		StatusChanged?.Invoke(this, Status.Started); | ||||||
| 	} | 	} | ||||||
| 	 | 	 | ||||||
| 	private async Task StopInternal() { | 	private void StopInternal() { | ||||||
| 		if (server == null) { | 		if (server == null) { | ||||||
| 			return; | 			return; | ||||||
| 		} | 		} | ||||||
| @@ -103,7 +130,7 @@ public sealed class ServerManager { | |||||||
| 		StatusChanged?.Invoke(this, Status.Stopping); | 		StatusChanged?.Invoke(this, Status.Stopping); | ||||||
| 		 | 		 | ||||||
| 		Log.Info("Stopping server..."); | 		Log.Info("Stopping server..."); | ||||||
| 		await server.StopAsync(); | 		server.Dispose(); | ||||||
| 		Log.Info("Server stopped"); | 		Log.Info("Server stopped"); | ||||||
| 		 | 		 | ||||||
| 		server.Dispose(); | 		server.Dispose(); | ||||||
|   | |||||||
| @@ -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<JsonOptions>(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<ServerLoggingMiddleware>(); |  | ||||||
| 		app.UseCors(); |  | ||||||
| 		app.UseRouting(); |  | ||||||
| 		app.UseMiddleware<ServerAuthorizationMiddleware>(); |  | ||||||
| 		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); |  | ||||||
| 		}); |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -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); |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
							
								
								
									
										399
									
								
								app/Utils/Http/MimeTypes.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										399
									
								
								app/Utils/Http/MimeTypes.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -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<string, string> ByFileExtension = new Dictionary<string, string> { | ||||||
|  | 		{ ".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); | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -10,10 +10,6 @@ | |||||||
|     <AllowUnsafeBlocks>true</AllowUnsafeBlocks> |     <AllowUnsafeBlocks>true</AllowUnsafeBlocks> | ||||||
|   </PropertyGroup> |   </PropertyGroup> | ||||||
|    |    | ||||||
|   <ItemGroup> |  | ||||||
|     <FrameworkReference Include="Microsoft.AspNetCore.App" /> |  | ||||||
|   </ItemGroup> |  | ||||||
|    |  | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|     <PackageReference Include="JetBrains.Annotations" Version="2023.2.0" /> |     <PackageReference Include="JetBrains.Annotations" Version="2023.2.0" /> | ||||||
|     <PackageReference Include="System.Reactive" Version="6.0.0" /> |     <PackageReference Include="System.Reactive" Version="6.0.0" /> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user