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..857c2ac 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/ChromiumDevTools.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/ChromiumDevTools.cs @@ -1,19 +1,25 @@ -// 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.Security; +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 +37,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 SecurityElement.Escape(text); } } } diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs index 4aba30c..43c8e6f 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WebSocketManager.cs @@ -1,6 +1,14 @@ -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.Security.Cryptography; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -8,46 +16,115 @@ 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) + { + // To create a unique key for the WebSocket connection + string input = host + ":" + port + "/" + path; + using (var md5 = MD5.Create()) { - _wsPool.TryRemove(port, out _); - ws.Dispose(); + byte[] hash = md5.ComputeHash(Encoding.UTF8.GetBytes(input)); + return BitConverter.ToString(hash).Replace("-", "").ToLower(); + } + } + + 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(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, int bufferSize = 65536) + { + 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[bufferSize]; + 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..ec3ecdb 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/editor.html +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/editor.html @@ -34,11 +34,21 @@ } .banner { - text-align: center; padding: 10px; background-color: #f1f1f1; font-size: 14px; } + + .banner a { + text-decoration: none; + color: #000; + } + + .banner a:hover { + text-decoration: none; + font-weight: bold; + color: inherit; /* keeps contrast consistent */ + } @@ -590,10 +600,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 +610,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 +623,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}`); @@ -663,7 +670,7 @@ _e(Editor, { editorRef }), _e(PromptEditor, { promptEditorRef, promptMessagesRef }) ), - _e('div', { className: 'banner' }, _e('a', { href: 'https://github.com/gnh1201/welsonjs' }, 'WelsonJS'), ' Editor powered by Metro UI, Monaco Editor, and JSONEditor.'), + _e('div', { className: 'banner' }, _e('a', { href: 'https://github.com/gnh1201/welsonjs' }, '❤️ Contribute this project')), ); }