// local_api.cpp — Servidor HTTP minimo (WinSock) para que scripts/agentes // hablen con el dashboard. Sin dependencias externas: parser de request + // dispatch + JSON escapado a mano. ~250 LoC. #include "local_api.h" #include "chrome_scanner.h" #include "chrome_launcher.h" #include "cdp_http.h" #include "session_state.h" #include #include #include #include #include #include #include #include #include #ifdef _WIN32 # define WIN32_LEAN_AND_MEAN # include # include # pragma comment(lib, "Ws2_32.lib") #endif namespace navegator { std::atomic g_api_running{false}; std::atomic g_api_port{0}; std::atomic g_api_request_count{0}; namespace { // ---------- JSON helpers ---------- std::string json_escape(const std::string& s) { std::string out; out.reserve(s.size() + 8); for (char c : s) { switch (c) { case '"': out += "\\\""; break; case '\\': out += "\\\\"; break; case '\b': out += "\\b"; break; case '\f': out += "\\f"; break; case '\n': out += "\\n"; break; case '\r': out += "\\r"; break; case '\t': out += "\\t"; break; default: if ((unsigned char)c < 0x20) { char buf[8]; std::snprintf(buf, sizeof(buf), "\\u%04x", (unsigned)c); out += buf; } else { out += c; } } } return out; } std::string instance_to_json(const ChromeInstance& i) { std::ostringstream os; os << "{\"pid\":" << i.pid << ",\"port\":" << i.port << ",\"profile\":\"" << json_escape(i.profile_name) << "\"" << ",\"user_data_dir\":\"" << json_escape(i.user_data_dir) << "\"" << ",\"headless\":" << (i.headless ? "true" : "false") << "}"; return os.str(); } // ---------- URL decoder + query parser ---------- std::string url_decode(const std::string& s) { std::string out; out.reserve(s.size()); for (size_t i = 0; i < s.size(); ++i) { if (s[i] == '+') { out += ' '; } else if (s[i] == '%' && i + 2 < s.size()) { int hi = 0, lo = 0; auto hex = [](char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; return 0; }; hi = hex(s[i + 1]); lo = hex(s[i + 2]); out += (char)((hi << 4) | lo); i += 2; } else { out += s[i]; } } return out; } std::map parse_query(const std::string& q) { std::map m; size_t start = 0; while (start < q.size()) { size_t amp = q.find('&', start); std::string pair = q.substr(start, amp == std::string::npos ? std::string::npos : amp - start); size_t eq = pair.find('='); if (eq != std::string::npos) { m[url_decode(pair.substr(0, eq))] = url_decode(pair.substr(eq + 1)); } else if (!pair.empty()) { m[url_decode(pair)] = ""; } if (amp == std::string::npos) break; start = amp + 1; } return m; } // ---------- Default user_data_dir resolver ---------- std::string default_user_data_dir(const std::string& profile) { #ifdef _WIN32 char buf[MAX_PATH] = {0}; DWORD n = GetEnvironmentVariableA("USERPROFILE", buf, sizeof(buf)); std::string base = (n > 0 && n < sizeof(buf)) ? buf : "C:\\Users\\Public"; return base + "\\AppData\\Local\\navegator_profiles\\" + profile; #else (void)profile; return ""; #endif } // ---------- Endpoint handlers ---------- struct Response { int status = 200; std::string content_type = "application/json"; std::string body; }; Response handle_health() { Response r; r.body = "{\"ok\":true,\"app\":\"navegator_dashboard\"}"; return r; } Response handle_browsers() { Response r; auto list = scan_chrome_instances(); std::ostringstream os; os << "["; for (size_t i = 0; i < list.size(); ++i) { if (i) os << ","; os << instance_to_json(list[i]); } os << "]"; r.body = os.str(); return r; } Response handle_spawn(const std::map& q) { Response r; LaunchOpts o; auto profile_it = q.find("profile"); std::string profile = (profile_it != q.end() && !profile_it->second.empty()) ? profile_it->second : "default"; auto port_it = q.find("port"); if (port_it != q.end() && !port_it->second.empty()) { try { o.port = std::stoi(port_it->second); } catch (...) {} } auto headless_it = q.find("headless"); if (headless_it != q.end()) { const std::string& v = headless_it->second; o.headless = (v == "1" || v == "true" || v == "yes"); } auto udd_it = q.find("user_data_dir"); o.user_data_dir = (udd_it != q.end() && !udd_it->second.empty()) ? udd_it->second : default_user_data_dir(profile); auto url_it = q.find("url"); if (url_it != q.end()) o.start_url = url_it->second; auto res = launch_chrome(o); std::ostringstream os; os << "{\"ok\":" << (res.ok ? "true" : "false") << ",\"pid\":" << res.pid << ",\"port\":" << o.port << ",\"profile\":\"" << json_escape(profile) << "\"" << ",\"user_data_dir\":\"" << json_escape(o.user_data_dir) << "\""; if (!res.ok) { os << ",\"error\":\"" << json_escape(res.error) << "\""; r.status = 500; } os << "}"; r.body = os.str(); return r; } Response handle_kill(const std::map& q) { Response r; std::string udd; auto udd_it = q.find("user_data_dir"); if (udd_it != q.end() && !udd_it->second.empty()) { udd = udd_it->second; } else { auto p_it = q.find("profile"); if (p_it != q.end() && !p_it->second.empty()) { udd = default_user_data_dir(p_it->second); } } if (udd.empty()) { r.status = 400; r.body = "{\"ok\":false,\"error\":\"need profile= or user_data_dir=\"}"; return r; } int killed = kill_chromes_by_userdata(udd); std::ostringstream os; os << "{\"ok\":" << (killed >= 0 ? "true" : "false") << ",\"killed\":" << (killed < 0 ? 0 : killed) << ",\"user_data_dir\":\"" << json_escape(udd) << "\"}"; r.body = os.str(); return r; } Response handle_not_found(const std::string& path) { Response r; r.status = 404; r.body = std::string("{\"ok\":false,\"error\":\"not found: ") + json_escape(path) + "\"}"; return r; } // ---------- /browser/{port}/... handlers ---------- Response handle_browser_version(int port) { Response r; CdpVersion v; std::string err; if (!cdp_get_version(port, v, &err)) { r.status = 502; r.body = std::string("{\"ok\":false,\"error\":\"") + json_escape(err) + "\"}"; return r; } std::ostringstream os; os << "{\"ok\":true" << ",\"browser\":\"" << json_escape(v.browser) << "\"" << ",\"protocolVersion\":\"" << json_escape(v.protocol_version) << "\"" << ",\"userAgent\":\"" << json_escape(v.user_agent) << "\"" << ",\"v8Version\":\"" << json_escape(v.v8_version) << "\"" << ",\"webkitVersion\":\"" << json_escape(v.webkit_version) << "\"" << ",\"webSocketDebuggerUrl\":\"" << json_escape(v.browser_ws_url) << "\"" << "}"; r.body = os.str(); return r; } Response handle_browser_tabs(int port) { Response r; std::vector tabs; std::string err; if (!cdp_list_tabs(port, tabs, &err)) { r.status = 502; r.body = std::string("{\"ok\":false,\"error\":\"") + json_escape(err) + "\"}"; return r; } std::ostringstream os; os << "["; for (size_t i = 0; i < tabs.size(); ++i) { if (i) os << ","; const auto& t = tabs[i]; os << "{\"id\":\"" << json_escape(t.id) << "\"" << ",\"type\":\"" << json_escape(t.type) << "\"" << ",\"title\":\"" << json_escape(t.title) << "\"" << ",\"url\":\"" << json_escape(t.url) << "\"" << ",\"webSocketDebuggerUrl\":\"" << json_escape(t.ws_url) << "\"" << ",\"attached\":" << (t.attached ? "true" : "false") << "}"; } os << "]"; r.body = os.str(); return r; } Response handle_browser_tab_new(int port, const std::map& q) { Response r; std::string url; auto it = q.find("url"); if (it != q.end()) url = it->second; CdpTab t; std::string err; if (!cdp_new_tab(port, url, t, &err)) { r.status = 502; r.body = std::string("{\"ok\":false,\"error\":\"") + json_escape(err) + "\"}"; return r; } std::ostringstream os; os << "{\"ok\":true,\"id\":\"" << json_escape(t.id) << "\"" << ",\"webSocketDebuggerUrl\":\"" << json_escape(t.ws_url) << "\"" << ",\"url\":\"" << json_escape(t.url) << "\"}"; r.body = os.str(); return r; } Response handle_browser_tab_focus(int port, const std::string& tab_id) { Response r; std::string err; if (!cdp_activate_tab(port, tab_id, &err)) { r.status = 502; r.body = std::string("{\"ok\":false,\"error\":\"") + json_escape(err) + "\"}"; return r; } r.body = "{\"ok\":true}"; return r; } Response handle_browser_tab_close(int port, const std::string& tab_id) { Response r; std::string err; if (!cdp_close_tab(port, tab_id, &err)) { r.status = 502; r.body = std::string("{\"ok\":false,\"error\":\"") + json_escape(err) + "\"}"; return r; } r.body = "{\"ok\":true}"; return r; } Response handle_browser_har(int port) { Response r; NetworkSession* net = nullptr; int sel_port = 0; { std::lock_guard lk(g_session().mu); sel_port = g_session().selected_port; net = g_session().net.get(); } if (sel_port != port || !net) { r.status = 409; r.body = "{\"ok\":false,\"error\":\"no active network session for this port (use UI to Select tab first)\"}"; return r; } r.body = net->export_har_json(); return r; } // Routes /browser/{port}/... — devuelve true si el path matchea (out llena la // resp). Si false, fallback al dispatch root. bool route_browser(const std::string& method, const std::string& path, const std::map& q, Response& out) { if (path.compare(0, 9, "/browser/") != 0) return false; size_t pos = 9; size_t slash = path.find('/', pos); std::string port_s = (slash == std::string::npos) ? path.substr(pos) : path.substr(pos, slash - pos); int port = 0; try { port = std::stoi(port_s); } catch (...) { return false; } if (port <= 0) return false; std::string rest = (slash == std::string::npos) ? "" : path.substr(slash); // /browser/{port}/version if (method == "GET" && rest == "/version") { out = handle_browser_version(port); return true; } // /browser/{port}/tabs if (method == "GET" && rest == "/tabs") { out = handle_browser_tabs(port); return true; } // /browser/{port}/tab/new if (method == "POST" && rest == "/tab/new"){ out = handle_browser_tab_new(port, q); return true; } // /browser/{port}/tab/{id}/focus|close if (rest.compare(0, 5, "/tab/") == 0) { std::string remainder = rest.substr(5); size_t s2 = remainder.find('/'); if (s2 != std::string::npos) { std::string tab_id = remainder.substr(0, s2); std::string action = remainder.substr(s2 + 1); if (method == "POST" && action == "focus") { out = handle_browser_tab_focus(port, tab_id); return true; } if (method == "POST" && action == "close") { out = handle_browser_tab_close(port, tab_id); return true; } } } // /browser/{port}/har if (method == "GET" && rest == "/har") { out = handle_browser_har(port); return true; } return false; } Response dispatch(const std::string& method, const std::string& path, const std::string& query) { auto q = parse_query(query); if (method == "GET" && path == "/health") return handle_health(); if (method == "GET" && path == "/browsers") return handle_browsers(); if (method == "POST" && path == "/spawn") return handle_spawn(q); if (method == "POST" && path == "/kill") return handle_kill(q); Response br; if (route_browser(method, path, q, br)) return br; return handle_not_found(path); } // ---------- HTTP request parsing ---------- struct Request { std::string method; std::string path; std::string query; }; bool parse_request_line(const std::string& line, Request& req) { size_t sp1 = line.find(' '); if (sp1 == std::string::npos) return false; size_t sp2 = line.find(' ', sp1 + 1); if (sp2 == std::string::npos) return false; req.method = line.substr(0, sp1); std::string url = line.substr(sp1 + 1, sp2 - sp1 - 1); size_t qm = url.find('?'); if (qm == std::string::npos) { req.path = url; req.query = ""; } else { req.path = url.substr(0, qm); req.query = url.substr(qm + 1); } return true; } #ifdef _WIN32 void send_all(SOCKET s, const std::string& data) { size_t sent = 0; while (sent < data.size()) { int n = ::send(s, data.data() + sent, (int)(data.size() - sent), 0); if (n <= 0) return; sent += (size_t)n; } } void handle_connection(SOCKET client) { // Leer hasta encontrar \r\n\r\n (cabeceras). Body POST: ignoramos para v0 // (los params van en query string, mas simple). std::string buf; char tmp[4096]; for (int i = 0; i < 16; ++i) { int n = ::recv(client, tmp, sizeof(tmp), 0); if (n <= 0) break; buf.append(tmp, n); if (buf.find("\r\n\r\n") != std::string::npos) break; if (buf.size() > 65536) break; } Request req; Response resp; size_t end_line = buf.find("\r\n"); if (end_line == std::string::npos || !parse_request_line(buf.substr(0, end_line), req)) { resp.status = 400; resp.body = "{\"ok\":false,\"error\":\"bad request\"}"; } else { resp = dispatch(req.method, req.path, req.query); } std::ostringstream out; const char* status_text = "OK"; if (resp.status == 400) status_text = "Bad Request"; else if (resp.status == 404) status_text = "Not Found"; else if (resp.status == 500) status_text = "Internal Server Error"; out << "HTTP/1.1 " << resp.status << " " << status_text << "\r\n" << "Content-Type: " << resp.content_type << "\r\n" << "Content-Length: " << resp.body.size() << "\r\n" << "Access-Control-Allow-Origin: *\r\n" << "Connection: close\r\n" << "\r\n" << resp.body; send_all(client, out.str()); closesocket(client); g_api_request_count.fetch_add(1); } void server_loop(int port) { WSADATA wsa; if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) return; SOCKET listener = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (listener == INVALID_SOCKET) { WSACleanup(); return; } BOOL yes = TRUE; setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, (const char*)&yes, sizeof(yes)); 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 (loopback) if (::bind(listener, (sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) { closesocket(listener); WSACleanup(); g_api_running.store(false); return; } if (::listen(listener, 16) == SOCKET_ERROR) { closesocket(listener); WSACleanup(); g_api_running.store(false); return; } g_api_running.store(true); g_api_port.store(port); while (true) { sockaddr_in client_addr{}; int alen = sizeof(client_addr); SOCKET client = ::accept(listener, (sockaddr*)&client_addr, &alen); if (client == INVALID_SOCKET) continue; std::thread(handle_connection, client).detach(); } } #endif } // namespace void start_api_server(int port) { #ifdef _WIN32 if (g_api_running.load()) return; std::thread([port]{ server_loop(port); }).detach(); #else (void)port; #endif } } // namespace navegator