cc1b324ffb
CDP HTTP client (cdp_http.h/cpp): WinSock raw + crude_json. GET /json/version,
/json (list tabs), PUT /json/new (Chrome 137+), GET /json/activate/{id},
/json/close/{id}.
CDP WebSocket client (cdp_ws.h/cpp): RFC 6455 handshake + framing manual,
masked client frames, async dispatcher con queue + wait_response. Soporta
fragmentacion (FIN=0 + continuation), ping/pong, close frame. Stats bytes
in/out + frames in.
Cross-panel session (session_state.h/cpp): selected_browser_port +
selected_tab_id. Cambiar tab cierra/abre NetworkSession.
Tabs panel: real. List + filtro titulo/URL + Refresh + New tab + Focus +
Close + Select (alimenta Network panel).
Network panel: DevTools-like.
- Tabla: Name | Status (color) | Method | Type | Initiator | Size | Time | Waterfall
- Filtros: text + invert + chips (Doc/CSS/JS/XHR/Img/Media/Font/WS/Other) + All toggle
- Toggles: Preserve log, Disable cache, Hide data:, Only failed, Pause/Resume
- Detalle por request: Headers (general + req + res) | Payload | Response (lazy
Network.getResponseBody) | Cookies | Timing | WS Messages (frames in/out)
- Right-click row: Copy URL / Copy as cURL / Copy as fetch
- Status bar: N requests | bytes transferred | resources | Finish | DCL | Load
- Export HAR 1.2 a archivo junto al exe
NetworkSession parsea Network.requestWillBeSent + ExtraInfo, responseReceived
+ ExtraInfo, dataReceived, loadingFinished, loadingFailed, webSocketCreated,
webSocketFrameSent/Received/Closed, Page.frameNavigated (autoclear si !preserve),
domContentEventFired, loadEventFired.
API local extendida (local_api.cpp):
- GET /browser/{port}/version
- GET /browser/{port}/tabs
- POST /browser/{port}/tab/new?url=
- POST /browser/{port}/tab/{id}/focus
- POST /browser/{port}/tab/{id}/close
- GET /browser/{port}/har (HAR 1.2 export de la sesion activa)
Build:
- CMakeLists.txt linka imgui_node_editor solo para reusar crude_json (sin
codigo de node-editor en runtime).
- 15 MB exe Windows. Cross-compile mingw-w64 OK.
app.md: bump version 0.2.0 -> 0.3.0, panels matrix actualizado, e2e_checks
añade api_health + api_browsers (warning).
Issue 0002 (sub-issue del roadmap navegator_dashboard 0001).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
286 lines
10 KiB
C++
286 lines
10 KiB
C++
#include "cdp_http.h"
|
|
|
|
#include "crude_json.h"
|
|
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <sstream>
|
|
|
|
#ifdef _WIN32
|
|
# define WIN32_LEAN_AND_MEAN
|
|
# include <winsock2.h>
|
|
# include <ws2tcpip.h>
|
|
#endif
|
|
|
|
namespace navegator {
|
|
|
|
namespace {
|
|
|
|
#ifdef _WIN32
|
|
|
|
bool set_socket_timeout(SOCKET s, int ms) {
|
|
DWORD t = (DWORD)ms;
|
|
if (setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&t, sizeof(t)) != 0) return false;
|
|
if (setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (const char*)&t, sizeof(t)) != 0) return false;
|
|
return true;
|
|
}
|
|
|
|
bool send_all(SOCKET s, const char* data, size_t len) {
|
|
size_t sent = 0;
|
|
while (sent < len) {
|
|
int n = ::send(s, data + sent, (int)(len - sent), 0);
|
|
if (n <= 0) return false;
|
|
sent += (size_t)n;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool recv_until_close(SOCKET s, std::string& out, size_t cap = 8 * 1024 * 1024) {
|
|
char buf[8192];
|
|
while (out.size() < cap) {
|
|
int n = ::recv(s, buf, sizeof(buf), 0);
|
|
if (n == 0) return true; // peer closed
|
|
if (n < 0) return !out.empty(); // timeout/err: si tenemos algo, asumimos completo
|
|
out.append(buf, (size_t)n);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool parse_http_response(const std::string& raw, int& status, std::string& body, std::string& err) {
|
|
auto eol = raw.find("\r\n");
|
|
if (eol == std::string::npos) { err = "no status line"; return false; }
|
|
std::string status_line = raw.substr(0, eol);
|
|
int sp1 = (int)status_line.find(' ');
|
|
int sp2 = (int)status_line.find(' ', sp1 + 1);
|
|
if (sp1 < 0 || sp2 < 0) { err = "bad status line"; return false; }
|
|
try { status = std::stoi(status_line.substr(sp1 + 1, sp2 - sp1 - 1)); }
|
|
catch (...) { err = "bad status code"; return false; }
|
|
|
|
auto headers_end = raw.find("\r\n\r\n");
|
|
if (headers_end == std::string::npos) { err = "no header terminator"; return false; }
|
|
body = raw.substr(headers_end + 4);
|
|
|
|
// Si Transfer-Encoding: chunked, decodear. CDP rara vez lo usa pero por si acaso.
|
|
std::string headers_lower;
|
|
headers_lower.reserve(headers_end);
|
|
for (size_t i = 0; i < headers_end; ++i) {
|
|
char c = raw[i];
|
|
headers_lower.push_back((c >= 'A' && c <= 'Z') ? (char)(c + 32) : c);
|
|
}
|
|
if (headers_lower.find("transfer-encoding: chunked") != std::string::npos) {
|
|
std::string decoded;
|
|
size_t p = 0;
|
|
while (p < body.size()) {
|
|
size_t crlf = body.find("\r\n", p);
|
|
if (crlf == std::string::npos) break;
|
|
std::string size_hex = body.substr(p, crlf - p);
|
|
// strip extensions
|
|
size_t semi = size_hex.find(';');
|
|
if (semi != std::string::npos) size_hex.resize(semi);
|
|
size_t chunk_size = 0;
|
|
try { chunk_size = std::stoul(size_hex, nullptr, 16); }
|
|
catch (...) { break; }
|
|
if (chunk_size == 0) break;
|
|
p = crlf + 2;
|
|
if (p + chunk_size > body.size()) break;
|
|
decoded.append(body, p, chunk_size);
|
|
p += chunk_size + 2;
|
|
}
|
|
body = std::move(decoded);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
CdpHttpResult do_request(const std::string& method,
|
|
int port,
|
|
const std::string& path,
|
|
const std::string& body,
|
|
int timeout_ms) {
|
|
CdpHttpResult r;
|
|
|
|
WSADATA wsa;
|
|
static bool wsa_ok = (WSAStartup(MAKEWORD(2, 2), &wsa) == 0);
|
|
if (!wsa_ok) { r.error = "WSAStartup failed"; return r; }
|
|
|
|
SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
|
if (s == INVALID_SOCKET) { r.error = "socket() failed"; return r; }
|
|
set_socket_timeout(s, timeout_ms);
|
|
|
|
sockaddr_in addr{};
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_port = htons((u_short)port);
|
|
addr.sin_addr.s_addr = htonl(0x7F000001); // 127.0.0.1
|
|
|
|
if (::connect(s, (sockaddr*)&addr, sizeof(addr)) != 0) {
|
|
r.error = "connect failed (port " + std::to_string(port) + ")";
|
|
closesocket(s);
|
|
return r;
|
|
}
|
|
|
|
std::ostringstream req;
|
|
req << method << " " << path << " HTTP/1.1\r\n"
|
|
<< "Host: 127.0.0.1:" << port << "\r\n"
|
|
<< "User-Agent: navegator_dashboard/1.0\r\n"
|
|
<< "Accept: */*\r\n"
|
|
<< "Connection: close\r\n";
|
|
if (!body.empty()) {
|
|
req << "Content-Type: application/json\r\n"
|
|
<< "Content-Length: " << body.size() << "\r\n";
|
|
}
|
|
req << "\r\n";
|
|
if (!body.empty()) req << body;
|
|
|
|
std::string raw_req = req.str();
|
|
if (!send_all(s, raw_req.data(), raw_req.size())) {
|
|
r.error = "send failed";
|
|
closesocket(s);
|
|
return r;
|
|
}
|
|
|
|
std::string raw_resp;
|
|
if (!recv_until_close(s, raw_resp)) {
|
|
r.error = "recv failed";
|
|
closesocket(s);
|
|
return r;
|
|
}
|
|
closesocket(s);
|
|
|
|
int status = 0;
|
|
std::string parsed_body;
|
|
std::string err;
|
|
if (!parse_http_response(raw_resp, status, parsed_body, err)) {
|
|
r.error = err;
|
|
return r;
|
|
}
|
|
r.ok = (status >= 200 && status < 300);
|
|
r.status = status;
|
|
r.body = std::move(parsed_body);
|
|
return r;
|
|
}
|
|
|
|
#else
|
|
|
|
CdpHttpResult do_request(const std::string&, int, const std::string&, const std::string&, int) {
|
|
CdpHttpResult r;
|
|
r.error = "cdp_http: Windows-only";
|
|
return r;
|
|
}
|
|
|
|
#endif
|
|
|
|
// ---------- JSON helpers ----------
|
|
std::string json_str_or_empty(const crude_json::value& v, const char* key) {
|
|
if (!v.is_object()) return "";
|
|
if (!v.contains(key)) return "";
|
|
const auto& f = v[key];
|
|
if (!f.is_string()) return "";
|
|
return f.get<std::string>();
|
|
}
|
|
|
|
bool json_bool_or_false(const crude_json::value& v, const char* key) {
|
|
if (!v.is_object() || !v.contains(key)) return false;
|
|
const auto& f = v[key];
|
|
if (!f.is_boolean()) return false;
|
|
return f.get<bool>();
|
|
}
|
|
|
|
void parse_tab(const crude_json::value& v, CdpTab& out) {
|
|
out.id = json_str_or_empty(v, "id");
|
|
out.type = json_str_or_empty(v, "type");
|
|
out.title = json_str_or_empty(v, "title");
|
|
out.url = json_str_or_empty(v, "url");
|
|
out.ws_url = json_str_or_empty(v, "webSocketDebuggerUrl");
|
|
out.favicon_url = json_str_or_empty(v, "faviconUrl");
|
|
out.description = json_str_or_empty(v, "description");
|
|
out.attached = json_bool_or_false(v, "attached");
|
|
}
|
|
|
|
} // namespace
|
|
|
|
CdpHttpResult cdp_http_request(const std::string& method,
|
|
int port,
|
|
const std::string& path,
|
|
const std::string& body,
|
|
int timeout_ms) {
|
|
return do_request(method, port, path, body, timeout_ms);
|
|
}
|
|
|
|
bool cdp_get_version(int port, CdpVersion& out, std::string* err) {
|
|
auto r = do_request("GET", port, "/json/version", "", 3000);
|
|
if (!r.ok) { if (err) *err = r.error.empty() ? ("HTTP " + std::to_string(r.status)) : r.error; return false; }
|
|
crude_json::value v = crude_json::value::parse(r.body);
|
|
if (!v.is_object()) { if (err) *err = "bad json"; return false; }
|
|
out.browser = json_str_or_empty(v, "Browser");
|
|
out.protocol_version = json_str_or_empty(v, "Protocol-Version");
|
|
out.user_agent = json_str_or_empty(v, "User-Agent");
|
|
out.v8_version = json_str_or_empty(v, "V8-Version");
|
|
out.webkit_version = json_str_or_empty(v, "WebKit-Version");
|
|
out.browser_ws_url = json_str_or_empty(v, "webSocketDebuggerUrl");
|
|
return true;
|
|
}
|
|
|
|
bool cdp_list_tabs(int port, std::vector<CdpTab>& out, std::string* err) {
|
|
out.clear();
|
|
auto r = do_request("GET", port, "/json", "", 3000);
|
|
if (!r.ok) { if (err) *err = r.error.empty() ? ("HTTP " + std::to_string(r.status)) : r.error; return false; }
|
|
crude_json::value v = crude_json::value::parse(r.body);
|
|
if (!v.is_array()) { if (err) *err = "bad json (not array)"; return false; }
|
|
const auto& arr = v.get<crude_json::array>();
|
|
for (const auto& item : arr) {
|
|
CdpTab t;
|
|
parse_tab(item, t);
|
|
if (!t.id.empty()) out.push_back(std::move(t));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
namespace {
|
|
std::string url_encode_query(const std::string& s) {
|
|
static const char* hex = "0123456789ABCDEF";
|
|
std::string out; out.reserve(s.size() * 3);
|
|
for (unsigned char c : s) {
|
|
bool unreserved =
|
|
(c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
|
(c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~' ||
|
|
c == ':' || c == '/' || c == '?' || c == '#' || c == '=' || c == '&';
|
|
if (unreserved) out.push_back((char)c);
|
|
else { out.push_back('%'); out.push_back(hex[c >> 4]); out.push_back(hex[c & 0xF]); }
|
|
}
|
|
return out;
|
|
}
|
|
}
|
|
|
|
bool cdp_new_tab(int port, const std::string& url, CdpTab& out, std::string* err) {
|
|
std::string path = "/json/new";
|
|
if (!url.empty()) path += "?" + url_encode_query(url);
|
|
// Chrome 137+ requiere PUT; versiones anteriores aceptan GET. Probamos PUT primero,
|
|
// si responde 405, fallback a GET.
|
|
auto r = do_request("PUT", port, path, "", 3000);
|
|
if (r.status == 405 || r.status == 404 || (!r.ok && r.error.find("connect") != std::string::npos)) {
|
|
if (r.status == 405 || r.status == 404) {
|
|
r = do_request("GET", port, path, "", 3000);
|
|
}
|
|
}
|
|
if (!r.ok) { if (err) *err = r.error.empty() ? ("HTTP " + std::to_string(r.status)) : r.error; return false; }
|
|
crude_json::value v = crude_json::value::parse(r.body);
|
|
if (!v.is_object()) { if (err) *err = "bad json"; return false; }
|
|
parse_tab(v, out);
|
|
return !out.id.empty();
|
|
}
|
|
|
|
bool cdp_activate_tab(int port, const std::string& tab_id, std::string* err) {
|
|
if (tab_id.empty()) { if (err) *err = "empty tab id"; return false; }
|
|
auto r = do_request("GET", port, "/json/activate/" + tab_id, "", 3000);
|
|
if (!r.ok && err) *err = r.error.empty() ? ("HTTP " + std::to_string(r.status)) : r.error;
|
|
return r.ok;
|
|
}
|
|
|
|
bool cdp_close_tab(int port, const std::string& tab_id, std::string* err) {
|
|
if (tab_id.empty()) { if (err) *err = "empty tab id"; return false; }
|
|
auto r = do_request("GET", port, "/json/close/" + tab_id, "", 3000);
|
|
if (!r.ok && err) *err = r.error.empty() ? ("HTTP " + std::to_string(r.status)) : r.error;
|
|
return r.ok;
|
|
}
|
|
|
|
} // namespace navegator
|