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')),
);
}