diff --git a/app/Desktop/App.axaml b/app/Desktop/App.axaml index f235c7f..dac0b78 100644 --- a/app/Desktop/App.axaml +++ b/app/Desktop/App.axaml @@ -51,6 +51,7 @@ diff --git a/app/Desktop/Main/Controls/ServerConfigurationPanel.axaml b/app/Desktop/Main/Controls/ServerConfigurationPanel.axaml index 14f5b66..e77f273 100644 --- a/app/Desktop/Main/Controls/ServerConfigurationPanel.axaml +++ b/app/Desktop/Main/Controls/ServerConfigurationPanel.axaml @@ -29,7 +29,7 @@ - - - - - - - - - + + By default, the Discord app blocks the Dev Tools shortcut. The button below changes a hidden setting to unblock the shortcut. + + + + + + diff --git a/app/Desktop/Main/Pages/TrackingPage.axaml.cs b/app/Desktop/Main/Pages/TrackingPage.axaml.cs index 0d04dd2..e61b6c4 100644 --- a/app/Desktop/Main/Pages/TrackingPage.axaml.cs +++ b/app/Desktop/Main/Pages/TrackingPage.axaml.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Avalonia.Controls; @@ -8,24 +9,36 @@ namespace DHT.Desktop.Main.Pages; [SuppressMessage("ReSharper", "MemberCanBeInternal")] public sealed partial class TrackingPage : UserControl { - private bool isCopyingScript; + private readonly HashSet + + + + + `; + + document.body.insertAdjacentElement("beforeend", dialogElement); + + const formElement = document.getElementById("dht-connector-dialog-form"); + const codeErrorElement = document.getElementById("dht-connector-dialog-input-error"); + const codeInputElement = document.getElementById("dht-connector-dialog-input-code"); + const connectButtonElement = document.getElementById("dht-connector-dialog-button-connect"); + const closeButtonElement = document.getElementById("dht-connector-dialog-button-close"); + + dialogElement.addEventListener("close", function() { + dialogElement.remove(); + }); + + closeButtonElement.addEventListener("click", function() { + dialogElement.close(); + }); + + codeInputElement.addEventListener("paste", function() { + setTimeout(async function() { + const code = parseConnectionCode(codeInputElement.value); + if (code !== null) { + await onSubmit(code); + } + }, 0); + }); + + formElement.addEventListener("submit", async function(e) { + e.preventDefault(); + + const code = parseConnectionCode(codeInputElement.value); + if (code === null) { + codeErrorElement.innerText = "Code is not valid."; + } + else { + await onSubmit(code); + } + }); + + let isSubmitting = false; + + async function onSubmit(code) { + if (isSubmitting) { + return; + } + + isSubmitting = true; + connectButtonElement.setAttribute("disabled", ""); + try { + await loadTrackingScript(code); + } catch (e) { + onError("Could not load tracking script.", e); + } finally { + isSubmitting = false; + connectButtonElement.removeAttribute("disabled"); + } + } + + async function loadTrackingScript(code) { + const trackingScript = await getTrackingScript(code.port, code.token); + + if (dialogElement.isConnected) { + // noinspection DynamicallyGeneratedCodeJS + eval(trackingScript); + dialogElement.close(); + } + } + + function onError(message, e) { + console.error(message, e); + codeErrorElement.innerText = message; + } + + dialogElement.showModal(); +} + +/** + * @param {string} code + * @return {?{port: number, token: string}} + */ +function parseConnectionCode(code) { + if (code.length > 5 + 1 + 100) { + return null; + } + + const match = code.match(/^(\d{1,5}):([a-zA-Z0-9]{1,100})$/); + if (!match) { + return null; + } + + const port = Number(match[1]); + if (port < 0 || port > 65535) { + return null; + } + + return { port, token: match[2] }; +} + +async function getTrackingScript(port, token) { + const url = "http://127.0.0.1:" + port + "/get-tracking-script?token=" + encodeURIComponent(token); + const response = await fetch(url, { + credentials: "omit", + signal: AbortSignal.timeout(2000), + }); + + if (!response.ok) { + throw response.status + " " + response.statusText; + } + + if (response.headers.get("X-DHT") !== "1") { + throw "Invalid response"; + } + + return await response.text(); +} diff --git a/app/Server/Endpoints/GetUserscriptEndpoint.cs b/app/Server/Endpoints/GetUserscriptEndpoint.cs new file mode 100644 index 0000000..e755b04 --- /dev/null +++ b/app/Server/Endpoints/GetUserscriptEndpoint.cs @@ -0,0 +1,18 @@ +using System.Threading; +using System.Threading.Tasks; +using DHT.Server.Service.Middlewares; +using DHT.Utils.Resources; +using Microsoft.AspNetCore.Http; + +namespace DHT.Server.Endpoints; + +[ServerAuthorizationMiddleware.NoAuthorization] +sealed class GetUserscriptEndpoint(ResourceLoader resources) : BaseEndpoint { + protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) { + const string FileName = "dht.user.js"; + const string ResourcePath = "Tracker/loader/" + FileName; + + byte[]? resourceBytes = await resources.ReadBytesAsyncIfExists(ResourcePath); + await WriteFileIfFound(response, FileName, resourceBytes, cancellationToken); + } +} diff --git a/app/Server/Server.csproj b/app/Server/Server.csproj index 5b97358..37d902f 100644 --- a/app/Server/Server.csproj +++ b/app/Server/Server.csproj @@ -29,6 +29,11 @@ Resources/Tracker/%(RecursiveDir)%(Filename)%(Extension) false + + Tracker\loader\%(RecursiveDir)%(Filename)%(Extension) + Resources/Tracker/loader/%(RecursiveDir)%(Filename)%(Extension) + false + Tracker\scripts\%(RecursiveDir)%(Filename)%(Extension) Resources/Tracker/scripts/%(RecursiveDir)%(Filename)%(Extension) diff --git a/app/Server/Service/ServerStartup.cs b/app/Server/Service/ServerStartup.cs index f3e085e..99b7a04 100644 --- a/app/Server/Service/ServerStartup.cs +++ b/app/Server/Service/ServerStartup.cs @@ -43,6 +43,7 @@ sealed class Startup { 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);