From 7b49817182918725947eddab02aa18a88d3fbb31 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Thu, 31 Jul 2025 16:59:03 +0900 Subject: [PATCH] Add WebSocket support for Chromium DevTools endpoints Implemented WebSocket communication in ChromiumDevTools to support 'page/' endpoints, allowing bidirectional messaging with Chromium DevTools Protocol. Added timeout configuration, improved error handling, and refactored WebSocketManager for connection pooling and reconnection. Updated resources and configuration files to support new timeout settings. Also fixed editor.html to handle direct JSON responses for citi-query. --- .../Properties/Resources.Designer.cs | 9 ++ .../Properties/Resources.resx | 3 + .../WelsonJS.Launcher/ResourceServer.cs | 5 - .../ResourceTools/ChromiumDevTools.cs | 105 ++++++++++++++-- .../WelsonJS.Launcher/WebSocketManager.cs | 116 ++++++++++++++---- WelsonJS.Toolkit/WelsonJS.Launcher/app.config | 1 + .../WelsonJS.Launcher/editor.html | 13 +- 7 files changed, 207 insertions(+), 45 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs index 24f5ca1..22454e5 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs @@ -114,6 +114,15 @@ namespace WelsonJS.Launcher.Properties { } } + /// + /// 5과(와) 유사한 지역화된 문자열을 찾습니다. + /// + internal static string ChromiumDevToolsTimeout { + get { + return ResourceManager.GetString("ChromiumDevToolsTimeout", resourceCulture); + } + } + /// /// msedge.exe과(와) 유사한 지역화된 문자열을 찾습니다. /// diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx index c619a4c..97efd4e 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx @@ -205,4 +205,7 @@ msedge.exe + + 5 + \ No newline at end of file diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs index 8b263d7..faef7fe 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs @@ -373,11 +373,6 @@ namespace WelsonJS.Launcher { data = xmlHeader + "\r\n" + data; } - else if (mimeType == "application/json") - { - data = xmlHeader + "\r\n"; - mimeType = "application/xml"; - } ServeResource(context, Encoding.UTF8.GetBytes(data), mimeType, statusCode); } diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/ChromiumDevTools.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/ChromiumDevTools.cs index 5914a40..733e01f 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/ChromiumDevTools.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/ChromiumDevTools.cs @@ -1,19 +1,24 @@ -// DevTools.cs +// ChromiumDevTools.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.IO; using System.Net; using System.Net.Http; +using System.Net.WebSockets; +using System.Text; +using System.Threading; using System.Threading.Tasks; namespace WelsonJS.Launcher.ResourceTools { public class ChromiumDevTools : IResourceTool { - private ResourceServer Server; + private readonly ResourceServer Server; private readonly HttpClient _httpClient; + private readonly WebSocketManager _wsManager = new WebSocketManager(); private const string Prefix = "devtools/"; public ChromiumDevTools(ResourceServer server, HttpClient httpClient) @@ -31,17 +36,99 @@ namespace WelsonJS.Launcher.ResourceTools { string endpoint = path.Substring(Prefix.Length); - try + if (endpoint.Equals("json", StringComparison.OrdinalIgnoreCase)) { - string url = Program.GetAppConfig("ChromiumDevToolsPrefix") + endpoint; - string data = await _httpClient.GetStringAsync(url); + try + { + 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"); + } + catch (Exception ex) + { + Server.ServeResource(context, $"Failed to process DevTools request. {EscapeXml(ex.Message)}", "application/xml", 500); + } + return; + } - Server.ServeResource(context, data, "application/json"); - } - catch (Exception ex) + if (endpoint.StartsWith("page/", StringComparison.OrdinalIgnoreCase)) { - Server.ServeResource(context, $"Failed to process DevTools request. {ex.Message}", "application/xml", 500); + // 기본 구성 + string baseUrl = Program.GetAppConfig("ChromiumDevToolsPrefix"); + if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out Uri uri)) + { + Server.ServeResource(context, "Invalid ChromiumDevToolsPrefix", "application/xml", 500); + return; + } + + string hostname = uri.Host; + int port = uri.Port; + + // 포트 덮어쓰기: ?port=1234 + string portQuery = context.Request.QueryString["port"]; + if (!string.IsNullOrEmpty(portQuery)) + { + int.TryParse(portQuery, out int parsedPort); + if (parsedPort > 0) port = parsedPort; + } + + // 타임아웃 처리 + int timeout = 5; + string timeoutConfig = Program.GetAppConfig("ChromiumDevToolsTimeout"); + if (!string.IsNullOrEmpty(timeoutConfig)) + int.TryParse(timeoutConfig, out timeout); + + // 경로 + string wsPath = "devtools/" + endpoint; + + // 본문 읽기 + string postBody; + using (var reader = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding)) + postBody = await reader.ReadToEndAsync(); + + ClientWebSocket ws; + try + { + ws = await _wsManager.GetOrCreateAsync(hostname, port, wsPath); + } + catch (Exception ex) + { + Server.ServeResource(context, $"WebSocket connection failed: {EscapeXml(ex.Message)}", "application/xml", 502); + return; + } + + try + { + var sendBuffer = Encoding.UTF8.GetBytes(postBody); + var sendToken = timeout == 0 ? CancellationToken.None : new CancellationTokenSource(TimeSpan.FromSeconds(timeout)).Token; + await ws.SendAsync(new ArraySegment(sendBuffer), WebSocketMessageType.Text, true, sendToken); + + var recvBuffer = new byte[4096]; + var recvToken = timeout == 0 ? CancellationToken.None : new CancellationTokenSource(TimeSpan.FromSeconds(timeout)).Token; + var result = await ws.ReceiveAsync(new ArraySegment(recvBuffer), recvToken); + + string response = Encoding.UTF8.GetString(recvBuffer, 0, result.Count); + Server.ServeResource(context, response, "application/json", 200); + } + catch (OperationCanceledException) + { + Server.ServeResource(context, "Timeout occurred", "application/xml", 504); + } + catch (Exception ex) + { + _wsManager.Remove(hostname, port, wsPath); + Server.ServeResource(context, $"WebSocket communication error: {EscapeXml(ex.Message)}", "application/xml", 500); + } + return; } + + Server.ServeResource(context, "Invalid DevTools endpoint", "application/xml", 404); + } + + private string EscapeXml(string text) + { + return WebUtility.HtmlEncode(text); } } } diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs index 4aba30c..fe22675 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs @@ -1,6 +1,13 @@ -using System; +// WebSocketManager.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.Concurrent; +using System.IO; using System.Net.WebSockets; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -8,46 +15,109 @@ namespace WelsonJS.Launcher { public class WebSocketManager { - private readonly ConcurrentDictionary _wsPool; - - public WebSocketManager() { - _wsPool = new ConcurrentDictionary(); + private class WebSocketEntry + { + public ClientWebSocket Socket { get; set; } + public string Host { get; set; } + public int Port { get; set; } + public string Path { get; set; } } - public async Task GetOrCreateAsync(int port) - { - if (_wsPool.TryGetValue(port, out var ws) && ws.State == WebSocketState.Open) - return ws; + private readonly ConcurrentDictionary _wsPool = new ConcurrentDictionary(); - if (ws != null) + private string MakeKey(string host, int port, string path) + { + return host + ":" + port + "/" + path; + } + + public async Task GetOrCreateAsync(string host, int port, string path) + { + string key = MakeKey(host, port, path); + + if (_wsPool.TryGetValue(key, out var entry) && entry.Socket?.State == WebSocketState.Open) + return entry.Socket; + + // 재연결 필요 + if (entry != null) { - _wsPool.TryRemove(port, out _); - ws.Dispose(); + _wsPool.TryRemove(key, out _); + entry.Socket?.Dispose(); } - var newWs = new ClientWebSocket(); - var uri = new Uri($"ws://localhost:{port}/ws"); + var ws = new ClientWebSocket(); + Uri uri = new Uri($"ws://{host}:{port}/{path}"); try { - await newWs.ConnectAsync(uri, CancellationToken.None); - _wsPool[port] = newWs; - return newWs; + await ws.ConnectAsync(uri, CancellationToken.None); + _wsPool[key] = new WebSocketEntry + { + Socket = ws, + Host = host, + Port = port, + Path = path + }; + return ws; } catch { - newWs.Dispose(); + ws.Dispose(); throw; } } - public void Remove(int port) + public void Remove(string host, int port, string path) { - if (_wsPool.TryRemove(port, out var ws)) + string key = MakeKey(host, port, path); + if (_wsPool.TryRemove(key, out var entry)) { - ws.Abort(); - ws.Dispose(); + entry.Socket?.Abort(); + entry.Socket?.Dispose(); } } + + public async Task SendWithReconnectAsync(string host, int port, string path, byte[] message, CancellationToken token) + { + ClientWebSocket ws; + + try + { + ws = await GetOrCreateAsync(host, port, path); + await ws.SendAsync(new ArraySegment(message), WebSocketMessageType.Text, true, token); + return true; + } + catch + { + Remove(host, port, path); + try + { + ws = await GetOrCreateAsync(host, port, path); + await ws.SendAsync(new ArraySegment(message), WebSocketMessageType.Text, true, token); + return true; + } + catch + { + Remove(host, port, path); + return false; + } + } + } + + public async Task SendAndReceiveAsync(string host, int port, string path, string message, int timeoutSeconds) + { + var buffer = Encoding.UTF8.GetBytes(message); + CancellationTokenSource cts = timeoutSeconds > 0 + ? new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)) + : new CancellationTokenSource(); + + if (!await SendWithReconnectAsync(host, port, path, buffer, cts.Token)) + throw new IOException("Failed to send after reconnect"); + + ClientWebSocket ws = await GetOrCreateAsync(host, port, path); + + byte[] recvBuffer = new byte[4096]; + WebSocketReceiveResult result = await ws.ReceiveAsync(new ArraySegment(recvBuffer), cts.Token); + return Encoding.UTF8.GetString(recvBuffer, 0, result.Count); + } } -} \ No newline at end of file +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/app.config b/WelsonJS.Toolkit/WelsonJS.Launcher/app.config index 59bea5f..8078a76 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/app.config +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/app.config @@ -6,6 +6,7 @@ + diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/editor.html b/WelsonJS.Toolkit/WelsonJS.Launcher/editor.html index 7c5c80b..7caded3 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/editor.html +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/editor.html @@ -590,10 +590,7 @@ const ip = encodeURIComponent(hostname.trim()); axios.get(`/citi-query/${hostname}`).then(response => { - const parser = new XMLParser(); - const result = parser.parse(response.data); - const data = JSON.parse(result.json); - if (!data) { + if (!response) { appendTextToEditor("\n// No data returned from Criminal IP."); return; } @@ -603,10 +600,10 @@ // network port data lines.push(`## Network ports:`); - if (data.port.data.length == 0) { + if (response.port.data.length == 0) { lines.push(`* No open ports found.`); } else { - data.port.data.forEach(x => { + response.port.data.forEach(x => { lines.push(`### ${x.open_port_no}/${x.socket}`); lines.push(`* Application: ${x.app_name} ${x.app_version}`); lines.push(`* Discovered hostnames: ${x.dns_names}`); @@ -616,10 +613,10 @@ // vulnerability data lines.push(`## Vulnerabilities:`); - if (data.vulnerability.data.length == 0) { + if (response.vulnerability.data.length == 0) { lines.push(`* No vulnerabilities found.`); } else { - data.vulnerability.data.forEach(x => { + response.vulnerability.data.forEach(x => { lines.push(`### ${x.cve_id}`); lines.push(`* ${x.cve_description}`); lines.push(`* CVSSV2 Score: ${x.cvssv2_score}`);