Files
registry_dashboard/http_client.cpp
T
egutierrez a466fff71a feat: Settings submenu (Settings.../About...), git column, projects tab
- main.cpp: registrar info About via fn_ui::about_window_set_info
- views.cpp: nueva columna "Git" en la tabla Apps (remote/local/-)
- data.h/cpp + data_http.cpp: AppRow gana repo_url + dir_path
- views.cpp: actions bar (Reindex / + Add / Reload / inbox) y modal Add
- views.cpp: tab Projects con tree + detalle anidado
- data_http.cpp: load_projects_http, load_project_detail_http, http_post_*
- http_client.cpp: SO_RCVTIMEO en Windows como DWORD ms (timeout 5 ms bug)
- CMakeLists: limpieza de srcs duplicados con fn_framework
- app.md: notas operativas y estado actual

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:06:31 +02:00

174 lines
5.3 KiB
C++

#include "http_client.h"
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <sstream>
#include <vector>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <mutex>
#pragma comment(lib, "ws2_32.lib")
// std::call_once para evitar race condition si hay peticiones simultaneas
// desde multiples threads (main + runners).
static std::once_flag g_wsa_once;
static bool g_wsa_ok = false;
static bool wsa_init() {
std::call_once(g_wsa_once, []() {
WSADATA wsa;
g_wsa_ok = (WSAStartup(MAKEWORD(2, 2), &wsa) == 0);
});
return g_wsa_ok;
}
typedef SOCKET sock_t;
#define SOCK_INVALID INVALID_SOCKET
#define SOCK_CLOSE closesocket
#define SOCK_ERR WSAGetLastError()
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netdb.h>
#include <errno.h>
typedef int sock_t;
#define SOCK_INVALID (-1)
#define SOCK_CLOSE close
#define SOCK_ERR errno
#endif
HttpClient::HttpClient(const std::string& host, int port)
: host_(host), port_(port) {}
HttpResponse HttpClient::get(const std::string& path) {
return request("GET", path, "", "");
}
HttpResponse HttpClient::post(const std::string& path, const std::string& body,
const std::string& content_type) {
return request("POST", path, body, content_type);
}
HttpResponse HttpClient::request(const std::string& method, const std::string& path,
const std::string& body, const std::string& content_type) {
HttpResponse resp;
#ifdef _WIN32
if (!wsa_init()) {
fprintf(stderr, "[http] WSAStartup failed\n");
return resp;
}
#endif
sock_t sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == SOCK_INVALID) {
fprintf(stderr, "[http] socket() failed: %d\n", SOCK_ERR);
return resp;
}
// Timeout — Windows y POSIX usan formatos distintos para SO_{RCV,SND}TIMEO.
// Windows: DWORD milisegundos. POSIX: struct timeval.
#ifdef _WIN32
DWORD timeout_ms = static_cast<DWORD>(timeout_sec_ * 1000);
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout_ms, sizeof(timeout_ms));
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (const char*)&timeout_ms, sizeof(timeout_ms));
#else
struct timeval tv;
tv.tv_sec = timeout_sec_;
tv.tv_usec = 0;
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof(tv));
#endif
// Connect
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(static_cast<uint16_t>(port_));
addr.sin_addr.s_addr = inet_addr(host_.c_str());
if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
int err = SOCK_ERR;
SOCK_CLOSE(sock);
// Reportamos el errno/WSAError en el body para que el toast sea util.
char buf[128];
std::snprintf(buf, sizeof(buf),
"connect() failed to %s:%d (err=%d)",
host_.c_str(), port_, err);
resp.body = buf;
return resp;
}
// Build request
std::ostringstream req;
req << method << " " << path << " HTTP/1.1\r\n";
req << "Host: " << host_ << ":" << port_ << "\r\n";
req << "Connection: close\r\n";
if (!body.empty()) {
req << "Content-Type: " << content_type << "\r\n";
req << "Content-Length: " << body.size() << "\r\n";
}
req << "\r\n";
if (!body.empty()) req << body;
std::string raw_req = req.str();
send(sock, raw_req.c_str(), static_cast<int>(raw_req.size()), 0);
// Read response
std::vector<char> buf(8192);
std::string raw;
for (;;) {
int n = recv(sock, buf.data(), static_cast<int>(buf.size()), 0);
if (n <= 0) break;
raw.append(buf.data(), n);
}
SOCK_CLOSE(sock);
// Parse status line
auto hdr_end = raw.find("\r\n\r\n");
if (hdr_end == std::string::npos) return resp;
// "HTTP/1.1 200 OK\r\n..."
auto first_line_end = raw.find("\r\n");
if (first_line_end == std::string::npos) return resp;
std::string status_line = raw.substr(0, first_line_end);
auto sp1 = status_line.find(' ');
if (sp1 != std::string::npos) {
resp.status = std::atoi(status_line.c_str() + sp1 + 1);
}
resp.body = raw.substr(hdr_end + 4);
// Handle chunked transfer encoding
std::string headers_str = raw.substr(0, hdr_end);
if (headers_str.find("chunked") != std::string::npos) {
// Decode chunked body
std::string decoded;
const char* p = resp.body.c_str();
const char* end = p + resp.body.size();
while (p < end) {
// Read chunk size (hex)
char* chunk_end = nullptr;
long chunk_size = strtol(p, &chunk_end, 16);
if (chunk_size <= 0) break;
// Skip \r\n after size
p = chunk_end;
if (p < end && *p == '\r') p++;
if (p < end && *p == '\n') p++;
// Read chunk data
if (p + chunk_size <= end) {
decoded.append(p, chunk_size);
}
p += chunk_size;
// Skip trailing \r\n
if (p < end && *p == '\r') p++;
if (p < end && *p == '\n') p++;
}
resp.body = decoded;
}
return resp;
}