mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-03 07:54:40 +08:00
7572 lines
303 KiB
C
7572 lines
303 KiB
C
/* node:http — the SERVER and CLIENT slices, layered on scr_net.c's
|
|
* native hooks: a hand-written HTTP/1.1 parser (request OR status line,
|
|
* headers, Content-Length / chunked / read-to-EOF bodies, keep-alive)
|
|
* and Node-shaped serializers (the response's writeHead/setHeader/end
|
|
* with Content-Length or chunked framing, the Date header, keep-alive
|
|
* Connection headers; the client's request head with Node's exact
|
|
* header order and framing decisions).
|
|
*
|
|
* The CLIENT (scr_http_request — http.request/http.get): one dialed
|
|
* connection per request, NO agent pooling (the wire still carries
|
|
* Node's Connection: keep-alive; the socket closes when the response
|
|
* completes — SEMANTICS.md). The response is an ordinary ScrHttpReq
|
|
* (IncomingMessage) parsed in RESPONSE mode: status line instead of
|
|
* request line, and a body that may be EOF-delimited; HEAD/204/304
|
|
* responses have none. Error shapes are Node's: the net layer's
|
|
* 'connect ECONNREFUSED ip:port' via the socket's native error hook, a
|
|
* premature close before any response is 'socket hang up' on the
|
|
* request, and mid-body death is 'aborted' on the RESPONSE (the request
|
|
* just closes) — all pinned by the client differential fixtures.
|
|
* Deferred 'close' emits ride the emit QUEUE drained from scr_net.c's
|
|
* proto sweep, so req/res 'close' fire a pass after the work that
|
|
* flagged them, Node's later-than-the-handler ordering (client order:
|
|
* res 'end', req 'close', res 'close').
|
|
* NO external dependencies — the parser implements exactly what the
|
|
* differential fixtures and portless-shaped handlers exercise, and
|
|
* SEMANTICS.md states its bounds honestly.
|
|
*
|
|
* Object model: http.createServer returns an ORDINARY ScrNetServer (the
|
|
* frontend maps http.Server to the same netServer kind — listen/close/
|
|
* address()/error all reuse the net lowering) whose native-connection
|
|
* hook installs one ScrHttpConn parser per accepted socket. ScrHttpReq /
|
|
* ScrHttpRes are lean refcounted handles (the ScrChild story): the req
|
|
* holds the parsed method/url/headers and the body listener lists
|
|
* (dropped when the body completes or the connection dies); the res
|
|
* holds the socket (+1), the pending header list, and the framing state.
|
|
*
|
|
* Keep-alive: HTTP/1.1 requests keep the connection open (1.0 opts in
|
|
* with Connection: keep-alive) and the parser resets for the next
|
|
* request on the same socket, pipelining included; Connection: close —
|
|
* either side — ends the socket after the response finishes. An open
|
|
* connection idles for keepAliveTimeout + keepAliveTimeoutBuffer (6 s by
|
|
* default) after each response, then times out like Node's.
|
|
*
|
|
* Event dispatch rides scr_net.c wholesale: the request handler and the
|
|
* req 'data'/'end' listeners fire from the socket read path (macrotasks,
|
|
* snapshot semantics, once-before-run), and errors/teardown follow the
|
|
* socket's own story. */
|
|
#include "scr_runtime.h"
|
|
|
|
#include <ctype.h>
|
|
#include <math.h> /* INFINITY — the Agent's maxSockets default */
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
/* The HTTP/1 parser has no configurable maxHeaderSize surface yet. Keep its
|
|
* head and trailer block within the existing 64 KiB parser bound, and cap
|
|
* fields per block at Node's default of 1000 (server.maxHeadersCount unset):
|
|
* each stored field owns three strings and three growing arrays. A server
|
|
* answers 431 past either bound or SCR_HTTP_MAX_HEADER_SIZE, like Node. */
|
|
#define SCR_HTTP_MAX_HEADER_BYTES 65536u
|
|
#define SCR_HTTP_MAX_HEADER_FIELDS 1000u
|
|
/* Node's default maxHeaderSize (http.maxHeaderSize): the request target
|
|
* plus every header name and value, without separators or line endings,
|
|
* must stay below it. */
|
|
#define SCR_HTTP_MAX_HEADER_SIZE 16384u
|
|
|
|
static void scr_http_oom(void) {
|
|
fputs("scriptc: out of memory\n", stderr);
|
|
abort();
|
|
}
|
|
|
|
static bool scr_http_timeout_valid(double ms) {
|
|
if (isfinite(ms) && ms >= 0) return true;
|
|
char recv[48], msg[176];
|
|
scr_num_received(ms, recv);
|
|
int len = snprintf(msg, sizeof msg,
|
|
"The value of \"msecs\" is out of range. It must be a non-negative finite number. Received %s", recv);
|
|
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
|
|
return false;
|
|
}
|
|
|
|
/* Node v24.22.0's STATUS_CODES table, shared by the module export and the
|
|
* default ServerResponse reason phrase. */
|
|
static const struct { int code; const char *reason; } scr_http_status_codes_table[] = {
|
|
{100, "Continue"}, {101, "Switching Protocols"}, {102, "Processing"},
|
|
{103, "Early Hints"}, {200, "OK"}, {201, "Created"}, {202, "Accepted"},
|
|
{203, "Non-Authoritative Information"}, {204, "No Content"},
|
|
{205, "Reset Content"}, {206, "Partial Content"}, {207, "Multi-Status"},
|
|
{208, "Already Reported"}, {226, "IM Used"}, {300, "Multiple Choices"},
|
|
{301, "Moved Permanently"}, {302, "Found"}, {303, "See Other"},
|
|
{304, "Not Modified"}, {305, "Use Proxy"}, {307, "Temporary Redirect"},
|
|
{308, "Permanent Redirect"}, {400, "Bad Request"}, {401, "Unauthorized"},
|
|
{402, "Payment Required"}, {403, "Forbidden"}, {404, "Not Found"},
|
|
{405, "Method Not Allowed"}, {406, "Not Acceptable"},
|
|
{407, "Proxy Authentication Required"}, {408, "Request Timeout"},
|
|
{409, "Conflict"}, {410, "Gone"}, {411, "Length Required"},
|
|
{412, "Precondition Failed"}, {413, "Payload Too Large"},
|
|
{414, "URI Too Long"}, {415, "Unsupported Media Type"},
|
|
{416, "Range Not Satisfiable"}, {417, "Expectation Failed"},
|
|
{418, "I'm a Teapot"}, {421, "Misdirected Request"},
|
|
{422, "Unprocessable Entity"}, {423, "Locked"},
|
|
{424, "Failed Dependency"}, {425, "Too Early"},
|
|
{426, "Upgrade Required"}, {428, "Precondition Required"},
|
|
{429, "Too Many Requests"}, {431, "Request Header Fields Too Large"},
|
|
{451, "Unavailable For Legal Reasons"}, {500, "Internal Server Error"},
|
|
{501, "Not Implemented"}, {502, "Bad Gateway"},
|
|
{503, "Service Unavailable"}, {504, "Gateway Timeout"},
|
|
{505, "HTTP Version Not Supported"}, {506, "Variant Also Negotiates"},
|
|
{507, "Insufficient Storage"}, {508, "Loop Detected"},
|
|
{509, "Bandwidth Limit Exceeded"}, {510, "Not Extended"},
|
|
{511, "Network Authentication Required"},
|
|
};
|
|
|
|
static const char *scr_http_reason(int code) {
|
|
for (size_t i = 0; i < sizeof scr_http_status_codes_table / sizeof scr_http_status_codes_table[0]; i++) {
|
|
if (scr_http_status_codes_table[i].code == code) return scr_http_status_codes_table[i].reason;
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
static SCR_TL ScrDyn *scr_http_status_codes_value;
|
|
|
|
static void scr_http_status_codes_cleanup(void) {
|
|
scr_dyn_release(scr_http_status_codes_value);
|
|
scr_http_status_codes_value = NULL;
|
|
}
|
|
|
|
ScrDyn *scr_http_status_codes(void) {
|
|
if (!scr_http_status_codes_value) {
|
|
ScrDyn *codes = scr_dyn_new_obj();
|
|
for (size_t i = 0; i < sizeof scr_http_status_codes_table / sizeof scr_http_status_codes_table[0]; i++) {
|
|
const int code = scr_http_status_codes_table[i].code;
|
|
const char *reason = scr_http_status_codes_table[i].reason;
|
|
char key[4];
|
|
const int key_len = snprintf(key, sizeof key, "%d", code);
|
|
ScrStr *value = scr_str_new(reason, strlen(reason));
|
|
scr_dyn_obj_set(codes, key, (size_t)key_len, scr_dyn_new_str(value));
|
|
scr_str_release(value);
|
|
}
|
|
scr_http_status_codes_value = codes;
|
|
scr_atexit(scr_http_status_codes_cleanup);
|
|
}
|
|
return scr_dyn_retain(scr_http_status_codes_value);
|
|
}
|
|
|
|
static const char *const scr_http_methods_table[] = {
|
|
"ACL", "BIND", "CHECKOUT", "CONNECT", "COPY", "DELETE", "GET", "HEAD",
|
|
"LINK", "LOCK", "M-SEARCH", "MERGE", "MKACTIVITY", "MKCALENDAR",
|
|
"MKCOL", "MOVE", "NOTIFY", "OPTIONS", "PATCH", "POST", "PROPFIND",
|
|
"PROPPATCH", "PURGE", "PUT", "QUERY", "REBIND", "REPORT", "SEARCH",
|
|
"SOURCE", "SUBSCRIBE", "TRACE", "UNBIND", "UNLINK", "UNLOCK", "UNSUBSCRIBE",
|
|
};
|
|
|
|
static SCR_TL ScrArr *scr_http_methods_value;
|
|
|
|
static void scr_http_methods_cleanup(void) {
|
|
scr_arr_release(scr_http_methods_value);
|
|
scr_http_methods_value = NULL;
|
|
}
|
|
|
|
ScrArr *scr_http_methods(void) {
|
|
if (!scr_http_methods_value) {
|
|
const size_t n = sizeof scr_http_methods_table / sizeof scr_http_methods_table[0];
|
|
ScrArr *methods = scr_arr_new(SCR_ELEM_STR, n);
|
|
for (size_t i = 0; i < n; i++) {
|
|
const char *name = scr_http_methods_table[i];
|
|
scr_arr_push_ref(methods, scr_str_new(name, strlen(name)));
|
|
}
|
|
scr_http_methods_value = methods;
|
|
scr_atexit(scr_http_methods_cleanup);
|
|
}
|
|
return scr_arr_retain(scr_http_methods_value);
|
|
}
|
|
|
|
/* ── the h2 compat transport seam ────────────────────────────────────────
|
|
*
|
|
* Http2ServerRequest/Http2ServerResponse ARE these req/res handles: the
|
|
* http/1 surface is the API template, the h2 stream machinery is the
|
|
* transport. scr_http2.c (linked exactly when the program uses the real
|
|
* h2 surface — this unit can link WITHOUT it) installs a vtable for the
|
|
* response write paths (HEADERS/DATA frames instead of HTTP/1 bytes) and
|
|
* a request-registration hook so server.on("request", ...) on an
|
|
* h2-tagged server ctx routes to the h2 request list. The vtable's
|
|
* typedef lives in scr_runtime.h (both units name it). */
|
|
static SCR_TL const ScrHttpH2Ops *scr_http_h2_ops = NULL;
|
|
static SCR_TL void (*scr_http_h2_request_hook)(void *h2ctx, ScrClosure *cb /*moves*/,
|
|
void *fn, bool once) = NULL;
|
|
static SCR_TL void (*scr_http_h2_connect_hook)(void *h2ctx, ScrClosure *cb /*moves*/,
|
|
void *h1_fn, void *h2_fn, bool once) = NULL;
|
|
static SCR_TL void (*scr_http_h2_upgrade_hook)(void *h2ctx, ScrClosure *cb /*moves*/,
|
|
void *fn, bool once) = NULL;
|
|
|
|
void scr_http_set_h2_ops(const ScrHttpH2Ops *ops) { scr_http_h2_ops = ops; }
|
|
|
|
void scr_http_set_h2_request_hook(void (*hook)(void *h2ctx, ScrClosure *cb, void *fn, bool once)) {
|
|
scr_http_h2_request_hook = hook;
|
|
}
|
|
void scr_http_set_h2_connect_hook(void (*hook)(void *h2ctx, ScrClosure *cb,
|
|
void *h1_fn, void *h2_fn, bool once)) {
|
|
scr_http_h2_connect_hook = hook;
|
|
}
|
|
void scr_http_set_h2_upgrade_hook(void (*hook)(void *h2ctx, ScrClosure *cb, void *fn, bool once)) {
|
|
scr_http_h2_upgrade_hook = hook;
|
|
}
|
|
|
|
/* ── the request handle ──────────────────────────────────────────────── */
|
|
|
|
struct ScrHttpReq {
|
|
size_t rc;
|
|
/* The AsyncLocalStorage context of its creation (+1, NULL outside a
|
|
* store): the deferred emits run in it (scr_http_proto_sweep). A server
|
|
* request is created in its connection's dispatch, which runs in the
|
|
* server's listen() context; a client response in its request's. */
|
|
ScrAlsCtx *als;
|
|
int status; /* -1 on server requests (Node's undefined statusCode) */
|
|
ScrStr *method;
|
|
ScrStr *url;
|
|
ScrNetSocket *sock; /* +1 — req.socket, may be NULL defensively */
|
|
void *h2_stream; /* +1 through scr_http_h2_ops — the h2 compat request's
|
|
* stream (destroy() RSTs it, never the shared socket) */
|
|
ScrStr **hnames; /* lowercased */
|
|
ScrStr **hnames_raw; /* arrival case — rawHeaders */
|
|
ScrStr **hvalues;
|
|
size_t nheaders;
|
|
ScrStr **tnames; /* parsed chunk trailers: lowercased, separate from headers */
|
|
ScrStr **tnames_raw; /* wire case for rawTrailers */
|
|
ScrStr **tvalues;
|
|
size_t ntrailers;
|
|
ScrStr *status_msg; /* client responses' reason phrase; NULL on server requests */
|
|
ScrNetLs data_ls, end_ls, err_ls, close_ls, timeout_ls;
|
|
/* pipe destinations (req.pipe(...) — one of each kind, +1; released at
|
|
* finish, so a piped destination never outlives the body) */
|
|
ScrHttpRes *pipe_res;
|
|
struct ScrHttpClientReq *pipe_client;
|
|
ScrNetSocket *pipe_sock;
|
|
bool ended; /* body complete (or connection dead): data/end drop */
|
|
bool end_consumed; /* 'end' fired on a body read through data listeners or a pipe */
|
|
bool enc_utf8; /* setEncoding('utf8'): 'data' delivers strings */
|
|
bool http10; /* the parsed request/status line's version (httpVersion) */
|
|
bool http2; /* an h2 compat request (httpVersion "2.0") */
|
|
bool aborted; /* h2: the stream died with our writable side open */
|
|
bool close_queued;
|
|
bool close_emitted; /* settled: err/close listeners dropped */
|
|
bool join_dup; /* joinDuplicateHeaders: repeated names read joined ", " */
|
|
/* pause(): delivery holds — arrived body bytes buffer in pend and
|
|
* 'end' defers (end_pending) until resume() drains through the emit
|
|
* queue, never the resuming stack. A held HTTP/1 server request stops
|
|
* its socket's reads once pend reaches SCR_HTTP_REQ_HOLD_BYTES
|
|
* (sock_held), as Node's readStop does. */
|
|
bool paused;
|
|
bool end_pending;
|
|
bool drain_queued;
|
|
char *pend;
|
|
size_t pend_len, pend_cap;
|
|
/* HTTP/1 server requests start paused like Node's IncomingMessage: body
|
|
* bytes and the completed body's 'end' are held in pend/end_pending until
|
|
* a consumer starts the flow (a 'data' listener, pipe() or resume()), or
|
|
* until the response finishes and the unread request is dumped. */
|
|
bool gated; /* holds until flowing */
|
|
bool flowing; /* a consumer or the dump started the flow */
|
|
bool received; /* the whole body arrived (req.complete) */
|
|
bool body_seen; /* body bytes arrived */
|
|
bool destroyed; /* req.destroy()/the teardown ran — req.destroyed */
|
|
bool sock_held; /* this request stopped its socket's reads */
|
|
ScrNetLs aborted_ls; /* req.on('aborted') — the h2 compat event */
|
|
};
|
|
|
|
#ifdef SCR_RC_AUDIT
|
|
static SCR_TL long scr_http_live = 0;
|
|
long scr_http_live_count(void) { return scr_http_live; }
|
|
#endif
|
|
|
|
ScrHttpReq *scr_http_req_retain(ScrHttpReq *r) {
|
|
if (r->rc != SIZE_MAX) r->rc++;
|
|
return r;
|
|
}
|
|
|
|
/* req.setEncoding(enc) — IncomingMessage's readable setEncoding (server
|
|
* requests and client responses alike): utf8 flips 'data' delivery to
|
|
* strings (the chunk-encoding window); other REAL Node encodings throw
|
|
* the "not supported yet" Error; unknown names throw Node's
|
|
* ERR_UNKNOWN_ENCODING TypeError. */
|
|
void scr_http_req_set_encoding(ScrHttpReq *r, ScrStr *enc /*borrowed*/) {
|
|
if ((enc->len == 4 && memcmp(enc->data, "utf8", 4) == 0) ||
|
|
(enc->len == 5 && memcmp(enc->data, "utf-8", 5) == 0)) {
|
|
r->enc_utf8 = true;
|
|
return;
|
|
}
|
|
static const char *const known[] = { "ascii", "latin1", "binary", "base64",
|
|
"base64url", "hex", "ucs2", "ucs-2", "utf16le", "utf-16le", NULL };
|
|
for (size_t i = 0; known[i]; i++) {
|
|
if (enc->len == strlen(known[i]) && memcmp(enc->data, known[i], enc->len) == 0) {
|
|
char msg[128];
|
|
int n = snprintf(msg, sizeof msg, "setEncoding('%s') is not supported yet (only 'utf8' here)",
|
|
known[i]);
|
|
scr_throw_error_msg(SCR_ERR_ERROR, msg, (size_t)n);
|
|
return;
|
|
}
|
|
}
|
|
char msg[128];
|
|
int n = snprintf(msg, sizeof msg, "Unknown encoding: %.*s",
|
|
(int)(enc->len < 64 ? enc->len : 64), enc->data);
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)n, "ERR_UNKNOWN_ENCODING");
|
|
}
|
|
|
|
void scr_http_req_release(ScrHttpReq *r) {
|
|
if (!r || r->rc == SIZE_MAX) return;
|
|
if (--r->rc == 0) {
|
|
scr_als_ctx_release(r->als);
|
|
scr_str_release(r->method);
|
|
scr_str_release(r->url);
|
|
for (size_t i = 0; i < r->nheaders; i++) {
|
|
scr_str_release(r->hnames[i]);
|
|
scr_str_release(r->hnames_raw[i]);
|
|
scr_str_release(r->hvalues[i]);
|
|
}
|
|
free(r->hnames);
|
|
free(r->hnames_raw);
|
|
free(r->hvalues);
|
|
for (size_t i = 0; i < r->ntrailers; i++) {
|
|
scr_str_release(r->tnames[i]);
|
|
scr_str_release(r->tnames_raw[i]);
|
|
scr_str_release(r->tvalues[i]);
|
|
}
|
|
free(r->tnames);
|
|
free(r->tnames_raw);
|
|
free(r->tvalues);
|
|
scr_str_release(r->status_msg);
|
|
scr_net_ls_drop(&r->data_ls);
|
|
scr_net_ls_drop(&r->end_ls);
|
|
scr_net_ls_drop(&r->err_ls);
|
|
scr_net_ls_drop(&r->close_ls);
|
|
scr_net_ls_drop(&r->timeout_ls);
|
|
scr_net_ls_drop(&r->aborted_ls);
|
|
scr_http_res_release(r->pipe_res);
|
|
scr_http_client_release(r->pipe_client);
|
|
scr_net_sock_release(r->pipe_sock);
|
|
free(r->pend);
|
|
if (r->sock) {
|
|
if (r->sock_held) scr_net_sock_hold(r->sock, false);
|
|
scr_net_sock_release(r->sock);
|
|
}
|
|
if (r->h2_stream) scr_http_h2_ops->release(r->h2_stream);
|
|
#ifdef SCR_RC_AUDIT
|
|
scr_http_live--;
|
|
#endif
|
|
free(r);
|
|
}
|
|
}
|
|
|
|
void *scr_http_req_retain_v(void *p) { return scr_http_req_retain((ScrHttpReq *)p); }
|
|
void scr_http_req_release_v(void *p) { scr_http_req_release((ScrHttpReq *)p); }
|
|
|
|
ScrStr *scr_http_req_url(ScrHttpReq *r) { return scr_str_retain(r->url); }
|
|
ScrStr *scr_http_req_method(ScrHttpReq *r) { return scr_str_retain(r->method); }
|
|
|
|
/* Case-insensitive equality of HTTP field names. Incoming names happen to
|
|
* be lowercased already; outgoing names retain the spelling of the setter. */
|
|
static bool scr_http_name_eq(const ScrStr *lower, const ScrStr *name) {
|
|
if (lower->len != name->len) return false;
|
|
for (size_t j = 0; j < name->len; j++) {
|
|
if (tolower((unsigned char)lower->data[j]) != tolower((unsigned char)name->data[j])) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool scr_http_trailer_single_value(const ScrStr *name);
|
|
|
|
/* The value Node's IncomingMessage stores for names[index]'s field
|
|
* (_addHeaderLine): repeated fields join with ", " ("; " for cookie),
|
|
* except that the single-value fields (host, content-type, ...) keep the
|
|
* first unless joinDuplicateHeaders is set. set-cookie joins with ", " in
|
|
* this string form; its array form is scr_http_req_header_list. Always
|
|
* +1. */
|
|
static ScrStr *scr_http_field_value(ScrStr *const *names, ScrStr *const *values, size_t count,
|
|
size_t index, bool join_dup) {
|
|
const ScrStr *name = names[index];
|
|
if (!join_dup && scr_http_trailer_single_value(name)) return scr_str_retain(values[index]);
|
|
size_t total = 0, n = 0;
|
|
for (size_t i = 0; i < count; i++) {
|
|
if (!scr_http_name_eq(names[i], name)) continue;
|
|
total += values[i]->len;
|
|
n++;
|
|
}
|
|
if (n == 1) return scr_str_retain(values[index]);
|
|
bool cookie = name->len == 6 && memcmp(name->data, "cookie", 6) == 0;
|
|
const char *sep = cookie ? "; " : ", ";
|
|
char *buf = malloc(total + (n - 1) * 2);
|
|
if (!buf) scr_http_oom();
|
|
size_t off = 0;
|
|
for (size_t i = 0; i < count; i++) {
|
|
if (!scr_http_name_eq(names[i], name)) continue;
|
|
if (off) {
|
|
memcpy(buf + off, sep, 2);
|
|
off += 2;
|
|
}
|
|
memcpy(buf + off, values[i]->data, values[i]->len);
|
|
off += values[i]->len;
|
|
}
|
|
ScrStr *value = scr_str_new(buf, off);
|
|
free(buf);
|
|
return value;
|
|
}
|
|
|
|
/* The snapshot pairs behind `{ ...req.headers }` (the record-building
|
|
* helper's feed): [lowercased name, value, ...] in arrival order — the
|
|
* same keys the per-name reads answer: each name once, at its first
|
|
* position, with scr_http_field_value's value. Always a fresh +1 array. */
|
|
ScrArr *scr_http_req_header_pairs(ScrHttpReq *r) {
|
|
ScrArr *out = scr_arr_new(SCR_ELEM_STR, r->nheaders * 2);
|
|
for (size_t i = 0; i < r->nheaders; i++) {
|
|
bool seen = false;
|
|
for (size_t k = 0; k < i && !seen; k++) seen = scr_http_name_eq(r->hnames[k], r->hnames[i]);
|
|
if (seen) continue;
|
|
scr_arr_push_ref(out, scr_str_retain(r->hnames[i]));
|
|
scr_arr_push_ref(out, scr_http_field_value(r->hnames, r->hvalues, r->nheaders, i, r->join_dup));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/* Every header line as [lowercased name, value, ...] in arrival order,
|
|
* repeated names included: the fetch Response's Headers feed. Always a
|
|
* fresh +1 array. */
|
|
ScrArr *scr_http_req_header_lines(ScrHttpReq *r) {
|
|
ScrArr *out = scr_arr_new(SCR_ELEM_STR, r->nheaders * 2);
|
|
for (size_t i = 0; i < r->nheaders; i++) {
|
|
scr_arr_push_ref(out, scr_str_retain(r->hnames[i]));
|
|
scr_arr_push_ref(out, scr_str_retain(r->hvalues[i]));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/* req.rawHeaders: [name, value, name, value, ...] in arrival order, names
|
|
* in their ORIGINAL case (Node's shape). Always a fresh +1 array. */
|
|
ScrArr *scr_http_req_raw_headers(ScrHttpReq *r) {
|
|
ScrArr *out = scr_arr_new(SCR_ELEM_STR, r->nheaders * 2);
|
|
for (size_t i = 0; i < r->nheaders; i++) {
|
|
scr_arr_push_ref(out, scr_str_retain(r->hnames_raw[i]));
|
|
scr_arr_push_ref(out, scr_str_retain(r->hvalues[i]));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
static ScrArr *scr_http_req_distinct_values(ScrStr *const *names, ScrStr *const *values,
|
|
size_t count, ScrStr *name) {
|
|
ScrArr *out = NULL;
|
|
for (size_t i = 0; i < count; i++) {
|
|
if (!scr_http_name_eq(names[i], name)) continue;
|
|
if (!out) out = scr_arr_new(SCR_ELEM_STR, count);
|
|
scr_arr_push_ref(out, scr_str_retain(values[i]));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
ScrArr *scr_http_req_header_values(ScrHttpReq *r, ScrStr *name /*borrowed*/) {
|
|
return scr_http_req_distinct_values(r->hnames, r->hvalues, r->nheaders, name);
|
|
}
|
|
|
|
/* Every value of a header in arrival order, as Node stores set-cookie:
|
|
* a fresh +1 array, empty when the header is absent. */
|
|
ScrArr *scr_http_req_header_list(ScrHttpReq *r, ScrStr *name /*borrowed*/) {
|
|
ScrArr *out = scr_http_req_header_values(r, name);
|
|
return out ? out : scr_arr_new(SCR_ELEM_STR, 0);
|
|
}
|
|
|
|
ScrArr *scr_http_req_trailer_values(ScrHttpReq *r, ScrStr *name /*borrowed*/) {
|
|
return r->ended ? scr_http_req_distinct_values(r->tnames, r->tvalues, r->ntrailers, name) : NULL;
|
|
}
|
|
|
|
/* Trailers become visible only once the body reaches its natural end.
|
|
* Node keeps the first value of these single-value field names and joins
|
|
* other repeated fields (scr_http_field_value), in headers and trailers
|
|
* alike. */
|
|
static bool scr_http_trailer_single_value(const ScrStr *name) {
|
|
static const char *const names[] = { "age", "authorization", "content-length",
|
|
"content-type", "etag", "expires", "from", "host", "if-modified-since",
|
|
"if-unmodified-since", "last-modified", "location", "max-forwards",
|
|
"proxy-authorization", "referer", "retry-after", "server", "user-agent", NULL };
|
|
for (size_t i = 0; names[i]; i++) {
|
|
if (name->len == strlen(names[i]) && memcmp(name->data, names[i], name->len) == 0) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static ScrStr *scr_http_req_trailer_at(ScrHttpReq *r, size_t index) {
|
|
return scr_http_field_value(r->tnames, r->tvalues, r->ntrailers, index, false);
|
|
}
|
|
|
|
ScrStr *scr_http_req_trailer(ScrHttpReq *r, ScrStr *name /*borrowed*/) {
|
|
if (!r->ended) return NULL;
|
|
for (size_t i = 0; i < r->ntrailers; i++) {
|
|
if (scr_http_name_eq(r->tnames[i], name)) return scr_http_req_trailer_at(r, i);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
ScrArr *scr_http_req_trailer_pairs(ScrHttpReq *r) {
|
|
ScrArr *out = scr_arr_new(SCR_ELEM_STR, r->ended ? r->ntrailers * 2 : 0);
|
|
if (!r->ended) return out;
|
|
for (size_t i = 0; i < r->ntrailers; i++) {
|
|
bool seen = false;
|
|
for (size_t j = 0; j < i && !seen; j++) seen = scr_http_name_eq(r->tnames[j], r->tnames[i]);
|
|
if (seen) continue;
|
|
scr_arr_push_ref(out, scr_str_retain(r->tnames[i]));
|
|
scr_arr_push_ref(out, scr_http_req_trailer_at(r, i));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
ScrArr *scr_http_req_raw_trailers(ScrHttpReq *r) {
|
|
ScrArr *out = scr_arr_new(SCR_ELEM_STR, r->ended ? r->ntrailers * 2 : 0);
|
|
if (!r->ended) return out;
|
|
for (size_t i = 0; i < r->ntrailers; i++) {
|
|
scr_arr_push_ref(out, scr_str_retain(r->tnames_raw[i]));
|
|
scr_arr_push_ref(out, scr_str_retain(r->tvalues[i]));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/* res.statusMessage: the reason phrase on client responses (+1, "" when
|
|
* the status line carried none); NULL on server requests — the
|
|
* compiler's undefined arm, the statusCode split. */
|
|
ScrStr *scr_http_req_status_message(ScrHttpReq *r) {
|
|
return r->status_msg ? scr_str_retain(r->status_msg) : NULL;
|
|
}
|
|
|
|
/* Header lookup by (case-insensitively matched) name: +1 value, or NULL —
|
|
* the compiler's undefined arm, exactly process.envGet's contract. */
|
|
ScrStr *scr_http_req_header(ScrHttpReq *r, ScrStr *name) {
|
|
for (size_t i = 0; i < r->nheaders; i++) {
|
|
if (scr_http_name_eq(r->hnames[i], name))
|
|
return scr_http_field_value(r->hnames, r->hvalues, r->nheaders, i, r->join_dup);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* req.pipe(dest) — the proxy legs: an IncomingMessage body streams into
|
|
* a ServerResponse, a ClientRequest, or a raw socket (chunk-for-chunk, no
|
|
* backpressure — divergence 54's stream model); the body's natural end
|
|
* ends the destination, Node's pipe default. A body that already ended
|
|
* ends the destination NOW (nothing more will flow). */
|
|
static void scr_http_req_consume(ScrHttpReq *r);
|
|
|
|
void scr_http_req_pipe_res(ScrHttpReq *r, ScrHttpRes *dst /*borrowed*/) {
|
|
if (r->ended) {
|
|
scr_http_res_end(dst);
|
|
return;
|
|
}
|
|
if (r->pipe_res) scr_http_res_release(r->pipe_res);
|
|
r->pipe_res = scr_http_res_retain(dst);
|
|
scr_http_req_consume(r);
|
|
}
|
|
|
|
/* socket.pipe(res) — the extended-CONNECT bridge leg: a native reader on
|
|
* the SOURCE socket turns raw chunks into response body writes (the
|
|
* response's own framing applies) and EOF into res.end(), pipe's
|
|
* default. The ctx owns the response (+1, released with the socket's
|
|
* native-reader teardown); errors/closes need no handling here — the
|
|
* caller registers its own socket listeners (the portless cleanup). */
|
|
typedef struct {
|
|
ScrHttpRes *res; /* +1 */
|
|
} ScrSockResPipe;
|
|
|
|
static void scr_http_sock_res_data(void *ctx, const char *buf, size_t n) {
|
|
ScrSockResPipe *p = (ScrSockResPipe *)ctx;
|
|
ScrBytes *chunk = scr_bytes_new(SCR_BYTES_U8, (double)n);
|
|
if (n > 0) memcpy(chunk->data, buf, n);
|
|
scr_http_res_write_bytes(p->res, chunk);
|
|
scr_bytes_release(chunk);
|
|
}
|
|
|
|
static void scr_http_sock_res_eof(void *ctx) {
|
|
ScrSockResPipe *p = (ScrSockResPipe *)ctx;
|
|
scr_http_res_end(p->res);
|
|
}
|
|
|
|
static void scr_http_sock_res_closed(void *ctx) { (void)ctx; }
|
|
|
|
static void scr_http_sock_res_free(void *ctx) {
|
|
ScrSockResPipe *p = (ScrSockResPipe *)ctx;
|
|
scr_http_res_release(p->res);
|
|
free(p);
|
|
}
|
|
|
|
void scr_http_sock_pipe_res(ScrNetSocket *src, ScrHttpRes *dst /*borrowed*/) {
|
|
ScrSockResPipe *p = calloc(1, sizeof *p);
|
|
if (!p) scr_http_oom();
|
|
p->res = scr_http_res_retain(dst);
|
|
scr_net_sock_set_native_reader(src, &scr_http_sock_res_data, &scr_http_sock_res_eof,
|
|
&scr_http_sock_res_closed, p, &scr_http_sock_res_free);
|
|
}
|
|
|
|
void scr_http_req_pipe_client(ScrHttpReq *r, ScrHttpClientReq *dst /*borrowed*/) {
|
|
if (r->ended) {
|
|
scr_http_client_end(dst);
|
|
return;
|
|
}
|
|
if (r->pipe_client) scr_http_client_release(r->pipe_client);
|
|
r->pipe_client = scr_http_client_retain(dst);
|
|
scr_http_req_consume(r);
|
|
}
|
|
|
|
void scr_http_req_pipe_sock(ScrHttpReq *r, ScrNetSocket *dst /*borrowed*/) {
|
|
if (r->ended) {
|
|
scr_net_sock_end(dst);
|
|
return;
|
|
}
|
|
if (r->pipe_sock) scr_net_sock_release(r->pipe_sock);
|
|
r->pipe_sock = scr_net_sock_retain(dst);
|
|
scr_http_req_consume(r);
|
|
}
|
|
|
|
void scr_http_req_on_data(ScrHttpReq *r, ScrClosure *cb /*moves*/, ScrNetDataFn fn, bool once) {
|
|
if (r->ended) {
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_net_ls_add(&r->data_ls, cb, (void *)fn, once);
|
|
scr_http_req_consume(r);
|
|
}
|
|
|
|
void scr_http_req_on_end(ScrHttpReq *r, ScrClosure *cb /*moves*/, bool once) {
|
|
if (r->ended) {
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_net_ls_add(&r->end_ls, cb, NULL, once);
|
|
}
|
|
|
|
double scr_http_req_status(ScrHttpReq *r) { return (double)r->status; }
|
|
|
|
ScrNetSocket *scr_http_req_socket(ScrHttpReq *r) {
|
|
/* +1; a NULL socket cannot reach the program (every req is built over
|
|
* a live connection), but stay defensive for the emitter's contract */
|
|
return r->sock ? scr_net_sock_retain(r->sock) : NULL;
|
|
}
|
|
|
|
/* the deferred-emit queue lives below (the proto sweep) — resume()'s
|
|
* drain rides it; the parser's deliver and the cork flush live below
|
|
* their callers too */
|
|
static void scr_http_emit_push(int kind, void *h /*moves +1*/);
|
|
static void scr_http_req_deliver(ScrHttpReq *r, const char *data, size_t n);
|
|
static void scr_http_res_cork_flush(ScrHttpRes *r);
|
|
#define SCR_HTTP_EMIT_REQ_DRAIN_K 6
|
|
|
|
static bool scr_http_req_holding(const ScrHttpReq *r) {
|
|
return r->paused || (r->gated && !r->flowing);
|
|
}
|
|
|
|
/* IncomingMessage's readableHighWaterMark (its socket's, 64 KiB): a held
|
|
* request buffers this many body bytes before its socket stops reading. */
|
|
#define SCR_HTTP_REQ_HOLD_BYTES 65536u
|
|
|
|
/* Stops the socket's reads while an HTTP/1 server request holds a full
|
|
* buffer, and restarts them once it flows, ends or is dumped, as Node's
|
|
* readStop when push() returns false. Client responses and h2 compat
|
|
* requests keep reading. */
|
|
static void scr_http_req_update_hold(ScrHttpReq *r) {
|
|
bool hold = r->sock != NULL && r->status == -1 && !r->http2 && !r->ended &&
|
|
scr_http_req_holding(r) && r->pend_len >= SCR_HTTP_REQ_HOLD_BYTES;
|
|
if (hold == r->sock_held) return;
|
|
r->sock_held = hold;
|
|
scr_net_sock_hold(r->sock, hold);
|
|
}
|
|
|
|
static void scr_http_req_queue_drain(ScrHttpReq *r) {
|
|
if (r->drain_queued) return;
|
|
r->drain_queued = true;
|
|
scr_http_emit_push(SCR_HTTP_EMIT_REQ_DRAIN_K, scr_http_req_retain(r));
|
|
}
|
|
|
|
/* Starts (or restarts) the flow: held bytes and a deferred 'end' drain
|
|
* through the emit queue, never the resuming stack. */
|
|
static void scr_http_req_flow(ScrHttpReq *r) {
|
|
r->paused = false;
|
|
r->flowing = true;
|
|
if (r->pend_len > 0 || r->end_pending) scr_http_req_queue_drain(r);
|
|
scr_http_req_update_hold(r);
|
|
}
|
|
|
|
/* A 'data' listener or pipe() starts the flow unless pause() stopped it,
|
|
* like Node's `if (state.flowing !== false) this.resume()`. */
|
|
static void scr_http_req_consume(ScrHttpReq *r) {
|
|
if (r->gated && !r->paused && !r->flowing) scr_http_req_flow(r);
|
|
}
|
|
|
|
/* resume(): releases what pause() or the initial paused state held. */
|
|
void scr_http_req_resume(ScrHttpReq *r) {
|
|
if (!scr_http_req_holding(r)) return;
|
|
scr_http_req_flow(r);
|
|
}
|
|
|
|
/* Node's resOnFinish: a request nothing consumed is dumped. Its 'data'
|
|
* listeners are removed, held bytes are discarded and the stream flows,
|
|
* so 'end' fires once the body is complete. */
|
|
static void scr_http_req_dump(ScrHttpReq *r) {
|
|
scr_net_ls_drop(&r->data_ls);
|
|
free(r->pend);
|
|
r->pend = NULL;
|
|
r->pend_len = r->pend_cap = 0;
|
|
r->paused = false;
|
|
r->flowing = true;
|
|
scr_http_req_update_hold(r);
|
|
}
|
|
|
|
/* pause(): delivery holds until resume() (scr_http_req_deliver buffers;
|
|
* a completed body's 'end' waits too). */
|
|
void scr_http_req_pause(ScrHttpReq *r) {
|
|
if (!r->ended) r->paused = true;
|
|
}
|
|
|
|
/* req.setTimeout(ms[, cb]): Node registers the callback on the message,
|
|
* then arms the socket's idle timer. An incomplete server request can
|
|
* handle the timeout and keep its connection alive. */
|
|
void scr_http_req_set_timeout(ScrHttpReq *r, double ms, ScrClosure *cb /*moves, nullable*/) {
|
|
if (!scr_http_timeout_valid(ms)) {
|
|
if (cb) scr_closure_release(cb);
|
|
return;
|
|
}
|
|
if (r->ended || r->destroyed || r->close_emitted || !r->sock) {
|
|
/* the message is done — Node no-ops there (never arms, never fires) */
|
|
if (cb) scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_net_sock_set_timeout(r->sock, ms);
|
|
if (cb) {
|
|
if (r->status < 0 && !r->h2_stream) scr_net_ls_add(&r->timeout_ls, cb, NULL, false);
|
|
else scr_net_sock_on_timeout(r->sock, cb, true);
|
|
}
|
|
}
|
|
|
|
void scr_http_req_set_timeout_plain(ScrHttpReq *r, double ms) {
|
|
scr_http_req_set_timeout(r, ms, NULL);
|
|
}
|
|
|
|
bool scr_http_req_destroyed_flag(ScrHttpReq *r) { return r->destroyed; }
|
|
bool scr_http_req_readable(ScrHttpReq *r) { return !r->ended && !r->destroyed; }
|
|
|
|
/* destroy(): tears the underlying connection down NOW; the teardown
|
|
* events flow through the socket's native hooks like a peer close. */
|
|
void scr_http_req_destroy(ScrHttpReq *r) {
|
|
r->destroyed = true;
|
|
if (r->h2_stream != NULL) {
|
|
scr_http_h2_ops->destroy(r->h2_stream);
|
|
return;
|
|
}
|
|
if (r->sock) scr_net_sock_destroy(r->sock);
|
|
}
|
|
|
|
void scr_http_req_on_error(ScrHttpReq *r, ScrClosure *cb /*moves*/, ScrChildErrFn fn, bool once) {
|
|
if (r->close_emitted) {
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_net_ls_add(&r->err_ls, cb, (void *)fn, once);
|
|
}
|
|
|
|
void scr_http_req_on_close(ScrHttpReq *r, ScrClosure *cb /*moves*/, bool once) {
|
|
if (r->close_emitted) {
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_net_ls_add(&r->close_ls, cb, NULL, once);
|
|
}
|
|
|
|
/* req.on('aborted') — the h2 compat event (an http/1 request registers
|
|
* too; the parser lane never fires it, matching its 'error'-only story). */
|
|
void scr_http_req_on_aborted(ScrHttpReq *r, ScrClosure *cb /*moves*/, bool once) {
|
|
if (r->close_emitted || r->aborted) {
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_net_ls_add(&r->aborted_ls, cb, NULL, once);
|
|
}
|
|
|
|
/* Body completion: 'end' fires, then both listener lists drop (the
|
|
* settle-releases-listeners story — a handler closure capturing its own
|
|
* req cannot cycle past the body). */
|
|
static void scr_http_req_finish(ScrHttpReq *r, bool fire) {
|
|
if (r->ended) return;
|
|
if (fire && scr_http_req_holding(r)) {
|
|
/* a held body holds 'end' too — the drain finishes the body */
|
|
r->end_pending = true;
|
|
return;
|
|
}
|
|
r->ended = true;
|
|
scr_http_req_update_hold(r);
|
|
r->end_consumed = fire && (r->data_ls.n > 0 || r->pipe_res || r->pipe_client || r->pipe_sock);
|
|
if (fire) scr_net_fire0_this(&r->end_ls, r, SCR_DYNH_HTTP_REQ);
|
|
scr_net_ls_drop(&r->data_ls);
|
|
scr_net_ls_drop(&r->end_ls);
|
|
/* pipes: a NATURAL end ends the destination (Node's pipe end:true
|
|
* default); an aborted body just drops the edge — the destination's
|
|
* own error/close story is already running */
|
|
if (r->pipe_res) {
|
|
if (fire) scr_http_res_end(r->pipe_res);
|
|
scr_http_res_release(r->pipe_res);
|
|
r->pipe_res = NULL;
|
|
}
|
|
if (r->pipe_client) {
|
|
if (fire) scr_http_client_end(r->pipe_client);
|
|
scr_http_client_release(r->pipe_client);
|
|
r->pipe_client = NULL;
|
|
}
|
|
if (r->pipe_sock) {
|
|
if (fire) scr_net_sock_end(r->pipe_sock);
|
|
scr_net_sock_release(r->pipe_sock);
|
|
r->pipe_sock = NULL;
|
|
}
|
|
}
|
|
|
|
/* A pending outgoing trailer block belongs to exactly one message.
|
|
* Repeated addTrailers calls replace it, as Node's _trailer assignment does. */
|
|
typedef struct {
|
|
ScrStr **names, **values;
|
|
size_t len;
|
|
} ScrHttpTrailers;
|
|
|
|
static void scr_http_trailers_clear(ScrHttpTrailers *t) {
|
|
for (size_t i = 0; i < t->len; i++) {
|
|
scr_str_release(t->names[i]);
|
|
scr_str_release(t->values[i]);
|
|
}
|
|
free(t->names);
|
|
free(t->values);
|
|
t->names = t->values = NULL;
|
|
t->len = 0;
|
|
}
|
|
|
|
static bool scr_http_token_char(unsigned char ch) {
|
|
return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') ||
|
|
(ch >= 'a' && ch <= 'z') || (ch != 0 && strchr("!#$%&'*+-.^_`|~", ch) != NULL);
|
|
}
|
|
|
|
static void scr_http_throw_invalid_header_name(ScrStr *name /*borrowed*/, ScrStr *label /*borrowed*/);
|
|
|
|
/* node:http's public validators use the same token and field-vchar rules
|
|
* as OutgoingMessage. Strings are UTF-8 here, so the Latin-1 obs-text
|
|
* range accepted by Node's JS regex is C2/C3 plus one continuation byte. */
|
|
void scr_http_validate_header_name(ScrStr *name /*borrowed*/, ScrStr *label /*borrowed*/) {
|
|
bool valid = name->len > 0;
|
|
for (size_t i = 0; i < name->len && valid; i++)
|
|
valid = scr_http_token_char((unsigned char)name->data[i]);
|
|
if (valid) return;
|
|
scr_http_throw_invalid_header_name(name, label);
|
|
}
|
|
|
|
/* Node's ERR_INVALID_HTTP_TOKEN for a header name; `name` is the name's
|
|
* string form. */
|
|
static void scr_http_throw_invalid_header_name(ScrStr *name /*borrowed*/, ScrStr *label /*borrowed*/) {
|
|
ScrJsonBuf b;
|
|
scr_jb_init(&b);
|
|
ScrStr *shown = label->len ? label : NULL;
|
|
if (shown) {
|
|
scr_jb_write(&b, shown->data, shown->len);
|
|
} else {
|
|
scr_jb_puts(&b, "Header name");
|
|
}
|
|
scr_jb_puts(&b, " must be a valid HTTP token [\"");
|
|
scr_jb_write(&b, name->data, name->len);
|
|
scr_jb_puts(&b, "\"]");
|
|
ScrStr *msg = scr_jb_finish(&b);
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, msg->data, msg->len, "ERR_INVALID_HTTP_TOKEN");
|
|
scr_str_release(msg);
|
|
}
|
|
|
|
void scr_http_validate_header_value(ScrStr *name /*borrowed*/, const ScrDyn *value /*borrowed*/) {
|
|
if (value->kind == SCR_DYN_UNDEF) {
|
|
ScrJsonBuf b;
|
|
scr_jb_init(&b);
|
|
scr_jb_puts(&b, "Invalid value \"undefined\" for header \"");
|
|
scr_jb_write(&b, name->data, name->len);
|
|
scr_jb_putc(&b, '"');
|
|
ScrStr *msg = scr_jb_finish(&b);
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, msg->data, msg->len, "ERR_HTTP_INVALID_HEADER_VALUE");
|
|
scr_str_release(msg);
|
|
return;
|
|
}
|
|
|
|
ScrStr *s = scr_dyn_string_coerce_js(value);
|
|
if (!s) return; /* object coercion threw */
|
|
bool invalid = false;
|
|
for (size_t i = 0; i < s->len; i++) {
|
|
unsigned char ch = (unsigned char)s->data[i];
|
|
if (ch == '\t' || (ch >= 0x20 && ch <= 0x7e)) continue;
|
|
if ((ch == 0xc2 || ch == 0xc3) && i + 1 < s->len &&
|
|
(unsigned char)s->data[i + 1] >= 0x80 &&
|
|
(unsigned char)s->data[i + 1] <= 0xbf) {
|
|
i++;
|
|
continue;
|
|
}
|
|
invalid = true;
|
|
break;
|
|
}
|
|
scr_str_release(s);
|
|
if (!invalid) return;
|
|
|
|
ScrJsonBuf b;
|
|
scr_jb_init(&b);
|
|
scr_jb_puts(&b, "Invalid character in header content [\"");
|
|
scr_jb_write(&b, name->data, name->len);
|
|
scr_jb_puts(&b, "\"]");
|
|
ScrStr *msg = scr_jb_finish(&b);
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, msg->data, msg->len, "ERR_INVALID_CHAR");
|
|
scr_str_release(msg);
|
|
}
|
|
|
|
static bool scr_http_trailers_replace(ScrHttpTrailers *t, ScrArr *pairs /*borrowed*/) {
|
|
scr_http_trailers_clear(t);
|
|
size_t n = (size_t)scr_arr_len(pairs);
|
|
if (n % 2 != 0) {
|
|
static const char msg[] = "The argument 'headers' is invalid.";
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_INVALID_ARG_VALUE");
|
|
return false;
|
|
}
|
|
t->names = calloc(n / 2, sizeof *t->names);
|
|
t->values = calloc(n / 2, sizeof *t->values);
|
|
if (n > 0 && (!t->names || !t->values)) scr_http_oom();
|
|
for (size_t i = 0; i < n; i += 2) {
|
|
ScrStr *name = (ScrStr *)scr_arr_get_ref(pairs, (double)i);
|
|
ScrStr *value = (ScrStr *)scr_arr_get_ref(pairs, (double)(i + 1));
|
|
bool valid = name->len > 0;
|
|
for (size_t j = 0; j < name->len; j++) valid &= scr_http_token_char((unsigned char)name->data[j]);
|
|
if (!valid) {
|
|
char msg[256];
|
|
int len = snprintf(msg, sizeof msg, "Trailer name must be a valid HTTP token [\"%.*s\"]",
|
|
(int)(name->len < 160 ? name->len : 160), name->data);
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)len, "ERR_INVALID_HTTP_TOKEN");
|
|
} else {
|
|
for (size_t j = 0; j < value->len; j++) {
|
|
unsigned char ch = (unsigned char)value->data[j];
|
|
if ((ch < 32 && ch != '\t') || ch == 127) { valid = false; break; }
|
|
}
|
|
if (!valid) {
|
|
char msg[256];
|
|
int len = snprintf(msg, sizeof msg, "Invalid character in trailer content [\"%.*s\"]",
|
|
(int)(name->len < 160 ? name->len : 160), name->data);
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)len, "ERR_INVALID_CHAR");
|
|
}
|
|
}
|
|
if (!valid) {
|
|
scr_str_release(name);
|
|
scr_str_release(value);
|
|
return false;
|
|
}
|
|
t->names[t->len] = name;
|
|
t->values[t->len] = value;
|
|
t->len++;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* ── the response handle ─────────────────────────────────────────────── */
|
|
|
|
struct ScrHttpRes {
|
|
size_t rc;
|
|
ScrAlsCtx *als; /* +1: as ScrHttpReq's */
|
|
ScrNetSocket *sock; /* +1; writes after the connection died are no-ops */
|
|
void *h2_stream; /* +1 through scr_http_h2_ops — set on h2 compat
|
|
* responses; every write path routes to frames */
|
|
struct ScrHttpConn *conn; /* backref, no ref (conn outlives via socket ctx) */
|
|
int status;
|
|
ScrStr *status_msg; /* res.statusMessage / writeHead's reason phrase;
|
|
* NULL = the status code's default phrase */
|
|
ScrStr **hnames; /* as-set (serialized verbatim, Node keeps the case) */
|
|
ScrStr **hvalues;
|
|
uint8_t *hkinds; /* SCR_HTTP_HV_*: how getHeader answers each line */
|
|
size_t nheaders, cap_headers;
|
|
ScrHttpTrailers trailers;
|
|
bool head_sent;
|
|
bool chunked;
|
|
bool finished;
|
|
bool no_date; /* res.sendDate = false: suppress the implicit Date header */
|
|
bool strict_content_length;
|
|
size_t strict_bytes_written; /* Node counts UTF-8 bytes, not JS characters */
|
|
bool keep_alive; /* the REQUEST's verdict; Connection: close overrides */
|
|
bool destroyed; /* res.destroy()/teardown — res.destroyed (true in 'close') */
|
|
bool socket_detached; /* finish/close removes the response's socket view */
|
|
/* cork()/uncork(): corked counts the nesting (res.writableCorked);
|
|
* writes while corked coalesce in cork_buf and flush as ONE write when
|
|
* the count reaches zero (or at end()) — Node's coalescing, and the
|
|
* h2 lane's single DATA frame. */
|
|
int corked;
|
|
char *cork_buf;
|
|
size_t cork_len, cork_cap;
|
|
/* A write while corked commits the head (headersSent, Node's implicit
|
|
* header) and holds its bytes here until the flush sends them first. */
|
|
char *held_head;
|
|
size_t held_head_len, held_head_cap;
|
|
/* res.req: the paired request (+1; req never points back — no cycle).
|
|
* A `res.req = null` write clears the READ (req_cleared) — Node's
|
|
* plain-property write. */
|
|
ScrHttpReq *req_ref;
|
|
bool no_body; /* HEAD request or a 1xx/204/304 status: Node's _hasBody false */
|
|
bool req_cleared;
|
|
ScrNetLs close_ls, timeout_ls;
|
|
ScrNetLs finish_ls; /* res.on('finish') — fires deferred once the body went out */
|
|
/* The AsyncLocalStorage context of the end() call (+1, NULL outside a
|
|
* store) once `end_ctx_set`: 'finish', the end() callbacks and 'close'
|
|
* after an end run in it, as Node's do. An http2 compatibility response
|
|
* never sets it: its events follow the server stream, which has no
|
|
* store. */
|
|
ScrAlsCtx *end_als;
|
|
bool end_ctx_set;
|
|
ScrNetLs endcb_ls; /* res.end(cb) — fires just before the 'finish' listeners */
|
|
ScrNetLs wcb_ls; /* res.write(chunk, cb) — fires from the queue */
|
|
bool writable_finished; /* set where Node emits 'finish' (writableFinished) */
|
|
bool finish_queued;
|
|
bool wcb_queued;
|
|
bool close_queued;
|
|
bool close_emitted;
|
|
};
|
|
|
|
ScrHttpRes *scr_http_res_retain(ScrHttpRes *r) {
|
|
if (r->rc != SIZE_MAX) r->rc++;
|
|
return r;
|
|
}
|
|
|
|
void scr_http_res_release(ScrHttpRes *r) {
|
|
if (!r || r->rc == SIZE_MAX) return;
|
|
if (--r->rc == 0) {
|
|
scr_als_ctx_release(r->als);
|
|
scr_als_ctx_release(r->end_als);
|
|
for (size_t i = 0; i < r->nheaders; i++) {
|
|
scr_str_release(r->hnames[i]);
|
|
scr_str_release(r->hvalues[i]);
|
|
}
|
|
free(r->hnames);
|
|
free(r->hvalues);
|
|
free(r->hkinds);
|
|
scr_http_trailers_clear(&r->trailers);
|
|
scr_str_release(r->status_msg);
|
|
scr_net_ls_drop(&r->close_ls);
|
|
scr_net_ls_drop(&r->timeout_ls);
|
|
scr_net_ls_drop(&r->finish_ls);
|
|
scr_net_ls_drop(&r->endcb_ls);
|
|
scr_net_ls_drop(&r->wcb_ls);
|
|
free(r->cork_buf);
|
|
free(r->held_head);
|
|
if (r->req_ref) scr_http_req_release(r->req_ref);
|
|
if (r->sock) scr_net_sock_release(r->sock);
|
|
if (r->h2_stream) scr_http_h2_ops->release(r->h2_stream);
|
|
#ifdef SCR_RC_AUDIT
|
|
scr_http_live--;
|
|
#endif
|
|
free(r);
|
|
}
|
|
}
|
|
|
|
void *scr_http_res_retain_v(void *p) { return scr_http_res_retain((ScrHttpRes *)p); }
|
|
void scr_http_res_release_v(void *p) { scr_http_res_release((ScrHttpRes *)p); }
|
|
|
|
bool scr_http_res_headers_sent(ScrHttpRes *r) { return r->head_sent; }
|
|
bool scr_http_res_writable_ended(ScrHttpRes *r) { return r->finished; }
|
|
bool scr_http_res_send_date(ScrHttpRes *r) { return !r->no_date; }
|
|
void scr_http_res_set_send_date(ScrHttpRes *r, bool value) { r->no_date = !value; }
|
|
bool scr_http_res_strict_content_length(ScrHttpRes *r) { return r->strict_content_length; }
|
|
void scr_http_res_set_strict_content_length(ScrHttpRes *r, bool value) { r->strict_content_length = value; }
|
|
ScrHttpReq *scr_http_res_request(ScrHttpRes *r) { return scr_http_req_retain(r->req_ref); }
|
|
ScrNetSocket *scr_http_res_socket(ScrHttpRes *r) {
|
|
return r->sock && !r->socket_detached ? scr_net_sock_retain(r->sock) : NULL;
|
|
}
|
|
/* HTTP/1 writableFinished turns true where 'finish' is emitted, not when
|
|
* end() returns; the h2 compat response keeps its stream's answer. */
|
|
bool scr_http_res_writable_finished(ScrHttpRes *r) {
|
|
return r->h2_stream != NULL ? r->finished : r->writable_finished;
|
|
}
|
|
|
|
static bool scr_http_res_has_header(ScrHttpRes *r, const char *name);
|
|
|
|
/* Node checks declared body length only while strictContentLength is true,
|
|
* with no transfer encoding and a body-bearing response. The stored header
|
|
* string has already passed setHeader's validation; strtod matches Node's
|
|
* numeric conversion for the ordinary decimal Content-Length values. */
|
|
static bool scr_http_res_expected_length(ScrHttpRes *r, double *expected) {
|
|
if (!r->strict_content_length || r->h2_stream || r->chunked ||
|
|
(r->status >= 100 && r->status < 200) || r->status == 204 || r->status == 304 ||
|
|
scr_http_res_has_header(r, "transfer-encoding")) return false;
|
|
if (r->req_ref && r->req_ref->method && r->req_ref->method->len == 4 &&
|
|
memcmp(r->req_ref->method->data, "HEAD", 4) == 0) return false;
|
|
for (size_t i = 0; i < r->nheaders; i++) {
|
|
if (r->hnames[i]->len != 14) continue;
|
|
bool match = true;
|
|
for (size_t j = 0; j < 14; j++) {
|
|
if (tolower((unsigned char)r->hnames[i]->data[j]) != "content-length"[j]) {
|
|
match = false;
|
|
break;
|
|
}
|
|
}
|
|
if (!match) continue;
|
|
*expected = strtod(r->hvalues[i]->data, NULL);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void scr_http_res_length_mismatch(ScrHttpRes *r, double expected) {
|
|
char msg[256];
|
|
int n = snprintf(msg, sizeof msg,
|
|
"Response body's content-length of %zu byte(s) does not match the content-length of %.15g byte(s) set in header",
|
|
r->strict_bytes_written, expected);
|
|
scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)n, "ERR_HTTP_CONTENT_LENGTH_MISMATCH");
|
|
}
|
|
|
|
/* res.statusCode: 200 until assigned (Node's fresh-response default);
|
|
* assignment after the head went out is inert (Node throws on the WRITE
|
|
* paths, not the property — the sent head simply no longer changes). */
|
|
double scr_http_res_status_get(ScrHttpRes *r) { return r->status > 0 ? (double)r->status : 200; }
|
|
|
|
void scr_http_res_status_set(ScrHttpRes *r, double status) {
|
|
if (r->head_sent) return;
|
|
r->status = (int)status;
|
|
}
|
|
|
|
/* An unset statusMessage is undefined until the head chooses the status
|
|
* code's default reason phrase. */
|
|
ScrStr *scr_http_res_status_msg_get(ScrHttpRes *r) {
|
|
if (r->status_msg) return scr_str_retain(r->status_msg);
|
|
if (!r->head_sent) return NULL;
|
|
const char *reason = scr_http_reason(r->status > 0 ? r->status : 200);
|
|
return scr_str_new(reason, strlen(reason));
|
|
}
|
|
|
|
void scr_http_res_status_msg_set(ScrHttpRes *r, ScrStr *msg /*borrowed*/) {
|
|
if (r->head_sent) return;
|
|
scr_str_release(r->status_msg);
|
|
r->status_msg = scr_str_retain(msg);
|
|
}
|
|
|
|
/* Outgoing header values keep the type they were set with, like Node's
|
|
* kOutHeaders entries: a string, a number (serialized as String(n)), or
|
|
* an array (one line per element). Each line records its kind; an array
|
|
* is its consecutive same-name LIST lines. */
|
|
enum { SCR_HTTP_HV_STR = 0, SCR_HTTP_HV_NUM = 1, SCR_HTTP_HV_LIST = 2 };
|
|
|
|
/* getHeader(name)'s first line, as a string (+1), or NULL. */
|
|
ScrStr *scr_http_res_get_header(ScrHttpRes *r, ScrStr *name) {
|
|
for (size_t i = 0; i < r->nheaders; i++) {
|
|
if (scr_http_name_eq(r->hnames[i], name)) return scr_str_retain(r->hvalues[i]);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* The checked-dynamic value of one header line group: a number, a string,
|
|
* or an array of the group's strings (+1). */
|
|
static ScrDyn *scr_http_out_value(ScrStr *const *names, ScrStr *const *values, const uint8_t *kinds,
|
|
size_t count, size_t first) {
|
|
size_t n = 0;
|
|
for (size_t i = first; i < count; i++) n += scr_http_name_eq(names[i], names[first]);
|
|
uint8_t kind = kinds ? kinds[first] : SCR_HTTP_HV_STR;
|
|
if (n == 1 && kind == SCR_HTTP_HV_NUM) return scr_dyn_new_num(strtod(values[first]->data, NULL));
|
|
if (n == 1 && kind == SCR_HTTP_HV_STR) return scr_dyn_new_str(values[first]);
|
|
ScrDyn *list = scr_dyn_new_arr();
|
|
for (size_t i = first; i < count; i++) {
|
|
if (scr_http_name_eq(names[i], names[first])) scr_dyn_arr_push(list, scr_dyn_new_str(values[i]));
|
|
}
|
|
return list;
|
|
}
|
|
|
|
/* getHeader(name): the value as set (case-insensitive), +1; the
|
|
* undefined value when the header is absent. */
|
|
ScrDyn *scr_http_res_get_header_dyn(ScrHttpRes *r, ScrStr *name) {
|
|
for (size_t i = 0; i < r->nheaders; i++) {
|
|
if (scr_http_name_eq(r->hnames[i], name))
|
|
return scr_http_out_value(r->hnames, r->hvalues, r->hkinds, r->nheaders, i);
|
|
}
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
|
|
/* Outgoing header snapshots omit the automatically serialized fields. The
|
|
* names API lowercases keys while the raw variant preserves their casing. */
|
|
static ScrArr *scr_http_out_header_names(ScrStr **names, size_t count, bool raw) {
|
|
ScrArr *out = scr_arr_new(SCR_ELEM_STR, count);
|
|
for (size_t i = 0; i < count; i++) {
|
|
bool seen = false;
|
|
for (size_t j = 0; j < i && !seen; j++) seen = scr_http_name_eq(names[j], names[i]);
|
|
if (seen) continue;
|
|
if (raw) {
|
|
scr_arr_push_ref(out, scr_str_retain(names[i]));
|
|
} else {
|
|
ScrStr *lower = scr_str_new(names[i]->data, names[i]->len);
|
|
for (size_t j = 0; j < lower->len; j++)
|
|
lower->data[j] = (char)tolower((unsigned char)lower->data[j]);
|
|
scr_arr_push_ref(out, lower);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/* getHeaders(): lowercased names to each group's value. `kinds` NULL
|
|
* reads every line as a string. */
|
|
static ScrDyn *scr_http_out_headers(ScrStr **names, ScrStr **values, const uint8_t *kinds,
|
|
size_t count) {
|
|
ScrDyn *out = scr_dyn_new_obj();
|
|
for (size_t i = 0; i < count; i++) {
|
|
bool seen = false;
|
|
for (size_t j = 0; j < i && !seen; j++) seen = scr_http_name_eq(names[j], names[i]);
|
|
if (seen) continue;
|
|
ScrStr *lower = scr_str_new(names[i]->data, names[i]->len);
|
|
for (size_t j = 0; j < lower->len; j++)
|
|
lower->data[j] = (char)tolower((unsigned char)lower->data[j]);
|
|
scr_dyn_obj_set(out, lower->data, lower->len, scr_http_out_value(names, values, kinds, count, i));
|
|
scr_str_release(lower);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
ScrArr *scr_http_res_get_header_names(ScrHttpRes *r) {
|
|
return scr_http_out_header_names(r->hnames, r->nheaders, false);
|
|
}
|
|
|
|
ScrArr *scr_http_res_get_raw_header_names(ScrHttpRes *r) {
|
|
return scr_http_out_header_names(r->hnames, r->nheaders, true);
|
|
}
|
|
|
|
ScrDyn *scr_http_res_get_headers(ScrHttpRes *r) {
|
|
return scr_http_out_headers(r->hnames, r->hvalues, r->hkinds, r->nheaders);
|
|
}
|
|
|
|
bool scr_http_res_has_header_named(ScrHttpRes *r, ScrStr *name) {
|
|
ScrStr *v = scr_http_res_get_header(r, name);
|
|
if (v) scr_str_release(v);
|
|
return v != NULL;
|
|
}
|
|
|
|
/* Drops every case-insensitive match of `name`; answers the index the
|
|
* first one held (nheaders when there was none), where setHeader puts the
|
|
* replacement so the header keeps its place, like Node's key order. */
|
|
static size_t scr_http_res_drop_named(ScrHttpRes *r, const ScrStr *name) {
|
|
size_t w = 0, first = SIZE_MAX;
|
|
for (size_t i = 0; i < r->nheaders; i++) {
|
|
if (scr_http_name_eq(r->hnames[i], name)) {
|
|
if (first == SIZE_MAX) first = w;
|
|
scr_str_release(r->hnames[i]);
|
|
scr_str_release(r->hvalues[i]);
|
|
} else {
|
|
r->hnames[w] = r->hnames[i];
|
|
r->hvalues[w] = r->hvalues[i];
|
|
r->hkinds[w] = r->hkinds[i];
|
|
w++;
|
|
}
|
|
}
|
|
r->nheaders = w;
|
|
return first == SIZE_MAX ? w : first;
|
|
}
|
|
|
|
/* removeHeader(name): drops every case-insensitive match. */
|
|
void scr_http_res_remove_header(ScrHttpRes *r, ScrStr *name) {
|
|
if (r->head_sent) return; /* Node throws ERR_HTTP_HEADERS_SENT; this slice drops */
|
|
scr_http_res_drop_named(r, name);
|
|
}
|
|
|
|
static bool scr_http_res_has_header(ScrHttpRes *r, const char *name) {
|
|
size_t len = strlen(name);
|
|
for (size_t i = 0; i < r->nheaders; i++) {
|
|
if (r->hnames[i]->len == len) {
|
|
bool eq = true;
|
|
for (size_t j = 0; j < len && eq; j++) {
|
|
if (tolower((unsigned char)r->hnames[i]->data[j]) != tolower((unsigned char)name[j])) eq = false;
|
|
}
|
|
if (eq) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* Inserts one line at `pos` (name and value borrowed). */
|
|
static void scr_http_res_insert_header(ScrHttpRes *r, size_t pos, ScrStr *name, ScrStr *value,
|
|
uint8_t kind) {
|
|
if (r->nheaders == r->cap_headers) {
|
|
r->cap_headers = r->cap_headers ? r->cap_headers * 2 : 8;
|
|
r->hnames = realloc(r->hnames, r->cap_headers * sizeof *r->hnames);
|
|
r->hvalues = realloc(r->hvalues, r->cap_headers * sizeof *r->hvalues);
|
|
r->hkinds = realloc(r->hkinds, r->cap_headers * sizeof *r->hkinds);
|
|
if (!r->hnames || !r->hvalues || !r->hkinds) scr_http_oom();
|
|
}
|
|
memmove(r->hnames + pos + 1, r->hnames + pos, (r->nheaders - pos) * sizeof *r->hnames);
|
|
memmove(r->hvalues + pos + 1, r->hvalues + pos, (r->nheaders - pos) * sizeof *r->hvalues);
|
|
memmove(r->hkinds + pos + 1, r->hkinds + pos, (r->nheaders - pos) * sizeof *r->hkinds);
|
|
r->hnames[pos] = scr_str_retain(name);
|
|
r->hvalues[pos] = scr_str_retain(value);
|
|
r->hkinds[pos] = kind;
|
|
r->nheaders++;
|
|
}
|
|
|
|
/* Append one header line verbatim (the array-valued writeHead expansion:
|
|
* repeated names each get their own line). */
|
|
static void scr_http_res_append_header(ScrHttpRes *r, ScrStr *name /*borrowed*/,
|
|
ScrStr *value /*borrowed*/) {
|
|
if (r->head_sent) return;
|
|
scr_http_res_insert_header(r, r->nheaders, name, value, SCR_HTTP_HV_STR);
|
|
}
|
|
|
|
/* setHeader(name, string): replace-by-name (case-insensitive), Node's
|
|
* semantics. */
|
|
void scr_http_res_set_header(ScrHttpRes *r, ScrStr *name /*borrowed*/, ScrStr *value /*borrowed*/) {
|
|
if (r->head_sent) return; /* Node throws ERR_HTTP_HEADERS_SENT; slice drops (SEMANTICS) */
|
|
scr_http_res_insert_header(r, scr_http_res_drop_named(r, name), name, value, SCR_HTTP_HV_STR);
|
|
}
|
|
|
|
/* One element of an outgoing header value: a string or a number
|
|
* (String(n)); anything else throws Node's ERR_HTTP_INVALID_HEADER_VALUE.
|
|
* +1, or NULL with the exception pending. */
|
|
static ScrStr *scr_http_header_value_text(const ScrStr *name, const ScrDyn *v) {
|
|
if (v->kind == SCR_DYN_STR) return scr_str_retain(v->v.str);
|
|
if (v->kind == SCR_DYN_NUM) return scr_f64_to_scrstr(v->v.num);
|
|
ScrJsonBuf b;
|
|
scr_jb_init(&b);
|
|
scr_jb_puts(&b, "Invalid value \"");
|
|
scr_jb_puts(&b, v->kind == SCR_DYN_UNDEF ? "undefined" : "object");
|
|
scr_jb_puts(&b, "\" for header \"");
|
|
scr_jb_write(&b, name->data, name->len);
|
|
scr_jb_putc(&b, '"');
|
|
ScrStr *msg = scr_jb_finish(&b);
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, msg->data, msg->len, "ERR_HTTP_INVALID_HEADER_VALUE");
|
|
scr_str_release(msg);
|
|
return NULL;
|
|
}
|
|
|
|
/* Inserts `value` (a string, a number or an array of either) at `pos`;
|
|
* array elements become LIST lines. Answers the line count, or SIZE_MAX
|
|
* with the exception pending. */
|
|
static size_t scr_http_res_insert_value(ScrHttpRes *r, size_t pos, ScrStr *name, const ScrDyn *value,
|
|
bool as_list) {
|
|
if (value->kind == SCR_DYN_ARR) {
|
|
size_t n = value->v.arr.len;
|
|
for (size_t i = 0; i < n; i++) {
|
|
ScrStr *text = scr_http_header_value_text(name, value->v.arr.items[i]);
|
|
if (!text) return SIZE_MAX;
|
|
scr_http_res_insert_header(r, pos + i, name, text, SCR_HTTP_HV_LIST);
|
|
scr_str_release(text);
|
|
}
|
|
return n;
|
|
}
|
|
ScrStr *text = scr_http_header_value_text(name, value);
|
|
if (!text) return SIZE_MAX;
|
|
uint8_t kind = as_list ? SCR_HTTP_HV_LIST : value->kind == SCR_DYN_NUM ? SCR_HTTP_HV_NUM : SCR_HTTP_HV_STR;
|
|
scr_http_res_insert_header(r, pos, name, text, kind);
|
|
scr_str_release(text);
|
|
return 1;
|
|
}
|
|
|
|
/* A typed array arrives as a typed reference; header code reads plain
|
|
* dyn arrays. +1. */
|
|
static ScrDyn *scr_http_header_value_plain(ScrDyn *value) {
|
|
if (value->kind == SCR_DYN_TYPED_REF && scr_dyn_ref_is_array(value))
|
|
return scr_dyn_typed_ref_materialize(value);
|
|
return scr_dyn_retain(value);
|
|
}
|
|
|
|
/* setHeader(name, value) for a number, a string or an array value. */
|
|
void scr_http_res_set_header_dyn(ScrHttpRes *r, ScrStr *name /*borrowed*/, ScrDyn *raw /*borrowed*/) {
|
|
if (r->head_sent) return;
|
|
ScrDyn *value = scr_http_header_value_plain(raw);
|
|
if (scr_exc_pending()) {
|
|
scr_dyn_release(value);
|
|
return;
|
|
}
|
|
bool valid = true;
|
|
size_t n = value->kind == SCR_DYN_ARR ? value->v.arr.len : 1;
|
|
for (size_t i = 0; i < n && valid; i++) {
|
|
ScrStr *text = scr_http_header_value_text(name, value->kind == SCR_DYN_ARR ? value->v.arr.items[i] : value);
|
|
valid = text != NULL;
|
|
scr_str_release(text);
|
|
}
|
|
if (valid) scr_http_res_insert_value(r, scr_http_res_drop_named(r, name), name, value, false);
|
|
scr_dyn_release(value);
|
|
}
|
|
|
|
/* appendHeader(name, value): an absent header is set as given; an existing
|
|
* one becomes an array holding its values followed by the new ones. */
|
|
void scr_http_res_append_header_dyn(ScrHttpRes *r, ScrStr *name /*borrowed*/, ScrDyn *raw /*borrowed*/) {
|
|
if (r->head_sent) return;
|
|
ScrDyn *value = scr_http_header_value_plain(raw);
|
|
if (scr_exc_pending()) {
|
|
scr_dyn_release(value);
|
|
return;
|
|
}
|
|
bool valid = true;
|
|
size_t n = value->kind == SCR_DYN_ARR ? value->v.arr.len : 1;
|
|
for (size_t i = 0; i < n && valid; i++) {
|
|
ScrStr *text = scr_http_header_value_text(name, value->kind == SCR_DYN_ARR ? value->v.arr.items[i] : value);
|
|
valid = text != NULL;
|
|
scr_str_release(text);
|
|
}
|
|
if (!valid) {
|
|
scr_dyn_release(value);
|
|
return;
|
|
}
|
|
size_t last = SIZE_MAX;
|
|
for (size_t i = 0; i < r->nheaders; i++) {
|
|
if (!scr_http_name_eq(r->hnames[i], name)) continue;
|
|
r->hkinds[i] = SCR_HTTP_HV_LIST;
|
|
last = i;
|
|
}
|
|
if (last == SIZE_MAX) scr_http_res_insert_value(r, r->nheaders, name, value, false);
|
|
else scr_http_res_insert_value(r, last + 1, r->hnames[last], value, true);
|
|
scr_dyn_release(value);
|
|
}
|
|
|
|
/* Grow-and-append head builder. */
|
|
typedef struct {
|
|
char *data;
|
|
size_t len, cap;
|
|
} ScrHttpBuf;
|
|
|
|
static void scr_http_buf_append(ScrHttpBuf *b, const char *s, size_t n) {
|
|
if (b->len + n > b->cap) {
|
|
size_t cap = b->cap ? b->cap : 512;
|
|
while (cap < b->len + n) cap *= 2;
|
|
b->data = realloc(b->data, cap);
|
|
if (!b->data) scr_http_oom();
|
|
b->cap = cap;
|
|
}
|
|
memcpy(b->data + b->len, s, n);
|
|
b->len += n;
|
|
}
|
|
|
|
static void scr_http_buf_str(ScrHttpBuf *b, const char *s) { scr_http_buf_append(b, s, strlen(s)); }
|
|
|
|
/* Decimal digits of v (the status code, Content-Length) without printf. */
|
|
static void scr_http_buf_uint(ScrHttpBuf *b, unsigned long long v) {
|
|
char digits[24];
|
|
size_t at = sizeof digits;
|
|
do {
|
|
digits[--at] = (char)('0' + v % 10);
|
|
v /= 10;
|
|
} while (v != 0);
|
|
scr_http_buf_append(b, digits + at, sizeof digits - at);
|
|
}
|
|
|
|
/* ASCII case-insensitive name compare against a lowercase literal. */
|
|
static bool scr_http_name_is(const ScrStr *name, const char *lower) {
|
|
size_t len = strlen(lower);
|
|
if (name->len != len) return false;
|
|
for (size_t i = 0; i < len; i++) {
|
|
if (tolower((unsigned char)name->data[i]) != lower[i]) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* Informational response heads are written with Node's `ascii` encoding.
|
|
* The header validator allows Latin-1 obs-text; collapse its UTF-8 C2/C3
|
|
* spelling back to one wire byte. */
|
|
static void scr_http_buf_header_value(ScrHttpBuf *b, const ScrStr *s) {
|
|
for (size_t i = 0; i < s->len; i++) {
|
|
unsigned char ch = (unsigned char)s->data[i];
|
|
if ((ch == 0xc2 || ch == 0xc3) && i + 1 < s->len) {
|
|
unsigned char next = (unsigned char)s->data[++i];
|
|
char byte = (char)(((ch & 1) << 6) | (next & 0x3f));
|
|
scr_http_buf_append(b, &byte, 1);
|
|
} else {
|
|
scr_http_buf_append(b, s->data + i, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool scr_http_res_informational_h1(ScrHttpRes *r, const char *method) {
|
|
if (r->h2_stream == NULL) return true;
|
|
char msg[128];
|
|
int n = snprintf(msg, sizeof msg, "Http2ServerResponse.%s is not supported by the static runtime yet", method);
|
|
scr_throw_error_msg(SCR_ERR_ERROR, msg, (size_t)n);
|
|
return false;
|
|
}
|
|
|
|
void scr_http_res_write_continue(ScrHttpRes *r) {
|
|
if (!scr_http_res_informational_h1(r, "writeContinue")) return;
|
|
if (r->sock) scr_net_sock_write_native(r->sock, "HTTP/1.1 100 Continue\r\n\r\n", 25);
|
|
}
|
|
|
|
void scr_http_res_write_processing(ScrHttpRes *r) {
|
|
if (!scr_http_res_informational_h1(r, "writeProcessing")) return;
|
|
if (r->sock) scr_net_sock_write_native(r->sock, "HTTP/1.1 102 Processing\r\n\r\n", 27);
|
|
}
|
|
|
|
/* Node's linkValueRegExp from internal/validators: `<...>` followed by
|
|
* optional semicolon parameters. Its string arm refuses an empty or
|
|
* malformed link before the general header-content check runs. */
|
|
static bool scr_http_early_link_format(const ScrStr *link) {
|
|
if (link->len < 2 || link->data[0] != '<') return false;
|
|
size_t i = 1;
|
|
while (i < link->len && link->data[i] != '>') {
|
|
if (link->data[i] == '\r' || link->data[i] == '\n') return false;
|
|
i++;
|
|
}
|
|
if (i == link->len) return false;
|
|
i++;
|
|
while (i < link->len) {
|
|
while (i < link->len && isspace((unsigned char)link->data[i])) i++;
|
|
if (i == link->len || link->data[i++] != ';') return false;
|
|
while (i < link->len && isspace((unsigned char)link->data[i])) i++;
|
|
size_t start = i;
|
|
while (i < link->len && link->data[i] != ';' && link->data[i] != '"' &&
|
|
!isspace((unsigned char)link->data[i])) i++;
|
|
if (i == start) return false;
|
|
if (i < link->len && link->data[i] == '"') {
|
|
if (link->data[i - 1] != '=') return false;
|
|
i++;
|
|
while (i < link->len && link->data[i] != ';' && link->data[i] != '"' &&
|
|
!isspace((unsigned char)link->data[i])) i++;
|
|
if (i == link->len || link->data[i++] != '"') return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* A lowercase `link` controls whether Node sends a 103 at all; when
|
|
* present it is always the first field, followed by the other object
|
|
* properties in insertion order. This static entry accepts string-valued
|
|
* fields. */
|
|
void scr_http_res_write_early_hints(ScrHttpRes *r, ScrArr *pairs /*borrowed*/) {
|
|
if (!scr_http_res_informational_h1(r, "writeEarlyHints")) return;
|
|
size_t count = (size_t)scr_arr_len(pairs);
|
|
ScrStr *link = NULL;
|
|
for (size_t i = 0; i + 1 < count; i += 2) {
|
|
ScrStr *name = (ScrStr *)scr_arr_get_ref(pairs, (double)i);
|
|
if (name->len == 4 && memcmp(name->data, "link", 4) == 0) {
|
|
scr_str_release(link);
|
|
link = (ScrStr *)scr_arr_get_ref(pairs, (double)(i + 1));
|
|
}
|
|
scr_str_release(name);
|
|
}
|
|
if (!link) return;
|
|
if (!scr_http_early_link_format(link)) {
|
|
ScrDyn *received = scr_dyn_new_str(link);
|
|
scr_dyn_arg_value_fail("hints", "must be an array or string of format \"</styles.css>; rel=preload; as=style\"", received);
|
|
scr_dyn_release(received);
|
|
scr_str_release(link);
|
|
return;
|
|
}
|
|
ScrStr *link_name = scr_str_new("Link", 4);
|
|
ScrDyn *link_value = scr_dyn_new_str(link);
|
|
scr_http_validate_header_value(link_name, link_value);
|
|
scr_dyn_release(link_value);
|
|
scr_str_release(link_name);
|
|
if (scr_exc_pending()) {
|
|
scr_str_release(link);
|
|
return;
|
|
}
|
|
ScrHttpBuf b = {NULL, 0, 0};
|
|
scr_http_buf_str(&b, "HTTP/1.1 103 Early Hints\r\nLink: ");
|
|
scr_http_buf_header_value(&b, link);
|
|
scr_http_buf_str(&b, "\r\n");
|
|
scr_str_release(link);
|
|
ScrStr *empty_label = scr_str_new("", 0);
|
|
for (size_t i = 0; i + 1 < count; i += 2) {
|
|
ScrStr *name = (ScrStr *)scr_arr_get_ref(pairs, (double)i);
|
|
ScrStr *value = (ScrStr *)scr_arr_get_ref(pairs, (double)(i + 1));
|
|
if (name->len == 4 && memcmp(name->data, "link", 4) == 0) {
|
|
scr_str_release(name);
|
|
scr_str_release(value);
|
|
continue;
|
|
}
|
|
scr_http_validate_header_name(name, empty_label);
|
|
if (!scr_exc_pending()) {
|
|
ScrDyn *wrapped = scr_dyn_new_str(value);
|
|
scr_http_validate_header_value(name, wrapped);
|
|
scr_dyn_release(wrapped);
|
|
}
|
|
if (scr_exc_pending()) {
|
|
scr_str_release(name);
|
|
scr_str_release(value);
|
|
scr_str_release(empty_label);
|
|
free(b.data);
|
|
return;
|
|
}
|
|
scr_http_buf_append(&b, name->data, name->len);
|
|
scr_http_buf_str(&b, ": ");
|
|
scr_http_buf_header_value(&b, value);
|
|
scr_http_buf_str(&b, "\r\n");
|
|
scr_str_release(name);
|
|
scr_str_release(value);
|
|
}
|
|
scr_str_release(empty_label);
|
|
scr_http_buf_str(&b, "\r\n");
|
|
if (r->sock) scr_net_sock_write_native(r->sock, b.data, b.len);
|
|
free(b.data);
|
|
}
|
|
|
|
static void scr_http_buf_chunk_end(ScrHttpBuf *b, const ScrHttpTrailers *t) {
|
|
scr_http_buf_str(b, "0\r\n");
|
|
for (size_t i = 0; i < t->len; i++) {
|
|
scr_http_buf_append(b, t->names[i]->data, t->names[i]->len);
|
|
scr_http_buf_str(b, ": ");
|
|
scr_http_buf_append(b, t->values[i]->data, t->values[i]->len);
|
|
scr_http_buf_str(b, "\r\n");
|
|
}
|
|
scr_http_buf_str(b, "\r\n");
|
|
}
|
|
|
|
static void scr_http_send_chunk_end(ScrNetSocket *sock, const ScrHttpTrailers *t) {
|
|
ScrHttpBuf b = {NULL, 0, 0};
|
|
scr_http_buf_chunk_end(&b, t);
|
|
scr_net_sock_write_native(sock, b.data, b.len);
|
|
free(b.data);
|
|
}
|
|
|
|
/* The Date line has one-second resolution, so it is formatted once per
|
|
* second and reused (Node caches its utcDate string the same way); SCR_TL
|
|
* keeps thread-instanced library sessions on their own copy. */
|
|
static void scr_http_buf_date(ScrHttpBuf *b) {
|
|
static SCR_TL time_t cached_sec = (time_t)-1;
|
|
static SCR_TL char cached[64];
|
|
static SCR_TL size_t cached_len;
|
|
time_t now = time(NULL);
|
|
if (now != cached_sec) {
|
|
struct tm tm;
|
|
gmtime_r(&now, &tm);
|
|
static const char *days[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
|
|
static const char *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
|
|
int n = snprintf(cached, sizeof cached, "Date: %s, %02d %s %04d %02d:%02d:%02d GMT\r\n",
|
|
days[tm.tm_wday], tm.tm_mday, months[tm.tm_mon], tm.tm_year + 1900,
|
|
tm.tm_hour, tm.tm_min, tm.tm_sec);
|
|
cached_len = n > 0 && (size_t)n < sizeof cached ? (size_t)n : 0;
|
|
cached_sec = cached_len > 0 ? now : (time_t)-1;
|
|
}
|
|
scr_http_buf_append(b, cached, cached_len);
|
|
}
|
|
|
|
static bool scr_http_header_has_token(const ScrStr *value, const char *token) {
|
|
size_t token_len = strlen(token);
|
|
for (size_t off = 0; off < value->len;) {
|
|
while (off < value->len &&
|
|
(value->data[off] == ',' || value->data[off] == ' ' || value->data[off] == '\t')) off++;
|
|
size_t end = off;
|
|
while (end < value->len && value->data[end] != ',' &&
|
|
value->data[end] != ' ' && value->data[end] != '\t') end++;
|
|
if (end - off == token_len) {
|
|
bool equal = true;
|
|
for (size_t i = 0; i < token_len && equal; i++) {
|
|
if (tolower((unsigned char)value->data[off + i]) != token[i]) equal = false;
|
|
}
|
|
if (equal) return true;
|
|
}
|
|
while (end < value->len && value->data[end] != ',') end++;
|
|
off = end;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* Serializes and sends the head. `body_len` >= 0 fixes Content-Length
|
|
* (the end(data)-before-head path); -1 means chunked unless the user set
|
|
* Content-Length or Transfer-Encoding (Node's useChunkedEncodingByDefault
|
|
* for 1.1). Header order matches Node's storeHeader: user headers as set,
|
|
* then the injected Date / Connection (+Keep-Alive) / framing header. */
|
|
static bool scr_http_res_build_head(ScrHttpRes *r, long long body_len, ScrHttpBuf *out);
|
|
|
|
static void scr_http_res_send_head(ScrHttpRes *r, long long body_len) {
|
|
if (r->head_sent) return;
|
|
if (r->h2_stream != NULL) {
|
|
/* the h2 transport: a HEADERS frame — no HTTP/1 framing headers */
|
|
(void)body_len;
|
|
int status = r->status > 0 ? r->status : 200;
|
|
scr_http_h2_ops->respond(r->h2_stream, (double)status, r->hnames, r->hvalues,
|
|
r->nheaders, !r->no_date);
|
|
r->head_sent = true;
|
|
return;
|
|
}
|
|
ScrHttpBuf b = {NULL, 0, 0};
|
|
if (!scr_http_res_build_head(r, body_len, &b)) return;
|
|
if (r->sock) scr_net_sock_write_native(r->sock, b.data, b.len);
|
|
free(b.data);
|
|
}
|
|
|
|
/* The HTTP/1 head serialization behind scr_http_res_send_head: fills *out
|
|
* (caller frees) and commits head_sent/chunked, or throws
|
|
* ERR_HTTP_TRAILER_INVALID and returns false with nothing to free. Callers
|
|
* that already hold the body send head and body as one write. */
|
|
static bool scr_http_res_build_head(ScrHttpRes *r, long long body_len, ScrHttpBuf *out) {
|
|
int status = r->status > 0 ? r->status : 200;
|
|
ScrHttpBuf b = {NULL, 0, 0};
|
|
scr_http_buf_str(&b, "HTTP/1.1 ");
|
|
scr_http_buf_uint(&b, (unsigned long long)status);
|
|
scr_http_buf_str(&b, " ");
|
|
/* the assigned reason phrase wins (writeHead's statusMessage argument /
|
|
* the res.statusMessage assignment — "" serializes as an empty phrase,
|
|
* Node's "HTTP/1.1 200 " line) */
|
|
if (r->status_msg) scr_http_buf_append(&b, r->status_msg->data, r->status_msg->len);
|
|
else scr_http_buf_str(&b, scr_http_reason(status));
|
|
scr_http_buf_str(&b, "\r\n");
|
|
/* One pass serializes the user headers and notes the framing-relevant
|
|
* names (case-insensitive, as scr_http_res_has_header compares). */
|
|
bool user_chunked = false, have_date = false, have_connection = false;
|
|
bool have_len = false, declared_trailer = false;
|
|
for (size_t i = 0; i < r->nheaders; i++) {
|
|
const ScrStr *name = r->hnames[i];
|
|
scr_http_buf_append(&b, name->data, name->len);
|
|
scr_http_buf_str(&b, ": ");
|
|
scr_http_buf_append(&b, r->hvalues[i]->data, r->hvalues[i]->len);
|
|
scr_http_buf_str(&b, "\r\n");
|
|
switch (name->len) {
|
|
case 4: have_date = have_date || scr_http_name_is(name, "date"); break;
|
|
case 7: declared_trailer = declared_trailer || scr_http_name_is(name, "trailer"); break;
|
|
case 10: have_connection = have_connection || scr_http_name_is(name, "connection"); break;
|
|
case 14: have_len = have_len || scr_http_name_is(name, "content-length"); break;
|
|
case 17:
|
|
if (scr_http_name_is(name, "transfer-encoding")) {
|
|
have_len = true;
|
|
if (scr_http_header_has_token(r->hvalues[i], "chunked")) user_chunked = true;
|
|
}
|
|
break;
|
|
default: break;
|
|
}
|
|
}
|
|
r->chunked = user_chunked;
|
|
if (!r->no_date && !have_date) {
|
|
scr_http_buf_date(&b);
|
|
}
|
|
/* An HTTP/1.0 request gets no chunked default (Node's
|
|
* useChunkedEncodingByDefault): without a user length the body goes out
|
|
* unframed and the connection closes after it. */
|
|
/* Node's _hasBody: a HEAD request or a 1xx, 204 or 304 status sends no
|
|
* body and no framing header; write() and end() data are dropped. */
|
|
r->no_body = (status >= 100 && status < 200) || status == 204 || status == 304 ||
|
|
(r->req_ref != NULL && r->req_ref->method != NULL && r->req_ref->method->len == 4 &&
|
|
memcmp(r->req_ref->method->data, "HEAD", 4) == 0);
|
|
bool unframed = !have_len && (r->no_body || (r->req_ref != NULL && r->req_ref->http10));
|
|
if (unframed && !r->no_body) r->keep_alive = false;
|
|
bool user_close = false;
|
|
if (have_connection) {
|
|
/* the user's own Connection header decides (already serialized) */
|
|
user_close = true; /* trust their framing intent; keep-alive logic off */
|
|
} else if (r->keep_alive) {
|
|
/* Node's Keep-Alive hint: whole seconds of server.keepAliveTimeout,
|
|
* omitted when it is 0. */
|
|
scr_http_buf_str(&b, "Connection: keep-alive\r\n");
|
|
ScrNetServer *srv = r->sock != NULL ? scr_net_sock_server(r->sock) : NULL;
|
|
double ka = srv != NULL ? scr_net_server_keep_alive_timeout(srv) : 5000;
|
|
if (ka > 0) {
|
|
scr_http_buf_str(&b, "Keep-Alive: timeout=");
|
|
scr_http_buf_uint(&b, (unsigned long long)floor(ka / 1000));
|
|
scr_http_buf_str(&b, "\r\n");
|
|
}
|
|
} else {
|
|
scr_http_buf_str(&b, "Connection: close\r\n");
|
|
}
|
|
(void)user_close;
|
|
if (!have_len && !unframed) {
|
|
if (body_len >= 0 && !declared_trailer) {
|
|
scr_http_buf_str(&b, "Content-Length: ");
|
|
scr_http_buf_uint(&b, (unsigned long long)body_len);
|
|
scr_http_buf_str(&b, "\r\n");
|
|
} else {
|
|
scr_http_buf_str(&b, "Transfer-Encoding: chunked\r\n");
|
|
r->chunked = true;
|
|
}
|
|
}
|
|
if (declared_trailer && !r->chunked) {
|
|
static const char msg[] = "Trailers are invalid with this transfer encoding";
|
|
free(b.data);
|
|
scr_throw_error_msg_code(SCR_ERR_ERROR, msg, sizeof msg - 1, "ERR_HTTP_TRAILER_INVALID");
|
|
return false;
|
|
}
|
|
scr_http_buf_str(&b, "\r\n");
|
|
r->head_sent = true;
|
|
*out = b;
|
|
return true;
|
|
}
|
|
|
|
/* One socket write for [head][body in the response's framing][the final
|
|
* chunk + trailers when `last` on a chunked body] — Node likewise hands
|
|
* the implicit head and the first chunk (and end()'s terminator) to the
|
|
* socket as one write, so a small response is one segment instead of a
|
|
* head segment followed by a body segment. The bytes are unchanged. */
|
|
static void scr_http_res_emit(ScrHttpRes *r, const ScrHttpBuf *head, const char *data,
|
|
size_t len, bool last) {
|
|
if (!r->sock) return;
|
|
if (r->no_body) len = 0;
|
|
ScrNetSlice v[4];
|
|
size_t n = 0;
|
|
char size[32];
|
|
ScrHttpBuf tail = {NULL, 0, 0};
|
|
if (head != NULL && head->len > 0) v[n++] = (ScrNetSlice){head->data, head->len};
|
|
if (r->chunked) {
|
|
if (len > 0) {
|
|
int sn = snprintf(size, sizeof size, "%zx\r\n", len);
|
|
v[n++] = (ScrNetSlice){size, (size_t)sn};
|
|
v[n++] = (ScrNetSlice){data, len};
|
|
scr_http_buf_str(&tail, "\r\n");
|
|
}
|
|
if (last) scr_http_buf_chunk_end(&tail, &r->trailers);
|
|
if (tail.len > 0) v[n++] = (ScrNetSlice){tail.data, tail.len};
|
|
} else if (len > 0) {
|
|
v[n++] = (ScrNetSlice){data, len};
|
|
}
|
|
scr_net_sock_writev_native(r->sock, v, n);
|
|
free(tail.data);
|
|
}
|
|
|
|
/* Writes while corked coalesce here and flush as ONE write when the
|
|
* cork count reaches zero (or at end()) — Node's coalescing, and one
|
|
* DATA frame on the h2 lane. */
|
|
static void scr_http_cork_buffer(char **buf, size_t *held_len, size_t *held_cap,
|
|
const char *data, size_t len) {
|
|
if (len == 0) return;
|
|
if (*held_len + len > *held_cap) {
|
|
size_t cap = *held_cap ? *held_cap : 1024;
|
|
while (cap < *held_len + len) cap *= 2;
|
|
*buf = realloc(*buf, cap);
|
|
if (!*buf) scr_http_oom();
|
|
*held_cap = cap;
|
|
}
|
|
memcpy(*buf + *held_len, data, len);
|
|
*held_len += len;
|
|
}
|
|
|
|
static void scr_http_res_write_raw(ScrHttpRes *r, const char *data, size_t len) {
|
|
if (r->finished) return;
|
|
if (r->corked > 0) {
|
|
/* Node's write_() builds the implicit head even while corked */
|
|
if (!r->head_sent && r->h2_stream == NULL) {
|
|
ScrHttpBuf held = {NULL, 0, 0};
|
|
if (!scr_http_res_build_head(r, -1, &held)) return;
|
|
r->held_head = held.data;
|
|
r->held_head_len = held.len;
|
|
r->held_head_cap = held.cap;
|
|
}
|
|
if (scr_exc_pending()) return;
|
|
scr_http_cork_buffer(&r->cork_buf, &r->cork_len, &r->cork_cap, data, len);
|
|
return;
|
|
}
|
|
if (r->h2_stream != NULL) {
|
|
if (!r->head_sent) scr_http_res_send_head(r, -1);
|
|
if (scr_exc_pending()) return;
|
|
scr_http_h2_ops->write(r->h2_stream, data, len);
|
|
return;
|
|
}
|
|
/* streaming: the implicit head (chunked unless the user fixed the
|
|
* length) leaves with this first chunk */
|
|
ScrHttpBuf head = {r->held_head, r->held_head_len, r->held_head_cap};
|
|
r->held_head = NULL;
|
|
r->held_head_len = r->held_head_cap = 0;
|
|
if (!r->head_sent && !scr_http_res_build_head(r, -1, &head)) return;
|
|
if (!scr_exc_pending()) scr_http_res_emit(r, &head, data, len, false);
|
|
free(head.data);
|
|
}
|
|
|
|
/* Node 24.22's write_(): a write with the socket uncorked corks the
|
|
* message and uncorks it on the next tick, so the chunked writes of one
|
|
* tick go out as one chunk. */
|
|
static void scr_http_res_autocork_nt(ScrClosure *cb) {
|
|
ScrHttpRes *r = scr_box_get_ref(cb->caps[0]);
|
|
if (r->corked > 0) scr_http_res_uncork(r);
|
|
scr_http_res_release(r);
|
|
}
|
|
|
|
static void scr_http_res_autocork(ScrHttpRes *r) {
|
|
if (r->finished || r->h2_stream != NULL || r->sock == NULL || r->corked > 0) return;
|
|
r->corked++;
|
|
ScrClosure *cb = scr_closure_new((void *)&scr_http_res_autocork_nt, 1);
|
|
cb->caps[0] = scr_box_new_obj(&scr_http_res_retain_v, &scr_http_res_release_v, NULL);
|
|
scr_box_set_ref(cb->caps[0], scr_http_res_retain(r));
|
|
scr_next_tick(cb);
|
|
}
|
|
|
|
void scr_http_res_write_str(ScrHttpRes *r, ScrStr *data /*borrowed*/) {
|
|
double expected;
|
|
if (r->head_sent && scr_http_res_expected_length(r, &expected) &&
|
|
(double)(r->strict_bytes_written + data->len) > expected) {
|
|
r->strict_bytes_written += data->len;
|
|
scr_http_res_length_mismatch(r, expected);
|
|
r->strict_bytes_written -= data->len;
|
|
return;
|
|
}
|
|
if (r->strict_content_length) r->strict_bytes_written += data->len;
|
|
scr_http_res_autocork(r);
|
|
char *owned;
|
|
const char *bytes = scr_str_utf8(data, &owned);
|
|
scr_http_res_write_raw(r, bytes, data->len);
|
|
scr_str_utf8_end(owned);
|
|
}
|
|
|
|
void scr_http_res_write_bytes(ScrHttpRes *r, ScrBytes *data /*borrowed*/) {
|
|
SCR_BYTES_SNAPSHOT(data);
|
|
double expected;
|
|
if (r->head_sent && scr_http_res_expected_length(r, &expected) &&
|
|
(double)(r->strict_bytes_written + (data->len * scr_bytes_elem_size(data->elem))) > expected) {
|
|
r->strict_bytes_written += (data->len * scr_bytes_elem_size(data->elem));
|
|
scr_http_res_length_mismatch(r, expected);
|
|
r->strict_bytes_written -= (data->len * scr_bytes_elem_size(data->elem));
|
|
return;
|
|
}
|
|
if (r->strict_content_length) r->strict_bytes_written += (data->len * scr_bytes_elem_size(data->elem));
|
|
scr_http_res_autocork(r);
|
|
scr_http_res_write_raw(r, (const char *)data->data, (data->len * scr_bytes_elem_size(data->elem)));
|
|
}
|
|
|
|
/* res.flushHeaders(): the head goes out NOW (streaming framing — chunked
|
|
* unless the user fixed the length; the h2 lane's HEADERS frame). */
|
|
void scr_http_res_flush_headers(ScrHttpRes *r) {
|
|
if (!r->head_sent && !r->finished) scr_http_res_send_head(r, -1);
|
|
}
|
|
|
|
void scr_http_res_add_trailers(ScrHttpRes *r, ScrArr *pairs /*borrowed*/) {
|
|
if (r->finished) return;
|
|
scr_http_trailers_replace(&r->trailers, pairs);
|
|
}
|
|
|
|
/* cork()/uncork(): the count IS res.writableCorked; the last uncork
|
|
* flushes the coalesced bytes as one write. */
|
|
void scr_http_res_cork(ScrHttpRes *r) { r->corked++; }
|
|
|
|
void scr_http_res_uncork(ScrHttpRes *r) {
|
|
if (r->corked == 0) return;
|
|
if (--r->corked == 0) scr_http_res_cork_flush(r);
|
|
}
|
|
|
|
double scr_http_res_writable_corked(ScrHttpRes *r) { return (double)r->corked; }
|
|
|
|
bool scr_http_res_destroyed_flag(ScrHttpRes *r) { return r->destroyed || r->close_emitted; }
|
|
|
|
/* res.req — the paired request; a res.req = null write cleared it. */
|
|
void scr_http_res_set_req(ScrHttpRes *r, ScrHttpReq *req /*borrowed*/) {
|
|
if (r->req_ref) scr_http_req_release(r->req_ref);
|
|
r->req_ref = req ? scr_http_req_retain(req) : NULL;
|
|
}
|
|
|
|
/* res.setTimeout(ms[, cb]): a server response handles its own timeout
|
|
* event before the server decides whether to destroy the socket. */
|
|
void scr_http_res_set_timeout(ScrHttpRes *r, double ms, ScrClosure *cb /*moves, nullable*/) {
|
|
if (r->finished || r->close_emitted || !r->sock) {
|
|
if (cb) scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_net_sock_set_timeout(r->sock, ms);
|
|
if (cb) {
|
|
if (!r->h2_stream) scr_net_ls_add(&r->timeout_ls, cb, NULL, false);
|
|
else scr_net_sock_on_timeout(r->sock, cb, true);
|
|
}
|
|
}
|
|
|
|
void scr_http_res_set_timeout_plain(ScrHttpRes *r, double ms) {
|
|
scr_http_res_set_timeout(r, ms, NULL);
|
|
}
|
|
|
|
static void scr_http_conn_response_finished(struct ScrHttpConn *conn, bool keep_alive);
|
|
static void scr_http_queue_res_finish(ScrHttpRes *res);
|
|
|
|
/* Flush corked bytes as one write (corked already zero, or forced by
|
|
* end()). */
|
|
static void scr_http_res_cork_flush(ScrHttpRes *r) {
|
|
if (r->cork_len == 0 && r->held_head_len == 0) return;
|
|
char *held = r->cork_buf;
|
|
size_t held_len = r->cork_len;
|
|
r->cork_buf = NULL;
|
|
r->cork_len = r->cork_cap = 0;
|
|
scr_http_res_write_raw(r, held, held_len);
|
|
free(held);
|
|
}
|
|
|
|
static void scr_http_res_end_raw(ScrHttpRes *r, const char *data, size_t len) {
|
|
if (r->finished) return;
|
|
if (!r->end_ctx_set && r->h2_stream == NULL) {
|
|
r->end_ctx_set = true;
|
|
r->end_als = scr_als_capture();
|
|
}
|
|
double expected;
|
|
if (len > 0 && r->head_sent && scr_http_res_expected_length(r, &expected) &&
|
|
(double)(r->strict_bytes_written + len) != expected) {
|
|
r->strict_bytes_written += len;
|
|
scr_http_res_length_mismatch(r, expected);
|
|
r->strict_bytes_written -= len;
|
|
return;
|
|
}
|
|
if (r->strict_content_length) r->strict_bytes_written += len;
|
|
if (r->corked > 0 || r->cork_len > 0 || r->held_head_len > 0) {
|
|
/* end() flushes every cork level (Node) — the body streamed, so the
|
|
* framing below takes the already-committed streaming path. A corked
|
|
* chunked message buffers end()'s chunk too: one chunk goes out. */
|
|
if (r->corked > 0 && r->head_sent && r->chunked && r->h2_stream == NULL && len > 0) {
|
|
scr_http_cork_buffer(&r->cork_buf, &r->cork_len, &r->cork_cap, data, len);
|
|
data = "";
|
|
len = 0;
|
|
}
|
|
r->corked = 0;
|
|
scr_http_res_cork_flush(r);
|
|
if (scr_exc_pending()) return;
|
|
if (r->finished) return; /* a teardown inside the flush */
|
|
}
|
|
if (r->h2_stream != NULL) {
|
|
if (!r->head_sent) scr_http_res_send_head(r, (long long)len);
|
|
scr_http_h2_ops->end(r->h2_stream, data, len);
|
|
r->finished = true;
|
|
scr_http_queue_res_finish(r);
|
|
return;
|
|
}
|
|
if (!r->head_sent) {
|
|
/* whole body known NOW: Content-Length framing, Node's implicit head,
|
|
* head + body (+ the chunked terminator) as one write */
|
|
ScrHttpBuf head = {NULL, 0, 0};
|
|
if (!scr_http_res_build_head(r, (long long)len, &head)) return;
|
|
if (!scr_exc_pending()) scr_http_res_emit(r, &head, data, len, r->chunked);
|
|
free(head.data);
|
|
if (scr_exc_pending()) return;
|
|
} else {
|
|
scr_http_res_emit(r, NULL, data, len, r->chunked);
|
|
}
|
|
if (scr_http_res_expected_length(r, &expected) &&
|
|
(double)r->strict_bytes_written != expected) {
|
|
scr_http_res_length_mismatch(r, expected);
|
|
return;
|
|
}
|
|
r->finished = true;
|
|
scr_http_queue_res_finish(r);
|
|
if (r->conn) scr_http_conn_response_finished(r->conn, r->keep_alive);
|
|
}
|
|
|
|
void scr_http_res_end(ScrHttpRes *r) { scr_http_res_end_raw(r, "", 0); }
|
|
|
|
void scr_http_res_end_str(ScrHttpRes *r, ScrStr *data /*borrowed*/) {
|
|
char *owned;
|
|
const char *bytes = scr_str_utf8(data, &owned);
|
|
scr_http_res_end_raw(r, bytes, data->len);
|
|
scr_str_utf8_end(owned);
|
|
}
|
|
|
|
void scr_http_res_end_bytes(ScrHttpRes *r, ScrBytes *data /*borrowed*/) {
|
|
SCR_BYTES_SNAPSHOT(data);
|
|
scr_http_res_end_raw(r, (const char *)data->data, (data->len * scr_bytes_elem_size(data->elem)));
|
|
}
|
|
|
|
void scr_http_res_write_head(ScrHttpRes *r, double status) {
|
|
if (r->head_sent) return;
|
|
r->status = (int)status;
|
|
scr_http_res_send_head(r, -1); /* Node: an explicit writeHead means chunked unless CL was set */
|
|
}
|
|
|
|
/* writeHead(status, { ...literal headers... }): the packed form — names
|
|
* and values arrive as parallel string arrays (evaluation order was the
|
|
* literal's), each setHeader'd, then the head goes out. */
|
|
/* writeHead(status, headers) with a CHECKED-DYNAMIC headers value (an
|
|
* untyped JS helper's parameter — the suite's test(headers) idiom): OBJ
|
|
* entries setHeader in insertion order (string values verbatim, numbers
|
|
* via ToString, string arrays one line per element via the append path),
|
|
* then the head goes out. undefined/null headers are the plain head;
|
|
* other kinds throw the ERR_INVALID_ARG_TYPE-flavored fence. */
|
|
void scr_http_res_write_head_dyn(ScrHttpRes *r, double status, const ScrDyn *headers) {
|
|
if (r->head_sent) return;
|
|
if (headers->kind == SCR_DYN_UNDEF || headers->kind == SCR_DYN_NULL) {
|
|
scr_http_res_write_head(r, status);
|
|
return;
|
|
}
|
|
if (headers->kind != SCR_DYN_OBJ) {
|
|
scr_dyn_arg_type_fail("headers", "an instance of Object", headers);
|
|
return;
|
|
}
|
|
for (size_t i = 0; i < headers->v.obj.len; i++) {
|
|
const ScrDynEntry *e = &headers->v.obj.entries[i];
|
|
const ScrDyn *v = e->value;
|
|
ScrStr *name = scr_str_new(e->key, e->key_len);
|
|
if (v->kind == SCR_DYN_STR) {
|
|
scr_http_res_set_header(r, name, v->v.str);
|
|
} else if (v->kind == SCR_DYN_NUM) {
|
|
char num[32];
|
|
size_t n = scr_f64_to_str(v->v.num, num);
|
|
ScrStr *value = scr_str_new(num, n);
|
|
scr_http_res_set_header(r, name, value);
|
|
scr_str_release(value);
|
|
} else {
|
|
scr_str_release(name);
|
|
scr_dyn_arg_type_fail("value", "of type string or number", v);
|
|
return;
|
|
}
|
|
scr_str_release(name);
|
|
}
|
|
scr_http_res_write_head(r, status);
|
|
}
|
|
|
|
void scr_http_res_write_head_n(ScrHttpRes *r, double status, ScrArr *names, ScrArr *values) {
|
|
if (r->head_sent) return;
|
|
size_t n = (size_t)scr_arr_len(names);
|
|
for (size_t i = 0; i < n; i++) {
|
|
ScrStr *name = (ScrStr *)scr_arr_get_ref(names, (double)i);
|
|
ScrStr *value = (ScrStr *)scr_arr_get_ref(values, (double)i);
|
|
scr_http_res_set_header(r, name, value);
|
|
scr_str_release(name);
|
|
scr_str_release(value);
|
|
}
|
|
scr_http_res_write_head(r, status);
|
|
}
|
|
|
|
/* writeHead(status, record): the flat [k0, v0, k1, v1, ...] pairs the
|
|
* env.pairs helper produces — each entry setHeader'd, then the head goes
|
|
* out (undefined-valued entries were dropped by the helper, the Node-
|
|
* drops-undefined env rule; Node's writeHead would throw on them). */
|
|
void scr_http_res_write_head_pairs(ScrHttpRes *r, double status, ScrArr *pairs /*borrowed*/) {
|
|
if (r->head_sent) return;
|
|
size_t n = (size_t)scr_arr_len(pairs);
|
|
for (size_t i = 0; i + 1 < n; i += 2) {
|
|
ScrStr *name = (ScrStr *)scr_arr_get_ref(pairs, (double)i);
|
|
ScrStr *value = (ScrStr *)scr_arr_get_ref(pairs, (double)(i + 1));
|
|
/* A REPEATED name within this pairs array is an ARRAY-VALUED header
|
|
* (["a", "b"] expands to consecutive same-name pairs): the first
|
|
* occurrence replaces any earlier setHeader (Node's writeHead-wins
|
|
* merge), the repeats APPEND — Node writes one line per element. */
|
|
bool repeat = false;
|
|
for (size_t j = 0; j < i && !repeat; j += 2) {
|
|
ScrStr *prev = (ScrStr *)scr_arr_get_ref(pairs, (double)j);
|
|
if (prev->len == name->len) {
|
|
bool eq = true;
|
|
for (size_t k = 0; k < name->len && eq; k++) {
|
|
if (tolower((unsigned char)prev->data[k]) != tolower((unsigned char)name->data[k])) eq = false;
|
|
}
|
|
repeat = eq;
|
|
}
|
|
scr_str_release(prev);
|
|
}
|
|
if (repeat) scr_http_res_append_header(r, name, value);
|
|
else scr_http_res_set_header(r, name, value);
|
|
scr_str_release(name);
|
|
scr_str_release(value);
|
|
}
|
|
scr_http_res_write_head(r, status);
|
|
}
|
|
|
|
/* res.destroy(): tears the connection down NOW (mid-stream RST is the
|
|
* point — portless aborts a proxied response with it); 'close' follows
|
|
* through the teardown path. Node-matched: no 'error' from a destroy. */
|
|
void scr_http_res_destroy(ScrHttpRes *r) {
|
|
r->destroyed = true;
|
|
if (r->h2_stream != NULL) {
|
|
/* h2 compat: destroy the STREAM (RST), never the shared session */
|
|
scr_http_h2_ops->destroy(r->h2_stream);
|
|
return;
|
|
}
|
|
if (r->sock) scr_net_sock_destroy(r->sock);
|
|
}
|
|
|
|
void scr_http_res_on_close(ScrHttpRes *r, ScrClosure *cb /*moves*/, bool once) {
|
|
if (r->close_emitted) {
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_net_ls_add(&r->close_ls, cb, NULL, once);
|
|
}
|
|
|
|
/* ── the deferred-emit queue (scr_net.c's proto sweep drains it) ───────
|
|
*
|
|
* 'close' on requests/responses and the client's own 'close' fire a
|
|
* sweep pass AFTER the work that flagged them — Node's ordering (the
|
|
* handler's synchronous tail runs first; res 'close' precedes req
|
|
* 'close'; the client's res 'end' precedes req 'close' precedes res
|
|
* 'close'). RES_ABORTED is the mid-body premature-close 'error' on a
|
|
* client response ("aborted", Node's message) — queued between the
|
|
* client close and the response close, the oracle-pinned order. Each
|
|
* entry holds +1 on its handle; firing settles the handle (listener
|
|
* lists drop — the cycle story). */
|
|
|
|
enum {
|
|
SCR_HTTP_EMIT_RES_CLOSE = 1, /* ScrHttpRes */
|
|
SCR_HTTP_EMIT_REQ_CLOSE = 2, /* ScrHttpReq */
|
|
SCR_HTTP_EMIT_REQ_ABORTED = 3, /* ScrHttpReq: 'error' ("aborted") */
|
|
SCR_HTTP_EMIT_CLIENT_CLOSE = 4, /* ScrHttpClientReq */
|
|
SCR_HTTP_EMIT_RES_FINISH = 5, /* ScrHttpRes: the end(cb) callbacks */
|
|
SCR_HTTP_EMIT_REQ_DRAIN = SCR_HTTP_EMIT_REQ_DRAIN_K, /* ScrHttpReq: resume()'s drain */
|
|
SCR_HTTP_EMIT_RES_WCB = 7, /* ScrHttpRes: write(chunk, cb) callbacks */
|
|
SCR_HTTP_EMIT_CLIENT_SOCKET = 8, /* ScrHttpClientReq: assigned socket */
|
|
SCR_HTTP_EMIT_CLIENT_FINISH = 9, /* ScrHttpClientReq: outgoing body flushed */
|
|
SCR_HTTP_EMIT_CLIENT_ABORT = 10, /* ScrHttpClientReq: abort() notification */
|
|
SCR_HTTP_EMIT_CLIENT_DESTROY_ERROR = 11 /* ScrHttpClientReq: destroy(err) before a dial */
|
|
};
|
|
|
|
typedef struct {
|
|
int kind;
|
|
void *h; /* +1 */
|
|
} ScrHttpEmit;
|
|
|
|
static SCR_TL ScrHttpEmit *scr_http_emits = NULL;
|
|
static SCR_TL size_t scr_http_emits_head = 0, scr_http_emits_len = 0, scr_http_emits_cap = 0;
|
|
|
|
static void scr_http_client_release_internal(struct ScrHttpClientReq *c);
|
|
static void scr_http_client_settle(struct ScrHttpClientReq *c);
|
|
static void scr_http_client_emit_socket(struct ScrHttpClientReq *c);
|
|
static void scr_http_client_emit_finish(struct ScrHttpClientReq *c);
|
|
static void scr_http_client_emit_destroy_error(struct ScrHttpClientReq *c);
|
|
static void scr_http_client_emit_abort(struct ScrHttpClientReq *c);
|
|
|
|
/* Exit-time flag (set by the atexit cleanup, which runs BEFORE the net
|
|
* unit's — atexit LIFO, http installs after net): once the emits queue
|
|
* has drained for the last time, teardown paths that would queue close
|
|
* emits (scr_net_cleanup_atexit freeing in-flight parsers) discard the
|
|
* payload instead — nothing will ever sweep again. */
|
|
static SCR_TL bool scr_http_exiting = false;
|
|
|
|
static void scr_http_emit_release(ScrHttpEmit *e);
|
|
|
|
static void scr_http_emit_push(int kind, void *h /*moves +1*/) {
|
|
if (scr_http_exiting) {
|
|
ScrHttpEmit e = { kind, h };
|
|
scr_http_emit_release(&e);
|
|
return;
|
|
}
|
|
if (scr_http_emits_len == scr_http_emits_cap) {
|
|
/* compact the consumed head first */
|
|
if (scr_http_emits_head > 0) {
|
|
memmove(scr_http_emits, scr_http_emits + scr_http_emits_head,
|
|
(scr_http_emits_len - scr_http_emits_head) * sizeof *scr_http_emits);
|
|
scr_http_emits_len -= scr_http_emits_head;
|
|
scr_http_emits_head = 0;
|
|
}
|
|
if (scr_http_emits_len == scr_http_emits_cap) {
|
|
scr_http_emits_cap = scr_http_emits_cap ? scr_http_emits_cap * 2 : 8;
|
|
scr_http_emits = realloc(scr_http_emits, scr_http_emits_cap * sizeof *scr_http_emits);
|
|
if (!scr_http_emits) scr_http_oom();
|
|
}
|
|
}
|
|
scr_http_emits[scr_http_emits_len].kind = kind;
|
|
scr_http_emits[scr_http_emits_len].h = h;
|
|
scr_http_emits_len++;
|
|
}
|
|
|
|
static void scr_http_emit_release(ScrHttpEmit *e) {
|
|
switch (e->kind) {
|
|
case SCR_HTTP_EMIT_RES_CLOSE:
|
|
case SCR_HTTP_EMIT_RES_WCB:
|
|
case SCR_HTTP_EMIT_RES_FINISH: scr_http_res_release((ScrHttpRes *)e->h); break;
|
|
case SCR_HTTP_EMIT_REQ_CLOSE:
|
|
case SCR_HTTP_EMIT_REQ_DRAIN:
|
|
case SCR_HTTP_EMIT_REQ_ABORTED: scr_http_req_release((ScrHttpReq *)e->h); break;
|
|
case SCR_HTTP_EMIT_CLIENT_CLOSE:
|
|
case SCR_HTTP_EMIT_CLIENT_SOCKET:
|
|
case SCR_HTTP_EMIT_CLIENT_FINISH:
|
|
case SCR_HTTP_EMIT_CLIENT_ABORT:
|
|
case SCR_HTTP_EMIT_CLIENT_DESTROY_ERROR:
|
|
scr_http_client_release_internal((struct ScrHttpClientReq *)e->h);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Enqueue helpers: idempotent per handle (the close_queued flags). */
|
|
static void scr_http_queue_res_close(ScrHttpRes *res) {
|
|
if (res->close_queued || res->close_emitted) return;
|
|
res->close_queued = true;
|
|
scr_http_emit_push(SCR_HTTP_EMIT_RES_CLOSE, scr_http_res_retain(res));
|
|
}
|
|
|
|
static void scr_http_queue_req_close(ScrHttpReq *req) {
|
|
if (req->close_queued || req->close_emitted) return;
|
|
req->close_queued = true;
|
|
scr_http_emit_push(SCR_HTTP_EMIT_REQ_CLOSE, scr_http_req_retain(req));
|
|
}
|
|
|
|
static void scr_http_queue_req_aborted(ScrHttpReq *req) {
|
|
scr_http_emit_push(SCR_HTTP_EMIT_REQ_ABORTED, scr_http_req_retain(req));
|
|
}
|
|
|
|
/* The end(cb) deferral: 'finish'-time callbacks fire one sweep after the
|
|
* body went out (the handler's synchronous tail runs first, Node's
|
|
* deferred emit — the res 'close' precedent). */
|
|
static void scr_http_queue_res_finish(ScrHttpRes *res) {
|
|
if (res->finish_queued) return;
|
|
res->finish_queued = true;
|
|
scr_http_emit_push(SCR_HTTP_EMIT_RES_FINISH, scr_http_res_retain(res));
|
|
}
|
|
|
|
/* res.on('finish', cb): fires deferred once the body went out, after the
|
|
* end(cb) callbacks and after the response detached its socket. */
|
|
void scr_http_res_on_finish(ScrHttpRes *r, ScrClosure *cb /*moves*/) {
|
|
scr_net_ls_add(&r->finish_ls, cb, NULL, true);
|
|
if (r->finished) scr_http_queue_res_finish(r);
|
|
}
|
|
|
|
/* res.end(cb)'s callback slot: registered just before the end call. The
|
|
* callbacks run where Node's OutgoingMessage flushes its end callbacks:
|
|
* after writableFinished turns true and before the 'finish' listeners,
|
|
* with the socket still attached. A callback registered after the
|
|
* response finished runs synchronously, where Node passes
|
|
* ERR_STREAM_ALREADY_FINISHED (this surface's callbacks take no
|
|
* arguments). The h2 compat response keeps the 'finish' listener order. */
|
|
void scr_http_res_on_end_callback(ScrHttpRes *r, ScrClosure *cb /*moves*/) {
|
|
if (r->h2_stream != NULL) {
|
|
scr_http_res_on_finish(r, cb);
|
|
return;
|
|
}
|
|
if (r->writable_finished) {
|
|
ScrNetLs now = {0};
|
|
scr_net_ls_add(&now, cb, NULL, true);
|
|
scr_net_fire0_this(&now, r, SCR_DYNH_HTTP_RES);
|
|
scr_net_ls_drop(&now);
|
|
return;
|
|
}
|
|
scr_net_ls_add(&r->endcb_ls, cb, NULL, true);
|
|
if (r->finished) scr_http_queue_res_finish(r);
|
|
}
|
|
|
|
/* res.write(chunk, cb): the callback fires from the queue (this surface
|
|
* flushes synchronously into the socket buffer — the deferral keeps the
|
|
* cb off the writing stack, Node's contract). */
|
|
void scr_http_res_on_write_flush(ScrHttpRes *r, ScrClosure *cb /*moves*/) {
|
|
scr_net_ls_add(&r->wcb_ls, cb, NULL, true);
|
|
if (!r->wcb_queued) {
|
|
r->wcb_queued = true;
|
|
scr_http_emit_push(SCR_HTTP_EMIT_RES_WCB, scr_http_res_retain(r));
|
|
}
|
|
}
|
|
|
|
static bool scr_http_proto_pending(void) { return scr_http_emits_head < scr_http_emits_len; }
|
|
|
|
static ScrAlsCtx *scr_http_client_ctx(struct ScrHttpClientReq *c);
|
|
|
|
/* The context an emit's handle was created in (borrowed). */
|
|
static ScrAlsCtx *scr_http_emit_ctx(const ScrHttpEmit *e) {
|
|
switch (e->kind) {
|
|
case SCR_HTTP_EMIT_RES_CLOSE:
|
|
case SCR_HTTP_EMIT_RES_FINISH: {
|
|
const ScrHttpRes *res = (const ScrHttpRes *)e->h;
|
|
return res->end_ctx_set ? res->end_als : res->als;
|
|
}
|
|
case SCR_HTTP_EMIT_RES_WCB: return ((ScrHttpRes *)e->h)->als;
|
|
case SCR_HTTP_EMIT_REQ_CLOSE:
|
|
case SCR_HTTP_EMIT_REQ_DRAIN:
|
|
case SCR_HTTP_EMIT_REQ_ABORTED: return ((ScrHttpReq *)e->h)->als;
|
|
default: return scr_http_client_ctx((struct ScrHttpClientReq *)e->h);
|
|
}
|
|
}
|
|
|
|
static void scr_http_proto_sweep(void) {
|
|
while (scr_http_emits_head < scr_http_emits_len) {
|
|
ScrHttpEmit e = scr_http_emits[scr_http_emits_head++];
|
|
ScrAlsCtx *prev = scr_als_dispatch_enter(scr_http_emit_ctx(&e));
|
|
switch (e.kind) {
|
|
case SCR_HTTP_EMIT_RES_CLOSE: {
|
|
ScrHttpRes *res = (ScrHttpRes *)e.h;
|
|
if (!res->close_emitted) {
|
|
res->close_emitted = true;
|
|
res->destroyed = true; /* Node: destroyed reads true inside 'close' */
|
|
res->socket_detached = true;
|
|
scr_net_fire0_this(&res->close_ls, res, SCR_DYNH_HTTP_RES);
|
|
scr_net_ls_drop(&res->close_ls);
|
|
scr_net_ls_drop(&res->timeout_ls);
|
|
}
|
|
break;
|
|
}
|
|
case SCR_HTTP_EMIT_RES_WCB: {
|
|
ScrHttpRes *res = (ScrHttpRes *)e.h;
|
|
res->wcb_queued = false; /* later write(chunk, cb)s re-queue */
|
|
scr_net_fire0_this(&res->wcb_ls, res, SCR_DYNH_HTTP_RES);
|
|
break;
|
|
}
|
|
case SCR_HTTP_EMIT_REQ_CLOSE: {
|
|
ScrHttpReq *req = (ScrHttpReq *)e.h;
|
|
if (!req->close_emitted) {
|
|
req->close_emitted = true;
|
|
req->destroyed = true; /* Node: destroyed reads true inside 'close' */
|
|
scr_net_fire0_this(&req->close_ls, req, SCR_DYNH_HTTP_REQ);
|
|
scr_net_ls_drop(&req->err_ls);
|
|
scr_net_ls_drop(&req->close_ls);
|
|
scr_net_ls_drop(&req->timeout_ls);
|
|
scr_net_ls_drop(&req->aborted_ls);
|
|
}
|
|
break;
|
|
}
|
|
case SCR_HTTP_EMIT_REQ_DRAIN: {
|
|
/* resume() after pause(): the held bytes deliver as one chunk (the
|
|
* reassembled body is the contract), then a deferred 'end'. A
|
|
* re-pause mid-drain re-buffers — the swap keeps the walk sound. */
|
|
ScrHttpReq *req = (ScrHttpReq *)e.h;
|
|
req->drain_queued = false;
|
|
if (!req->paused) {
|
|
char *held = req->pend;
|
|
size_t held_len = req->pend_len;
|
|
req->pend = NULL;
|
|
req->pend_len = req->pend_cap = 0;
|
|
if (held_len > 0) scr_http_req_deliver(req, held, held_len);
|
|
free(held);
|
|
scr_http_req_update_hold(req);
|
|
if (!scr_exc_pending() && !req->paused && req->end_pending) {
|
|
req->end_pending = false;
|
|
scr_http_req_finish(req, true);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case SCR_HTTP_EMIT_REQ_ABORTED: {
|
|
ScrHttpReq *req = (ScrHttpReq *)e.h;
|
|
if (!req->close_emitted) {
|
|
ScrStr *msg = scr_str_new("aborted", 7);
|
|
scr_net_fire_err_this(&req->err_ls, msg, req, SCR_DYNH_HTTP_REQ);
|
|
scr_str_release(msg);
|
|
}
|
|
break;
|
|
}
|
|
case SCR_HTTP_EMIT_CLIENT_CLOSE:
|
|
scr_http_client_settle((struct ScrHttpClientReq *)e.h);
|
|
break;
|
|
case SCR_HTTP_EMIT_CLIENT_SOCKET:
|
|
scr_http_client_emit_socket((struct ScrHttpClientReq *)e.h);
|
|
break;
|
|
case SCR_HTTP_EMIT_CLIENT_FINISH:
|
|
scr_http_client_emit_finish((struct ScrHttpClientReq *)e.h);
|
|
break;
|
|
case SCR_HTTP_EMIT_CLIENT_ABORT:
|
|
scr_http_client_emit_abort((struct ScrHttpClientReq *)e.h);
|
|
break;
|
|
case SCR_HTTP_EMIT_CLIENT_DESTROY_ERROR:
|
|
scr_http_client_emit_destroy_error((struct ScrHttpClientReq *)e.h);
|
|
break;
|
|
case SCR_HTTP_EMIT_RES_FINISH: {
|
|
ScrHttpRes *res = (ScrHttpRes *)e.h;
|
|
res->finish_queued = false; /* a later on('finish') on the finished res re-queues */
|
|
res->writable_finished = true;
|
|
ScrNetLs endcbs = res->endcb_ls;
|
|
res->endcb_ls = (ScrNetLs){0};
|
|
scr_net_fire0_this(&endcbs, res, SCR_DYNH_HTTP_RES);
|
|
scr_net_ls_drop(&endcbs);
|
|
res->socket_detached = true;
|
|
if (!scr_exc_pending()) scr_net_fire0_this(&res->finish_ls, res, SCR_DYNH_HTTP_RES);
|
|
break;
|
|
}
|
|
}
|
|
scr_als_dispatch_leave(prev);
|
|
scr_http_emit_release(&e);
|
|
if (scr_exc_pending()) return;
|
|
}
|
|
scr_http_emits_head = scr_http_emits_len = 0;
|
|
}
|
|
|
|
static void scr_http_clients_cleanup(void);
|
|
static void scr_http_agents_cleanup(void);
|
|
|
|
static void scr_http_cleanup_atexit(void) {
|
|
scr_http_exiting = true;
|
|
while (scr_http_emits_head < scr_http_emits_len) {
|
|
ScrHttpEmit e = scr_http_emits[scr_http_emits_head++];
|
|
scr_http_emit_release(&e);
|
|
}
|
|
free(scr_http_emits);
|
|
scr_http_emits = NULL;
|
|
scr_http_emits_head = scr_http_emits_len = scr_http_emits_cap = 0;
|
|
scr_http_agents_cleanup(); /* break agent↔client edges first */
|
|
scr_http_clients_cleanup();
|
|
}
|
|
|
|
/* One-time wiring into the net sweep (both createServer and request call
|
|
* through here). */
|
|
static void scr_http_install(void) {
|
|
static SCR_TL bool installed = false;
|
|
if (installed) return;
|
|
installed = true;
|
|
scr_atexit(scr_http_cleanup_atexit);
|
|
scr_net_set_proto_sweep(&scr_http_proto_pending, &scr_http_proto_sweep);
|
|
}
|
|
|
|
/* ── the per-connection parser ───────────────────────────────────────── */
|
|
|
|
typedef enum {
|
|
SCR_HTTP_HEAD = 0,
|
|
SCR_HTTP_BODY_CL,
|
|
SCR_HTTP_CHUNK_SIZE,
|
|
SCR_HTTP_CHUNK_DATA,
|
|
SCR_HTTP_CHUNK_CRLF,
|
|
SCR_HTTP_CHUNK_TRAILER,
|
|
SCR_HTTP_BODY_EOF, /* RESPONSE mode: no framing header — body until EOF */
|
|
} ScrHttpParseState;
|
|
|
|
/* agent.totalSocketCount: the sockets an agent dialed that have not
|
|
* closed yet, idle pooled ones included. The agent and each counted
|
|
* connection hold the cell (+1), so a socket that closes after the agent
|
|
* is gone still lowers a live count. */
|
|
typedef struct ScrHttpSockCount {
|
|
size_t rc;
|
|
size_t n;
|
|
} ScrHttpSockCount;
|
|
|
|
static void scr_http_sock_count_release(ScrHttpSockCount *k) {
|
|
if (k != NULL && --k->rc == 0) free(k);
|
|
}
|
|
|
|
typedef struct ScrHttpConn {
|
|
ScrNetSocket *sock; /* BORROWED: the ctx lives and dies with the socket
|
|
* (a retained ref here would be a cycle the RC
|
|
* could never break) */
|
|
struct ScrHttpSrvCtx *srv; /* +1; NULL in client mode — the server ctx
|
|
* whose 'request' listeners fire per parsed
|
|
* request (EMIT-time resolution, Node's
|
|
* dispatch: a listener installed after the
|
|
* accept but before the request still runs) */
|
|
char *buf;
|
|
size_t len, cap;
|
|
ScrHttpParseState state;
|
|
size_t body_remaining;
|
|
size_t head_bytes;
|
|
size_t trailer_bytes, trailer_fields;
|
|
ScrHttpReq *req; /* the in-flight request (server) or response (client),
|
|
* +1; NULL between requests / before the head */
|
|
ScrHttpRes *res; /* +1; server mode only */
|
|
bool close_after; /* the response said Connection: close */
|
|
bool pumping; /* inside scr_http_conn_pump */
|
|
bool keep_alive_timer; /* the socket's idle timer is the keep-alive timeout */
|
|
size_t max_header_size; /* maxHeaderSize option; 0 = SCR_HTTP_MAX_HEADER_SIZE */
|
|
struct ScrHttpSockCount *sock_count; /* client mode: the agent count that includes
|
|
* this socket (+1), or NULL */
|
|
struct ScrHttpAgent *pool_agent; /* client mode: the agent whose free list holds
|
|
* this idle socket (borrowed), or NULL */
|
|
bool header_overflow; /* client mode: the response head exceeded a bound */
|
|
bool res_close_deferred; /* the response finished inside the pump while
|
|
* the request body was still arriving */
|
|
bool client_mode; /* RESPONSE parsing: status line, EOF bodies */
|
|
bool client_resp_started; /* the status line parsed (conn-level twin of
|
|
* the client's response_started — the pump
|
|
* runs below the client struct's definition) */
|
|
struct ScrHttpClientReq *client; /* +1 in client mode; released at close */
|
|
} ScrHttpConn;
|
|
|
|
/* The connection's socket joins an agent's totalSocketCount when it starts
|
|
* to dial, and leaves it when it closes. */
|
|
static void scr_http_conn_count(ScrHttpConn *conn, ScrHttpSockCount *k) {
|
|
if (conn == NULL || conn->sock_count != NULL) return;
|
|
k->rc++;
|
|
k->n++;
|
|
conn->sock_count = k;
|
|
}
|
|
|
|
static void scr_http_conn_uncount(ScrHttpConn *conn) {
|
|
ScrHttpSockCount *k = conn != NULL ? conn->sock_count : NULL;
|
|
if (k == NULL) return;
|
|
conn->sock_count = NULL;
|
|
k->n--;
|
|
scr_http_sock_count_release(k);
|
|
}
|
|
|
|
/* Client-mode forward declarations (implementations live with the client
|
|
* below the server parser). */
|
|
static bool scr_http_client_parse_head(ScrHttpConn *conn, size_t head_len);
|
|
static void scr_http_client_head_overflow(ScrHttpConn *conn);
|
|
|
|
/* The maxHeaderSize bound (target or status message plus header names and
|
|
* values), and the raw-byte bound that backs it while a head is still
|
|
* arriving: large enough for any head under the size bound. */
|
|
static size_t scr_http_conn_header_limit(const ScrHttpConn *conn) {
|
|
return conn->max_header_size ? conn->max_header_size : SCR_HTTP_MAX_HEADER_SIZE;
|
|
}
|
|
|
|
static size_t scr_http_conn_raw_limit(const ScrHttpConn *conn) {
|
|
size_t limit = 2 * scr_http_conn_header_limit(conn) + 8192;
|
|
return limit > SCR_HTTP_MAX_HEADER_BYTES ? limit : SCR_HTTP_MAX_HEADER_BYTES;
|
|
}
|
|
|
|
/* The trailer byte budget includes the request/response head and every
|
|
* trailer line on the wire, including the terminating blank line; the field
|
|
* count covers the trailer block alone, like Node's. Counters live on
|
|
* the connection because an early response can release conn->req while the
|
|
* parser must continue consuming and validating its incoming chunk stream. */
|
|
static bool scr_http_conn_trailer_fits(const ScrHttpConn *conn, size_t bytes, bool field) {
|
|
size_t raw = scr_http_conn_raw_limit(conn);
|
|
if (conn->head_bytes > raw || conn->trailer_bytes > raw - conn->head_bytes) return false;
|
|
size_t remaining = raw - conn->head_bytes - conn->trailer_bytes;
|
|
if (bytes > remaining) return false;
|
|
return !field || conn->trailer_fields < SCR_HTTP_MAX_HEADER_FIELDS;
|
|
}
|
|
|
|
/* The server-side ctx: the 'request' listener list, shared by every
|
|
* connection. REFCOUNTED: the server's native-conn chain holds one ref
|
|
* (released through scr_http_srv_ctx_free) and each live connection
|
|
* holds one — a socket can outlive its server handle. createServer's
|
|
* eager handler is just the first listener; server.on("request", ...)
|
|
* appends (scr_http_server_on_request), and requests fire the list
|
|
* snapshot-at-emit like every other event here. */
|
|
typedef struct ScrHttpSrvCtx {
|
|
int proto; /* SCR_NET_PROTO_HTTP1 — FIRST: the http_ctx alias slot is
|
|
* shared with scr_http2.c's ScrH2SrvCtx; each family checks
|
|
* the tag before treating the ctx as its own */
|
|
size_t rc;
|
|
ScrNetLs request_ls;
|
|
ScrNetLs check_continue_ls; /* 'checkContinue' (req, res) */
|
|
ScrNetLs check_expectation_ls; /* 'checkExpectation' (req, res) */
|
|
ScrNetLs upgrade_ls;
|
|
ScrNetLs connect_ls; /* HTTP CONNECT — the upgrade machinery's twin */
|
|
bool join_dup; /* createServer({ joinDuplicateHeaders: true }) */
|
|
size_t max_header_size; /* createServer({ maxHeaderSize }); 0 = the default */
|
|
bool require_host_header; /* HTTP/1.1 Host is required by default */
|
|
} ScrHttpSrvCtx;
|
|
|
|
static ScrHttpSrvCtx *scr_http_srv_ctx_retain(ScrHttpSrvCtx *ctx) {
|
|
ctx->rc++;
|
|
return ctx;
|
|
}
|
|
|
|
static void scr_http_srv_ctx_release(ScrHttpSrvCtx *ctx) {
|
|
if (--ctx->rc > 0) return;
|
|
scr_net_ls_drop(&ctx->request_ls);
|
|
scr_net_ls_drop(&ctx->check_continue_ls);
|
|
scr_net_ls_drop(&ctx->check_expectation_ls);
|
|
scr_net_ls_drop(&ctx->upgrade_ls);
|
|
scr_net_ls_drop(&ctx->connect_ls);
|
|
free(ctx);
|
|
}
|
|
|
|
static void scr_http_srv_ctx_free(void *p) { scr_http_srv_ctx_release((ScrHttpSrvCtx *)p); }
|
|
|
|
/* The server-settle hook (scr_net_server_set_proto_settle): the ctx's own
|
|
* listener lists drop when the server settles — no request can fire past
|
|
* the settle (the fd is closed and every connection drained), and a
|
|
* handler closure capturing its own server through a dyn binding box
|
|
* would otherwise cycle through collector-invisible edges. */
|
|
static void scr_http_srv_ctx_settle(void *p) {
|
|
ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)p;
|
|
scr_net_ls_drop(&ctx->request_ls);
|
|
scr_net_ls_drop(&ctx->check_continue_ls);
|
|
scr_net_ls_drop(&ctx->check_expectation_ls);
|
|
scr_net_ls_drop(&ctx->upgrade_ls);
|
|
scr_net_ls_drop(&ctx->connect_ls);
|
|
}
|
|
|
|
/* Combined HTTP/2 allowHTTP1 servers retain the HTTP/1 parser ctx behind
|
|
* their h2-facing alias. These narrow hooks let scr_http2.c mirror request
|
|
* listeners and settle both protocol lists without exposing the ctx shape. */
|
|
void scr_http1_ctx_on_request(void *p, ScrClosure *cb /*moves*/, ScrHttpReqFn fn, bool once,
|
|
ScrNetOnce *shared_once /*moves, nullable*/) {
|
|
ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)p;
|
|
if (ctx == NULL || ctx->proto != SCR_NET_PROTO_HTTP1) {
|
|
scr_closure_release(cb);
|
|
scr_net_once_release(shared_once);
|
|
return;
|
|
}
|
|
if (shared_once != NULL) {
|
|
scr_net_ls_add_shared_once(&ctx->request_ls, cb, (void *)fn, shared_once);
|
|
} else {
|
|
scr_net_ls_add(&ctx->request_ls, cb, (void *)fn, once);
|
|
}
|
|
}
|
|
|
|
void scr_http1_ctx_on_connect(void *p, ScrClosure *cb /*moves*/, ScrHttpUpgradeFn fn, bool once,
|
|
ScrNetOnce *shared_once /*moves, nullable*/) {
|
|
ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)p;
|
|
if (ctx == NULL || ctx->proto != SCR_NET_PROTO_HTTP1) {
|
|
scr_closure_release(cb);
|
|
scr_net_once_release(shared_once);
|
|
return;
|
|
}
|
|
if (shared_once != NULL) {
|
|
scr_net_ls_add_shared_once(&ctx->connect_ls, cb, (void *)fn, shared_once);
|
|
} else {
|
|
scr_net_ls_add(&ctx->connect_ls, cb, (void *)fn, once);
|
|
}
|
|
}
|
|
|
|
void scr_http1_ctx_on_upgrade(void *p, ScrClosure *cb /*moves*/, ScrHttpUpgradeFn fn, bool once) {
|
|
ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)p;
|
|
if (ctx == NULL || ctx->proto != SCR_NET_PROTO_HTTP1) {
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_net_ls_add(&ctx->upgrade_ls, cb, (void *)fn, once);
|
|
}
|
|
|
|
void scr_http1_ctx_settle(void *p) { scr_http_srv_ctx_settle(p); }
|
|
|
|
static void scr_http_conn_drop_request(ScrHttpConn *conn, bool fire_end) {
|
|
/* the deferred 'close' emits: a request whose body was read to 'end'
|
|
* was destroyed then and closes first; otherwise res before req (Node
|
|
* ends an unread request after the response finishes) */
|
|
if (conn->req && conn->req->end_consumed) scr_http_queue_req_close(conn->req);
|
|
if (conn->res) scr_http_queue_res_close(conn->res);
|
|
if (conn->req) scr_http_queue_req_close(conn->req);
|
|
if (conn->req) {
|
|
scr_http_req_finish(conn->req, fire_end);
|
|
scr_http_req_release(conn->req);
|
|
conn->req = NULL;
|
|
}
|
|
if (conn->res) {
|
|
conn->res->conn = NULL;
|
|
scr_http_res_release(conn->res);
|
|
conn->res = NULL;
|
|
}
|
|
}
|
|
|
|
/* Ends a request whose response already finished and detaches the
|
|
* exchange; the queued emits hold their own references. Node emits the
|
|
* request's 'end' after the response's 'close' when body bytes arrived,
|
|
* and before it otherwise. */
|
|
static void scr_http_conn_end_after_response(ScrHttpConn *conn) {
|
|
ScrHttpReq *r = conn->req;
|
|
bool before_close = !r->body_seen;
|
|
conn->res_close_deferred = false;
|
|
r->received = true;
|
|
r->end_pending = true;
|
|
if (before_close) scr_http_req_queue_drain(r);
|
|
if (conn->res) scr_http_queue_res_close(conn->res);
|
|
if (!before_close) scr_http_req_queue_drain(r);
|
|
scr_http_queue_req_close(r);
|
|
conn->req = NULL;
|
|
scr_http_req_release(r);
|
|
if (conn->res) {
|
|
conn->res->conn = NULL;
|
|
scr_http_res_release(conn->res);
|
|
conn->res = NULL;
|
|
}
|
|
}
|
|
|
|
/* The response finished: with keep-alive the parser waits for the next
|
|
* request (pipelined bytes may already sit in the buffer — the read path
|
|
* keeps parsing); without it the socket half-closes, Node's teardown. A
|
|
* request nothing consumed is dumped first. While its body is still
|
|
* arriving the exchange stays attached, and scr_http_conn_body_done ends
|
|
* the request when the body completes. */
|
|
static void scr_http_conn_response_finished(ScrHttpConn *conn, bool keep_alive) {
|
|
ScrHttpReq *r = conn->req;
|
|
bool close = !keep_alive || conn->close_after;
|
|
if (r != NULL && !r->ended && r->gated && !r->flowing) scr_http_req_dump(r);
|
|
if (r != NULL && !r->ended && conn->state != SCR_HTTP_HEAD) {
|
|
/* The rest of the body may already sit in the buffer: inside the pump
|
|
* the response's 'close' waits until the pump stops, so a body that
|
|
* completes there orders its 'end' first. */
|
|
if (conn->pumping) conn->res_close_deferred = true;
|
|
else scr_http_queue_res_close(conn->res);
|
|
} else if (r != NULL && !r->ended) {
|
|
/* the body is complete (a request without one completes after the
|
|
* 'request' listeners return) */
|
|
scr_http_conn_end_after_response(conn);
|
|
} else {
|
|
scr_http_conn_drop_request(conn, false);
|
|
}
|
|
if (close) {
|
|
scr_net_sock_end(conn->sock);
|
|
} else {
|
|
/* Node's resOnFinish: an open connection idles for keepAliveTimeout +
|
|
* keepAliveTimeoutBuffer, then the socket times out and is destroyed
|
|
* unless a 'timeout' listener handles it. */
|
|
ScrNetServer *srv = scr_net_sock_server(conn->sock);
|
|
double ms = srv != NULL ? scr_net_server_keep_alive_ms(srv) : 0;
|
|
if (ms > 0) {
|
|
scr_net_sock_set_timeout(conn->sock, ms);
|
|
conn->keep_alive_timer = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* One parsed header line (name lowercased into the req). */
|
|
static void scr_http_req_add_header(ScrHttpReq *r, const char *name, size_t nlen,
|
|
const char *value, size_t vlen) {
|
|
size_t n = r->nheaders;
|
|
/* Geometric growth with the capacity implied by the count (4, 8, 16,
|
|
* ...): this is the arrays' only appender. */
|
|
if (n == 0 || (n >= 4 && (n & (n - 1)) == 0)) {
|
|
size_t cap = n == 0 ? 4 : n * 2;
|
|
r->hnames = realloc(r->hnames, cap * sizeof *r->hnames);
|
|
r->hnames_raw = realloc(r->hnames_raw, cap * sizeof *r->hnames_raw);
|
|
r->hvalues = realloc(r->hvalues, cap * sizeof *r->hvalues);
|
|
if (!r->hnames || !r->hnames_raw || !r->hvalues) scr_http_oom();
|
|
}
|
|
ScrStr *raw = scr_str_new(name, nlen);
|
|
size_t first_upper = 0;
|
|
while (first_upper < nlen && !(name[first_upper] >= 'A' && name[first_upper] <= 'Z')) first_upper++;
|
|
ScrStr *lower;
|
|
if (first_upper == nlen) {
|
|
lower = scr_str_retain(raw); /* already lowercase: one immutable string serves both */
|
|
} else {
|
|
char small[128];
|
|
char *buf = nlen <= sizeof small ? small : malloc(nlen);
|
|
if (!buf) scr_http_oom();
|
|
for (size_t i = 0; i < nlen; i++) buf[i] = (char)tolower((unsigned char)name[i]);
|
|
lower = scr_str_new(buf, nlen);
|
|
if (buf != small) free(buf);
|
|
}
|
|
r->hnames[n] = lower;
|
|
r->hnames_raw[n] = raw;
|
|
r->hvalues[n] = scr_str_new(value, vlen);
|
|
r->nheaders = n + 1;
|
|
}
|
|
|
|
static void scr_http_req_add_trailer(ScrHttpReq *r, const char *name, size_t nlen,
|
|
const char *value, size_t vlen) {
|
|
char *lower = malloc(nlen);
|
|
if (!lower && nlen > 0) scr_http_oom();
|
|
for (size_t i = 0; i < nlen; i++) lower[i] = (char)tolower((unsigned char)name[i]);
|
|
size_t n = r->ntrailers + 1;
|
|
r->tnames = realloc(r->tnames, n * sizeof *r->tnames);
|
|
r->tnames_raw = realloc(r->tnames_raw, n * sizeof *r->tnames_raw);
|
|
r->tvalues = realloc(r->tvalues, n * sizeof *r->tvalues);
|
|
if (!r->tnames || !r->tnames_raw || !r->tvalues) scr_http_oom();
|
|
r->tnames[r->ntrailers] = scr_str_new(lower, nlen);
|
|
r->tnames_raw[r->ntrailers] = scr_str_new(name, nlen);
|
|
r->tvalues[r->ntrailers] = scr_str_new(value, vlen);
|
|
r->ntrailers = n;
|
|
free(lower);
|
|
}
|
|
|
|
/* Parser errors answer Node's fixed responses and close: 400 for
|
|
* malformed input, 431 when a head or trailer block exceeds the field or
|
|
* byte bound. Node's lenient spots (bare LF line endings, obsolete
|
|
* folding) are NOT accepted; SEMANTICS.md states the bound. */
|
|
static void scr_http_conn_reject(ScrHttpConn *conn, const char *response, size_t len) {
|
|
scr_net_sock_write_native(conn->sock, response, len);
|
|
scr_net_sock_end(conn->sock);
|
|
conn->len = 0;
|
|
scr_http_conn_drop_request(conn, false);
|
|
}
|
|
|
|
static void scr_http_conn_bad_request(ScrHttpConn *conn) {
|
|
static const char bad[] = "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n";
|
|
scr_http_conn_reject(conn, bad, sizeof bad - 1);
|
|
}
|
|
|
|
static void scr_http_conn_header_overflow(ScrHttpConn *conn) {
|
|
static const char overflow[] =
|
|
"HTTP/1.1 431 Request Header Fields Too Large\r\nConnection: close\r\n\r\n";
|
|
scr_http_conn_reject(conn, overflow, sizeof overflow - 1);
|
|
}
|
|
|
|
/* Node answers a missing HTTP/1.1 Host through ServerResponse rather than
|
|
* the parser-error path: the 400 is chunked and includes Date. */
|
|
static void scr_http_conn_missing_host(ScrHttpConn *conn) {
|
|
ScrHttpBuf b = {NULL, 0, 0};
|
|
scr_http_buf_str(&b, "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n");
|
|
scr_http_buf_date(&b);
|
|
scr_http_buf_str(&b, "Transfer-Encoding: chunked\r\n\r\n0\r\n\r\n");
|
|
scr_net_sock_write_native(conn->sock, b.data, b.len);
|
|
free(b.data);
|
|
scr_net_sock_end(conn->sock);
|
|
conn->len = 0;
|
|
scr_http_conn_drop_request(conn, false);
|
|
}
|
|
|
|
/* Delivers a body slice to a request's data listeners (the parser's
|
|
* feed, and the h2 compat DATA-frame feed). */
|
|
static void scr_http_req_deliver(ScrHttpReq *r, const char *data, size_t n) {
|
|
if (!r || n == 0 || r->ended) return;
|
|
r->body_seen = true;
|
|
if (scr_http_req_holding(r)) {
|
|
/* held: delivery waits for the drain; a full buffer stops the
|
|
* socket's reads */
|
|
if (r->pend_len + n > r->pend_cap) {
|
|
size_t cap = r->pend_cap ? r->pend_cap : 4096;
|
|
while (cap < r->pend_len + n) cap *= 2;
|
|
r->pend = realloc(r->pend, cap);
|
|
if (!r->pend) scr_http_oom();
|
|
r->pend_cap = cap;
|
|
}
|
|
memcpy(r->pend + r->pend_len, data, n);
|
|
r->pend_len += n;
|
|
scr_http_req_update_hold(r);
|
|
return;
|
|
}
|
|
bool piped = r->pipe_res != NULL || r->pipe_client != NULL || r->pipe_sock != NULL;
|
|
if (r->data_ls.n == 0 && !piped) return;
|
|
ScrBytes *chunk = scr_bytes_new(SCR_BYTES_U8, (double)n);
|
|
chunk->is_buffer = true; /* Node delivers a Buffer */
|
|
memcpy(chunk->data, data, n);
|
|
if (r->data_ls.n > 0) {
|
|
ScrNetL *snap;
|
|
size_t nl = scr_net_ls_snapshot(&r->data_ls, &snap);
|
|
scr_dyn_this_push(r, SCR_DYNH_HTTP_REQ);
|
|
scr_dyn_chunk_enc(r->enc_utf8);
|
|
for (size_t i = 0; i < nl; i++) {
|
|
if (!scr_exc_pending()) ((ScrNetDataFn)snap[i].fn)(snap[i].cb, chunk);
|
|
scr_closure_release(snap[i].cb);
|
|
}
|
|
scr_dyn_chunk_enc(false);
|
|
scr_dyn_this_pop();
|
|
free(snap);
|
|
}
|
|
if (!scr_exc_pending()) {
|
|
if (r->pipe_res) scr_http_res_write_bytes(r->pipe_res, chunk);
|
|
if (r->pipe_client) scr_http_client_write_bytes(r->pipe_client, chunk);
|
|
if (r->pipe_sock) scr_net_sock_write_bytes(r->pipe_sock, chunk);
|
|
}
|
|
scr_bytes_release(chunk);
|
|
}
|
|
|
|
static void scr_http_conn_body_data(ScrHttpConn *conn, const char *data, size_t n) {
|
|
scr_http_req_deliver(conn->req, data, n);
|
|
}
|
|
|
|
static void scr_http_client_response_done(ScrHttpConn *conn);
|
|
|
|
/* Body complete: 'end' fires on the request; the response may already be
|
|
* finished (handler answered from the head alone) — then the connection
|
|
* resets here instead. Client mode: 'end' on the response, then the
|
|
* whole exchange tears down (one request per connection). */
|
|
static void scr_http_conn_body_done(ScrHttpConn *conn) {
|
|
conn->state = SCR_HTTP_HEAD;
|
|
conn->body_remaining = 0;
|
|
if (conn->client_mode) {
|
|
scr_http_client_response_done(conn);
|
|
return;
|
|
}
|
|
ScrHttpReq *r = conn->req;
|
|
if (r && !r->ended && conn->res && conn->res->finished) {
|
|
/* the response finished while the body was arriving */
|
|
scr_http_conn_end_after_response(conn);
|
|
return;
|
|
}
|
|
if (r) {
|
|
r->received = true;
|
|
scr_http_req_retain(r);
|
|
scr_http_req_finish(r, true);
|
|
scr_http_req_release(r);
|
|
}
|
|
if (conn->res && conn->res->finished) {
|
|
bool keep = conn->res->keep_alive && !conn->close_after;
|
|
scr_http_conn_drop_request(conn, false);
|
|
if (!keep) scr_net_sock_end(conn->sock);
|
|
}
|
|
}
|
|
|
|
/* Node's continueExpression, /(?:^|\W)100-continue(?:$|\W)/i. */
|
|
static bool scr_http_expects_continue(const ScrStr *value) {
|
|
static const char token[] = "100-continue";
|
|
size_t n = sizeof token - 1;
|
|
for (size_t i = 0; i + n <= value->len; i++) {
|
|
bool before = i == 0 || !(isalnum((unsigned char)value->data[i - 1]) || value->data[i - 1] == '_');
|
|
size_t end = i + n;
|
|
bool after = end == value->len || !(isalnum((unsigned char)value->data[end]) || value->data[end] == '_');
|
|
bool match = before && after;
|
|
for (size_t k = 0; k < n && match; k++) match = tolower((unsigned char)value->data[i + k]) == token[k];
|
|
if (match) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* Parses one complete request HEAD sitting at buf[0..head_len). Returns
|
|
* false on malformed input, with *overflow set when the head carries more
|
|
* fields than the parser accepts. */
|
|
static bool scr_http_conn_parse_head(ScrHttpConn *conn, size_t head_len, bool *overflow) {
|
|
const char *buf = conn->buf;
|
|
const char *line_end = memchr(buf, '\r', head_len);
|
|
if (!line_end || line_end + 1 >= buf + head_len || line_end[1] != '\n') return false;
|
|
/* METHOD SP request-target SP HTTP/1.x */
|
|
const char *sp1 = memchr(buf, ' ', (size_t)(line_end - buf));
|
|
if (!sp1) return false;
|
|
const char *sp2 = memchr(sp1 + 1, ' ', (size_t)(line_end - sp1 - 1));
|
|
if (!sp2) return false;
|
|
const char *ver = sp2 + 1;
|
|
size_t verlen = (size_t)(line_end - ver);
|
|
bool http10;
|
|
if (verlen == 8 && memcmp(ver, "HTTP/1.1", 8) == 0) http10 = false;
|
|
else if (verlen == 8 && memcmp(ver, "HTTP/1.0", 8) == 0) http10 = true;
|
|
else return false;
|
|
|
|
ScrHttpReq *req = calloc(1, sizeof *req);
|
|
if (!req) scr_http_oom();
|
|
req->rc = 1;
|
|
req->als = scr_als_capture();
|
|
req->status = -1; /* Node: statusCode is undefined on server requests */
|
|
req->gated = true;
|
|
req->join_dup = conn->srv != NULL && conn->srv->join_dup;
|
|
req->sock = scr_net_sock_retain(conn->sock);
|
|
#ifdef SCR_RC_AUDIT
|
|
scr_http_live++;
|
|
#endif
|
|
req->method = scr_str_new(buf, (size_t)(sp1 - buf));
|
|
req->url = scr_str_new(sp1 + 1, (size_t)(sp2 - sp1 - 1));
|
|
req->http10 = http10;
|
|
|
|
/* header lines until the blank line */
|
|
const char *p = line_end + 2;
|
|
const char *end = buf + head_len;
|
|
bool ok = true;
|
|
while (p < end) {
|
|
const char *eol = memchr(p, '\r', (size_t)(end - p));
|
|
if (!eol || eol + 1 >= end || eol[1] != '\n') {
|
|
ok = false;
|
|
break;
|
|
}
|
|
if (eol == p) {
|
|
p += 2; /* the blank line */
|
|
break;
|
|
}
|
|
const char *colon = memchr(p, ':', (size_t)(eol - p));
|
|
if (!colon || colon == p) {
|
|
ok = false;
|
|
break;
|
|
}
|
|
const char *v = colon + 1;
|
|
while (v < eol && (*v == ' ' || *v == '\t')) v++;
|
|
const char *ve = eol;
|
|
while (ve > v && (ve[-1] == ' ' || ve[-1] == '\t')) ve--;
|
|
scr_http_req_add_header(req, p, (size_t)(colon - p), v, (size_t)(ve - v));
|
|
p = eol + 2;
|
|
}
|
|
size_t header_size = req->url->len;
|
|
for (size_t i = 0; i < req->nheaders; i++) header_size += req->hnames[i]->len + req->hvalues[i]->len;
|
|
if (!ok || req->nheaders > SCR_HTTP_MAX_HEADER_FIELDS || header_size >= scr_http_conn_header_limit(conn)) {
|
|
*overflow = ok;
|
|
scr_http_req_release(req);
|
|
return false;
|
|
}
|
|
conn->head_bytes = head_len;
|
|
conn->trailer_bytes = 0;
|
|
conn->trailer_fields = 0;
|
|
|
|
/* framing: chunked wins over Content-Length (RFC 9112) */
|
|
bool chunked = false;
|
|
long long content_length = 0;
|
|
bool req_keep_alive = !http10;
|
|
bool has_upgrade_hdr = false;
|
|
bool conn_upgrade_token = false;
|
|
for (size_t i = 0; i < req->nheaders; i++) {
|
|
const ScrStr *n = req->hnames[i];
|
|
const ScrStr *v = req->hvalues[i];
|
|
if (n->len == 17 && memcmp(n->data, "transfer-encoding", 17) == 0) {
|
|
if (v->len >= 7 && strstr(v->data, "chunked") != NULL) chunked = true;
|
|
} else if (n->len == 14 && memcmp(n->data, "content-length", 14) == 0) {
|
|
content_length = atoll(v->data);
|
|
if (content_length < 0) content_length = 0;
|
|
} else if (n->len == 7 && memcmp(n->data, "upgrade", 7) == 0) {
|
|
has_upgrade_hdr = true;
|
|
} else if (n->len == 10 && memcmp(n->data, "connection", 10) == 0) {
|
|
/* token scan, case-insensitive enough for close/keep-alive */
|
|
if (strcasestr(v->data, "upgrade") != NULL) conn_upgrade_token = true;
|
|
if (strcasestr(v->data, "close") != NULL) req_keep_alive = false;
|
|
else if (strcasestr(v->data, "keep-alive") != NULL) req_keep_alive = true;
|
|
}
|
|
}
|
|
|
|
/* HTTP/1.1 requires Host (requireHostHeader). CONNECT requests, and
|
|
* upgrade requests an 'upgrade' listener takes, leave the parser before
|
|
* Node's check. */
|
|
bool is_connect = req->method->len == 7 && memcmp(req->method->data, "CONNECT", 7) == 0;
|
|
bool takes_upgrade = has_upgrade_hdr && conn_upgrade_token && conn->srv->upgrade_ls.n > 0;
|
|
if (!http10 && conn->srv->require_host_header && !is_connect && !takes_upgrade) {
|
|
bool has_host = false;
|
|
for (size_t i = 0; i < req->nheaders; i++) {
|
|
const ScrStr *name = req->hnames[i];
|
|
if (name->len == 4 && memcmp(name->data, "host", 4) == 0) {
|
|
has_host = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!has_host) {
|
|
scr_http_req_release(req);
|
|
scr_http_conn_missing_host(conn);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/* A CONNECT request: 'connect' fires INSTEAD of 'request' — the
|
|
* upgrade machinery's exact shape ((req, socket, head); the parser
|
|
* steps aside; no listener destroys the socket, Node's default). The
|
|
* h2 compat server's HTTP/1.1 arm lands here too; its HTTP/2 arm is
|
|
* dispatched separately with the compatibility response handle. */
|
|
if (is_connect) {
|
|
memmove(conn->buf, conn->buf + head_len, conn->len - head_len);
|
|
conn->len -= head_len;
|
|
if (!scr_net_ls_has_live(&conn->srv->connect_ls)) {
|
|
scr_http_req_release(req);
|
|
conn->len = 0;
|
|
scr_net_sock_destroy(conn->sock);
|
|
return true;
|
|
}
|
|
ScrBytes *head = scr_bytes_new(SCR_BYTES_U8, (double)conn->len);
|
|
head->is_buffer = true; /* the upgrade head is a Buffer */
|
|
if (conn->len > 0) memcpy(head->data, conn->buf, conn->len);
|
|
conn->len = 0;
|
|
scr_net_sock_clear_native_reader(conn->sock);
|
|
ScrNetL *snap;
|
|
size_t n = scr_net_ls_snapshot(&conn->srv->connect_ls, &snap);
|
|
scr_dyn_this_push(scr_net_sock_server(conn->sock), SCR_DYNH_NET_SERVER);
|
|
for (size_t i = 0; i < n; i++) {
|
|
if (!scr_exc_pending()) {
|
|
((ScrHttpUpgradeFn)snap[i].fn)(snap[i].cb, scr_http_req_retain(req),
|
|
scr_net_sock_retain(conn->sock), scr_bytes_retain(head));
|
|
}
|
|
scr_closure_release(snap[i].cb);
|
|
}
|
|
scr_dyn_this_pop();
|
|
free(snap);
|
|
scr_bytes_release(head);
|
|
scr_http_req_release(req);
|
|
return true;
|
|
}
|
|
|
|
/* An Upgrade request (Connection: upgrade + an Upgrade header): the
|
|
* 'upgrade' listeners take the socket RAW — the parser steps aside
|
|
* (fn pointers clear; the conn ctx stays until the socket dies), bytes
|
|
* already read past the head travel as `head`, and no 'request' fires.
|
|
* With no listener Node destroys the socket; so does this slice. */
|
|
if (has_upgrade_hdr && conn_upgrade_token) {
|
|
memmove(conn->buf, conn->buf + head_len, conn->len - head_len);
|
|
conn->len -= head_len;
|
|
if (conn->srv->upgrade_ls.n == 0) {
|
|
scr_http_req_release(req);
|
|
conn->len = 0;
|
|
scr_net_sock_destroy(conn->sock);
|
|
return true;
|
|
}
|
|
ScrBytes *head = scr_bytes_new(SCR_BYTES_U8, (double)conn->len);
|
|
head->is_buffer = true; /* the upgrade head is a Buffer */
|
|
if (conn->len > 0) memcpy(head->data, conn->buf, conn->len);
|
|
conn->len = 0;
|
|
scr_net_sock_clear_native_reader(conn->sock);
|
|
ScrNetL *snap;
|
|
size_t n = scr_net_ls_snapshot(&conn->srv->upgrade_ls, &snap);
|
|
scr_dyn_this_push(scr_net_sock_server(conn->sock), SCR_DYNH_NET_SERVER);
|
|
for (size_t i = 0; i < n; i++) {
|
|
if (!scr_exc_pending()) {
|
|
((ScrHttpUpgradeFn)snap[i].fn)(snap[i].cb, scr_http_req_retain(req),
|
|
scr_net_sock_retain(conn->sock), scr_bytes_retain(head));
|
|
}
|
|
scr_closure_release(snap[i].cb);
|
|
}
|
|
scr_dyn_this_pop();
|
|
free(snap);
|
|
scr_bytes_release(head);
|
|
scr_http_req_release(req);
|
|
return true;
|
|
}
|
|
|
|
ScrHttpRes *res = calloc(1, sizeof *res);
|
|
if (!res) scr_http_oom();
|
|
res->rc = 1;
|
|
res->als = scr_als_capture();
|
|
#ifdef SCR_RC_AUDIT
|
|
scr_http_live++;
|
|
#endif
|
|
res->sock = scr_net_sock_retain(conn->sock);
|
|
res->conn = conn;
|
|
res->keep_alive = req_keep_alive;
|
|
res->req_ref = scr_http_req_retain(req); /* res.req — req never points back */
|
|
conn->req = req; /* conn's +1 */
|
|
conn->res = res;
|
|
conn->close_after = !req_keep_alive;
|
|
|
|
if (chunked) {
|
|
conn->state = SCR_HTTP_CHUNK_SIZE;
|
|
} else if (content_length > 0) {
|
|
conn->state = SCR_HTTP_BODY_CL;
|
|
conn->body_remaining = (size_t)content_length;
|
|
} else {
|
|
conn->state = SCR_HTTP_HEAD; /* no body */
|
|
}
|
|
|
|
/* consume the head bytes */
|
|
memmove(conn->buf, conn->buf + head_len, conn->len - head_len);
|
|
conn->len -= head_len;
|
|
|
|
/* Expect on an HTTP/1.1 request, as Node's server handles it: with a
|
|
* 'checkContinue' listener, 100-continue goes to it instead of
|
|
* 'request' (the handler calls writeContinue()); without one the server
|
|
* writes "100 Continue" and emits 'request'. Any other expectation goes
|
|
* to 'checkExpectation', or answers 417 without a 'request'. */
|
|
ScrNetLs *handlers = &conn->srv->request_ls;
|
|
const ScrStr *expect = NULL;
|
|
for (size_t i = 0; i < req->nheaders && !http10; i++) {
|
|
if (req->hnames[i]->len == 6 && memcmp(req->hnames[i]->data, "expect", 6) == 0) {
|
|
expect = req->hvalues[i];
|
|
break;
|
|
}
|
|
}
|
|
if (expect != NULL) {
|
|
if (scr_http_expects_continue(expect)) {
|
|
if (scr_net_ls_has_live(&conn->srv->check_continue_ls)) handlers = &conn->srv->check_continue_ls;
|
|
else scr_http_res_write_continue(res);
|
|
} else if (scr_net_ls_has_live(&conn->srv->check_expectation_ls)) {
|
|
handlers = &conn->srv->check_expectation_ls;
|
|
} else {
|
|
scr_http_res_write_head(res, 417);
|
|
scr_http_res_end(res);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/* fire the 'request' listeners (macrotask, main stack) — snapshot at
|
|
* emit time, the scr_net_fire0 discipline; each listener gets its own
|
|
* req/res refs (the adapters release what their shape drops) */
|
|
{
|
|
ScrNetL *snap;
|
|
size_t n = scr_net_ls_snapshot(handlers, &snap);
|
|
scr_dyn_this_push(scr_net_sock_server(conn->sock), SCR_DYNH_NET_SERVER);
|
|
for (size_t i = 0; i < n; i++) {
|
|
if (!scr_exc_pending()) {
|
|
((ScrHttpReqFn)snap[i].fn)(snap[i].cb, scr_http_req_retain(req), scr_http_res_retain(res));
|
|
}
|
|
scr_closure_release(snap[i].cb);
|
|
}
|
|
scr_dyn_this_pop();
|
|
free(snap);
|
|
}
|
|
if (scr_exc_pending()) return true; /* the loop surfaces it */
|
|
if (!chunked && content_length == 0) {
|
|
/* headless body: 'end' delivers to the listeners the handler attached */
|
|
scr_http_conn_body_done(conn);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* The parse pump: consume as much of conn->buf as the state machine can. */
|
|
static void scr_http_conn_pump(ScrHttpConn *conn) {
|
|
for (;;) {
|
|
if (scr_exc_pending()) return;
|
|
if (conn->state == SCR_HTTP_HEAD) {
|
|
if (conn->client_mode) {
|
|
if (conn->client == NULL || conn->client_resp_started) {
|
|
/* response already delivered (or torn down): stray bytes after
|
|
* the exchange are discarded — the socket is closing anyway */
|
|
conn->len = 0;
|
|
return;
|
|
}
|
|
} else if (conn->req != NULL && conn->res == NULL) {
|
|
return; /* torn down */
|
|
}
|
|
/* between requests, or awaiting a head */
|
|
if (conn->len == 0) return;
|
|
/* find CRLFCRLF */
|
|
char *hit = NULL;
|
|
if (conn->len >= 4) {
|
|
for (size_t i = 0; i + 3 < conn->len; i++) {
|
|
if (conn->buf[i] == '\r' && conn->buf[i + 1] == '\n' && conn->buf[i + 2] == '\r' &&
|
|
conn->buf[i + 3] == '\n') {
|
|
hit = conn->buf + i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!hit) {
|
|
if (conn->len > scr_http_conn_raw_limit(conn)) {
|
|
conn->header_overflow = true;
|
|
if (conn->client_mode) scr_http_client_head_overflow(conn);
|
|
else scr_http_conn_header_overflow(conn);
|
|
}
|
|
return;
|
|
}
|
|
size_t head_len = (size_t)(hit - conn->buf) + 4;
|
|
if (head_len > scr_http_conn_raw_limit(conn)) {
|
|
conn->header_overflow = true;
|
|
if (conn->client_mode) scr_http_client_head_overflow(conn);
|
|
else scr_http_conn_header_overflow(conn);
|
|
return;
|
|
}
|
|
if (conn->client_mode) {
|
|
if (!scr_http_client_parse_head(conn, head_len)) {
|
|
scr_http_client_head_overflow(conn); /* malformed: hang up */
|
|
}
|
|
if (scr_exc_pending()) return;
|
|
continue;
|
|
}
|
|
/* a previous exchange must be COMPLETE before the next parses: the
|
|
* response may still be streaming (keep-alive pipelining waits) */
|
|
if (conn->res != NULL && !conn->res->finished) return;
|
|
if (conn->res != NULL) scr_http_conn_drop_request(conn, false);
|
|
bool overflow = false;
|
|
if (!scr_http_conn_parse_head(conn, head_len, &overflow)) {
|
|
if (overflow) scr_http_conn_header_overflow(conn);
|
|
else scr_http_conn_bad_request(conn);
|
|
return;
|
|
}
|
|
continue;
|
|
}
|
|
if (conn->state == SCR_HTTP_BODY_CL) {
|
|
if (conn->len == 0) return;
|
|
size_t take = conn->len < conn->body_remaining ? conn->len : conn->body_remaining;
|
|
scr_http_conn_body_data(conn, conn->buf, take);
|
|
memmove(conn->buf, conn->buf + take, conn->len - take);
|
|
conn->len -= take;
|
|
conn->body_remaining -= take;
|
|
if (conn->body_remaining == 0) scr_http_conn_body_done(conn);
|
|
else return;
|
|
continue;
|
|
}
|
|
if (conn->state == SCR_HTTP_CHUNK_SIZE) {
|
|
char *eol = NULL;
|
|
for (size_t i = 0; i + 1 < conn->len; i++) {
|
|
if (conn->buf[i] == '\r' && conn->buf[i + 1] == '\n') {
|
|
eol = conn->buf + i;
|
|
break;
|
|
}
|
|
}
|
|
if (!eol) return;
|
|
size_t size = 0;
|
|
bool any = false;
|
|
for (char *p = conn->buf; p < eol; p++) {
|
|
char c = *p;
|
|
if (c == ';') break; /* chunk extensions: ignored */
|
|
int digit;
|
|
if (c >= '0' && c <= '9') digit = c - '0';
|
|
else if (c >= 'a' && c <= 'f') digit = c - 'a' + 10;
|
|
else if (c >= 'A' && c <= 'F') digit = c - 'A' + 10;
|
|
else {
|
|
scr_http_conn_bad_request(conn);
|
|
return;
|
|
}
|
|
size = size * 16 + (size_t)digit;
|
|
any = true;
|
|
}
|
|
if (!any) {
|
|
scr_http_conn_bad_request(conn);
|
|
return;
|
|
}
|
|
size_t consumed = (size_t)(eol - conn->buf) + 2;
|
|
memmove(conn->buf, conn->buf + consumed, conn->len - consumed);
|
|
conn->len -= consumed;
|
|
if (size == 0) {
|
|
conn->state = SCR_HTTP_CHUNK_TRAILER;
|
|
} else {
|
|
conn->state = SCR_HTTP_CHUNK_DATA;
|
|
conn->body_remaining = size;
|
|
}
|
|
continue;
|
|
}
|
|
if (conn->state == SCR_HTTP_CHUNK_DATA) {
|
|
if (conn->len == 0) return;
|
|
size_t take = conn->len < conn->body_remaining ? conn->len : conn->body_remaining;
|
|
scr_http_conn_body_data(conn, conn->buf, take);
|
|
memmove(conn->buf, conn->buf + take, conn->len - take);
|
|
conn->len -= take;
|
|
conn->body_remaining -= take;
|
|
if (conn->body_remaining == 0) conn->state = SCR_HTTP_CHUNK_CRLF;
|
|
else return;
|
|
continue;
|
|
}
|
|
if (conn->state == SCR_HTTP_CHUNK_CRLF) {
|
|
if (conn->len < 2) return;
|
|
if (conn->buf[0] != '\r' || conn->buf[1] != '\n') {
|
|
scr_http_conn_bad_request(conn);
|
|
return;
|
|
}
|
|
memmove(conn->buf, conn->buf + 2, conn->len - 2);
|
|
conn->len -= 2;
|
|
conn->state = SCR_HTTP_CHUNK_SIZE;
|
|
continue;
|
|
}
|
|
if (conn->state == SCR_HTTP_BODY_EOF) {
|
|
/* RESPONSE mode, no framing header: every arrived byte is body;
|
|
* EOF completes it (the eof hook calls body_done). */
|
|
if (conn->len == 0) return;
|
|
scr_http_conn_body_data(conn, conn->buf, conn->len);
|
|
conn->len = 0;
|
|
return;
|
|
}
|
|
if (conn->state == SCR_HTTP_CHUNK_TRAILER) {
|
|
/* One complete field per pass; preserve wire case/order and keep
|
|
* trailers separate from the head until 'end' fires. */
|
|
if (conn->len < 2) return;
|
|
if (conn->buf[0] == '\r' && conn->buf[1] == '\n') {
|
|
if (!scr_http_conn_trailer_fits(conn, 2, false)) {
|
|
conn->header_overflow = true;
|
|
if (conn->client_mode) scr_http_client_head_overflow(conn);
|
|
else scr_http_conn_header_overflow(conn);
|
|
return;
|
|
}
|
|
memmove(conn->buf, conn->buf + 2, conn->len - 2);
|
|
conn->len -= 2;
|
|
conn->trailer_bytes += 2;
|
|
scr_http_conn_body_done(conn);
|
|
continue;
|
|
}
|
|
char *eol = NULL;
|
|
for (size_t i = 0; i + 1 < conn->len; i++) {
|
|
if (conn->buf[i] == '\r' && conn->buf[i + 1] == '\n') {
|
|
eol = conn->buf + i;
|
|
break;
|
|
}
|
|
}
|
|
if (!eol) {
|
|
if (!scr_http_conn_trailer_fits(conn, conn->len, false)) {
|
|
conn->header_overflow = true;
|
|
if (conn->client_mode) scr_http_client_head_overflow(conn);
|
|
else scr_http_conn_header_overflow(conn);
|
|
}
|
|
return;
|
|
}
|
|
char *colon = memchr(conn->buf, ':', (size_t)(eol - conn->buf));
|
|
bool valid = colon != NULL && colon != conn->buf;
|
|
for (char *p = conn->buf; valid && p < colon; p++) valid = scr_http_token_char((unsigned char)*p);
|
|
if (!valid) {
|
|
if (conn->client_mode) scr_http_client_head_overflow(conn);
|
|
else scr_http_conn_bad_request(conn);
|
|
return;
|
|
}
|
|
char *value = colon + 1;
|
|
while (value < eol && (*value == ' ' || *value == '\t')) value++;
|
|
char *end = eol;
|
|
while (end > value && (end[-1] == ' ' || end[-1] == '\t')) end--;
|
|
for (char *p = value; valid && p < end; p++) {
|
|
unsigned char ch = (unsigned char)*p;
|
|
valid = (ch >= 32 || ch == '\t') && ch != 127;
|
|
}
|
|
if (!valid) {
|
|
if (conn->client_mode) scr_http_client_head_overflow(conn);
|
|
else scr_http_conn_bad_request(conn);
|
|
return;
|
|
}
|
|
size_t consumed = (size_t)(eol - conn->buf) + 2;
|
|
if (!scr_http_conn_trailer_fits(conn, consumed, true)) {
|
|
conn->header_overflow = true;
|
|
if (conn->client_mode) scr_http_client_head_overflow(conn);
|
|
else scr_http_conn_header_overflow(conn);
|
|
return;
|
|
}
|
|
/* An early server response releases the request but deliberately leaves
|
|
* this parser in its chunk states so the wire remains validated. */
|
|
if (conn->req) {
|
|
scr_http_req_add_trailer(conn->req, conn->buf, (size_t)(colon - conn->buf),
|
|
value, (size_t)(end - value));
|
|
}
|
|
memmove(conn->buf, conn->buf + consumed, conn->len - consumed);
|
|
conn->len -= consumed;
|
|
conn->trailer_bytes += consumed;
|
|
conn->trailer_fields++;
|
|
continue;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* ── the native socket hooks ─────────────────────────────────────────── */
|
|
|
|
static void scr_http_conn_data(void *ctx, const char *buf, size_t n) {
|
|
ScrHttpConn *conn = (ScrHttpConn *)ctx;
|
|
if (conn->pool_agent != NULL) {
|
|
/* Data on an idle pooled socket: the agent destroys the socket, as
|
|
* Node's free-socket read guard does, so the bytes are never read as
|
|
* the next request's response. */
|
|
scr_net_sock_destroy(conn->sock);
|
|
return;
|
|
}
|
|
if (conn->len + n > conn->cap) {
|
|
size_t cap = conn->cap ? conn->cap : 8192;
|
|
while (cap < conn->len + n) cap *= 2;
|
|
conn->buf = realloc(conn->buf, cap);
|
|
if (!conn->buf) scr_http_oom();
|
|
conn->cap = cap;
|
|
}
|
|
memcpy(conn->buf + conn->len, buf, n);
|
|
conn->len += n;
|
|
if (conn->keep_alive_timer) {
|
|
/* the next request restores server.timeout (Node's onParserExecute) */
|
|
conn->keep_alive_timer = false;
|
|
ScrNetServer *srv = scr_net_sock_server(conn->sock);
|
|
scr_net_sock_set_timeout(conn->sock, srv != NULL ? scr_net_server_idle_ms(srv) : 0);
|
|
}
|
|
conn->pumping = true;
|
|
scr_http_conn_pump(conn);
|
|
conn->pumping = false;
|
|
if (conn->res_close_deferred) {
|
|
conn->res_close_deferred = false;
|
|
if (conn->res) scr_http_queue_res_close(conn->res);
|
|
}
|
|
}
|
|
|
|
static void scr_http_client_eof(ScrHttpConn *conn);
|
|
static void scr_http_client_closed(ScrHttpConn *conn);
|
|
static void scr_http_client_conn_free(ScrHttpConn *conn);
|
|
|
|
static void scr_http_conn_eof(void *ctx) {
|
|
ScrHttpConn *conn = (ScrHttpConn *)ctx;
|
|
if (conn->client_mode) {
|
|
scr_http_client_eof(conn);
|
|
return;
|
|
}
|
|
/* client FIN between requests: our half closes too (keep-alive over);
|
|
* mid-request it is an aborted request — everything settles quietly */
|
|
scr_http_conn_drop_request(conn, false);
|
|
conn->len = 0;
|
|
}
|
|
|
|
static void scr_http_conn_closed(void *ctx) {
|
|
ScrHttpConn *conn = (ScrHttpConn *)ctx;
|
|
if (conn->client_mode) {
|
|
scr_http_client_closed(conn);
|
|
return;
|
|
}
|
|
scr_http_conn_drop_request(conn, false);
|
|
}
|
|
|
|
/* Socket errors under a parser: the protocol layer owns the story. On
|
|
* the SERVER an abrupt client death (ECONNRESET mid-request) must never
|
|
* crash the process — Node's http server swallows these into request
|
|
* teardown ('error' listeners on the req get "aborted"); the CLIENT
|
|
* routes them to the request handle's 'error'. Always consumed: the
|
|
* underlying socket's own listeners never see a parser-owned error. */
|
|
static bool scr_http_client_sock_err(ScrHttpConn *conn, ScrStr *msg);
|
|
|
|
static bool scr_http_conn_err(void *ctx, ScrStr *msg) {
|
|
ScrHttpConn *conn = (ScrHttpConn *)ctx;
|
|
if (conn->client_mode) return scr_http_client_sock_err(conn, msg);
|
|
if (conn->req && conn->req->err_ls.n > 0) {
|
|
ScrStr *aborted = scr_str_new("aborted", 7);
|
|
scr_net_fire_err_this(&conn->req->err_ls, aborted, conn->req, SCR_DYNH_HTTP_REQ);
|
|
scr_str_release(aborted);
|
|
}
|
|
scr_http_conn_drop_request(conn, false);
|
|
return true;
|
|
}
|
|
|
|
static void scr_http_conn_free(void *ctx) {
|
|
ScrHttpConn *conn = (ScrHttpConn *)ctx;
|
|
if (conn->client_mode) {
|
|
scr_http_client_conn_free(conn);
|
|
return;
|
|
}
|
|
scr_http_conn_drop_request(conn, false);
|
|
if (conn->srv) scr_http_srv_ctx_release(conn->srv);
|
|
free(conn->buf);
|
|
free(conn);
|
|
}
|
|
|
|
static bool scr_http_conn_is_idle(void *ctx) {
|
|
ScrHttpConn *conn = (ScrHttpConn *)ctx;
|
|
return !conn->client_mode && conn->state == SCR_HTTP_HEAD &&
|
|
conn->req == NULL && conn->len == 0;
|
|
}
|
|
|
|
/* Node's server socketOnTimeout emits on an incomplete request, then on
|
|
* the active response. Listener presence, including a once listener
|
|
* removed by the emit, prevents the server's unhandled auto-destroy. */
|
|
static bool scr_http_conn_server_timeout(void *ctx) {
|
|
ScrHttpConn *conn = (ScrHttpConn *)ctx;
|
|
bool handled = false;
|
|
if (conn->client_mode) return false;
|
|
ScrHttpReq *req = conn->req;
|
|
if (req && !req->ended && !req->destroyed && req->timeout_ls.n > 0) {
|
|
handled = true;
|
|
scr_http_req_retain(req);
|
|
scr_net_fire0_this(&req->timeout_ls, req, SCR_DYNH_HTTP_REQ);
|
|
scr_http_req_release(req);
|
|
}
|
|
if (scr_exc_pending()) return handled;
|
|
ScrHttpRes *res = conn->res;
|
|
if (res && !res->close_emitted && res->timeout_ls.n > 0) {
|
|
handled = true;
|
|
scr_http_res_retain(res);
|
|
scr_net_fire0_this(&res->timeout_ls, res, SCR_DYNH_HTTP_RES);
|
|
scr_http_res_release(res);
|
|
}
|
|
return handled;
|
|
}
|
|
|
|
/* The server's native connection hook: one parser per accepted socket. */
|
|
static void scr_http_on_connection(void *ctx, ScrNetSocket *sock) {
|
|
ScrHttpSrvCtx *srv = (ScrHttpSrvCtx *)ctx;
|
|
ScrHttpConn *conn = calloc(1, sizeof *conn);
|
|
if (!conn) scr_http_oom();
|
|
conn->sock = sock; /* borrowed — see the struct comment */
|
|
conn->srv = scr_http_srv_ctx_retain(srv);
|
|
conn->max_header_size = srv->max_header_size;
|
|
scr_net_sock_set_native_reader(sock, &scr_http_conn_data, &scr_http_conn_eof,
|
|
&scr_http_conn_closed, conn, &scr_http_conn_free);
|
|
scr_net_sock_set_native_idle_checker(sock, &scr_http_conn_is_idle);
|
|
scr_net_sock_set_native_events(sock, NULL, &scr_http_conn_err);
|
|
scr_net_sock_set_native_http_timeout(sock, &scr_http_conn_server_timeout);
|
|
}
|
|
|
|
/* The unguarded h2-only stream call (`req.stream.on(...)`): stream IS
|
|
* undefined on every connection this lowering accepts — and on every
|
|
* HTTP/1.1 connection of Node's own allowHTTP1 server — so calling
|
|
* through it is Node's member read on undefined: the exact catchable
|
|
* TypeError, member name interpolated. Borrows the name; never returns. */
|
|
void scr_http2_stream_undef_call(ScrStr *member) {
|
|
char msg[128];
|
|
int n = snprintf(msg, sizeof msg, "Cannot read properties of undefined (reading '%.*s')",
|
|
(int)member->len, member->data);
|
|
scr_throw_error_msg(SCR_ERR_TYPE, msg, (size_t)n);
|
|
}
|
|
|
|
/* http.createServer(handler): an ordinary net server whose connections
|
|
* feed the parser; listen/close/address/'error' all ride the net surface.
|
|
* A NULL handler (http2.createSecureServer's route) creates the server
|
|
* with an empty 'request' list — server.on("request", ...) fills it. */
|
|
ScrNetServer *scr_http_create_server(ScrClosure *handler /*moves, nullable*/, ScrHttpReqFn fn) {
|
|
scr_http_install();
|
|
ScrNetServer *s = scr_net_create_server(NULL, NULL);
|
|
scr_net_server_enable_http_timeout_surface(s);
|
|
ScrHttpSrvCtx *ctx = calloc(1, sizeof *ctx);
|
|
if (!ctx) scr_http_oom();
|
|
ctx->proto = SCR_NET_PROTO_HTTP1;
|
|
ctx->rc = 1;
|
|
ctx->require_host_header = true;
|
|
if (handler != NULL) scr_net_ls_add(&ctx->request_ls, handler, (void *)fn, false);
|
|
scr_net_server_set_native_conn(s, &scr_http_on_connection, ctx, &scr_http_srv_ctx_free);
|
|
scr_net_server_set_http_ctx(s, ctx);
|
|
scr_net_server_set_proto_settle(s, &scr_http_srv_ctx_settle);
|
|
return s;
|
|
}
|
|
|
|
/* Node's validateInteger(value, 'maxHeaderSize', 0). False with the
|
|
* exception pending. */
|
|
static bool scr_http_max_header_size_valid(double value) {
|
|
bool integer = isfinite(value) && value == floor(value);
|
|
if (integer && value >= 0 && value <= 9007199254740991.0) return true;
|
|
char shown[48];
|
|
scr_num_received(value, shown);
|
|
char msg[192];
|
|
int n = snprintf(msg, sizeof msg,
|
|
integer ? "The value of \"maxHeaderSize\" is out of range. It must be >= 0 && <= 9007199254740991. Received %s"
|
|
: "The value of \"maxHeaderSize\" is out of range. It must be an integer. Received %s",
|
|
shown);
|
|
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)n, "ERR_OUT_OF_RANGE");
|
|
return false;
|
|
}
|
|
|
|
/* createServer({ maxHeaderSize }): the server's header size bound. */
|
|
void scr_http_server_max_header_size(ScrNetServer *s, double value) {
|
|
if (!scr_http_max_header_size_valid(value)) return;
|
|
ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)scr_net_server_get_http_ctx(s);
|
|
if (ctx != NULL && ctx->proto == SCR_NET_PROTO_HTTP1) ctx->max_header_size = (size_t)value;
|
|
}
|
|
|
|
/* createServer({ joinDuplicateHeaders: true, ... }): repeated request
|
|
* header names read back joined with ", " where Node's default (and this
|
|
* parser's) keeps the first occurrence. A no-op on parserless servers. */
|
|
void scr_http_server_join_duplicate_headers(ScrNetServer *s) {
|
|
ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)scr_net_server_get_http_ctx(s);
|
|
if (ctx != NULL && ctx->proto == SCR_NET_PROTO_HTTP1) ctx->join_dup = true;
|
|
}
|
|
|
|
void scr_http_server_allow_missing_host_header(ScrNetServer *s) {
|
|
ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)scr_net_server_get_http_ctx(s);
|
|
if (ctx != NULL && ctx->proto == SCR_NET_PROTO_HTTP1) ctx->require_host_header = false;
|
|
}
|
|
|
|
/* Late 'request' listener installs (server.on/once("request", ...)): the
|
|
* http2 allowHTTP1 server's handler route, and http.Server's 'request'
|
|
* event. On a server with no HTTP parser the closure releases
|
|
* unregistered — 'request' never fires there, like Node. */
|
|
void scr_http_server_on_request(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrHttpReqFn fn,
|
|
bool once) {
|
|
ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)scr_net_server_get_http_ctx(s);
|
|
if (ctx != NULL && ctx->proto != SCR_NET_PROTO_HTTP1) {
|
|
/* an h2 server's ctx: the compat seam — scr_http2.c's hook owns the
|
|
* registration (Http2ServerRequest/Response over h2 streams) */
|
|
if (scr_http_h2_request_hook != NULL && !scr_net_server_settled(s)) {
|
|
scr_http_h2_request_hook(ctx, cb, (void *)fn, once);
|
|
return;
|
|
}
|
|
ctx = NULL;
|
|
}
|
|
if (ctx == NULL || scr_net_server_settled(s)) {
|
|
/* no parser, or the server already settled ('request' can never fire
|
|
* again — an install would only re-arm the cycle the settle broke) */
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_net_ls_add(&ctx->request_ls, cb, (void *)fn, once);
|
|
}
|
|
|
|
/* server.on('checkContinue' | 'checkExpectation', (req, res) => ...). */
|
|
void scr_http_server_on_check(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrHttpReqFn fn, bool once,
|
|
bool expectation) {
|
|
ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)scr_net_server_get_http_ctx(s);
|
|
if (ctx == NULL || ctx->proto != SCR_NET_PROTO_HTTP1 || scr_net_server_settled(s)) {
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_net_ls_add(expectation ? &ctx->check_expectation_ls : &ctx->check_continue_ls, cb, (void *)fn,
|
|
once);
|
|
}
|
|
|
|
/* ── the h2 compat pair (built by scr_http2.c per server stream) ───────
|
|
*
|
|
* The request materializes from the decoded header list (:method/:path
|
|
* feed the line; EVERY pair — pseudo-headers included — lands in
|
|
* req.headers, Node's compat shape); the response routes its write paths
|
|
* through the h2 ops vtable. Body DATA frames and END_STREAM feed the
|
|
* req exactly like the parser's body states; 'close' rides this unit's
|
|
* deferred-emit queue so ordering matches the http/1 story (res before
|
|
* req). */
|
|
|
|
ScrHttpReq *scr_http_h2_req_new(ScrNetSocket *sock /*borrowed, nullable*/,
|
|
void *stream /*borrowed, nullable*/) {
|
|
scr_http_install();
|
|
ScrHttpReq *req = calloc(1, sizeof *req);
|
|
if (!req) scr_http_oom();
|
|
req->rc = 1;
|
|
req->als = scr_als_capture();
|
|
req->status = -1; /* server requests: statusCode is Node's undefined */
|
|
req->http2 = true;
|
|
req->sock = sock ? scr_net_sock_retain(sock) : NULL;
|
|
req->h2_stream = stream != NULL ? scr_http_h2_ops->retain(stream) : NULL;
|
|
req->method = scr_str_new("GET", 3); /* :method replaces it */
|
|
req->url = scr_str_new("/", 1); /* :path replaces it */
|
|
#ifdef SCR_RC_AUDIT
|
|
scr_http_live++;
|
|
#endif
|
|
return req;
|
|
}
|
|
|
|
void *scr_http_req_h2_stream(ScrHttpReq *r) {
|
|
return r->h2_stream != NULL ? scr_http_h2_ops->retain(r->h2_stream) : NULL;
|
|
}
|
|
|
|
void *scr_http_req_h2_stream_or_throw(ScrHttpReq *r, ScrStr *member /*borrowed*/) {
|
|
if (r->h2_stream != NULL) return scr_http_h2_ops->retain(r->h2_stream);
|
|
scr_http2_stream_undef_call(member);
|
|
return NULL;
|
|
}
|
|
|
|
void scr_http_h2_req_line(ScrHttpReq *r, ScrStr *method /*borrowed, nullable*/,
|
|
ScrStr *url /*borrowed, nullable*/) {
|
|
if (method != NULL) {
|
|
scr_str_release(r->method);
|
|
r->method = scr_str_retain(method);
|
|
}
|
|
if (url != NULL) {
|
|
scr_str_release(r->url);
|
|
r->url = scr_str_retain(url);
|
|
}
|
|
}
|
|
|
|
void scr_http_h2_req_header(ScrHttpReq *r, ScrStr *name /*borrowed*/, ScrStr *value /*borrowed*/) {
|
|
scr_http_req_add_header(r, name->data, name->len, value->data, value->len);
|
|
}
|
|
|
|
void scr_http_h2_req_data(ScrHttpReq *r, const char *data, size_t n) {
|
|
scr_http_req_deliver(r, data, n);
|
|
}
|
|
|
|
void scr_http_h2_req_end(ScrHttpReq *r) { scr_http_req_finish(r, true); }
|
|
|
|
/* Typed member reads shared by both parser lanes: the version triple and
|
|
* the compat pair's aborted/complete flags. */
|
|
ScrStr *scr_http_req_http_version(ScrHttpReq *r) {
|
|
return scr_str_new(r->http2 ? "2.0" : r->http10 ? "1.0" : "1.1", 3);
|
|
}
|
|
double scr_http_req_http_version_major(ScrHttpReq *r) { return r->http2 ? 2 : 1; }
|
|
double scr_http_req_http_version_minor(ScrHttpReq *r) { return r->http2 || r->http10 ? 0 : 1; }
|
|
bool scr_http_req_aborted_flag(ScrHttpReq *r) { return r->aborted; }
|
|
bool scr_http_req_complete(ScrHttpReq *r) { return (r->ended || r->received) && !r->aborted; }
|
|
|
|
/* The stream died with the response side open: 'aborted' fires NOW (the
|
|
* teardown macrotask, Node's position), then the close rides the sweep. */
|
|
void scr_http_h2_req_aborted(ScrHttpReq *r) {
|
|
if (r->close_emitted || r->aborted) return;
|
|
r->aborted = true;
|
|
scr_net_fire0_this(&r->aborted_ls, r, SCR_DYNH_HTTP_REQ);
|
|
scr_net_ls_drop(&r->aborted_ls);
|
|
}
|
|
|
|
void scr_http_h2_req_close(ScrHttpReq *r) { scr_http_queue_req_close(r); }
|
|
|
|
ScrHttpRes *scr_http_h2_res_new(ScrNetSocket *sock /*borrowed, nullable*/,
|
|
void *stream /*borrowed*/) {
|
|
scr_http_install();
|
|
ScrHttpRes *res = calloc(1, sizeof *res);
|
|
if (!res) scr_http_oom();
|
|
res->rc = 1;
|
|
res->als = scr_als_capture();
|
|
res->sock = sock ? scr_net_sock_retain(sock) : NULL;
|
|
res->h2_stream = scr_http_h2_ops->retain(stream);
|
|
res->keep_alive = true; /* inert on h2 — no Connection header exists */
|
|
#ifdef SCR_RC_AUDIT
|
|
scr_http_live++;
|
|
#endif
|
|
return res;
|
|
}
|
|
|
|
void scr_http_h2_res_close(ScrHttpRes *r) { scr_http_queue_res_close(r); }
|
|
|
|
bool scr_http_h2_res_finished(ScrHttpRes *r) { return r->finished; }
|
|
|
|
/* server.on("connect", (req, socket, head) => ...) — HTTP CONNECT, and
|
|
* server.on("upgrade", ...) below: the registration twins of on_request.
|
|
* On a server with no HTTP parser neither list ever fires — honest dead
|
|
* weight, Node's shape. */
|
|
void scr_http_server_on_connect(ScrNetServer *s, ScrClosure *cb /*moves*/,
|
|
ScrHttpUpgradeFn h1_fn, ScrHttpConnectH2Fn h2_fn,
|
|
bool once) {
|
|
ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)scr_net_server_get_http_ctx(s);
|
|
if (ctx != NULL && ctx->proto != SCR_NET_PROTO_HTTP1) {
|
|
if (scr_http_h2_connect_hook != NULL && !scr_net_server_settled(s)) {
|
|
scr_http_h2_connect_hook(ctx, cb, (void *)h1_fn, (void *)h2_fn, once);
|
|
return;
|
|
}
|
|
ctx = NULL;
|
|
}
|
|
if (ctx == NULL) {
|
|
/* no HTTP parser on this server: the event can never fire (Node's
|
|
* net server never emits 'connect' either) — honest dead weight */
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_net_ls_add(&ctx->connect_ls, cb, (void *)h1_fn, once);
|
|
}
|
|
|
|
void scr_http_server_on_upgrade(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrHttpUpgradeFn fn,
|
|
bool once) {
|
|
ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)scr_net_server_get_http_ctx(s);
|
|
if (ctx != NULL && ctx->proto != SCR_NET_PROTO_HTTP1) {
|
|
if (scr_http_h2_upgrade_hook != NULL && !scr_net_server_settled(s)) {
|
|
scr_http_h2_upgrade_hook(ctx, cb, (void *)fn, once);
|
|
return;
|
|
}
|
|
ctx = NULL;
|
|
}
|
|
if (ctx == NULL) {
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_net_ls_add(&ctx->upgrade_ls, cb, (void *)fn, once);
|
|
}
|
|
|
|
/* The upgrade adapters: (req, socket, head), and every shorter prefix.
|
|
* All three arrive +1; unused ones release here. */
|
|
void scr_http_upgrade_thunk3(ScrClosure *cb, ScrHttpReq *req, ScrNetSocket *sock, ScrBytes *head) {
|
|
((void (*)(ScrClosure *, ScrHttpReq *, ScrNetSocket *, ScrBytes *))cb->fn)(cb, req, sock, head);
|
|
}
|
|
|
|
void scr_http_upgrade_thunk2(ScrClosure *cb, ScrHttpReq *req, ScrNetSocket *sock, ScrBytes *head) {
|
|
scr_bytes_release(head);
|
|
((void (*)(ScrClosure *, ScrHttpReq *, ScrNetSocket *))cb->fn)(cb, req, sock);
|
|
}
|
|
|
|
void scr_http_upgrade_thunk1(ScrClosure *cb, ScrHttpReq *req, ScrNetSocket *sock, ScrBytes *head) {
|
|
scr_bytes_release(head);
|
|
scr_net_sock_release(sock);
|
|
((void (*)(ScrClosure *, ScrHttpReq *))cb->fn)(cb, req);
|
|
}
|
|
|
|
void scr_http_upgrade_thunk0(ScrClosure *cb, ScrHttpReq *req, ScrNetSocket *sock, ScrBytes *head) {
|
|
scr_bytes_release(head);
|
|
scr_net_sock_release(sock);
|
|
scr_http_req_release(req);
|
|
((void (*)(ScrClosure *))cb->fn)(cb);
|
|
}
|
|
|
|
/* The handler adapters: (req, res), (req), and (). Both handles arrive
|
|
* +1; unused ones release here. */
|
|
void scr_http_handler_thunk2(ScrClosure *cb, ScrHttpReq *req, ScrHttpRes *res) {
|
|
((void (*)(ScrClosure *, ScrHttpReq *, ScrHttpRes *))cb->fn)(cb, req, res);
|
|
}
|
|
|
|
void scr_http_handler_thunk1(ScrClosure *cb, ScrHttpReq *req, ScrHttpRes *res) {
|
|
scr_http_res_release(res);
|
|
((void (*)(ScrClosure *, ScrHttpReq *))cb->fn)(cb, req);
|
|
}
|
|
|
|
void scr_http_handler_thunk0(ScrClosure *cb, ScrHttpReq *req, ScrHttpRes *res) {
|
|
scr_http_req_release(req);
|
|
scr_http_res_release(res);
|
|
((void (*)(ScrClosure *))cb->fn)(cb);
|
|
}
|
|
|
|
/* ── the CLIENT (http.request / http.get) ────────────────────────────────
|
|
*
|
|
* One handle per request, one dialed connection per handle (no agent
|
|
* pooling — SEMANTICS.md; the wire is Node's regardless, Connection:
|
|
* keep-alive included, and the socket closes when the response
|
|
* completes). Ownership: the handle holds the socket (+1) and, once
|
|
* parsed, the response req (+1); the socket's parser ctx holds the
|
|
* handle (+1) until the connection dies — the registry-shaped cycle
|
|
* breaks at teardown exactly like the server's conn→req edge. A live
|
|
* client is registered (+1, atexit-cleaned) so abandoned handles behave
|
|
* like abandoned sockets. */
|
|
|
|
struct ScrHttpClientReq {
|
|
size_t rc;
|
|
/* The http.request() call's AsyncLocalStorage context (+1): the
|
|
* request's deferred emits ('socket', 'finish', 'close') run in it. */
|
|
ScrAlsCtx *als;
|
|
ScrNetSocket *sock; /* +1 */
|
|
ScrHttpConn *conn; /* borrowed backref (the socket's ctx); NULL after free */
|
|
ScrStr *host, *path, *method;
|
|
int port;
|
|
int default_port; /* the Host header omits it (80 http, 443 https) */
|
|
ScrStr **hnames; /* user headers, verbatim case */
|
|
ScrStr **hvalues;
|
|
size_t nheaders;
|
|
bool removed_host;
|
|
bool removed_connection;
|
|
ScrHttpTrailers trailers;
|
|
bool user_cl; /* caller set content-length/transfer-encoding */
|
|
bool head_sent;
|
|
bool chunked; /* streaming framing committed */
|
|
int corked;
|
|
char *cork_buf;
|
|
size_t cork_len, cork_cap;
|
|
/* the head a corked write committed, sent first at the flush */
|
|
char *held_head;
|
|
size_t held_head_len, held_head_cap;
|
|
bool hold_head;
|
|
bool ended;
|
|
bool destroyed;
|
|
bool aborted;
|
|
bool abort_emitted;
|
|
bool sock_ready;
|
|
bool socket_emitted;
|
|
bool finish_queued;
|
|
bool writable_finished;
|
|
bool response_started; /* head parsed, res exists */
|
|
bool response_done;
|
|
bool had_error;
|
|
bool close_queued;
|
|
bool close_emitted; /* settled: listeners dropped, off the registry */
|
|
ScrHttpReq *res; /* +1 once the head parses */
|
|
ScrNetLs resp_ls, err_ls, timeout_ls, close_ls, upgrade_ls, socket_ls, finish_ls, abort_ls;
|
|
ScrNetLs continue_ls; /* 'continue': a 100 Continue response arrived */
|
|
/* the owning Agent (+1; the agent's entry holds this client +1 too —
|
|
* the cycle breaks at settle, or at the atexit agent sweep) */
|
|
struct ScrHttpAgent *agent;
|
|
ScrError *destroy_error; /* destroy(err) or the signal's AbortError (+1):
|
|
* fired in place of the teardown's own error */
|
|
ScrDyn *abort_dispose; /* options.signal: removes the abort listener (+1) */
|
|
double timeout_ms; /* the timeout option (0 = none), reapplied on a pooled socket */
|
|
size_t max_header_size; /* the maxHeaderSize option (0 = the default), applied to
|
|
* whichever parser serves the request */
|
|
bool reused; /* req.reusedSocket: the socket came from an agent's free list */
|
|
bool sock_pooled; /* the socket went back to the agent after the response */
|
|
ScrStr *pool_key; /* https: TLS options a pooled socket must match (+1), or NULL */
|
|
ScrClosure *abort_listener; /* its closure (borrowed); caps[0] points back here */
|
|
bool in_registry;
|
|
struct ScrHttpClientReq *next;
|
|
};
|
|
|
|
static SCR_TL ScrHttpClientReq *scr_http_clients = NULL; /* registry: +1 each */
|
|
|
|
ScrHttpClientReq *scr_http_client_retain(ScrHttpClientReq *c) {
|
|
if (c->rc != SIZE_MAX) c->rc++;
|
|
return c;
|
|
}
|
|
|
|
void scr_http_client_release(ScrHttpClientReq *c) {
|
|
if (!c || c->rc == SIZE_MAX) return;
|
|
if (--c->rc == 0) {
|
|
scr_als_ctx_release(c->als);
|
|
scr_net_ls_drop(&c->resp_ls);
|
|
scr_net_ls_drop(&c->err_ls);
|
|
scr_net_ls_drop(&c->timeout_ls);
|
|
scr_net_ls_drop(&c->close_ls);
|
|
scr_net_ls_drop(&c->upgrade_ls);
|
|
scr_net_ls_drop(&c->socket_ls);
|
|
scr_net_ls_drop(&c->finish_ls);
|
|
scr_net_ls_drop(&c->abort_ls);
|
|
scr_net_ls_drop(&c->continue_ls);
|
|
scr_str_release(c->host);
|
|
scr_str_release(c->path);
|
|
scr_str_release(c->method);
|
|
for (size_t i = 0; i < c->nheaders; i++) {
|
|
scr_str_release(c->hnames[i]);
|
|
scr_str_release(c->hvalues[i]);
|
|
}
|
|
free(c->hnames);
|
|
free(c->hvalues);
|
|
scr_http_trailers_clear(&c->trailers);
|
|
free(c->cork_buf);
|
|
free(c->held_head);
|
|
if (c->res) scr_http_req_release(c->res);
|
|
if (c->sock) scr_net_sock_release(c->sock);
|
|
if (c->destroy_error) scr_error_release(c->destroy_error);
|
|
scr_str_release(c->pool_key);
|
|
if (c->abort_listener) scr_box_set_ref(c->abort_listener->caps[0], NULL);
|
|
scr_dyn_release(c->abort_dispose);
|
|
#ifdef SCR_RC_AUDIT
|
|
scr_http_live--;
|
|
#endif
|
|
free(c);
|
|
}
|
|
}
|
|
|
|
static void scr_http_client_release_internal(struct ScrHttpClientReq *c) {
|
|
scr_http_client_release(c);
|
|
}
|
|
|
|
static ScrAlsCtx *scr_http_client_ctx(struct ScrHttpClientReq *c) { return c->als; }
|
|
|
|
void *scr_http_client_retain_v(void *p) { return scr_http_client_retain((ScrHttpClientReq *)p); }
|
|
void scr_http_client_release_v(void *p) { scr_http_client_release((ScrHttpClientReq *)p); }
|
|
|
|
static void scr_http_client_register(ScrHttpClientReq *c) {
|
|
if (c->in_registry) return;
|
|
c->in_registry = true;
|
|
c->next = NULL;
|
|
ScrHttpClientReq **link = &scr_http_clients;
|
|
while (*link) link = &(*link)->next;
|
|
*link = scr_http_client_retain(c);
|
|
}
|
|
|
|
static void scr_http_client_unregister(ScrHttpClientReq *c) {
|
|
if (!c->in_registry) return;
|
|
ScrHttpClientReq **link = &scr_http_clients;
|
|
while (*link && *link != c) link = &(*link)->next;
|
|
if (*link) {
|
|
*link = c->next;
|
|
c->next = NULL;
|
|
c->in_registry = false;
|
|
scr_http_client_release(c);
|
|
}
|
|
}
|
|
|
|
/* Agent bookkeeping (implementation below, with the Agent unit): the
|
|
* settling client leaves its agent's lists and frees a slot. */
|
|
static void scr_http_agent_client_done(struct ScrHttpAgent *ag, struct ScrHttpClientReq *c);
|
|
static void scr_http_agent_free_remove(struct ScrHttpAgent *a, struct ScrHttpConn *conn);
|
|
static bool scr_http_client_pool(struct ScrHttpConn *conn, struct ScrHttpClientReq *c, double hint);
|
|
static double scr_http_response_keep_alive(ScrHttpReq *res, const struct ScrHttpClientReq *c);
|
|
static struct ScrHttpAgent *scr_http_agent_release_p(struct ScrHttpAgent *ag);
|
|
static void scr_http_client_agent_detach(struct ScrHttpClientReq *c);
|
|
|
|
/* The queue's CLIENT_CLOSE emit: 'close' fires, the handle settles
|
|
* (listeners drop — the cycle story) and leaves the registry. */
|
|
/* Detaches the options.signal listener: the closure stops reaching this
|
|
* request and the signal drops the listener. */
|
|
static void scr_http_client_signal_detach(ScrHttpClientReq *c) {
|
|
if (c->abort_listener) {
|
|
scr_box_set_ref(c->abort_listener->caps[0], NULL);
|
|
c->abort_listener = NULL;
|
|
}
|
|
if (c->abort_dispose) {
|
|
ScrDyn *dispose = c->abort_dispose;
|
|
c->abort_dispose = NULL;
|
|
ScrDyn *r = scr_dyn_call(dispose, NULL, 0, "dispose");
|
|
if (r) scr_dyn_release(r);
|
|
scr_dyn_release(dispose);
|
|
}
|
|
}
|
|
|
|
static void scr_http_client_settle(struct ScrHttpClientReq *c) {
|
|
if (c->close_emitted) return;
|
|
c->close_emitted = true;
|
|
c->destroyed = true;
|
|
scr_http_client_signal_detach(c);
|
|
scr_http_client_agent_detach(c); /* usually already detached at socket close */
|
|
/* a socket that does not go back to the pool leaves totalSocketCount
|
|
* before the request's 'close', as Node's agent removes it at the
|
|
* socket's 'close' (c->sock keeps c->conn alive) */
|
|
if (c->sock != NULL && !c->sock_pooled) scr_http_conn_uncount(c->conn);
|
|
scr_net_fire0(&c->close_ls);
|
|
scr_net_ls_drop(&c->resp_ls);
|
|
scr_net_ls_drop(&c->err_ls);
|
|
scr_net_ls_drop(&c->timeout_ls);
|
|
scr_net_ls_drop(&c->close_ls);
|
|
scr_net_ls_drop(&c->socket_ls);
|
|
scr_net_ls_drop(&c->finish_ls);
|
|
scr_net_ls_drop(&c->continue_ls);
|
|
scr_http_client_unregister(c);
|
|
}
|
|
|
|
static void scr_http_client_queue_close(ScrHttpClientReq *c) {
|
|
if (c->close_queued || c->close_emitted) return;
|
|
c->close_queued = true;
|
|
scr_http_emit_push(SCR_HTTP_EMIT_CLIENT_CLOSE, scr_http_client_retain(c));
|
|
}
|
|
|
|
static void scr_http_client_queue_finish(ScrHttpClientReq *c) {
|
|
if (!c->ended || !c->sock_ready || c->destroyed || c->close_emitted || c->finish_queued || c->writable_finished) return;
|
|
c->finish_queued = true;
|
|
scr_http_emit_push(SCR_HTTP_EMIT_CLIENT_FINISH, scr_http_client_retain(c));
|
|
}
|
|
|
|
static void scr_http_client_emit_socket(ScrHttpClientReq *c) {
|
|
if (c->socket_emitted) return;
|
|
if (c->destroyed) {
|
|
scr_net_ls_drop(&c->socket_ls);
|
|
return;
|
|
}
|
|
c->socket_emitted = true;
|
|
ScrNetL *snap;
|
|
size_t count = scr_net_ls_snapshot(&c->socket_ls, &snap);
|
|
scr_dyn_this_push(c, SCR_DYNH_HTTP_CLIENT);
|
|
for (size_t i = 0; i < count; i++) {
|
|
if (!scr_exc_pending()) ((ScrNetConnFn)snap[i].fn)(snap[i].cb, scr_net_sock_retain(c->sock));
|
|
scr_closure_release(snap[i].cb);
|
|
}
|
|
scr_dyn_this_pop();
|
|
free(snap);
|
|
scr_net_ls_drop(&c->socket_ls);
|
|
}
|
|
|
|
static void scr_http_client_emit_finish(ScrHttpClientReq *c) {
|
|
c->finish_queued = false;
|
|
if (c->destroyed || c->close_emitted || !c->ended || !c->sock_ready || c->writable_finished) return;
|
|
c->writable_finished = true;
|
|
scr_net_fire0_this(&c->finish_ls, c, SCR_DYNH_HTTP_CLIENT);
|
|
scr_net_ls_drop(&c->finish_ls);
|
|
}
|
|
|
|
static void scr_http_client_emit_abort(ScrHttpClientReq *c) {
|
|
c->abort_emitted = true;
|
|
scr_net_fire0_this(&c->abort_ls, c, SCR_DYNH_HTTP_CLIENT);
|
|
scr_net_ls_drop(&c->abort_ls);
|
|
}
|
|
|
|
/* 'error' on the request handle: fires NOW (the callers sit in the net
|
|
* sweep / dispatch, Node's own emit spots); no listener exits 1, the
|
|
* unhandled-'error' story. The close follows through the queue. */
|
|
static void scr_http_client_error(ScrHttpClientReq *c, ScrStr *msg /*borrowed*/) {
|
|
if (c->close_emitted) return;
|
|
c->had_error = true;
|
|
if (c->destroy_error != NULL) {
|
|
/* Node emits the error destroy() was given instead of the teardown's */
|
|
ScrError *saved = scr_event_error_payload;
|
|
scr_event_error_payload = c->destroy_error;
|
|
scr_net_fire_err(&c->err_ls, c->destroy_error->message);
|
|
scr_event_error_payload = saved;
|
|
return;
|
|
}
|
|
scr_net_fire_err(&c->err_ls, msg);
|
|
}
|
|
|
|
/* ── the wire: Node's exact request head ─────────────────────────────── */
|
|
|
|
static bool scr_http_client_has_header(ScrHttpClientReq *c, const char *name) {
|
|
size_t len = strlen(name);
|
|
for (size_t i = 0; i < c->nheaders; i++) {
|
|
if (c->hnames[i]->len == len) {
|
|
bool eq = true;
|
|
for (size_t j = 0; j < len && eq; j++) {
|
|
if (tolower((unsigned char)c->hnames[i]->data[j]) != tolower((unsigned char)name[j])) eq = false;
|
|
}
|
|
if (eq) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool scr_http_header_name_is(ScrStr *name, const char *text) {
|
|
size_t len = strlen(text);
|
|
if (name->len != len) return false;
|
|
for (size_t i = 0; i < len; i++)
|
|
if (tolower((unsigned char)name->data[i]) != text[i]) return false;
|
|
return true;
|
|
}
|
|
|
|
ScrStr *scr_http_client_get_header(ScrHttpClientReq *c, ScrStr *name) {
|
|
for (size_t i = 0; i < c->nheaders; i++)
|
|
if (scr_http_name_eq(c->hnames[i], name)) return scr_str_retain(c->hvalues[i]);
|
|
return NULL;
|
|
}
|
|
|
|
bool scr_http_client_has_header_named(ScrHttpClientReq *c, ScrStr *name) {
|
|
for (size_t i = 0; i < c->nheaders; i++)
|
|
if (scr_http_name_eq(c->hnames[i], name)) return true;
|
|
return false;
|
|
}
|
|
|
|
ScrArr *scr_http_client_get_header_names(ScrHttpClientReq *c) {
|
|
return scr_http_out_header_names(c->hnames, c->nheaders, false);
|
|
}
|
|
|
|
ScrArr *scr_http_client_get_raw_header_names(ScrHttpClientReq *c) {
|
|
return scr_http_out_header_names(c->hnames, c->nheaders, true);
|
|
}
|
|
|
|
ScrDyn *scr_http_client_get_headers(ScrHttpClientReq *c) {
|
|
return scr_http_out_headers(c->hnames, c->hvalues, NULL, c->nheaders);
|
|
}
|
|
|
|
ScrStr *scr_http_client_method(ScrHttpClientReq *c) { return scr_str_retain(c->method); }
|
|
ScrStr *scr_http_client_path(ScrHttpClientReq *c) { return scr_str_retain(c->path); }
|
|
ScrStr *scr_http_client_host(ScrHttpClientReq *c) { return scr_str_retain(c->host); }
|
|
ScrStr *scr_http_client_protocol(ScrHttpClientReq *c) {
|
|
return c->default_port == 443 ? scr_str_new("https:", 6) : scr_str_new("http:", 5);
|
|
}
|
|
bool scr_http_client_headers_sent(ScrHttpClientReq *c) { return c->head_sent; }
|
|
bool scr_http_client_writable_ended(ScrHttpClientReq *c) { return c->ended; }
|
|
bool scr_http_client_writable_finished(ScrHttpClientReq *c) { return c->writable_finished; }
|
|
ScrNetSocket *scr_http_client_socket(ScrHttpClientReq *c) { return scr_net_sock_retain(c->sock); }
|
|
bool scr_http_client_reused_socket(ScrHttpClientReq *c) { return c->reused; }
|
|
void scr_http_client_set_nodelay(ScrHttpClientReq *c, bool enable) {
|
|
if (!c->sock) return;
|
|
scr_net_sock_release(scr_net_sock_set_nodelay(c->sock, enable));
|
|
}
|
|
void scr_http_client_set_socket_keepalive(ScrHttpClientReq *c, bool enable, double delay_ms) {
|
|
if (!c->sock) return;
|
|
scr_net_sock_set_keepalive(c->sock, enable, delay_ms);
|
|
}
|
|
|
|
static bool scr_http_client_header_mutable(ScrHttpClientReq *c, const char *op) {
|
|
if (!c->head_sent) return true;
|
|
char msg[80];
|
|
int len = snprintf(msg, sizeof msg, "Cannot %s headers after they are sent to the client", op);
|
|
scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)len, "ERR_HTTP_HEADERS_SENT");
|
|
return false;
|
|
}
|
|
|
|
void scr_http_client_set_header(ScrHttpClientReq *c, ScrStr *name, ScrStr *value) {
|
|
if (!scr_http_client_header_mutable(c, "set")) return;
|
|
ScrStr *label = scr_str_new("Header name", 11);
|
|
scr_http_validate_header_name(name, label);
|
|
scr_str_release(label);
|
|
if (scr_exc_pending()) return;
|
|
ScrDyn *wrapped = scr_dyn_new_str(value);
|
|
scr_http_validate_header_value(name, wrapped);
|
|
scr_dyn_release(wrapped);
|
|
if (scr_exc_pending()) return;
|
|
for (size_t i = 0; i < c->nheaders; i++) {
|
|
if (!scr_http_name_eq(c->hnames[i], name)) continue;
|
|
scr_str_release(c->hnames[i]);
|
|
scr_str_release(c->hvalues[i]);
|
|
c->hnames[i] = scr_str_retain(name);
|
|
c->hvalues[i] = scr_str_retain(value);
|
|
goto updated;
|
|
}
|
|
c->hnames = realloc(c->hnames, (c->nheaders + 1) * sizeof *c->hnames);
|
|
c->hvalues = realloc(c->hvalues, (c->nheaders + 1) * sizeof *c->hvalues);
|
|
if (!c->hnames || !c->hvalues) scr_http_oom();
|
|
c->hnames[c->nheaders] = scr_str_retain(name);
|
|
c->hvalues[c->nheaders] = scr_str_retain(value);
|
|
c->nheaders++;
|
|
updated:
|
|
if (scr_http_header_name_is(name, "host")) c->removed_host = false;
|
|
if (scr_http_header_name_is(name, "connection")) c->removed_connection = false;
|
|
c->user_cl = scr_http_client_has_header(c, "content-length") ||
|
|
scr_http_client_has_header(c, "transfer-encoding");
|
|
}
|
|
|
|
void scr_http_client_remove_header(ScrHttpClientReq *c, ScrStr *name) {
|
|
if (!scr_http_client_header_mutable(c, "remove")) return;
|
|
size_t w = 0;
|
|
for (size_t i = 0; i < c->nheaders; i++) {
|
|
if (scr_http_name_eq(c->hnames[i], name)) {
|
|
scr_str_release(c->hnames[i]);
|
|
scr_str_release(c->hvalues[i]);
|
|
} else {
|
|
c->hnames[w] = c->hnames[i];
|
|
c->hvalues[w] = c->hvalues[i];
|
|
w++;
|
|
}
|
|
}
|
|
c->nheaders = w;
|
|
if (scr_http_header_name_is(name, "host")) c->removed_host = true;
|
|
if (scr_http_header_name_is(name, "connection")) c->removed_connection = true;
|
|
c->user_cl = scr_http_client_has_header(c, "content-length") ||
|
|
scr_http_client_has_header(c, "transfer-encoding");
|
|
}
|
|
|
|
struct ScrHttpAgent;
|
|
static bool scr_http_agent_keeps_alive(const struct ScrHttpAgent *a);
|
|
|
|
/* Serializes and sends the head. `body_len` >= 0 fixes Content-Length
|
|
* (the end(data) path — and Node's Content-Length: 0 for empty POST/PUT/
|
|
* PATCH bodies); -1 means STREAMING: chunked unless the caller set the
|
|
* framing. Node's order: user headers verbatim, then Host (unless user-
|
|
* set), Connection: keep-alive, and the framing header. */
|
|
static void scr_http_client_send_head(ScrHttpClientReq *c, long long body_len) {
|
|
if (c->head_sent) return;
|
|
ScrHttpBuf b = {NULL, 0, 0};
|
|
scr_http_buf_append(&b, c->method->data, c->method->len);
|
|
scr_http_buf_str(&b, " ");
|
|
scr_http_buf_append(&b, c->path->data, c->path->len);
|
|
scr_http_buf_str(&b, " HTTP/1.1\r\n");
|
|
for (size_t i = 0; i < c->nheaders; i++) {
|
|
scr_http_buf_append(&b, c->hnames[i]->data, c->hnames[i]->len);
|
|
scr_http_buf_str(&b, ": ");
|
|
scr_http_buf_append(&b, c->hvalues[i]->data, c->hvalues[i]->len);
|
|
scr_http_buf_str(&b, "\r\n");
|
|
bool transfer = c->hnames[i]->len == 17;
|
|
for (size_t j = 0; j < 17 && transfer; j++) {
|
|
if (tolower((unsigned char)c->hnames[i]->data[j]) != "transfer-encoding"[j]) transfer = false;
|
|
}
|
|
if (transfer && scr_http_header_has_token(c->hvalues[i], "chunked")) c->chunked = true;
|
|
}
|
|
if (!c->removed_host && !scr_http_client_has_header(c, "host")) {
|
|
/* Host: name[:port] — the scheme's default port omitted (80 http,
|
|
* 443 https), IPv6 literals bracketed (Node) */
|
|
bool v6 = memchr(c->host->data, ':', c->host->len) != NULL;
|
|
scr_http_buf_str(&b, "Host: ");
|
|
if (v6) scr_http_buf_str(&b, "[");
|
|
scr_http_buf_append(&b, c->host->data, c->host->len);
|
|
if (v6) scr_http_buf_str(&b, "]");
|
|
if (c->port != c->default_port) {
|
|
char pbuf[16];
|
|
snprintf(pbuf, sizeof pbuf, ":%d", c->port);
|
|
scr_http_buf_str(&b, pbuf);
|
|
}
|
|
scr_http_buf_str(&b, "\r\n");
|
|
}
|
|
if (!c->removed_connection && !scr_http_client_has_header(c, "connection")) {
|
|
/* Node asks the server to close only for an agent without keepAlive
|
|
* and without a finite maxSockets (scr_http_agent_keeps_alive). */
|
|
if (c->agent != NULL && !scr_http_agent_keeps_alive(c->agent)) scr_http_buf_str(&b, "Connection: close\r\n");
|
|
else scr_http_buf_str(&b, "Connection: keep-alive\r\n");
|
|
}
|
|
if (!c->user_cl) {
|
|
if (body_len < 0 || scr_http_client_has_header(c, "trailer")) {
|
|
scr_http_buf_str(&b, "Transfer-Encoding: chunked\r\n");
|
|
c->chunked = true;
|
|
} else if (body_len > 0) {
|
|
char cl[48];
|
|
snprintf(cl, sizeof cl, "Content-Length: %lld\r\n", body_len);
|
|
scr_http_buf_str(&b, cl);
|
|
} else {
|
|
/* empty body: POST/PUT/PATCH send Content-Length: 0, everything
|
|
* else sends no framing header at all — Node's method split */
|
|
const ScrStr *m = c->method;
|
|
bool bodied = (m->len == 4 && memcmp(m->data, "POST", 4) == 0) ||
|
|
(m->len == 3 && memcmp(m->data, "PUT", 3) == 0) ||
|
|
(m->len == 5 && memcmp(m->data, "PATCH", 5) == 0);
|
|
if (bodied) scr_http_buf_str(&b, "Content-Length: 0\r\n");
|
|
}
|
|
}
|
|
if (scr_http_client_has_header(c, "trailer") && !c->chunked) {
|
|
static const char msg[] = "Trailers are invalid with this transfer encoding";
|
|
free(b.data);
|
|
scr_throw_error_msg_code(SCR_ERR_ERROR, msg, sizeof msg - 1, "ERR_HTTP_TRAILER_INVALID");
|
|
return;
|
|
}
|
|
scr_http_buf_str(&b, "\r\n");
|
|
c->head_sent = true;
|
|
if (c->hold_head) {
|
|
c->held_head = b.data;
|
|
c->held_head_len = b.len;
|
|
c->held_head_cap = b.cap;
|
|
return;
|
|
}
|
|
if (c->sock) scr_net_sock_write_native(c->sock, b.data, b.len);
|
|
free(b.data);
|
|
}
|
|
|
|
static void scr_http_client_send_held_head(ScrHttpClientReq *c) {
|
|
if (c->held_head == NULL) return;
|
|
char *head = c->held_head;
|
|
size_t len = c->held_head_len;
|
|
c->held_head = NULL;
|
|
c->held_head_len = c->held_head_cap = 0;
|
|
if (c->sock) scr_net_sock_write_native(c->sock, head, len);
|
|
free(head);
|
|
}
|
|
|
|
static void scr_http_client_write_raw(ScrHttpClientReq *c, const char *data, size_t len) {
|
|
if (c->ended || c->destroyed || c->close_emitted) return; /* write-after-end drops (divergence 48's stance) */
|
|
if (c->corked > 0) {
|
|
/* Node's write_() builds the implicit head even while corked */
|
|
if (!c->head_sent) {
|
|
c->hold_head = true;
|
|
scr_http_client_send_head(c, -1);
|
|
c->hold_head = false;
|
|
if (scr_exc_pending()) return;
|
|
}
|
|
scr_http_cork_buffer(&c->cork_buf, &c->cork_len, &c->cork_cap, data, len);
|
|
return;
|
|
}
|
|
if (!c->head_sent) scr_http_client_send_head(c, -1); /* streaming: chunked */
|
|
if (scr_exc_pending()) return;
|
|
scr_http_client_send_held_head(c);
|
|
if (!c->sock || len == 0) return;
|
|
if (c->chunked) {
|
|
char size[32];
|
|
snprintf(size, sizeof size, "%zx\r\n", len);
|
|
scr_net_sock_write_native(c->sock, size, strlen(size));
|
|
scr_net_sock_write_native(c->sock, data, len);
|
|
scr_net_sock_write_native(c->sock, "\r\n", 2);
|
|
} else {
|
|
scr_net_sock_write_native(c->sock, data, len);
|
|
}
|
|
}
|
|
|
|
/* Node 24.22's write_(): once the request has its socket, a write with the
|
|
* socket uncorked corks the message and uncorks it on the next tick, so the
|
|
* chunked writes of one tick go out as one chunk. */
|
|
static void scr_http_client_autocork_nt(ScrClosure *cb) {
|
|
ScrHttpClientReq *c = scr_box_get_ref(cb->caps[0]);
|
|
if (c->corked > 0) scr_http_client_uncork(c);
|
|
scr_http_client_release(c);
|
|
}
|
|
|
|
static void scr_http_client_autocork(ScrHttpClientReq *c) {
|
|
if (c->ended || c->destroyed || c->close_emitted || !c->socket_emitted || c->sock == NULL ||
|
|
c->corked > 0)
|
|
return;
|
|
c->corked++;
|
|
ScrClosure *cb = scr_closure_new((void *)&scr_http_client_autocork_nt, 1);
|
|
cb->caps[0] = scr_box_new_obj(&scr_http_client_retain_v, &scr_http_client_release_v, NULL);
|
|
scr_box_set_ref(cb->caps[0], scr_http_client_retain(c));
|
|
scr_next_tick(cb);
|
|
}
|
|
|
|
void scr_http_client_write_str(ScrHttpClientReq *c, ScrStr *data /*borrowed*/) {
|
|
scr_http_client_autocork(c);
|
|
char *owned;
|
|
const char *bytes = scr_str_utf8(data, &owned);
|
|
scr_http_client_write_raw(c, bytes, data->len);
|
|
scr_str_utf8_end(owned);
|
|
}
|
|
|
|
void scr_http_client_write_bytes(ScrHttpClientReq *c, ScrBytes *data /*borrowed*/) {
|
|
SCR_BYTES_SNAPSHOT(data);
|
|
scr_http_client_autocork(c);
|
|
scr_http_client_write_raw(c, (const char *)data->data, (data->len * scr_bytes_elem_size(data->elem)));
|
|
}
|
|
|
|
static void scr_http_client_cork_flush(ScrHttpClientReq *c) {
|
|
if (c->cork_len == 0 && c->held_head == NULL) return;
|
|
char *held = c->cork_buf;
|
|
size_t held_len = c->cork_len;
|
|
c->cork_buf = NULL;
|
|
c->cork_len = c->cork_cap = 0;
|
|
scr_http_client_write_raw(c, held, held_len);
|
|
free(held);
|
|
}
|
|
|
|
void scr_http_client_cork(ScrHttpClientReq *c) { c->corked++; }
|
|
|
|
void scr_http_client_uncork(ScrHttpClientReq *c) {
|
|
if (c->corked == 0) return;
|
|
if (--c->corked == 0) scr_http_client_cork_flush(c);
|
|
}
|
|
|
|
double scr_http_client_writable_corked(ScrHttpClientReq *c) { return (double)c->corked; }
|
|
|
|
static void scr_http_client_end_raw(ScrHttpClientReq *c, const char *data, size_t len) {
|
|
if (c->ended || c->destroyed || c->close_emitted) return;
|
|
if (c->corked > 0 || c->cork_len > 0 || c->held_head != NULL) {
|
|
/* A corked chunked request buffers end()'s chunk too: one chunk. */
|
|
if (c->corked > 0 && c->head_sent && c->chunked && len > 0) {
|
|
scr_http_cork_buffer(&c->cork_buf, &c->cork_len, &c->cork_cap, data, len);
|
|
data = "";
|
|
len = 0;
|
|
}
|
|
c->corked = 0;
|
|
scr_http_client_cork_flush(c);
|
|
if (scr_exc_pending()) return;
|
|
if (c->destroyed || c->close_emitted) return;
|
|
}
|
|
if (!c->head_sent) {
|
|
/* whole body known NOW: Content-Length framing (or none — the head
|
|
* serializer's empty-body method split). Head and body go out as one
|
|
* write, as Node sends them: on a connected (reused) socket two writes
|
|
* would be two segments, and the server could answer before the body
|
|
* arrives. */
|
|
c->hold_head = true;
|
|
scr_http_client_send_head(c, (long long)len);
|
|
c->hold_head = false;
|
|
if (scr_exc_pending()) return;
|
|
if (c->chunked) {
|
|
scr_http_client_write_raw(c, data, len);
|
|
if (c->sock) scr_http_send_chunk_end(c->sock, &c->trailers);
|
|
} else if (c->held_head != NULL) {
|
|
char *head = c->held_head;
|
|
size_t head_len = c->held_head_len;
|
|
c->held_head = NULL;
|
|
c->held_head_len = c->held_head_cap = 0;
|
|
if (c->sock) {
|
|
if (len > 0) {
|
|
char *joined = realloc(head, head_len + len);
|
|
if (!joined) scr_http_oom();
|
|
head = joined;
|
|
memcpy(head + head_len, data, len);
|
|
}
|
|
scr_net_sock_write_native(c->sock, head, head_len + len);
|
|
}
|
|
free(head);
|
|
}
|
|
} else {
|
|
scr_http_client_write_raw(c, data, len);
|
|
if (c->chunked && c->sock) scr_http_send_chunk_end(c->sock, &c->trailers);
|
|
}
|
|
c->ended = true;
|
|
scr_http_client_queue_finish(c);
|
|
}
|
|
|
|
void scr_http_client_end(ScrHttpClientReq *c) { scr_http_client_end_raw(c, "", 0); }
|
|
|
|
void scr_http_client_end_str(ScrHttpClientReq *c, ScrStr *data /*borrowed*/) {
|
|
char *owned;
|
|
const char *bytes = scr_str_utf8(data, &owned);
|
|
scr_http_client_end_raw(c, bytes, data->len);
|
|
scr_str_utf8_end(owned);
|
|
}
|
|
|
|
void scr_http_client_end_bytes(ScrHttpClientReq *c, ScrBytes *data /*borrowed*/) {
|
|
SCR_BYTES_SNAPSHOT(data);
|
|
scr_http_client_end_raw(c, (const char *)data->data, (data->len * scr_bytes_elem_size(data->elem)));
|
|
}
|
|
|
|
void scr_http_client_flush_headers(ScrHttpClientReq *c) {
|
|
if (!c->head_sent && !c->ended && !c->destroyed) scr_http_client_send_head(c, -1);
|
|
}
|
|
|
|
void scr_http_client_add_trailers(ScrHttpClientReq *c, ScrArr *pairs /*borrowed*/) {
|
|
if (c->ended || c->destroyed) return;
|
|
scr_http_trailers_replace(&c->trailers, pairs);
|
|
}
|
|
|
|
void scr_http_client_set_timeout(ScrHttpClientReq *c, double ms) {
|
|
if (!scr_http_timeout_valid(ms)) return;
|
|
if (c->sock && !c->sock_pooled) scr_net_sock_set_timeout(c->sock, ms);
|
|
}
|
|
|
|
void scr_http_client_set_timeout_cb(ScrHttpClientReq *c, double ms, ScrClosure *cb /*moves*/) {
|
|
scr_http_client_set_timeout(c, ms);
|
|
if (scr_exc_pending()) {
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_http_client_on_timeout(c, cb, false);
|
|
}
|
|
|
|
/* destroy(): tears the connection down NOW. A destroy after the response
|
|
* completed is a quiet no-op past the socket close (Node: just 'close');
|
|
* before it, the teardown surfaces as 'socket hang up' — both flow
|
|
* through the closed hook below. */
|
|
void scr_http_client_destroy(ScrHttpClientReq *c) {
|
|
if (c->destroyed) return;
|
|
c->destroyed = true;
|
|
/* a socket already back in the agent's pool belongs to the agent */
|
|
if (c->sock && !c->sock_pooled) scr_net_sock_destroy(c->sock);
|
|
}
|
|
|
|
static void scr_http_client_emit_destroy_error(ScrHttpClientReq *c) {
|
|
if (c->close_emitted) return;
|
|
scr_http_client_error(c, c->destroy_error->message);
|
|
if (!scr_exc_pending()) scr_http_client_queue_close(c);
|
|
}
|
|
|
|
/* http.request({ maxHeaderSize }): the response head's size bound. */
|
|
void scr_http_client_max_header_size(ScrHttpClientReq *c, double value) {
|
|
if (!scr_http_max_header_size_valid(value)) {
|
|
/* Node throws before dialing: the request goes away without events */
|
|
c->had_error = true;
|
|
scr_http_client_destroy(c);
|
|
return;
|
|
}
|
|
c->max_header_size = (size_t)value;
|
|
if (c->conn != NULL) c->conn->max_header_size = c->max_header_size;
|
|
}
|
|
|
|
/* req.destroy(err): tears the request down like destroy() and reports
|
|
* `err` (borrowed) as its 'error'. */
|
|
void scr_http_client_destroy_error(ScrHttpClientReq *c, ScrError *err) {
|
|
if (c->destroyed || c->close_emitted) return;
|
|
if (c->destroy_error == NULL) c->destroy_error = scr_error_retain(err);
|
|
if (c->sock == NULL) {
|
|
/* not dialed yet: nothing will tear down, so report it from the queue */
|
|
c->destroyed = true;
|
|
c->response_done = true;
|
|
scr_http_emit_push(SCR_HTTP_EMIT_CLIENT_DESTROY_ERROR, scr_http_client_retain(c));
|
|
return;
|
|
}
|
|
scr_http_client_destroy(c);
|
|
}
|
|
|
|
static void *scr_http_ptr_keep_v(void *p) { return p; }
|
|
static void scr_http_ptr_drop_v(void *p) { (void)p; }
|
|
|
|
/* signal.reason (+1, NULL when absent). */
|
|
static ScrDyn *scr_http_signal_reason(ScrDyn *signal) {
|
|
if (signal->kind == SCR_DYN_OBJ) {
|
|
ScrDyn *v = scr_dyn_obj_get(signal, "reason", 6);
|
|
return v ? scr_dyn_retain(v) : NULL;
|
|
}
|
|
ScrStr *key = scr_str_new("reason", 6);
|
|
ScrDyn *reason = scr_dyn_handle_key_get(signal, key);
|
|
scr_str_release(key);
|
|
return reason;
|
|
}
|
|
|
|
/* The options.signal listener: caps[0] the request (a plain pointer the
|
|
* request clears when it settles), caps[1] the signal. */
|
|
static ScrDyn *scr_http_client_abort_thunk(ScrClosure *clo, ScrDyn *const *args, size_t argc) {
|
|
(void)args;
|
|
(void)argc;
|
|
ScrHttpClientReq *c = scr_box_get_ref(clo->caps[0]);
|
|
if (c != NULL) {
|
|
ScrDyn *signal = scr_box_get_ref(clo->caps[1]); /* +1 */
|
|
ScrDyn *reason = scr_http_signal_reason(signal);
|
|
ScrError *e = scr_error_abort(reason);
|
|
if (reason) scr_dyn_release(reason);
|
|
scr_dyn_release(signal);
|
|
scr_http_client_retain(c);
|
|
scr_http_client_destroy_error(c, e);
|
|
scr_http_client_release(c);
|
|
scr_error_release(e);
|
|
}
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
|
|
static void *scr_http_dyn_retain_v(void *p) { return p ? scr_dyn_retain(p) : NULL; }
|
|
static void scr_http_dyn_release_v(void *p) { scr_dyn_release(p); }
|
|
|
|
/* The signal's `aborted` value (+1), or NULL with Node's
|
|
* ERR_INVALID_ARG_TYPE pending when the value is not an AbortSignal
|
|
* (validateAbortSignal: an object with an `aborted` property). */
|
|
static ScrDyn *scr_http_signal_aborted(const ScrDyn *signal) {
|
|
ScrDyn *aborted_value = NULL;
|
|
if (signal->kind == SCR_DYN_OBJ) {
|
|
ScrDyn *v = scr_dyn_obj_get(signal, "aborted", 7);
|
|
if (v) aborted_value = scr_dyn_retain(v);
|
|
} else if (signal->kind == SCR_DYN_HANDLE) {
|
|
ScrStr *key = scr_str_new("aborted", 7);
|
|
aborted_value = scr_dyn_handle_key_get(signal, key);
|
|
scr_str_release(key);
|
|
if (aborted_value && aborted_value->kind == SCR_DYN_UNDEF) {
|
|
scr_dyn_release(aborted_value);
|
|
aborted_value = NULL;
|
|
}
|
|
}
|
|
if (aborted_value == NULL) scr_dyn_arg_type_fail("signal", "an instance of AbortSignal", signal);
|
|
return aborted_value;
|
|
}
|
|
|
|
/* http.request's `signal` option before the request is built: Node's
|
|
* ClientRequest constructor validates a truthy signal before it dials, so
|
|
* a value that is not an AbortSignal throws ERR_INVALID_ARG_TYPE and no
|
|
* request exists. Borrows the signal. */
|
|
void scr_http_client_signal_check(const ScrDyn *signal) {
|
|
if (signal == NULL || !scr_dyn_truthy(signal)) return;
|
|
ScrDyn *aborted_value = scr_http_signal_aborted(signal);
|
|
if (aborted_value != NULL) scr_dyn_release(aborted_value);
|
|
}
|
|
|
|
/* http.request's `signal` option: an aborted signal destroys the request
|
|
* with Node's AbortError now; otherwise the abort event does, until the
|
|
* request closes. A falsy signal is ignored; anything else that is not an
|
|
* AbortSignal throws ERR_INVALID_ARG_TYPE. Borrows the signal. */
|
|
void scr_http_client_add_abort_signal(ScrHttpClientReq *c, ScrDyn *signal,
|
|
ScrDyn *(*add)(ScrDyn *signal, ScrDyn *listener)) {
|
|
if (signal == NULL || !scr_dyn_truthy(signal)) return;
|
|
ScrDyn *aborted_value = scr_http_signal_aborted(signal);
|
|
if (aborted_value == NULL) return;
|
|
bool aborted = scr_dyn_truthy(aborted_value);
|
|
scr_dyn_release(aborted_value);
|
|
if (aborted) {
|
|
ScrDyn *reason = scr_http_signal_reason(signal);
|
|
ScrError *e = scr_error_abort(reason);
|
|
if (reason) scr_dyn_release(reason);
|
|
scr_http_client_destroy_error(c, e);
|
|
scr_error_release(e);
|
|
return;
|
|
}
|
|
ScrClosure *clo = scr_closure_new((void *)&scr_http_client_abort_thunk, 2);
|
|
clo->caps[0] = scr_box_new_obj(&scr_http_ptr_keep_v, &scr_http_ptr_drop_v, NULL);
|
|
scr_box_set_ref(clo->caps[0], c);
|
|
clo->caps[1] = scr_box_new_obj(&scr_http_dyn_retain_v, &scr_http_dyn_release_v, NULL);
|
|
scr_box_set_ref(clo->caps[1], scr_dyn_retain(signal));
|
|
ScrDyn *listener = scr_dyn_new_func(clo, &scr_http_client_abort_thunk, 0, "()", "onAbort");
|
|
ScrDyn *dispose = add(signal, listener);
|
|
scr_dyn_release(listener);
|
|
if (dispose == NULL) return; /* exception pending */
|
|
c->abort_listener = clo;
|
|
c->abort_dispose = dispose;
|
|
}
|
|
|
|
void scr_http_client_abort(ScrHttpClientReq *c) {
|
|
if (c->aborted) return;
|
|
c->aborted = true;
|
|
bool early_or_complete = !c->socket_emitted || c->response_done;
|
|
/* A request whose socket was already handed to JS emits abort before
|
|
* teardown; an early abort reports close first. The queued emits keep
|
|
* both paths off the caller's stack. */
|
|
if (!early_or_complete)
|
|
scr_http_emit_push(SCR_HTTP_EMIT_CLIENT_ABORT, scr_http_client_retain(c));
|
|
scr_http_client_destroy(c);
|
|
if (early_or_complete) {
|
|
scr_http_client_queue_close(c);
|
|
scr_http_emit_push(SCR_HTTP_EMIT_CLIENT_ABORT, scr_http_client_retain(c));
|
|
}
|
|
}
|
|
|
|
bool scr_http_client_aborted(ScrHttpClientReq *c) { return c->aborted; }
|
|
|
|
bool scr_http_client_destroyed(ScrHttpClientReq *c) {
|
|
return c->destroyed || c->close_emitted;
|
|
}
|
|
|
|
void scr_http_client_on_response(ScrHttpClientReq *c, ScrClosure *cb /*moves*/, ScrHttpRespFn fn, bool once) {
|
|
if (c->close_emitted || c->response_started) {
|
|
/* the event is past (or the handle settled): never fires */
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_net_ls_add(&c->resp_ls, cb, (void *)fn, once);
|
|
}
|
|
|
|
void scr_http_client_on_socket(ScrHttpClientReq *c, ScrClosure *cb /*moves*/, ScrNetConnFn fn, bool once) {
|
|
if (c->socket_emitted || c->close_emitted) {
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_net_ls_add(&c->socket_ls, cb, (void *)fn, once);
|
|
}
|
|
|
|
void scr_http_client_on_finish(ScrHttpClientReq *c, ScrClosure *cb /*moves*/, bool once) {
|
|
if (c->writable_finished || c->close_emitted) {
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_net_ls_add(&c->finish_ls, cb, NULL, once);
|
|
}
|
|
|
|
void scr_http_client_on_continue(ScrHttpClientReq *c, ScrClosure *cb /*moves*/, bool once) {
|
|
if (c->close_emitted || c->response_started) {
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_net_ls_add(&c->continue_ls, cb, NULL, once);
|
|
}
|
|
|
|
void scr_http_client_on_abort(ScrHttpClientReq *c, ScrClosure *cb /*moves*/, bool once) {
|
|
if (c->abort_emitted) {
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_net_ls_add(&c->abort_ls, cb, NULL, once);
|
|
}
|
|
|
|
void scr_http_client_on_error(ScrHttpClientReq *c, ScrClosure *cb /*moves*/, ScrChildErrFn fn, bool once) {
|
|
if (c->close_emitted) {
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_net_ls_add(&c->err_ls, cb, (void *)fn, once);
|
|
}
|
|
|
|
void scr_http_client_on_upgrade(ScrHttpClientReq *c, ScrClosure *cb /*moves*/, ScrHttpUpgradeFn fn, bool once) {
|
|
if (c->close_emitted || c->response_started) {
|
|
/* the exchange already resolved (a 101 fired or a response landed) */
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_net_ls_add(&c->upgrade_ls, cb, (void *)fn, once);
|
|
}
|
|
|
|
void scr_http_client_on_timeout(ScrHttpClientReq *c, ScrClosure *cb /*moves*/, bool once) {
|
|
if (c->close_emitted) {
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_net_ls_add(&c->timeout_ls, cb, NULL, once);
|
|
}
|
|
|
|
void scr_http_client_on_close(ScrHttpClientReq *c, ScrClosure *cb /*moves*/, bool once) {
|
|
if (c->close_emitted) {
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_net_ls_add(&c->close_ls, cb, NULL, once);
|
|
}
|
|
|
|
/* ── response parsing (the conn's client mode) ───────────────────────── */
|
|
|
|
/* Parses one complete response HEAD sitting at buf[0..head_len):
|
|
* "HTTP/1.x SP status SP reason CRLF" then headers. Returns false on
|
|
* malformed input (the caller hangs up). */
|
|
static bool scr_http_client_parse_head(ScrHttpConn *conn, size_t head_len) {
|
|
ScrHttpClientReq *c = conn->client;
|
|
if (!c) return false;
|
|
const char *buf = conn->buf;
|
|
const char *line_end = memchr(buf, '\r', head_len);
|
|
if (!line_end || line_end + 1 >= buf + head_len || line_end[1] != '\n') return false;
|
|
const char *sp1 = memchr(buf, ' ', (size_t)(line_end - buf));
|
|
if (!sp1) return false;
|
|
size_t verlen = (size_t)(sp1 - buf);
|
|
if (!(verlen == 8 && (memcmp(buf, "HTTP/1.1", 8) == 0 || memcmp(buf, "HTTP/1.0", 8) == 0))) {
|
|
return false;
|
|
}
|
|
const char *st = sp1 + 1;
|
|
int status = 0;
|
|
int digits = 0;
|
|
while (st < line_end && *st >= '0' && *st <= '9' && digits < 3) {
|
|
status = status * 10 + (*st - '0');
|
|
st++;
|
|
digits++;
|
|
}
|
|
if (digits != 3) return false;
|
|
|
|
ScrHttpReq *res = calloc(1, sizeof *res);
|
|
if (!res) scr_http_oom();
|
|
res->rc = 1;
|
|
res->als = scr_als_capture();
|
|
res->status = status;
|
|
res->sock = scr_net_sock_retain(conn->sock);
|
|
#ifdef SCR_RC_AUDIT
|
|
scr_http_live++;
|
|
#endif
|
|
res->method = scr_str_new("", 0);
|
|
res->url = scr_str_new("", 0); /* Node: res.url is "" on client responses */
|
|
res->http10 = memcmp(buf, "HTTP/1.0", 8) == 0;
|
|
{
|
|
/* the reason phrase — res.statusMessage ("" when absent, Node) */
|
|
const char *reason = st;
|
|
if (reason < line_end && *reason == ' ') reason++;
|
|
res->status_msg = scr_str_new(reason, (size_t)(line_end - reason));
|
|
}
|
|
|
|
/* header lines until the blank line */
|
|
const char *p = line_end + 2;
|
|
const char *end = buf + head_len;
|
|
bool ok = true;
|
|
while (p < end) {
|
|
const char *eol = memchr(p, '\r', (size_t)(end - p));
|
|
if (!eol || eol + 1 >= end || eol[1] != '\n') {
|
|
ok = false;
|
|
break;
|
|
}
|
|
if (eol == p) break; /* the blank line */
|
|
const char *colon = memchr(p, ':', (size_t)(eol - p));
|
|
if (!colon || colon == p) {
|
|
ok = false;
|
|
break;
|
|
}
|
|
const char *v = colon + 1;
|
|
while (v < eol && (*v == ' ' || *v == '\t')) v++;
|
|
const char *ve = eol;
|
|
while (ve > v && (ve[-1] == ' ' || ve[-1] == '\t')) ve--;
|
|
scr_http_req_add_header(res, p, (size_t)(colon - p), v, (size_t)(ve - v));
|
|
p = eol + 2;
|
|
}
|
|
size_t header_size = res->status_msg->len;
|
|
for (size_t i = 0; i < res->nheaders; i++) header_size += res->hnames[i]->len + res->hvalues[i]->len;
|
|
if (!ok || res->nheaders > SCR_HTTP_MAX_HEADER_FIELDS || header_size >= scr_http_conn_header_limit(conn)) {
|
|
conn->header_overflow = ok;
|
|
scr_http_req_release(res);
|
|
return false;
|
|
}
|
|
conn->head_bytes = head_len;
|
|
conn->trailer_bytes = 0;
|
|
conn->trailer_fields = 0;
|
|
|
|
/*
|
|
* Informational responses do not settle the request: Node emits
|
|
* 'continue' for a 100 and keeps parsing until the final response. The
|
|
* 'information' event has no listener surface here yet. 101 is the
|
|
* upgrade case below.
|
|
*/
|
|
if (status >= 100 && status < 200 && status != 101) {
|
|
memmove(conn->buf, conn->buf + head_len, conn->len - head_len);
|
|
conn->len -= head_len;
|
|
scr_http_req_release(res);
|
|
if (status == 100) scr_net_fire0(&c->continue_ls);
|
|
return true;
|
|
}
|
|
|
|
/* framing: HEAD requests and 204/304 responses have NO body; chunked
|
|
* wins over Content-Length; neither means EOF-delimited */
|
|
bool chunked = false;
|
|
long long content_length = -1;
|
|
for (size_t i = 0; i < res->nheaders; i++) {
|
|
const ScrStr *n = res->hnames[i];
|
|
const ScrStr *v = res->hvalues[i];
|
|
if (n->len == 17 && memcmp(n->data, "transfer-encoding", 17) == 0) {
|
|
if (strstr(v->data, "chunked") != NULL) chunked = true;
|
|
} else if (n->len == 14 && memcmp(n->data, "content-length", 14) == 0) {
|
|
content_length = atoll(v->data);
|
|
if (content_length < 0) content_length = 0;
|
|
}
|
|
}
|
|
bool head_req = c->method->len == 4 && memcmp(c->method->data, "HEAD", 4) == 0;
|
|
bool no_body = head_req || status == 204 || status == 304;
|
|
|
|
/* 101 Switching Protocols: 'upgrade' fires INSTEAD of 'response' with
|
|
* (res, socket, head) — the parser steps aside and the raw socket is
|
|
* the listener's (the server-side handover, mirrored). With no
|
|
* listener Node destroys the connection; so does this slice. */
|
|
if (status == 101) {
|
|
conn->client_resp_started = true;
|
|
c->response_started = true;
|
|
c->response_done = true; /* the exchange settled: no timeout/hang-up paths */
|
|
c->res = scr_http_req_retain(res);
|
|
memmove(conn->buf, conn->buf + head_len, conn->len - head_len);
|
|
conn->len -= head_len;
|
|
if (c->upgrade_ls.n == 0) {
|
|
scr_http_req_release(res);
|
|
conn->len = 0;
|
|
scr_net_sock_destroy(conn->sock);
|
|
return true;
|
|
}
|
|
ScrBytes *head = scr_bytes_new(SCR_BYTES_U8, (double)conn->len);
|
|
head->is_buffer = true; /* the upgrade head is a Buffer */
|
|
if (conn->len > 0) memcpy(head->data, conn->buf, conn->len);
|
|
conn->len = 0;
|
|
scr_net_sock_clear_native_reader(conn->sock);
|
|
ScrNetL *snap;
|
|
size_t n = scr_net_ls_snapshot(&c->upgrade_ls, &snap);
|
|
for (size_t i = 0; i < n; i++) {
|
|
if (!scr_exc_pending()) {
|
|
((ScrHttpUpgradeFn)snap[i].fn)(snap[i].cb, scr_http_req_retain(res),
|
|
scr_net_sock_retain(conn->sock), scr_bytes_retain(head));
|
|
}
|
|
scr_closure_release(snap[i].cb);
|
|
}
|
|
free(snap);
|
|
scr_bytes_release(head);
|
|
scr_http_req_release(res);
|
|
/* The REQUEST handle's exchange is over: break the ctx→client edge
|
|
* (the parser's native hooks are cleared, so the socket's 'closed'
|
|
* would never break it — the cycle story) and settle 'close' through
|
|
* the queue, Node's req 'close' after an upgrade. The handed-over
|
|
* SOCKET lives on through its own refs. */
|
|
conn->client = NULL;
|
|
scr_http_client_queue_close(c);
|
|
scr_http_client_release(c);
|
|
return true;
|
|
}
|
|
|
|
conn->req = res; /* conn's +1 */
|
|
conn->client_resp_started = true;
|
|
c->response_started = true;
|
|
c->res = scr_http_req_retain(res);
|
|
|
|
/* consume the head bytes */
|
|
memmove(conn->buf, conn->buf + head_len, conn->len - head_len);
|
|
conn->len -= head_len;
|
|
|
|
if (no_body) {
|
|
conn->state = SCR_HTTP_HEAD;
|
|
} else if (chunked) {
|
|
conn->state = SCR_HTTP_CHUNK_SIZE;
|
|
} else if (content_length > 0) {
|
|
conn->state = SCR_HTTP_BODY_CL;
|
|
conn->body_remaining = (size_t)content_length;
|
|
} else if (content_length == 0) {
|
|
conn->state = SCR_HTTP_HEAD;
|
|
} else {
|
|
conn->state = SCR_HTTP_BODY_EOF;
|
|
}
|
|
|
|
/* 'response' fires NOW (macrotask, main stack — Node emits it from the
|
|
* socket's data path too) */
|
|
ScrNetL *snap;
|
|
size_t n = scr_net_ls_snapshot(&c->resp_ls, &snap);
|
|
for (size_t i = 0; i < n; i++) {
|
|
if (!scr_exc_pending()) ((ScrHttpRespFn)snap[i].fn)(snap[i].cb, scr_http_req_retain(res));
|
|
scr_closure_release(snap[i].cb);
|
|
}
|
|
free(snap);
|
|
if (scr_exc_pending()) return true;
|
|
|
|
if (conn->state == SCR_HTTP_HEAD) {
|
|
/* bodiless response: 'end' + teardown immediately */
|
|
scr_http_conn_body_done(conn);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* The response completed: 'end' on the response, then the exchange tears
|
|
* down — the socket closes (no pooling) and the deferred closes queue in
|
|
* Node's order (req 'close' before res 'close'). */
|
|
static void scr_http_client_response_done(ScrHttpConn *conn) {
|
|
ScrHttpClientReq *c = conn->client;
|
|
ScrHttpReq *res = conn->req;
|
|
if (!c || c->response_done) return;
|
|
c->response_done = true;
|
|
/* the response's reuse verdict, read before the response detaches:
|
|
* -2 = the connection closes, -1 = reusable with no Keep-Alive timeout
|
|
* hint, otherwise the hint in milliseconds */
|
|
double keep_alive_hint = res != NULL ? scr_http_response_keep_alive(res, c) : -2;
|
|
if (res) {
|
|
scr_http_req_retain(res);
|
|
scr_http_req_finish(res, true); /* fires 'end' */
|
|
}
|
|
/* Node's order: the REQUEST's 'close' precedes the response's */
|
|
scr_http_client_queue_close(c);
|
|
if (res) {
|
|
scr_http_queue_req_close(res);
|
|
scr_http_req_release(res);
|
|
/* drop the conn's ref NOW: the response outlives the exchange only
|
|
* through user refs / c->res — a retained conn->req would cycle
|
|
* (res→sock→ctx→res) past every settle */
|
|
conn->req = NULL;
|
|
scr_http_req_release(res);
|
|
}
|
|
/* a keepAlive agent takes the socket back; otherwise it closes quietly */
|
|
if (c->sock && !scr_http_client_pool(conn, c, keep_alive_hint)) scr_net_sock_destroy(c->sock);
|
|
}
|
|
|
|
/* Premature teardown classification, shared by eof/closed/error:
|
|
* before any response head → 'socket hang up' on the REQUEST; mid-body →
|
|
* 'aborted' on the RESPONSE (the request just closes) — both orders
|
|
* oracle-pinned. */
|
|
/* An incomplete client response that will never complete: 'aborted' and
|
|
* 'close' queue on it, its body never ends, and the conn drops its
|
|
* reference so the res→sock→ctx→res cycle breaks. */
|
|
static void scr_http_client_settle_response(ScrHttpConn *conn) {
|
|
ScrHttpReq *res = conn->req;
|
|
if (res == NULL) return;
|
|
scr_http_h2_req_aborted(res);
|
|
if (!res->destroyed) scr_http_queue_req_aborted(res);
|
|
scr_http_queue_req_close(res);
|
|
scr_http_req_finish(res, false); /* body never completes */
|
|
conn->req = NULL;
|
|
scr_http_req_release(res);
|
|
}
|
|
|
|
static void scr_http_client_premature(ScrHttpConn *conn) {
|
|
ScrHttpClientReq *c = conn->client;
|
|
if (!c || c->response_done || c->close_emitted) return;
|
|
c->response_done = true; /* the exchange is over, one way or another */
|
|
if (!c->response_started) {
|
|
if (!c->had_error && (!c->aborted || c->socket_emitted)) {
|
|
ScrStr *msg = scr_str_new("socket hang up", 14);
|
|
scr_http_client_error(c, msg);
|
|
scr_str_release(msg);
|
|
if (scr_exc_pending()) return;
|
|
}
|
|
scr_http_client_queue_close(c);
|
|
} else {
|
|
/* An incomplete client response fires 'aborted' before 'close'. A
|
|
* caller-initiated response.destroy() does not emit an 'error'; a
|
|
* premature peer close still does. */
|
|
scr_http_client_queue_close(c);
|
|
scr_http_client_settle_response(conn);
|
|
}
|
|
}
|
|
|
|
static void scr_http_client_eof(ScrHttpConn *conn) {
|
|
ScrHttpClientReq *c = conn->client;
|
|
if (conn->pool_agent != NULL) {
|
|
/* the server closed an idle pooled connection */
|
|
scr_net_sock_destroy(conn->sock);
|
|
return;
|
|
}
|
|
if (!c) return;
|
|
if (conn->state == SCR_HTTP_BODY_EOF && c->response_started && !c->response_done) {
|
|
/* EOF-delimited body: this IS completion */
|
|
scr_http_conn_body_done(conn);
|
|
return;
|
|
}
|
|
scr_http_client_premature(conn);
|
|
}
|
|
|
|
/* Detach a client from its agent (the socket died, or the handle
|
|
* settled): the entry leaves the lists BEFORE the socket's own 'close'
|
|
* listeners run — Node's agent removes its socket first too, so
|
|
* agent.sockets no longer names it inside a user 'close' handler. */
|
|
static void scr_http_client_agent_detach(ScrHttpClientReq *c) {
|
|
if (!c->agent) return;
|
|
struct ScrHttpAgent *ag = c->agent;
|
|
c->agent = NULL;
|
|
scr_http_agent_client_done(ag, c); /* frees the slot; may dial the next */
|
|
scr_http_agent_release_p(ag);
|
|
}
|
|
|
|
static void scr_http_client_closed(ScrHttpConn *conn) {
|
|
scr_http_conn_uncount(conn);
|
|
if (conn->pool_agent != NULL) scr_http_agent_free_remove(conn->pool_agent, conn);
|
|
ScrHttpClientReq *c = conn->client;
|
|
if (!c) return;
|
|
scr_http_client_agent_detach(c);
|
|
scr_http_client_premature(conn);
|
|
/* the connection is gone: break the ctx→client edge (the client stays
|
|
* alive through user refs / the queue until its 'close' settles) */
|
|
conn->client = NULL;
|
|
scr_http_client_release(c);
|
|
}
|
|
|
|
static bool scr_http_client_sock_err(ScrHttpConn *conn, ScrStr *msg) {
|
|
ScrHttpClientReq *c = conn->client;
|
|
if (!c) return true;
|
|
if (c->response_done || c->close_emitted || (c->aborted && !c->socket_emitted)) return true; /* teardown noise */
|
|
if (!c->response_started) {
|
|
/* connect/read failure before any response: the socket's message IS
|
|
* Node's ('connect ECONNREFUSED ip:port') — fire it on the request */
|
|
c->response_done = true;
|
|
scr_http_client_error(c, msg);
|
|
if (!scr_exc_pending()) scr_http_client_queue_close(c);
|
|
} else {
|
|
scr_http_client_premature(conn);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* Header-cap overflow / malformed response head: Node surfaces a parse
|
|
* error on the request; this slice hangs up with the same teardown shape
|
|
* ('socket hang up' when nothing parsed yet). */
|
|
static void scr_http_client_head_overflow(ScrHttpConn *conn) {
|
|
conn->len = 0;
|
|
ScrHttpClientReq *c = conn->client;
|
|
if (conn->header_overflow && c != NULL && !c->response_done && !c->close_emitted) {
|
|
/* Node's parser error for a head over maxHeaderSize or the field
|
|
* bound: code HPE_HEADER_OVERFLOW on the request. */
|
|
c->response_done = true;
|
|
ScrStr *msg = scr_str_new("Parse Error: Header overflow", 28);
|
|
scr_http_client_error(c, msg);
|
|
scr_str_release(msg);
|
|
/* a trailer overflow ends a response already in progress: settle it
|
|
* now, since the exchange no longer reaches the premature-close path */
|
|
scr_http_client_settle_response(conn);
|
|
if (scr_exc_pending()) return;
|
|
scr_http_client_queue_close(c);
|
|
} else {
|
|
scr_http_client_premature(conn);
|
|
}
|
|
if (conn->client && conn->client->sock) scr_net_sock_destroy(conn->client->sock);
|
|
}
|
|
|
|
/* The client socket's idle timeout: 'timeout' on the request handle
|
|
* (never destroys anything — Node; the caller destroys). */
|
|
static void scr_http_client_sock_timeout(void *ctx) {
|
|
ScrHttpConn *conn = (ScrHttpConn *)ctx;
|
|
if (conn->pool_agent != NULL) {
|
|
/* an idle pooled socket timed out: the agent destroys it */
|
|
scr_net_sock_destroy(conn->sock);
|
|
return;
|
|
}
|
|
ScrHttpClientReq *c = conn->client;
|
|
if (!c || c->response_done || c->close_emitted) return;
|
|
scr_net_fire0(&c->timeout_ls);
|
|
}
|
|
|
|
static void scr_http_client_sock_established(void *ctx) {
|
|
ScrHttpConn *conn = (ScrHttpConn *)ctx;
|
|
ScrHttpClientReq *c = conn->client;
|
|
if (!c) return;
|
|
c->sock_ready = true;
|
|
scr_http_client_queue_finish(c);
|
|
}
|
|
|
|
static void scr_http_client_conn_free(ScrHttpConn *conn) {
|
|
scr_http_conn_uncount(conn);
|
|
if (conn->pool_agent != NULL) scr_http_agent_free_remove(conn->pool_agent, conn);
|
|
if (conn->req) {
|
|
scr_http_req_finish(conn->req, false);
|
|
scr_http_req_release(conn->req);
|
|
conn->req = NULL;
|
|
}
|
|
if (conn->client) {
|
|
ScrHttpClientReq *c = conn->client;
|
|
conn->client = NULL;
|
|
scr_http_client_release(c);
|
|
}
|
|
free(conn->buf);
|
|
free(conn);
|
|
}
|
|
|
|
/* ── http.request / http.get ─────────────────────────────────────────── */
|
|
|
|
static ScrHttpClientReq *scr_http_request_impl(ScrStr *host /*borrowed*/, double port,
|
|
ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
|
|
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
|
|
bool auto_end, ScrClosure *cb /*moves, nullable*/,
|
|
ScrHttpRespFn fn, int default_port,
|
|
void (*wrap)(ScrNetSocket *, void *), void *wrap_ctx,
|
|
ScrNetSocket *presock /*moves, nullable*/) {
|
|
/* Node's ClientRequest constructor validates the method token
|
|
* SYNCHRONOUSLY (RFC 9110 tchar, at least one): the catchable
|
|
* ERR_INVALID_HTTP_TOKEN TypeError with Node's exact message, the raw
|
|
* method text inside the quotes. Moved-in arguments release — the
|
|
* request never exists. */
|
|
bool method_ok = method->len > 0;
|
|
for (size_t i = 0; i < method->len && method_ok; i++) {
|
|
unsigned char ch = (unsigned char)method->data[i];
|
|
method_ok = (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
|
|
(ch >= '0' && ch <= '9') ||
|
|
(ch != 0 && strchr("!#$%&'*+-.^_`|~", ch) != NULL);
|
|
}
|
|
if (!method_ok) {
|
|
if (cb) scr_closure_release(cb);
|
|
if (presock) scr_net_sock_release(presock);
|
|
ScrJsonBuf b;
|
|
scr_jb_init(&b);
|
|
scr_jb_puts(&b, "Method must be a valid HTTP token [\"");
|
|
scr_jb_write(&b, method->data, method->len);
|
|
scr_jb_puts(&b, "\"]");
|
|
ScrStr *msg = scr_jb_finish(&b);
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, msg->data, msg->len, "ERR_INVALID_HTTP_TOKEN");
|
|
scr_str_release(msg);
|
|
return NULL;
|
|
}
|
|
scr_http_install();
|
|
ScrHttpClientReq *c = calloc(1, sizeof *c);
|
|
if (!c) scr_http_oom();
|
|
c->rc = 1;
|
|
c->als = scr_als_capture();
|
|
#ifdef SCR_RC_AUDIT
|
|
scr_http_live++;
|
|
#endif
|
|
c->host = scr_str_retain(host);
|
|
c->path = scr_str_retain(path);
|
|
c->method = scr_str_retain(method);
|
|
c->port = (int)port;
|
|
c->default_port = default_port;
|
|
size_t npairs = (size_t)scr_arr_len(header_pairs);
|
|
if (npairs >= 2) {
|
|
c->hnames = malloc((npairs / 2) * sizeof *c->hnames);
|
|
c->hvalues = malloc((npairs / 2) * sizeof *c->hvalues);
|
|
if (!c->hnames || !c->hvalues) scr_http_oom();
|
|
for (size_t i = 0; i + 1 < npairs; i += 2) {
|
|
c->hnames[c->nheaders] = (ScrStr *)scr_arr_get_ref(header_pairs, (double)i);
|
|
c->hvalues[c->nheaders] = (ScrStr *)scr_arr_get_ref(header_pairs, (double)(i + 1));
|
|
c->nheaders++;
|
|
}
|
|
}
|
|
c->user_cl = scr_http_client_has_header(c, "content-length") ||
|
|
scr_http_client_has_header(c, "transfer-encoding");
|
|
if (!scr_http_client_has_header(c, "host")) {
|
|
/* Node queues the implicit Host header at construction, so all three
|
|
* outgoing header readers see it before end()/flushHeaders(). */
|
|
ScrHttpBuf hostbuf = {NULL, 0, 0};
|
|
bool v6 = memchr(c->host->data, ':', c->host->len) != NULL;
|
|
if (v6) scr_http_buf_str(&hostbuf, "[");
|
|
scr_http_buf_append(&hostbuf, c->host->data, c->host->len);
|
|
if (v6) scr_http_buf_str(&hostbuf, "]");
|
|
if (c->port != c->default_port) {
|
|
char portbuf[16];
|
|
snprintf(portbuf, sizeof portbuf, ":%d", c->port);
|
|
scr_http_buf_str(&hostbuf, portbuf);
|
|
}
|
|
ScrStr *host_name = scr_str_new("Host", 4);
|
|
ScrStr *host_value = scr_str_new(hostbuf.data, hostbuf.len);
|
|
free(hostbuf.data);
|
|
scr_http_client_set_header(c, host_name, host_value);
|
|
scr_str_release(host_name);
|
|
scr_str_release(host_value);
|
|
}
|
|
if (cb) scr_net_ls_add(&c->resp_ls, cb, (void *)fn, true);
|
|
|
|
if (presock != NULL) {
|
|
c->sock = presock;
|
|
scr_net_sock_set_context(c->sock, c->als);
|
|
} else {
|
|
ScrStr *dial = scr_net_blocking_lookup(host);
|
|
c->sock = scr_net_connect(port, dial, NULL);
|
|
scr_str_release(dial);
|
|
}
|
|
ScrHttpConn *conn = calloc(1, sizeof *conn);
|
|
if (!conn) scr_http_oom();
|
|
conn->sock = c->sock; /* borrowed */
|
|
conn->client_mode = true;
|
|
conn->client = scr_http_client_retain(c);
|
|
c->conn = conn;
|
|
scr_net_sock_set_native_reader(c->sock, &scr_http_conn_data, &scr_http_conn_eof,
|
|
&scr_http_conn_closed, conn, &scr_http_conn_free);
|
|
scr_net_sock_set_native_events(c->sock, &scr_http_client_sock_timeout, &scr_http_conn_err);
|
|
/* the transport (scr_tls.c's https) wraps the dialed socket before any
|
|
* bytes go out — everything below buffers until its handshake ends */
|
|
if (wrap) wrap(c->sock, wrap_ctx);
|
|
scr_net_sock_set_native_established(c->sock, &scr_http_client_sock_established);
|
|
c->sock_ready = scr_net_sock_established(c->sock);
|
|
c->timeout_ms = timeout_ms > 0 ? timeout_ms : 0;
|
|
if (timeout_ms > 0) scr_net_sock_set_timeout(c->sock, timeout_ms);
|
|
scr_http_client_register(c);
|
|
scr_http_emit_push(SCR_HTTP_EMIT_CLIENT_SOCKET, scr_http_client_retain(c));
|
|
if (auto_end) scr_http_client_end(c);
|
|
return c;
|
|
}
|
|
|
|
ScrHttpClientReq *scr_http_request_ex(ScrStr *host /*borrowed*/, double port,
|
|
ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
|
|
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
|
|
bool auto_end, ScrClosure *cb /*moves, nullable*/,
|
|
ScrHttpRespFn fn, int default_port,
|
|
void (*wrap)(ScrNetSocket *, void *), void *wrap_ctx) {
|
|
return scr_http_request_impl(host, port, path, method, timeout_ms, header_pairs, auto_end,
|
|
cb, fn, default_port, wrap, wrap_ctx, NULL);
|
|
}
|
|
|
|
/* http.request({ createConnection, ... }): the caller's DIALER supplies
|
|
* the socket — conn_cb runs ONCE, synchronously (Node invokes it from
|
|
* the request constructor's onSocket path), and its socket carries the
|
|
* exchange exactly like a dialed one (head bytes buffer through connect
|
|
* and flush on establishment). The Host header defaults to "localhost"
|
|
* — the proxy shape sets headers.host itself, which wins verbatim. */
|
|
ScrHttpClientReq *scr_http_request_conn(ScrClosure *conn_cb /*moves*/, ScrStr *path /*borrowed*/,
|
|
ScrStr *method /*borrowed*/, double timeout_ms,
|
|
ScrArr *header_pairs /*borrowed*/, bool auto_end,
|
|
ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn) {
|
|
ScrNetSocket *sock = ((ScrNetSocket *(*)(ScrClosure *))conn_cb->fn)(conn_cb); /* +1 result */
|
|
scr_closure_release(conn_cb);
|
|
if (scr_exc_pending() || sock == NULL) {
|
|
/* the dialer threw: NULL result, dummy-released past the emitted
|
|
* pending check (the libCall is in the may-throw seed set) */
|
|
if (cb != NULL) scr_closure_release(cb);
|
|
scr_net_sock_release(sock);
|
|
return NULL;
|
|
}
|
|
ScrStr *host = scr_str_new("localhost", 9);
|
|
ScrHttpClientReq *c = scr_http_request_impl(host, 80, path, method, timeout_ms, header_pairs,
|
|
auto_end, cb, fn, 80, NULL, NULL, sock);
|
|
scr_str_release(host);
|
|
return c;
|
|
}
|
|
|
|
/* http.request without an agent option: the request goes through
|
|
* http.globalAgent, as in Node. */
|
|
ScrHttpClientReq *scr_http_request(ScrStr *host /*borrowed*/, double port,
|
|
ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
|
|
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
|
|
bool auto_end, ScrClosure *cb /*moves, nullable*/,
|
|
ScrHttpRespFn fn) {
|
|
return scr_http_request_agent_ex(host, port, path, method, timeout_ms, header_pairs, auto_end,
|
|
NULL, cb, fn, 80, NULL, NULL, NULL);
|
|
}
|
|
|
|
/* ── the http Agent ──────────────────────────────────────────────────────
|
|
*
|
|
* Options, getName, destroy, and REAL maxSockets accounting over the
|
|
* one-dial-per-request connection model: an over-limit request's socket
|
|
* defers its dial (scr_net_connect_deferred) and queues; a dying
|
|
* active connection frees the slot and starts the next dial. What the
|
|
* runtime cannot express stays a NAMED fence: keep-alive socket POOLING
|
|
* (keepAlive: true) fences at construction — agent.freeSockets is
|
|
* always empty; the runtime does not emulate a pool.
|
|
*
|
|
* Ownership: the agent registers (+1, atexit-swept); entries hold their
|
|
* client +1 and the client holds the agent +1 — the cycle breaks when
|
|
* the client's socket dies (scr_http_client_agent_detach) or at the
|
|
* atexit sweep. */
|
|
|
|
typedef struct ScrHttpAgentEnt {
|
|
ScrStr *name; /* getName's shape: "host:port:" */
|
|
ScrHttpClientReq *client; /* +1 while listed */
|
|
bool queued; /* waiting for a slot: the dial is deferred */
|
|
struct ScrHttpAgentEnt *next;
|
|
} ScrHttpAgentEnt;
|
|
|
|
typedef struct ScrHttpAgent {
|
|
size_t rc;
|
|
bool secure; /* https.Agent: protocol/defaultPort answers */
|
|
bool keep_alive; /* keepAlive: finished connections return to free_socks */
|
|
bool fifo; /* scheduling: 'fifo' takes the oldest free socket, 'lifo' the newest */
|
|
double ka_msecs;
|
|
double max_sockets; /* INFINITY = Node's default */
|
|
double max_free;
|
|
double timeout_ms; /* < 0 = unset */
|
|
double default_port; /* settable (agent.defaultPort = p) */
|
|
bool destroyed;
|
|
ScrHttpAgentEnt *ents; /* append order — Node's FIFO queue */
|
|
struct ScrHttpAgentFree *free_socks; /* keepAlive: idle sockets, most recent first */
|
|
ScrHttpSockCount *sock_count; /* totalSocketCount (+1) */
|
|
bool in_registry;
|
|
struct ScrHttpAgent *next;
|
|
} ScrHttpAgent;
|
|
|
|
/* One idle socket in a keepAlive agent's free list, with its parser. */
|
|
typedef struct ScrHttpAgentFree {
|
|
ScrStr *name; /* getName's key */
|
|
ScrStr *pool_key; /* https: the TLS options the socket was verified with (+1), or NULL */
|
|
ScrNetSocket *sock; /* +1 */
|
|
struct ScrHttpConn *conn; /* the socket's parser (owned by the socket) */
|
|
struct ScrHttpAgentFree *next;
|
|
} ScrHttpAgentFree;
|
|
|
|
static SCR_TL ScrHttpAgent *scr_http_agents = NULL; /* registry: +1 each, atexit-swept */
|
|
|
|
/* Node's ClientRequest: an agent without keepAlive and without a finite
|
|
* maxSockets asks the server to close; any other agent keeps alive. */
|
|
static bool scr_http_agent_keeps_alive(const ScrHttpAgent *a) {
|
|
return a->keep_alive || isfinite(a->max_sockets);
|
|
}
|
|
|
|
static ScrHttpAgent *scr_http_agent_retain(ScrHttpAgent *a) {
|
|
a->rc++;
|
|
return a;
|
|
}
|
|
|
|
static void scr_http_agent_release(ScrHttpAgent *a) {
|
|
if (!a || --a->rc > 0) return;
|
|
while (a->free_socks) {
|
|
ScrHttpAgentFree *f = a->free_socks;
|
|
a->free_socks = f->next;
|
|
f->conn->pool_agent = NULL;
|
|
scr_str_release(f->name);
|
|
scr_str_release(f->pool_key);
|
|
scr_net_sock_release(f->sock);
|
|
free(f);
|
|
}
|
|
ScrHttpAgentEnt *e = a->ents;
|
|
while (e) {
|
|
ScrHttpAgentEnt *next = e->next;
|
|
scr_str_release(e->name);
|
|
scr_http_client_release(e->client);
|
|
free(e);
|
|
e = next;
|
|
}
|
|
scr_http_sock_count_release(a->sock_count);
|
|
free(a);
|
|
}
|
|
|
|
static struct ScrHttpAgent *scr_http_agent_release_p(struct ScrHttpAgent *ag) {
|
|
scr_http_agent_release(ag);
|
|
return NULL;
|
|
}
|
|
|
|
static void *scr_http_agent_retain_v(void *p) { return scr_http_agent_retain((ScrHttpAgent *)p); }
|
|
static void scr_http_agent_release_v(void *p) { scr_http_agent_release((ScrHttpAgent *)p); }
|
|
|
|
/* Actives (dialed, not settled) under a name. */
|
|
static size_t scr_http_agent_active(const ScrHttpAgent *a, const ScrStr *name) {
|
|
size_t n = 0;
|
|
for (const ScrHttpAgentEnt *e = a->ents; e; e = e->next) {
|
|
if (!e->queued && e->name->len == name->len &&
|
|
memcmp(e->name->data, name->data, name->len) == 0) n++;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
static bool scr_http_name_same(const ScrStr *a, const ScrStr *b) {
|
|
return a->len == b->len && memcmp(a->data, b->data, a->len) == 0;
|
|
}
|
|
|
|
static size_t scr_http_agent_free_count(const ScrHttpAgent *a, const ScrStr *name) {
|
|
size_t n = 0;
|
|
for (const ScrHttpAgentFree *f = a->free_socks; f; f = f->next) n += scr_http_name_same(f->name, name);
|
|
return n;
|
|
}
|
|
|
|
static bool scr_http_key_same(const ScrStr *a, const ScrStr *b) {
|
|
return a == NULL ? b == NULL : b != NULL && scr_http_name_same(a, b);
|
|
}
|
|
|
|
/* Takes a free socket for `name` off the list, or NULL: the most recently
|
|
* freed one under 'lifo' scheduling, the oldest under 'fifo'. */
|
|
static ScrHttpAgentFree *scr_http_agent_free_take(ScrHttpAgent *a, const ScrStr *name,
|
|
const ScrStr *pool_key) {
|
|
ScrHttpAgentFree **found = NULL;
|
|
for (ScrHttpAgentFree **link = &a->free_socks; *link; link = &(*link)->next) {
|
|
if (!scr_http_name_same((*link)->name, name) || !scr_http_key_same((*link)->pool_key, pool_key))
|
|
continue;
|
|
found = link;
|
|
if (!a->fifo) break;
|
|
}
|
|
if (found == NULL) return NULL;
|
|
ScrHttpAgentFree *f = *found;
|
|
*found = f->next;
|
|
return f;
|
|
}
|
|
|
|
/* Puts a socket taken by scr_http_agent_free_take back where it was
|
|
* relative to the other sockets for its name. */
|
|
static void scr_http_agent_free_untake(ScrHttpAgent *a, ScrHttpAgentFree *f) {
|
|
ScrHttpAgentFree **link = &a->free_socks;
|
|
if (a->fifo) {
|
|
while (*link) link = &(*link)->next;
|
|
}
|
|
f->next = *link;
|
|
*link = f;
|
|
}
|
|
|
|
/* The idle socket closed (or its parser is going away): it leaves the
|
|
* free list. */
|
|
static void scr_http_agent_free_remove(struct ScrHttpAgent *a, struct ScrHttpConn *conn) {
|
|
for (ScrHttpAgentFree **link = &a->free_socks; *link; link = &(*link)->next) {
|
|
if ((*link)->conn != conn) continue;
|
|
ScrHttpAgentFree *f = *link;
|
|
*link = f->next;
|
|
conn->pool_agent = NULL;
|
|
scr_str_release(f->name);
|
|
scr_str_release(f->pool_key);
|
|
scr_net_sock_release(f->sock);
|
|
free(f);
|
|
return;
|
|
}
|
|
conn->pool_agent = NULL;
|
|
}
|
|
|
|
/* Binds request `c` to a pooled socket in place of its deferred,
|
|
* never-dialed one: the deferred socket's parser is dropped, its buffered
|
|
* bytes move to the pooled socket, and the pooled parser serves `c`.
|
|
* `reused` is req.reusedSocket: Node sets it when a new request takes a
|
|
* free socket, not when a queued request receives a freed one. */
|
|
static void scr_http_client_rebind(ScrHttpClientReq *c, ScrHttpAgentFree *f, bool reused) {
|
|
ScrNetSocket *old = c->sock;
|
|
ScrHttpConn *oldconn = c->conn;
|
|
scr_net_sock_set_native_reader(old, NULL, NULL, NULL, NULL, NULL);
|
|
scr_net_sock_set_native_events(old, NULL, NULL);
|
|
scr_net_sock_set_native_established(old, NULL);
|
|
scr_net_sock_move_writes(old, f->sock);
|
|
if (oldconn != NULL) {
|
|
scr_http_conn_uncount(oldconn);
|
|
if (oldconn->client) scr_http_client_release(oldconn->client);
|
|
free(oldconn->buf);
|
|
free(oldconn);
|
|
}
|
|
scr_net_sock_destroy(old);
|
|
scr_net_sock_release(old);
|
|
ScrHttpConn *conn = f->conn;
|
|
c->sock = f->sock; /* the free entry's +1 moves */
|
|
/* Node runs a pooled socket's events for this request in its context. */
|
|
scr_net_sock_set_context(c->sock, c->als);
|
|
c->conn = conn;
|
|
conn->client = scr_http_client_retain(c);
|
|
conn->pool_agent = NULL;
|
|
/* the pooled parser still holds the previous request's limit */
|
|
conn->max_header_size = c->max_header_size;
|
|
conn->state = SCR_HTTP_HEAD;
|
|
conn->len = 0;
|
|
conn->body_remaining = 0;
|
|
conn->client_resp_started = false;
|
|
conn->header_overflow = false;
|
|
conn->head_bytes = conn->trailer_bytes = conn->trailer_fields = 0;
|
|
scr_str_release(f->name);
|
|
scr_str_release(f->pool_key);
|
|
free(f);
|
|
c->reused = reused;
|
|
c->sock_ready = true;
|
|
scr_net_sock_set_ref(c->sock, true);
|
|
scr_net_sock_set_timeout(c->sock, c->timeout_ms);
|
|
scr_http_client_queue_finish(c);
|
|
}
|
|
|
|
/* Whether the request's own header `name` carries `token`. */
|
|
static bool scr_http_client_has_header_value(const ScrHttpClientReq *c, const char *name,
|
|
const char *token) {
|
|
for (size_t i = 0; i < c->nheaders; i++) {
|
|
if (scr_http_header_name_is(c->hnames[i], name) && scr_http_header_has_token(c->hvalues[i], token))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* A client response's keep-alive verdict: -2 when the connection closes
|
|
* after it (Connection: close, HTTP/1.0 without keep-alive, or a body
|
|
* delimited by the connection closing), otherwise the Keep-Alive header's
|
|
* /^timeout=(\d+)/ hint in milliseconds, or -1 without one. */
|
|
static double scr_http_response_keep_alive(ScrHttpReq *res, const struct ScrHttpClientReq *c) {
|
|
bool keep = !res->http10, framed = false;
|
|
double hint = -1;
|
|
for (size_t i = 0; i < res->nheaders; i++) {
|
|
const ScrStr *n = res->hnames[i];
|
|
const ScrStr *v = res->hvalues[i];
|
|
if (scr_http_header_name_is((ScrStr *)n, "connection")) {
|
|
if (scr_http_header_has_token(v, "close")) keep = false;
|
|
else if (scr_http_header_has_token(v, "keep-alive")) keep = true;
|
|
} else if (scr_http_header_name_is((ScrStr *)n, "content-length") ||
|
|
scr_http_header_name_is((ScrStr *)n, "transfer-encoding")) {
|
|
framed = true;
|
|
} else if (scr_http_header_name_is((ScrStr *)n, "keep-alive") && v->len > 8 &&
|
|
memcmp(v->data, "timeout=", 8) == 0 && isdigit((unsigned char)v->data[8])) {
|
|
double seconds = 0;
|
|
for (size_t k = 8; k < v->len && isdigit((unsigned char)v->data[k]); k++) seconds = seconds * 10 + (v->data[k] - '0');
|
|
hint = seconds * 1000;
|
|
}
|
|
}
|
|
bool head = c->method->len == 4 && memcmp(c->method->data, "HEAD", 4) == 0;
|
|
bool bodiless = head || res->status == 204 || res->status == 304 || (res->status >= 100 && res->status < 200);
|
|
if (!keep || (!framed && !bodiless)) return -2;
|
|
return hint;
|
|
}
|
|
|
|
/* Node's agent.keepSocketAlive after a response: the socket returns to the
|
|
* agent's free list (unref'd, with the agent's timeout lowered to the
|
|
* server's Keep-Alive hint less one second) unless the response or request
|
|
* asked to close, the request did not finish, or the free list for the
|
|
* name is full. Answers whether the socket was kept. */
|
|
static bool scr_http_client_pool(struct ScrHttpConn *conn, struct ScrHttpClientReq *c, double hint) {
|
|
ScrHttpAgent *ag = c->agent;
|
|
if (ag == NULL || !ag->keep_alive || ag->destroyed || c->destroyed || c->aborted) return false;
|
|
if (hint == -2 || !c->ended || conn->len != 0 || scr_http_client_has_header_value(c, "connection", "close"))
|
|
return false;
|
|
ScrStr *name = NULL;
|
|
for (ScrHttpAgentEnt *e = ag->ents; e && name == NULL; e = e->next)
|
|
if (e->client == c) name = scr_str_retain(e->name);
|
|
if (name == NULL) return false;
|
|
if ((double)scr_http_agent_free_count(ag, name) >= ag->max_free) {
|
|
scr_str_release(name);
|
|
return false;
|
|
}
|
|
double timeout = ag->timeout_ms > 0 ? ag->timeout_ms : 0;
|
|
if (hint >= 0) {
|
|
double server = hint - 1000;
|
|
if (server < 0) {
|
|
scr_str_release(name);
|
|
return false;
|
|
}
|
|
if (server < timeout) timeout = server;
|
|
}
|
|
ScrHttpAgentFree *f = calloc(1, sizeof *f);
|
|
if (!f) scr_http_oom();
|
|
f->name = name;
|
|
f->pool_key = c->pool_key ? scr_str_retain(c->pool_key) : NULL;
|
|
f->sock = scr_net_sock_retain(c->sock);
|
|
f->conn = conn;
|
|
/* the parser no longer serves this request */
|
|
conn->client = NULL;
|
|
conn->pool_agent = ag;
|
|
f->next = ag->free_socks;
|
|
ag->free_socks = f;
|
|
c->sock_pooled = true;
|
|
scr_net_sock_set_ref(f->sock, false);
|
|
scr_net_sock_set_timeout(f->sock, timeout);
|
|
scr_http_client_agent_detach(c); /* frees the slot: a queued request takes the socket */
|
|
scr_http_client_release(c); /* the parser's reference */
|
|
return true;
|
|
}
|
|
|
|
/* The settling/dying client leaves the lists; a freed slot starts the
|
|
* first queued dial for the same name (Node's FIFO). */
|
|
static void scr_http_agent_client_done(struct ScrHttpAgent *ag, struct ScrHttpClientReq *c) {
|
|
ScrHttpAgentEnt **link = &ag->ents;
|
|
ScrStr *name = NULL;
|
|
while (*link && (*link)->client != c) link = &(*link)->next;
|
|
if (*link) {
|
|
ScrHttpAgentEnt *e = *link;
|
|
*link = e->next;
|
|
name = e->name; /* ownership moves out for the pump below */
|
|
scr_http_client_release(e->client);
|
|
free(e);
|
|
}
|
|
if (name != NULL && !ag->destroyed) {
|
|
for (ScrHttpAgentEnt *e = ag->ents; e; e = e->next) {
|
|
if (e->queued && e->name->len == name->len &&
|
|
memcmp(e->name->data, name->data, name->len) == 0) {
|
|
ScrHttpAgentFree *f = scr_http_agent_free_take(ag, name, e->client->pool_key);
|
|
if (f != NULL) {
|
|
e->queued = false;
|
|
scr_http_client_rebind(e->client, f, false);
|
|
} else if (scr_http_agent_active(ag, name) < ag->max_sockets) {
|
|
e->queued = false;
|
|
if (e->client->sock) {
|
|
scr_http_conn_count(e->client->conn, ag->sock_count);
|
|
scr_net_sock_dial_start(e->client->sock);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
scr_str_release(name);
|
|
}
|
|
|
|
/* getName's string builder — Node's exact shape:
|
|
* host:port:localAddress[:family][:socketPath]. */
|
|
static ScrStr *scr_http_agent_name(const char *host, size_t host_len, const char *port,
|
|
size_t port_len, const char *laddr, size_t laddr_len,
|
|
int family, const char *spath, size_t spath_len) {
|
|
ScrJsonBuf b;
|
|
scr_jb_init(&b);
|
|
if (host_len == 0) { scr_jb_puts(&b, "localhost"); }
|
|
else for (size_t i = 0; i < host_len; i++) scr_jb_putc(&b, host[i]);
|
|
scr_jb_putc(&b, ':');
|
|
for (size_t i = 0; i < port_len; i++) scr_jb_putc(&b, port[i]);
|
|
scr_jb_putc(&b, ':');
|
|
for (size_t i = 0; i < laddr_len; i++) scr_jb_putc(&b, laddr[i]);
|
|
if (family == 4 || family == 6) {
|
|
scr_jb_putc(&b, ':');
|
|
scr_jb_putc(&b, family == 4 ? '4' : '6');
|
|
}
|
|
if (spath_len > 0) {
|
|
scr_jb_putc(&b, ':');
|
|
for (size_t i = 0; i < spath_len; i++) scr_jb_putc(&b, spath[i]);
|
|
}
|
|
return scr_jb_finish(&b);
|
|
}
|
|
|
|
ScrDyn *scr_http_agent_new(bool secure, bool keep_alive, double ka_msecs,
|
|
double max_sockets, double max_free, double timeout_ms,
|
|
double port /* < 0 = unset */, const ScrDyn *scheduling) {
|
|
scr_http_install();
|
|
/* Node: options.scheduling || 'lifo', then validateOneOf */
|
|
bool fifo = false;
|
|
if (scheduling != NULL && scr_dyn_truthy(scheduling)) {
|
|
const ScrStr *s = scheduling->kind == SCR_DYN_STR ? scheduling->v.str : NULL;
|
|
if (s != NULL && s->len == 4 && memcmp(s->data, "fifo", 4) == 0) fifo = true;
|
|
else if (s == NULL || s->len != 4 || memcmp(s->data, "lifo", 4) != 0) {
|
|
scr_dyn_arg_value_fail("scheduling", "must be one of: 'fifo', 'lifo'", scheduling);
|
|
return NULL;
|
|
}
|
|
}
|
|
ScrHttpAgent *a = calloc(1, sizeof *a);
|
|
if (!a) scr_http_oom();
|
|
a->rc = 1;
|
|
a->sock_count = calloc(1, sizeof *a->sock_count);
|
|
if (!a->sock_count) scr_http_oom();
|
|
a->sock_count->rc = 1;
|
|
a->secure = secure;
|
|
a->keep_alive = keep_alive;
|
|
a->fifo = fifo;
|
|
a->ka_msecs = ka_msecs >= 0 ? ka_msecs : 1000;
|
|
a->max_sockets = max_sockets >= 0 ? max_sockets : (double)INFINITY;
|
|
a->max_free = max_free >= 0 ? max_free : 256;
|
|
a->timeout_ms = timeout_ms;
|
|
/* an agent-options `port` merges under portless request options (Node
|
|
* merges agent options into each connection) — the settable
|
|
* defaultPort carries it */
|
|
a->default_port = port >= 0 ? port : secure ? 443 : 80;
|
|
/* registry (+1): the atexit sweep breaks agent↔client edges a program
|
|
* legitimately leaves in flight */
|
|
a->in_registry = true;
|
|
a->next = scr_http_agents;
|
|
scr_http_agents = scr_http_agent_retain(a);
|
|
ScrDyn *d = scr_dyn_new_handle(a, SCR_DYNH_HTTP_AGENT);
|
|
scr_http_agent_release(a); /* the handle's retain holds it */
|
|
return d;
|
|
}
|
|
|
|
/* http.globalAgent / https.globalAgent: Node's
|
|
* new Agent({ keepAlive: true, scheduling: 'lifo', timeout: 5000 }),
|
|
* created on first use. Borrowed (the thread keeps it until exit). */
|
|
static SCR_TL ScrDyn *scr_http_global_agents[2];
|
|
|
|
const ScrDyn *scr_http_global_agent(bool secure) {
|
|
ScrDyn **slot = &scr_http_global_agents[secure ? 1 : 0];
|
|
if (*slot == NULL) *slot = scr_http_agent_new(secure, true, -1, -1, -1, 5000, -1, NULL);
|
|
return *slot;
|
|
}
|
|
|
|
/* http.globalAgent as a value (+1). */
|
|
ScrDyn *scr_http_global_agent_value(bool secure) {
|
|
return scr_dyn_retain((ScrDyn *)scr_http_global_agent(secure));
|
|
}
|
|
|
|
/* agent.destroy(): tears down every listed connection (actives destroy
|
|
* their sockets, queued dials never start) — Node destroys in-use
|
|
* sockets too; there is no free pool here. */
|
|
static void scr_http_agent_destroy(ScrHttpAgent *a) {
|
|
a->destroyed = true;
|
|
/* idle pooled sockets close; their 'close' removes each from the list */
|
|
while (a->free_socks) {
|
|
ScrNetSocket *sock = scr_net_sock_retain(a->free_socks->sock);
|
|
struct ScrHttpConn *conn = a->free_socks->conn;
|
|
scr_http_agent_free_remove(a, conn);
|
|
scr_net_sock_destroy(sock);
|
|
scr_net_sock_release(sock);
|
|
}
|
|
/* destroying sockets detaches entries re-entrantly — walk a snapshot */
|
|
for (;;) {
|
|
ScrHttpClientReq *victim = NULL;
|
|
for (ScrHttpAgentEnt *e = a->ents; e; e = e->next) {
|
|
if (e->client->sock && !e->client->close_emitted) {
|
|
victim = e->client;
|
|
break;
|
|
}
|
|
}
|
|
if (!victim) break;
|
|
scr_http_client_agent_detach(victim); /* leaves the list first */
|
|
scr_net_sock_destroy(victim->sock);
|
|
}
|
|
}
|
|
|
|
/* The socketPath option of the next request built through
|
|
* scr_http_request_agent_ex (borrowed; the caller keeps it alive across
|
|
* the call), like https's servername: that request dials the Unix socket
|
|
* instead of host:port, and the agent keys its sockets by the path, as
|
|
* Node's getName does. */
|
|
static SCR_TL ScrStr *scr_http_next_socket_path = NULL;
|
|
/* The Unix-socket dialer, set by the runtime options path that sets the
|
|
* path; a pointer (not static) so ordinary requests do not link the
|
|
* Unix-socket half of the net unit. */
|
|
extern SCR_TL ScrNetSocket *(*scr_http_socket_dialer)(ScrStr *path, bool deferred);
|
|
SCR_TL ScrNetSocket *(*scr_http_socket_dialer)(ScrStr *path, bool deferred) = NULL;
|
|
|
|
ScrHttpClientReq *scr_http_request_agent_ex(ScrStr *host /*borrowed*/, double port,
|
|
ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
|
|
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
|
|
bool auto_end, const ScrDyn *agent /*borrowed*/,
|
|
ScrClosure *cb /*moves, nullable*/,
|
|
ScrHttpRespFn fn, int default_port,
|
|
void (*wrap)(ScrNetSocket *, void *), void *wrap_ctx,
|
|
ScrStr *pool_key /*borrowed, nullable*/) {
|
|
ScrStr *socket_path = scr_http_next_socket_path;
|
|
scr_http_next_socket_path = NULL;
|
|
/* the null/undefined arms: the scheme's global agent, like Node */
|
|
if (agent == NULL || agent->kind == SCR_DYN_UNDEF || agent->kind == SCR_DYN_NULL)
|
|
agent = scr_http_global_agent(default_port == 443);
|
|
/* agent: false — Node's one-shot Agent: exactly this client's model,
|
|
* plus its Connection: close request header (unless the caller set one) */
|
|
if (agent->kind == SCR_DYN_BOOL && !agent->v.b) {
|
|
double p = port >= 0 ? port : (double)default_port;
|
|
bool has_conn = false;
|
|
size_t npairs = (size_t)scr_arr_len(header_pairs);
|
|
for (size_t i = 0; i + 1 < npairs && !has_conn; i += 2) {
|
|
ScrStr *n = (ScrStr *)scr_arr_get_ref(header_pairs, (double)i);
|
|
if (n->len == 10) {
|
|
bool eq = true;
|
|
for (size_t j = 0; j < 10 && eq; j++) {
|
|
if (tolower((unsigned char)n->data[j]) != "connection"[j]) eq = false;
|
|
}
|
|
has_conn = eq;
|
|
}
|
|
scr_str_release(n);
|
|
}
|
|
ScrArr *pairs = header_pairs;
|
|
ScrArr *copy = NULL;
|
|
if (!has_conn) {
|
|
copy = scr_arr_new(SCR_ELEM_STR, npairs + 2);
|
|
for (size_t i = 0; i < npairs; i++) scr_arr_push_ref(copy, scr_arr_get_ref(header_pairs, (double)i));
|
|
scr_arr_push_ref(copy, scr_str_new("Connection", 10));
|
|
scr_arr_push_ref(copy, scr_str_new("close", 5));
|
|
pairs = copy;
|
|
}
|
|
ScrNetSocket *presock = socket_path != NULL ? scr_http_socket_dialer(socket_path, false) : NULL;
|
|
ScrHttpClientReq *c = scr_http_request_impl(host, p, path, method, timeout_ms, pairs,
|
|
auto_end, cb, fn, default_port, wrap, wrap_ctx,
|
|
presock);
|
|
if (copy) scr_arr_release(copy);
|
|
return c;
|
|
}
|
|
if (agent->kind != SCR_DYN_HANDLE || agent->v.handle.tag != SCR_DYNH_HTTP_AGENT) {
|
|
if (cb) scr_closure_release(cb);
|
|
scr_dyn_arg_type_fail("options.agent", "an instance of http.Agent, false, null, or undefined", agent);
|
|
return NULL;
|
|
}
|
|
ScrHttpAgent *ag = (ScrHttpAgent *)agent->v.handle.ptr;
|
|
double p = port >= 0 ? port
|
|
: ag->default_port > 0 ? ag->default_port
|
|
: (double)default_port;
|
|
char portbuf[16];
|
|
int portn = snprintf(portbuf, sizeof portbuf, "%d", (int)p);
|
|
ScrStr *name = scr_http_agent_name(host->data, host->len, portbuf, (size_t)portn,
|
|
NULL, 0, 0, socket_path ? socket_path->data : NULL,
|
|
socket_path ? socket_path->len : 0);
|
|
/* a free pooled socket for the name serves the request right away */
|
|
ScrHttpAgentFree *reuse = ag->keep_alive ? scr_http_agent_free_take(ag, name, pool_key) : NULL;
|
|
bool queue = reuse == NULL && scr_http_agent_active(ag, name) >= ag->max_sockets;
|
|
ScrNetSocket *presock = socket_path != NULL ? scr_http_socket_dialer(socket_path, queue || reuse)
|
|
: queue || reuse ? scr_net_connect_deferred(p, host)
|
|
: NULL;
|
|
ScrHttpClientReq *c = scr_http_request_impl(host, p, path, method, timeout_ms, header_pairs,
|
|
auto_end, cb, fn, default_port, wrap, wrap_ctx,
|
|
presock);
|
|
if (c == NULL) { /* the method-token throw: nothing registered */
|
|
if (reuse != NULL) scr_http_agent_free_untake(ag, reuse); /* back to the list it came from */
|
|
scr_str_release(name);
|
|
return NULL;
|
|
}
|
|
c->pool_key = pool_key ? scr_str_retain(pool_key) : NULL;
|
|
if (ag->timeout_ms >= 0 && timeout_ms <= 0) {
|
|
c->timeout_ms = ag->timeout_ms;
|
|
scr_net_sock_set_timeout(c->sock, ag->timeout_ms);
|
|
}
|
|
if (reuse != NULL) scr_http_client_rebind(c, reuse, true);
|
|
else if (!queue) scr_http_conn_count(c->conn, ag->sock_count); /* dialed now */
|
|
c->agent = scr_http_agent_retain(ag);
|
|
ScrHttpAgentEnt *e = calloc(1, sizeof *e);
|
|
if (!e) scr_http_oom();
|
|
e->name = name; /* ownership moves */
|
|
e->client = scr_http_client_retain(c);
|
|
e->queued = queue;
|
|
ScrHttpAgentEnt **link = &ag->ents;
|
|
while (*link) link = &(*link)->next;
|
|
*link = e;
|
|
return c;
|
|
}
|
|
|
|
ScrHttpClientReq *scr_http_request_agent(ScrStr *host /*borrowed*/, double port,
|
|
ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
|
|
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
|
|
bool auto_end, const ScrDyn *agent /*borrowed*/,
|
|
ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn) {
|
|
return scr_http_request_agent_ex(host, port, path, method, timeout_ms, header_pairs, auto_end,
|
|
agent, cb, fn, 80, NULL, NULL, NULL);
|
|
}
|
|
|
|
/* The atexit sweep: break agent↔client edges a program leaves in flight
|
|
* (queued dials that never ran) so the RC audit sees a clean heap. */
|
|
static void scr_http_agents_cleanup(void) {
|
|
for (size_t i = 0; i < 2; i++) {
|
|
scr_dyn_release(scr_http_global_agents[i]);
|
|
scr_http_global_agents[i] = NULL;
|
|
}
|
|
while (scr_http_agents) {
|
|
ScrHttpAgent *a = scr_http_agents;
|
|
scr_http_agents = a->next;
|
|
a->in_registry = false;
|
|
ScrHttpAgentEnt *e = a->ents;
|
|
a->ents = NULL;
|
|
while (e) {
|
|
ScrHttpAgentEnt *next = e->next;
|
|
if (e->client->agent) {
|
|
scr_http_agent_release(e->client->agent);
|
|
e->client->agent = NULL;
|
|
}
|
|
scr_str_release(e->name);
|
|
scr_http_client_release(e->client);
|
|
free(e);
|
|
e = next;
|
|
}
|
|
scr_http_agent_release(a);
|
|
}
|
|
}
|
|
|
|
/* Exit-time registry cleanup (the net-unit precedent): clients a program
|
|
* legitimately leaves in flight at exit release their listeners and
|
|
* registry references so the RC audit sees a clean heap. */
|
|
static void scr_http_clients_cleanup(void) {
|
|
while (scr_http_clients) {
|
|
ScrHttpClientReq *c = scr_http_clients;
|
|
scr_net_ls_drop(&c->resp_ls);
|
|
scr_net_ls_drop(&c->err_ls);
|
|
scr_net_ls_drop(&c->timeout_ls);
|
|
scr_net_ls_drop(&c->close_ls);
|
|
scr_net_ls_drop(&c->upgrade_ls);
|
|
scr_http_client_unregister(c);
|
|
}
|
|
}
|
|
|
|
/* The response-callback adapters: res arrives +1; the zero-param shape
|
|
* releases it. */
|
|
void scr_http_resp_thunk0(ScrClosure *cb, ScrHttpReq *res) {
|
|
scr_http_req_release(res);
|
|
((void (*)(ScrClosure *))cb->fn)(cb);
|
|
}
|
|
|
|
void scr_http_resp_thunk_res(ScrClosure *cb, ScrHttpReq *res) {
|
|
((void (*)(ScrClosure *, ScrHttpReq *))cb->fn)(cb, res);
|
|
}
|
|
|
|
/* ── checked-dynamic handle dispatch (SCR_DYN_HANDLE ops) ──────────────
|
|
*
|
|
* The receiver surface behind `server.on('request', mustCall((req, res)
|
|
* => ...))`: the mustCall wrapper makes the listener dyn, req/res box
|
|
* into the checked-dynamic tree as HANDLE values, and member uses inside the listener
|
|
* body dispatch HERE — onto the very entry points the static lowerings
|
|
* use, with per-argument gates shaped like Node's (the libCall signature
|
|
* table in validate.ts is the modeled surface; this dispatcher mirrors
|
|
* it name for name).
|
|
*
|
|
* Honesty ladder (the scr_dyn_invoke stance):
|
|
* - modeled member: the static entry point's exact semantics;
|
|
* - a member the class HAS but this table does not model: a
|
|
* "not supported yet" Error instead of a wrong result;
|
|
* - anything else: Node's "<spelling> is not a function" for calls,
|
|
* the undefined singleton for reads (the checked-dynamic tree's own-property answer;
|
|
* SEMANTICS.md documents the remainder).
|
|
*
|
|
* Ownership: invoke/get/set receive BORROWED handles/args and answer
|
|
* owned (+1) results; listener registrations retain the dyn callback
|
|
* into a runtime-built adapter closure (scr_dyn_listener_closure*). */
|
|
|
|
static void scr_http_dynh_unsupported(const char *cls, const char *member, const char *what) {
|
|
char msg[160];
|
|
int n = snprintf(msg, sizeof msg, "'%s.prototype.%s' on a dynamic value is not supported yet%s%s",
|
|
cls, member, what ? " — " : "", what ? what : "");
|
|
scr_throw_error_msg(SCR_ERR_ERROR, msg, (size_t)n);
|
|
}
|
|
|
|
static void scr_http_dynh_not_fn(const char *what) {
|
|
char msg[160];
|
|
int n = snprintf(msg, sizeof msg, "%s is not a function", what);
|
|
scr_throw_error_msg(SCR_ERR_TYPE, msg, (size_t)n);
|
|
}
|
|
|
|
static void scr_http_dynh_event_unsupported(const char *cls, const ScrDyn *name) {
|
|
ScrJsonBuf b;
|
|
scr_jb_init(&b);
|
|
scr_jb_puts(&b, "listening for '");
|
|
if (name->kind == SCR_DYN_STR) {
|
|
scr_jb_write(&b, name->v.str->data, name->v.str->len);
|
|
}
|
|
scr_jb_puts(&b, "' on a dynamic ");
|
|
scr_jb_puts(&b, cls);
|
|
scr_jb_puts(&b, " is not supported yet");
|
|
scr_throw_error(SCR_ERR_ERROR, scr_jb_finish(&b));
|
|
}
|
|
|
|
static bool scr_http_dynh_name_is(const ScrDyn *name, const char *lit) {
|
|
size_t n = strlen(lit);
|
|
return name->kind == SCR_DYN_STR && name->v.str->len == n &&
|
|
memcmp(name->v.str->data, lit, n) == 0;
|
|
}
|
|
|
|
static bool scr_http_dynh_in(const char *m, const char *const *names) {
|
|
for (size_t i = 0; names[i]; i++) {
|
|
if (strcmp(m, names[i]) == 0) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* The registration-family gate shared by every handle class: on/once/
|
|
* addListener (prepend* is unmodeled — these per-event lists know no
|
|
* front). Answers the once flag; leaves reg=false for other names. */
|
|
static bool scr_http_dynh_reg(const char *method, bool *once) {
|
|
if (strcmp(method, "on") == 0 || strcmp(method, "addListener") == 0) {
|
|
*once = false;
|
|
return true;
|
|
}
|
|
if (strcmp(method, "once") == 0) {
|
|
*once = true;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* String/Buffer chunk gate (res.end/res.write/socket.write): answers
|
|
* which flavor, throws Node's ERR_INVALID_ARG_TYPE otherwise. */
|
|
static bool scr_http_dynh_chunk_ok(const ScrDyn *chunk) {
|
|
if (chunk->kind == SCR_DYN_STR || chunk->kind == SCR_DYN_BYTES) return true;
|
|
scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", chunk);
|
|
return false;
|
|
}
|
|
|
|
/* An optional trailing encoding argument on write/end: utf8 spellings
|
|
* pass through (string chunks already carry utf8 bytes); every other
|
|
* real Node encoding decodes the STRING chunk through Buffer.from's
|
|
* decoder into *out (+1; buffer chunks ignore the encoding, Node);
|
|
* unknown names throw Node's ERR_UNKNOWN_ENCODING. False = exception
|
|
* pending. */
|
|
static bool scr_http_dynh_encode(const ScrDyn *chunk, const ScrDyn *enc, ScrBytes **out) {
|
|
*out = NULL;
|
|
if (enc->kind != SCR_DYN_STR || !scr_bytes_is_encoding(enc->v.str)) {
|
|
ScrJsonBuf b;
|
|
scr_jb_init(&b);
|
|
scr_jb_puts(&b, "Unknown encoding: ");
|
|
if (enc->kind == SCR_DYN_STR) {
|
|
for (size_t i = 0; i < enc->v.str->len && i < 64; i++) scr_jb_putc(&b, enc->v.str->data[i]);
|
|
}
|
|
ScrStr *msg = scr_jb_finish(&b);
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, msg->data, msg->len, "ERR_UNKNOWN_ENCODING");
|
|
scr_str_release(msg);
|
|
return false;
|
|
}
|
|
if (chunk->kind != SCR_DYN_STR) return true; /* buffers carry their bytes */
|
|
/* lowercase + alias normalization down to scr_bytes_from_str's arms */
|
|
char low[10];
|
|
size_t n = enc->v.str->len < 9 ? enc->v.str->len : 9;
|
|
for (size_t i = 0; i < n; i++) {
|
|
char c = enc->v.str->data[i];
|
|
low[i] = c >= 'A' && c <= 'Z' ? (char)(c + 32) : c;
|
|
}
|
|
low[n] = 0;
|
|
const char *canon = low;
|
|
if (strcmp(low, "utf8") == 0 || strcmp(low, "utf-8") == 0) return true; /* passthrough */
|
|
if (strcmp(low, "binary") == 0) canon = "latin1";
|
|
else if (strcmp(low, "ucs2") == 0 || strcmp(low, "ucs-2") == 0 || strcmp(low, "utf-16le") == 0) canon = "utf16le";
|
|
ScrStr *cs = scr_str_new(canon, strlen(canon));
|
|
*out = scr_bytes_from_str(chunk->v.str, cs);
|
|
scr_str_release(cs);
|
|
return true;
|
|
}
|
|
|
|
/* ── IncomingMessage (SCR_DYNH_HTTP_REQ) ─────────────────────────────── */
|
|
|
|
static ScrDyn *scr_http_dynh_req_invoke(void *h, ScrDyn *self, const char *method,
|
|
ScrDyn *const *args, size_t argc, const char *what) {
|
|
ScrHttpReq *r = (ScrHttpReq *)h;
|
|
bool once = false;
|
|
if (scr_http_dynh_reg(method, &once)) {
|
|
const ScrDyn *name = argc > 0 ? args[0] : scr_dyn_undefined();
|
|
const ScrDyn *cb = argc > 1 ? args[1] : scr_dyn_undefined();
|
|
scr_dyn_check_listener(cb, "listener");
|
|
if (scr_exc_pending()) return NULL;
|
|
if (scr_http_dynh_name_is(name, "data")) {
|
|
scr_http_req_on_data(r, scr_dyn_listener_closure_data(cb), (ScrNetDataFn)&scr_dyn_listener_fire_data, once);
|
|
} else if (scr_http_dynh_name_is(name, "end")) {
|
|
scr_http_req_on_end(r, scr_dyn_listener_closure0(cb), once);
|
|
} else if (scr_http_dynh_name_is(name, "error")) {
|
|
scr_http_req_on_error(r, scr_dyn_listener_closure_err(cb), (ScrChildErrFn)&scr_dyn_listener_fire_err, once);
|
|
} else if (scr_http_dynh_name_is(name, "close")) {
|
|
scr_http_req_on_close(r, scr_dyn_listener_closure0(cb), once);
|
|
} else if (scr_http_dynh_name_is(name, "aborted")) {
|
|
scr_http_req_on_aborted(r, scr_dyn_listener_closure0(cb), once);
|
|
} else if (scr_http_dynh_name_is(name, "timeout") && r->sock != NULL) {
|
|
/* the socket's idle timer — req.setTimeout's event, Node's delegation */
|
|
scr_net_sock_on_timeout(r->sock, scr_dyn_listener_closure0(cb), once);
|
|
} else {
|
|
scr_http_dynh_event_unsupported("IncomingMessage", name);
|
|
return NULL;
|
|
}
|
|
return scr_dyn_retain(self); /* chaining, like Node */
|
|
}
|
|
if (strcmp(method, "resume") == 0) {
|
|
scr_http_req_resume(r);
|
|
return scr_dyn_retain(self);
|
|
}
|
|
if (strcmp(method, "pause") == 0) {
|
|
scr_http_req_pause(r);
|
|
return scr_dyn_retain(self);
|
|
}
|
|
if (strcmp(method, "setTimeout") == 0) {
|
|
const ScrDyn *ms = argc > 0 ? args[0] : scr_dyn_undefined();
|
|
if (ms->kind != SCR_DYN_NUM) {
|
|
scr_dyn_arg_type_fail("msecs", "of type number", ms);
|
|
return NULL;
|
|
}
|
|
ScrClosure *cb = NULL;
|
|
if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) cb = scr_dyn_listener_closure0(args[1]);
|
|
else if (argc > 1 && args[1]->kind != SCR_DYN_UNDEF) {
|
|
scr_dyn_check_listener(args[1], "callback");
|
|
return NULL;
|
|
}
|
|
scr_http_req_set_timeout(r, ms->v.num, cb);
|
|
return scr_dyn_retain(self);
|
|
}
|
|
if (strcmp(method, "setEncoding") == 0) {
|
|
const ScrDyn *enc = argc > 0 ? args[0] : scr_dyn_undefined();
|
|
if (enc->kind != SCR_DYN_STR) {
|
|
scr_dyn_arg_type_fail("encoding", "of type string", enc);
|
|
return NULL;
|
|
}
|
|
scr_http_req_set_encoding(r, enc->v.str);
|
|
if (scr_exc_pending()) return NULL;
|
|
return scr_dyn_retain(self);
|
|
}
|
|
if (strcmp(method, "destroy") == 0) {
|
|
if (argc > 0 && args[0]->kind != SCR_DYN_UNDEF) {
|
|
scr_http_dynh_unsupported("IncomingMessage", "destroy", "destroy(error) carries a payload this surface does not model");
|
|
return NULL;
|
|
}
|
|
scr_http_req_destroy(r);
|
|
return scr_dyn_retain(self);
|
|
}
|
|
if (strcmp(method, "pipe") == 0) {
|
|
const ScrDyn *dst = argc > 0 ? args[0] : scr_dyn_undefined();
|
|
if (dst->kind == SCR_DYN_HANDLE && dst->v.handle.tag == SCR_DYNH_HTTP_RES) {
|
|
scr_http_req_pipe_res(r, (ScrHttpRes *)dst->v.handle.ptr);
|
|
return scr_dyn_retain((ScrDyn *)dst); /* pipe answers the destination */
|
|
}
|
|
if (dst->kind == SCR_DYN_HANDLE && dst->v.handle.tag == SCR_DYNH_NET_SOCKET) {
|
|
scr_http_req_pipe_sock(r, (ScrNetSocket *)dst->v.handle.ptr);
|
|
return scr_dyn_retain((ScrDyn *)dst);
|
|
}
|
|
scr_http_dynh_unsupported("IncomingMessage", "pipe", "only ServerResponse and Socket destinations are modeled");
|
|
return NULL;
|
|
}
|
|
if (strcmp(method, "toString") == 0) {
|
|
ScrStr *s = scr_str_new("[object Object]", 15);
|
|
ScrDyn *d = scr_dyn_new_str(s);
|
|
scr_str_release(s);
|
|
return d;
|
|
}
|
|
{
|
|
/* Real prototype members without a modeled dispatch: "not supported yet". */
|
|
static const char *const known[] = { "unpipe",
|
|
"read", "push", "off", "removeListener", "removeAllListeners", "emit",
|
|
"prependListener", "prependOnceListener", "listenerCount", "listeners",
|
|
"isPaused", "wrap", "cork", "uncork", NULL };
|
|
if (scr_http_dynh_in(method, known)) {
|
|
scr_http_dynh_unsupported("IncomingMessage", method, NULL);
|
|
return NULL;
|
|
}
|
|
}
|
|
scr_http_dynh_not_fn(what);
|
|
return NULL;
|
|
}
|
|
|
|
static ScrDyn *scr_http_dynh_req_get(void *h, const char *key, size_t key_len) {
|
|
ScrHttpReq *r = (ScrHttpReq *)h;
|
|
(void)key_len;
|
|
if (strcmp(key, "url") == 0) {
|
|
ScrStr *s = scr_http_req_url(r);
|
|
ScrDyn *d = scr_dyn_new_str(s);
|
|
scr_str_release(s);
|
|
return d;
|
|
}
|
|
if (strcmp(key, "httpVersion") == 0) {
|
|
/* The parsed request/status line's version — the 1.0 tests read it;
|
|
* the h2 compat request answers Node's "2.0". */
|
|
ScrStr *v = scr_str_new(r->http2 ? "2.0" : r->http10 ? "1.0" : "1.1", 3);
|
|
ScrDyn *d = scr_dyn_new_str(v);
|
|
scr_str_release(v);
|
|
return d;
|
|
}
|
|
if (strcmp(key, "httpVersionMajor") == 0) return scr_dyn_new_num(r->http2 ? 2 : 1);
|
|
if (strcmp(key, "httpVersionMinor") == 0) return scr_dyn_new_num(r->http2 ? 0 : r->http10 ? 0 : 1);
|
|
if (strcmp(key, "method") == 0) {
|
|
ScrStr *s = scr_http_req_method(r);
|
|
ScrDyn *d = scr_dyn_new_str(s);
|
|
scr_str_release(s);
|
|
return d;
|
|
}
|
|
if (strcmp(key, "headers") == 0) {
|
|
/* The snapshot object `{ ...req.headers }` builds — lowercased
|
|
* names, arrival order, keep-first duplicates (joined under the
|
|
* joinDuplicateHeaders option), the static lane's exact feed. */
|
|
ScrArr *pairs = scr_http_req_header_pairs(r);
|
|
ScrDyn *obj = scr_dyn_new_obj();
|
|
size_t n = (size_t)scr_arr_len(pairs);
|
|
for (size_t i = 0; i + 1 < n; i += 2) {
|
|
ScrStr *name = (ScrStr *)scr_arr_get_ref(pairs, (double)i);
|
|
ScrStr *value = (ScrStr *)scr_arr_get_ref(pairs, (double)(i + 1));
|
|
/* Keep-FIRST on repeated names (scr_dyn_obj_set is later-wins) —
|
|
* the per-name read's rule (scr_http_req_header), so h.headers.x
|
|
* and h.headers['x'] agree; joinDuplicateHeaders pairs arrive
|
|
* pre-joined and unique. */
|
|
if (!scr_dyn_obj_get(obj, name->data, name->len)) {
|
|
scr_dyn_obj_set(obj, name->data, name->len, scr_dyn_new_str(value));
|
|
}
|
|
scr_str_release(name);
|
|
scr_str_release(value);
|
|
}
|
|
scr_arr_release(pairs);
|
|
return obj;
|
|
}
|
|
if (strcmp(key, "rawHeaders") == 0) {
|
|
ScrArr *raw = scr_http_req_raw_headers(r);
|
|
ScrDyn *arr = scr_dyn_new_arr();
|
|
size_t n = (size_t)scr_arr_len(raw);
|
|
for (size_t i = 0; i < n; i++) {
|
|
ScrStr *s = (ScrStr *)scr_arr_get_ref(raw, (double)i);
|
|
scr_dyn_arr_push(arr, scr_dyn_new_str(s));
|
|
scr_str_release(s);
|
|
}
|
|
scr_arr_release(raw);
|
|
return arr;
|
|
}
|
|
if (strcmp(key, "statusCode") == 0) {
|
|
double st = scr_http_req_status(r);
|
|
/* Node's IncomingMessage constructor sets statusCode = null; only a
|
|
* client response assigns a number (the static lane's typed surface
|
|
* spells this number|undefined — the checked-dynamic tree can afford Node's null). */
|
|
return st < 0 ? scr_dyn_new_null() : scr_dyn_new_num(st);
|
|
}
|
|
if (strcmp(key, "statusMessage") == 0) {
|
|
ScrStr *s = scr_http_req_status_message(r);
|
|
if (!s) return scr_dyn_new_null(); /* null on server requests, like statusCode */
|
|
ScrDyn *d = scr_dyn_new_str(s);
|
|
scr_str_release(s);
|
|
return d;
|
|
}
|
|
if (strcmp(key, "socket") == 0 || strcmp(key, "connection") == 0) {
|
|
ScrNetSocket *s = scr_http_req_socket(r);
|
|
if (!s) return NULL;
|
|
ScrDyn *d = scr_dyn_new_handle(s, SCR_DYNH_NET_SOCKET);
|
|
scr_net_sock_release(s);
|
|
return d;
|
|
}
|
|
if (strcmp(key, "aborted") == 0) return scr_dyn_new_bool(r->aborted);
|
|
if (strcmp(key, "complete") == 0) return scr_dyn_new_bool(r->ended);
|
|
if (strcmp(key, "destroyed") == 0) return scr_dyn_new_bool(scr_http_req_destroyed_flag(r));
|
|
if (strcmp(key, "readable") == 0) return scr_dyn_new_bool(scr_http_req_readable(r));
|
|
if (strcmp(key, "readableEnded") == 0) return scr_dyn_new_bool(r->ended);
|
|
if (strcmp(key, "closed") == 0) return scr_dyn_new_bool(r->close_emitted);
|
|
{
|
|
/* Real instance properties without a modeled read throw "not supported
|
|
* yet" instead of answering undefined where Node has a value. */
|
|
static const char *const known[] = {
|
|
"trailers", "rawTrailers", NULL };
|
|
if (scr_http_dynh_in(key, known)) {
|
|
scr_http_dynh_unsupported("IncomingMessage", key, NULL);
|
|
return NULL;
|
|
}
|
|
}
|
|
return NULL; /* unknown key: the undefined singleton (SEMANTICS.md) */
|
|
}
|
|
|
|
static bool scr_http_dynh_req_set(void *h, const char *key, size_t key_len, const ScrDyn *value) {
|
|
(void)h; (void)key; (void)key_len; (void)value;
|
|
return false; /* no modeled writable properties — the caller throws "not supported yet" */
|
|
}
|
|
|
|
/* ── ServerResponse (SCR_DYNH_HTTP_RES) ──────────────────────────────── */
|
|
|
|
/* writeHead's header-object form over a dyn OBJ: flatten into the
|
|
* [k0, v0, k1, v1, ...] pairs the static lowering feeds
|
|
* scr_http_res_write_head_pairs (repeats append — array values expand to
|
|
* consecutive same-name pairs). Numbers format through String(n). NULL =
|
|
* a value this surface cannot honestly carry (exception pending). */
|
|
static ScrArr *scr_http_dynh_pairs(const ScrDyn *headers) {
|
|
ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, headers->v.obj.len * 2);
|
|
for (size_t i = 0; i < headers->v.obj.len; i++) {
|
|
const ScrDynEntry *e = &headers->v.obj.entries[i];
|
|
const ScrDyn *v = e->value;
|
|
size_t reps = v->kind == SCR_DYN_ARR ? v->v.arr.len : 1;
|
|
for (size_t k = 0; k < reps; k++) {
|
|
const ScrDyn *one = v->kind == SCR_DYN_ARR ? v->v.arr.items[k] : v;
|
|
ScrStr *vs;
|
|
if (one->kind == SCR_DYN_STR) {
|
|
vs = scr_str_retain(one->v.str);
|
|
} else if (one->kind == SCR_DYN_NUM) {
|
|
vs = scr_f64_to_scrstr(one->v.num);
|
|
} else {
|
|
ScrJsonBuf b;
|
|
scr_jb_init(&b);
|
|
scr_jb_puts(&b, "Invalid value \"");
|
|
scr_jb_puts(&b, one->kind == SCR_DYN_UNDEF ? "undefined" : "object");
|
|
scr_jb_puts(&b, "\" for header \"");
|
|
for (size_t c = 0; c < e->key_len; c++) scr_jb_putc(&b, e->key[c]);
|
|
scr_jb_putc(&b, '"');
|
|
ScrStr *msg = scr_jb_finish(&b);
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, msg->data, msg->len, "ERR_HTTP_INVALID_HEADER_VALUE");
|
|
scr_str_release(msg);
|
|
scr_arr_release(pairs);
|
|
return NULL;
|
|
}
|
|
scr_arr_push_ref(pairs, scr_str_new(e->key, e->key_len));
|
|
scr_arr_push_ref(pairs, vs);
|
|
}
|
|
}
|
|
return pairs;
|
|
}
|
|
|
|
/* writeHead's RAW-array form over a dyn ARR: [k0, v0, k1, v1, ...]
|
|
* flattens into the same pairs feed; an odd length throws Node's
|
|
* ERR_INVALID_ARG_VALUE; value slots may be arrays (consecutive
|
|
* same-name lines) or numbers (String(n)). NULL = exception pending. */
|
|
static ScrArr *scr_http_dynh_flat_pairs(const ScrDyn *list) {
|
|
size_t n = list->v.arr.len;
|
|
if (n % 2 != 0) {
|
|
static const char msg[] = "The argument 'headers' is invalid.";
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_INVALID_ARG_VALUE");
|
|
return NULL;
|
|
}
|
|
ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, n);
|
|
for (size_t i = 0; i + 1 < n; i += 2) {
|
|
const ScrDyn *k = list->v.arr.items[i];
|
|
const ScrDyn *v = list->v.arr.items[i + 1];
|
|
if (k->kind != SCR_DYN_STR) {
|
|
scr_arr_release(pairs);
|
|
scr_dyn_arg_type_fail("name", "of type string", k);
|
|
return NULL;
|
|
}
|
|
size_t reps = v->kind == SCR_DYN_ARR ? v->v.arr.len : 1;
|
|
for (size_t rep = 0; rep < reps; rep++) {
|
|
const ScrDyn *one = v->kind == SCR_DYN_ARR ? v->v.arr.items[rep] : v;
|
|
ScrStr *vs;
|
|
if (one->kind == SCR_DYN_STR) {
|
|
vs = scr_str_retain(one->v.str);
|
|
} else if (one->kind == SCR_DYN_NUM) {
|
|
vs = scr_f64_to_scrstr(one->v.num);
|
|
} else {
|
|
ScrJsonBuf b;
|
|
scr_jb_init(&b);
|
|
scr_jb_puts(&b, "Invalid value \"");
|
|
scr_jb_puts(&b, one->kind == SCR_DYN_UNDEF ? "undefined" : "object");
|
|
scr_jb_puts(&b, "\" for header \"");
|
|
scr_jb_write(&b, k->v.str->data, k->v.str->len);
|
|
scr_jb_putc(&b, '"');
|
|
ScrStr *msg = scr_jb_finish(&b);
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, msg->data, msg->len, "ERR_HTTP_INVALID_HEADER_VALUE");
|
|
scr_str_release(msg);
|
|
scr_arr_release(pairs);
|
|
return NULL;
|
|
}
|
|
scr_arr_push_ref(pairs, scr_str_new(k->v.str->data, k->v.str->len));
|
|
scr_arr_push_ref(pairs, vs);
|
|
}
|
|
}
|
|
return pairs;
|
|
}
|
|
|
|
static ScrDyn *scr_http_dynh_res_invoke(void *h, ScrDyn *self, const char *method,
|
|
ScrDyn *const *args, size_t argc, const char *what) {
|
|
ScrHttpRes *r = (ScrHttpRes *)h;
|
|
bool once = false;
|
|
if (scr_http_dynh_reg(method, &once)) {
|
|
const ScrDyn *name = argc > 0 ? args[0] : scr_dyn_undefined();
|
|
const ScrDyn *cb = argc > 1 ? args[1] : scr_dyn_undefined();
|
|
scr_dyn_check_listener(cb, "listener");
|
|
if (scr_exc_pending()) return NULL;
|
|
if (scr_http_dynh_name_is(name, "close")) {
|
|
scr_http_res_on_close(r, scr_dyn_listener_closure0(cb), once);
|
|
} else if (scr_http_dynh_name_is(name, "finish")) {
|
|
scr_http_res_on_finish(r, scr_dyn_listener_closure0(cb));
|
|
} else if (scr_http_dynh_name_is(name, "drain")) {
|
|
/* This surface never backpressures (write answers true), so Node's
|
|
* contract says 'drain' never fires — an accepted, never-fired
|
|
* registration is the consistent answer (SEMANTICS.md). */
|
|
} else if (scr_http_dynh_name_is(name, "timeout") && r->sock != NULL) {
|
|
/* the socket's idle timer — res.setTimeout's event, Node's delegation */
|
|
scr_net_sock_on_timeout(r->sock, scr_dyn_listener_closure0(cb), once);
|
|
} else {
|
|
scr_http_dynh_event_unsupported("ServerResponse", name);
|
|
return NULL;
|
|
}
|
|
return scr_dyn_retain(self);
|
|
}
|
|
if (strcmp(method, "end") == 0) {
|
|
/* end() / end(chunk) / end(chunk, enc) / end(cb) / end(chunk, cb) /
|
|
* end(chunk, enc, cb) — Node's whole shape family. */
|
|
size_t i = 0;
|
|
const ScrDyn *chunk = NULL;
|
|
const ScrDyn *cb = NULL;
|
|
ScrBytes *decoded = NULL;
|
|
if (i < argc && args[i]->kind == SCR_DYN_FUNC) {
|
|
cb = args[i++];
|
|
} else if (i < argc && args[i]->kind != SCR_DYN_UNDEF && args[i]->kind != SCR_DYN_NULL) {
|
|
if (!scr_http_dynh_chunk_ok(args[i])) return NULL;
|
|
chunk = args[i++];
|
|
if (i < argc && args[i]->kind == SCR_DYN_STR) { /* encoding */
|
|
if (!scr_http_dynh_encode(chunk, args[i], &decoded)) return NULL;
|
|
i++;
|
|
}
|
|
if (i < argc && args[i]->kind == SCR_DYN_FUNC) cb = args[i++];
|
|
}
|
|
if (i < argc && args[i]->kind != SCR_DYN_UNDEF) {
|
|
scr_bytes_release(decoded);
|
|
scr_http_dynh_unsupported("ServerResponse", "end", "this argument shape is not modeled");
|
|
return NULL;
|
|
}
|
|
if (cb) scr_http_res_on_end_callback(r, scr_dyn_listener_closure0(cb));
|
|
if (decoded != NULL) {
|
|
scr_http_res_end_bytes(r, decoded);
|
|
scr_bytes_release(decoded);
|
|
} else if (chunk == NULL) {
|
|
scr_http_res_end(r);
|
|
} else if (chunk->kind == SCR_DYN_STR) {
|
|
scr_http_res_end_str(r, chunk->v.str);
|
|
} else {
|
|
scr_http_res_end_bytes(r, chunk->v.bytes);
|
|
}
|
|
return scr_dyn_retain(self);
|
|
}
|
|
if (strcmp(method, "write") == 0) {
|
|
const ScrDyn *chunk = argc > 0 ? args[0] : scr_dyn_undefined();
|
|
if (!scr_http_dynh_chunk_ok(chunk)) return NULL;
|
|
ScrBytes *decoded = NULL;
|
|
size_t i = 1;
|
|
if (i < argc && args[i]->kind == SCR_DYN_STR) { /* encoding */
|
|
if (!scr_http_dynh_encode(chunk, args[i], &decoded)) return NULL;
|
|
i++;
|
|
}
|
|
if (i < argc && args[i]->kind == SCR_DYN_FUNC) {
|
|
/* write(chunk[, enc], cb): fires from the queue once the chunk
|
|
* entered the socket buffer — this surface's flush moment */
|
|
scr_http_res_on_write_flush(r, scr_dyn_listener_closure0(args[i]));
|
|
i++;
|
|
}
|
|
if (decoded != NULL) {
|
|
scr_http_res_write_bytes(r, decoded);
|
|
scr_bytes_release(decoded);
|
|
} else if (chunk->kind == SCR_DYN_STR) {
|
|
scr_http_res_write_str(r, chunk->v.str);
|
|
} else {
|
|
scr_http_res_write_bytes(r, chunk->v.bytes);
|
|
}
|
|
/* Always-true: this surface buffers without a highWaterMark verdict
|
|
* (SEMANTICS.md — backpressure is not modeled). */
|
|
return scr_dyn_new_bool(true);
|
|
}
|
|
if (strcmp(method, "flushHeaders") == 0) {
|
|
scr_http_res_flush_headers(r);
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
if (strcmp(method, "cork") == 0) {
|
|
scr_http_res_cork(r);
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
if (strcmp(method, "uncork") == 0) {
|
|
scr_http_res_uncork(r);
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
if (strcmp(method, "setTimeout") == 0) {
|
|
const ScrDyn *ms = argc > 0 ? args[0] : scr_dyn_undefined();
|
|
if (ms->kind != SCR_DYN_NUM) {
|
|
scr_dyn_arg_type_fail("msecs", "of type number", ms);
|
|
return NULL;
|
|
}
|
|
ScrClosure *tcb = NULL;
|
|
if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) tcb = scr_dyn_listener_closure0(args[1]);
|
|
else if (argc > 1 && args[1]->kind != SCR_DYN_UNDEF) {
|
|
scr_dyn_check_listener(args[1], "callback");
|
|
return NULL;
|
|
}
|
|
scr_http_res_set_timeout(r, ms->v.num, tcb);
|
|
return scr_dyn_retain(self);
|
|
}
|
|
if (strcmp(method, "getHeaders") == 0) return scr_http_res_get_headers(r);
|
|
if (strcmp(method, "getHeaderNames") == 0) {
|
|
ScrArr *names = scr_http_res_get_header_names(r);
|
|
ScrDyn *out = scr_dyn_new_arr();
|
|
for (size_t k = 0; k < names->len; k++) {
|
|
ScrStr *n = (ScrStr *)scr_arr_get_ref(names, (double)k);
|
|
scr_dyn_arr_push(out, scr_dyn_new_str(n));
|
|
scr_str_release(n);
|
|
}
|
|
scr_arr_release(names);
|
|
return out;
|
|
}
|
|
if (strcmp(method, "writeHead") == 0) {
|
|
const ScrDyn *st = argc > 0 ? args[0] : scr_dyn_undefined();
|
|
if (st->kind != SCR_DYN_NUM) {
|
|
scr_dyn_arg_type_fail("statusCode", "of type number", st);
|
|
return NULL;
|
|
}
|
|
size_t i = 1;
|
|
if (i < argc && args[i]->kind == SCR_DYN_STR) {
|
|
scr_http_res_status_msg_set(r, args[i]->v.str);
|
|
i++;
|
|
}
|
|
if (i < argc && args[i]->kind == SCR_DYN_OBJ) {
|
|
ScrArr *pairs = scr_http_dynh_pairs(args[i]);
|
|
if (!pairs) return NULL;
|
|
scr_http_res_write_head_pairs(r, st->v.num, pairs);
|
|
scr_arr_release(pairs);
|
|
i++;
|
|
} else if (i < argc && args[i]->kind == SCR_DYN_ARR) {
|
|
/* the RAW-array form: [k0, v0, k1, v1, ...] — an even length is
|
|
* Node's contract (ERR_INVALID_ARG_VALUE otherwise), values may be
|
|
* arrays (consecutive same-name lines, the setHeader expansion),
|
|
* and per NAME the list overrides what setHeader() stored while
|
|
* other names survive — the object form's writeHead-wins merge
|
|
* (oracle-pinned: a res with setHeader('a') plus writeHead(200,
|
|
* ['test', ...]) sends both). */
|
|
ScrArr *pairs = scr_http_dynh_flat_pairs(args[i]);
|
|
if (!pairs) return NULL;
|
|
scr_http_res_write_head_pairs(r, st->v.num, pairs);
|
|
scr_arr_release(pairs);
|
|
i++;
|
|
} else if (i < argc && args[i]->kind != SCR_DYN_UNDEF) {
|
|
scr_http_dynh_unsupported("ServerResponse", "writeHead", "only the header-object and raw-array forms are modeled");
|
|
return NULL;
|
|
} else {
|
|
scr_http_res_write_head(r, st->v.num);
|
|
}
|
|
return scr_dyn_retain(self); /* chaining: writeHead(...).end(...) */
|
|
}
|
|
if (strcmp(method, "setHeader") == 0) {
|
|
const ScrDyn *name = argc > 0 ? args[0] : scr_dyn_undefined();
|
|
const ScrDyn *value = argc > 1 ? args[1] : scr_dyn_undefined();
|
|
if (name->kind != SCR_DYN_STR) {
|
|
scr_dyn_arg_type_fail("name", "of type string", name);
|
|
return NULL;
|
|
}
|
|
scr_http_res_set_header_dyn(r, name->v.str, (ScrDyn *)value);
|
|
if (scr_exc_pending()) return NULL;
|
|
return scr_dyn_retain(self);
|
|
}
|
|
if (strcmp(method, "appendHeader") == 0) {
|
|
const ScrDyn *name = argc > 0 ? args[0] : scr_dyn_undefined();
|
|
const ScrDyn *value = argc > 1 ? args[1] : scr_dyn_undefined();
|
|
if (name->kind != SCR_DYN_STR) {
|
|
scr_dyn_arg_type_fail("name", "of type string", name);
|
|
return NULL;
|
|
}
|
|
scr_http_res_append_header_dyn(r, name->v.str, (ScrDyn *)value);
|
|
if (scr_exc_pending()) return NULL;
|
|
return scr_dyn_retain(self);
|
|
}
|
|
if (strcmp(method, "getHeader") == 0) {
|
|
const ScrDyn *name = argc > 0 ? args[0] : scr_dyn_undefined();
|
|
if (name->kind != SCR_DYN_STR) {
|
|
scr_dyn_arg_type_fail("name", "of type string", name);
|
|
return NULL;
|
|
}
|
|
return scr_http_res_get_header_dyn(r, name->v.str);
|
|
}
|
|
if (strcmp(method, "hasHeader") == 0) {
|
|
const ScrDyn *name = argc > 0 ? args[0] : scr_dyn_undefined();
|
|
if (name->kind != SCR_DYN_STR) {
|
|
scr_dyn_arg_type_fail("name", "of type string", name);
|
|
return NULL;
|
|
}
|
|
return scr_dyn_new_bool(scr_http_res_has_header_named(r, name->v.str));
|
|
}
|
|
if (strcmp(method, "removeHeader") == 0) {
|
|
const ScrDyn *name = argc > 0 ? args[0] : scr_dyn_undefined();
|
|
if (name->kind != SCR_DYN_STR) {
|
|
scr_dyn_arg_type_fail("name", "of type string", name);
|
|
return NULL;
|
|
}
|
|
scr_http_res_remove_header(r, name->v.str);
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
if (strcmp(method, "destroy") == 0) {
|
|
scr_http_res_destroy(r);
|
|
return scr_dyn_retain(self);
|
|
}
|
|
if (strcmp(method, "toString") == 0) {
|
|
ScrStr *s = scr_str_new("[object Object]", 15);
|
|
ScrDyn *d = scr_dyn_new_str(s);
|
|
scr_str_release(s);
|
|
return d;
|
|
}
|
|
{
|
|
static const char *const known[] = { "writeContinue", "writeEarlyHints",
|
|
"addTrailers", "off", "removeListener", "removeAllListeners", "emit",
|
|
"prependListener", "prependOnceListener", "listenerCount", "listeners", "setDefaultEncoding", NULL };
|
|
if (scr_http_dynh_in(method, known)) {
|
|
scr_http_dynh_unsupported("ServerResponse", method, NULL);
|
|
return NULL;
|
|
}
|
|
}
|
|
scr_http_dynh_not_fn(what);
|
|
return NULL;
|
|
}
|
|
|
|
static ScrDyn *scr_http_dynh_res_get(void *h, const char *key, size_t key_len) {
|
|
ScrHttpRes *r = (ScrHttpRes *)h;
|
|
(void)key_len;
|
|
if (strcmp(key, "statusCode") == 0) return scr_dyn_new_num(scr_http_res_status_get(r));
|
|
if (strcmp(key, "statusMessage") == 0) {
|
|
ScrStr *s = scr_http_res_status_msg_get(r);
|
|
if (!s) return scr_dyn_retain(scr_dyn_undefined());
|
|
ScrDyn *d = scr_dyn_new_str(s);
|
|
scr_str_release(s);
|
|
return d;
|
|
}
|
|
if (strcmp(key, "headersSent") == 0) return scr_dyn_new_bool(scr_http_res_headers_sent(r));
|
|
if (strcmp(key, "finished") == 0 || strcmp(key, "writableEnded") == 0) {
|
|
/* One flag serves both (Node splits ended-vs-flushed; this surface
|
|
* flushes synchronously into the socket buffer — SEMANTICS.md). */
|
|
return scr_dyn_new_bool(r->finished);
|
|
}
|
|
if (strcmp(key, "socket") == 0 || strcmp(key, "connection") == 0) {
|
|
ScrNetSocket *sock = scr_http_res_socket(r);
|
|
if (!sock) return scr_dyn_new_null();
|
|
ScrDyn *view = scr_dyn_new_handle(sock, SCR_DYNH_NET_SOCKET);
|
|
scr_net_sock_release(sock);
|
|
return view;
|
|
}
|
|
if (strcmp(key, "req") == 0) {
|
|
if (r->req_cleared || r->req_ref == NULL) return scr_dyn_new_null();
|
|
return scr_dyn_new_handle(r->req_ref, SCR_DYNH_HTTP_REQ);
|
|
}
|
|
if (strcmp(key, "writableCorked") == 0) return scr_dyn_new_num(scr_http_res_writable_corked(r));
|
|
if (strcmp(key, "writableFinished") == 0) return scr_dyn_new_bool(scr_http_res_writable_finished(r));
|
|
if (strcmp(key, "writableHighWaterMark") == 0) {
|
|
/* Node's default socket highWaterMark — a constant here (backpressure
|
|
* is not modeled; SEMANTICS.md) */
|
|
return scr_dyn_new_num(16384);
|
|
}
|
|
if (strcmp(key, "destroyed") == 0) return scr_dyn_new_bool(scr_http_res_destroyed_flag(r));
|
|
if (strcmp(key, "closed") == 0) return scr_dyn_new_bool(r->close_emitted);
|
|
{
|
|
static const char *const known[] = { "chunkedEncoding", "sendDate",
|
|
"strictContentLength", "writableLength",
|
|
"writableObjectMode", "errored", NULL };
|
|
if (scr_http_dynh_in(key, known)) {
|
|
scr_http_dynh_unsupported("ServerResponse", key, NULL);
|
|
return NULL;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static bool scr_http_dynh_res_set(void *h, const char *key, size_t key_len, const ScrDyn *value) {
|
|
ScrHttpRes *r = (ScrHttpRes *)h;
|
|
(void)key_len;
|
|
if (strcmp(key, "statusCode") == 0) {
|
|
if (value->kind != SCR_DYN_NUM) {
|
|
scr_dyn_arg_type_fail("statusCode", "of type number", value);
|
|
return true; /* handled: the exception is pending */
|
|
}
|
|
scr_http_res_status_set(r, value->v.num);
|
|
return true;
|
|
}
|
|
if (strcmp(key, "statusMessage") == 0) {
|
|
if (value->kind != SCR_DYN_STR) {
|
|
scr_dyn_arg_type_fail("statusMessage", "of type string", value);
|
|
return true;
|
|
}
|
|
scr_http_res_status_msg_set(r, value->v.str);
|
|
return true;
|
|
}
|
|
if (strcmp(key, "sendDate") == 0) {
|
|
/* Node's boolean flag over the implicit Date header (ToBoolean, like
|
|
* the property's own coercion). */
|
|
r->no_date = !scr_dyn_truthy(value);
|
|
return true;
|
|
}
|
|
if (strcmp(key, "req") == 0 &&
|
|
(value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_UNDEF)) {
|
|
/* res.req is a plain property in Node — a null/undefined write
|
|
* clears the read; other writes raise the named fence (the handle
|
|
* cannot carry arbitrary values). */
|
|
r->req_cleared = true;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* Pipe DESTINATION hook: `req.pipe(res)` dispatches inside this unit,
|
|
* but `socket.pipe(res)` arrives from scr_net.c, which cannot name this
|
|
* unit's entry points — the destination accepts the source here. */
|
|
static bool scr_http_dynh_res_pipe_from(void *dst, const ScrDyn *src) {
|
|
ScrHttpRes *r = (ScrHttpRes *)dst;
|
|
if (src->kind == SCR_DYN_HANDLE && src->v.handle.tag == SCR_DYNH_NET_SOCKET) {
|
|
scr_http_sock_pipe_res((ScrNetSocket *)src->v.handle.ptr, r);
|
|
return true;
|
|
}
|
|
if (src->kind == SCR_DYN_HANDLE && src->v.handle.tag == SCR_DYNH_HTTP_REQ) {
|
|
scr_http_req_pipe_res((ScrHttpReq *)src->v.handle.ptr, r);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static const ScrDynHandleOps scr_http_dynh_req_ops = {
|
|
"IncomingMessage",
|
|
&scr_http_req_retain_v,
|
|
&scr_http_req_release_v,
|
|
&scr_http_dynh_req_invoke,
|
|
&scr_http_dynh_req_get,
|
|
&scr_http_dynh_req_set,
|
|
NULL,
|
|
};
|
|
|
|
static const ScrDynHandleOps scr_http_dynh_res_ops = {
|
|
"ServerResponse",
|
|
&scr_http_res_retain_v,
|
|
&scr_http_res_release_v,
|
|
&scr_http_dynh_res_invoke,
|
|
&scr_http_dynh_res_get,
|
|
&scr_http_dynh_res_set,
|
|
&scr_http_dynh_res_pipe_from,
|
|
};
|
|
|
|
/* The 'request' fire for a DYN listener registered through the netServer
|
|
* handle dispatch (`server.on('request', wrapper)` where `server` lives
|
|
* in a dyn binding): box req/res by reference and call through the
|
|
* checked-dynamic machinery. The ScrHttpReqFn ABI — req/res arrive +1
|
|
* and the adapter owns them, like the compiler-emitted adapters. */
|
|
static void scr_http_dynh_fire_reqres(ScrClosure *cb, ScrHttpReq *req /* +1 */,
|
|
ScrHttpRes *res /* +1 */) {
|
|
ScrDyn *fn = scr_dyn_listener_fn(cb);
|
|
ScrDyn *dreq = scr_dyn_new_handle(req, SCR_DYNH_HTTP_REQ);
|
|
ScrDyn *dres = scr_dyn_new_handle(res, SCR_DYNH_HTTP_RES);
|
|
ScrDyn *args[2] = { dreq, dres };
|
|
ScrDyn *r = scr_dyn_call(fn, args, 2, "listener");
|
|
scr_dyn_release(r);
|
|
scr_dyn_release(dres);
|
|
scr_dyn_release(dreq);
|
|
scr_dyn_release(fn);
|
|
scr_http_req_release(req);
|
|
scr_http_res_release(res);
|
|
}
|
|
|
|
/* The netServer dyn dispatch's http-event hook (scr_net_set_dynh_http_on):
|
|
* 'request' registers; anything else answers false and the net side
|
|
* throws the "not supported yet" Error. */
|
|
static bool scr_http_dynh_server_on(ScrNetServer *s, const char *event, const ScrDyn *cb,
|
|
bool once) {
|
|
if (strcmp(event, "request") == 0) {
|
|
scr_http_server_on_request(s, scr_dyn_listener_closure_fn(cb, (void *)&scr_http_dynh_fire_reqres),
|
|
(ScrHttpReqFn)&scr_http_dynh_fire_reqres, once);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* ── ClientRequest (SCR_DYNH_HTTP_CLIENT) ────────────────────────────── */
|
|
|
|
/* The 'response' fire for a DYN listener (the reqres adapter's shape):
|
|
* res arrives +1, boxes, and the checked-dynamic call runs. */
|
|
static void scr_http_dynh_fire_resp(ScrClosure *cb, ScrHttpReq *res /* +1 */) {
|
|
ScrDyn *fn = scr_dyn_listener_fn(cb);
|
|
ScrDyn *dres = scr_dyn_new_handle(res, SCR_DYNH_HTTP_REQ);
|
|
ScrDyn *args1[1] = { dres };
|
|
ScrDyn *r = scr_dyn_call(fn, args1, 1, "listener");
|
|
scr_dyn_release(r);
|
|
scr_dyn_release(dres);
|
|
scr_dyn_release(fn);
|
|
scr_http_req_release(res);
|
|
}
|
|
|
|
static ScrDyn *scr_http_dynh_client_invoke(void *h, ScrDyn *self, const char *method,
|
|
ScrDyn *const *args, size_t argc, const char *what) {
|
|
ScrHttpClientReq *c = (ScrHttpClientReq *)h;
|
|
bool once = false;
|
|
if (scr_http_dynh_reg(method, &once)) {
|
|
const ScrDyn *name = argc > 0 ? args[0] : scr_dyn_undefined();
|
|
const ScrDyn *cb = argc > 1 ? args[1] : scr_dyn_undefined();
|
|
scr_dyn_check_listener(cb, "listener");
|
|
if (scr_exc_pending()) return NULL;
|
|
if (scr_http_dynh_name_is(name, "response")) {
|
|
scr_http_client_on_response(c, scr_dyn_listener_closure_fn(cb, (void *)&scr_http_dynh_fire_resp),
|
|
(ScrHttpRespFn)&scr_http_dynh_fire_resp, once);
|
|
} else if (scr_http_dynh_name_is(name, "error")) {
|
|
scr_http_client_on_error(c, scr_dyn_listener_closure_err(cb), (ScrChildErrFn)&scr_dyn_listener_fire_err, once);
|
|
} else if (scr_http_dynh_name_is(name, "close")) {
|
|
scr_http_client_on_close(c, scr_dyn_listener_closure0(cb), once);
|
|
} else if (scr_http_dynh_name_is(name, "timeout")) {
|
|
scr_http_client_on_timeout(c, scr_dyn_listener_closure0(cb), once);
|
|
} else {
|
|
scr_http_dynh_event_unsupported("ClientRequest", name);
|
|
return NULL;
|
|
}
|
|
return scr_dyn_retain(self);
|
|
}
|
|
if (strcmp(method, "end") == 0) {
|
|
const ScrDyn *chunk = argc > 0 ? args[0] : scr_dyn_undefined();
|
|
if (chunk->kind == SCR_DYN_STR) scr_http_client_end_str(c, chunk->v.str);
|
|
else if (chunk->kind == SCR_DYN_BYTES) scr_http_client_end_bytes(c, chunk->v.bytes);
|
|
else if (chunk->kind == SCR_DYN_UNDEF || chunk->kind == SCR_DYN_NULL) scr_http_client_end(c);
|
|
else {
|
|
scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", chunk);
|
|
return NULL;
|
|
}
|
|
return scr_dyn_retain(self);
|
|
}
|
|
if (strcmp(method, "write") == 0) {
|
|
const ScrDyn *chunk = argc > 0 ? args[0] : scr_dyn_undefined();
|
|
if (!scr_http_dynh_chunk_ok(chunk)) return NULL;
|
|
if (chunk->kind == SCR_DYN_STR) scr_http_client_write_str(c, chunk->v.str);
|
|
else scr_http_client_write_bytes(c, chunk->v.bytes);
|
|
return scr_dyn_new_bool(true);
|
|
}
|
|
if (strcmp(method, "destroy") == 0) {
|
|
scr_http_client_destroy(c);
|
|
return scr_dyn_retain(self);
|
|
}
|
|
if (strcmp(method, "setTimeout") == 0) {
|
|
const ScrDyn *ms = argc > 0 ? args[0] : scr_dyn_undefined();
|
|
if (ms->kind != SCR_DYN_NUM) {
|
|
scr_dyn_arg_type_fail("msecs", "of type number", ms);
|
|
return NULL;
|
|
}
|
|
scr_http_client_set_timeout(c, ms->v.num);
|
|
if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) {
|
|
scr_http_client_on_timeout(c, scr_dyn_listener_closure0(args[1]), true);
|
|
}
|
|
return scr_dyn_retain(self);
|
|
}
|
|
if (strcmp(method, "flushHeaders") == 0) {
|
|
if (!c->head_sent && !c->destroyed) scr_http_client_send_head(c, -1);
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
if (strcmp(method, "toString") == 0) {
|
|
ScrStr *s = scr_str_new("[object Object]", 15);
|
|
ScrDyn *d = scr_dyn_new_str(s);
|
|
scr_str_release(s);
|
|
return d;
|
|
}
|
|
{
|
|
static const char *const known[] = { "abort", "setNoDelay", "setSocketKeepAlive",
|
|
"getHeader", "setHeader", "removeHeader", "getHeaders", "getHeaderNames", "hasHeader",
|
|
"cork", "uncork", "pipe", "off", "removeListener", "removeAllListeners", "emit",
|
|
"prependListener", "prependOnceListener", "listenerCount", "listeners", NULL };
|
|
if (scr_http_dynh_in(method, known)) {
|
|
scr_http_dynh_unsupported("ClientRequest", method, NULL);
|
|
return NULL;
|
|
}
|
|
}
|
|
scr_http_dynh_not_fn(what);
|
|
return NULL;
|
|
}
|
|
|
|
static ScrDyn *scr_http_dynh_client_get(void *h, const char *key, size_t key_len) {
|
|
ScrHttpClientReq *c = (ScrHttpClientReq *)h;
|
|
(void)key_len;
|
|
if (strcmp(key, "socket") == 0 || strcmp(key, "connection") == 0) {
|
|
if (!c->sock) return scr_dyn_new_null();
|
|
return scr_dyn_new_handle(c->sock, SCR_DYNH_NET_SOCKET);
|
|
}
|
|
if (strcmp(key, "destroyed") == 0) return scr_dyn_new_bool(scr_http_client_destroyed(c));
|
|
if (strcmp(key, "writableEnded") == 0) return scr_dyn_new_bool(c->ended);
|
|
if (strcmp(key, "path") == 0) {
|
|
ScrDyn *d = scr_dyn_new_str(c->path);
|
|
return d;
|
|
}
|
|
if (strcmp(key, "method") == 0) return scr_dyn_new_str(c->method);
|
|
{
|
|
static const char *const known[] = { "aborted", "host", "protocol", "res",
|
|
"reusedSocket", "maxHeadersCount", "headersSent", "writableFinished", NULL };
|
|
if (scr_http_dynh_in(key, known)) {
|
|
scr_http_dynh_unsupported("ClientRequest", key, NULL);
|
|
return NULL;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static bool scr_http_dynh_client_set(void *h, const char *key, size_t key_len, const ScrDyn *value) {
|
|
(void)h; (void)key; (void)key_len; (void)value;
|
|
return false;
|
|
}
|
|
|
|
static const ScrDynHandleOps scr_http_dynh_client_ops = {
|
|
"ClientRequest",
|
|
&scr_http_client_retain_v,
|
|
&scr_http_client_release_v,
|
|
&scr_http_dynh_client_invoke,
|
|
&scr_http_dynh_client_get,
|
|
&scr_http_dynh_client_set,
|
|
NULL,
|
|
};
|
|
|
|
/* ── Agent (SCR_DYNH_HTTP_AGENT) ─────────────────────────────────────── */
|
|
|
|
/* A string-ish option field out of a dyn OBJ: STR answers its bytes,
|
|
* NUM formats (the port), everything else reads as absent. */
|
|
static ScrStr *scr_http_dynh_opt_str(const ScrDyn *opts, const char *key) {
|
|
if (opts == NULL || opts->kind != SCR_DYN_OBJ) return NULL;
|
|
const ScrDyn *v = scr_dyn_obj_get(opts, key, strlen(key));
|
|
if (v == NULL) return NULL;
|
|
if (v->kind == SCR_DYN_STR) return scr_str_retain(v->v.str);
|
|
if (v->kind == SCR_DYN_NUM) return scr_f64_to_scrstr(v->v.num);
|
|
return NULL;
|
|
}
|
|
|
|
static ScrDyn *scr_http_dynh_agent_invoke(void *h, ScrDyn *self, const char *method,
|
|
ScrDyn *const *args, size_t argc, const char *what) {
|
|
ScrHttpAgent *a = (ScrHttpAgent *)h;
|
|
bool once = false;
|
|
if (scr_http_dynh_reg(method, &once)) {
|
|
/* Agent events ('free'/'timeout') ride the pooling the runtime does
|
|
* not have — a named error, never an inert listener. */
|
|
const ScrDyn *name = argc > 0 ? args[0] : scr_dyn_undefined();
|
|
scr_http_dynh_event_unsupported("Agent", name);
|
|
return NULL;
|
|
}
|
|
if (strcmp(method, "getName") == 0) {
|
|
const ScrDyn *opts = argc > 0 ? args[0] : scr_dyn_undefined();
|
|
ScrDyn *view = opts->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(opts) : NULL;
|
|
if (view != NULL) opts = view;
|
|
ScrStr *host = scr_http_dynh_opt_str(opts, "host");
|
|
ScrStr *port = scr_http_dynh_opt_str(opts, "port");
|
|
ScrStr *laddr = scr_http_dynh_opt_str(opts, "localAddress");
|
|
ScrStr *spath = scr_http_dynh_opt_str(opts, "socketPath");
|
|
int family = 0;
|
|
if (opts != NULL && opts->kind == SCR_DYN_OBJ) {
|
|
const ScrDyn *f = scr_dyn_obj_get(opts, "family", 6);
|
|
if (f != NULL && f->kind == SCR_DYN_NUM && (f->v.num == 4 || f->v.num == 6)) {
|
|
family = (int)f->v.num;
|
|
}
|
|
}
|
|
ScrStr *name = scr_http_agent_name(host ? host->data : NULL, host ? host->len : 0,
|
|
port ? port->data : NULL, port ? port->len : 0,
|
|
laddr ? laddr->data : NULL, laddr ? laddr->len : 0,
|
|
family, spath ? spath->data : NULL,
|
|
spath ? spath->len : 0);
|
|
scr_str_release(host);
|
|
scr_str_release(port);
|
|
scr_str_release(laddr);
|
|
scr_str_release(spath);
|
|
scr_dyn_release(view);
|
|
ScrDyn *d = scr_dyn_new_str(name);
|
|
scr_str_release(name);
|
|
return d;
|
|
}
|
|
if (strcmp(method, "destroy") == 0) {
|
|
scr_http_agent_destroy(a);
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
if (strcmp(method, "toString") == 0) {
|
|
ScrStr *s = scr_str_new("[object Object]", 15);
|
|
ScrDyn *d = scr_dyn_new_str(s);
|
|
scr_str_release(s);
|
|
return d;
|
|
}
|
|
{
|
|
static const char *const known[] = { "createConnection", "createSocket", "keepSocketAlive",
|
|
"reuseSocket", "addRequest", "removeSocket", "off", "removeListener",
|
|
"removeAllListeners", "emit", "prependListener", "prependOnceListener",
|
|
"listenerCount", "listeners", NULL };
|
|
if (scr_http_dynh_in(method, known)) {
|
|
scr_http_dynh_unsupported("Agent", method, NULL);
|
|
return NULL;
|
|
}
|
|
}
|
|
scr_http_dynh_not_fn(what);
|
|
(void)self;
|
|
return NULL;
|
|
}
|
|
|
|
/* The sockets/requests snapshots: name → array (fresh objects per read;
|
|
* the tests read lengths, membership, and key presence — empty buckets
|
|
* are OMITTED, Node deletes drained keys). */
|
|
static ScrDyn *scr_http_dynh_agent_table(ScrHttpAgent *a, bool queued) {
|
|
ScrDyn *obj = scr_dyn_new_obj();
|
|
for (ScrHttpAgentEnt *e = a->ents; e; e = e->next) {
|
|
if (e->queued != queued) continue;
|
|
ScrDyn *item = queued
|
|
? scr_dyn_new_handle(e->client, SCR_DYNH_HTTP_CLIENT)
|
|
: e->client->sock ? scr_dyn_new_handle(e->client->sock, SCR_DYNH_NET_SOCKET) : NULL;
|
|
if (item == NULL) continue;
|
|
ScrDyn *arr = scr_dyn_obj_get(obj, e->name->data, e->name->len); /* borrowed */
|
|
if (arr == NULL || arr->kind != SCR_DYN_ARR) {
|
|
ScrDyn *fresh = scr_dyn_new_arr();
|
|
scr_dyn_arr_push(fresh, item); /* moves */
|
|
scr_dyn_obj_set(obj, e->name->data, e->name->len, fresh); /* moves */
|
|
} else {
|
|
scr_dyn_arr_push(arr, item); /* moves */
|
|
}
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
static ScrDyn *scr_http_dynh_agent_get(void *h, const char *key, size_t key_len) {
|
|
ScrHttpAgent *a = (ScrHttpAgent *)h;
|
|
(void)key_len;
|
|
if (strcmp(key, "maxSockets") == 0) return scr_dyn_new_num(a->max_sockets);
|
|
if (strcmp(key, "maxFreeSockets") == 0) return scr_dyn_new_num(a->max_free);
|
|
if (strcmp(key, "keepAlive") == 0) return scr_dyn_new_bool(a->keep_alive);
|
|
if (strcmp(key, "keepAliveMsecs") == 0) return scr_dyn_new_num(a->ka_msecs);
|
|
if (strcmp(key, "scheduling") == 0) {
|
|
ScrStr *s = scr_str_new(a->fifo ? "fifo" : "lifo", 4);
|
|
ScrDyn *d = scr_dyn_new_str(s);
|
|
scr_str_release(s);
|
|
return d;
|
|
}
|
|
if (strcmp(key, "defaultPort") == 0) return scr_dyn_new_num(a->default_port);
|
|
if (strcmp(key, "protocol") == 0) {
|
|
ScrStr *s = scr_str_new(a->secure ? "https:" : "http:", a->secure ? 6 : 5);
|
|
ScrDyn *d = scr_dyn_new_str(s);
|
|
scr_str_release(s);
|
|
return d;
|
|
}
|
|
if (strcmp(key, "sockets") == 0) return scr_http_dynh_agent_table(a, false);
|
|
if (strcmp(key, "requests") == 0) return scr_http_dynh_agent_table(a, true);
|
|
if (strcmp(key, "freeSockets") == 0) {
|
|
/* { name: [socket, ...] }, oldest first like Node's arrays */
|
|
ScrDyn *obj = scr_dyn_new_obj();
|
|
for (ScrHttpAgentFree *f = a->free_socks; f; f = f->next) {
|
|
ScrDyn *arr = scr_dyn_obj_get(obj, f->name->data, f->name->len);
|
|
if (arr == NULL) {
|
|
arr = scr_dyn_new_arr();
|
|
scr_dyn_obj_set(obj, f->name->data, f->name->len, arr);
|
|
}
|
|
ScrDyn *item = scr_dyn_new_handle(f->sock, SCR_DYNH_NET_SOCKET);
|
|
scr_dyn_arr_push(arr, item);
|
|
}
|
|
/* the list is most recent first: reverse each array into Node's order */
|
|
for (size_t i = 0; i < obj->v.obj.len; i++) {
|
|
ScrDyn *arr = obj->v.obj.entries[i].value;
|
|
for (size_t l = 0, r = arr->v.arr.len ? arr->v.arr.len - 1 : 0; l < r; l++, r--) {
|
|
ScrDyn *t = arr->v.arr.items[l];
|
|
arr->v.arr.items[l] = arr->v.arr.items[r];
|
|
arr->v.arr.items[r] = t;
|
|
}
|
|
}
|
|
return obj;
|
|
}
|
|
if (strcmp(key, "totalSocketCount") == 0) return scr_dyn_new_num((double)a->sock_count->n);
|
|
{
|
|
static const char *const known[] = { "options", "maxTotalSockets", NULL };
|
|
if (scr_http_dynh_in(key, known)) {
|
|
scr_http_dynh_unsupported("Agent", key, NULL);
|
|
return NULL;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static bool scr_http_dynh_agent_set(void *h, const char *key, size_t key_len, const ScrDyn *value) {
|
|
ScrHttpAgent *a = (ScrHttpAgent *)h;
|
|
(void)key_len;
|
|
if (strcmp(key, "defaultPort") == 0 || strcmp(key, "maxSockets") == 0 ||
|
|
strcmp(key, "maxFreeSockets") == 0 || strcmp(key, "keepAliveMsecs") == 0) {
|
|
if (value->kind != SCR_DYN_NUM) {
|
|
scr_dyn_arg_type_fail(key, "of type number", value);
|
|
return true; /* handled: the exception is pending */
|
|
}
|
|
if (strcmp(key, "defaultPort") == 0) a->default_port = value->v.num;
|
|
else if (strcmp(key, "maxSockets") == 0) a->max_sockets = value->v.num;
|
|
else if (strcmp(key, "maxFreeSockets") == 0) a->max_free = value->v.num;
|
|
else a->ka_msecs = value->v.num;
|
|
return true;
|
|
}
|
|
return false; /* createSocket/createConnection installs: the named fence */
|
|
}
|
|
|
|
static const ScrDynHandleOps scr_http_dynh_agent_ops = {
|
|
"Agent",
|
|
&scr_http_agent_retain_v,
|
|
&scr_http_agent_release_v,
|
|
&scr_http_dynh_agent_invoke,
|
|
&scr_http_dynh_agent_get,
|
|
&scr_http_dynh_agent_set,
|
|
NULL,
|
|
};
|
|
|
|
void scr_http_dyn_install(void) {
|
|
scr_dyn_handle_install(SCR_DYNH_HTTP_REQ, &scr_http_dynh_req_ops);
|
|
scr_dyn_handle_install(SCR_DYNH_HTTP_RES, &scr_http_dynh_res_ops);
|
|
scr_dyn_handle_install(SCR_DYNH_HTTP_CLIENT, &scr_http_dynh_client_ops);
|
|
scr_dyn_handle_install(SCR_DYNH_HTTP_AGENT, &scr_http_dynh_agent_ops);
|
|
scr_net_set_dynh_http_on(&scr_http_dynh_server_on);
|
|
}
|
|
|
|
/* Checked-dynamic chunks into TYPED res/client handles — the
|
|
* scr_net_sock_write_dynv story (the static lowering's dyn-data arm). */
|
|
void scr_http_res_write_dynv(ScrHttpRes *r, const ScrDyn *d) {
|
|
if (d->kind == SCR_DYN_STR) scr_http_res_write_str(r, d->v.str);
|
|
else if (d->kind == SCR_DYN_BYTES) scr_http_res_write_bytes(r, d->v.bytes);
|
|
else scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", d);
|
|
}
|
|
|
|
void scr_http_res_end_dynv(ScrHttpRes *r, const ScrDyn *d) {
|
|
if (d->kind == SCR_DYN_STR) scr_http_res_end_str(r, d->v.str);
|
|
else if (d->kind == SCR_DYN_BYTES) scr_http_res_end_bytes(r, d->v.bytes);
|
|
else if (d->kind == SCR_DYN_UNDEF || d->kind == SCR_DYN_NULL) scr_http_res_end(r);
|
|
else scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", d);
|
|
}
|
|
|
|
void scr_http_client_write_dynv(ScrHttpClientReq *c, const ScrDyn *d) {
|
|
if (d->kind == SCR_DYN_STR) scr_http_client_write_str(c, d->v.str);
|
|
else if (d->kind == SCR_DYN_BYTES) scr_http_client_write_bytes(c, d->v.bytes);
|
|
else scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", d);
|
|
}
|
|
|
|
void scr_http_client_end_dynv(ScrHttpClientReq *c, const ScrDyn *d) {
|
|
if (d->kind == SCR_DYN_STR) scr_http_client_end_str(c, d->v.str);
|
|
else if (d->kind == SCR_DYN_BYTES) scr_http_client_end_bytes(c, d->v.bytes);
|
|
else if (d->kind == SCR_DYN_UNDEF || d->kind == SCR_DYN_NULL) scr_http_client_end(c);
|
|
else scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", d);
|
|
}
|
|
|
|
/* http.request/get with a URL-STRING first argument — the suite's
|
|
* `http.get(`http://127.0.0.1:${port}/x`)` spelling. Parses through the
|
|
* WHATWG unit (scr_url.c, always linked): an unparsable input throws
|
|
* scr_url_new's catchable "Invalid URL" TypeError; a non-http scheme
|
|
* throws Node's ERR_INVALID_PROTOCOL shape. Path is pathname + search
|
|
* (fragment dropped, Node's client behavior). cb MOVES (nullable);
|
|
* result +1 or NULL with the exception pending. */
|
|
#include "scr_url_internal.h"
|
|
|
|
bool scr_http_url_parts(ScrStr *url /*borrowed*/, bool secure, ScrStr **host_out /*+1*/,
|
|
double *port_out, ScrStr **path_out /*+1*/) {
|
|
ScrUrl *u = scr_url_new(url);
|
|
if (!u) return false; /* Invalid URL pending */
|
|
/* The scheme is checked against the MODULE the call came from, not read
|
|
* off the URL: http.get('https://…') is Node's ERR_INVALID_PROTOCOL, not
|
|
* an implicit upgrade to https. Hence `secure` as a parameter. */
|
|
const char *want = secure ? "https" : "http";
|
|
const size_t wantlen = secure ? 5 : 4;
|
|
if (!(u->scheme->len == wantlen && memcmp(u->scheme->data, want, wantlen) == 0)) {
|
|
char msg[128];
|
|
int n = snprintf(msg, sizeof msg, "Protocol \"%.*s:\" not supported. Expected \"%s:\"",
|
|
(int)(u->scheme->len < 32 ? u->scheme->len : 32), u->scheme->data, want);
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)n, "ERR_INVALID_PROTOCOL");
|
|
scr_url_release(u);
|
|
return false;
|
|
}
|
|
double port = secure ? 443 : 80;
|
|
if (u->port->len > 0) {
|
|
port = 0;
|
|
for (size_t i = 0; i < u->port->len; i++) port = port * 10 + (u->port->data[i] - '0');
|
|
}
|
|
/* IPv6 authority arrives bracketed ("[::1]") — the dial wants the bare
|
|
* address, like Node's client strips them. */
|
|
if (u->host->len >= 2 && u->host->data[0] == '[' && u->host->data[u->host->len - 1] == ']') {
|
|
*host_out = scr_str_new(u->host->data + 1, u->host->len - 2);
|
|
} else {
|
|
*host_out = scr_str_retain(u->host);
|
|
}
|
|
if (u->query->len > 0) {
|
|
*path_out = scr_str_concat(u->path, u->query);
|
|
} else {
|
|
*path_out = u->path->len > 0 ? scr_str_retain(u->path) : scr_str_new("/", 1);
|
|
}
|
|
*port_out = port;
|
|
scr_url_release(u);
|
|
return true;
|
|
}
|
|
|
|
ScrHttpClientReq *scr_http_request_url(ScrStr *url /*borrowed*/, ScrStr *method /*borrowed*/,
|
|
bool auto_end, ScrClosure *cb /*moves, nullable*/,
|
|
ScrHttpRespFn fn) {
|
|
ScrStr *host;
|
|
ScrStr *path;
|
|
double port;
|
|
if (!scr_http_url_parts(url, false, &host, &port, &path)) {
|
|
if (cb) scr_closure_release(cb);
|
|
return NULL; /* Invalid URL / ERR_INVALID_PROTOCOL pending */
|
|
}
|
|
ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, 0); /* request_impl reads the pairs array */
|
|
ScrHttpClientReq *c = scr_http_request_ex(host, port, path, method, 0, pairs, auto_end,
|
|
cb, fn, 80, NULL, NULL);
|
|
scr_arr_release(pairs);
|
|
scr_str_release(host);
|
|
scr_str_release(path);
|
|
return c;
|
|
}
|
|
|
|
/* ── http.request(options) with a runtime options record ─────────────────
|
|
*
|
|
* The options value of http.request/get when the compiler cannot see an
|
|
* object literal: a RequestOptions variable, or a literal that spreads one
|
|
* or sets socketPath. Node's ClientRequest constructor reads it at run
|
|
* time; so does this walk, in Node's validation order and with its error
|
|
* shapes. Documented options outside the implemented set throw the
|
|
* runtime fence when they carry a value, rather than being ignored;
|
|
* undocumented keys are ignored, as Node ignores them. */
|
|
|
|
/* The documented options this walk does not implement. A truthy value
|
|
* fences; setHost and setDefaultHeaders fence only when false. */
|
|
static const char *const SCR_HTTP_REQ_FENCED_OPTIONS[] = {
|
|
"auth", "createConnection", "family", "hints", "httpValidation", "insecureHTTPParser",
|
|
"joinDuplicateHeaders", "localAddress", "localPort", "lookup", "uniqueHeaders", NULL,
|
|
};
|
|
|
|
static const char SCR_HTTP_REQ_SUPPORTED[] =
|
|
"hostname/host, port, path, method, headers, timeout, agent, signal, socketPath, "
|
|
"defaultPort, protocol, and maxHeaderSize are the supported options";
|
|
|
|
/* validateHeaderName and validateHeaderValue over one pair of the flat
|
|
* headers array: a name that is not a string throws
|
|
* ERR_INVALID_HTTP_TOKEN with its string form, and an array value
|
|
* validates each element. */
|
|
static void scr_http_req_validate_header_pair(const ScrDyn *name, const ScrDyn *value) {
|
|
ScrStr *label = scr_str_new("", 0);
|
|
if (name->kind == SCR_DYN_STR) {
|
|
scr_http_validate_header_name(name->v.str, label);
|
|
} else {
|
|
ScrStr *text = scr_dyn_string_coerce_js(name);
|
|
if (text != NULL) {
|
|
scr_http_throw_invalid_header_name(text, label);
|
|
scr_str_release(text);
|
|
}
|
|
}
|
|
scr_str_release(label);
|
|
if (scr_exc_pending()) return;
|
|
if (value->kind == SCR_DYN_ARR) {
|
|
for (size_t k = 0; k < value->v.arr.len && !scr_exc_pending(); k++)
|
|
scr_http_validate_header_value(name->v.str, value->v.arr.items[k]);
|
|
} else {
|
|
scr_http_validate_header_value(name->v.str, value);
|
|
}
|
|
}
|
|
|
|
/* util.inspect's one-line rendering of a short array ("[ 'x-a', 5 ]"): up
|
|
* to six primitive entries whose rendering stays within inspect's
|
|
* 80-column break length (a start of n + 11, plus the separators and each
|
|
* entry). NULL when inspect would group or break the entries, or an entry
|
|
* is not a primitive or renders outside ASCII (inspect counts UTF-16
|
|
* units). */
|
|
static ScrStr *scr_http_inspect_short_array(const ScrDyn *list) {
|
|
size_t n = list->v.arr.len;
|
|
if (n == 0 || n > 6) return NULL;
|
|
ScrJsonBuf b;
|
|
scr_jb_init(&b);
|
|
scr_jb_puts(&b, "[ ");
|
|
size_t total = 2 * n + 11;
|
|
bool fits = true;
|
|
for (size_t i = 0; i < n && fits; i++) {
|
|
const ScrDyn *v = list->v.arr.items[i];
|
|
ScrStr *part = NULL;
|
|
switch (v->kind) {
|
|
case SCR_DYN_STR: part = scr_str_inspect_quoted(v->v.str, false); break;
|
|
case SCR_DYN_NUM:
|
|
part = v->v.num == 0 && signbit(v->v.num) ? scr_str_new("-0", 2) : scr_f64_to_scrstr(v->v.num);
|
|
break;
|
|
case SCR_DYN_BOOL: part = v->v.b ? scr_str_new("true", 4) : scr_str_new("false", 5); break;
|
|
case SCR_DYN_NULL: part = scr_str_new("null", 4); break;
|
|
case SCR_DYN_UNDEF: part = scr_str_new("undefined", 9); break;
|
|
default: break;
|
|
}
|
|
if (part == NULL) {
|
|
fits = false;
|
|
break;
|
|
}
|
|
for (size_t k = 0; k < part->len && fits; k++) fits = (unsigned char)part->data[k] < 0x80;
|
|
total += part->len;
|
|
if (total > 80) fits = false;
|
|
if (i > 0) scr_jb_puts(&b, ", ");
|
|
scr_jb_write(&b, part->data, part->len);
|
|
scr_str_release(part);
|
|
}
|
|
scr_jb_puts(&b, " ]");
|
|
ScrStr *out = scr_jb_finish(&b);
|
|
if (fits) return out;
|
|
scr_str_release(out);
|
|
return NULL;
|
|
}
|
|
|
|
/* Node's ERR_INVALID_ARG_VALUE for a flat headers array of odd length.
|
|
* Arrays whose inspected form scr_http_inspect_short_array cannot
|
|
* reproduce fence instead. */
|
|
static void scr_http_req_odd_headers_fail(const char *api, const ScrDyn *list) {
|
|
ScrStr *shown = scr_http_inspect_short_array(list);
|
|
if (shown == NULL) {
|
|
scr_http_req_opts_fence(api, "headers",
|
|
"the odd-length headers array error prints up to six short primitive entries here");
|
|
return;
|
|
}
|
|
ScrJsonBuf b;
|
|
scr_jb_init(&b);
|
|
scr_jb_puts(&b, "The argument 'headers' is invalid. Received ");
|
|
scr_jb_write(&b, shown->data, shown->len);
|
|
scr_str_release(shown);
|
|
ScrStr *msg = scr_jb_finish(&b);
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, msg->data, msg->len, "ERR_INVALID_ARG_VALUE");
|
|
scr_str_release(msg);
|
|
}
|
|
|
|
void scr_http_req_opts_fence(const char *api, const char *key, const char *hint) {
|
|
ScrJsonBuf b;
|
|
scr_jb_init(&b);
|
|
scr_jb_puts(&b, api);
|
|
scr_jb_puts(&b, " option '");
|
|
scr_jb_puts(&b, key);
|
|
scr_jb_puts(&b, "' has no static lowering — ");
|
|
scr_jb_puts(&b, hint);
|
|
scr_throw_error(SCR_ERR_ERROR, scr_jb_finish(&b));
|
|
}
|
|
|
|
void scr_http_req_opts_free(ScrHttpReqOpts *o) {
|
|
scr_str_release(o->host);
|
|
scr_str_release(o->path);
|
|
scr_str_release(o->method);
|
|
scr_str_release(o->socket_path);
|
|
if (o->headers) scr_arr_release(o->headers);
|
|
scr_dyn_release(o->agent);
|
|
scr_dyn_release(o->signal);
|
|
scr_dyn_release(o->view);
|
|
memset(o, 0, sizeof *o);
|
|
}
|
|
|
|
/* A member of the options record, NULL when absent or undefined. */
|
|
static const ScrDyn *scr_http_req_opt(const ScrDyn *opts, const char *key) {
|
|
const ScrDyn *v = scr_dyn_obj_get(opts, key, strlen(key));
|
|
return v == NULL || v->kind == SCR_DYN_UNDEF ? NULL : v;
|
|
}
|
|
|
|
static bool scr_http_req_truthy(const ScrDyn *v) { return v != NULL && scr_dyn_truthy(v); }
|
|
|
|
/* validatePort's accepted values: an integer in [0, 65535] or a string
|
|
* holding one. NaN when the value is not a valid port. */
|
|
static double scr_http_req_port_value(const ScrDyn *v) {
|
|
if (v->kind == SCR_DYN_NUM) {
|
|
double n = v->v.num;
|
|
return n == n && trunc(n) == n && n >= 0 && n <= 65535 ? n : NAN;
|
|
}
|
|
if (v->kind == SCR_DYN_STR) {
|
|
bool blank = true;
|
|
for (size_t i = 0; i < v->v.str->len && blank; i++)
|
|
blank = isspace((unsigned char)v->v.str->data[i]) != 0;
|
|
if (blank) return NAN;
|
|
ScrStr *s = scr_str_retain(v->v.str);
|
|
double n = scr_string_to_number(s);
|
|
scr_str_release(s);
|
|
return n == n && trunc(n) == n && n >= 0 && n <= 65535 ? n : NAN;
|
|
}
|
|
return NAN;
|
|
}
|
|
|
|
/* Reads `opts` (borrowed) into `o` in the ClientRequest constructor's
|
|
* order. False with the exception pending (`o` holds nothing). */
|
|
bool scr_http_req_opts_read(const ScrDyn *opts, bool secure, const char *api, ScrHttpReqOpts *o) {
|
|
memset(o, 0, sizeof *o);
|
|
o->port = -1;
|
|
o->default_port = secure ? 443 : 80;
|
|
if (opts != NULL && opts->kind == SCR_DYN_TYPED_REF) {
|
|
o->view = scr_dyn_typed_ref_materialize(opts);
|
|
opts = o->view;
|
|
}
|
|
if (opts == NULL || opts->kind == SCR_DYN_UNDEF || opts->kind == SCR_DYN_NULL) {
|
|
opts = NULL; /* Node spreads it: every option takes its default */
|
|
} else if (opts->kind != SCR_DYN_OBJ) {
|
|
scr_http_req_opts_fence(api, "options", "pass an options object or a URL string");
|
|
scr_http_req_opts_free(o);
|
|
return false;
|
|
}
|
|
#define OPT(key) (opts != NULL ? scr_http_req_opt(opts, key) : NULL)
|
|
/* agent: false, null/undefined (the global agent), or an Agent */
|
|
const ScrDyn *agent = OPT("agent");
|
|
if (agent != NULL && agent->kind != SCR_DYN_NULL &&
|
|
!(agent->kind == SCR_DYN_BOOL && !agent->v.b) &&
|
|
!(agent->kind == SCR_DYN_HANDLE && agent->v.handle.tag == SCR_DYNH_HTTP_AGENT)) {
|
|
scr_dyn_prop_type_fail("options.agent", "one of Agent-like Object, undefined, or false", agent);
|
|
scr_http_req_opts_free(o);
|
|
return false;
|
|
}
|
|
if (agent != NULL) o->agent = scr_dyn_retain((ScrDyn *)agent);
|
|
bool agent_secure = secure;
|
|
if (agent != NULL && agent->kind == SCR_DYN_HANDLE)
|
|
agent_secure = ((ScrHttpAgent *)agent->v.handle.ptr)->secure;
|
|
/* path: String(options.path) when truthy, without unescaped characters */
|
|
const ScrDyn *path = OPT("path");
|
|
if (scr_http_req_truthy(path)) {
|
|
o->path = scr_dyn_string_coerce_js(path);
|
|
if (o->path == NULL) {
|
|
scr_http_req_opts_free(o);
|
|
return false;
|
|
}
|
|
bool bad = false;
|
|
for (size_t i = 0; i < o->path->len && !bad; i++) {
|
|
unsigned char ch = (unsigned char)o->path->data[i];
|
|
if (ch < 0x21) bad = true;
|
|
else if (ch >= 0x80) {
|
|
/* U+0080..U+00FF (lead byte C2/C3) pass; anything above is unescaped */
|
|
if (ch != 0xc2 && ch != 0xc3) bad = true;
|
|
else i++;
|
|
}
|
|
}
|
|
if (bad) {
|
|
static const char msg[] = "Request path contains unescaped characters";
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_UNESCAPED_CHARACTERS");
|
|
scr_http_req_opts_free(o);
|
|
return false;
|
|
}
|
|
} else {
|
|
o->path = scr_str_new("/", 1);
|
|
}
|
|
/* protocol: options.protocol || the agent's, which must match */
|
|
const ScrDyn *protocol = OPT("protocol");
|
|
const char *expected = agent_secure ? "https:" : "http:";
|
|
if (scr_http_req_truthy(protocol)) {
|
|
ScrStr *p = scr_dyn_string_coerce_js(protocol);
|
|
if (p == NULL) {
|
|
scr_http_req_opts_free(o);
|
|
return false;
|
|
}
|
|
if (p->len != strlen(expected) || memcmp(p->data, expected, p->len) != 0) {
|
|
char msg[192];
|
|
int n = snprintf(msg, sizeof msg, "Protocol \"%.*s\" not supported. Expected \"%s\"",
|
|
(int)(p->len < 64 ? p->len : 64), p->data, expected);
|
|
scr_str_release(p);
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)n, "ERR_INVALID_PROTOCOL");
|
|
scr_http_req_opts_free(o);
|
|
return false;
|
|
}
|
|
scr_str_release(p);
|
|
}
|
|
/* defaultPort, then port (validated when the dial is TCP) */
|
|
const ScrDyn *default_port = OPT("defaultPort");
|
|
if (scr_http_req_truthy(default_port) && default_port->kind == SCR_DYN_NUM)
|
|
o->default_port = (int)default_port->v.num;
|
|
else if (agent != NULL && agent->kind == SCR_DYN_HANDLE)
|
|
o->default_port = (int)((ScrHttpAgent *)agent->v.handle.ptr)->default_port;
|
|
const ScrDyn *port = OPT("port");
|
|
/* host: validateHost(hostname) || validateHost(host) || 'localhost' */
|
|
static const char *const host_keys[] = { "hostname", "host" };
|
|
for (size_t i = 0; i < 2; i++) {
|
|
const ScrDyn *h = OPT(host_keys[i]);
|
|
if (h == NULL || h->kind == SCR_DYN_NULL) continue;
|
|
if (h->kind != SCR_DYN_STR) {
|
|
char name[24];
|
|
snprintf(name, sizeof name, "options.%s", host_keys[i]);
|
|
scr_dyn_prop_type_fail(name, "of type string or one of undefined or null", h);
|
|
scr_http_req_opts_free(o);
|
|
return false;
|
|
}
|
|
if (o->host == NULL && h->v.str->len > 0) o->host = scr_str_retain(h->v.str);
|
|
}
|
|
if (o->host == NULL) o->host = scr_str_new("localhost", 9);
|
|
const ScrDyn *set_host = OPT("setHost");
|
|
if (set_host != NULL && set_host->kind == SCR_DYN_BOOL && !set_host->v.b) {
|
|
scr_http_req_opts_fence(api, "setHost", "the Host header is always sent — leave setHost at its true default");
|
|
scr_http_req_opts_free(o);
|
|
return false;
|
|
}
|
|
const ScrDyn *set_default = OPT("setDefaultHeaders");
|
|
if (set_default != NULL && set_default->kind == SCR_DYN_BOOL && !set_default->v.b) {
|
|
scr_http_req_opts_fence(api, "setDefaultHeaders", "the default headers are always sent — leave setDefaultHeaders at its true default");
|
|
scr_http_req_opts_free(o);
|
|
return false;
|
|
}
|
|
const ScrDyn *socket_path = OPT("socketPath");
|
|
/* timeout: getTimerDuration(options.timeout, 'timeout') */
|
|
const ScrDyn *timeout = OPT("timeout");
|
|
if (timeout != NULL) {
|
|
if (timeout->kind != SCR_DYN_NUM) {
|
|
scr_dyn_arg_type_fail("timeout", "of type number", timeout);
|
|
scr_http_req_opts_free(o);
|
|
return false;
|
|
}
|
|
double t = timeout->v.num;
|
|
if (!(t >= 0 && isfinite(t))) {
|
|
char recv[48], msg[192];
|
|
scr_num_received(t, recv);
|
|
int n = snprintf(msg, sizeof msg,
|
|
"The value of \"timeout\" is out of range. It must be a non-negative finite number. Received %s",
|
|
recv);
|
|
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)n, "ERR_OUT_OF_RANGE");
|
|
scr_http_req_opts_free(o);
|
|
return false;
|
|
}
|
|
o->timeout = t;
|
|
}
|
|
/* method: a string token, upper-cased; GET when absent or empty */
|
|
const ScrDyn *method = OPT("method");
|
|
if (method != NULL && method->kind != SCR_DYN_NULL && method->kind != SCR_DYN_STR) {
|
|
scr_dyn_prop_type_fail("options.method", "of type string", method);
|
|
scr_http_req_opts_free(o);
|
|
return false;
|
|
}
|
|
if (method != NULL && method->kind == SCR_DYN_STR && method->v.str->len > 0) {
|
|
o->method = scr_str_new(method->v.str->data, method->v.str->len);
|
|
for (size_t i = 0; i < o->method->len; i++) {
|
|
if (!scr_http_token_char((unsigned char)o->method->data[i])) {
|
|
ScrJsonBuf b;
|
|
scr_jb_init(&b);
|
|
scr_jb_puts(&b, "Method must be a valid HTTP token [\"");
|
|
scr_jb_write(&b, o->method->data, o->method->len);
|
|
scr_jb_puts(&b, "\"]");
|
|
ScrStr *msg = scr_jb_finish(&b);
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, msg->data, msg->len, "ERR_INVALID_HTTP_TOKEN");
|
|
scr_str_release(msg);
|
|
scr_http_req_opts_free(o);
|
|
return false;
|
|
}
|
|
}
|
|
for (size_t i = 0; i < o->method->len; i++) {
|
|
unsigned char ch = (unsigned char)o->method->data[i];
|
|
if (ch >= 'a' && ch <= 'z') o->method->data[i] = (char)(ch - 'a' + 'A');
|
|
}
|
|
} else {
|
|
o->method = scr_str_new("GET", 3);
|
|
}
|
|
const ScrDyn *max_header = OPT("maxHeaderSize");
|
|
if (max_header != NULL) {
|
|
if (max_header->kind != SCR_DYN_NUM) {
|
|
scr_dyn_arg_type_fail("maxHeaderSize", "of type number", max_header);
|
|
scr_http_req_opts_free(o);
|
|
return false;
|
|
}
|
|
o->max_header_size = max_header->v.num;
|
|
o->has_max_header_size = true;
|
|
}
|
|
for (size_t i = 0; SCR_HTTP_REQ_FENCED_OPTIONS[i] != NULL; i++) {
|
|
if (scr_http_req_truthy(OPT(SCR_HTTP_REQ_FENCED_OPTIONS[i]))) {
|
|
scr_http_req_opts_fence(api, SCR_HTTP_REQ_FENCED_OPTIONS[i], SCR_HTTP_REQ_SUPPORTED);
|
|
scr_http_req_opts_free(o);
|
|
return false;
|
|
}
|
|
}
|
|
/* headers: an object (String(number) values, arrays as repeated lines)
|
|
* or the flat [name, value, ...] array */
|
|
const ScrDyn *headers = OPT("headers");
|
|
ScrDyn *headers_view = NULL;
|
|
if (headers != NULL && headers->kind == SCR_DYN_TYPED_REF) {
|
|
headers_view = scr_dyn_typed_ref_materialize(headers);
|
|
headers = headers_view;
|
|
}
|
|
if (headers != NULL && headers->kind == SCR_DYN_OBJ) {
|
|
for (size_t i = 0; i < headers->v.obj.len; i++) {
|
|
const ScrDynEntry *e = &headers->v.obj.entries[i];
|
|
ScrStr *name = scr_str_new(e->key, e->key_len);
|
|
ScrStr *label = scr_str_new("", 0);
|
|
scr_http_validate_header_name(name, label);
|
|
scr_str_release(label);
|
|
const ScrDyn *v = e->value;
|
|
if (!scr_exc_pending()) {
|
|
if (v->kind == SCR_DYN_ARR) {
|
|
for (size_t k = 0; k < v->v.arr.len && !scr_exc_pending(); k++)
|
|
scr_http_validate_header_value(name, v->v.arr.items[k]);
|
|
} else {
|
|
scr_http_validate_header_value(name, v);
|
|
}
|
|
}
|
|
scr_str_release(name);
|
|
if (scr_exc_pending()) break;
|
|
}
|
|
if (!scr_exc_pending()) o->headers = scr_http_dynh_pairs(headers);
|
|
} else if (headers != NULL && headers->kind == SCR_DYN_ARR) {
|
|
/* Node's _storeHeader checks that the flat [name, value, ...] form has
|
|
* an even length, then validates each name and value. The
|
|
* [[name, value], ...] entry form is not modeled. */
|
|
size_t n = headers->v.arr.len;
|
|
if (n > 0 && headers->v.arr.items[0]->kind == SCR_DYN_ARR) {
|
|
scr_http_req_opts_fence(
|
|
api, "headers",
|
|
"headers is an object of string or number values or a flat [name, value, ...] array here");
|
|
} else if (n % 2 != 0) {
|
|
scr_http_req_odd_headers_fail(api, headers);
|
|
} else {
|
|
for (size_t i = 0; i < n && !scr_exc_pending(); i += 2)
|
|
scr_http_req_validate_header_pair(headers->v.arr.items[i], headers->v.arr.items[i + 1]);
|
|
}
|
|
if (!scr_exc_pending()) o->headers = scr_http_dynh_flat_pairs(headers);
|
|
} else if (headers != NULL && headers->kind != SCR_DYN_NULL) {
|
|
scr_http_req_opts_fence(api, "headers", "headers is an object of string or number values here");
|
|
}
|
|
scr_dyn_release(headers_view);
|
|
if (scr_exc_pending()) {
|
|
scr_http_req_opts_free(o);
|
|
return false;
|
|
}
|
|
if (o->headers == NULL) o->headers = scr_arr_new(SCR_ELEM_STR, 0);
|
|
const ScrDyn *signal = OPT("signal");
|
|
if (signal != NULL) o->signal = scr_dyn_retain((ScrDyn *)signal);
|
|
/* the connection: a Unix socket (net validates the path) or host:port */
|
|
if (scr_http_req_truthy(socket_path)) {
|
|
if (socket_path->kind != SCR_DYN_STR) {
|
|
scr_dyn_prop_type_fail("options.path", "of type string", socket_path);
|
|
scr_http_req_opts_free(o);
|
|
return false;
|
|
}
|
|
o->socket_path = scr_str_retain(socket_path->v.str);
|
|
if (scr_http_req_truthy(port)) {
|
|
double n = scr_http_req_port_value(port);
|
|
if (n == n) o->port = n;
|
|
}
|
|
} else if (scr_http_req_truthy(port)) {
|
|
double n = scr_http_req_port_value(port);
|
|
if (n != n) {
|
|
char detail[96], msg[192];
|
|
const char *d = scr_dyn_specific_type(port, detail, sizeof detail);
|
|
int len = snprintf(msg, sizeof msg, "Port should be >= 0 and < 65536. Received %s.", d);
|
|
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_SOCKET_BAD_PORT");
|
|
scr_http_req_opts_free(o);
|
|
return false;
|
|
}
|
|
o->port = n;
|
|
}
|
|
#undef OPT
|
|
return true;
|
|
}
|
|
|
|
/* Builds the request `o` describes (its references stay with `o`). The
|
|
* signal is checked before construction and attached after it, like the
|
|
* literal lowering's sequence. +1, or NULL with the exception pending. */
|
|
ScrHttpClientReq *scr_http_req_opts_send(ScrHttpReqOpts *o, bool auto_end,
|
|
ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn,
|
|
ScrDyn *(*add_abort)(ScrDyn *signal, ScrDyn *listener),
|
|
void (*wrap)(ScrNetSocket *, void *), void *wrap_ctx,
|
|
ScrStr *pool_key /*borrowed, nullable*/) {
|
|
if (o->signal != NULL) {
|
|
scr_http_client_signal_check(o->signal);
|
|
if (scr_exc_pending()) {
|
|
if (cb) scr_closure_release(cb);
|
|
return NULL;
|
|
}
|
|
}
|
|
/* without a port option the agent's default port applies (< 0), and
|
|
* without an agent the scheme's */
|
|
double port = o->port >= 0 ? o->port : o->agent != NULL && o->agent->kind == SCR_DYN_HANDLE ? -1
|
|
: o->default_port;
|
|
scr_http_next_socket_path = o->socket_path;
|
|
scr_http_socket_dialer = &scr_net_connect_path;
|
|
ScrHttpClientReq *c = scr_http_request_agent_ex(
|
|
o->host, port, o->path, o->method, o->timeout, o->headers, auto_end, o->agent, cb, fn,
|
|
o->default_port, wrap, wrap_ctx, pool_key);
|
|
if (c == NULL) return NULL;
|
|
if (o->has_max_header_size) {
|
|
scr_http_client_max_header_size(c, o->max_header_size);
|
|
if (scr_exc_pending()) return c;
|
|
}
|
|
if (o->signal != NULL) scr_http_client_add_abort_signal(c, o->signal, add_abort);
|
|
return c;
|
|
}
|
|
|
|
/* http.request/http.get(options[, cb]) with a runtime options record. */
|
|
ScrHttpClientReq *scr_http_request_dyn(const ScrDyn *opts /*borrowed*/, bool auto_end,
|
|
ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn,
|
|
ScrDyn *(*add_abort)(ScrDyn *signal, ScrDyn *listener)) {
|
|
ScrHttpReqOpts o;
|
|
if (!scr_http_req_opts_read(opts, false, "http.request", &o)) {
|
|
if (cb) scr_closure_release(cb);
|
|
return NULL;
|
|
}
|
|
ScrHttpClientReq *c = scr_http_req_opts_send(&o, auto_end, cb, fn, add_abort, NULL, NULL, NULL);
|
|
scr_http_req_opts_free(&o);
|
|
return c;
|
|
}
|