mirror of
https://github.com/gnh1201/welsonjs.git
synced 2025-11-27 18:11:20 +00:00
Merge pull request #303 from gnh1201/dev
WebSocket support for DevTools protocol, and minor fixes
This commit is contained in:
commit
147ef3c21b
|
|
@ -114,6 +114,15 @@ namespace WelsonJS.Launcher.Properties {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 5과(와) 유사한 지역화된 문자열을 찾습니다.
|
||||||
|
/// </summary>
|
||||||
|
internal static string ChromiumDevToolsTimeout {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ChromiumDevToolsTimeout", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// msedge.exe과(와) 유사한 지역화된 문자열을 찾습니다.
|
/// msedge.exe과(와) 유사한 지역화된 문자열을 찾습니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -205,4 +205,7 @@
|
||||||
<data name="ChromiumFileName" xml:space="preserve">
|
<data name="ChromiumFileName" xml:space="preserve">
|
||||||
<value>msedge.exe</value>
|
<value>msedge.exe</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ChromiumDevToolsTimeout" xml:space="preserve">
|
||||||
|
<value>5</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -373,11 +373,6 @@ namespace WelsonJS.Launcher
|
||||||
{
|
{
|
||||||
data = xmlHeader + "\r\n" + data;
|
data = xmlHeader + "\r\n" + data;
|
||||||
}
|
}
|
||||||
else if (mimeType == "application/json")
|
|
||||||
{
|
|
||||||
data = xmlHeader + "\r\n<json><![CDATA[" + data + "]]></json>";
|
|
||||||
mimeType = "application/xml";
|
|
||||||
}
|
|
||||||
|
|
||||||
ServeResource(context, Encoding.UTF8.GetBytes(data), mimeType, statusCode);
|
ServeResource(context, Encoding.UTF8.GetBytes(data), mimeType, statusCode);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,25 @@
|
||||||
// DevTools.cs
|
// ChromiumDevTools.cs
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors
|
// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors
|
||||||
// https://github.com/gnh1201/welsonjs
|
// https://github.com/gnh1201/welsonjs
|
||||||
//
|
//
|
||||||
using System;
|
using System;
|
||||||
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Security;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace WelsonJS.Launcher.ResourceTools
|
namespace WelsonJS.Launcher.ResourceTools
|
||||||
{
|
{
|
||||||
public class ChromiumDevTools : IResourceTool
|
public class ChromiumDevTools : IResourceTool
|
||||||
{
|
{
|
||||||
private ResourceServer Server;
|
private readonly ResourceServer Server;
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly WebSocketManager _wsManager = new WebSocketManager();
|
||||||
private const string Prefix = "devtools/";
|
private const string Prefix = "devtools/";
|
||||||
|
|
||||||
public ChromiumDevTools(ResourceServer server, HttpClient httpClient)
|
public ChromiumDevTools(ResourceServer server, HttpClient httpClient)
|
||||||
|
|
@ -31,17 +37,99 @@ namespace WelsonJS.Launcher.ResourceTools
|
||||||
{
|
{
|
||||||
string endpoint = path.Substring(Prefix.Length);
|
string endpoint = path.Substring(Prefix.Length);
|
||||||
|
|
||||||
try
|
if (endpoint.Equals("json", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
string url = Program.GetAppConfig("ChromiumDevToolsPrefix") + endpoint;
|
try
|
||||||
string data = await _httpClient.GetStringAsync(url);
|
{
|
||||||
|
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, $"<error>Failed to process DevTools request. {EscapeXml(ex.Message)}</error>", "application/xml", 500);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Server.ServeResource(context, data, "application/json");
|
if (endpoint.StartsWith("page/", StringComparison.OrdinalIgnoreCase))
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
{
|
||||||
Server.ServeResource(context, $"<error>Failed to process DevTools request. {ex.Message}</error>", "application/xml", 500);
|
// 기본 구성
|
||||||
|
string baseUrl = Program.GetAppConfig("ChromiumDevToolsPrefix");
|
||||||
|
if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out Uri uri))
|
||||||
|
{
|
||||||
|
Server.ServeResource(context, "<error>Invalid ChromiumDevToolsPrefix</error>", "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, $"<error>WebSocket connection failed: {EscapeXml(ex.Message)}</error>", "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<byte>(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<byte>(recvBuffer), recvToken);
|
||||||
|
|
||||||
|
string response = Encoding.UTF8.GetString(recvBuffer, 0, result.Count);
|
||||||
|
Server.ServeResource(context, response, "application/json", 200);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
Server.ServeResource(context, "<error>Timeout occurred</error>", "application/xml", 504);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_wsManager.Remove(hostname, port, wsPath);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string EscapeXml(string text)
|
||||||
|
{
|
||||||
|
return SecurityElement.Escape(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.Collections.Concurrent;
|
||||||
|
using System.IO;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
|
@ -8,46 +16,115 @@ namespace WelsonJS.Launcher
|
||||||
{
|
{
|
||||||
public class WebSocketManager
|
public class WebSocketManager
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<int, ClientWebSocket> _wsPool;
|
private class WebSocketEntry
|
||||||
|
{
|
||||||
public WebSocketManager() {
|
public ClientWebSocket Socket { get; set; }
|
||||||
_wsPool = new ConcurrentDictionary<int, ClientWebSocket>();
|
public string Host { get; set; }
|
||||||
|
public int Port { get; set; }
|
||||||
|
public string Path { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ClientWebSocket> GetOrCreateAsync(int port)
|
private readonly ConcurrentDictionary<string, WebSocketEntry> _wsPool = new ConcurrentDictionary<string, WebSocketEntry>();
|
||||||
{
|
|
||||||
if (_wsPool.TryGetValue(port, out var ws) && ws.State == WebSocketState.Open)
|
|
||||||
return ws;
|
|
||||||
|
|
||||||
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 _);
|
byte[] hash = md5.ComputeHash(Encoding.UTF8.GetBytes(input));
|
||||||
ws.Dispose();
|
return BitConverter.ToString(hash).Replace("-", "").ToLower();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ClientWebSocket> 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 ws = new ClientWebSocket();
|
||||||
var uri = new Uri($"ws://localhost:{port}/ws");
|
Uri uri = new Uri($"ws://{host}:{port}/{path}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await newWs.ConnectAsync(uri, CancellationToken.None);
|
await ws.ConnectAsync(uri, CancellationToken.None);
|
||||||
_wsPool[port] = newWs;
|
_wsPool[key] = new WebSocketEntry
|
||||||
return newWs;
|
{
|
||||||
|
Socket = ws,
|
||||||
|
Host = host,
|
||||||
|
Port = port,
|
||||||
|
Path = path
|
||||||
|
};
|
||||||
|
return ws;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
newWs.Dispose();
|
ws.Dispose();
|
||||||
throw;
|
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();
|
entry.Socket?.Abort();
|
||||||
ws.Dispose();
|
entry.Socket?.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> 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<byte>(message), WebSocketMessageType.Text, true, token);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Remove(host, port, path);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ws = await GetOrCreateAsync(host, port, path);
|
||||||
|
await ws.SendAsync(new ArraySegment<byte>(message), WebSocketMessageType.Text, true, token);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Remove(host, port, path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> 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<byte>(recvBuffer), cts.Token);
|
||||||
|
return Encoding.UTF8.GetString(recvBuffer, 0, result.Count);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
<add key="RepositoryUrl" value="https://github.com/gnh1201/welsonjs"/>
|
<add key="RepositoryUrl" value="https://github.com/gnh1201/welsonjs"/>
|
||||||
<add key="CopilotUrl" value="https://copilot.microsoft.com/"/>
|
<add key="CopilotUrl" value="https://copilot.microsoft.com/"/>
|
||||||
<add key="ChromiumDevToolsPrefix" value="http://localhost:9222/"/>
|
<add key="ChromiumDevToolsPrefix" value="http://localhost:9222/"/>
|
||||||
|
<add key="ChromiumDevToolsTimeout" value="5"/>
|
||||||
<add key="ChromiumFileName" value="msedge.exe"/>
|
<add key="ChromiumFileName" value="msedge.exe"/>
|
||||||
<add key="AzureAiServicePrefix" value="https://ai-catswords656881030318.services.ai.azure.com/"/>
|
<add key="AzureAiServicePrefix" value="https://ai-catswords656881030318.services.ai.azure.com/"/>
|
||||||
<add key="AzureAiServiceApiKey" value=""/>
|
<add key="AzureAiServiceApiKey" value=""/>
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,21 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner {
|
.banner {
|
||||||
text-align: center;
|
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: #f1f1f1;
|
background-color: #f1f1f1;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.banner a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner a:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
color: inherit; /* keeps contrast consistent */
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -590,10 +600,7 @@
|
||||||
const ip = encodeURIComponent(hostname.trim());
|
const ip = encodeURIComponent(hostname.trim());
|
||||||
|
|
||||||
axios.get(`/citi-query/${hostname}`).then(response => {
|
axios.get(`/citi-query/${hostname}`).then(response => {
|
||||||
const parser = new XMLParser();
|
if (!response) {
|
||||||
const result = parser.parse(response.data);
|
|
||||||
const data = JSON.parse(result.json);
|
|
||||||
if (!data) {
|
|
||||||
appendTextToEditor("\n// No data returned from Criminal IP.");
|
appendTextToEditor("\n// No data returned from Criminal IP.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -603,10 +610,10 @@
|
||||||
|
|
||||||
// network port data
|
// network port data
|
||||||
lines.push(`## Network ports:`);
|
lines.push(`## Network ports:`);
|
||||||
if (data.port.data.length == 0) {
|
if (response.port.data.length == 0) {
|
||||||
lines.push(`* No open ports found.`);
|
lines.push(`* No open ports found.`);
|
||||||
} else {
|
} else {
|
||||||
data.port.data.forEach(x => {
|
response.port.data.forEach(x => {
|
||||||
lines.push(`### ${x.open_port_no}/${x.socket}`);
|
lines.push(`### ${x.open_port_no}/${x.socket}`);
|
||||||
lines.push(`* Application: ${x.app_name} ${x.app_version}`);
|
lines.push(`* Application: ${x.app_name} ${x.app_version}`);
|
||||||
lines.push(`* Discovered hostnames: ${x.dns_names}`);
|
lines.push(`* Discovered hostnames: ${x.dns_names}`);
|
||||||
|
|
@ -616,10 +623,10 @@
|
||||||
|
|
||||||
// vulnerability data
|
// vulnerability data
|
||||||
lines.push(`## Vulnerabilities:`);
|
lines.push(`## Vulnerabilities:`);
|
||||||
if (data.vulnerability.data.length == 0) {
|
if (response.vulnerability.data.length == 0) {
|
||||||
lines.push(`* No vulnerabilities found.`);
|
lines.push(`* No vulnerabilities found.`);
|
||||||
} else {
|
} else {
|
||||||
data.vulnerability.data.forEach(x => {
|
response.vulnerability.data.forEach(x => {
|
||||||
lines.push(`### ${x.cve_id}`);
|
lines.push(`### ${x.cve_id}`);
|
||||||
lines.push(`* ${x.cve_description}`);
|
lines.push(`* ${x.cve_description}`);
|
||||||
lines.push(`* CVSSV2 Score: ${x.cvssv2_score}`);
|
lines.push(`* CVSSV2 Score: ${x.cvssv2_score}`);
|
||||||
|
|
@ -663,7 +670,7 @@
|
||||||
_e(Editor, { editorRef }),
|
_e(Editor, { editorRef }),
|
||||||
_e(PromptEditor, { promptEditorRef, promptMessagesRef })
|
_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')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user