diff --git a/include/streams/brotli.h b/include/streams/brotli.h index 36b52c5..facc356 100644 --- a/include/streams/brotli.h +++ b/include/streams/brotli.h @@ -3,12 +3,20 @@ #include #include +#include #include "types.h" -typedef struct brotli_stream_state brotli_stream_state_t; +typedef int (*brotli_stream_chunk_cb)( + void *ctx, + const uint8_t *chunk, + size_t len +); +typedef struct brotli_stream_state brotli_stream_state_t; brotli_stream_state_t *brotli_stream_state_new(bool decompress); + void brotli_stream_state_destroy(brotli_stream_state_t *st); +bool brotli_stream_is_finished(brotli_stream_state_t *st); ant_value_t brotli_stream_transform( ant_t *js, brotli_stream_state_t *st, @@ -20,4 +28,18 @@ ant_value_t brotli_stream_flush( ant_value_t ctrl_obj ); +int brotli_stream_process( + brotli_stream_state_t *st, + const uint8_t *input, + size_t input_len, + brotli_stream_chunk_cb cb, + void *ctx +); + +int brotli_stream_finish( + brotli_stream_state_t *st, + brotli_stream_chunk_cb cb, + void *ctx +); + #endif diff --git a/src/modules/http.c b/src/modules/http.c new file mode 100644 index 0000000..19f965f --- /dev/null +++ b/src/modules/http.c @@ -0,0 +1,315 @@ +#include +#include +#include +#include +#include + +#include "modules/http.h" +#include "streams/brotli.h" + +struct ant_http_request_s { + tlsuv_http_t client; + ant_http_response_t response; + tlsuv_http_req_t *req; + + ant_http_response_cb on_response; + ant_http_body_cb on_body; + ant_http_complete_cb on_complete; + + void *user_data; + char *error_message; + brotli_stream_state_t *brotli_decoder; + + int error_code; + bool completed; + bool decode_brotli; +}; + +void ant_http_headers_free(ant_http_header_t *headers) { +while (headers) { + ant_http_header_t *next = headers->next; + free(headers->name); + free(headers->value); + free(headers); + headers = next; +}} + +static void ant_http_request_free(ant_http_request_t *req) { + if (!req) return; + free((char *)req->response.status_text); + ant_http_headers_free((ant_http_header_t *)req->response.headers); + free(req->error_message); + if (req->brotli_decoder) brotli_stream_state_destroy(req->brotli_decoder); + free(req); +} + +static char *ant_http_copy_slice(const char *src, size_t len) { + char *out = malloc(len + 1); + if (!out) return NULL; + memcpy(out, src, len); + out[len] = '\0'; + return out; +} + +static char *ant_http_build_host_url(const struct tlsuv_url_s *url) { + size_t size = 0; + char port_buf[16] = {0}; + int port_len = 0; + + if (url->port != 0) port_len = snprintf(port_buf, sizeof(port_buf), ":%u", url->port); + size = url->scheme_len + 3 + url->hostname_len + (size_t)port_len + 1; + + char *host_url = malloc(size); + if (!host_url) return NULL; + snprintf( + host_url, size, "%.*s://%.*s%s", + (int)url->scheme_len, url->scheme, + (int)url->hostname_len, url->hostname, + port_buf + ); + + return host_url; +} + +static char *ant_http_build_path(const struct tlsuv_url_s *url) { + const char *path = (url->path && url->path_len > 0) ? url->path : "/"; + size_t path_len = (url->path && url->path_len > 0) ? url->path_len : 1; + size_t query_extra = url->query && url->query_len > 0 ? (size_t)url->query_len + 1 : 0; + char *request_path = malloc(path_len + query_extra + 1); + if (!request_path) return NULL; + + memcpy(request_path, path, path_len); + if (query_extra > 0) { + request_path[path_len] = '?'; + memcpy(request_path + path_len + 1, url->query, url->query_len); + request_path[path_len + query_extra] = '\0'; + } else request_path[path_len] = '\0'; + + return request_path; +} + +static ant_http_header_t *ant_http_header_dup(const char *name, const char *value) { + ant_http_header_t *hdr = calloc(1, sizeof(*hdr)); + if (!hdr) return NULL; + + hdr->name = strdup(name ? name : ""); + hdr->value = strdup(value ? value : ""); + + if (!hdr->name || !hdr->value) { + free(hdr->name); + free(hdr->value); + free(hdr); + return NULL; + } + + return hdr; +} + +static ant_http_header_t *ant_http_copy_headers(tlsuv_http_resp_t *resp) { + ant_http_header_t *head = NULL; + ant_http_header_t **tail = &head; + tlsuv_http_hdr *hdr = NULL; + + LIST_FOREACH(hdr, &resp->headers, _next) { + ant_http_header_t *copy = ant_http_header_dup(hdr->name, hdr->value); + if (!copy) { + ant_http_headers_free(head); + return NULL; + } + + *tail = copy; + tail = ©->next; + } + + return head; +} + + +static const char *ant_http_find_header(const ant_http_header_t *headers, const char *name) { + for (const ant_http_header_t *entry = headers; entry; entry = entry->next) { + if (entry->name && name && strcasecmp(entry->name, name) == 0) return entry->value; + } + return NULL; +} + +static void ant_http_on_close(tlsuv_http_t *client) { + ant_http_request_t *req = (ant_http_request_t *)client->data; + if (!req) return; + + if (req->on_complete) req->on_complete( + req, req->error_code, + req->error_message, + req->user_data + ); + + ant_http_request_free(req); +} + +static void ant_http_complete(ant_http_request_t *req, int error_code, const char *error_message) { + if (!req || req->completed) return; + req->completed = 1; + req->error_code = error_code; + + free(req->error_message); + req->error_message = error_message ? strdup(error_message) : NULL; + + tlsuv_http_close(&req->client, ant_http_on_close); +} + +static int ant_http_brotli_body_cb(void *ctx, const uint8_t *chunk, size_t len) { + ant_http_request_t *req = (ant_http_request_t *)ctx; + if (req->on_body && len > 0) req->on_body(req, chunk, len, req->user_data); + return 0; +} + +static void ant_http_tlsuv_body_cb(tlsuv_http_req_t *http_req, char *body, ssize_t len) { + ant_http_request_t *req = (ant_http_request_t *)http_req->data; + if (!req) return; + + if (len == UV_EOF) { + if (req->decode_brotli && req->brotli_decoder && + brotli_stream_finish(req->brotli_decoder, ant_http_brotli_body_cb, req) != 0) { + ant_http_complete(req, UV_EINVAL, "brotli decompression failed"); + return; + } + + ant_http_complete(req, 0, NULL); + return; + } + + if (len < 0) { + ant_http_complete(req, (int)len, uv_strerror((int)len)); + return; + } + + if (req->decode_brotli && req->brotli_decoder) { + if (brotli_stream_process( + req->brotli_decoder, (const uint8_t *)body, (size_t)len, + ant_http_brotli_body_cb, req) != 0 + ) ant_http_complete(req, UV_EINVAL, "brotli decompression failed"); + return; + } + + if (req->on_body && len > 0) req->on_body(req, (const uint8_t *)body, (size_t)len, req->user_data); +} + +static void ant_http_resp_cb(tlsuv_http_resp_t *resp, void *data) { + ant_http_request_t *req = (ant_http_request_t *)resp->req->data; + + const char *content_encoding = NULL; + if (!req) return; + + if (resp->code < 0) { + ant_http_complete(req, resp->code, uv_strerror(resp->code)); + return; + } + + req->response.status = resp->code; + req->response.status_text = resp->status ? strdup(resp->status) : strdup(""); + req->response.headers = ant_http_copy_headers(resp); + + if (!req->response.status_text || (resp->headers.lh_first && !req->response.headers)) { + ant_http_complete(req, UV_ENOMEM, "out of memory"); + return; + } + + content_encoding = ant_http_find_header(req->response.headers, "content-encoding"); + if (content_encoding && strcasecmp(content_encoding, "br") == 0) { + req->brotli_decoder = brotli_stream_state_new(true); + if (!req->brotli_decoder) { + ant_http_complete(req, UV_ENOMEM, "out of memory"); + return; + } + req->decode_brotli = 1; + } + + resp->body_cb = ant_http_tlsuv_body_cb; + if (req->on_response) req->on_response(req, &req->response, req->user_data); +} + +const ant_http_response_t *ant_http_request_response(ant_http_request_t *req) { + return req ? &req->response : NULL; +} + +int ant_http_request_cancel(ant_http_request_t *req) { + if (!req || !req->req || req->completed) return 0; + return tlsuv_http_req_cancel(&req->client, req->req); +} + +int ant_http_request_start( + uv_loop_t *loop, + const ant_http_request_options_t *options, + ant_http_response_cb on_response, + ant_http_body_cb on_body, + ant_http_complete_cb on_complete, + void *user_data, + ant_http_request_t **out_req +) { + struct tlsuv_url_s parsed = {0}; + ant_http_request_t *req = NULL; + char *host_url = NULL; + char *request_path = NULL; + int rc = 0; + + if (out_req) *out_req = NULL; + if (!loop || !options || !options->method || !options->url) return UV_EINVAL; + if (tlsuv_parse_url(&parsed, options->url) != 0) return UV_EINVAL; + if (!parsed.scheme || !parsed.hostname) return UV_EINVAL; + + req = calloc(1, sizeof(ant_http_request_t)); + if (!req) return UV_ENOMEM; + + req->on_response = on_response; + req->on_body = on_body; + req->on_complete = on_complete; + req->user_data = user_data; + + host_url = ant_http_build_host_url(&parsed); + request_path = ant_http_build_path(&parsed); + if (!host_url || !request_path) { + free(host_url); + free(request_path); + ant_http_request_free(req); + return UV_ENOMEM; + } + + rc = tlsuv_http_init(loop, &req->client, host_url); + free(host_url); + if (rc != 0) { + free(request_path); + ant_http_request_free(req); + return rc; + } + + req->client.data = req; + tlsuv_http_header(&req->client, "Accept-Encoding", NULL); + + req->req = tlsuv_http_req( + &req->client, + options->method, + request_path, ant_http_resp_cb, req + ); + free(request_path); + + if (!req->req) { + tlsuv_http_close(&req->client, NULL); + ant_http_request_free(req); + return UV_ENOMEM; + } + + req->req->data = req; + for (const ant_http_header_t *hdr = options->headers; hdr; hdr = hdr->next) { + tlsuv_http_req_header(req->req, hdr->name, hdr->value); + } + + if (options->body && options->body_len > 0) { + rc = tlsuv_http_req_data(req->req, (const char *)options->body, options->body_len, NULL); + if (rc != 0) { + ant_http_complete(req, rc, uv_strerror(rc)); + if (out_req) *out_req = req; + return 0; + }} + + if (out_req) *out_req = req; + return 0; +} diff --git a/src/streams/brotli.c b/src/streams/brotli.c index dbbef31..b8710a1 100644 --- a/src/streams/brotli.c +++ b/src/streams/brotli.c @@ -11,7 +11,7 @@ #include "streams/brotli.h" #include "streams/transform.h" -#define BROTLI_CHUNK_SIZE 16384 +#define BROTLI_CHUNK_SIZE (1024 * 32) struct brotli_stream_state { bool decompress; @@ -21,6 +21,14 @@ struct brotli_stream_state { } u; }; +static int brotli_emit_chunk( + brotli_stream_chunk_cb cb, void *ctx, + const uint8_t *data, size_t len +) { + if (!cb || len == 0) return 0; + return cb(ctx, data, len); +} + static ant_value_t brotli_enqueue_buffer( ant_t *js, ant_value_t ctrl_obj, const uint8_t *data, size_t len ) { @@ -33,9 +41,13 @@ static ant_value_t brotli_enqueue_buffer( return ts_ctrl_enqueue(js, ctrl_obj, arr); } -static ant_value_t brotli_encoder_step( - ant_t *js, brotli_stream_state_t *st, ant_value_t ctrl_obj, - const uint8_t *input, size_t input_len, BrotliEncoderOperation op +static int brotli_encoder_step( + brotli_stream_state_t *st, + const uint8_t *input, + size_t input_len, + BrotliEncoderOperation op, + brotli_stream_chunk_cb cb, + void *ctx ) { size_t avail_in = input_len; const uint8_t *next_in = input; @@ -47,14 +59,11 @@ static ant_value_t brotli_encoder_step( if (!BrotliEncoderCompressStream( st->u.enc, op, &avail_in, &next_in, &avail_out, &next_out, NULL)) { - return js_mkerr_typed(js, JS_ERR_TYPE, "Compression failed"); + return -1; } size_t have = sizeof(out_buf) - avail_out; - if (have > 0) { - ant_value_t r = brotli_enqueue_buffer(js, ctrl_obj, out_buf, have); - if (is_err(r)) return r; - } + if (brotli_emit_chunk(cb, ctx, out_buf, have) != 0) return -1; if (op == BROTLI_OPERATION_FINISH) { if (BrotliEncoderIsFinished(st->u.enc) && @@ -63,12 +72,15 @@ static ant_value_t brotli_encoder_step( } else if (avail_in == 0 && !BrotliEncoderHasMoreOutput(st->u.enc)) break; } - return js_mkundef(); + return 0; } -static ant_value_t brotli_decoder_step( - ant_t *js, brotli_stream_state_t *st, ant_value_t ctrl_obj, - const uint8_t *input, size_t input_len +static int brotli_decoder_step( + brotli_stream_state_t *st, + const uint8_t *input, + size_t input_len, + brotli_stream_chunk_cb cb, + void *ctx ) { size_t avail_in = input_len; const uint8_t *next_in = input; @@ -82,20 +94,16 @@ static ant_value_t brotli_decoder_step( st->u.dec, &avail_in, &next_in, &avail_out, &next_out, NULL); size_t have = sizeof(out_buf) - avail_out; - if (have > 0) { - ant_value_t r = brotli_enqueue_buffer(js, ctrl_obj, out_buf, have); - if (is_err(r)) return r; - } + if (brotli_emit_chunk(cb, ctx, out_buf, have) != 0) return -1; - if (ret == BROTLI_DECODER_RESULT_ERROR) - return js_mkerr_typed(js, JS_ERR_TYPE, "Decompression failed"); + if (ret == BROTLI_DECODER_RESULT_ERROR) return -1; if (ret == BROTLI_DECODER_RESULT_SUCCESS) break; if (ret == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT || BrotliDecoderHasMoreOutput(st->u.dec)) continue; if (ret == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT) break; } - return js_mkundef(); + return 0; } brotli_stream_state_t *brotli_stream_state_new(bool decompress) { @@ -125,26 +133,75 @@ void brotli_stream_state_destroy(brotli_stream_state_t *st) { free(st); } +int brotli_stream_process( + brotli_stream_state_t *st, + const uint8_t *input, + size_t input_len, + brotli_stream_chunk_cb cb, + void *ctx +) { + if (!st) return -1; + if (st->decompress) return brotli_decoder_step(st, input, input_len, cb, ctx); + return brotli_encoder_step(st, input, input_len, BROTLI_OPERATION_PROCESS, cb, ctx); +} + +int brotli_stream_finish( + brotli_stream_state_t *st, + brotli_stream_chunk_cb cb, + void *ctx +) { + if (!st) return -1; + + if (st->decompress) { + if (brotli_decoder_step(st, NULL, 0, cb, ctx) != 0) return -1; + return BrotliDecoderIsFinished(st->u.dec) ? 0 : -1; + } + + return brotli_encoder_step(st, NULL, 0, BROTLI_OPERATION_FINISH, cb, ctx); +} + +bool brotli_stream_is_finished(brotli_stream_state_t *st) { + if (!st) return false; + if (st->decompress) return BrotliDecoderIsFinished(st->u.dec); + return BrotliEncoderIsFinished(st->u.enc); +} + +typedef struct { + ant_t *js; + ant_value_t ctrl_obj; + ant_value_t error; +} brotli_js_emit_ctx_t; + +static int brotli_enqueue_chunk_cb(void *ctx, const uint8_t *chunk, size_t len) { + brotli_js_emit_ctx_t *emit = (brotli_js_emit_ctx_t *)ctx; + ant_value_t result = brotli_enqueue_buffer(emit->js, emit->ctrl_obj, chunk, len); + if (is_err(result)) { + emit->error = result; + return -1; + } + return 0; +} + ant_value_t brotli_stream_transform( ant_t *js, brotli_stream_state_t *st, ant_value_t ctrl_obj, const uint8_t *input, size_t input_len ) { - if (st->decompress) - return brotli_decoder_step(js, st, ctrl_obj, input, input_len); - return brotli_encoder_step(js, st, ctrl_obj, input, input_len, BROTLI_OPERATION_PROCESS); + brotli_js_emit_ctx_t emit = { .js = js, .ctrl_obj = ctrl_obj, .error = js_mkundef() }; + if (brotli_stream_process(st, input, input_len, brotli_enqueue_chunk_cb, &emit) != 0) { + if (is_err(emit.error)) return emit.error; + return js_mkerr_typed(js, JS_ERR_TYPE, st && st->decompress ? "Decompression failed" : "Compression failed"); + } + return js_mkundef(); } ant_value_t brotli_stream_flush( ant_t *js, brotli_stream_state_t *st, ant_value_t ctrl_obj ) { - if (st->decompress) { - ant_value_t result = brotli_decoder_step(js, st, ctrl_obj, NULL, 0); - if (is_err(result)) return result; - if (!BrotliDecoderIsFinished(st->u.dec)) - return js_mkerr_typed(js, JS_ERR_TYPE, "Decompression failed"); - return js_mkundef(); + brotli_js_emit_ctx_t emit = { .js = js, .ctrl_obj = ctrl_obj, .error = js_mkundef() }; + if (brotli_stream_finish(st, brotli_enqueue_chunk_cb, &emit) != 0) { + if (is_err(emit.error)) return emit.error; + return js_mkerr_typed(js, JS_ERR_TYPE, st && st->decompress ? "Decompression failed" : "Compression failed"); } - - return brotli_encoder_step(js, st, ctrl_obj, NULL, 0, BROTLI_OPERATION_FINISH); + return js_mkundef(); }