commits
tags
#include "http.h"
#include "net.h"
#include "util.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#define INITIAL_BUF 8192
typedef struct {
char *data;
size_t len;
size_t cap;
} Buf;
static int buf_ensure(Buf *b, size_t extra)
{
if (b->len + extra + 1 <= b->cap)
return 0;
size_t ncap = b->cap ? b->cap * 2 : INITIAL_BUF;
while (ncap < b->len + extra + 1)
ncap *= 2;
char *nd = realloc(b->data, ncap);
if (!nd)
return -1;
b->data = nd;
b->cap = ncap;
return 0;
}
static int buf_append(Buf *b, const char *s, size_t n)
{
if (buf_ensure(b, n) != 0)
return -1;
memcpy(b->data + b->len, s, n);
b->len += n;
b->data[b->len] = '\0';
return 0;
}
/* Appends a NUL-terminated C string, sizing itself via strlen() so callers
* never have to hand-count literal lengths (a previous version did, and got
* it wrong, silently splicing NUL bytes into outgoing HTTP headers). */
static int buf_append_str(Buf *b, const char *s)
{
return buf_append(b, s, strlen(s));
}
/* Case-insensitive substring search (portable stand-in for strcasestr). */
static const char *ci_find(const char *hay, size_t haylen, const char *needle)
{
size_t nlen = strlen(needle);
if (nlen == 0 || nlen > haylen)
return NULL;
for (size_t i = 0; i + nlen <= haylen; i++) {
size_t j = 0;
for (; j < nlen; j++) {
if (tolower((unsigned char)hay[i + j]) != tolower((unsigned char)needle[j]))
break;
}
if (j == nlen)
return hay + i;
}
return NULL;
}
/* block is the raw response from the status line through the blank line
* that terminates the headers (block_len does NOT need to include the body). */
static void extract_header(const char *block, size_t block_len, const char *name,
char *out, size_t outsize)
{
out[0] = '\0';
char pat[64];
snprintf(pat, sizeof(pat), "\r\n%s:", name);
const char *p = ci_find(block, block_len, pat);
if (!p)
return;
const char *v = p + 2 + strlen(name) + 1; /* skip \r\n NAME: */
const char *block_end = block + block_len;
while (v < block_end && (*v == ' ' || *v == '\t'))
v++;
const char *end = ci_find(v, (size_t)(block_end - v), "\r\n");
if (!end)
end = block_end;
size_t vlen = (size_t)(end - v);
if (vlen >= outsize)
vlen = outsize - 1;
memcpy(out, v, vlen);
out[vlen] = '\0';
}
static long extract_content_length(const char *headers, size_t headers_len)
{
char v[32];
extract_header(headers, headers_len, "Content-Length", v, sizeof(v));
if (v[0] == '\0')
return -1;
return atol(v);
}
static int is_chunked(const char *headers, size_t headers_len)
{
char v[64];
extract_header(headers, headers_len, "Transfer-Encoding", v, sizeof(v));
return strcasecmp(v, "chunked") == 0;
}
/* Decodes an HTTP chunked body in-place; returns decoded length, or -1 on malformed input. */
static long decode_chunked(const char *in, size_t inlen, char **out)
{
Buf b = {0};
size_t pos = 0;
while (pos < inlen) {
const char *line_end = NULL;
for (size_t i = pos; i + 1 < inlen; i++) {
if (in[i] == '\r' && in[i + 1] == '\n') {
line_end = in + i;
break;
}
}
if (!line_end)
break;
long chunk_len = strtol(in + pos, NULL, 16);
pos = (size_t)(line_end - in) + 2;
if (chunk_len <= 0)
break;
if (pos + (size_t)chunk_len > inlen)
break;
if (buf_append(&b, in + pos, (size_t)chunk_len) != 0) {
free(b.data);
return -1;
}
pos += (size_t)chunk_len;
if (pos + 2 <= inlen && in[pos] == '\r' && in[pos + 1] == '\n')
pos += 2;
}
*out = b.data;
return (long)b.len;
}
int http_post_json(const char *host, int port, const char *path,
const char *user, const char *pass,
const char *session_id_in,
const char *body, size_t body_len,
HttpResponse *resp, char *err, size_t errlen)
{
memset(resp, 0, sizeof(*resp));
Buf req = {0};
char line[1024];
int n = snprintf(line, sizeof(line), "POST %s HTTP/1.1\r\n", path);
buf_append(&req, line, (size_t)n);
n = snprintf(line, sizeof(line), "Host: %s:%d\r\n", host, port);
buf_append(&req, line, (size_t)n);
buf_append_str(&req, "User-Agent: transtui/1.0\r\n");
buf_append_str(&req, "Content-Type: application/json\r\n");
buf_append_str(&req, "Accept: application/json\r\n");
buf_append_str(&req, "Connection: close\r\n");
n = snprintf(line, sizeof(line), "Content-Length: %zu\r\n", body_len);
buf_append(&req, line, (size_t)n);
if (session_id_in && session_id_in[0]) {
n = snprintf(line, sizeof(line), "X-Transmission-Session-Id: %s\r\n", session_id_in);
buf_append(&req, line, (size_t)n);
}
if (user && user[0]) {
char cred[256];
n = snprintf(cred, sizeof(cred), "%s:%s", user, pass ? pass : "");
char b64[400];
long enc = base64_encode((unsigned char *)cred, (size_t)n, b64, sizeof(b64));
if (enc > 0) {
n = snprintf(line, sizeof(line), "Authorization: Basic %s\r\n", b64);
buf_append(&req, line, (size_t)n);
}
}
buf_append_str(&req, "\r\n");
if (body_len)
buf_append(&req, body, body_len);
int fd = net_connect(host, port, err, errlen);
if (fd < 0) {
free(req.data);
return -1;
}
size_t sent = 0;
while (sent < req.len) {
ssize_t w = write(fd, req.data + sent, req.len - sent);
if (w <= 0) {
snprintf(err, errlen, "failed to send HTTP request");
free(req.data);
close(fd);
return -1;
}
sent += (size_t)w;
}
free(req.data);
Buf resb = {0};
char chunk[4096];
long content_length = -1;
int chunked = 0;
size_t header_end = 0;
int have_headers = 0;
for (;;) {
ssize_t r = read(fd, chunk, sizeof(chunk));
if (r < 0) {
snprintf(err, errlen, "error reading response");
free(resb.data);
close(fd);
return -1;
}
if (r == 0)
break; /* server closed connection */
if (buf_append(&resb, chunk, (size_t)r) != 0) {
snprintf(err, errlen, "out of memory");
free(resb.data);
close(fd);
return -1;
}
if (!have_headers) {
char *sep = memmem(resb.data, resb.len, "\r\n\r\n", 4);
if (sep) {
have_headers = 1;
header_end = (size_t)(sep - resb.data) + 4;
content_length = extract_content_length(resb.data, header_end);
chunked = is_chunked(resb.data, header_end);
}
}
if (have_headers && !chunked && content_length >= 0) {
if (resb.len - header_end >= (size_t)content_length)
break;
}
}
close(fd);
if (!have_headers) {
snprintf(err, errlen, "incomplete HTTP response");
free(resb.data);
return -1;
}
/* Parse status line: "HTTP/1.1 200 OK" */
int status = 0;
sscanf(resb.data, "HTTP/%*d.%*d %d", &status);
resp->status = status;
extract_header(resb.data, header_end, "X-Transmission-Session-Id", resp->session_id, sizeof(resp->session_id));
const char *body_start = resb.data + header_end;
size_t avail = resb.len - header_end;
if (chunked) {
char *decoded = NULL;
long dlen = decode_chunked(body_start, avail, &decoded);
if (dlen < 0) {
snprintf(err, errlen, "could not decode chunked response");
free(resb.data);
return -1;
}
resp->body = decoded;
resp->body_len = (size_t)dlen;
free(resb.data);
} else {
size_t blen = avail;
if (content_length >= 0 && (size_t)content_length < blen)
blen = (size_t)content_length;
char *b = malloc(blen + 1);
if (!b) {
snprintf(err, errlen, "out of memory");
free(resb.data);
return -1;
}
memcpy(b, body_start, blen);
b[blen] = '\0';
resp->body = b;
resp->body_len = blen;
free(resb.data);
}
return 0;
}
void http_response_free(HttpResponse *resp)
{
free(resp->body);
resp->body = NULL;
resp->body_len = 0;
}