csharp/0x0ade/CelesteNet/CelesteNet.Client/CelesteNetClientRC.cs

CelesteNetClientRC.cs
using MC = Mono.Cecil;
using CIL = Mono.Cecil.Cil;

using Celeste.Mod.CelesteNet.DataTypes;
using Celeste.Mod.Helpers;
using Microsoft.Xna.Framework;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Monocle;
using MonoMod.Utils;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Net;
using System.Collections.Specialized;

namespace Celeste.Mod.CelesteNet.Client {
    // Based off of Everest's own DebugRC.
    public static clast CelesteNetClientRC {

        private static HttpListener Listener;
        private static Thread ListenerThread;

        public static void Initialize() {
            if (Listener != null)
                return;

            try {
                Listener = new();
                // Port MUST be fixed as the website expects it to be the same for everyone.
                Listener.Prefixes.Add($"http://localhost:{CelesteNetUtils.ClientRCPort}/");
                Listener.Start();
            } catch (Exception e) {
                e.LogDetailed();
                try {
                    Listener?.Stop();
                } catch { }
                return;
            }

            ListenerThread = new(ListenerLoop) {
                IsBackground = true,
                Priority = ThreadPriority.BelowNormal
            };
            ListenerThread.Start();
        }

        public static void Shutdown() {
            Listener?.Abort();
            ListenerThread?.Abort();
            Listener = null;
            ListenerThread = null;
        }

        private static void ListenerLoop() {
            Logger.Log(LogLevel.INF, "rc", $"Started ClientRC thread, available via http://localhost:{CelesteNetUtils.ClientRCPort}/");
            try {
                while (Listener?.IsListening ?? false) {
                    ThreadPool.QueueUserWorkItem(c => {
                        HttpListenerContext context = c as HttpListenerContext;

                        if (context.Request.HttpMethod == "OPTIONS") {
                            context.Response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With");
                            context.Response.AddHeader("Access-Control-Allow-Methods", "GET, POST");
                            context.Response.AddHeader("Access-Control-Max-Age", "1728000");
                            return;
                        }
                        context.Response.AppendHeader("Access-Control-Allow-Origin", "*");

                        try {
                            using (context.Request.InputStream)
                            using (context.Response) {
                                HandleRequest(context);
                            }
                        } catch (ThreadAbortException) {
                            throw;
                        } catch (ThreadInterruptedException) {
                            throw;
                        } catch (Exception e) {
                            Logger.Log(LogLevel.INF, "rc", $"ClientRC failed responding: {e}");
                        }
                    }, Listener.GetContext());
                }
            } catch (ThreadAbortException) {
                throw;
            } catch (ThreadInterruptedException) {
                throw;
            } catch (HttpListenerException e) {
                // 500 = Listener closed.
                // 995 = I/O abort due to thread abort or application shutdown.
                if (e.ErrorCode != 500 &&
                    e.ErrorCode != 995) {
                    Logger.Log(LogLevel.INF, "rc", $"ClientRC failed listening ({e.ErrorCode}): {e}");
                }
            } catch (Exception e) {
                Logger.Log(LogLevel.INF, "rc", $"ClientRC failed listening: {e}");
            }
        }

        private static void HandleRequest(HttpListenerContext c) {
            Logger.Log(LogLevel.VVV, "rc", $"Requested: {c.Request.RawUrl}");

            string url = c.Request.RawUrl;
            int indexOfSplit = url.IndexOf('?');
            if (indexOfSplit != -1)
                url = url.Substring(0, indexOfSplit);

            RCEndPoint endpoint =
                EndPoints.FirstOrDefault(ep => ep.Path == c.Request.RawUrl) ??
                EndPoints.FirstOrDefault(ep => ep.Path == url) ??
                EndPoints.FirstOrDefault(ep => ep.Path.ToLowerInvariant() == c.Request.RawUrl.ToLowerInvariant()) ??
                EndPoints.FirstOrDefault(ep => ep.Path.ToLowerInvariant() == url.ToLowerInvariant()) ??
                EndPoints.FirstOrDefault(ep => ep.Path == "/404");
            endpoint.Handle(c);
        }


        #region Read / Parse Helpers

        public static NameValueCollection ParseQueryString(string url) {
            NameValueCollection nvc = new();

            int indexOfSplit = url.IndexOf('?');
            if (indexOfSplit == -1)
                return nvc;
            url = url.Substring(indexOfSplit + 1);

            string[] args = url.Split('&');
            foreach (string arg in args) {
                indexOfSplit = arg.IndexOf('=');
                if (indexOfSplit == -1)
                    continue;
                nvc[arg.Substring(0, indexOfSplit)] = arg.Substring(indexOfSplit + 1);
            }

            return nvc;
        }

        #endregion

        #region Write Helpers

        public static void WriteHTMLStart(HttpListenerContext c, StringBuilder builder) {
            builder.Append(
@"

    
        
        
        CelesteNet ClientRC
        
@font-face {
    font-family: Renogare;
    src:
    url(""https://everestapi.github.io/fonts/Renogare-Regular.woff"") format(""woff""),
    url(""https://everestapi.github.io/fonts/Renogare-Regular.otf"") format(""opentype"");
}
body {
    color: rgba(0, 0, 0, 0.87);
    font-family: sans-serif;
    padding: 0;
    margin: 0;
    line-height: 1.5em;
}
header {
    background: #3b2d4a;
    color: white;
    font-family: Renogare, sans-serif;
    font-size: 32px;
    position: sticky;
    top: 0;
    left: 0;
    right: 0;
    height: 64px;
    line-height: 64px;
    padding: 8px 48px;
    z-index: 100;
}
#main {
    position: relative;
    margin: 8px;
    min-height: 100vh;
}
#endpoints li h3 {
    margin-bottom: 0;
}
#endpoints li p {
    margin-top: 0;
}
        
    
    
"
            );

            builder.AppendLine(@"CelesteNet ClientRC");
            builder.AppendLine(@"");
        }

