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.
This commit is contained in:
Namhyeon Go 2025-09-28 03:03:19 +09:00
parent b1b36744cf
commit 676f791fab
11 changed files with 485 additions and 46 deletions

View File

@ -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 ----------------
/// <summary>
/// Parses JSON once and stores it in the engine under a numeric id.
/// Returns the id which can be used for fast repeated extraction.
/// </summary>
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;
}
/// <summary>
/// Removes a previously loaded document from the engine store.
/// After this, the id becomes invalid.
/// </summary>
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);
}
/// <summary>
/// Replaces the stored JSON at a given id (parse once, reuse later).
/// </summary>
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);
}
/// <summary>
/// Stringifies the stored value identified by id (no reparse).
/// </summary>
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);
}
/// <summary>
/// Extracts from a stored document identified by id (no reparse).
/// Returns JSON of the selected value.
/// </summary>
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<p.length;i++){var k=p[i];");
sb.Append("if(Array.isArray(v) && typeof k==='number'){ v=v[k]; }");
sb.Append("else { v=(v==null?null:v[k]); }}");
sb.Append("return JSON.stringify(v);})()");
return _core.EvaluateToString(sb.ToString());
}
// Initialize the global store only once per JsSerializer instance/context
private void EnsureStore()
{
if (_storeReady) return;
// Create a single global dictionary: globalThis.__WJ_STORE
// Object.create(null) prevents prototype pollution and accidental hits on built-ins.
string script =
"(function(){var g=globalThis||this;" +
"if(!g.__WJ_STORE){Object.defineProperty(g,'__WJ_STORE',{value:Object.create(null),writable:false,enumerable:false,configurable:false});}" +
"return '1';})()";
string r = _core.EvaluateToString(script);
_storeReady = (r == "1");
if (!_storeReady) throw new InvalidOperationException("Failed to initialize the engine-backed JSON store.");
}
// ---------------- Existing API (kept for compatibility) ----------------
public bool IsValid(string json)
{
if (json == null) throw new ArgumentNullException("json");
@ -55,8 +153,8 @@ namespace WelsonJS.Launcher
}
/// <summary>
/// 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.
/// </summary>
public string Extract(string json, params string[] path)
{
@ -67,9 +165,8 @@ namespace WelsonJS.Launcher
}
/// <summary>
/// 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.
/// </summary>
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;

View File

@ -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<bool> 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, "<error>Not Found</error>", "application/xml", 404);
await ServeResource(context, "<error>Not Found</error>", "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 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
@ -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 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
@ -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)

View File

@ -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, $"<error>Failed to process DevTools request. {EscapeXml(ex.Message)}</error>", "application/xml", 500);
await Server.ServeResource(context, $"<error>Failed to process DevTools request. {EscapeXml(ex.Message)}</error>", "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, "<error>Invalid ChromiumDevToolsPrefix</error>", "application/xml", 500);
await Server.ServeResource(context, "<error>Invalid ChromiumDevToolsPrefix</error>", "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, $"<error>WebSocket communication error: {EscapeXml(ex.Message)}</error>", "application/xml", 500);
await Server.ServeResource(context, $"<error>WebSocket communication error: {EscapeXml(ex.Message)}</error>", "application/xml", 500);
}
return;
}
Server.ServeResource(context, "<error>Invalid DevTools endpoint</error>", "application/xml", 404);
await Server.ServeResource(context, "<error>Invalid DevTools endpoint</error>", "application/xml", 404);
}
private string EscapeXml(string text)

View File

@ -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, $"<error>Failed to try completion. {ex.Message}</error>", "application/xml", 500);
await Server.ServeResource(context, $"<error>Failed to try completion. {ex.Message}</error>", "application/xml", 500);
}
}

View File

@ -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, "<error>Invalid query parameter</error>", "application/xml", 400);
await Server.ServeResource(context, "<error>Invalid query parameter</error>", "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, $"<error>Failed to process DNS query. {ex.Message}</error>", "application/xml", 500);
await Server.ServeResource(context, $"<error>Failed to process DNS query. {ex.Message}</error>", "application/xml", 500);
}
}

View File

@ -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
{
/// <summary>
/// POST image-color-picker/ with a unified JSON body:
/// { "image": "<B64>", "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<string, object> → JsSerializer.Serialize(...).
/// </summary>
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<string, object>
{
["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<string, object>
{
["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": <num>, "y": <num> }
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<string, object>
{
["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<string, object>
{
["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<string, object>
{
["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<string, object>
{
["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<string, object>
{
["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<string, object>
{
["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<string, object>
{
["ok"] = true,
["hex"] = hex,
["name"] = name, // may be null → serializer will emit "name": null
["rgb"] = new Dictionary<string, object>
{
["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<string, object>
{
["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<string, object> 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);
}
}
}
}

View File

@ -38,7 +38,7 @@ namespace WelsonJS.Launcher.ResourceTools
string apiKey = Program.GetAppConfig("CriminalIpApiKey");
if (string.IsNullOrEmpty(apiKey))
{
Server.ServeResource(context, "<error>Missing API key</error>", "application/xml", 500);
await Server.ServeResource(context, "<error>Missing API key</error>", "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, $"<error>{ex.Message}</error>", "application/xml", 500);
await Server.ServeResource(context, $"<error>{ex.Message}</error>", "application/xml", 500);
}
}
}

View File

@ -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");
}
}
}

View File

@ -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);
}
/// <summary>

View File

@ -37,7 +37,7 @@ namespace WelsonJS.Launcher.ResourceTools
if (string.IsNullOrWhiteSpace(query) || query.Length > 255)
{
Server.ServeResource(context, "<error>Invalid query parameter</error>", "application/xml", 400);
await Server.ServeResource(context, "<error>Invalid query parameter</error>", "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, $"<error>Failed to process WHOIS request. {ex.Message}</error>", "application/xml", 500);
await Server.ServeResource(context, $"<error>Failed to process WHOIS request. {ex.Message}</error>", "application/xml", 500);
}
}
}

View File

@ -92,6 +92,7 @@
<Compile Include="JsNative.cs" />
<Compile Include="JsSerializer.cs" />
<Compile Include="NativeBootstrap.cs" />
<Compile Include="ResourceTools\ImageColorPicker.cs" />
<Compile Include="ResourceTools\IpQuery.cs" />
<Compile Include="ResourceTools\Settings.cs" />
<Compile Include="ResourceTools\Completion.cs" />