#include "cdp_http.h" #include "crude_json.h" #include #include #include #ifdef _WIN32 # define WIN32_LEAN_AND_MEAN # include # include #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(); } 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(); } 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& 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(); 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