// 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 readonly ResourceServer Server;
private readonly HttpClient _httpClient;
private readonly WebSocketManager _wsManager = new WebSocketManager();
private const string Prefix = "devtools/";
public ChromiumDevTools(ResourceServer server, HttpClient httpClient)
{
Server = server;
_httpClient = httpClient;
}
public bool CanHandle(string path)
{
return path.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase);
}
public async Task HandleAsync(HttpListenerContext context, string path)
{
string endpoint = path.Substring(Prefix.Length);
if (endpoint.Equals("json", StringComparison.OrdinalIgnoreCase))
{
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;
}
if (endpoint.StartsWith("page/", StringComparison.OrdinalIgnoreCase))
{
// 기본 구성
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);
}
}
}