491 lines
14 KiB
C
491 lines
14 KiB
C
/*
|
|
* clavitor CLI — HTTP/HTTPS client using BearSSL
|
|
*
|
|
* Supports both plain HTTP and HTTPS (TLS via BearSSL).
|
|
* URL scheme determines protocol: http:// = plain, https:// = TLS.
|
|
*
|
|
* Trust anchors: TODO — currently no certificate validation.
|
|
*/
|
|
|
|
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "http.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
|
|
#ifdef _WIN32
|
|
#include <winsock2.h>
|
|
#include <ws2tcpip.h>
|
|
#pragma comment(lib, "ws2_32.lib")
|
|
#define SOCKCLOSE(s) closesocket(s)
|
|
typedef int socklen_t;
|
|
#else
|
|
#include <sys/types.h>
|
|
#include <sys/socket.h>
|
|
#include <netdb.h>
|
|
#include <unistd.h>
|
|
#define SOCKET int
|
|
#define INVALID_SOCKET (-1)
|
|
#define SOCKCLOSE(s) close(s)
|
|
#endif
|
|
|
|
#include "bearssl.h"
|
|
|
|
/* --- System CA trust anchor loading --- */
|
|
|
|
/*
|
|
* Load PEM CA certificates from system trust store.
|
|
* Parses each certificate, extracts DN + public key → br_x509_trust_anchor.
|
|
*/
|
|
|
|
struct pem_accum {
|
|
unsigned char *buf;
|
|
size_t len;
|
|
size_t cap;
|
|
};
|
|
|
|
static void pem_accum_push(void *dest_ctx, const void *src, size_t len) {
|
|
struct pem_accum *a = (struct pem_accum *)dest_ctx;
|
|
if (a->len + len > a->cap) {
|
|
a->cap = (a->len + len) * 2;
|
|
a->buf = realloc(a->buf, a->cap);
|
|
}
|
|
memcpy(a->buf + a->len, src, len);
|
|
a->len += len;
|
|
}
|
|
|
|
/* DN callback for x509 decoder */
|
|
struct dn_accum {
|
|
unsigned char *buf;
|
|
size_t len;
|
|
size_t cap;
|
|
};
|
|
|
|
static void dn_push(void *dest_ctx, const void *src, size_t len) {
|
|
struct dn_accum *a = (struct dn_accum *)dest_ctx;
|
|
if (a->len + len > a->cap) {
|
|
a->cap = (a->len + len) * 2;
|
|
a->buf = realloc(a->buf, a->cap);
|
|
}
|
|
memcpy(a->buf + a->len, src, len);
|
|
a->len += len;
|
|
}
|
|
|
|
static void load_system_tas(br_x509_trust_anchor **out_tas, size_t *out_count) {
|
|
*out_tas = NULL;
|
|
*out_count = 0;
|
|
|
|
static const char *ca_paths[] = {
|
|
"/etc/ssl/certs/ca-certificates.crt",
|
|
"/etc/pki/tls/certs/ca-bundle.crt",
|
|
"/etc/ssl/cert.pem",
|
|
"/usr/local/share/certs/ca-root-nss.crt",
|
|
NULL
|
|
};
|
|
|
|
FILE *f = NULL;
|
|
for (int i = 0; ca_paths[i]; i++) {
|
|
f = fopen(ca_paths[i], "r");
|
|
if (f) break;
|
|
}
|
|
if (!f) return;
|
|
|
|
fseek(f, 0, SEEK_END);
|
|
long fsize = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
if (fsize <= 0 || fsize > 4 * 1024 * 1024) { fclose(f); return; }
|
|
|
|
char *pem = malloc((size_t)fsize);
|
|
size_t nr = fread(pem, 1, (size_t)fsize, f);
|
|
fclose(f);
|
|
|
|
/* Parse PEM → DER certificates → trust anchors */
|
|
size_t ta_cap = 256;
|
|
br_x509_trust_anchor *tas = calloc(ta_cap, sizeof(br_x509_trust_anchor));
|
|
size_t ta_count = 0;
|
|
|
|
br_pem_decoder_context pc;
|
|
br_pem_decoder_init(&pc);
|
|
|
|
struct pem_accum der = { malloc(8192), 0, 8192 };
|
|
int in_cert = 0;
|
|
|
|
size_t pos = 0;
|
|
while (pos < nr) {
|
|
size_t pushed = br_pem_decoder_push(&pc, pem + pos, nr - pos);
|
|
pos += pushed;
|
|
|
|
int event = br_pem_decoder_event(&pc);
|
|
if (event == BR_PEM_BEGIN_OBJ) {
|
|
in_cert = (strcmp(br_pem_decoder_name(&pc), "CERTIFICATE") == 0);
|
|
der.len = 0;
|
|
if (in_cert) br_pem_decoder_setdest(&pc, pem_accum_push, &der);
|
|
} else if (event == BR_PEM_END_OBJ && in_cert && der.len > 0) {
|
|
/* Decode X.509 certificate */
|
|
struct dn_accum dn = { malloc(512), 0, 512 };
|
|
br_x509_decoder_context dc;
|
|
br_x509_decoder_init(&dc, dn_push, &dn);
|
|
br_x509_decoder_push(&dc, der.buf, der.len);
|
|
|
|
if (br_x509_decoder_last_error(&dc) == 0 && br_x509_decoder_isCA(&dc)) {
|
|
br_x509_pkey *pk = br_x509_decoder_get_pkey(&dc);
|
|
if (pk && ta_count < ta_cap) {
|
|
br_x509_trust_anchor *ta = &tas[ta_count];
|
|
ta->dn.data = dn.buf;
|
|
ta->dn.len = dn.len;
|
|
ta->flags = BR_X509_TA_CA;
|
|
ta->pkey = *pk;
|
|
|
|
/* Deep copy key data (decoder buffer will be reused) */
|
|
if (pk->key_type == BR_KEYTYPE_RSA) {
|
|
unsigned char *n = malloc(pk->key.rsa.nlen);
|
|
unsigned char *e = malloc(pk->key.rsa.elen);
|
|
memcpy(n, pk->key.rsa.n, pk->key.rsa.nlen);
|
|
memcpy(e, pk->key.rsa.e, pk->key.rsa.elen);
|
|
ta->pkey.key.rsa.n = n;
|
|
ta->pkey.key.rsa.e = e;
|
|
} else if (pk->key_type == BR_KEYTYPE_EC) {
|
|
unsigned char *q = malloc(pk->key.ec.qlen);
|
|
memcpy(q, pk->key.ec.q, pk->key.ec.qlen);
|
|
ta->pkey.key.ec.q = q;
|
|
}
|
|
ta_count++;
|
|
dn.buf = NULL; /* ownership transferred */
|
|
}
|
|
}
|
|
free(dn.buf);
|
|
in_cert = 0;
|
|
} else if (event == BR_PEM_ERROR) {
|
|
in_cert = 0;
|
|
}
|
|
}
|
|
|
|
free(pem);
|
|
free(der.buf);
|
|
*out_tas = tas;
|
|
*out_count = ta_count;
|
|
}
|
|
|
|
/* --- socket connect --- */
|
|
|
|
static SOCKET tcp_connect(const char *host, const char *port) {
|
|
#ifdef _WIN32
|
|
WSADATA wsa;
|
|
WSAStartup(MAKEWORD(2, 2), &wsa);
|
|
#endif
|
|
|
|
struct addrinfo hints, *res, *p;
|
|
memset(&hints, 0, sizeof(hints));
|
|
hints.ai_family = AF_UNSPEC;
|
|
hints.ai_socktype = SOCK_STREAM;
|
|
|
|
int err = getaddrinfo(host, port, &hints, &res);
|
|
if (err != 0) {
|
|
fprintf(stderr, "error: DNS lookup failed for %s: %s\n", host, gai_strerror(err));
|
|
return INVALID_SOCKET;
|
|
}
|
|
|
|
SOCKET fd = INVALID_SOCKET;
|
|
for (p = res; p; p = p->ai_next) {
|
|
fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
|
|
if (fd == INVALID_SOCKET) continue;
|
|
if (connect(fd, p->ai_addr, p->ai_addrlen) == 0) break;
|
|
SOCKCLOSE(fd);
|
|
fd = INVALID_SOCKET;
|
|
}
|
|
freeaddrinfo(res);
|
|
|
|
if (fd == INVALID_SOCKET) {
|
|
fprintf(stderr, "error: cannot connect to %s:%s\n", host, port);
|
|
}
|
|
return fd;
|
|
}
|
|
|
|
/* --- BearSSL I/O callbacks --- */
|
|
|
|
static int sock_read(void *ctx, unsigned char *buf, size_t len) {
|
|
SOCKET fd = *(SOCKET *)ctx;
|
|
int n;
|
|
for (;;) {
|
|
n = (int)recv(fd, (char *)buf, (int)len, 0);
|
|
if (n <= 0) {
|
|
if (n < 0 && errno == EINTR) continue;
|
|
return -1;
|
|
}
|
|
return n;
|
|
}
|
|
}
|
|
|
|
static int sock_write(void *ctx, const unsigned char *buf, size_t len) {
|
|
SOCKET fd = *(SOCKET *)ctx;
|
|
int n;
|
|
for (;;) {
|
|
n = (int)send(fd, (const char *)buf, (int)len, 0);
|
|
if (n <= 0) {
|
|
if (n < 0 && errno == EINTR) continue;
|
|
return -1;
|
|
}
|
|
return n;
|
|
}
|
|
}
|
|
|
|
/* --- plain socket send/recv wrappers --- */
|
|
|
|
static int plain_send_all(SOCKET fd, const char *data, size_t len) {
|
|
size_t sent = 0;
|
|
while (sent < len) {
|
|
int n = (int)send(fd, data + sent, (int)(len - sent), 0);
|
|
if (n <= 0) {
|
|
if (n < 0 && errno == EINTR) continue;
|
|
return -1;
|
|
}
|
|
sent += (size_t)n;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int plain_recv_all(SOCKET fd, char **out_buf, size_t *out_len) {
|
|
size_t cap = 8192;
|
|
size_t len = 0;
|
|
char *buf = malloc(cap);
|
|
if (!buf) return -1;
|
|
|
|
for (;;) {
|
|
if (len + 1024 > cap) {
|
|
cap *= 2;
|
|
char *tmp = realloc(buf, cap);
|
|
if (!tmp) { free(buf); return -1; }
|
|
buf = tmp;
|
|
}
|
|
int n = (int)recv(fd, buf + len, (int)(cap - len - 1), 0);
|
|
if (n <= 0) break;
|
|
len += (size_t)n;
|
|
}
|
|
buf[len] = '\0';
|
|
*out_buf = buf;
|
|
*out_len = len;
|
|
return 0;
|
|
}
|
|
|
|
/* --- URL parsing --- */
|
|
|
|
struct parsed_url {
|
|
char host[256];
|
|
char port[8];
|
|
char path[1024];
|
|
int use_tls;
|
|
};
|
|
|
|
static int parse_url(const char *url, struct parsed_url *out) {
|
|
memset(out, 0, sizeof(*out));
|
|
|
|
if (strncmp(url, "https://", 8) == 0) {
|
|
out->use_tls = 1;
|
|
url += 8;
|
|
} else if (strncmp(url, "http://", 7) == 0) {
|
|
out->use_tls = 0;
|
|
url += 7;
|
|
} else {
|
|
fprintf(stderr, "error: URL must start with http:// or https://\n");
|
|
return -1;
|
|
}
|
|
|
|
/* host[:port]/path */
|
|
const char *slash = strchr(url, '/');
|
|
const char *colon = strchr(url, ':');
|
|
|
|
if (colon && (!slash || colon < slash)) {
|
|
size_t hlen = (size_t)(colon - url);
|
|
if (hlen >= sizeof(out->host)) return -1;
|
|
memcpy(out->host, url, hlen);
|
|
out->host[hlen] = '\0';
|
|
|
|
colon++;
|
|
const char *pend = slash ? slash : (url + strlen(url));
|
|
size_t plen = (size_t)(pend - colon);
|
|
if (plen >= sizeof(out->port)) return -1;
|
|
memcpy(out->port, colon, plen);
|
|
out->port[plen] = '\0';
|
|
} else {
|
|
const char *hend = slash ? slash : (url + strlen(url));
|
|
size_t hlen = (size_t)(hend - url);
|
|
if (hlen >= sizeof(out->host)) return -1;
|
|
memcpy(out->host, url, hlen);
|
|
out->host[hlen] = '\0';
|
|
strcpy(out->port, out->use_tls ? "443" : "80");
|
|
}
|
|
|
|
if (slash) {
|
|
snprintf(out->path, sizeof(out->path), "%s", slash);
|
|
} else {
|
|
strcpy(out->path, "/");
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* --- parse HTTP response --- */
|
|
|
|
static int parse_response(char *resp_buf, size_t resp_len, struct clv_response *resp) {
|
|
if (resp_len < 12 || strncmp(resp_buf, "HTTP/", 5) != 0) {
|
|
fprintf(stderr, "error: invalid HTTP response\n");
|
|
free(resp_buf);
|
|
return -1;
|
|
}
|
|
|
|
const char *sp = strchr(resp_buf, ' ');
|
|
if (!sp) { free(resp_buf); return -1; }
|
|
resp->status = atoi(sp + 1);
|
|
|
|
const char *body = strstr(resp_buf, "\r\n\r\n");
|
|
if (body) {
|
|
body += 4;
|
|
resp->body_len = resp_len - (size_t)(body - resp_buf);
|
|
resp->body = malloc(resp->body_len + 1);
|
|
if (resp->body) {
|
|
memcpy(resp->body, body, resp->body_len);
|
|
resp->body[resp->body_len] = '\0';
|
|
}
|
|
} else {
|
|
resp->body = malloc(1);
|
|
if (resp->body) resp->body[0] = '\0';
|
|
resp->body_len = 0;
|
|
}
|
|
|
|
free(resp_buf);
|
|
return 0;
|
|
}
|
|
|
|
/* --- HTTP GET (plain) --- */
|
|
|
|
static int http_get_plain(const struct parsed_url *pu, const char *bearer_token,
|
|
const char *agent_name, struct clv_response *resp) {
|
|
SOCKET fd = tcp_connect(pu->host, pu->port);
|
|
if (fd == INVALID_SOCKET) return -1;
|
|
|
|
char request[2048];
|
|
char agent_hdr[256] = "";
|
|
if (agent_name && agent_name[0])
|
|
snprintf(agent_hdr, sizeof(agent_hdr), "X-Agent: %s\r\n", agent_name);
|
|
int reqlen = snprintf(request, sizeof(request),
|
|
"GET %s HTTP/1.1\r\n"
|
|
"Host: %s\r\n"
|
|
"Authorization: Bearer %s\r\n"
|
|
"%s"
|
|
"Connection: close\r\n"
|
|
"Accept: application/json\r\n"
|
|
"\r\n",
|
|
pu->path, pu->host, bearer_token, agent_hdr);
|
|
|
|
if (plain_send_all(fd, request, (size_t)reqlen) != 0) {
|
|
fprintf(stderr, "error: send failed\n");
|
|
SOCKCLOSE(fd);
|
|
return -1;
|
|
}
|
|
|
|
char *resp_buf = NULL;
|
|
size_t resp_len = 0;
|
|
if (plain_recv_all(fd, &resp_buf, &resp_len) != 0) {
|
|
fprintf(stderr, "error: recv failed\n");
|
|
SOCKCLOSE(fd);
|
|
return -1;
|
|
}
|
|
|
|
SOCKCLOSE(fd);
|
|
return parse_response(resp_buf, resp_len, resp);
|
|
}
|
|
|
|
/* --- HTTPS GET (BearSSL TLS) --- */
|
|
|
|
static int http_get_tls(const struct parsed_url *pu, const char *bearer_token,
|
|
const char *agent_name, struct clv_response *resp) {
|
|
SOCKET fd = tcp_connect(pu->host, pu->port);
|
|
if (fd == INVALID_SOCKET) return -1;
|
|
|
|
br_ssl_client_context sc;
|
|
br_x509_minimal_context xc;
|
|
unsigned char iobuf[BR_SSL_BUFSIZE_BIDI];
|
|
|
|
/*
|
|
* Load system CA certificates for TLS validation.
|
|
* Falls back to no-validation mode if CAs can't be loaded.
|
|
*/
|
|
size_t ta_count = 0;
|
|
br_x509_trust_anchor *tas = NULL;
|
|
load_system_tas(&tas, &ta_count);
|
|
|
|
br_x509_minimal_init(&xc, &br_sha256_vtable, tas, ta_count);
|
|
br_x509_minimal_set_rsa(&xc, &br_rsa_i31_pkcs1_vrfy);
|
|
br_x509_minimal_set_ecdsa(&xc, &br_ec_prime_i31, &br_ecdsa_i31_vrfy_asn1);
|
|
|
|
br_ssl_client_init_full(&sc, &xc, tas, ta_count);
|
|
br_ssl_engine_set_buffer(&sc.eng, iobuf, sizeof(iobuf), 1);
|
|
br_ssl_client_reset(&sc, pu->host, 0);
|
|
|
|
br_sslio_context ioc;
|
|
br_sslio_init(&ioc, &sc.eng, sock_read, &fd, sock_write, &fd);
|
|
|
|
char request[2048];
|
|
char agent_hdr[256] = "";
|
|
if (agent_name && agent_name[0])
|
|
snprintf(agent_hdr, sizeof(agent_hdr), "X-Agent: %s\r\n", agent_name);
|
|
int reqlen = snprintf(request, sizeof(request),
|
|
"GET %s HTTP/1.1\r\n"
|
|
"Host: %s\r\n"
|
|
"Authorization: Bearer %s\r\n"
|
|
"%s"
|
|
"Connection: close\r\n"
|
|
"Accept: application/json\r\n"
|
|
"\r\n",
|
|
pu->path, pu->host, bearer_token, agent_hdr);
|
|
|
|
if (br_sslio_write_all(&ioc, request, (size_t)reqlen) != 0) {
|
|
fprintf(stderr, "error: TLS write failed\n");
|
|
int err = br_ssl_engine_last_error(&sc.eng);
|
|
if (err) fprintf(stderr, " BearSSL error: %d\n", err);
|
|
SOCKCLOSE(fd);
|
|
return -1;
|
|
}
|
|
br_sslio_flush(&ioc);
|
|
|
|
size_t resp_cap = 8192;
|
|
size_t resp_len = 0;
|
|
char *resp_buf = malloc(resp_cap);
|
|
if (!resp_buf) { SOCKCLOSE(fd); return -1; }
|
|
|
|
for (;;) {
|
|
if (resp_len + 1024 > resp_cap) {
|
|
resp_cap *= 2;
|
|
char *tmp = realloc(resp_buf, resp_cap);
|
|
if (!tmp) { free(resp_buf); SOCKCLOSE(fd); return -1; }
|
|
resp_buf = tmp;
|
|
}
|
|
int n = br_sslio_read(&ioc, resp_buf + resp_len, resp_cap - resp_len - 1);
|
|
if (n <= 0) break;
|
|
resp_len += (size_t)n;
|
|
}
|
|
resp_buf[resp_len] = '\0';
|
|
|
|
SOCKCLOSE(fd);
|
|
return parse_response(resp_buf, resp_len, resp);
|
|
}
|
|
|
|
/* --- public API --- */
|
|
|
|
int http_get(const char *url, const char *bearer_token, const char *agent_name,
|
|
struct clv_response *resp) {
|
|
memset(resp, 0, sizeof(*resp));
|
|
|
|
struct parsed_url pu;
|
|
if (parse_url(url, &pu) != 0) return -1;
|
|
|
|
if (pu.use_tls) {
|
|
return http_get_tls(&pu, bearer_token, agent_name, resp);
|
|
} else {
|
|
return http_get_plain(&pu, bearer_token, agent_name, resp);
|
|
}
|
|
}
|