mirror of
https://github.com/gnh1201/welsonjs.git
synced 2025-11-29 19:03:42 +00:00
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.
This commit is contained in:
parent
2b30e864f0
commit
7b49817182
|
|
@ -114,6 +114,15 @@ namespace WelsonJS.Launcher.Properties {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 5과(와) 유사한 지역화된 문자열을 찾습니다.
|
||||
/// </summary>
|
||||
internal static string ChromiumDevToolsTimeout {
|
||||
get {
|
||||
return ResourceManager.GetString("ChromiumDevToolsTimeout", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// msedge.exe과(와) 유사한 지역화된 문자열을 찾습니다.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -205,4 +205,7 @@
|
|||
<data name="ChromiumFileName" xml:space="preserve">
|
||||
<value>msedge.exe</value>
|
||||
</data>
|
||||
<data name="ChromiumDevToolsTimeout" xml:space="preserve">
|
||||
<value>5</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -373,11 +373,6 @@ namespace WelsonJS.Launcher
|
|||
{
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, $"<error>Failed to process DevTools request. {EscapeXml(ex.Message)}</error>", "application/xml", 500);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Server.ServeResource(context, data, "application/json");
|
||||
}
|
||||
catch (Exception ex)
|
||||
if (endpoint.StartsWith("page/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
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 WebUtility.HtmlEncode(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<int, ClientWebSocket> _wsPool;
|
||||
|
||||
public WebSocketManager() {
|
||||
_wsPool = new ConcurrentDictionary<int, ClientWebSocket>();
|
||||
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<ClientWebSocket> GetOrCreateAsync(int port)
|
||||
{
|
||||
if (_wsPool.TryGetValue(port, out var ws) && ws.State == WebSocketState.Open)
|
||||
return ws;
|
||||
private readonly ConcurrentDictionary<string, WebSocketEntry> _wsPool = new ConcurrentDictionary<string, WebSocketEntry>();
|
||||
|
||||
if (ws != null)
|
||||
private string MakeKey(string host, int port, string path)
|
||||
{
|
||||
return host + ":" + port + "/" + path;
|
||||
}
|
||||
|
||||
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(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<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)
|
||||
{
|
||||
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<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="CopilotUrl" value="https://copilot.microsoft.com/"/>
|
||||
<add key="ChromiumDevToolsPrefix" value="http://localhost:9222/"/>
|
||||
<add key="ChromiumDevToolsTimeout" value="5"/>
|
||||
<add key="ChromiumFileName" value="msedge.exe"/>
|
||||
<add key="AzureAiServicePrefix" value="https://ai-catswords656881030318.services.ai.azure.com/"/>
|
||||
<add key="AzureAiServiceApiKey" value=""/>
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user