356 lines
11 KiB
C++
356 lines
11 KiB
C++
// 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 <atomic>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <map>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#ifdef _WIN32
|
|
# define WIN32_LEAN_AND_MEAN
|
|
# include <winsock2.h>
|
|
# include <ws2tcpip.h>
|
|
# pragma comment(lib, "Ws2_32.lib")
|
|
#endif
|
|
|
|
namespace navegator {
|
|
|
|
std::atomic<bool> g_api_running{false};
|
|
std::atomic<int> g_api_port{0};
|
|
std::atomic<int> 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<std::string, std::string> parse_query(const std::string& q) {
|
|
std::map<std::string, std::string> 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<std::string, std::string>& 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<std::string, std::string>& 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;
|
|
}
|
|
|
|
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);
|
|
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
|