net: add blocking HTTP/HTTPS GET client
A small libssl-backed client (naut_http_get) that fetches a URL over plain HTTP or TLS, follows 3xx redirects, and decodes Content-Length and chunked bodies. Foundation for the RSS poller and Torznab search. Built as a PIC static lib so it can link into the webui plugin module. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
dbd0d6f78e
commit
0393ed429b
3 changed files with 299 additions and 2 deletions
264
src/net/http_client.c
Normal file
264
src/net/http_client.c
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
/* http_client.c — blocking HTTP/HTTPS GET with redirect handling.
|
||||
*
|
||||
* A small, dependency-light client: raw sockets for HTTP, OpenSSL for HTTPS.
|
||||
* It reads the whole response into memory (capped), handles both Content-Length
|
||||
* and chunked transfer-encoding, and follows 3xx redirects. This is deliberately
|
||||
* simple — it serves RSS/Torznab fetches, not a general-purpose user agent. */
|
||||
#include "naut/http_client.h"
|
||||
#include "naut/log.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <netdb.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
#include <openssl/ssl.h>
|
||||
#include <openssl/err.h>
|
||||
|
||||
#define HTTP_MAX_BODY (16 * 1024 * 1024) /* 16 MiB cap */
|
||||
#define HTTP_MAX_REDIR 5
|
||||
|
||||
/* A transport: either a plain fd or an SSL session over it. */
|
||||
typedef struct {
|
||||
int fd;
|
||||
SSL_CTX *ctx;
|
||||
SSL *ssl;
|
||||
} conn_t;
|
||||
|
||||
static void conn_close(conn_t *c) {
|
||||
if (c->ssl) { SSL_shutdown(c->ssl); SSL_free(c->ssl); c->ssl = NULL; }
|
||||
if (c->ctx) { SSL_CTX_free(c->ctx); c->ctx = NULL; }
|
||||
if (c->fd >= 0) { close(c->fd); c->fd = -1; }
|
||||
}
|
||||
|
||||
static int dial(const char *host, const char *port) {
|
||||
struct addrinfo hints, *res = NULL, *ai;
|
||||
memset(&hints, 0, sizeof hints);
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
if (getaddrinfo(host, port, &hints, &res) != 0) return -1;
|
||||
int fd = -1;
|
||||
for (ai = res; ai; ai = ai->ai_next) {
|
||||
fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
|
||||
if (fd < 0) continue;
|
||||
struct timeval tv = { .tv_sec = 15, .tv_usec = 0 };
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
|
||||
if (connect(fd, ai->ai_addr, ai->ai_addrlen) == 0) break;
|
||||
close(fd); fd = -1;
|
||||
}
|
||||
freeaddrinfo(res);
|
||||
return fd;
|
||||
}
|
||||
|
||||
static bool conn_open(conn_t *c, const char *host, const char *port, bool tls) {
|
||||
memset(c, 0, sizeof *c);
|
||||
c->fd = dial(host, port);
|
||||
if (c->fd < 0) { NAUT_WARN("http: connect %s:%s failed", host, port); return false; }
|
||||
if (!tls) return true;
|
||||
|
||||
c->ctx = SSL_CTX_new(TLS_client_method());
|
||||
if (!c->ctx) { conn_close(c); return false; }
|
||||
SSL_CTX_set_verify(c->ctx, SSL_VERIFY_NONE, NULL); /* best-effort fetch */
|
||||
c->ssl = SSL_new(c->ctx);
|
||||
if (!c->ssl) { conn_close(c); return false; }
|
||||
SSL_set_fd(c->ssl, c->fd);
|
||||
SSL_set_tlsext_host_name(c->ssl, host); /* SNI */
|
||||
if (SSL_connect(c->ssl) != 1) {
|
||||
NAUT_WARN("http: TLS handshake with %s failed", host);
|
||||
conn_close(c);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool conn_write(conn_t *c, const void *data, size_t len) {
|
||||
const char *p = data;
|
||||
while (len) {
|
||||
int n = c->ssl ? SSL_write(c->ssl, p, (int)len)
|
||||
: (int)write(c->fd, p, len);
|
||||
if (n <= 0) {
|
||||
if (!c->ssl && n < 0 && errno == EINTR) continue;
|
||||
return false;
|
||||
}
|
||||
p += n; len -= (size_t)n;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static int conn_read(conn_t *c, void *buf, size_t len) {
|
||||
for (;;) {
|
||||
int n = c->ssl ? SSL_read(c->ssl, buf, (int)len)
|
||||
: (int)read(c->fd, buf, len);
|
||||
if (n < 0 && !c->ssl && errno == EINTR) continue;
|
||||
return n;
|
||||
}
|
||||
}
|
||||
|
||||
/* Parse "scheme://host[:port]/path". Fills host/port/path; sets *tls. */
|
||||
static bool parse_url(const char *url, char *host, size_t hostsz,
|
||||
char *port, size_t portsz, char *path, size_t pathsz,
|
||||
bool *tls) {
|
||||
const char *h;
|
||||
if (strncasecmp(url, "https://", 8) == 0) { *tls = true; h = url + 8; }
|
||||
else if (strncasecmp(url, "http://", 7) == 0) { *tls = false; h = url + 7; }
|
||||
else return false;
|
||||
|
||||
const char *slash = strchr(h, '/');
|
||||
const char *hostend = slash ? slash : h + strlen(h);
|
||||
const char *colon = memchr(h, ':', (size_t)(hostend - h));
|
||||
size_t hlen = colon ? (size_t)(colon - h) : (size_t)(hostend - h);
|
||||
if (hlen == 0 || hlen >= hostsz) return false;
|
||||
memcpy(host, h, hlen); host[hlen] = 0;
|
||||
if (colon) {
|
||||
size_t plen = (size_t)(hostend - colon - 1);
|
||||
if (plen == 0 || plen >= portsz) return false;
|
||||
memcpy(port, colon + 1, plen); port[plen] = 0;
|
||||
} else {
|
||||
snprintf(port, portsz, "%s", *tls ? "443" : "80");
|
||||
}
|
||||
if (slash) { if (strlen(slash) >= pathsz) return false; snprintf(path, pathsz, "%s", slash); }
|
||||
else snprintf(path, pathsz, "/");
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Decode a chunked-transfer body in place; returns new length. */
|
||||
static size_t dechunk(char *body, size_t len) {
|
||||
char *out = body;
|
||||
const char *in = body, *end = body + len;
|
||||
while (in < end) {
|
||||
char *nl = (char *)memchr(in, '\n', (size_t)(end - in));
|
||||
if (!nl) break;
|
||||
long sz = strtol(in, NULL, 16);
|
||||
in = nl + 1;
|
||||
if (sz <= 0) break;
|
||||
if (in + sz > end) sz = (long)(end - in);
|
||||
memmove(out, in, (size_t)sz);
|
||||
out += sz;
|
||||
in += sz;
|
||||
/* skip trailing CRLF after the chunk */
|
||||
if (in < end && *in == '\r') in++;
|
||||
if (in < end && *in == '\n') in++;
|
||||
}
|
||||
*out = 0;
|
||||
return (size_t)(out - body);
|
||||
}
|
||||
|
||||
/* One request/response round-trip. On a 3xx with Location, writes the target
|
||||
* into `redirect` (caller retries) and returns NAUT_OK with out->body == NULL. */
|
||||
static naut_err fetch_once(const char *url, naut_http_response *out,
|
||||
char *redirect, size_t redirsz) {
|
||||
char host[256], port[16], path[2048];
|
||||
bool tls;
|
||||
if (!parse_url(url, host, sizeof host, port, sizeof port,
|
||||
path, sizeof path, &tls))
|
||||
return NAUT_ERR_INVAL;
|
||||
|
||||
conn_t c;
|
||||
if (!conn_open(&c, host, port, tls)) return NAUT_ERR_IO;
|
||||
|
||||
char req[3072];
|
||||
int rn = snprintf(req, sizeof req,
|
||||
"GET %s HTTP/1.1\r\nHost: %s\r\nUser-Agent: Naut/0.1\r\n"
|
||||
"Accept: */*\r\nConnection: close\r\n\r\n", path, host);
|
||||
if (rn < 0 || (size_t)rn >= sizeof req || !conn_write(&c, req, (size_t)rn)) {
|
||||
conn_close(&c); return NAUT_ERR_IO;
|
||||
}
|
||||
|
||||
size_t cap = 1 << 16, len = 0;
|
||||
char *buf = malloc(cap);
|
||||
if (!buf) { conn_close(&c); return NAUT_ERR_NOMEM; }
|
||||
for (;;) {
|
||||
if (len + 1 >= cap) {
|
||||
if (cap >= HTTP_MAX_BODY) break;
|
||||
size_t ncap = cap * 2 > HTTP_MAX_BODY ? HTTP_MAX_BODY : cap * 2;
|
||||
char *nb = realloc(buf, ncap);
|
||||
if (!nb) { free(buf); conn_close(&c); return NAUT_ERR_NOMEM; }
|
||||
buf = nb; cap = ncap;
|
||||
}
|
||||
int r = conn_read(&c, buf + len, cap - len - 1);
|
||||
if (r < 0) { free(buf); conn_close(&c); return NAUT_ERR_IO; }
|
||||
if (r == 0) break;
|
||||
len += (size_t)r;
|
||||
}
|
||||
conn_close(&c);
|
||||
buf[len] = 0;
|
||||
|
||||
if (len < 12 || memcmp(buf, "HTTP/", 5) != 0) { free(buf); return NAUT_ERR_PROTO; }
|
||||
long status = strtol(buf + 9, NULL, 10);
|
||||
|
||||
char *hdr_end = NULL;
|
||||
for (size_t i = 0; i + 3 < len; i++)
|
||||
if (buf[i]=='\r'&&buf[i+1]=='\n'&&buf[i+2]=='\r'&&buf[i+3]=='\n') {
|
||||
hdr_end = buf + i + 4; break;
|
||||
}
|
||||
if (!hdr_end) { free(buf); return NAUT_ERR_PROTO; }
|
||||
|
||||
/* Headers are everything before hdr_end; scan them case-insensitively. */
|
||||
size_t hdr_len = (size_t)(hdr_end - buf);
|
||||
bool chunked = false;
|
||||
char *loc = NULL;
|
||||
for (char *p = buf; p < buf + hdr_len; ) {
|
||||
char *eol = memchr(p, '\n', (size_t)(buf + hdr_len - p));
|
||||
size_t line = eol ? (size_t)(eol - p) : (size_t)(buf + hdr_len - p);
|
||||
if (strncasecmp(p, "Transfer-Encoding:", 18) == 0 &&
|
||||
line < 256 && memmem(p, line, "chunked", 7))
|
||||
chunked = true;
|
||||
if (strncasecmp(p, "Location:", 9) == 0) loc = p + 9;
|
||||
if (!eol) break;
|
||||
p = eol + 1;
|
||||
}
|
||||
|
||||
if (status >= 300 && status < 400 && loc && redirect) {
|
||||
while (*loc == ' ' || *loc == '\t') loc++;
|
||||
size_t n = strcspn(loc, "\r\n");
|
||||
if (n && n < redirsz) { memcpy(redirect, loc, n); redirect[n] = 0; }
|
||||
else redirect[0] = 0;
|
||||
free(buf);
|
||||
out->body = NULL; out->status = status; out->body_len = 0;
|
||||
return NAUT_OK;
|
||||
}
|
||||
|
||||
/* Move body to the front of the allocation so the caller owns one buffer. */
|
||||
size_t blen = len - hdr_len;
|
||||
memmove(buf, hdr_end, blen);
|
||||
buf[blen] = 0;
|
||||
if (chunked) blen = dechunk(buf, blen);
|
||||
|
||||
out->status = status;
|
||||
out->body = buf;
|
||||
out->body_len = blen;
|
||||
if (redirect) redirect[0] = 0;
|
||||
return NAUT_OK;
|
||||
}
|
||||
|
||||
naut_err naut_http_get(const char *url, naut_http_response *out) {
|
||||
if (!url || !out) return NAUT_ERR_INVAL;
|
||||
out->body = NULL; out->status = 0; out->body_len = 0;
|
||||
|
||||
char current[2048];
|
||||
if (strlen(url) >= sizeof current) return NAUT_ERR_INVAL;
|
||||
snprintf(current, sizeof current, "%s", url);
|
||||
|
||||
for (int hop = 0; hop <= HTTP_MAX_REDIR; hop++) {
|
||||
char redirect[2048] = {0};
|
||||
naut_err e = fetch_once(current, out, redirect, sizeof redirect);
|
||||
if (e != NAUT_OK) return e;
|
||||
if (out->body) return NAUT_OK; /* got a real response */
|
||||
if (!redirect[0]) return NAUT_ERR_PROTO;
|
||||
/* Relative redirect: only absolute URLs are followed here. */
|
||||
if (strncasecmp(redirect, "http", 4) != 0) return NAUT_ERR_PROTO;
|
||||
snprintf(current, sizeof current, "%s", redirect);
|
||||
}
|
||||
return NAUT_ERR_PROTO; /* too many redirects */
|
||||
}
|
||||
|
||||
void naut_http_response_free(naut_http_response *r) {
|
||||
if (!r) return;
|
||||
free(r->body);
|
||||
r->body = NULL; r->body_len = 0; r->status = 0;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue