From 676f791fab26916d696321e47b2ddd138acdb4e0 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sun, 28 Sep 2025 03:03:19 +0900 Subject: [PATCH] Add ImageColorPicker tool and async resource serving Introduces the ImageColorPicker resource tool for extracting color information from images via a POST API. Refactors ResourceServer and all resource tools to use async ServeResource methods, improving scalability and consistency. Updates JsSerializer with an engine-backed document store for efficient repeated JSON extraction. --- .../WelsonJS.Launcher/JsSerializer.cs | 113 +++++- .../WelsonJS.Launcher/ResourceServer.cs | 26 +- .../ResourceTools/ChromiumDevTools.cs | 12 +- .../ResourceTools/Completion.cs | 4 +- .../ResourceTools/DnsQuery.cs | 8 +- .../ResourceTools/ImageColorPicker.cs | 343 ++++++++++++++++++ .../ResourceTools/IpQuery.cs | 6 +- .../ResourceTools/Settings.cs | 2 +- .../ResourceTools/TwoFactorAuth.cs | 10 +- .../WelsonJS.Launcher/ResourceTools/Whois.cs | 6 +- .../WelsonJS.Launcher.csproj | 1 + 11 files changed, 485 insertions(+), 46 deletions(-) create mode 100644 WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/ImageColorPicker.cs diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs index 5eddd2d..8261edc 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors // https://github.com/gnh1201/welsonjs -// +// using System; using System.Collections; using System.Collections.Generic; @@ -16,6 +16,10 @@ namespace WelsonJS.Launcher private readonly JsCore _core; private readonly bool _ownsCore; + // In-engine parsed document store management + private bool _storeReady; + private int _nextId = 1; // 0 is reserved (unused) + public JsSerializer() : this(new JsCore(), true) { } public JsSerializer(JsCore core, bool ownsCore) @@ -25,6 +29,100 @@ namespace WelsonJS.Launcher _ownsCore = ownsCore; } + // ---------------- Engine-backed document store ---------------- + + /// + /// Parses JSON once and stores it in the engine under a numeric id. + /// Returns the id which can be used for fast repeated extraction. + /// + public int Load(string json) + { + if (json == null) throw new ArgumentNullException("json"); + EnsureStore(); + + int id = _nextId++; + // Create slot and parse + // Using Object.create(null) for a clean dictionary without prototype. + var sb = new StringBuilder(); + sb.Append("(function(){var S=globalThis.__WJ_STORE;"); + sb.Append("S[").Append(id.ToString(CultureInfo.InvariantCulture)).Append("]=JSON.parse(").Append(Q(json)).Append(");"); + sb.Append("return '1';})()"); + string r = _core.EvaluateToString(sb.ToString()); + if (r != "1") throw new InvalidOperationException("Failed to load JSON into the engine store."); + return id; + } + + /// + /// Removes a previously loaded document from the engine store. + /// After this, the id becomes invalid. + /// + public void Unload(int id) + { + EnsureStore(); + string script = "(function(){var S=globalThis.__WJ_STORE; delete S[" + id.ToString(CultureInfo.InvariantCulture) + "]; return '1';})()"; + _core.EvaluateToString(script); + } + + /// + /// Replaces the stored JSON at a given id (parse once, reuse later). + /// + public void Reload(int id, string json) + { + if (json == null) throw new ArgumentNullException("json"); + EnsureStore(); + string script = "(function(){var S=globalThis.__WJ_STORE; S[" + id.ToString(CultureInfo.InvariantCulture) + "]=JSON.parse(" + Q(json) + "); return '1';})()"; + _core.EvaluateToString(script); + } + + /// + /// Stringifies the stored value identified by id (no reparse). + /// + public string ToJson(int id, int space) + { + EnsureStore(); + space = Clamp(space, 0, 10); + string script = "(function(){var v=globalThis.__WJ_STORE[" + id.ToString(CultureInfo.InvariantCulture) + "]; return JSON.stringify(v,null," + space.ToString(CultureInfo.InvariantCulture) + ");})()"; + return _core.EvaluateToString(script); + } + + /// + /// Extracts from a stored document identified by id (no reparse). + /// Returns JSON of the selected value. + /// + public string ExtractFrom(int id, params object[] path) + { + EnsureStore(); + if (path == null) path = new object[0]; + string jsPath = BuildJsPath(path); + + var sb = new StringBuilder(); + sb.Append("(function(){var v=globalThis.__WJ_STORE[") + .Append(id.ToString(CultureInfo.InvariantCulture)) + .Append("];var p=").Append(jsPath).Append(";"); + sb.Append("for(var i=0;i - /// Extracts a value by a simple path of property names (numeric segment as string = array index). - /// Returns the selected value as a JSON string. + /// Extracts by string-only path (kept for backward compatibility). + /// Internally forwards to the mixed-path overload. /// public string Extract(string json, params string[] path) { @@ -67,9 +165,8 @@ namespace WelsonJS.Launcher } /// - /// Extracts by a mixed path. Segments can be strings (object keys) or integers (array indices). - /// Returns the selected value as a JSON string (e.g., a JS string returns with quotes). - /// Usage: Extract(json, "items", 0, "name") + /// Extracts by a mixed path directly from a JSON string (parses every call). + /// Prefer Load(...) + ExtractFrom(...) to avoid repeated parsing. /// public string Extract(string json, params object[] path) { @@ -96,6 +193,8 @@ namespace WelsonJS.Launcher return _core.EvaluateToString(script); } + // ---------------- Helpers ---------------- + private static int Clamp(int v, int min, int max) { if (v < min) return min; @@ -292,4 +391,4 @@ namespace WelsonJS.Launcher } } } -} \ No newline at end of file +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs index be77132..e9830b0 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs @@ -67,6 +67,7 @@ namespace WelsonJS.Launcher _tools.Add(new ResourceTools.IpQuery(this, _httpClient, _logger)); _tools.Add(new ResourceTools.TwoFactorAuth(this, _httpClient, _logger)); _tools.Add(new ResourceTools.Whois(this, _httpClient, _logger)); + _tools.Add(new ResourceTools.ImageColorPicker(this, _httpClient, _logger)); // Register the prefix _listener.Prefixes.Add(prefix); @@ -132,14 +133,14 @@ namespace WelsonJS.Launcher // Serve from a resource name if (String.IsNullOrEmpty(path)) { - ServeResource(context, GetResource(_resourceName), "text/html"); + await ServeResource(context, GetResource(_resourceName), "text/html"); return; } // Serve the favicon.ico file if ("favicon.ico".Equals(path, StringComparison.OrdinalIgnoreCase)) { - ServeResource(context, GetResource("favicon"), "image/x-icon"); + await ServeResource(context, GetResource("favicon"), "image/x-icon"); return; } @@ -157,7 +158,7 @@ namespace WelsonJS.Launcher if (await ServeBlob(context, path)) return; // Fallback to serve from a resource name - ServeResource(context, GetResource(_resourceName), "text/html"); + await ServeResource(context, GetResource(_resourceName), "text/html"); } private async Task ServeBlob(HttpListenerContext context, string path, string prefix = null) @@ -188,7 +189,7 @@ namespace WelsonJS.Launcher data = await response.Content.ReadAsByteArrayAsync(); mimeType = response.Content.Headers.ContentType?.MediaType ?? _defaultMimeType; - ServeResource(context, data, mimeType); + await ServeResource(context, data, mimeType); _ = TrySaveCachedBlob(path, data, mimeType); return true; @@ -211,7 +212,7 @@ namespace WelsonJS.Launcher mimeType = _defaultMimeType; } - ServeResource(context, data, mimeType); + await ServeResource(context, data, mimeType); return true; } } @@ -345,12 +346,12 @@ namespace WelsonJS.Launcher } } - public void ServeResource(HttpListenerContext context) + public async Task ServeResource(HttpListenerContext context) { - ServeResource(context, "Not Found", "application/xml", 404); + await ServeResource(context, "Not Found", "application/xml", 404); } - public void ServeResource(HttpListenerContext context, byte[] data, string mimeType = "text/html", int statusCode = 200) + public async Task ServeResource(HttpListenerContext context, byte[] data, string mimeType = "text/html", int statusCode = 200) { string xmlHeader = ""; @@ -364,13 +365,10 @@ namespace WelsonJS.Launcher context.Response.StatusCode = statusCode; context.Response.ContentType = mimeType; context.Response.ContentLength64 = data.Length; - using (Stream outputStream = context.Response.OutputStream) - { - outputStream.Write(data, 0, data.Length); - } + await context.Response.OutputStream.WriteAsync(data, 0, data.Length); } - public void ServeResource(HttpListenerContext context, string data, string mimeType = "text/html", int statusCode = 200) + public async Task ServeResource(HttpListenerContext context, string data, string mimeType = "text/html", int statusCode = 200) { string xmlHeader = ""; @@ -385,7 +383,7 @@ namespace WelsonJS.Launcher data = xmlHeader + "\r\n" + data; } - ServeResource(context, Encoding.UTF8.GetBytes(data), mimeType, statusCode); + await ServeResource(context, Encoding.UTF8.GetBytes(data), mimeType, statusCode); } private byte[] GetResource(string resourceName) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/ChromiumDevTools.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/ChromiumDevTools.cs index d56c5ec..b06368e 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/ChromiumDevTools.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/ChromiumDevTools.cs @@ -47,11 +47,11 @@ namespace WelsonJS.Launcher.ResourceTools string baseUrl = Program.GetAppConfig("ChromiumDevToolsPrefix"); // e.g., http://localhost:9222/ string url = baseUrl.TrimEnd('/') + "/" + endpoint; string data = await _httpClient.GetStringAsync(url); - Server.ServeResource(context, data, "application/json"); + await Server.ServeResource(context, data, "application/json"); } catch (Exception ex) { - Server.ServeResource(context, $"Failed to process DevTools request. {EscapeXml(ex.Message)}", "application/xml", 500); + await Server.ServeResource(context, $"Failed to process DevTools request. {EscapeXml(ex.Message)}", "application/xml", 500); } return; } @@ -62,7 +62,7 @@ namespace WelsonJS.Launcher.ResourceTools string baseUrl = Program.GetAppConfig("ChromiumDevToolsPrefix"); if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out Uri uri)) { - Server.ServeResource(context, "Invalid ChromiumDevToolsPrefix", "application/xml", 500); + await Server.ServeResource(context, "Invalid ChromiumDevToolsPrefix", "application/xml", 500); return; } @@ -97,17 +97,17 @@ namespace WelsonJS.Launcher.ResourceTools try { string response = await _wsManager.SendAndReceiveAsync(hostname, port, wsPath, postBody, timeout); - Server.ServeResource(context, response, "application/json", 200); + await Server.ServeResource(context, response, "application/json", 200); } catch (Exception ex) { _wsManager.Remove(hostname, port, wsPath); - Server.ServeResource(context, $"WebSocket communication error: {EscapeXml(ex.Message)}", "application/xml", 500); + await Server.ServeResource(context, $"WebSocket communication error: {EscapeXml(ex.Message)}", "application/xml", 500); } return; } - Server.ServeResource(context, "Invalid DevTools endpoint", "application/xml", 404); + await Server.ServeResource(context, "Invalid DevTools endpoint", "application/xml", 404); } private string EscapeXml(string text) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Completion.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Completion.cs index 34abe58..ea11b3b 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Completion.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Completion.cs @@ -72,11 +72,11 @@ namespace WelsonJS.Launcher.ResourceTools )) ); - Server.ServeResource(context, response.ToString(), "application/xml"); + await Server.ServeResource(context, response.ToString(), "application/xml"); } catch (Exception ex) { - Server.ServeResource(context, $"Failed to try completion. {ex.Message}", "application/xml", 500); + await Server.ServeResource(context, $"Failed to try completion. {ex.Message}", "application/xml", 500); } } diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/DnsQuery.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/DnsQuery.cs index 831294b..1522b75 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/DnsQuery.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/DnsQuery.cs @@ -42,13 +42,11 @@ namespace WelsonJS.Launcher.ResourceTools public async Task HandleAsync(HttpListenerContext context, string path) { - await Task.Delay(0); - string query = path.Substring(Prefix.Length); if (string.IsNullOrWhiteSpace(query) || query.Length > 255) { - Server.ServeResource(context, "Invalid query parameter", "application/xml", 400); + await Server.ServeResource(context, "Invalid query parameter", "application/xml", 400); return; } @@ -67,11 +65,11 @@ namespace WelsonJS.Launcher.ResourceTools } string data = result.ToString(); - Server.ServeResource(context, data, "text/plain", 200); + await Server.ServeResource(context, data, "text/plain", 200); } catch (Exception ex) { - Server.ServeResource(context, $"Failed to process DNS query. {ex.Message}", "application/xml", 500); + await Server.ServeResource(context, $"Failed to process DNS query. {ex.Message}", "application/xml", 500); } } diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/ImageColorPicker.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/ImageColorPicker.cs new file mode 100644 index 0000000..bfff778 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/ImageColorPicker.cs @@ -0,0 +1,343 @@ +// ImageColorPicker.cs +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors +// https://github.com/gnh1201/welsonjs +// +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Globalization; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace WelsonJS.Launcher.ResourceTools +{ + /// + /// POST image-color-picker/ with a unified JSON body: + /// { "image": "", "point": { "x": 10, "y": 20 } } + /// + /// Response (success): + /// { "ok": true, "hex": "#RRGGBB", "name": "white", "rgb": { "r": 255, "g": 255, "b": 255 } } + /// + /// Response (error): + /// { "ok": false, "error": "..." } + /// + /// JSON handling: + /// - Request parse: JsSerializer.Load/ExtractFrom (engine) + tiny local decoders for literals. + /// - Response encode: Dictionary → JsSerializer.Serialize(...). + /// + public class ImageColorPicker : IResourceTool + { + private readonly ResourceServer Server; + private readonly HttpClient _httpClient; + private readonly ICompatibleLogger _logger; + private const string Prefix = "image-color-picker"; + + public ImageColorPicker(ResourceServer server, HttpClient httpClient, ICompatibleLogger logger) + { + Server = server; + + _httpClient = httpClient; + _logger = logger; + } + + public bool CanHandle(string path) + { + return path.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase); + } + + public async Task HandleAsync(HttpListenerContext context, string path) + { + context.Response.ContentType = "application/json; charset=utf-8"; + + try + { + if (!string.Equals(context.Request.HttpMethod, "POST", StringComparison.OrdinalIgnoreCase)) + { + context.Response.StatusCode = (int)HttpStatusCode.MethodNotAllowed; + await WriteJsonAsync(context, new Dictionary + { + ["ok"] = false, + ["error"] = "Only POST is supported." + }); + return; + } + + // ---- Read request body --------------------------------------------------------- + string body; + using (var reader = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding ?? Encoding.UTF8)) + body = await reader.ReadToEndAsync(); + + if (string.IsNullOrWhiteSpace(body)) + { + context.Response.StatusCode = (int)HttpStatusCode.BadRequest; + await WriteJsonAsync(context, new Dictionary + { + ["ok"] = false, + ["error"] = "Empty request body." + }); + return; + } + + // ---- Extract strictly in unified format --------------------------------------- + string imageJson, xJson, yJson; + using (var ser = new JsSerializer()) + { + int id = ser.Load(body); + + // Required: "image" as JSON string + imageJson = ser.ExtractFrom(id, "image"); + + // Required: "point": { "x": , "y": } + xJson = ser.ExtractFrom(id, "point", "x"); + yJson = ser.ExtractFrom(id, "point", "y"); + + ser.Unload(id); + } + + if (string.IsNullOrEmpty(imageJson)) + { + context.Response.StatusCode = (int)HttpStatusCode.BadRequest; + await WriteJsonAsync(context, new Dictionary + { + ["ok"] = false, + ["error"] = "Missing required field: 'image' (base64 string)." + }); + return; + } + if (string.IsNullOrEmpty(xJson) || string.IsNullOrEmpty(yJson)) + { + context.Response.StatusCode = (int)HttpStatusCode.BadRequest; + await WriteJsonAsync(context, new Dictionary + { + ["ok"] = false, + ["error"] = "Missing required field: 'point' with numeric 'x' and 'y'." + }); + return; + } + + // ---- Convert JSON literals → CLR ---------------------------------------------- + string imageB64; + if (!TryParseJsonString(imageJson, out imageB64) || string.IsNullOrWhiteSpace(imageB64)) + { + context.Response.StatusCode = (int)HttpStatusCode.BadRequest; + await WriteJsonAsync(context, new Dictionary + { + ["ok"] = false, + ["error"] = "'image' must be a non-empty JSON string." + }); + return; + } + + if (!TryParseJsonInt(xJson, out var x) || !TryParseJsonInt(yJson, out var y)) + { + context.Response.StatusCode = (int)HttpStatusCode.BadRequest; + await WriteJsonAsync(context, new Dictionary + { + ["ok"] = false, + ["error"] = "'point.x' and 'point.y' must be JSON numbers." + }); + return; + } + + // Allow data URL prefix (data:*;base64,....) + imageB64 = StripDataUrlPrefix(imageB64); + + byte[] bytes; + try { bytes = Convert.FromBase64String(imageB64); } + catch + { + context.Response.StatusCode = (int)HttpStatusCode.BadRequest; + await WriteJsonAsync(context, new Dictionary + { + ["ok"] = false, + ["error"] = "Invalid base64 image data." + }); + return; + } + + // ---- Decode image and sample pixel -------------------------------------------- + using (var ms = new MemoryStream(bytes, writable: false)) + using (var bmp = new Bitmap(ms)) + { + if (x < 0 || y < 0 || x >= bmp.Width || y >= bmp.Height) + { + context.Response.StatusCode = (int)HttpStatusCode.BadRequest; + await WriteJsonAsync(context, new Dictionary + { + ["ok"] = false, + ["error"] = $"Point out of bounds. Image size: {bmp.Width}x{bmp.Height}, requested: ({x},{y})." + }); + return; + } + + var color = bmp.GetPixel(x, y); + var hex = $"#{color.R:X2}{color.G:X2}{color.B:X2}"; + var name = ToCommonColorName(color); // null if not close to a known color + + context.Response.StatusCode = (int)HttpStatusCode.OK; + await WriteJsonAsync(context, new Dictionary + { + ["ok"] = true, + ["hex"] = hex, + ["name"] = name, // may be null → serializer will emit "name": null + ["rgb"] = new Dictionary + { + ["r"] = color.R, + ["g"] = color.G, + ["b"] = color.B + } + }); + } + } + catch (Exception ex) + { + _logger?.Error($"ImageColorPicker error: {ex}"); + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + await WriteJsonAsync(context, new Dictionary + { + ["ok"] = false, + ["error"] = "Internal error while processing image." + }); + } + finally + { + try { context.Response.OutputStream.Flush(); } catch { } + try { context.Response.OutputStream.Close(); } catch { } + } + } + + // ---------------------------- Helpers ----------------------------------------------- + + private static bool TryParseJsonInt(string jsonNumberLiteral, out int value) + { + // Accept JSON numbers like: 12, -3, 10.0, 1e2, -4E+1 + // Strategy: parse as double using invariant culture, then cast if within int range. + value = default; + + if (string.IsNullOrWhiteSpace(jsonNumberLiteral)) + return false; + + var s = jsonNumberLiteral.Trim(); + + // Reject if wrapped in quotes (should be a number literal, not a string) + if (s.Length >= 2 && s[0] == '"' && s[s.Length - 1] == '"') + return false; + + if (double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var d)) + { + if (d >= int.MinValue && d <= int.MaxValue) + { + value = (int)d; // truncate toward zero (JSON has no ints; this is reasonable) + return true; + } + } + return false; + } + + private static bool TryParseJsonString(string jsonStringLiteral, out string value) + { + value = null; + if (string.IsNullOrWhiteSpace(jsonStringLiteral)) + return false; + + var s = jsonStringLiteral.Trim(); + if (s.Length < 2 || s[0] != '"' || s[s.Length - 1] != '"') + return false; + + // Remove surrounding quotes and unescape JSON sequences + value = JsonUnescape(s.Substring(1, s.Length - 2)); + return true; + } + + private static string JsonUnescape(string s) + { + var sb = new StringBuilder(s.Length); + for (int i = 0; i < s.Length; i++) + { + char c = s[i]; + if (c != '\\') { sb.Append(c); continue; } + + if (i + 1 >= s.Length) { sb.Append('\\'); break; } + char n = s[++i]; + switch (n) + { + case '\"': sb.Append('\"'); break; + case '\\': sb.Append('\\'); break; + case '/': sb.Append('/'); break; + case 'b': sb.Append('\b'); break; + case 'f': sb.Append('\f'); break; + case 'n': sb.Append('\n'); break; + case 'r': sb.Append('\r'); break; + case 't': sb.Append('\t'); break; + case 'u': + if (i + 4 < s.Length) + { + string hex = s.Substring(i + 1, 4); + if (ushort.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var cp)) + { + sb.Append((char)cp); + i += 4; + } + else sb.Append('u'); + } + else sb.Append('u'); + break; + default: + sb.Append(n); + break; + } + } + return sb.ToString(); + } + + private static string StripDataUrlPrefix(string b64) + { + var idx = b64.IndexOf("base64,", StringComparison.OrdinalIgnoreCase); + return idx >= 0 ? b64.Substring(idx + "base64,".Length) : b64; + } + + private static string ToCommonColorName(Color c) + { + (string name, Color color)[] palette = + { + ("white", Color.FromArgb(255,255,255)), + ("black", Color.FromArgb(0,0,0)), + ("red", Color.FromArgb(255,0,0)), + ("green", Color.FromArgb(0,128,0)), + ("blue", Color.FromArgb(0,0,255)), + ("yellow", Color.FromArgb(255,255,0)), + ("cyan", Color.FromArgb(0,255,255)), + ("magenta", Color.FromArgb(255,0,255)), + ("gray", Color.FromArgb(128,128,128)) + }; + + const int tolerance = 20; // distance threshold + string best = null; + int bestDist = int.MaxValue; + + foreach (var (name, col) in palette) + { + int dr = c.R - col.R, dg = c.G - col.G, db = c.B - col.B; + int dist = dr * dr + dg * dg + db * db; + if (dist < bestDist) { bestDist = dist; best = name; } + } + + return bestDist <= tolerance * tolerance ? best : null; + } + + private async Task WriteJsonAsync(HttpListenerContext ctx, Dictionary doc, int space = 0) + { + using (var ser = new JsSerializer()) + { + string json = ser.Serialize(doc, space); + byte[] payload = Encoding.UTF8.GetBytes(json); + + await Server.ServeResource(ctx, payload, "application/json", 200); + } + } + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/IpQuery.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/IpQuery.cs index 68d7400..42475c2 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/IpQuery.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/IpQuery.cs @@ -38,7 +38,7 @@ namespace WelsonJS.Launcher.ResourceTools string apiKey = Program.GetAppConfig("CriminalIpApiKey"); if (string.IsNullOrEmpty(apiKey)) { - Server.ServeResource(context, "Missing API key", "application/xml", 500); + await Server.ServeResource(context, "Missing API key", "application/xml", 500); return; } @@ -54,11 +54,11 @@ namespace WelsonJS.Launcher.ResourceTools string content = await response.Content.ReadAsStringAsync(); context.Response.StatusCode = (int)response.StatusCode; - Server.ServeResource(context, content, "application/json", (int)response.StatusCode); + await Server.ServeResource(context, content, "application/json", (int)response.StatusCode); } catch (Exception ex) { - Server.ServeResource(context, $"{ex.Message}", "application/xml", 500); + await Server.ServeResource(context, $"{ex.Message}", "application/xml", 500); } } } diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Settings.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Settings.cs index 6d6fd94..9be9759 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Settings.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Settings.cs @@ -79,7 +79,7 @@ namespace WelsonJS.Launcher.ResourceTools finalConfig.Select(kv => new XElement(kv.Key, kv.Value)) ); - Server.ServeResource(context, xml.ToString(), "application/xml"); + await Server.ServeResource(context, xml.ToString(), "application/xml"); } } } diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/TwoFactorAuth.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/TwoFactorAuth.cs index aa44ba6..ed0832a 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/TwoFactorAuth.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/TwoFactorAuth.cs @@ -53,7 +53,7 @@ namespace WelsonJS.Launcher.ResourceTools length = parsed; } - Server.ServeResource(context, GetPubKey(length), "text/plain", 200); + await Server.ServeResource(context, GetPubKey(length), "text/plain", 200); return; } @@ -71,7 +71,7 @@ namespace WelsonJS.Launcher.ResourceTools var parsed = ParseFormEncoded(body); if (!parsed.TryGetValue("secret", out string secret) || string.IsNullOrWhiteSpace(secret)) { - Server.ServeResource(context, "missing 'secret' parameter", "text/plain", 400); + await Server.ServeResource(context, "missing 'secret' parameter", "text/plain", 400); return; } @@ -79,17 +79,17 @@ namespace WelsonJS.Launcher.ResourceTools { int otp = GetOtp(secret.Trim()); string otp6 = otp.ToString("D6"); // always 6 digits - Server.ServeResource(context, otp6, "text/plain", 200); + await Server.ServeResource(context, otp6, "text/plain", 200); } catch (Exception ex) { - Server.ServeResource(context, $"invalid secret: {ex.Message}", "text/plain", 400); + await Server.ServeResource(context, $"invalid secret: {ex.Message}", "text/plain", 400); } return; } // Default: not found - Server.ServeResource(context, "not found", "text/plain", 404); + await Server.ServeResource(context, "not found", "text/plain", 404); } /// diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Whois.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Whois.cs index fc82e85..463fb04 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Whois.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Whois.cs @@ -37,7 +37,7 @@ namespace WelsonJS.Launcher.ResourceTools if (string.IsNullOrWhiteSpace(query) || query.Length > 255) { - Server.ServeResource(context, "Invalid query parameter", "application/xml", 400); + await Server.ServeResource(context, "Invalid query parameter", "application/xml", 400); return; } @@ -57,11 +57,11 @@ namespace WelsonJS.Launcher.ResourceTools HttpResponseMessage response = await _httpClient.SendAsync(request); string responseBody = await response.Content.ReadAsStringAsync(); - Server.ServeResource(context, responseBody, "text/plain", (int)response.StatusCode); + await Server.ServeResource(context, responseBody, "text/plain", (int)response.StatusCode); } catch (Exception ex) { - Server.ServeResource(context, $"Failed to process WHOIS request. {ex.Message}", "application/xml", 500); + await Server.ServeResource(context, $"Failed to process WHOIS request. {ex.Message}", "application/xml", 500); } } } diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj index 0d5a5fc..229b09b 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj @@ -92,6 +92,7 @@ +