        public static void WriteHTMLEnd(HttpListenerContext c, StringBuilder builder) {
            builder.AppendLine(@"");

            builder.Append(
@"
    

"
            );
        }

        public static void Write(HttpListenerContext c, string str) {
            byte[] buf = CelesteNetUtils.UTF8NoBOM.GetBytes(str);
            c.Response.ContentLength64 = buf.Length;
            c.Response.OutputStream.Write(buf, 0, buf.Length);
        }

        #endregion

        #region Default RCEndPoint Handlers

        public static List EndPoints = new() {

                new RCEndPoint {
                    Path = "/",
                    Name = "Info",
                    InfoHTML = "Basic CelesteNet ClientRC info.",
                    Handle = c => {
                        StringBuilder builder = new();

                        WriteHTMLStart(c, builder);

                        builder.AppendLine(
@"
Info

    This weird website exists so that the main CelesteNet website can give your clients special commands.
    For example, clicking on the ""send key to client"" button will send it to /setname.
    It's only accessible from your local machine, meaning that nobody else can see this.

"
                        );

                        builder.AppendLine(@"");
                        builder.AppendLine(@"Endpoints");
                        foreach (RCEndPoint ep in EndPoints) {
                            builder.AppendLine(@"");
                            builder.AppendLine($@"{ep.Name}");
                            builder.AppendLine($@"{ep.PathHelp ?? ep.Path}{ep.InfoHTML}");
                            builder.AppendLine(@"");
                        }
                        builder.AppendLine(@"");

                        WriteHTMLEnd(c, builder);

                        Write(c, builder.ToString());
                    }
                },

                new RCEndPoint {
                    Path = "/404",
                    Name = "404",
                    InfoHTML = "Basic 404.",
                    Handle = c => {
                        c.Response.StatusCode = (int) HttpStatusCode.NotFound;
                        Write(c, "ERROR: Endpoint not found.");
                    }
                },

                new RCEndPoint {
                    Path = "/setkey",
                    PathHelp = "/setkey?value={key} (Example: ?value=1a2b3d4e)",
                    PathExample = "/setkey?value=Guest",
                    Name = "Set Key",
                    InfoHTML = "Set the key as the client name for the next connection.",
                    Handle = c => {
                        NameValueCollection data = ParseQueryString(c.Request.RawUrl);

                        string name = data["value"].Trim();
                        if (string.IsNullOrEmpty(name)) {
                            c.Response.StatusCode = (int) HttpStatusCode.BadRequest;
                            Write(c, $"ERROR: No value given.");
                            return;
                        }

                        CelesteNetClientModule.Settings.Name = "#" + name;
                        CelesteNetClientModule.Instance.SaveSettings();
                        Write(c, "OK");
                    }
                },

            };

        #endregion

    }
}