mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-24 06:14:01 +08:00
6632 lines
220 KiB
C
6632 lines
220 KiB
C
/* fetch over scriptc's OWN net stack — the static tier below is entirely
|
|
* engine-free, while the dynamic tier exposes the same transport through
|
|
* the island's full web API. Both use the
|
|
* scr_platform poller + scr_net sockets, scr_http's HTTP/1.1 client
|
|
* parser, scr_tls (vendored mbedTLS) for https, and zlib for response
|
|
* decompression. No libcurl anywhere: the architecture is Node's own
|
|
* (libuv sockets + OpenSSL + undici; ours: poller + mbedTLS + this
|
|
* unit), which is what lets fetch compile for every target the socket
|
|
* units reach — win32 included, where no system-libcurl contract
|
|
* exists. The curl implementation this replaces stays compilable for
|
|
* one release as the reference (scr_fetch_curl.c, selected by
|
|
* SCRIPTC_FETCH_CURL=1 at build time; same install symbol, same island
|
|
* surface, same fixture suite).
|
|
*
|
|
* Architecture (the JS half is UNCHANGED from the curl bridge):
|
|
* - The JS half (fetch_glue below) defines globalThis.fetch over two host
|
|
* functions: host.start(req, cbs) begins a transfer and host.abort(id)
|
|
* cancels one (response-body cancel). fetch() returns a REAL island
|
|
* promise; the Response's body is an island ReadableStream fed by
|
|
* C→engine callbacks as data arrives.
|
|
* - The C half is a fetch-layer client over scr_http's client machinery:
|
|
* one ScrHttpClientReq per hop, minted runtime closures (caps[0] boxes
|
|
* the transfer) as its response/data/end/error listeners, callbacks
|
|
* firing from the net unit's dispatch station — the loop's own poller
|
|
* sleeps on socket readiness, so this unit registers NO poll hook with
|
|
* the island (the curl bridge needed one to sleep on curl's fds).
|
|
* AbortSignal.timeout stays punctual through the loop's island-timer
|
|
* deadline hook (scr_loop_set_island_deadline — armed island timers cap
|
|
* the idle sleep exactly the way the curl poll capped its own).
|
|
* - fetch SEMANTICS, matched to Node/undici: HTTP errors RESOLVE (only
|
|
* network failure rejects, with TypeError "fetch failed" — Node's exact
|
|
* message); redirects are followed IN THIS UNIT (20 hops, fetch's
|
|
* limit; 303 — and 301/302 for POST — rewrite to GET and drop the body
|
|
* and its content-* headers, the fetch spec's rules; authorization is
|
|
* stripped on cross-origin hops; response.url = the final URL without
|
|
* its fragment, response.redirected set; a redirect hop's own body
|
|
* never reaches the island). The request head carries undici's exact
|
|
* default header set in undici's order (host, connection, the user
|
|
* headers, accept, accept-language, sec-fetch-mode, user-agent,
|
|
* accept-encoding — captured from Node 24 on the wire); gzip/zlib
|
|
* response bodies arrive decompressed (accept-encoding advertises
|
|
* "gzip, deflate", and content-encoding: gzip/x-gzip/deflate inflates
|
|
* through zlib's 15+32 auto-detect — raw-deflate servers and br/zstd
|
|
* are out of the slice, honestly: br/zstd are never offered).
|
|
* - DNS: scr_net dials IP literals only (Node's shape for its slice), so
|
|
* this unit resolves hostnames with getaddrinfo AT HOP START — the
|
|
* dns.lookup precedent (scr_dgram.c): synchronous resolution, first
|
|
* answer wins; failures ride the socket's deferred
|
|
* "getaddrinfo ENOTFOUND host" error so the rejection's cause is
|
|
* Node's exact shape. TLS handshakes verify/SNI against the URL
|
|
* HOSTNAME while the socket dials the resolved IP.
|
|
* - Proxies, matched to Node: undici's global fetch IGNORES http_proxy/
|
|
* https_proxy unless NODE_USE_ENV_PROXY=1 opts in (Node 24's
|
|
* EnvHttpProxyAgent). Opted in, http:// targets relay through the
|
|
* proxy as absolute-URI requests (the classic forward-proxy wire form;
|
|
* undici tunnels CONNECT, but the fixture proxy counts either form
|
|
* identically), no_proxy exclusions honored. https-over-proxy
|
|
* (CONNECT) is out of the slice, so an opted-in HTTPS proxy rejects
|
|
* instead of silently bypassing the configured policy.
|
|
* - AbortSignal is wired in both tiers: an already-aborted signal rejects
|
|
* with its reason before any transfer starts and aborting a live
|
|
* transfer destroys the hop's connection. The dynamic tier uses the
|
|
* island's Web/timer glue; the static tier below uses native signal
|
|
* handles and the runtime's unref'd timer heap.
|
|
* - Error causes, Node's shapes: connect ECONNREFUSED ip:port /
|
|
* getaddrinfo ENOTFOUND host pass through from the net layer with
|
|
* their codes; a server that dies before the response head maps to
|
|
* undici's cause exactly — "other side closed", code UND_ERR_SOCKET
|
|
* (the curl bridge could only offer curl's own string there).
|
|
* - Ownership: each transfer is refcounted — the live registry holds +1
|
|
* and every minted listener closure's box holds +1; settling releases
|
|
* the engine callback object and the hop client, and teardown
|
|
* (registered with the island) frees whatever is still live before the
|
|
* engine goes down, so the island audit stays zero. */
|
|
/* Fetch's bad-port table. Keep the static and dynamic transports on one
|
|
* source of truth: both start a fresh hop after every followed redirect. */
|
|
#define SCR_FETCH_BAD_PORTS \
|
|
1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, \
|
|
77, 79, 87, 95, 101, 102, 103, 104, 109, 110, 111, 113, 115, 117, 119, \
|
|
123, 135, 137, 139, 143, 161, 179, 389, 427, 465, 512, 513, 514, 515, \
|
|
526, 530, 531, 532, 540, 548, 554, 556, 563, 587, 601, 636, 989, 990, \
|
|
993, 995, 1719, 1720, 1723, 2049, 3659, 4045, 5060, 5061, 6000, 6566, \
|
|
6665, 6666, 6667, 6668, 6669, 6679, 6697, 10080
|
|
|
|
#ifndef SCR_DYNAMIC
|
|
|
|
#include "scr_runtime.h"
|
|
#include "scr_url_internal.h"
|
|
|
|
#include <math.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <zlib.h>
|
|
|
|
/*
|
|
* The static tier is a small native Web-platform island of its own:
|
|
* AbortSignal, Response, ReadableStream, its default reader, and its
|
|
* controller are SCR_DYN_HANDLE values. That preserves identity and
|
|
* permits normal typed member syntax while every operation still lands
|
|
* in C. Response promises resolve at the response HEAD; body chunks flow
|
|
* into the same native stream consumed by reader.read() or json().
|
|
*/
|
|
|
|
typedef struct SfTransfer SfTransfer;
|
|
typedef struct SfSignal SfSignal;
|
|
typedef struct SfSignalWatch SfSignalWatch;
|
|
typedef struct SfEvent SfEvent;
|
|
typedef struct SfStream SfStream;
|
|
typedef struct SfReader SfReader;
|
|
typedef struct SfResponse SfResponse;
|
|
typedef struct SfHeaders SfHeaders;
|
|
typedef struct SfCollector SfCollector;
|
|
typedef struct SfReadRequest SfReadRequest;
|
|
typedef struct SfPullWait SfPullWait;
|
|
typedef struct SfStartWait SfStartWait;
|
|
typedef struct SfCancelWait SfCancelWait;
|
|
typedef struct SfTypedRef SfTypedRef;
|
|
|
|
typedef struct SfChunk {
|
|
ScrDyn *value;
|
|
struct SfChunk *next;
|
|
} SfChunk;
|
|
|
|
struct SfReadRequest {
|
|
ScrPromise *promise;
|
|
SfReadRequest *next;
|
|
};
|
|
|
|
struct SfTypedRef {
|
|
ScrDyn *value;
|
|
SfTypedRef *next;
|
|
};
|
|
|
|
typedef struct SfAbortListener {
|
|
ScrDyn *listener;
|
|
bool capture;
|
|
bool once;
|
|
size_t id;
|
|
SfSignal *target; /* weak; the target owns this listener */
|
|
SfSignal *option_signal; /* owned unless it is the target */
|
|
SfSignalWatch *signal_watch;
|
|
struct SfAbortListener *next;
|
|
} SfAbortListener;
|
|
|
|
struct SfSignalWatch {
|
|
SfSignal *source; /* weak; NULL after the source fires */
|
|
void *owner; /* owner keeps source alive */
|
|
void (*fire)(SfSignalWatch *, SfSignal *);
|
|
SfSignalWatch *next;
|
|
};
|
|
|
|
struct SfSignal {
|
|
size_t rc;
|
|
bool aborted;
|
|
ScrDyn *reason;
|
|
ScrError *error_reason;
|
|
ScrDyn *onabort;
|
|
size_t onabort_order;
|
|
SfAbortListener *listeners;
|
|
size_t next_listener_id;
|
|
SfSignalWatch *watchers;
|
|
SfSignal **sources;
|
|
SfSignalWatch **source_watches;
|
|
size_t source_count;
|
|
double timer_id;
|
|
bool callbacks_tracked;
|
|
SfSignal *callbacks_next;
|
|
};
|
|
|
|
struct SfEvent {
|
|
size_t rc;
|
|
SfSignal *target;
|
|
double time_stamp;
|
|
bool dispatching;
|
|
bool stop_propagation;
|
|
bool stop_immediate;
|
|
};
|
|
|
|
struct SfReader {
|
|
size_t rc;
|
|
size_t handle_refs;
|
|
SfStream *stream; /* weak; handle refs retain it while the lock is live */
|
|
SfReadRequest *pending_head;
|
|
SfReadRequest *pending_tail;
|
|
ScrPromise *closed;
|
|
};
|
|
|
|
enum {
|
|
SF_COLLECT_JSON,
|
|
SF_COLLECT_TEXT,
|
|
SF_COLLECT_BYTES,
|
|
};
|
|
|
|
enum {
|
|
SF_REDIRECT_FOLLOW,
|
|
SF_REDIRECT_ERROR,
|
|
SF_REDIRECT_MANUAL,
|
|
};
|
|
|
|
struct SfCollector {
|
|
ScrPromise *promise;
|
|
SfStream *held_stream;
|
|
char *data;
|
|
size_t len;
|
|
size_t cap;
|
|
int mode;
|
|
};
|
|
|
|
struct SfPullWait {
|
|
SfStream *stream;
|
|
ScrPromise *promise;
|
|
};
|
|
|
|
struct SfStartWait {
|
|
SfStream *stream;
|
|
ScrPromise *promise;
|
|
};
|
|
|
|
struct SfCancelWait {
|
|
ScrPromise *source;
|
|
ScrPromise *result;
|
|
};
|
|
|
|
typedef struct SfThenState {
|
|
size_t rc;
|
|
bool called;
|
|
ScrPromise *promise;
|
|
} SfThenState;
|
|
|
|
struct SfStream {
|
|
size_t rc;
|
|
SfChunk *head;
|
|
SfChunk *tail;
|
|
size_t queued;
|
|
bool started;
|
|
bool close_requested;
|
|
bool closed;
|
|
bool disturbed;
|
|
bool internal_lock;
|
|
bool initial_pull_pending;
|
|
bool enqueue_draining;
|
|
bool pulling;
|
|
bool pull_again;
|
|
bool discarded;
|
|
bool request_discarding;
|
|
ScrDyn *error;
|
|
SfReader *reader; /* owned while locked */
|
|
SfCollector *collector; /* owned while a body reader is active */
|
|
ScrDyn *pull_cb;
|
|
ScrDyn *cancel_cb;
|
|
ScrDyn *source_this;
|
|
ScrDyn *from_dyn;
|
|
ScrArr *from_array;
|
|
ScrBytes *from_bytes;
|
|
ScrStr *from_string;
|
|
ScrDyn *(*from_array_item)(ScrArr *, double);
|
|
double from_index;
|
|
/* Canonical transit capsules for repeated references in a typed source
|
|
* array. Keeping one capsule per source identity gives both typed
|
|
* structural conversions and unknown/any reads one stable JS object. */
|
|
SfTypedRef *typed_refs;
|
|
SfTransfer *request_owner; /* weak; transfer owns the stream */
|
|
SfTransfer *response_owner; /* weak; transfer owns the stream */
|
|
bool callbacks_tracked;
|
|
SfStream *callbacks_next;
|
|
};
|
|
|
|
struct SfHeaders {
|
|
size_t rc;
|
|
ScrArr *pairs;
|
|
};
|
|
|
|
struct SfResponse {
|
|
size_t rc;
|
|
SfStream *body;
|
|
SfHeaders *headers;
|
|
ScrStr *url;
|
|
ScrStr *status_text;
|
|
int status;
|
|
bool redirected;
|
|
bool null_body;
|
|
};
|
|
|
|
struct SfTransfer {
|
|
size_t rc; /* live registry + one per listener closure */
|
|
ScrPromise *promise; /* owned */
|
|
ScrUrl *url; /* current hop, owned */
|
|
ScrStr *method; /* current method, owned */
|
|
ScrArr *headers; /* user header pairs, owned */
|
|
ScrDyn *body; /* replayable body or source stream, owned */
|
|
int redirect_mode;
|
|
int hops;
|
|
bool redirected;
|
|
ScrHttpClientReq *client; /* owned */
|
|
ScrNetSocket *response_socket; /* owned while the response is live */
|
|
bool response_paused;
|
|
bool response_null_body;
|
|
SfSignal *signal; /* owned */
|
|
SfSignalWatch *signal_watch;
|
|
SfStream *request_stream; /* owned */
|
|
SfStream *response_stream; /* owned */
|
|
bool request_has_content_length;
|
|
size_t request_content_length;
|
|
size_t request_body_sent;
|
|
z_stream zs;
|
|
ScrBytes *inflate_pending;
|
|
bool inflating;
|
|
bool inflate_member_end;
|
|
bool inflate_needs_drain;
|
|
bool inflate_draining;
|
|
bool response_ended;
|
|
bool response_sent;
|
|
bool done;
|
|
struct SfTransfer *next;
|
|
};
|
|
|
|
static SfTransfer *sf_live;
|
|
/* Weak registries: owners unlink themselves on destruction. They let the
|
|
* fetch teardown sever native→callback edges before final cycle collection,
|
|
* including an otherwise-untraceable callback→captured-handle backedge. */
|
|
static SfSignal *sf_callback_signals;
|
|
static SfStream *sf_callback_streams;
|
|
|
|
static void sf_oom(void) {
|
|
fputs("scriptc: out of memory\n", stderr);
|
|
abort();
|
|
}
|
|
|
|
static bool sf_name(const char *key, size_t len, const char *want) {
|
|
return strlen(want) == len && memcmp(key, want, len) == 0;
|
|
}
|
|
|
|
static void sf_type_error(const char *message) {
|
|
scr_throw_error_msg(SCR_ERR_TYPE, message, strlen(message));
|
|
}
|
|
|
|
static void sf_not_function(const char *what) {
|
|
size_t n = strlen(what);
|
|
char *message = malloc(n + 19);
|
|
if (!message) sf_oom();
|
|
memcpy(message, what, n);
|
|
memcpy(message + n, " is not a function", 19);
|
|
scr_throw_error_msg(SCR_ERR_TYPE, message, n + 18);
|
|
free(message);
|
|
}
|
|
|
|
static bool sf_is_error_dyn(const ScrDyn *d) {
|
|
if (!d || d->kind != SCR_DYN_OBJ) return false;
|
|
const ScrDyn *mark = scr_dyn_obj_get(d, "%error", 6);
|
|
return mark && mark->kind == SCR_DYN_BOOL && mark->v.b;
|
|
}
|
|
|
|
static void sf_throw_dyn_reason(ScrDyn *reason) {
|
|
if (sf_is_error_dyn(reason)) {
|
|
ScrError *e = scr_error_from_dyn(reason);
|
|
scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v,
|
|
scr_error_trace_arg());
|
|
return;
|
|
}
|
|
switch (reason ? reason->kind : SCR_DYN_UNDEF) {
|
|
case SCR_DYN_NUM:
|
|
scr_throw_f64(reason->v.num);
|
|
return;
|
|
case SCR_DYN_BOOL:
|
|
scr_throw_bool(reason->v.b);
|
|
return;
|
|
case SCR_DYN_STR:
|
|
scr_throw_str(scr_str_retain(reason->v.str));
|
|
return;
|
|
default:
|
|
scr_throw_ref(scr_dyn_retain(reason ? reason : scr_dyn_undefined()),
|
|
&scr_dyn_retain_v, &scr_dyn_release_v, NULL);
|
|
return;
|
|
}
|
|
}
|
|
|
|
static void sf_reject_promise_reason(ScrPromise *p, ScrDyn *reason) {
|
|
sf_throw_dyn_reason(reason);
|
|
scr_promise_reject_pending(p);
|
|
}
|
|
|
|
static ScrDyn *sf_dom_reason(const char *name, const char *message,
|
|
ScrError **error_out) {
|
|
ScrStr *message_str = scr_str_new(message, strlen(message));
|
|
ScrStr *name_str = scr_str_new(name, strlen(name));
|
|
ScrDyn *m = scr_dyn_new_str(message_str);
|
|
ScrDyn *n = scr_dyn_new_str(name_str);
|
|
scr_str_release(message_str);
|
|
scr_str_release(name_str);
|
|
ScrError *e = scr_domex_new(m, n);
|
|
scr_dyn_release(m);
|
|
scr_dyn_release(n);
|
|
ScrDyn *d = scr_dyn_from_error(e);
|
|
*error_out = e; /* constructor +1 moves to the signal */
|
|
return d;
|
|
}
|
|
|
|
static ScrDyn *sf_type_reason(const char *message, ScrError **error_out) {
|
|
ScrStr *message_str = scr_str_new(message, strlen(message));
|
|
ScrError *e = scr_error_new(SCR_ERR_TYPE, message_str);
|
|
scr_str_release(message_str);
|
|
ScrDyn *d = scr_dyn_from_error(e);
|
|
*error_out = e;
|
|
return d;
|
|
}
|
|
|
|
/* ── AbortSignal ─────────────────────────────────────────────────── */
|
|
|
|
static void sf_signal_track_callbacks(SfSignal *s) {
|
|
if (s->callbacks_tracked) return;
|
|
s->callbacks_tracked = true;
|
|
s->callbacks_next = sf_callback_signals;
|
|
sf_callback_signals = s;
|
|
}
|
|
|
|
static void sf_signal_untrack_callbacks(SfSignal *s) {
|
|
if (!s->callbacks_tracked) return;
|
|
for (SfSignal **at = &sf_callback_signals; *at;
|
|
at = &(*at)->callbacks_next) {
|
|
if (*at == s) {
|
|
*at = s->callbacks_next;
|
|
break;
|
|
}
|
|
}
|
|
s->callbacks_tracked = false;
|
|
s->callbacks_next = NULL;
|
|
}
|
|
|
|
static SfSignal *sf_signal_new(void) {
|
|
SfSignal *s = calloc(1, sizeof *s);
|
|
if (!s) sf_oom();
|
|
s->rc = 1;
|
|
return s;
|
|
}
|
|
|
|
static SfSignal *sf_signal_retain(SfSignal *s) {
|
|
s->rc++;
|
|
return s;
|
|
}
|
|
|
|
static void sf_signal_release(SfSignal *s);
|
|
|
|
static void sf_watch_free(SfSignalWatch *w) {
|
|
if (!w) return;
|
|
if (w->source) {
|
|
for (SfSignalWatch **at = &w->source->watchers; *at; at = &(*at)->next) {
|
|
if (*at == w) {
|
|
*at = w->next;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
free(w);
|
|
}
|
|
|
|
static void sf_abort_listener_free(SfAbortListener *l) {
|
|
if (!l) return;
|
|
sf_watch_free(l->signal_watch);
|
|
if (l->option_signal && l->option_signal != l->target) {
|
|
sf_signal_release(l->option_signal);
|
|
}
|
|
scr_dyn_release(l->listener);
|
|
free(l);
|
|
}
|
|
|
|
static void sf_signal_drop_callbacks(SfSignal *s) {
|
|
SfAbortListener *listeners = s->listeners;
|
|
ScrDyn *onabort = s->onabort;
|
|
s->listeners = NULL; /* unlink before callback releases can re-enter */
|
|
s->onabort = NULL;
|
|
s->onabort_order = 0;
|
|
sf_signal_untrack_callbacks(s);
|
|
while (listeners) {
|
|
SfAbortListener *next = listeners->next;
|
|
listeners->next = NULL;
|
|
sf_abort_listener_free(listeners);
|
|
listeners = next;
|
|
}
|
|
scr_dyn_release(onabort);
|
|
}
|
|
|
|
static void sf_signal_release(SfSignal *s) {
|
|
if (!s || --s->rc > 0) return;
|
|
for (size_t i = 0; i < s->source_count; i++) {
|
|
sf_watch_free(s->source_watches[i]);
|
|
sf_signal_release(s->sources[i]);
|
|
}
|
|
free(s->source_watches);
|
|
free(s->sources);
|
|
sf_signal_drop_callbacks(s);
|
|
scr_dyn_release(s->reason);
|
|
scr_error_release(s->error_reason);
|
|
free(s);
|
|
}
|
|
|
|
static void *sf_signal_retain_v(void *p) {
|
|
return sf_signal_retain((SfSignal *)p);
|
|
}
|
|
static void sf_signal_release_v(void *p) {
|
|
sf_signal_release((SfSignal *)p);
|
|
}
|
|
|
|
static SfEvent *sf_event_new(SfSignal *target) {
|
|
SfEvent *event = calloc(1, sizeof *event);
|
|
if (!event) sf_oom();
|
|
event->rc = 1;
|
|
event->target = sf_signal_retain(target);
|
|
event->time_stamp = scr_perf_now();
|
|
event->dispatching = true;
|
|
return event;
|
|
}
|
|
|
|
static SfEvent *sf_event_retain(SfEvent *event) {
|
|
event->rc++;
|
|
return event;
|
|
}
|
|
|
|
static void sf_event_release(SfEvent *event) {
|
|
if (!event || --event->rc > 0) return;
|
|
sf_signal_release(event->target);
|
|
free(event);
|
|
}
|
|
|
|
static void *sf_event_retain_v(void *ptr) {
|
|
return sf_event_retain((SfEvent *)ptr);
|
|
}
|
|
|
|
static void sf_event_release_v(void *ptr) {
|
|
sf_event_release((SfEvent *)ptr);
|
|
}
|
|
|
|
static void *sf_listener_error_retain_v(void *ptr) {
|
|
return scr_caught_retain((ScrCaught *)ptr);
|
|
}
|
|
|
|
static void sf_listener_error_release_v(void *ptr) {
|
|
scr_caught_release((ScrCaught *)ptr);
|
|
}
|
|
|
|
static void sf_listener_error_tick(ScrClosure *cb) {
|
|
ScrCaught *caught = scr_box_get_ref(cb->caps[0]);
|
|
scr_rethrow(caught);
|
|
scr_caught_release(caught);
|
|
}
|
|
|
|
/*
|
|
* EventTarget reports listener exceptions as uncaught next-tick failures;
|
|
* dispatchEvent itself keeps running listeners and returns its boolean.
|
|
* Queue every failure separately so an uncaughtException handler that
|
|
* recovers from one still observes later listener failures in order.
|
|
*/
|
|
static void sf_defer_listener_error(ScrCaught *caught) {
|
|
ScrClosure *cb =
|
|
scr_closure_new((void *)&sf_listener_error_tick, 1);
|
|
cb->caps[0] = scr_box_new_obj(
|
|
&sf_listener_error_retain_v, &sf_listener_error_release_v, NULL);
|
|
scr_box_set_ref(cb->caps[0], caught); /* moves */
|
|
scr_next_tick(cb);
|
|
}
|
|
|
|
static ScrDyn *sf_event_invoke(void *ptr, ScrDyn *self,
|
|
const char *method, ScrDyn *const *args,
|
|
size_t argc, const char *what) {
|
|
SfEvent *event = ptr;
|
|
(void)self;
|
|
(void)args;
|
|
(void)argc;
|
|
if (strcmp(method, "preventDefault") == 0) {
|
|
/* Abort events are not cancelable. */
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
if (strcmp(method, "stopPropagation") == 0) {
|
|
event->stop_propagation = true;
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
if (strcmp(method, "stopImmediatePropagation") == 0) {
|
|
event->stop_propagation = true;
|
|
event->stop_immediate = true;
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
if (strcmp(method, "composedPath") == 0) {
|
|
ScrDyn *path = scr_dyn_new_arr();
|
|
if (event->dispatching) {
|
|
scr_dyn_arr_push(
|
|
path,
|
|
scr_dyn_new_handle(event->target, SCR_DYNH_ABORT_SIGNAL));
|
|
}
|
|
return path;
|
|
}
|
|
sf_not_function(what);
|
|
return NULL;
|
|
}
|
|
|
|
static ScrDyn *sf_event_get(void *ptr, const char *key, size_t len) {
|
|
SfEvent *event = ptr;
|
|
if (sf_name(key, len, "type")) {
|
|
ScrStr *type = scr_str_new("abort", 5);
|
|
ScrDyn *out = scr_dyn_new_str(type);
|
|
scr_str_release(type);
|
|
return out;
|
|
}
|
|
if (sf_name(key, len, "bubbles") ||
|
|
sf_name(key, len, "cancelable") ||
|
|
sf_name(key, len, "composed") ||
|
|
sf_name(key, len, "defaultPrevented")) {
|
|
return scr_dyn_new_bool(false);
|
|
}
|
|
if (sf_name(key, len, "eventPhase")) {
|
|
return scr_dyn_new_num(event->dispatching ? 2 : 0);
|
|
}
|
|
if (sf_name(key, len, "target") ||
|
|
sf_name(key, len, "srcElement")) {
|
|
return scr_dyn_new_handle(event->target, SCR_DYNH_ABORT_SIGNAL);
|
|
}
|
|
if (sf_name(key, len, "currentTarget")) {
|
|
return event->dispatching
|
|
? scr_dyn_new_handle(event->target, SCR_DYNH_ABORT_SIGNAL)
|
|
: scr_dyn_new_null();
|
|
}
|
|
if (sf_name(key, len, "isTrusted")) return scr_dyn_new_bool(true);
|
|
if (sf_name(key, len, "timeStamp")) {
|
|
return scr_dyn_new_num(event->time_stamp);
|
|
}
|
|
if (sf_name(key, len, "cancelBubble")) {
|
|
return scr_dyn_new_bool(event->stop_propagation);
|
|
}
|
|
if (sf_name(key, len, "returnValue")) return scr_dyn_new_bool(true);
|
|
return NULL;
|
|
}
|
|
|
|
static bool sf_event_set(void *ptr, const char *key, size_t len,
|
|
const ScrDyn *value) {
|
|
SfEvent *event = ptr;
|
|
if (sf_name(key, len, "cancelBubble")) {
|
|
if (scr_dyn_truthy(value)) event->stop_propagation = true;
|
|
return true;
|
|
}
|
|
if (sf_name(key, len, "returnValue")) {
|
|
/* Setting false only prevents a cancelable event's default action. */
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static SfSignalWatch *sf_signal_watch(SfSignal *source, void *owner,
|
|
void (*fire)(SfSignalWatch *,
|
|
SfSignal *)) {
|
|
SfSignalWatch *w = calloc(1, sizeof *w);
|
|
if (!w) sf_oom();
|
|
w->source = source;
|
|
w->owner = owner;
|
|
w->fire = fire;
|
|
w->next = source->watchers;
|
|
source->watchers = w;
|
|
return w;
|
|
}
|
|
|
|
static void sf_listener_signal_abort(SfSignalWatch *w,
|
|
SfSignal *source) {
|
|
(void)source;
|
|
SfAbortListener *listener = w->owner;
|
|
SfSignal *target = listener->target;
|
|
for (SfAbortListener **at = &target->listeners; *at;
|
|
at = &(*at)->next) {
|
|
if (*at == listener) {
|
|
*at = listener->next;
|
|
sf_abort_listener_free(listener);
|
|
return;
|
|
}
|
|
}
|
|
sf_watch_free(w);
|
|
}
|
|
|
|
static void sf_signal_dispatch_listeners(SfSignal *s, ScrDyn *provided_event) {
|
|
SfEvent *event_state;
|
|
ScrDyn *event;
|
|
if (provided_event) {
|
|
event = scr_dyn_retain(provided_event);
|
|
event_state = (SfEvent *)provided_event->v.handle.ptr;
|
|
SfSignal *old_target = event_state->target;
|
|
event_state->target = sf_signal_retain(s);
|
|
sf_signal_release(old_target);
|
|
event_state->dispatching = true;
|
|
} else {
|
|
event_state = sf_event_new(s);
|
|
event = scr_dyn_new_handle(event_state, SCR_DYNH_EVENT);
|
|
sf_event_release(event_state);
|
|
}
|
|
/*
|
|
* EventTarget dispatch snapshots the handlers present at the start:
|
|
* additions wait for the next event, removals before a listener's turn
|
|
* suppress it, and a listener may remove itself safely. `onabort` occupies
|
|
* the position where it was first assigned, just like an event-handler
|
|
* attribute in the DOM. Holding raw list nodes across the user callback is
|
|
* not safe because removeEventListener frees those nodes.
|
|
*/
|
|
size_t count = s->onabort && s->onabort->kind == SCR_DYN_FUNC ? 1 : 0;
|
|
for (SfAbortListener *l = s->listeners; l; l = l->next) count++;
|
|
typedef struct {
|
|
ScrDyn *listener;
|
|
bool once;
|
|
bool attribute;
|
|
size_t id;
|
|
} SfAbortDispatch;
|
|
SfAbortDispatch *dispatch =
|
|
count ? calloc(count, sizeof *dispatch) : NULL;
|
|
if (count && !dispatch) sf_oom();
|
|
size_t i = 0;
|
|
if (s->onabort && s->onabort->kind == SCR_DYN_FUNC) {
|
|
dispatch[i].listener = scr_dyn_retain(s->onabort);
|
|
dispatch[i].attribute = true;
|
|
dispatch[i].id = s->onabort_order;
|
|
i++;
|
|
}
|
|
for (SfAbortListener *l = s->listeners; l; l = l->next) {
|
|
dispatch[i].listener = scr_dyn_retain(l->listener);
|
|
dispatch[i].once = l->once;
|
|
dispatch[i].id = l->id;
|
|
i++;
|
|
}
|
|
for (i = 1; i < count; i++) {
|
|
SfAbortDispatch entry = dispatch[i];
|
|
size_t j = i;
|
|
while (j > 0 && dispatch[j - 1].id > entry.id) {
|
|
dispatch[j] = dispatch[j - 1];
|
|
j--;
|
|
}
|
|
dispatch[j] = entry;
|
|
}
|
|
|
|
for (i = 0; i < count && !event_state->stop_immediate; i++) {
|
|
if (dispatch[i].attribute) {
|
|
if (!s->onabort || s->onabort_order != dispatch[i].id) {
|
|
scr_dyn_release(dispatch[i].listener);
|
|
continue;
|
|
}
|
|
/*
|
|
* Assignment replaces the handler without moving its registration
|
|
* position. Invoke the current value if an earlier callback replaced
|
|
* it during this dispatch.
|
|
*/
|
|
ScrDyn *handler = scr_dyn_retain(s->onabort);
|
|
ScrDyn *args[1] = {event};
|
|
scr_dyn_this_push(s, SCR_DYNH_ABORT_SIGNAL);
|
|
ScrDyn *r = scr_dyn_call(handler, args, 1, "signal.onabort");
|
|
scr_dyn_this_pop();
|
|
scr_dyn_release(r);
|
|
scr_dyn_release(handler);
|
|
scr_dyn_release(dispatch[i].listener);
|
|
if (scr_exc_pending()) {
|
|
sf_defer_listener_error(scr_exc_take());
|
|
}
|
|
if (event_state->stop_immediate) {
|
|
i++;
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
SfAbortListener **at = &s->listeners;
|
|
while (*at && (*at)->id != dispatch[i].id) {
|
|
at = &(*at)->next;
|
|
}
|
|
if (!*at) {
|
|
scr_dyn_release(dispatch[i].listener);
|
|
continue;
|
|
}
|
|
if (dispatch[i].once) {
|
|
SfAbortListener *l = *at;
|
|
*at = l->next;
|
|
sf_abort_listener_free(l);
|
|
}
|
|
ScrDyn *callable = dispatch[i].listener;
|
|
ScrDyn *current_listener = NULL;
|
|
ScrDyn *owned_handle = NULL;
|
|
ScrDyn *this_listener = NULL;
|
|
if (callable->kind == SCR_DYN_OBJ) {
|
|
/*
|
|
* EventListener is a Web IDL callback interface: handleEvent is read
|
|
* when the callback runs, not when it is registered. Typed records
|
|
* normally cross into dyn as value snapshots, so explicitly refresh
|
|
* an identity-carrying listener from its retained source here.
|
|
*/
|
|
current_listener =
|
|
callable->v.obj.source_identity && callable->v.obj.source_access
|
|
? callable->v.obj.source_access(
|
|
callable->v.obj.source_identity, true)
|
|
: scr_dyn_retain(callable);
|
|
this_listener = current_listener;
|
|
const ScrDyn *handle =
|
|
scr_dyn_obj_get(current_listener, "handleEvent", 11);
|
|
if (!handle) {
|
|
scr_dyn_release(current_listener);
|
|
scr_dyn_release(dispatch[i].listener);
|
|
continue;
|
|
}
|
|
callable = (ScrDyn *)handle;
|
|
} else if (callable->kind == SCR_DYN_TYPED_REF) {
|
|
/* A live static record must remain the EventListener receiver so
|
|
* handleEvent mutations commit to the original source. Arrays and
|
|
* byte views are valid Web IDL objects but have no handleEvent in
|
|
* the statically modeled surface, so dispatching them is a no-op. */
|
|
current_listener = scr_dyn_typed_ref_materialize(callable);
|
|
if (current_listener->kind != SCR_DYN_OBJ) {
|
|
scr_dyn_release(current_listener);
|
|
scr_dyn_release(dispatch[i].listener);
|
|
continue;
|
|
}
|
|
const ScrDyn *handle =
|
|
scr_dyn_obj_get(current_listener, "handleEvent", 11);
|
|
if (!handle) {
|
|
scr_dyn_release(current_listener);
|
|
scr_dyn_release(dispatch[i].listener);
|
|
continue;
|
|
}
|
|
this_listener = callable;
|
|
callable = (ScrDyn *)handle;
|
|
} else if (callable->kind == SCR_DYN_JSVAL &&
|
|
!scr_dyn_isl_typeof_is(callable, "function")) {
|
|
ScrStr *key = scr_str_new("handleEvent", 11);
|
|
owned_handle = scr_dyn_isl_key_get(callable, key);
|
|
scr_str_release(key);
|
|
if (!owned_handle) {
|
|
scr_dyn_release(dispatch[i].listener);
|
|
if (scr_exc_pending()) {
|
|
sf_defer_listener_error(scr_exc_take());
|
|
}
|
|
continue;
|
|
}
|
|
if (owned_handle->kind == SCR_DYN_UNDEF) {
|
|
scr_dyn_release(owned_handle);
|
|
scr_dyn_release(dispatch[i].listener);
|
|
continue;
|
|
}
|
|
this_listener = callable;
|
|
callable = owned_handle;
|
|
} else if (callable->kind != SCR_DYN_FUNC) {
|
|
/* Web IDL accepts every object as an EventListener. Native arrays,
|
|
* byte views, handles, and promises cannot carry an expando
|
|
* handleEvent in the static runtime, so a missing method is a no-op. */
|
|
scr_dyn_release(dispatch[i].listener);
|
|
continue;
|
|
}
|
|
ScrDyn *args[1] = {event};
|
|
if (this_listener) {
|
|
scr_dyn_this_push_dyn(this_listener);
|
|
} else {
|
|
scr_dyn_this_push(s, SCR_DYNH_ABORT_SIGNAL);
|
|
}
|
|
ScrDyn *r =
|
|
scr_dyn_call(callable, args, 1, "abort listener");
|
|
scr_dyn_this_pop();
|
|
scr_dyn_release(r);
|
|
scr_dyn_release(owned_handle);
|
|
scr_dyn_release(current_listener);
|
|
scr_dyn_release(dispatch[i].listener);
|
|
if (scr_exc_pending()) {
|
|
sf_defer_listener_error(scr_exc_take());
|
|
}
|
|
if (event_state->stop_immediate) {
|
|
i++;
|
|
break;
|
|
}
|
|
}
|
|
while (i < count) {
|
|
scr_dyn_release(dispatch[i].listener);
|
|
i++;
|
|
}
|
|
event_state->dispatching = false;
|
|
free(dispatch);
|
|
scr_dyn_release(event);
|
|
}
|
|
|
|
static void sf_signal_abort_full(SfSignal *s, ScrDyn *reason,
|
|
ScrError *error_reason) {
|
|
if (s->aborted) return;
|
|
s->aborted = true;
|
|
s->reason = scr_dyn_retain(reason);
|
|
s->error_reason = error_reason ? scr_error_retain(error_reason) : NULL;
|
|
while (s->watchers) {
|
|
SfSignalWatch *w = s->watchers;
|
|
s->watchers = w->next;
|
|
w->source = NULL;
|
|
w->fire(w, s); /* the owner may free w */
|
|
}
|
|
sf_signal_dispatch_listeners(s, NULL);
|
|
}
|
|
|
|
static void sf_signal_abort_default(SfSignal *s, bool timeout) {
|
|
ScrError *e = NULL;
|
|
ScrDyn *reason = sf_dom_reason(
|
|
timeout ? "TimeoutError" : "AbortError",
|
|
timeout ? "The operation was aborted due to timeout"
|
|
: "This operation was aborted",
|
|
&e);
|
|
sf_signal_abort_full(s, reason, e);
|
|
scr_dyn_release(reason);
|
|
scr_error_release(e);
|
|
}
|
|
|
|
static SfSignal *sf_signal_of(const ScrDyn *d, const char *where) {
|
|
if (!d || d->kind != SCR_DYN_HANDLE ||
|
|
d->v.handle.tag != SCR_DYNH_ABORT_SIGNAL) {
|
|
sf_type_error(where);
|
|
return NULL;
|
|
}
|
|
return (SfSignal *)d->v.handle.ptr;
|
|
}
|
|
|
|
static void sf_signal_timer_fire(ScrClosure *cb) {
|
|
SfSignal *s = (SfSignal *)scr_box_get_ref(cb->caps[0]);
|
|
if (!s) return;
|
|
s->timer_id = 0;
|
|
sf_signal_abort_default(s, true);
|
|
sf_signal_release(s);
|
|
}
|
|
|
|
ScrDyn *scr_fetch_abort_timeout(ScrDyn *delay) {
|
|
if (!delay || delay->kind != SCR_DYN_NUM) {
|
|
if (delay) {
|
|
scr_dyn_arg_type_fail("delay", "of type number", delay);
|
|
} else {
|
|
static const char message[] =
|
|
"The \"delay\" argument must be of type number. Received undefined";
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1,
|
|
"ERR_INVALID_ARG_TYPE");
|
|
}
|
|
return NULL;
|
|
}
|
|
double ms = delay->v.num;
|
|
if (!isfinite(ms) || trunc(ms) != ms || ms < 0 || ms > 4294967295.0) {
|
|
char received[48];
|
|
scr_num_received(ms, received);
|
|
const char *range =
|
|
!isfinite(ms) || trunc(ms) != ms
|
|
? "an integer"
|
|
: ">= 0 && <= 4294967295";
|
|
char message[224];
|
|
int len = snprintf(
|
|
message, sizeof message,
|
|
"The value of \"delay\" is out of range. It must be %s. Received %s",
|
|
range, received);
|
|
if (len < 0 || (size_t)len >= sizeof message) sf_oom();
|
|
scr_throw_error_msg_code(SCR_ERR_RANGE, message, (size_t)len,
|
|
"ERR_OUT_OF_RANGE");
|
|
return NULL;
|
|
}
|
|
SfSignal *s = sf_signal_new();
|
|
ScrClosure *cb = scr_closure_new((void *)&sf_signal_timer_fire, 1);
|
|
ScrBox *box =
|
|
scr_box_new_obj(&sf_signal_retain_v, &sf_signal_release_v, NULL);
|
|
scr_box_set_ref(box, sf_signal_retain(s));
|
|
cb->caps[0] = box;
|
|
s->timer_id = scr_set_timeout_handle(cb, ms);
|
|
scr_timer_unref(s->timer_id);
|
|
ScrDyn *out = scr_dyn_new_handle(s, SCR_DYNH_ABORT_SIGNAL);
|
|
sf_signal_release(s);
|
|
return out;
|
|
}
|
|
|
|
ScrDyn *scr_fetch_abort_now(ScrDyn *reason) {
|
|
SfSignal *s = sf_signal_new();
|
|
if (!reason || reason->kind == SCR_DYN_UNDEF) {
|
|
sf_signal_abort_default(s, false);
|
|
} else {
|
|
ScrError *e = sf_is_error_dyn(reason) ? scr_error_from_dyn(reason) : NULL;
|
|
sf_signal_abort_full(s, reason, e);
|
|
scr_error_release(e);
|
|
}
|
|
ScrDyn *out = scr_dyn_new_handle(s, SCR_DYNH_ABORT_SIGNAL);
|
|
sf_signal_release(s);
|
|
return out;
|
|
}
|
|
|
|
static void sf_any_source_abort(SfSignalWatch *w, SfSignal *source) {
|
|
SfSignal *out = (SfSignal *)w->owner;
|
|
sf_signal_abort_full(out, source->reason, source->error_reason);
|
|
}
|
|
|
|
ScrDyn *scr_fetch_abort_any(ScrDyn *signals) {
|
|
if (!signals || signals->kind != SCR_DYN_ARR) {
|
|
static const char message[] =
|
|
"signals can not be converted to sequence.";
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1,
|
|
"ERR_INVALID_ARG_TYPE");
|
|
return NULL;
|
|
}
|
|
SfSignal *out = sf_signal_new();
|
|
out->source_count = signals->v.arr.len;
|
|
if (out->source_count) {
|
|
out->sources = calloc(out->source_count, sizeof *out->sources);
|
|
out->source_watches =
|
|
calloc(out->source_count, sizeof *out->source_watches);
|
|
if (!out->sources || !out->source_watches) sf_oom();
|
|
}
|
|
for (size_t i = 0; i < out->source_count; i++) {
|
|
SfSignal *source =
|
|
sf_signal_of(signals->v.arr.items[i],
|
|
"AbortSignal.any requires an array of AbortSignals");
|
|
if (!source) {
|
|
sf_signal_release(out);
|
|
return NULL;
|
|
}
|
|
out->sources[i] = sf_signal_retain(source);
|
|
if (source->aborted) {
|
|
sf_signal_abort_full(out, source->reason, source->error_reason);
|
|
} else {
|
|
out->source_watches[i] =
|
|
sf_signal_watch(source, out, &sf_any_source_abort);
|
|
}
|
|
}
|
|
ScrDyn *boxed = scr_dyn_new_handle(out, SCR_DYNH_ABORT_SIGNAL);
|
|
sf_signal_release(out);
|
|
return boxed;
|
|
}
|
|
|
|
static bool sf_signal_listener_options(
|
|
ScrDyn *const *args, size_t argc, bool *capture, bool *once,
|
|
SfSignal **option_signal) {
|
|
*capture = false;
|
|
*once = false;
|
|
if (option_signal) *option_signal = NULL;
|
|
if (argc < 3) return true;
|
|
if (args[2]->kind == SCR_DYN_BOOL) {
|
|
/* EventTarget's boolean overload is `useCapture`, never `once`. */
|
|
*capture = args[2]->v.b;
|
|
return true;
|
|
}
|
|
if (args[2]->kind != SCR_DYN_OBJ) return true;
|
|
const ScrDyn *capture_value =
|
|
scr_dyn_obj_get(args[2], "capture", 7);
|
|
const ScrDyn *once_value = scr_dyn_obj_get(args[2], "once", 4);
|
|
*capture = capture_value && scr_dyn_truthy(capture_value);
|
|
*once = once_value && scr_dyn_truthy(once_value);
|
|
if (option_signal) {
|
|
const ScrDyn *signal_value =
|
|
scr_dyn_obj_get(args[2], "signal", 6);
|
|
if (signal_value && signal_value->kind != SCR_DYN_UNDEF) {
|
|
*option_signal = sf_signal_of(
|
|
signal_value,
|
|
"AddEventListenerOptions.signal must be an AbortSignal");
|
|
if (!*option_signal) return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool sf_abort_listener_value(const ScrDyn *listener) {
|
|
/* Web IDL callback interfaces accept any object. handleEvent is looked up
|
|
* when the event is dispatched, where a non-callable value reports an
|
|
* asynchronous listener error and a missing value is a no-op. */
|
|
switch (listener->kind) {
|
|
case SCR_DYN_FUNC:
|
|
case SCR_DYN_OBJ:
|
|
case SCR_DYN_ARR:
|
|
case SCR_DYN_BYTES:
|
|
case SCR_DYN_HANDLE:
|
|
case SCR_DYN_PROMISE:
|
|
return true;
|
|
case SCR_DYN_JSVAL:
|
|
case SCR_DYN_TYPED_REF:
|
|
return scr_dyn_isl_typeof_is(listener, "function") ||
|
|
scr_dyn_isl_typeof_is(listener, "object");
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static bool sf_abort_listener_equal(
|
|
const ScrDyn *a, const ScrDyn *b) {
|
|
return scr_dyn_strict_eq(a, b) || scr_dyn_obj_same_source(a, b);
|
|
}
|
|
|
|
static ScrDyn *sf_signal_invoke(void *ptr, ScrDyn *self, const char *method,
|
|
ScrDyn *const *args, size_t argc,
|
|
const char *what) {
|
|
SfSignal *s = ptr;
|
|
if (strcmp(method, "throwIfAborted") == 0) {
|
|
if (s->aborted) {
|
|
sf_throw_dyn_reason(s->reason);
|
|
return NULL;
|
|
}
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
if (strcmp(method, "addEventListener") == 0) {
|
|
if (argc < 2) {
|
|
sf_type_error("AbortSignal.addEventListener requires an event name");
|
|
return NULL;
|
|
}
|
|
ScrStr *event_name = scr_dyn_string_coerce_js(args[0]);
|
|
if (!event_name) return NULL;
|
|
bool is_abort_event =
|
|
sf_name(event_name->data, event_name->len, "abort");
|
|
scr_str_release(event_name);
|
|
/* Web IDL's nullable EventListener argument: null is a no-op. */
|
|
if (args[1]->kind == SCR_DYN_NULL ||
|
|
args[1]->kind == SCR_DYN_UNDEF) {
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
if (!sf_abort_listener_value(args[1])) {
|
|
sf_type_error(
|
|
"AbortSignal.addEventListener listener must be a function, "
|
|
"an object with handleEvent, or null");
|
|
return NULL;
|
|
}
|
|
bool capture = false;
|
|
bool once = false;
|
|
SfSignal *option_signal = NULL;
|
|
if (!sf_signal_listener_options(
|
|
args, argc, &capture, &once, &option_signal)) {
|
|
return NULL;
|
|
}
|
|
/* AbortSignal is an EventTarget: listeners for other event names are
|
|
* valid registrations even though this target only dispatches abort. */
|
|
if (!is_abort_event) {
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
if (option_signal && option_signal->aborted) {
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
SfAbortListener **tail = &s->listeners;
|
|
while (*tail) {
|
|
if ((*tail)->capture == capture &&
|
|
sf_abort_listener_equal((*tail)->listener, args[1])) {
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
tail = &(*tail)->next;
|
|
}
|
|
SfAbortListener *l = calloc(1, sizeof *l);
|
|
if (!l) sf_oom();
|
|
l->listener = scr_dyn_retain(args[1]);
|
|
l->capture = capture;
|
|
l->once = once;
|
|
l->id = ++s->next_listener_id;
|
|
l->target = s;
|
|
l->option_signal =
|
|
option_signal && option_signal != s
|
|
? sf_signal_retain(option_signal)
|
|
: option_signal;
|
|
*tail = l;
|
|
if (option_signal) {
|
|
l->signal_watch =
|
|
sf_signal_watch(option_signal, l, &sf_listener_signal_abort);
|
|
}
|
|
sf_signal_track_callbacks(s);
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
if (strcmp(method, "removeEventListener") == 0) {
|
|
if (argc < 2) {
|
|
sf_type_error(
|
|
"AbortSignal.removeEventListener requires an event name and listener");
|
|
return NULL;
|
|
}
|
|
ScrStr *event_name = scr_dyn_string_coerce_js(args[0]);
|
|
if (!event_name) return NULL;
|
|
bool is_abort_event =
|
|
sf_name(event_name->data, event_name->len, "abort");
|
|
scr_str_release(event_name);
|
|
if (is_abort_event) {
|
|
bool capture = false;
|
|
bool once = false;
|
|
(void)sf_signal_listener_options(
|
|
args, argc, &capture, &once, NULL);
|
|
(void)once;
|
|
for (SfAbortListener **at = &s->listeners; *at; at = &(*at)->next) {
|
|
if ((*at)->capture == capture &&
|
|
sf_abort_listener_equal((*at)->listener, args[1])) {
|
|
SfAbortListener *l = *at;
|
|
*at = l->next;
|
|
sf_abort_listener_free(l);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
if (strcmp(method, "dispatchEvent") == 0) {
|
|
if (argc != 1 || args[0]->kind != SCR_DYN_HANDLE ||
|
|
args[0]->v.handle.tag != SCR_DYNH_EVENT) {
|
|
sf_type_error("AbortSignal.dispatchEvent requires an Event");
|
|
return NULL;
|
|
}
|
|
SfEvent *event = (SfEvent *)args[0]->v.handle.ptr;
|
|
if (event->dispatching) {
|
|
sf_type_error("The event is already being dispatched");
|
|
return NULL;
|
|
}
|
|
sf_signal_dispatch_listeners(s, args[0]);
|
|
if (scr_exc_pending()) return NULL;
|
|
return scr_dyn_new_bool(true);
|
|
}
|
|
(void)self;
|
|
sf_not_function(what);
|
|
return NULL;
|
|
}
|
|
|
|
static ScrDyn *sf_signal_get(void *ptr, const char *key, size_t len) {
|
|
SfSignal *s = ptr;
|
|
if (sf_name(key, len, "aborted")) return scr_dyn_new_bool(s->aborted);
|
|
if (sf_name(key, len, "reason")) {
|
|
return s->aborted ? scr_dyn_retain(s->reason)
|
|
: scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
if (sf_name(key, len, "onabort")) {
|
|
return s->onabort ? scr_dyn_retain(s->onabort) : scr_dyn_new_null();
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static bool sf_signal_set(void *ptr, const char *key, size_t len,
|
|
const ScrDyn *value) {
|
|
SfSignal *s = ptr;
|
|
if (!sf_name(key, len, "onabort")) return false;
|
|
bool had_handler = s->onabort && s->onabort->kind == SCR_DYN_FUNC;
|
|
scr_dyn_release(s->onabort);
|
|
if (value->kind != SCR_DYN_NULL && value->kind != SCR_DYN_UNDEF) {
|
|
s->onabort = scr_dyn_retain((ScrDyn *)value);
|
|
} else {
|
|
s->onabort = NULL;
|
|
}
|
|
if (value->kind == SCR_DYN_FUNC) {
|
|
if (!had_handler) s->onabort_order = ++s->next_listener_id;
|
|
sf_signal_track_callbacks(s);
|
|
} else {
|
|
s->onabort_order = 0;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* ── ReadableStream ──────────────────────────────────────────────── */
|
|
|
|
static void sf_stream_track_callbacks(SfStream *s) {
|
|
if (s->callbacks_tracked || (!s->pull_cb && !s->cancel_cb)) return;
|
|
s->callbacks_tracked = true;
|
|
s->callbacks_next = sf_callback_streams;
|
|
sf_callback_streams = s;
|
|
}
|
|
|
|
static void sf_stream_untrack_callbacks(SfStream *s) {
|
|
if (!s->callbacks_tracked) return;
|
|
for (SfStream **at = &sf_callback_streams; *at;
|
|
at = &(*at)->callbacks_next) {
|
|
if (*at == s) {
|
|
*at = s->callbacks_next;
|
|
break;
|
|
}
|
|
}
|
|
s->callbacks_tracked = false;
|
|
s->callbacks_next = NULL;
|
|
}
|
|
|
|
static SfStream *sf_stream_new_native(void) {
|
|
SfStream *s = calloc(1, sizeof *s);
|
|
if (!s) sf_oom();
|
|
s->rc = 1;
|
|
s->started = true;
|
|
return s;
|
|
}
|
|
|
|
static SfStream *sf_stream_retain(SfStream *s) {
|
|
s->rc++;
|
|
return s;
|
|
}
|
|
|
|
static SfReader *sf_reader_retain(SfReader *r) {
|
|
r->rc++;
|
|
return r;
|
|
}
|
|
|
|
static SfReadRequest *sf_reader_take_request(SfReader *r) {
|
|
SfReadRequest *request = r->pending_head;
|
|
if (!request) return NULL;
|
|
r->pending_head = request->next;
|
|
if (!r->pending_head) r->pending_tail = NULL;
|
|
request->next = NULL;
|
|
return request;
|
|
}
|
|
|
|
static void sf_reader_reject_message_all(SfReader *r, const char *message) {
|
|
SfReadRequest *request;
|
|
while ((request = sf_reader_take_request(r)) != NULL) {
|
|
sf_type_error(message);
|
|
scr_promise_reject_pending(request->promise);
|
|
scr_promise_release(request->promise);
|
|
free(request);
|
|
}
|
|
}
|
|
|
|
static void sf_reader_release(SfReader *r) {
|
|
if (!r || --r->rc > 0) return;
|
|
sf_reader_reject_message_all(r, "ReadableStream was released");
|
|
scr_promise_release(r->closed);
|
|
free(r);
|
|
}
|
|
|
|
static void sf_chunk_release(SfChunk *c) {
|
|
if (!c) return;
|
|
scr_dyn_release(c->value);
|
|
free(c);
|
|
}
|
|
|
|
static void sf_stream_drop_chunks(SfStream *s) {
|
|
while (s->head) {
|
|
SfChunk *c = s->head;
|
|
s->head = c->next;
|
|
sf_chunk_release(c);
|
|
}
|
|
s->tail = NULL;
|
|
s->queued = 0;
|
|
}
|
|
|
|
static void sf_inflate_finish_if_ended(SfTransfer *t);
|
|
|
|
/*
|
|
* The live transfer owns one stream reference. When every user-visible
|
|
* Response/body/reader/collector reference is gone, only that transfer
|
|
* reference remains. Stop queueing at that point and drain the wire so a
|
|
* paused unread response cannot keep the process alive forever.
|
|
*
|
|
* A pending read remains a consumer even if its reader handle was dropped:
|
|
* fulfill that request first, then reconsider from sf_stream_drain().
|
|
*/
|
|
static void sf_response_discard_if_unobserved(SfStream *s) {
|
|
if (s->rc != 1 || s->discarded || !s->response_owner ||
|
|
s->collector || (s->reader && s->reader->pending_head)) {
|
|
return;
|
|
}
|
|
s->discarded = true;
|
|
sf_stream_drop_chunks(s);
|
|
SfTransfer *t = s->response_owner;
|
|
scr_bytes_release(t->inflate_pending);
|
|
t->inflate_pending = NULL;
|
|
t->inflate_needs_drain = false;
|
|
if (!t->inflate_draining) {
|
|
t->zs.next_in = Z_NULL;
|
|
t->zs.avail_in = 0;
|
|
}
|
|
/*
|
|
* EOF may already have arrived while the decoded high-water-mark chunk
|
|
* kept sf_inflate_finish_if_ended() from settling. Dropping that last
|
|
* chunk removes the only remaining blocker. Finish before attempting to
|
|
* resume an already-ended socket; settlement may release both t and s.
|
|
*/
|
|
if (t->response_ended) {
|
|
sf_inflate_finish_if_ended(t);
|
|
return;
|
|
}
|
|
if (!t->done && t->response_paused && t->response_socket) {
|
|
t->response_paused = false;
|
|
scr_net_sock_release(scr_net_sock_resume(t->response_socket));
|
|
}
|
|
}
|
|
|
|
static void sf_stream_drop_source_callbacks(
|
|
SfStream *s, bool include_cancel) {
|
|
ScrDyn *pull = s->pull_cb;
|
|
ScrDyn *cancel = include_cancel ? s->cancel_cb : NULL;
|
|
ScrDyn *source_this = NULL;
|
|
s->pull_cb = NULL;
|
|
if (include_cancel) s->cancel_cb = NULL;
|
|
if (!s->pull_cb && !s->cancel_cb) {
|
|
source_this = s->source_this;
|
|
s->source_this = NULL;
|
|
sf_stream_untrack_callbacks(s);
|
|
}
|
|
/* Unlink both edges before either closure release can re-enter through a
|
|
* captured stream handle. */
|
|
scr_dyn_release(pull);
|
|
scr_dyn_release(cancel);
|
|
scr_dyn_release(source_this);
|
|
}
|
|
|
|
static void sf_stream_release(SfStream *s) {
|
|
if (!s) return;
|
|
if (--s->rc > 0) {
|
|
sf_response_discard_if_unobserved(s);
|
|
return;
|
|
}
|
|
sf_stream_drop_chunks(s);
|
|
if (s->reader) {
|
|
s->reader->stream = NULL;
|
|
sf_reader_reject_message_all(s->reader, "ReadableStream was released");
|
|
sf_reader_release(s->reader);
|
|
s->reader = NULL;
|
|
}
|
|
scr_dyn_release(s->error);
|
|
sf_stream_drop_source_callbacks(s, true);
|
|
scr_dyn_release(s->from_dyn);
|
|
scr_arr_release(s->from_array);
|
|
scr_bytes_release(s->from_bytes);
|
|
scr_str_release(s->from_string);
|
|
while (s->typed_refs) {
|
|
SfTypedRef *ref = s->typed_refs;
|
|
s->typed_refs = ref->next;
|
|
scr_dyn_release(ref->value);
|
|
free(ref);
|
|
}
|
|
free(s);
|
|
}
|
|
|
|
static void *sf_stream_retain_v(void *p) {
|
|
return sf_stream_retain((SfStream *)p);
|
|
}
|
|
static void sf_stream_release_v(void *p) {
|
|
sf_stream_release((SfStream *)p);
|
|
}
|
|
|
|
/*
|
|
* The stream owns its active reader so the lock survives loss of the
|
|
* user-visible reader box. Conversely, every reader HANDLE retains the
|
|
* stream while the lock is live. Keeping those two ownership classes
|
|
* distinct avoids a permanent stream↔reader cycle while making chained
|
|
* expressions such as `new ReadableStream(...).getReader()` safe.
|
|
*/
|
|
static void *sf_reader_handle_retain_v(void *p) {
|
|
SfReader *r = p;
|
|
sf_reader_retain(r);
|
|
r->handle_refs++;
|
|
if (r->stream) sf_stream_retain(r->stream);
|
|
return r;
|
|
}
|
|
|
|
static void sf_reader_handle_release_v(void *p) {
|
|
SfReader *r = p;
|
|
SfStream *stream = r->stream;
|
|
if (r->handle_refs > 0) r->handle_refs--;
|
|
sf_reader_release(r);
|
|
sf_stream_release(stream);
|
|
}
|
|
|
|
static ScrBytes *sf_chunk_bytes(const ScrDyn *chunk) {
|
|
if (chunk && chunk->kind == SCR_DYN_TYPED_REF) {
|
|
ScrDyn *materialized = scr_dyn_typed_ref_materialize(chunk);
|
|
ScrBytes *bytes = sf_chunk_bytes(materialized);
|
|
scr_dyn_release(materialized);
|
|
return bytes;
|
|
}
|
|
if (chunk && chunk->kind == SCR_DYN_BYTES) {
|
|
return scr_bytes_copy(chunk->v.bytes);
|
|
}
|
|
if (chunk && chunk->kind == SCR_DYN_STR) {
|
|
ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)chunk->v.str->len);
|
|
memcpy(b->data, chunk->v.str->data, chunk->v.str->len);
|
|
return b;
|
|
}
|
|
sf_type_error("ReadableStream chunks must be Uint8Array or string values");
|
|
return NULL;
|
|
}
|
|
|
|
static ScrDyn *sf_read_result(bool done, ScrDyn *value) {
|
|
ScrDyn *result = scr_dyn_new_obj();
|
|
scr_dyn_obj_set(result, "value", 5,
|
|
value ? scr_dyn_retain(value)
|
|
: scr_dyn_retain(scr_dyn_undefined()));
|
|
scr_dyn_obj_set(result, "done", 4, scr_dyn_new_bool(done));
|
|
return result;
|
|
}
|
|
|
|
static void sf_reader_fulfill_one(SfReader *r, bool done, ScrDyn *value) {
|
|
if (!r) return;
|
|
SfReadRequest *request = sf_reader_take_request(r);
|
|
if (!request) return;
|
|
scr_promise_fulfill_ref(request->promise, sf_read_result(done, value),
|
|
&scr_dyn_retain_v, &scr_dyn_release_v, NULL);
|
|
scr_promise_release(request->promise);
|
|
free(request);
|
|
}
|
|
|
|
static void sf_reader_reject_all(SfReader *r, ScrDyn *reason) {
|
|
if (!r) return;
|
|
SfReadRequest *request;
|
|
while ((request = sf_reader_take_request(r)) != NULL) {
|
|
sf_reject_promise_reason(request->promise, reason);
|
|
scr_promise_release(request->promise);
|
|
free(request);
|
|
}
|
|
}
|
|
|
|
static void sf_collector_append(SfCollector *c, const ScrBytes *bytes) {
|
|
if (bytes->len == 0) return;
|
|
if (c->len > SIZE_MAX - bytes->len) sf_oom();
|
|
size_t need = c->len + bytes->len;
|
|
if (need > c->cap) {
|
|
size_t cap = c->cap ? c->cap : 4096;
|
|
while (cap < need) {
|
|
if (cap > SIZE_MAX / 2) {
|
|
cap = need;
|
|
break;
|
|
}
|
|
cap *= 2;
|
|
}
|
|
char *next = realloc(c->data, cap);
|
|
if (!next) sf_oom();
|
|
c->data = next;
|
|
c->cap = cap;
|
|
}
|
|
memcpy(c->data + c->len, bytes->data, bytes->len);
|
|
c->len = need;
|
|
}
|
|
|
|
static void sf_collector_drop(SfStream *s) {
|
|
SfCollector *c = s->collector;
|
|
if (!c) return;
|
|
s->collector = NULL;
|
|
/*
|
|
* Body-mixin consumption acquires a reader that is never exposed or
|
|
* released. Keep that lock after text()/json()/bytes() settles: Node
|
|
* reports response.body.locked === true and rejects a later getReader(),
|
|
* on both fulfillment and rejection.
|
|
*/
|
|
scr_promise_release(c->promise);
|
|
free(c->data);
|
|
SfStream *held = c->held_stream;
|
|
free(c);
|
|
sf_stream_release(held);
|
|
}
|
|
|
|
static void sf_collector_finish(SfStream *s) {
|
|
SfCollector *c = s->collector;
|
|
if (!c) return;
|
|
ScrDyn *value = NULL;
|
|
ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)c->len);
|
|
if (c->len) memcpy(bytes->data, c->data, c->len);
|
|
if (c->mode == SF_COLLECT_JSON) {
|
|
ScrStr *text = scr_text_decode(bytes);
|
|
value = scr_json_parse(text);
|
|
scr_str_release(text);
|
|
} else if (c->mode == SF_COLLECT_TEXT) {
|
|
ScrStr *text = scr_text_decode(bytes);
|
|
value = scr_dyn_new_str(text);
|
|
scr_str_release(text);
|
|
} else {
|
|
value = scr_dyn_new_bytes_copy(bytes);
|
|
}
|
|
scr_bytes_release(bytes);
|
|
if (scr_exc_pending()) {
|
|
scr_dyn_release(value);
|
|
scr_promise_reject_pending(c->promise);
|
|
} else {
|
|
scr_promise_fulfill_ref(c->promise, value, &scr_dyn_retain_v,
|
|
&scr_dyn_release_v, NULL);
|
|
}
|
|
sf_collector_drop(s);
|
|
}
|
|
|
|
static void sf_collector_reject(SfStream *s, ScrDyn *reason) {
|
|
if (!s->collector) return;
|
|
sf_reject_promise_reason(s->collector->promise, reason);
|
|
sf_collector_drop(s);
|
|
}
|
|
|
|
static void sf_stream_request_flush(SfStream *s);
|
|
static void sf_transfer_stream_error(SfTransfer *t, ScrDyn *reason);
|
|
static void sf_settle(SfTransfer *t);
|
|
static SfTransfer *sf_retain(SfTransfer *t);
|
|
static void sf_release(SfTransfer *t);
|
|
static void sf_inflate_resume(SfTransfer *t);
|
|
static void sf_stream_pull(SfStream *s);
|
|
static void sf_stream_schedule_initial_pull(SfStream *s);
|
|
|
|
/*
|
|
* A default ReadableStream has a one-chunk high-water mark. Apply that
|
|
* bound to the transport itself: pausing the IncomingMessage only moves
|
|
* bytes into its parser-side buffer, while pausing the socket leaves them
|
|
* in the kernel/TCP window. resume() merely re-arms the poller, so calling
|
|
* it while satisfying a read never re-enters this stack.
|
|
*/
|
|
static void sf_response_pause_if_full(SfStream *s) {
|
|
SfTransfer *t = s->response_owner;
|
|
if (s->discarded || !t || t->done || t->response_paused ||
|
|
!t->response_socket ||
|
|
s->queued < 1) {
|
|
return;
|
|
}
|
|
t->response_paused = true;
|
|
scr_net_sock_release(scr_net_sock_pause(t->response_socket));
|
|
}
|
|
|
|
static void sf_response_resume_if_ready(SfStream *s) {
|
|
SfTransfer *t = s->response_owner;
|
|
if (!t || t->done || !t->response_paused || !t->response_socket ||
|
|
s->queued >= 1 || s->close_requested || s->closed || s->error) {
|
|
return;
|
|
}
|
|
if (t->inflate_draining) return;
|
|
if (t->inflating && (t->inflate_pending || t->inflate_needs_drain)) {
|
|
sf_inflate_resume(t);
|
|
return;
|
|
}
|
|
t->response_paused = false;
|
|
scr_net_sock_release(scr_net_sock_resume(t->response_socket));
|
|
}
|
|
|
|
static void sf_stream_finish_close(SfStream *s) {
|
|
if (!s->close_requested || s->head || s->closed || s->error) return;
|
|
s->closed = true;
|
|
sf_stream_drop_source_callbacks(s, true);
|
|
if (s->reader) scr_promise_fulfill_void(s->reader->closed);
|
|
}
|
|
|
|
static void sf_stream_drain(SfStream *s) {
|
|
if (s->request_owner) {
|
|
sf_stream_request_flush(s);
|
|
return;
|
|
}
|
|
if (s->request_discarding) {
|
|
/* Fetch keeps a reader attached to an upload body after aborting the
|
|
* transfer. Its chunks are no longer written, but demand continues
|
|
* until the source closes/errors; notably, source.cancel is not run. */
|
|
sf_stream_drop_chunks(s);
|
|
sf_stream_finish_close(s);
|
|
return;
|
|
}
|
|
if (s->collector) {
|
|
while (s->head) {
|
|
SfChunk *c = s->head;
|
|
s->head = c->next;
|
|
if (!s->head) s->tail = NULL;
|
|
s->queued--;
|
|
ScrBytes *bytes = sf_chunk_bytes(c->value);
|
|
if (!bytes) {
|
|
sf_chunk_release(c);
|
|
scr_promise_reject_pending(s->collector->promise);
|
|
sf_collector_drop(s);
|
|
sf_stream_drop_chunks(s);
|
|
return;
|
|
}
|
|
sf_collector_append(s->collector, bytes);
|
|
scr_bytes_release(bytes);
|
|
sf_chunk_release(c);
|
|
}
|
|
sf_stream_finish_close(s);
|
|
sf_response_resume_if_ready(s);
|
|
if (s->error) sf_collector_reject(s, s->error);
|
|
else if (s->closed) sf_collector_finish(s);
|
|
return;
|
|
}
|
|
if (s->reader) {
|
|
bool dequeued = false;
|
|
while (s->reader->pending_head && s->head) {
|
|
SfChunk *c = s->head;
|
|
s->head = c->next;
|
|
if (!s->head) s->tail = NULL;
|
|
s->queued--;
|
|
dequeued = true;
|
|
sf_reader_fulfill_one(s->reader, false, c->value);
|
|
sf_chunk_release(c);
|
|
}
|
|
sf_stream_finish_close(s);
|
|
if (s->error) {
|
|
sf_reader_reject_all(s->reader, s->error);
|
|
} else if (s->closed) {
|
|
while (s->reader->pending_head) {
|
|
sf_reader_fulfill_one(s->reader, true, NULL);
|
|
}
|
|
}
|
|
/* Consuming the last queued chunk raises desiredSize above zero even
|
|
* when no read request remains. That transition is fresh demand and
|
|
* must replenish the queue up to its default high-water mark. */
|
|
if (dequeued && !s->enqueue_draining && !s->head &&
|
|
!s->close_requested && !s->closed && !s->error) {
|
|
sf_stream_pull(s);
|
|
}
|
|
sf_response_resume_if_ready(s);
|
|
sf_response_discard_if_unobserved(s);
|
|
return;
|
|
}
|
|
sf_stream_finish_close(s);
|
|
sf_response_resume_if_ready(s);
|
|
}
|
|
|
|
static void sf_stream_enqueue_value(SfStream *s, ScrDyn *value) {
|
|
if (s->close_requested || s->closed || s->error) {
|
|
sf_type_error("Invalid state: the ReadableStream is already closed");
|
|
return;
|
|
}
|
|
SfChunk *c = calloc(1, sizeof *c);
|
|
if (!c) sf_oom();
|
|
ScrDyn *stored = value;
|
|
if (value->kind == SCR_DYN_TYPED_REF) {
|
|
for (SfTypedRef **at = &s->typed_refs; *at;) {
|
|
SfTypedRef *ref = *at;
|
|
/* A typed ReadableStream.from(array) retains the source array for the
|
|
* stream's lifetime, so a source object can recur after an arbitrary
|
|
* number of pulls and must still denote the same JS object. Keep its
|
|
* capsule as long as that source is live. Callback-fed streams have no
|
|
* such bounded owner; drop an otherwise-unobserved capsule there so a
|
|
* long-running producer does not retain every object it ever emitted. */
|
|
if (!s->from_array && ref->value->rc == 1) {
|
|
*at = ref->next;
|
|
scr_dyn_release(ref->value);
|
|
free(ref);
|
|
continue;
|
|
}
|
|
if (scr_dyn_strict_eq(ref->value, value)) {
|
|
stored = ref->value;
|
|
break;
|
|
}
|
|
at = &ref->next;
|
|
}
|
|
if (stored == value) {
|
|
SfTypedRef *ref = malloc(sizeof *ref);
|
|
if (!ref) sf_oom();
|
|
ref->value = scr_dyn_retain(value);
|
|
ref->next = s->typed_refs;
|
|
s->typed_refs = ref;
|
|
}
|
|
}
|
|
c->value = scr_dyn_retain(stored);
|
|
if (s->tail) s->tail->next = c;
|
|
else s->head = c;
|
|
s->tail = c;
|
|
s->queued++;
|
|
bool was_enqueue_draining = s->enqueue_draining;
|
|
s->enqueue_draining = true;
|
|
sf_stream_drain(s);
|
|
s->enqueue_draining = was_enqueue_draining;
|
|
/* Enqueue is the event that can create fresh demand after a deferred
|
|
* pull. Merely having a waiting reader/request is not: looping on that
|
|
* state re-enters pull synchronously and starves timers. */
|
|
if (
|
|
s->started &&
|
|
!s->close_requested &&
|
|
!s->closed &&
|
|
!s->error &&
|
|
(s->request_owner || s->request_discarding ||
|
|
(s->reader && s->reader->pending_head) || !s->head)
|
|
) {
|
|
sf_stream_pull(s);
|
|
}
|
|
sf_response_pause_if_full(s);
|
|
}
|
|
|
|
static void sf_stream_enqueue_bytes(SfStream *s, ScrBytes *bytes) {
|
|
ScrDyn *value = scr_dyn_new_bytes_copy(bytes);
|
|
sf_stream_enqueue_value(s, value);
|
|
scr_dyn_release(value);
|
|
}
|
|
|
|
static void sf_stream_close(SfStream *s) {
|
|
if (s->close_requested || s->closed || s->error) return;
|
|
s->close_requested = true;
|
|
/* pull can never run again once close is requested. Keep cancel until
|
|
* the queued tail drains because cancel() may still invoke it. */
|
|
sf_stream_drop_source_callbacks(s, false);
|
|
sf_stream_drain(s);
|
|
sf_stream_finish_close(s);
|
|
}
|
|
|
|
static void sf_stream_error(SfStream *s, ScrDyn *reason) {
|
|
if (s->closed || s->error) return;
|
|
s->error = scr_dyn_retain(reason);
|
|
sf_stream_drop_chunks(s);
|
|
sf_stream_drain(s);
|
|
if (s->reader) {
|
|
sf_reject_promise_reason(s->reader->closed, reason);
|
|
}
|
|
if (s->request_owner) {
|
|
sf_transfer_stream_error(s->request_owner, reason);
|
|
}
|
|
sf_stream_drop_source_callbacks(s, true);
|
|
}
|
|
|
|
/* Web Streams adopts promise-like results from start/pull/cancel through
|
|
* PromiseResolve, not merely native Promise instances. Keep that adoption
|
|
* local to this surface: the checked-dynamic language intentionally does
|
|
* not otherwise assimilate arbitrary objects carrying a `then` member. */
|
|
static SfThenState *sf_then_state_retain(SfThenState *state) {
|
|
state->rc++;
|
|
return state;
|
|
}
|
|
|
|
static void sf_then_state_release(SfThenState *state) {
|
|
if (--state->rc > 0) return;
|
|
scr_promise_release(state->promise);
|
|
free(state);
|
|
}
|
|
|
|
static void *sf_then_state_retain_v(void *ptr) {
|
|
return sf_then_state_retain((SfThenState *)ptr);
|
|
}
|
|
|
|
static void sf_then_state_release_v(void *ptr) {
|
|
sf_then_state_release((SfThenState *)ptr);
|
|
}
|
|
|
|
static ScrBox *sf_then_state_box(SfThenState *state) {
|
|
ScrBox *box = scr_box_new_obj(
|
|
&sf_then_state_retain_v, &sf_then_state_release_v, NULL);
|
|
scr_box_set_ref(box, sf_then_state_retain(state));
|
|
return box;
|
|
}
|
|
|
|
static void sf_then_adopt(ScrPromise *target, ScrDyn *value);
|
|
|
|
static ScrDyn *sf_then_resolve_thunk(
|
|
ScrClosure *cb, ScrDyn *const *args, size_t argc) {
|
|
SfThenState *state = (SfThenState *)scr_box_get_ref(cb->caps[0]);
|
|
if (!state->called) {
|
|
state->called = true;
|
|
sf_then_adopt(
|
|
state->promise, argc ? args[0] : scr_dyn_undefined());
|
|
}
|
|
sf_then_state_release(state);
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
|
|
static ScrDyn *sf_then_reject_thunk(
|
|
ScrClosure *cb, ScrDyn *const *args, size_t argc) {
|
|
SfThenState *state = (SfThenState *)scr_box_get_ref(cb->caps[0]);
|
|
if (!state->called) {
|
|
state->called = true;
|
|
sf_reject_promise_reason(
|
|
state->promise, argc ? args[0] : scr_dyn_undefined());
|
|
}
|
|
sf_then_state_release(state);
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
|
|
static void sf_then_call_task(ScrClosure *cb) {
|
|
SfThenState *state = (SfThenState *)scr_box_get_ref(cb->caps[0]);
|
|
ScrDyn *thenable = (ScrDyn *)scr_box_get_ref(cb->caps[1]);
|
|
ScrDyn *then_fn = (ScrDyn *)scr_box_get_ref(cb->caps[2]);
|
|
|
|
ScrClosure *resolve_cb =
|
|
scr_closure_new((void *)&sf_then_resolve_thunk, 1);
|
|
resolve_cb->caps[0] = sf_then_state_box(state);
|
|
ScrDyn *resolve = scr_dyn_new_func(
|
|
resolve_cb, &sf_then_resolve_thunk, 1, "(value)", "resolve");
|
|
|
|
ScrClosure *reject_cb =
|
|
scr_closure_new((void *)&sf_then_reject_thunk, 1);
|
|
reject_cb->caps[0] = sf_then_state_box(state);
|
|
ScrDyn *reject = scr_dyn_new_func(
|
|
reject_cb, &sf_then_reject_thunk, 1, "(reason)", "reject");
|
|
|
|
ScrDyn *args[2] = {resolve, reject};
|
|
scr_dyn_this_push_dyn(thenable);
|
|
ScrDyn *result =
|
|
scr_dyn_call(then_fn, args, 2, "thenable.then");
|
|
scr_dyn_this_pop();
|
|
if (result) {
|
|
scr_dyn_release(result);
|
|
} else if (!state->called) {
|
|
state->called = true;
|
|
scr_promise_reject_pending(state->promise);
|
|
} else {
|
|
/* A throw after either resolving function ran is ignored. */
|
|
scr_exc_clear();
|
|
}
|
|
|
|
scr_dyn_release(resolve);
|
|
scr_dyn_release(reject);
|
|
scr_dyn_release(then_fn);
|
|
scr_dyn_release(thenable);
|
|
sf_then_state_release(state);
|
|
}
|
|
|
|
static void sf_then_schedule(
|
|
ScrPromise *target, ScrDyn *thenable, ScrDyn *then_fn) {
|
|
SfThenState *state = calloc(1, sizeof *state);
|
|
if (!state) sf_oom();
|
|
state->rc = 1;
|
|
state->promise = scr_promise_retain(target);
|
|
|
|
ScrClosure *task =
|
|
scr_closure_new((void *)&sf_then_call_task, 3);
|
|
task->caps[0] = sf_then_state_box(state);
|
|
task->caps[1] =
|
|
scr_box_new_obj(&scr_dyn_retain_v, &scr_dyn_release_v, NULL);
|
|
scr_box_set_ref(task->caps[1], scr_dyn_retain(thenable));
|
|
task->caps[2] =
|
|
scr_box_new_obj(&scr_dyn_retain_v, &scr_dyn_release_v, NULL);
|
|
scr_box_set_ref(task->caps[2], scr_dyn_retain(then_fn));
|
|
sf_then_state_release(state);
|
|
scr_queue_microtask(task);
|
|
}
|
|
|
|
static void sf_then_adopt(ScrPromise *target, ScrDyn *value) {
|
|
if (value->kind == SCR_DYN_PROMISE) {
|
|
scr_promise_race_add(
|
|
target, value->v.promise, &scr_promise_adapt_copy);
|
|
return;
|
|
}
|
|
if (value->kind == SCR_DYN_OBJ) {
|
|
const ScrDyn *then_fn = scr_dyn_obj_get(value, "then", 4);
|
|
if (then_fn && then_fn->kind == SCR_DYN_FUNC) {
|
|
sf_then_schedule(target, value, (ScrDyn *)then_fn);
|
|
return;
|
|
}
|
|
}
|
|
scr_promise_fulfill_ref(
|
|
target, scr_dyn_retain(value), &scr_dyn_retain_v,
|
|
&scr_dyn_release_v, NULL);
|
|
}
|
|
|
|
/* +1 when the callback result must be awaited; NULL for a plain value. */
|
|
static ScrPromise *sf_stream_callback_promise(ScrDyn *value) {
|
|
if (value->kind == SCR_DYN_PROMISE) {
|
|
return scr_promise_retain(value->v.promise);
|
|
}
|
|
if (value->kind != SCR_DYN_OBJ) return NULL;
|
|
const ScrDyn *then_fn = scr_dyn_obj_get(value, "then", 4);
|
|
if (!then_fn || then_fn->kind != SCR_DYN_FUNC) return NULL;
|
|
ScrPromise *promise = scr_promise_new();
|
|
sf_then_schedule(promise, value, (ScrDyn *)then_fn);
|
|
return promise;
|
|
}
|
|
|
|
static void sf_stream_pull_wait_entry(ScrFiber *self, void *arg) {
|
|
(void)self;
|
|
SfPullWait *wait = arg;
|
|
SfStream *s = wait->stream;
|
|
ScrDyn *value = scr_await_dyn(wait->promise);
|
|
bool rejected = scr_exc_pending();
|
|
ScrCaught *caught = rejected ? scr_exc_take() : NULL;
|
|
scr_dyn_release(value);
|
|
scr_promise_release(wait->promise);
|
|
free(wait);
|
|
|
|
s->pulling = false;
|
|
if (rejected) {
|
|
s->pull_again = false;
|
|
ScrDyn *reason = scr_caught_to_dyn(caught);
|
|
sf_stream_error(s, reason);
|
|
scr_dyn_release(reason);
|
|
} else {
|
|
bool again = s->pull_again;
|
|
s->pull_again = false;
|
|
if (again && !s->close_requested && !s->closed && !s->error) {
|
|
sf_stream_pull(s);
|
|
}
|
|
}
|
|
scr_caught_release(caught);
|
|
sf_stream_release(s);
|
|
}
|
|
|
|
static void sf_stream_start_wait_entry(ScrFiber *self, void *arg) {
|
|
(void)self;
|
|
SfStartWait *wait = arg;
|
|
SfStream *s = wait->stream;
|
|
ScrDyn *value = scr_await_dyn(wait->promise);
|
|
bool rejected = scr_exc_pending();
|
|
ScrCaught *caught = rejected ? scr_exc_take() : NULL;
|
|
scr_dyn_release(value);
|
|
scr_promise_release(wait->promise);
|
|
free(wait);
|
|
|
|
if (rejected) {
|
|
ScrDyn *reason = scr_caught_to_dyn(caught);
|
|
sf_stream_error(s, reason);
|
|
scr_dyn_release(reason);
|
|
} else {
|
|
s->started = true;
|
|
sf_stream_drain(s);
|
|
if (!s->close_requested && !s->closed && !s->error &&
|
|
s->queued == 0) {
|
|
sf_stream_pull(s);
|
|
sf_stream_drain(s);
|
|
}
|
|
}
|
|
scr_caught_release(caught);
|
|
sf_stream_release(s);
|
|
}
|
|
|
|
static ScrDyn *sf_controller_box(SfStream *s) {
|
|
return scr_dyn_new_handle(s, SCR_DYNH_WEB_CONTROLLER);
|
|
}
|
|
|
|
static bool sf_stream_has_from_source(const SfStream *s) {
|
|
return s->from_dyn || s->from_array || s->from_bytes || s->from_string;
|
|
}
|
|
|
|
static void sf_stream_pull_from_source(SfStream *s) {
|
|
ScrDyn *value = NULL;
|
|
if (s->from_array) {
|
|
if (s->from_index >= scr_arr_len(s->from_array)) {
|
|
sf_stream_close(s);
|
|
return;
|
|
}
|
|
value = s->from_array_item(s->from_array, s->from_index++);
|
|
} else if (s->from_bytes) {
|
|
if (s->from_index >= scr_bytes_len(s->from_bytes)) {
|
|
sf_stream_close(s);
|
|
return;
|
|
}
|
|
value = scr_dyn_new_num(
|
|
scr_bytes_get(s->from_bytes, s->from_index++));
|
|
} else if (s->from_string) {
|
|
double len = scr_str_utf16_len(s->from_string);
|
|
if (s->from_index >= len) {
|
|
sf_stream_close(s);
|
|
return;
|
|
}
|
|
ScrStr *cp = scr_str_cp_at(s->from_string, s->from_index);
|
|
s->from_index += scr_str_utf16_len(cp);
|
|
value = scr_dyn_new_str(cp);
|
|
scr_str_release(cp);
|
|
} else if (s->from_dyn->kind == SCR_DYN_ARR) {
|
|
if (s->from_index >= (double)s->from_dyn->v.arr.len) {
|
|
sf_stream_close(s);
|
|
return;
|
|
}
|
|
value = scr_dyn_retain(
|
|
s->from_dyn->v.arr.items[(size_t)s->from_index++]);
|
|
} else if (s->from_dyn->kind == SCR_DYN_BYTES) {
|
|
if (s->from_index >= scr_bytes_len(s->from_dyn->v.bytes)) {
|
|
sf_stream_close(s);
|
|
return;
|
|
}
|
|
value = scr_dyn_new_num(
|
|
scr_bytes_get(s->from_dyn->v.bytes, s->from_index++));
|
|
} else {
|
|
double len = scr_str_utf16_len(s->from_dyn->v.str);
|
|
if (s->from_index >= len) {
|
|
sf_stream_close(s);
|
|
return;
|
|
}
|
|
ScrStr *cp = scr_str_cp_at(s->from_dyn->v.str, s->from_index);
|
|
s->from_index += scr_str_utf16_len(cp);
|
|
value = scr_dyn_new_str(cp);
|
|
scr_str_release(cp);
|
|
}
|
|
sf_stream_enqueue_value(s, value);
|
|
scr_dyn_release(value);
|
|
}
|
|
|
|
static void sf_stream_initial_pull_task(ScrClosure *cb) {
|
|
SfStream *s = (SfStream *)scr_box_get_ref(cb->caps[0]);
|
|
if (!s) return;
|
|
s->initial_pull_pending = false;
|
|
if (s->started && !s->close_requested && !s->closed && !s->error &&
|
|
s->queued == 0) {
|
|
sf_stream_pull(s);
|
|
sf_stream_drain(s);
|
|
}
|
|
sf_stream_release(s);
|
|
}
|
|
|
|
static void sf_stream_schedule_initial_pull(SfStream *s) {
|
|
if ((!s->pull_cb && !sf_stream_has_from_source(s)) ||
|
|
s->initial_pull_pending || s->close_requested || s->closed ||
|
|
s->error) {
|
|
return;
|
|
}
|
|
s->initial_pull_pending = true;
|
|
ScrClosure *cb =
|
|
scr_closure_new((void *)&sf_stream_initial_pull_task, 1);
|
|
ScrBox *box =
|
|
scr_box_new_obj(&sf_stream_retain_v, &sf_stream_release_v, NULL);
|
|
scr_box_set_ref(box, sf_stream_retain(s));
|
|
cb->caps[0] = box;
|
|
scr_queue_microtask(cb);
|
|
}
|
|
|
|
/* Web Streams wraps an underlying source's plain pull return in a resolved
|
|
* promise. Its fulfillment step therefore runs as a promise job, never in
|
|
* the pull callback's stack. In particular, enqueueing into a pending read
|
|
* wakes that read's awaiter before a desiredSize-driven repull: the awaiter
|
|
* can cancel or release the reader first. Keep pulling=true through this
|
|
* microtask so demand raised by enqueue/read records pull_again exactly as
|
|
* it does while awaiting a genuinely asynchronous pull result. */
|
|
static void sf_stream_pull_complete_task(ScrClosure *cb) {
|
|
SfStream *s = (SfStream *)scr_box_get_ref(cb->caps[0]);
|
|
if (!s) return;
|
|
s->pulling = false;
|
|
bool again = s->pull_again;
|
|
s->pull_again = false;
|
|
if (again && !s->close_requested && !s->closed && !s->error) {
|
|
sf_stream_pull(s);
|
|
}
|
|
sf_stream_release(s);
|
|
}
|
|
|
|
static void sf_stream_schedule_pull_complete(SfStream *s) {
|
|
ScrClosure *cb =
|
|
scr_closure_new((void *)&sf_stream_pull_complete_task, 1);
|
|
ScrBox *box =
|
|
scr_box_new_obj(&sf_stream_retain_v, &sf_stream_release_v, NULL);
|
|
scr_box_set_ref(box, sf_stream_retain(s));
|
|
cb->caps[0] = box;
|
|
scr_queue_microtask(cb);
|
|
}
|
|
|
|
static void sf_stream_pull(SfStream *s) {
|
|
if (!s->started ||
|
|
(!s->pull_cb && !sf_stream_has_from_source(s)) ||
|
|
s->initial_pull_pending || s->close_requested || s->closed ||
|
|
s->error) {
|
|
return;
|
|
}
|
|
if (s->pulling) {
|
|
s->pull_again = true;
|
|
return;
|
|
}
|
|
if (sf_stream_has_from_source(s)) {
|
|
s->pulling = true;
|
|
sf_stream_pull_from_source(s);
|
|
sf_stream_schedule_pull_complete(s);
|
|
return;
|
|
}
|
|
s->pulling = true;
|
|
ScrDyn *controller = sf_controller_box(s);
|
|
ScrDyn *args[1] = {controller};
|
|
ScrDyn *pull_cb = scr_dyn_retain(s->pull_cb);
|
|
scr_dyn_this_push_dyn(s->source_this);
|
|
ScrDyn *r =
|
|
scr_dyn_call(pull_cb, args, 1, "underlyingSource.pull");
|
|
scr_dyn_this_pop();
|
|
scr_dyn_release(pull_cb);
|
|
scr_dyn_release(controller);
|
|
if (!r) {
|
|
s->pulling = false;
|
|
s->pull_again = false;
|
|
ScrCaught *caught = scr_exc_take();
|
|
ScrDyn *reason = scr_caught_to_dyn(caught);
|
|
sf_stream_error(s, reason);
|
|
scr_dyn_release(reason);
|
|
scr_caught_release(caught);
|
|
return;
|
|
}
|
|
ScrPromise *callback_promise = sf_stream_callback_promise(r);
|
|
if (callback_promise) {
|
|
SfPullWait *wait = malloc(sizeof *wait);
|
|
if (!wait) sf_oom();
|
|
wait->stream = sf_stream_retain(s);
|
|
wait->promise = callback_promise;
|
|
ScrPromise *watcher =
|
|
scr_async_spawn(&sf_stream_pull_wait_entry, wait);
|
|
scr_promise_release(watcher);
|
|
scr_dyn_release(r);
|
|
return;
|
|
}
|
|
scr_dyn_release(r);
|
|
sf_stream_schedule_pull_complete(s);
|
|
}
|
|
|
|
static SfReader *sf_stream_get_reader(SfStream *s) {
|
|
if (s->reader || s->internal_lock) {
|
|
sf_type_error("Invalid state: ReadableStream is locked");
|
|
return NULL;
|
|
}
|
|
SfReader *r = calloc(1, sizeof *r);
|
|
if (!r) sf_oom();
|
|
r->rc = 1;
|
|
r->stream = s;
|
|
r->closed = scr_promise_new();
|
|
/* A reader's internal closed capability never reports as an unhandled
|
|
* rejection merely because user code does not read the property. */
|
|
scr_promise_mark_handled(r->closed);
|
|
if (s->closed) scr_promise_fulfill_void(r->closed);
|
|
else if (s->error) sf_reject_promise_reason(r->closed, s->error);
|
|
s->reader = sf_reader_retain(r);
|
|
return r;
|
|
}
|
|
|
|
static ScrPromise *sf_reader_read(SfReader *r) {
|
|
ScrPromise *p = scr_promise_new();
|
|
SfStream *s = r->stream;
|
|
if (!s || s->reader != r) {
|
|
sf_type_error("This reader has been released");
|
|
scr_promise_reject_pending(p);
|
|
return p;
|
|
}
|
|
s->disturbed = true;
|
|
SfReadRequest *request = calloc(1, sizeof *request);
|
|
if (!request) sf_oom();
|
|
request->promise = scr_promise_retain(p);
|
|
if (r->pending_tail) r->pending_tail->next = request;
|
|
else r->pending_head = request;
|
|
r->pending_tail = request;
|
|
sf_stream_drain(s);
|
|
if (r->pending_head && !s->head && !s->closed && !s->error) {
|
|
sf_stream_pull(s);
|
|
sf_stream_drain(s);
|
|
}
|
|
return p;
|
|
}
|
|
|
|
static void sf_reader_release_lock(SfReader *r) {
|
|
SfStream *s = r->stream;
|
|
if (!s || s->reader != r) return;
|
|
|
|
ScrPromise *old_closed = r->closed;
|
|
if (!s->closed && !s->error) {
|
|
sf_type_error("Invalid state: Reader released");
|
|
scr_promise_reject_pending(old_closed);
|
|
} else {
|
|
r->closed = scr_promise_new();
|
|
scr_promise_mark_handled(r->closed);
|
|
sf_type_error("Invalid state: Reader released");
|
|
scr_promise_reject_pending(r->closed);
|
|
scr_promise_release(old_closed);
|
|
}
|
|
|
|
sf_reader_reject_message_all(r, "Invalid state: Releasing reader");
|
|
|
|
size_t handle_refs = r->handle_refs;
|
|
s->reader = NULL;
|
|
r->stream = NULL;
|
|
sf_reader_release(r); /* stream's ownership */
|
|
while (handle_refs-- > 0) sf_stream_release(s);
|
|
}
|
|
|
|
static void sf_stream_cancel_wait_entry(ScrFiber *self, void *arg) {
|
|
(void)self;
|
|
SfCancelWait *wait = arg;
|
|
ScrDyn *value = scr_await_dyn(wait->source);
|
|
bool rejected = scr_exc_pending();
|
|
ScrCaught *caught = rejected ? scr_exc_take() : NULL;
|
|
scr_dyn_release(value);
|
|
if (rejected) {
|
|
ScrDyn *reason = scr_caught_to_dyn(caught);
|
|
sf_reject_promise_reason(wait->result, reason);
|
|
scr_dyn_release(reason);
|
|
} else {
|
|
scr_promise_fulfill_void(wait->result);
|
|
}
|
|
scr_caught_release(caught);
|
|
scr_promise_release(wait->source);
|
|
scr_promise_release(wait->result);
|
|
free(wait);
|
|
}
|
|
|
|
static ScrPromise *sf_stream_cancel(SfStream *s, ScrDyn *reason,
|
|
bool through_reader) {
|
|
ScrPromise *p = scr_promise_new();
|
|
if (!through_reader && (s->reader || s->internal_lock)) {
|
|
sf_type_error("Invalid state: cannot cancel a locked ReadableStream");
|
|
scr_promise_reject_pending(p);
|
|
return p;
|
|
}
|
|
/*
|
|
* ReadableStreamCancel disturbs the stream before inspecting its state.
|
|
* This remains observable through Response.bodyUsed even when the
|
|
* stream had already closed or errored.
|
|
*/
|
|
s->disturbed = true;
|
|
if (s->closed) {
|
|
/* WHATWG cancel is a no-op once the stream is closed. */
|
|
scr_promise_fulfill_void(p);
|
|
return p;
|
|
}
|
|
if (s->error) {
|
|
/* An errored stream preserves and rejects with its stored reason. */
|
|
sf_reject_promise_reason(p, s->error);
|
|
return p;
|
|
}
|
|
ScrDyn *cancel_cb =
|
|
s->cancel_cb ? scr_dyn_retain(s->cancel_cb) : NULL;
|
|
ScrDyn *cancel_this =
|
|
cancel_cb ? scr_dyn_retain(s->source_this) : NULL;
|
|
sf_stream_drop_chunks(s);
|
|
SfTransfer *response_owner = s->response_owner;
|
|
if (response_owner && response_owner->client) {
|
|
scr_http_client_destroy(response_owner->client);
|
|
}
|
|
if (s->close_requested) sf_stream_finish_close(s);
|
|
else sf_stream_close(s);
|
|
if (response_owner && !response_owner->done) sf_settle(response_owner);
|
|
|
|
if (cancel_cb) {
|
|
ScrDyn *args[1] = {reason ? reason : scr_dyn_undefined()};
|
|
scr_dyn_this_push_dyn(cancel_this);
|
|
ScrDyn *r =
|
|
scr_dyn_call(cancel_cb, args, 1, "underlyingSource.cancel");
|
|
scr_dyn_this_pop();
|
|
scr_dyn_release(cancel_cb);
|
|
scr_dyn_release(cancel_this);
|
|
if (!r) {
|
|
scr_promise_reject_pending(p);
|
|
return p;
|
|
}
|
|
ScrPromise *callback_promise = sf_stream_callback_promise(r);
|
|
if (callback_promise) {
|
|
SfCancelWait *wait = malloc(sizeof *wait);
|
|
if (!wait) sf_oom();
|
|
wait->source = callback_promise;
|
|
wait->result = scr_promise_retain(p);
|
|
ScrPromise *watcher =
|
|
scr_async_spawn(&sf_stream_cancel_wait_entry, wait);
|
|
scr_promise_release(watcher);
|
|
scr_dyn_release(r);
|
|
return p;
|
|
}
|
|
scr_dyn_release(r);
|
|
} else {
|
|
scr_dyn_release(cancel_cb);
|
|
}
|
|
scr_promise_fulfill_void(p);
|
|
return p;
|
|
}
|
|
|
|
static ScrPromise *sf_stream_collect(SfStream *s, int mode) {
|
|
ScrPromise *p = scr_promise_new();
|
|
if (s->disturbed || s->reader || s->internal_lock) {
|
|
sf_type_error("Body is unusable: Body has already been read");
|
|
scr_promise_reject_pending(p);
|
|
return p;
|
|
}
|
|
s->disturbed = true;
|
|
s->internal_lock = true;
|
|
SfCollector *c = calloc(1, sizeof *c);
|
|
if (!c) sf_oom();
|
|
c->promise = scr_promise_retain(p);
|
|
c->held_stream = sf_stream_retain(s);
|
|
c->mode = mode;
|
|
s->collector = c;
|
|
sf_stream_drain(s);
|
|
return p;
|
|
}
|
|
|
|
ScrDyn *scr_fetch_stream_new(ScrDyn *source) {
|
|
ScrDyn *source_view = source;
|
|
ScrDyn *materialized = NULL;
|
|
if (source && source->kind == SCR_DYN_TYPED_REF) {
|
|
materialized = scr_dyn_typed_ref_materialize(source);
|
|
source_view = materialized;
|
|
}
|
|
if (source_view && source_view->kind != SCR_DYN_UNDEF &&
|
|
source_view->kind != SCR_DYN_OBJ) {
|
|
scr_dyn_release(materialized);
|
|
sf_type_error("ReadableStream source must be an object");
|
|
return NULL;
|
|
}
|
|
SfStream *s = sf_stream_new_native();
|
|
if (source_view && source_view->kind == SCR_DYN_OBJ) {
|
|
const ScrDyn *type = scr_dyn_obj_get(source_view, "type", 4);
|
|
if (type && type->kind != SCR_DYN_UNDEF) {
|
|
sf_stream_release(s);
|
|
scr_dyn_release(materialized);
|
|
sf_type_error("byte streams are not supported by static ReadableStream");
|
|
return NULL;
|
|
}
|
|
const ScrDyn *pull = scr_dyn_obj_get(source_view, "pull", 4);
|
|
const ScrDyn *cancel = scr_dyn_obj_get(source_view, "cancel", 6);
|
|
const ScrDyn *start = scr_dyn_obj_get(source_view, "start", 5);
|
|
if (pull && pull->kind != SCR_DYN_UNDEF) {
|
|
if (pull->kind != SCR_DYN_FUNC) {
|
|
sf_stream_release(s);
|
|
scr_dyn_release(materialized);
|
|
sf_type_error("ReadableStream source.pull must be a function");
|
|
return NULL;
|
|
}
|
|
s->pull_cb = scr_dyn_retain((ScrDyn *)pull);
|
|
}
|
|
if (cancel && cancel->kind != SCR_DYN_UNDEF) {
|
|
if (cancel->kind != SCR_DYN_FUNC) {
|
|
sf_stream_release(s);
|
|
scr_dyn_release(materialized);
|
|
sf_type_error("ReadableStream source.cancel must be a function");
|
|
return NULL;
|
|
}
|
|
s->cancel_cb = scr_dyn_retain((ScrDyn *)cancel);
|
|
}
|
|
if (s->pull_cb || s->cancel_cb) {
|
|
s->source_this = scr_dyn_retain(source);
|
|
}
|
|
sf_stream_track_callbacks(s);
|
|
if (start && start->kind != SCR_DYN_UNDEF) {
|
|
if (start->kind != SCR_DYN_FUNC) {
|
|
sf_stream_release(s);
|
|
scr_dyn_release(materialized);
|
|
sf_type_error("ReadableStream source.start must be a function");
|
|
return NULL;
|
|
}
|
|
s->started = false;
|
|
/* `this` may be a live typed capsule. A property write from start()
|
|
* refreshes its cached object and releases the old field snapshot,
|
|
* so retain the borrowed callback across that refresh. */
|
|
ScrDyn *start_cb = scr_dyn_retain((ScrDyn *)start);
|
|
ScrDyn *controller = sf_controller_box(s);
|
|
ScrDyn *args[1] = {controller};
|
|
scr_dyn_this_push_dyn(source);
|
|
ScrDyn *r =
|
|
scr_dyn_call(start_cb, args, 1, "underlyingSource.start");
|
|
scr_dyn_this_pop();
|
|
scr_dyn_release(start_cb);
|
|
scr_dyn_release(controller);
|
|
if (!r) {
|
|
sf_stream_release(s);
|
|
scr_dyn_release(materialized);
|
|
return NULL;
|
|
}
|
|
ScrPromise *callback_promise = sf_stream_callback_promise(r);
|
|
if (callback_promise) {
|
|
SfStartWait *wait = malloc(sizeof *wait);
|
|
if (!wait) sf_oom();
|
|
wait->stream = sf_stream_retain(s);
|
|
wait->promise = callback_promise;
|
|
ScrPromise *watcher =
|
|
scr_async_spawn(&sf_stream_start_wait_entry, wait);
|
|
scr_promise_release(watcher);
|
|
} else {
|
|
s->started = true;
|
|
}
|
|
scr_dyn_release(r);
|
|
}
|
|
}
|
|
if (s->started) sf_stream_schedule_initial_pull(s);
|
|
ScrDyn *out = scr_dyn_new_handle(s, SCR_DYNH_WEB_STREAM);
|
|
sf_stream_release(s);
|
|
scr_dyn_release(materialized);
|
|
return out;
|
|
}
|
|
|
|
ScrDyn *scr_fetch_stream_from(ScrDyn *iterable) {
|
|
if (!iterable) {
|
|
sf_type_error("ReadableStream.from requires an iterable");
|
|
return NULL;
|
|
}
|
|
if (iterable->kind == SCR_DYN_UNDEF) {
|
|
sf_type_error(
|
|
"Cannot read properties of undefined (reading 'Symbol(Symbol.asyncIterator)')");
|
|
return NULL;
|
|
}
|
|
if (iterable->kind != SCR_DYN_ARR &&
|
|
iterable->kind != SCR_DYN_BYTES &&
|
|
iterable->kind != SCR_DYN_STR &&
|
|
iterable->kind != SCR_DYN_JSVAL) {
|
|
/* Reuse the checked-dynamic iterable error wording. */
|
|
ScrDyn *packed = scr_dyn_iter_pack(iterable, NULL);
|
|
scr_dyn_release(packed);
|
|
return NULL;
|
|
}
|
|
ScrDyn *source = iterable;
|
|
if (iterable->kind == SCR_DYN_JSVAL) {
|
|
/* Static builds do not normally carry engine values. If one reaches
|
|
* this fallback, validate/drain it through the engine once; native
|
|
* arrays/bytes/strings retain their live container below. */
|
|
source = scr_dyn_iter_pack(iterable, NULL);
|
|
if (!source) return NULL;
|
|
}
|
|
SfStream *s = sf_stream_new_native();
|
|
s->from_dyn = scr_dyn_retain(source);
|
|
sf_stream_schedule_initial_pull(s);
|
|
ScrDyn *out = scr_dyn_new_handle(s, SCR_DYNH_WEB_STREAM);
|
|
sf_stream_release(s);
|
|
if (source != iterable) scr_dyn_release(source);
|
|
return out;
|
|
}
|
|
|
|
ScrDyn *scr_fetch_stream_from_array(
|
|
ScrArr *iterable, ScrDyn *(*item)(ScrArr *, double)) {
|
|
SfStream *s = sf_stream_new_native();
|
|
s->from_array = scr_arr_retain(iterable);
|
|
s->from_array_item = item;
|
|
sf_stream_schedule_initial_pull(s);
|
|
ScrDyn *out = scr_dyn_new_handle(s, SCR_DYNH_WEB_STREAM);
|
|
sf_stream_release(s);
|
|
return out;
|
|
}
|
|
|
|
ScrDyn *scr_fetch_stream_from_bytes(ScrBytes *iterable) {
|
|
SfStream *s = sf_stream_new_native();
|
|
s->from_bytes = scr_bytes_retain(iterable);
|
|
sf_stream_schedule_initial_pull(s);
|
|
ScrDyn *out = scr_dyn_new_handle(s, SCR_DYNH_WEB_STREAM);
|
|
sf_stream_release(s);
|
|
return out;
|
|
}
|
|
|
|
ScrDyn *scr_fetch_stream_from_string(ScrStr *iterable) {
|
|
SfStream *s = sf_stream_new_native();
|
|
s->from_string = scr_str_retain(iterable);
|
|
sf_stream_schedule_initial_pull(s);
|
|
ScrDyn *out = scr_dyn_new_handle(s, SCR_DYNH_WEB_STREAM);
|
|
sf_stream_release(s);
|
|
return out;
|
|
}
|
|
|
|
ScrPromise *scr_fetch_reader_read(ScrDyn *reader) {
|
|
if (!reader || reader->kind != SCR_DYN_HANDLE ||
|
|
reader->v.handle.tag != SCR_DYNH_WEB_READER) {
|
|
sf_type_error("Expected a ReadableStreamDefaultReader");
|
|
ScrPromise *p = scr_promise_new();
|
|
scr_promise_reject_pending(p);
|
|
return p;
|
|
}
|
|
return sf_reader_read((SfReader *)reader->v.handle.ptr);
|
|
}
|
|
|
|
static SfStream *sf_stream_of(const ScrDyn *d) {
|
|
if (!d || d->kind != SCR_DYN_HANDLE ||
|
|
d->v.handle.tag != SCR_DYNH_WEB_STREAM) {
|
|
sf_type_error("Expected a ReadableStream");
|
|
return NULL;
|
|
}
|
|
return (SfStream *)d->v.handle.ptr;
|
|
}
|
|
|
|
static ScrDyn *sf_stream_invoke(void *ptr, ScrDyn *self, const char *method,
|
|
ScrDyn *const *args, size_t argc,
|
|
const char *what) {
|
|
SfStream *s = ptr;
|
|
if (strcmp(method, "getReader") == 0) {
|
|
ScrDyn *options = argc ? args[0] : NULL;
|
|
ScrDyn *materialized = NULL;
|
|
ScrDyn *mode = NULL;
|
|
bool owned_mode = false;
|
|
if (options && options->kind == SCR_DYN_TYPED_REF) {
|
|
materialized = scr_dyn_typed_ref_materialize(options);
|
|
options = materialized;
|
|
}
|
|
if (options && options->kind == SCR_DYN_OBJ) {
|
|
mode = scr_dyn_obj_get(options, "mode", 4);
|
|
} else if (options && options->kind == SCR_DYN_JSVAL) {
|
|
ScrStr *key = scr_str_new("mode", 4);
|
|
mode = scr_dyn_isl_key_get(options, key);
|
|
scr_str_release(key);
|
|
owned_mode = mode != NULL;
|
|
}
|
|
bool byob = mode && mode->kind != SCR_DYN_UNDEF;
|
|
if (owned_mode) scr_dyn_release(mode);
|
|
scr_dyn_release(materialized);
|
|
if (scr_exc_pending()) return NULL;
|
|
if (byob) {
|
|
sf_type_error("BYOB readers are not supported");
|
|
return NULL;
|
|
}
|
|
SfReader *r = sf_stream_get_reader(s);
|
|
if (!r) return NULL;
|
|
ScrDyn *out = scr_dyn_new_handle(r, SCR_DYNH_WEB_READER);
|
|
sf_reader_release(r);
|
|
return out;
|
|
}
|
|
if (strcmp(method, "cancel") == 0) {
|
|
ScrPromise *p =
|
|
sf_stream_cancel(s, argc ? args[0] : scr_dyn_undefined(), false);
|
|
ScrDyn *out = scr_dyn_new_promise(p);
|
|
scr_promise_release(p);
|
|
return out;
|
|
}
|
|
(void)self;
|
|
sf_not_function(what);
|
|
return NULL;
|
|
}
|
|
|
|
static ScrDyn *sf_stream_get(void *ptr, const char *key, size_t len) {
|
|
SfStream *s = ptr;
|
|
if (sf_name(key, len, "locked")) {
|
|
return scr_dyn_new_bool(s->reader != NULL || s->internal_lock);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static ScrDyn *sf_reader_invoke(void *ptr, ScrDyn *self, const char *method,
|
|
ScrDyn *const *args, size_t argc,
|
|
const char *what) {
|
|
SfReader *r = ptr;
|
|
if (strcmp(method, "read") == 0) {
|
|
ScrPromise *p = sf_reader_read(r);
|
|
ScrDyn *out = scr_dyn_new_promise(p);
|
|
scr_promise_release(p);
|
|
return out;
|
|
}
|
|
if (strcmp(method, "cancel") == 0) {
|
|
ScrPromise *p;
|
|
if (!r->stream) {
|
|
sf_type_error("This reader has been released");
|
|
p = scr_promise_new();
|
|
scr_promise_reject_pending(p);
|
|
} else {
|
|
p = sf_stream_cancel(
|
|
r->stream, argc ? args[0] : scr_dyn_undefined(), true);
|
|
}
|
|
ScrDyn *out = scr_dyn_new_promise(p);
|
|
scr_promise_release(p);
|
|
return out;
|
|
}
|
|
if (strcmp(method, "releaseLock") == 0) {
|
|
sf_reader_release_lock(r);
|
|
if (scr_exc_pending()) return NULL;
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
(void)self;
|
|
sf_not_function(what);
|
|
return NULL;
|
|
}
|
|
|
|
static ScrDyn *sf_reader_get(void *ptr, const char *key, size_t len) {
|
|
SfReader *r = ptr;
|
|
if (sf_name(key, len, "closed")) return scr_dyn_new_promise(r->closed);
|
|
return NULL;
|
|
}
|
|
|
|
static ScrDyn *sf_controller_invoke(void *ptr, ScrDyn *self,
|
|
const char *method,
|
|
ScrDyn *const *args, size_t argc,
|
|
const char *what) {
|
|
SfStream *s = ptr;
|
|
if (strcmp(method, "enqueue") == 0) {
|
|
sf_stream_enqueue_value(
|
|
s, argc ? args[0] : scr_dyn_undefined());
|
|
if (scr_exc_pending()) return NULL;
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
if (strcmp(method, "close") == 0) {
|
|
if (s->close_requested || s->closed || s->error) {
|
|
sf_type_error("Invalid state: Controller is already closed");
|
|
return NULL;
|
|
}
|
|
sf_stream_close(s);
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
if (strcmp(method, "error") == 0) {
|
|
sf_stream_error(s, argc ? args[0] : scr_dyn_undefined());
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
(void)self;
|
|
sf_not_function(what);
|
|
return NULL;
|
|
}
|
|
|
|
static ScrDyn *sf_controller_get(void *ptr, const char *key, size_t len) {
|
|
SfStream *s = ptr;
|
|
if (sf_name(key, len, "desiredSize")) {
|
|
if (s->error) return scr_dyn_new_null();
|
|
if (s->closed) return scr_dyn_new_num(0);
|
|
return scr_dyn_new_num(1.0 - (double)s->queued);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* ── Headers ─────────────────────────────────────────────────────── */
|
|
|
|
static bool sf_header_name_ok(const char *s, size_t len);
|
|
|
|
/* HTTP field values are ByteStrings: each wire octet becomes the same
|
|
* U+00XX code point in the JS string. ScrStr stores UTF-8, so obs-text
|
|
* bytes need a real Latin-1→UTF-8 expansion before they enter a Headers
|
|
* handle (and before string methods assume the storage invariant). */
|
|
static ScrStr *sf_latin1_to_utf8(const ScrStr *raw) {
|
|
size_t extra = 0;
|
|
for (size_t i = 0; i < raw->len; i++) {
|
|
if ((unsigned char)raw->data[i] >= 0x80) extra++;
|
|
}
|
|
if (extra == 0) return scr_str_retain((ScrStr *)raw);
|
|
char *utf8 = malloc(raw->len + extra);
|
|
if (!utf8) sf_oom();
|
|
size_t at = 0;
|
|
for (size_t i = 0; i < raw->len; i++) {
|
|
unsigned char byte = (unsigned char)raw->data[i];
|
|
if (byte < 0x80) {
|
|
utf8[at++] = (char)byte;
|
|
} else {
|
|
utf8[at++] = (char)(0xc0 | (byte >> 6));
|
|
utf8[at++] = (char)(0x80 | (byte & 0x3f));
|
|
}
|
|
}
|
|
ScrStr *out = scr_str_new(utf8, at);
|
|
free(utf8);
|
|
return out;
|
|
}
|
|
|
|
/*
|
|
* Redirect Location values are byte sequences until Fetch's header-value
|
|
* extraction decodes them as UTF-8. Invalid wire sequences become U+FFFD;
|
|
* treating the bytes as an already-valid ScrStr instead percent-encodes
|
|
* each invalid octet and can follow a different resource.
|
|
*/
|
|
static ScrStr *sf_location_to_utf8(const ScrStr *raw) {
|
|
ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)raw->len);
|
|
if (raw->len > 0) memcpy(bytes->data, raw->data, raw->len);
|
|
ScrStr *encoding = scr_str_new("utf8", 4);
|
|
ScrStr *out = scr_bytes_to_str(bytes, encoding);
|
|
scr_str_release(encoding);
|
|
scr_bytes_release(bytes);
|
|
return out;
|
|
}
|
|
|
|
static SfHeaders *sf_headers_new(ScrArr *pairs) {
|
|
SfHeaders *h = calloc(1, sizeof *h);
|
|
if (!h) sf_oom();
|
|
h->rc = 1;
|
|
h->pairs = pairs;
|
|
return h;
|
|
}
|
|
|
|
static SfHeaders *sf_headers_new_response(ScrArr *raw_pairs) {
|
|
size_t n = (size_t)scr_arr_len(raw_pairs);
|
|
ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, n);
|
|
for (size_t i = 0; i + 1 < n; i += 2) {
|
|
scr_arr_push_ref(pairs, scr_arr_get_ref(raw_pairs, (double)i));
|
|
ScrStr *raw = scr_arr_get_ref(raw_pairs, (double)(i + 1));
|
|
scr_arr_push_ref(pairs, sf_latin1_to_utf8(raw));
|
|
scr_str_release(raw);
|
|
}
|
|
scr_arr_release(raw_pairs);
|
|
return sf_headers_new(pairs);
|
|
}
|
|
|
|
static SfHeaders *sf_headers_retain(SfHeaders *h) {
|
|
h->rc++;
|
|
return h;
|
|
}
|
|
|
|
static void sf_headers_release(SfHeaders *h) {
|
|
if (!h || --h->rc > 0) return;
|
|
scr_arr_release(h->pairs);
|
|
free(h);
|
|
}
|
|
|
|
static void *sf_headers_retain_v(void *p) {
|
|
return sf_headers_retain((SfHeaders *)p);
|
|
}
|
|
|
|
static void sf_headers_release_v(void *p) {
|
|
sf_headers_release((SfHeaders *)p);
|
|
}
|
|
|
|
static ScrStr *sf_headers_name(const ScrDyn *value) {
|
|
ScrStr *raw =
|
|
scr_dyn_string_coerce_js(value ? value : scr_dyn_undefined());
|
|
if (!raw) return NULL;
|
|
if (!sf_header_name_ok(raw->data, raw->len)) {
|
|
scr_str_release(raw);
|
|
sf_type_error("Headers: invalid header name");
|
|
return NULL;
|
|
}
|
|
char *lower = malloc(raw->len);
|
|
if (!lower) sf_oom();
|
|
for (size_t i = 0; i < raw->len; i++) {
|
|
char c = raw->data[i];
|
|
lower[i] =
|
|
c >= 'A' && c <= 'Z' ? (char)(c - 'A' + 'a') : c;
|
|
}
|
|
ScrStr *out = scr_str_new(lower, raw->len);
|
|
free(lower);
|
|
scr_str_release(raw);
|
|
return out;
|
|
}
|
|
|
|
static bool sf_headers_key_eq(const ScrStr *left, const ScrStr *right) {
|
|
return left->len == right->len &&
|
|
memcmp(left->data, right->data, left->len) == 0;
|
|
}
|
|
|
|
static ScrStr *sf_headers_get_value(SfHeaders *h, const ScrStr *name) {
|
|
size_t n = (size_t)scr_arr_len(h->pairs);
|
|
size_t count = 0;
|
|
size_t total = 0;
|
|
for (size_t i = 0; i + 1 < n; i += 2) {
|
|
ScrStr *key = scr_arr_get_ref(h->pairs, (double)i);
|
|
ScrStr *value = scr_arr_get_ref(h->pairs, (double)(i + 1));
|
|
if (sf_headers_key_eq(key, name)) {
|
|
if (count > 0) total += 2;
|
|
total += value->len;
|
|
count++;
|
|
}
|
|
scr_str_release(key);
|
|
scr_str_release(value);
|
|
}
|
|
if (count == 0) return NULL;
|
|
|
|
char *joined = malloc(total > 0 ? total : 1);
|
|
if (!joined) sf_oom();
|
|
size_t at = 0;
|
|
size_t emitted = 0;
|
|
for (size_t i = 0; i + 1 < n; i += 2) {
|
|
ScrStr *key = scr_arr_get_ref(h->pairs, (double)i);
|
|
ScrStr *value = scr_arr_get_ref(h->pairs, (double)(i + 1));
|
|
if (sf_headers_key_eq(key, name)) {
|
|
if (emitted > 0) {
|
|
joined[at++] = ',';
|
|
joined[at++] = ' ';
|
|
}
|
|
memcpy(joined + at, value->data, value->len);
|
|
at += value->len;
|
|
emitted++;
|
|
}
|
|
scr_str_release(key);
|
|
scr_str_release(value);
|
|
}
|
|
ScrStr *out = scr_str_new(joined, total);
|
|
free(joined);
|
|
return out;
|
|
}
|
|
|
|
static int sf_headers_name_cmp(const void *left, const void *right) {
|
|
const ScrStr *a = *(ScrStr *const *)left;
|
|
const ScrStr *b = *(ScrStr *const *)right;
|
|
size_t shared = a->len < b->len ? a->len : b->len;
|
|
int cmp = memcmp(a->data, b->data, shared);
|
|
if (cmp != 0) return cmp;
|
|
return a->len < b->len ? -1 : a->len > b->len ? 1 : 0;
|
|
}
|
|
|
|
static ScrStr **sf_headers_sorted_names(SfHeaders *h, size_t *count_out) {
|
|
size_t n = (size_t)scr_arr_len(h->pairs);
|
|
size_t capacity = n / 2;
|
|
ScrStr **names = capacity ? malloc(capacity * sizeof *names) : NULL;
|
|
if (capacity && !names) sf_oom();
|
|
size_t count = 0;
|
|
for (size_t i = 0; i + 1 < n; i += 2) {
|
|
ScrStr *key = scr_arr_get_ref(h->pairs, (double)i);
|
|
bool seen = false;
|
|
for (size_t j = 0; j < count; j++) {
|
|
if (sf_headers_key_eq(names[j], key)) {
|
|
seen = true;
|
|
break;
|
|
}
|
|
}
|
|
if (seen) scr_str_release(key);
|
|
else names[count++] = key;
|
|
}
|
|
if (count > 1) {
|
|
qsort(names, count, sizeof *names, &sf_headers_name_cmp);
|
|
}
|
|
*count_out = count;
|
|
return names;
|
|
}
|
|
|
|
static ScrDyn *sf_headers_invoke(void *ptr, ScrDyn *self,
|
|
const char *method,
|
|
ScrDyn *const *args, size_t argc,
|
|
const char *what) {
|
|
SfHeaders *h = ptr;
|
|
if (strcmp(method, "get") == 0 || strcmp(method, "has") == 0) {
|
|
if (argc < 1) {
|
|
sf_type_error("Headers.get/has requires a header name");
|
|
return NULL;
|
|
}
|
|
ScrStr *name = sf_headers_name(args[0]);
|
|
if (!name) return NULL;
|
|
ScrStr *value = sf_headers_get_value(h, name);
|
|
scr_str_release(name);
|
|
if (strcmp(method, "has") == 0) {
|
|
bool present = value != NULL;
|
|
scr_str_release(value);
|
|
return scr_dyn_new_bool(present);
|
|
}
|
|
if (!value) return scr_dyn_new_null();
|
|
ScrDyn *out = scr_dyn_new_str(value);
|
|
scr_str_release(value);
|
|
return out;
|
|
}
|
|
if (strcmp(method, "getSetCookie") == 0) {
|
|
ScrDyn *out = scr_dyn_new_arr();
|
|
size_t n = (size_t)scr_arr_len(h->pairs);
|
|
for (size_t i = 0; i + 1 < n; i += 2) {
|
|
ScrStr *key = scr_arr_get_ref(h->pairs, (double)i);
|
|
ScrStr *value = scr_arr_get_ref(h->pairs, (double)(i + 1));
|
|
if (key->len == 10 &&
|
|
memcmp(key->data, "set-cookie", 10) == 0) {
|
|
scr_dyn_arr_push(out, scr_dyn_new_str(value));
|
|
}
|
|
scr_str_release(key);
|
|
scr_str_release(value);
|
|
}
|
|
return out;
|
|
}
|
|
if (strcmp(method, "forEach") == 0) {
|
|
if (argc < 1 || args[0]->kind != SCR_DYN_FUNC) {
|
|
sf_type_error("Headers.forEach requires a callback function");
|
|
return NULL;
|
|
}
|
|
ScrDyn *callback = args[0];
|
|
const ScrDyn *this_arg =
|
|
argc > 1 ? args[1] : scr_dyn_undefined();
|
|
size_t count = 0;
|
|
ScrStr **names = sf_headers_sorted_names(h, &count);
|
|
for (size_t i = 0; i < count; i++) {
|
|
ScrStr *value = sf_headers_get_value(h, names[i]);
|
|
ScrDyn *boxed_value = scr_dyn_new_str(value);
|
|
ScrDyn *boxed_name = scr_dyn_new_str(names[i]);
|
|
ScrDyn *call_args[3] = {boxed_value, boxed_name, self};
|
|
scr_dyn_this_push_dyn(this_arg);
|
|
ScrDyn *result =
|
|
scr_dyn_call(callback, call_args, 3, "Headers.forEach callback");
|
|
scr_dyn_this_pop();
|
|
scr_dyn_release(boxed_value);
|
|
scr_dyn_release(boxed_name);
|
|
scr_str_release(value);
|
|
scr_str_release(names[i]);
|
|
if (!result) {
|
|
for (size_t j = i + 1; j < count; j++) {
|
|
scr_str_release(names[j]);
|
|
}
|
|
free(names);
|
|
return NULL;
|
|
}
|
|
scr_dyn_release(result);
|
|
}
|
|
free(names);
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
if (strcmp(method, "append") == 0 || strcmp(method, "delete") == 0 ||
|
|
strcmp(method, "set") == 0) {
|
|
size_t required = strcmp(method, "delete") == 0 ? 1 : 2;
|
|
if (argc < required) {
|
|
sf_type_error("Headers mutation requires its declared arguments");
|
|
return NULL;
|
|
}
|
|
sf_type_error("immutable");
|
|
return NULL;
|
|
}
|
|
sf_not_function(what);
|
|
return NULL;
|
|
}
|
|
|
|
static ScrDyn *sf_headers_get(void *ptr, const char *key, size_t len) {
|
|
(void)ptr;
|
|
(void)key;
|
|
(void)len;
|
|
return NULL;
|
|
}
|
|
|
|
/* ── Response ────────────────────────────────────────────────────── */
|
|
|
|
static SfResponse *sf_response_retain(SfResponse *r) {
|
|
r->rc++;
|
|
return r;
|
|
}
|
|
|
|
static void sf_response_release(SfResponse *r) {
|
|
if (!r || --r->rc > 0) return;
|
|
sf_stream_release(r->body);
|
|
sf_headers_release(r->headers);
|
|
scr_str_release(r->url);
|
|
scr_str_release(r->status_text);
|
|
free(r);
|
|
}
|
|
|
|
static void *sf_response_retain_v(void *p) {
|
|
return sf_response_retain((SfResponse *)p);
|
|
}
|
|
static void sf_response_release_v(void *p) {
|
|
sf_response_release((SfResponse *)p);
|
|
}
|
|
|
|
static ScrPromise *sf_response_collect(SfResponse *r, int mode) {
|
|
if (!r->null_body) return sf_stream_collect(r->body, mode);
|
|
|
|
/*
|
|
* A null body is not a disturbed empty stream: body stays null,
|
|
* bodyUsed stays false, and the body readers may be called repeatedly.
|
|
* Each call therefore consumes its own empty payload.
|
|
*/
|
|
ScrPromise *p = scr_promise_new();
|
|
ScrDyn *value = NULL;
|
|
if (mode == SF_COLLECT_JSON) {
|
|
ScrStr *empty = scr_str_new("", 0);
|
|
value = scr_json_parse(empty);
|
|
scr_str_release(empty);
|
|
} else if (mode == SF_COLLECT_TEXT) {
|
|
ScrStr *empty = scr_str_new("", 0);
|
|
value = scr_dyn_new_str(empty);
|
|
scr_str_release(empty);
|
|
} else {
|
|
ScrBytes *empty = scr_bytes_new(SCR_BYTES_U8, 0);
|
|
value = scr_dyn_new_bytes_copy(empty);
|
|
scr_bytes_release(empty);
|
|
}
|
|
if (scr_exc_pending()) {
|
|
scr_dyn_release(value);
|
|
scr_promise_reject_pending(p);
|
|
} else {
|
|
scr_promise_fulfill_ref(p, value, &scr_dyn_retain_v,
|
|
&scr_dyn_release_v, NULL);
|
|
}
|
|
return p;
|
|
}
|
|
|
|
static ScrDyn *sf_response_invoke(void *ptr, ScrDyn *self,
|
|
const char *method,
|
|
ScrDyn *const *args, size_t argc,
|
|
const char *what) {
|
|
SfResponse *r = ptr;
|
|
int mode = -1;
|
|
if (strcmp(method, "json") == 0) mode = SF_COLLECT_JSON;
|
|
else if (strcmp(method, "text") == 0) mode = SF_COLLECT_TEXT;
|
|
else if (strcmp(method, "bytes") == 0) mode = SF_COLLECT_BYTES;
|
|
if (mode >= 0) {
|
|
ScrPromise *p = sf_response_collect(r, mode);
|
|
ScrDyn *out = scr_dyn_new_promise(p);
|
|
scr_promise_release(p);
|
|
return out;
|
|
}
|
|
(void)args;
|
|
(void)self;
|
|
sf_not_function(what);
|
|
return NULL;
|
|
}
|
|
|
|
static ScrDyn *sf_response_get(void *ptr, const char *key, size_t len) {
|
|
SfResponse *r = ptr;
|
|
if (sf_name(key, len, "status")) return scr_dyn_new_num((double)r->status);
|
|
if (sf_name(key, len, "ok")) {
|
|
return scr_dyn_new_bool(r->status >= 200 && r->status <= 299);
|
|
}
|
|
if (sf_name(key, len, "bodyUsed")) {
|
|
return scr_dyn_new_bool(!r->null_body && r->body->disturbed);
|
|
}
|
|
if (sf_name(key, len, "body")) {
|
|
return r->null_body
|
|
? scr_dyn_new_null()
|
|
: scr_dyn_new_handle(r->body, SCR_DYNH_WEB_STREAM);
|
|
}
|
|
if (sf_name(key, len, "url")) return scr_dyn_new_str(r->url);
|
|
if (sf_name(key, len, "redirected")) {
|
|
return scr_dyn_new_bool(r->redirected);
|
|
}
|
|
if (sf_name(key, len, "statusText")) {
|
|
return scr_dyn_new_str(r->status_text);
|
|
}
|
|
if (sf_name(key, len, "headers")) {
|
|
return scr_dyn_new_handle(r->headers, SCR_DYNH_FETCH_HEADERS);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static ScrPromise *sf_response_collect_dyn(ScrDyn *response, int mode) {
|
|
if (!response || response->kind != SCR_DYN_HANDLE ||
|
|
response->v.handle.tag != SCR_DYNH_FETCH_RESPONSE) {
|
|
sf_type_error("Illegal invocation");
|
|
return scr_promise_settled_ref(NULL, &scr_dyn_retain_v,
|
|
&scr_dyn_release_v, NULL);
|
|
}
|
|
SfResponse *r = response->v.handle.ptr;
|
|
return sf_response_collect(r, mode);
|
|
}
|
|
|
|
ScrPromise *scr_fetch_response_json(ScrDyn *response) {
|
|
return sf_response_collect_dyn(response, SF_COLLECT_JSON);
|
|
}
|
|
|
|
ScrPromise *scr_fetch_response_text(ScrDyn *response) {
|
|
return sf_response_collect_dyn(response, SF_COLLECT_TEXT);
|
|
}
|
|
|
|
ScrPromise *scr_fetch_response_bytes(ScrDyn *response) {
|
|
return sf_response_collect_dyn(response, SF_COLLECT_BYTES);
|
|
}
|
|
|
|
/* ── fetch transfer ──────────────────────────────────────────────── */
|
|
|
|
static SfTransfer *sf_retain(SfTransfer *t) {
|
|
t->rc++;
|
|
return t;
|
|
}
|
|
|
|
static void sf_release(SfTransfer *t) {
|
|
if (--t->rc > 0) return;
|
|
scr_promise_release(t->promise);
|
|
scr_url_release(t->url);
|
|
scr_str_release(t->method);
|
|
scr_arr_release(t->headers);
|
|
scr_dyn_release(t->body);
|
|
if (t->client) scr_http_client_release(t->client);
|
|
scr_net_sock_release(t->response_socket);
|
|
scr_bytes_release(t->inflate_pending);
|
|
if (t->inflating) inflateEnd(&t->zs);
|
|
sf_watch_free(t->signal_watch);
|
|
sf_signal_release(t->signal);
|
|
sf_stream_release(t->request_stream);
|
|
sf_stream_release(t->response_stream);
|
|
free(t);
|
|
}
|
|
|
|
static void *sf_retain_v(void *p) { return sf_retain((SfTransfer *)p); }
|
|
static void sf_release_v(void *p) { sf_release((SfTransfer *)p); }
|
|
|
|
static ScrClosure *sf_closure(SfTransfer *t, void *fn) {
|
|
ScrClosure *cb = scr_closure_new(fn, 1);
|
|
ScrBox *box = scr_box_new_obj(&sf_retain_v, &sf_release_v, NULL);
|
|
scr_box_set_ref(box, sf_retain(t));
|
|
cb->caps[0] = box;
|
|
return cb;
|
|
}
|
|
|
|
static SfTransfer *sf_from(ScrClosure *cb) {
|
|
return (SfTransfer *)scr_box_get_ref(cb->caps[0]);
|
|
}
|
|
|
|
static void sf_settle(SfTransfer *t) {
|
|
if (t->done) return;
|
|
t->done = true;
|
|
if (t->signal_watch) {
|
|
sf_watch_free(t->signal_watch);
|
|
t->signal_watch = NULL;
|
|
}
|
|
if (t->request_stream && t->request_stream->request_owner == t) {
|
|
t->request_stream->request_owner = NULL;
|
|
}
|
|
if (t->response_stream && t->response_stream->response_owner == t) {
|
|
t->response_stream->response_owner = NULL;
|
|
}
|
|
if (t->client) {
|
|
scr_http_client_release(t->client);
|
|
t->client = NULL;
|
|
}
|
|
if (t->response_socket) {
|
|
scr_net_sock_release(t->response_socket);
|
|
t->response_socket = NULL;
|
|
t->response_paused = false;
|
|
}
|
|
for (SfTransfer **link = &sf_live; *link; link = &(*link)->next) {
|
|
if (*link == t) {
|
|
*link = t->next;
|
|
sf_release(t); /* registry reference */
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void sf_reject(SfTransfer *t, const char *message) {
|
|
if (t->done) return;
|
|
sf_type_error(message);
|
|
scr_promise_reject_pending(t->promise);
|
|
sf_settle(t);
|
|
}
|
|
|
|
static void sf_reject_reason(SfTransfer *t, ScrDyn *reason) {
|
|
if (t->done) return;
|
|
sf_reject_promise_reason(t->promise, reason);
|
|
sf_settle(t);
|
|
}
|
|
|
|
static void sf_transfer_abort_watch(SfSignalWatch *w, SfSignal *signal) {
|
|
SfTransfer *t = w->owner;
|
|
if (t->done) return;
|
|
if (t->request_stream && t->request_stream->request_owner == t) {
|
|
SfStream *request = t->request_stream;
|
|
request->request_owner = NULL;
|
|
if (!request->close_requested && !request->closed && !request->error) {
|
|
request->request_discarding = true;
|
|
sf_stream_drain(request);
|
|
sf_stream_pull(request);
|
|
}
|
|
}
|
|
if (t->client) scr_http_client_destroy(t->client);
|
|
if (t->response_sent && t->response_stream) {
|
|
sf_stream_error(t->response_stream, signal->reason);
|
|
sf_settle(t);
|
|
} else {
|
|
sf_reject_reason(t, signal->reason);
|
|
}
|
|
}
|
|
|
|
static void sf_transfer_stream_error(SfTransfer *t, ScrDyn *reason) {
|
|
if (!t || t->done) return;
|
|
if (t->client) scr_http_client_destroy(t->client);
|
|
if (t->response_sent && t->response_stream) {
|
|
sf_stream_error(t->response_stream, reason);
|
|
sf_settle(t);
|
|
} else {
|
|
sf_reject(t, "fetch failed");
|
|
}
|
|
}
|
|
|
|
static bool sf_eq_ci(const ScrStr *s, const char *lit) {
|
|
size_t n = strlen(lit);
|
|
if (s->len != n) return false;
|
|
for (size_t i = 0; i < n; i++) {
|
|
char c = s->data[i];
|
|
if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
|
|
if (c != lit[i]) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static ScrStr *sf_bare_host(const ScrStr *host) {
|
|
if (host->len >= 2 && host->data[0] == '[' &&
|
|
host->data[host->len - 1] == ']') {
|
|
return scr_str_new(host->data + 1, host->len - 2);
|
|
}
|
|
return scr_str_retain((ScrStr *)host);
|
|
}
|
|
|
|
static int sf_port(const ScrUrl *u, int dflt) {
|
|
if (u->port->len == 0) return dflt;
|
|
int p = 0;
|
|
for (size_t i = 0; i < u->port->len; i++) {
|
|
p = p * 10 + (u->port->data[i] - '0');
|
|
}
|
|
return p;
|
|
}
|
|
|
|
static bool sf_bad_port(int port) {
|
|
static const int ports[] = {SCR_FETCH_BAD_PORTS};
|
|
for (size_t i = 0; i < sizeof ports / sizeof ports[0]; i++) {
|
|
if (port == ports[i]) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static ScrStr *sf_path(const ScrUrl *u) {
|
|
size_t plen = (u->path->len > 0 ? u->path->len : 1) + u->query->len;
|
|
char *buf = malloc(plen);
|
|
if (!buf) sf_oom();
|
|
size_t n = 0;
|
|
if (u->path->len > 0) {
|
|
memcpy(buf, u->path->data, u->path->len);
|
|
n = u->path->len;
|
|
} else {
|
|
buf[n++] = '/';
|
|
}
|
|
memcpy(buf + n, u->query->data, u->query->len);
|
|
n += u->query->len;
|
|
ScrStr *out = scr_str_new(buf, n);
|
|
free(buf);
|
|
return out;
|
|
}
|
|
|
|
static ScrStr *sf_url_serialize(const ScrUrl *u) {
|
|
size_t cap = u->scheme->len + u->userinfo->len + u->host->len +
|
|
u->port->len + u->path->len + u->query->len + 8;
|
|
char *buf = malloc(cap);
|
|
if (!buf) sf_oom();
|
|
size_t n = 0;
|
|
memcpy(buf + n, u->scheme->data, u->scheme->len);
|
|
n += u->scheme->len;
|
|
buf[n++] = ':';
|
|
if (u->has_authority) {
|
|
buf[n++] = '/';
|
|
buf[n++] = '/';
|
|
if (u->userinfo->len > 0) {
|
|
memcpy(buf + n, u->userinfo->data, u->userinfo->len);
|
|
n += u->userinfo->len;
|
|
buf[n++] = '@';
|
|
}
|
|
memcpy(buf + n, u->host->data, u->host->len);
|
|
n += u->host->len;
|
|
if (u->port->len > 0) {
|
|
buf[n++] = ':';
|
|
memcpy(buf + n, u->port->data, u->port->len);
|
|
n += u->port->len;
|
|
}
|
|
}
|
|
if (u->path->len > 0) {
|
|
memcpy(buf + n, u->path->data, u->path->len);
|
|
n += u->path->len;
|
|
} else {
|
|
buf[n++] = '/';
|
|
}
|
|
memcpy(buf + n, u->query->data, u->query->len);
|
|
n += u->query->len;
|
|
ScrStr *out = scr_str_new(buf, n);
|
|
free(buf);
|
|
return out;
|
|
}
|
|
|
|
static ScrUrl *sf_url_parse_quiet(ScrStr *text) {
|
|
ScrUrl *url = scr_url_new(text);
|
|
if (!url) scr_exc_clear();
|
|
return url;
|
|
}
|
|
|
|
static bool sf_proxy_enabled;
|
|
static char *sf_http_proxy;
|
|
static char *sf_https_proxy;
|
|
static char *sf_all_proxy;
|
|
static char *sf_no_proxy;
|
|
|
|
static char *sf_env_copy(const char *lower, const char *upper) {
|
|
const char *value = getenv(lower);
|
|
if (!value || value[0] == '\0') value = getenv(upper);
|
|
if (!value || value[0] == '\0') return NULL;
|
|
size_t len = strlen(value);
|
|
char *copy = malloc(len + 1);
|
|
if (!copy) sf_oom();
|
|
memcpy(copy, value, len + 1);
|
|
return copy;
|
|
}
|
|
|
|
static void sf_proxy_snapshot(void) {
|
|
const char *optin = getenv("NODE_USE_ENV_PROXY");
|
|
sf_proxy_enabled = optin && strcmp(optin, "1") == 0;
|
|
if (!sf_proxy_enabled) return;
|
|
sf_http_proxy = sf_env_copy("http_proxy", "HTTP_PROXY");
|
|
sf_https_proxy = sf_env_copy("https_proxy", "HTTPS_PROXY");
|
|
sf_all_proxy = sf_env_copy("all_proxy", "ALL_PROXY");
|
|
sf_no_proxy = sf_env_copy("no_proxy", "NO_PROXY");
|
|
}
|
|
|
|
static void sf_proxy_snapshot_free(void) {
|
|
free(sf_http_proxy);
|
|
free(sf_https_proxy);
|
|
free(sf_all_proxy);
|
|
free(sf_no_proxy);
|
|
sf_http_proxy = sf_https_proxy = sf_all_proxy = sf_no_proxy = NULL;
|
|
}
|
|
|
|
static int sf_hex_value(char c) {
|
|
if (c >= '0' && c <= '9') return c - '0';
|
|
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
|
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
|
return -1;
|
|
}
|
|
|
|
/* EnvHttpProxyAgent derives Basic proxy authentication from the proxy URL's
|
|
* decoded username/password. ScrUrl keeps the encoded userinfo as one string,
|
|
* so decode it here and add the empty-password separator when necessary. */
|
|
static ScrStr *sf_proxy_authorization(const ScrStr *userinfo) {
|
|
char *plain = malloc(userinfo->len + 1);
|
|
if (!plain) sf_oom();
|
|
size_t plain_len = 0;
|
|
bool has_separator = false;
|
|
for (size_t i = 0; i < userinfo->len;) {
|
|
char c = userinfo->data[i];
|
|
if (c == '%' && i + 2 < userinfo->len) {
|
|
int hi = sf_hex_value(userinfo->data[i + 1]);
|
|
int lo = sf_hex_value(userinfo->data[i + 2]);
|
|
if (hi >= 0 && lo >= 0) {
|
|
plain[plain_len++] = (char)((hi << 4) | lo);
|
|
i += 3;
|
|
continue;
|
|
}
|
|
}
|
|
if (c == ':') has_separator = true;
|
|
plain[plain_len++] = c;
|
|
i++;
|
|
}
|
|
if (!has_separator) plain[plain_len++] = ':';
|
|
|
|
static const char b64[] =
|
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
size_t encoded_len = ((plain_len + 2) / 3) * 4;
|
|
char *header = malloc(6 + encoded_len);
|
|
if (!header) sf_oom();
|
|
memcpy(header, "Basic ", 6);
|
|
size_t out = 6;
|
|
for (size_t i = 0; i < plain_len; i += 3) {
|
|
unsigned b0 = (unsigned char)plain[i];
|
|
unsigned b1 = i + 1 < plain_len ? (unsigned char)plain[i + 1] : 0;
|
|
unsigned b2 = i + 2 < plain_len ? (unsigned char)plain[i + 2] : 0;
|
|
unsigned triple = (b0 << 16) | (b1 << 8) | b2;
|
|
header[out++] = b64[(triple >> 18) & 63];
|
|
header[out++] = b64[(triple >> 12) & 63];
|
|
header[out++] = i + 1 < plain_len ? b64[(triple >> 6) & 63] : '=';
|
|
header[out++] = i + 2 < plain_len ? b64[triple & 63] : '=';
|
|
}
|
|
free(plain);
|
|
ScrStr *result = scr_str_new(header, out);
|
|
free(header);
|
|
return result;
|
|
}
|
|
|
|
static bool sf_no_proxy_match(const char *list, const ScrStr *host,
|
|
int port) {
|
|
const char *p = list;
|
|
while (*p) {
|
|
while (*p == ',' || *p == ' ' || *p == '\t') p++;
|
|
const char *start = p;
|
|
while (*p && *p != ',' && *p != ' ' && *p != '\t') p++;
|
|
size_t len = (size_t)(p - start);
|
|
if (len == 0) continue;
|
|
if (len == 1 && start[0] == '*') return true;
|
|
size_t host_len = len;
|
|
long entry_port = -1;
|
|
if (start[0] == '[') {
|
|
size_t close = 1;
|
|
while (close < len && start[close] != ']') close++;
|
|
if (close < len && close + 1 < len && start[close + 1] == ':') {
|
|
host_len = close + 1;
|
|
entry_port = strtol(start + close + 2, NULL, 10);
|
|
}
|
|
} else {
|
|
for (size_t i = 0; i < len; i++) {
|
|
if (start[i] == ':') {
|
|
host_len = i;
|
|
entry_port = strtol(start + i + 1, NULL, 10);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (host_len >= 2 && start[0] == '*' && start[1] == '.') {
|
|
start += 2;
|
|
host_len -= 2;
|
|
} else if (host_len > 0 && start[0] == '.') {
|
|
start++;
|
|
host_len--;
|
|
}
|
|
if (host_len == 0 || (entry_port >= 0 && entry_port != port)) {
|
|
continue;
|
|
}
|
|
size_t offset = host->len >= host_len ? host->len - host_len : 0;
|
|
bool boundary =
|
|
host->len == host_len ||
|
|
(host->len > host_len && host->data[offset - 1] == '.');
|
|
if (!boundary) continue;
|
|
bool equal = true;
|
|
for (size_t i = 0; i < host_len; i++) {
|
|
char a = host->data[offset + i];
|
|
char b = start[i];
|
|
if (a >= 'A' && a <= 'Z') a = (char)(a - 'A' + 'a');
|
|
if (b >= 'A' && b <= 'Z') b = (char)(b - 'A' + 'a');
|
|
if (a != b) {
|
|
equal = false;
|
|
break;
|
|
}
|
|
}
|
|
if (equal) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static ScrUrl *sf_proxy_for(const ScrUrl *target, bool https,
|
|
int target_port, bool *invalid) {
|
|
*invalid = false;
|
|
if (!sf_proxy_enabled) return NULL;
|
|
const char *proxy = https ? sf_https_proxy : sf_http_proxy;
|
|
if (!proxy) proxy = sf_all_proxy;
|
|
if (!proxy) return NULL;
|
|
if (sf_no_proxy &&
|
|
sf_no_proxy_match(sf_no_proxy, target->host, target_port)) {
|
|
return NULL;
|
|
}
|
|
ScrStr *text = scr_str_new(proxy, strlen(proxy));
|
|
ScrUrl *url = sf_url_parse_quiet(text);
|
|
scr_str_release(text);
|
|
*invalid = url == NULL;
|
|
return url;
|
|
}
|
|
|
|
static bool sf_same_scheme_relative(const ScrUrl *base,
|
|
const ScrStr *location) {
|
|
if (location->len <= base->scheme->len ||
|
|
location->data[base->scheme->len] != ':') {
|
|
return false;
|
|
}
|
|
for (size_t i = 0; i < base->scheme->len; i++) {
|
|
char a = location->data[i];
|
|
char b = base->scheme->data[i];
|
|
if (a >= 'A' && a <= 'Z') a = (char)(a - 'A' + 'a');
|
|
if (b >= 'A' && b <= 'Z') b = (char)(b - 'A' + 'a');
|
|
if (a != b) return false;
|
|
}
|
|
size_t tail = base->scheme->len + 1;
|
|
return location->len - tail < 2 ||
|
|
!((location->data[tail] == '/' ||
|
|
location->data[tail] == '\\') &&
|
|
(location->data[tail + 1] == '/' ||
|
|
location->data[tail + 1] == '\\'));
|
|
}
|
|
|
|
static ScrUrl *sf_resolve_url(const ScrUrl *base, const ScrStr *location) {
|
|
bool same_scheme_relative = sf_same_scheme_relative(base, location);
|
|
const char *ref_data =
|
|
location->data +
|
|
(same_scheme_relative ? base->scheme->len + 1 : 0);
|
|
size_t ref_len =
|
|
location->len -
|
|
(same_scheme_relative ? base->scheme->len + 1 : 0);
|
|
if (!same_scheme_relative) {
|
|
ScrStr *absolute = scr_str_new(location->data, location->len);
|
|
ScrUrl *parsed = sf_url_parse_quiet(absolute);
|
|
scr_str_release(absolute);
|
|
if (parsed) return parsed;
|
|
}
|
|
|
|
/*
|
|
* Empty and fragment-only references preserve the complete base path
|
|
* and query. The request serializer drops the resulting fragment, but
|
|
* resolving it first is significant: `Location: #next` re-requests
|
|
* `/a/start`, not its parent `/a/`.
|
|
*/
|
|
if (ref_len == 0 || ref_data[0] == '#') {
|
|
ScrStr *base_text = sf_url_serialize(base);
|
|
size_t len = base_text->len + ref_len;
|
|
char *joined = malloc(len);
|
|
if (!joined) sf_oom();
|
|
memcpy(joined, base_text->data, base_text->len);
|
|
memcpy(joined + base_text->len, ref_data, ref_len);
|
|
ScrStr *text = scr_str_new(joined, len);
|
|
free(joined);
|
|
scr_str_release(base_text);
|
|
ScrUrl *out = sf_url_parse_quiet(text);
|
|
scr_str_release(text);
|
|
return out;
|
|
}
|
|
|
|
size_t cap = base->scheme->len + base->userinfo->len + base->host->len +
|
|
base->port->len + base->path->len + base->query->len +
|
|
ref_len + 16;
|
|
char *buf = malloc(cap);
|
|
if (!buf) sf_oom();
|
|
size_t n = 0;
|
|
memcpy(buf + n, base->scheme->data, base->scheme->len);
|
|
n += base->scheme->len;
|
|
buf[n++] = ':';
|
|
if (ref_len >= 2 &&
|
|
(ref_data[0] == '/' || ref_data[0] == '\\') &&
|
|
(ref_data[1] == '/' || ref_data[1] == '\\')) {
|
|
memcpy(buf + n, ref_data, ref_len);
|
|
n += ref_len;
|
|
} else {
|
|
buf[n++] = '/';
|
|
buf[n++] = '/';
|
|
if (base->userinfo->len > 0) {
|
|
memcpy(buf + n, base->userinfo->data, base->userinfo->len);
|
|
n += base->userinfo->len;
|
|
buf[n++] = '@';
|
|
}
|
|
memcpy(buf + n, base->host->data, base->host->len);
|
|
n += base->host->len;
|
|
if (base->port->len > 0) {
|
|
buf[n++] = ':';
|
|
memcpy(buf + n, base->port->data, base->port->len);
|
|
n += base->port->len;
|
|
}
|
|
if (ref_len > 0 &&
|
|
(ref_data[0] == '/' || ref_data[0] == '\\')) {
|
|
memcpy(buf + n, ref_data, ref_len);
|
|
n += ref_len;
|
|
} else if (ref_len > 0 && ref_data[0] == '?') {
|
|
memcpy(buf + n, base->path->data, base->path->len);
|
|
n += base->path->len;
|
|
memcpy(buf + n, ref_data, ref_len);
|
|
n += ref_len;
|
|
} else {
|
|
size_t keep = 0;
|
|
for (size_t i = 0; i < base->path->len; i++) {
|
|
if (base->path->data[i] == '/') keep = i + 1;
|
|
}
|
|
memcpy(buf + n, base->path->data, keep);
|
|
n += keep;
|
|
if (keep == 0) buf[n++] = '/';
|
|
memcpy(buf + n, ref_data, ref_len);
|
|
n += ref_len;
|
|
}
|
|
}
|
|
ScrStr *text = scr_str_new(buf, n);
|
|
free(buf);
|
|
ScrUrl *out = sf_url_parse_quiet(text);
|
|
scr_str_release(text);
|
|
return out;
|
|
}
|
|
|
|
static bool sf_pairs_have(ScrArr *pairs, const char *name) {
|
|
size_t n = (size_t)scr_arr_len(pairs);
|
|
size_t want = strlen(name);
|
|
for (size_t i = 0; i + 1 < n; i += 2) {
|
|
ScrStr *key = (ScrStr *)scr_arr_get_ref(pairs, (double)i);
|
|
bool same = key->len == want;
|
|
for (size_t j = 0; j < want && same; j++) {
|
|
char c = key->data[j];
|
|
if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
|
|
same = c == name[j];
|
|
}
|
|
scr_str_release(key);
|
|
if (same) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool sf_header_name_ok(const char *s, size_t len) {
|
|
if (len == 0) return false;
|
|
for (size_t i = 0; i < len; i++) {
|
|
unsigned char c = (unsigned char)s[i];
|
|
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
|
(c >= '0' && c <= '9') ||
|
|
strchr("!#$%&'*+-.^_`|~", c) != NULL)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool sf_header_value_ok(const ScrStr *value) {
|
|
for (size_t i = 0; i < value->len; i++) {
|
|
unsigned char c = (unsigned char)value->data[i];
|
|
if ((c < 0x20 && c != '\t') || c == 0x7f) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static ScrStr *sf_header_value_bytestring(ScrStr *value) {
|
|
size_t len = (size_t)scr_str_utf16_len(value);
|
|
char *bytes = malloc(len ? len : 1);
|
|
if (!bytes) sf_oom();
|
|
for (size_t i = 0; i < len; i++) {
|
|
double code = scr_str_char_code_at(value, (double)i);
|
|
if (code > 255) {
|
|
char message[192];
|
|
int message_len = snprintf(
|
|
message, sizeof message,
|
|
"Cannot convert argument to a ByteString because the character "
|
|
"at index %zu has a value of %.0f which is greater than 255.",
|
|
i, code);
|
|
free(bytes);
|
|
if (message_len < 0 || (size_t)message_len >= sizeof message) {
|
|
sf_oom();
|
|
}
|
|
sf_type_error(message);
|
|
return NULL;
|
|
}
|
|
bytes[i] = (char)(unsigned char)code;
|
|
}
|
|
ScrStr *out = scr_str_new(bytes, len);
|
|
free(bytes);
|
|
return out;
|
|
}
|
|
|
|
static bool sf_header_name_ci(
|
|
const char *name, size_t name_len, const char *want) {
|
|
size_t want_len = strlen(want);
|
|
if (name_len != want_len) return false;
|
|
for (size_t i = 0; i < name_len; i++) {
|
|
char c = name[i];
|
|
if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
|
|
if (c != want[i]) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool sf_header_value_ci(const ScrStr *value, const char *want) {
|
|
size_t start = 0;
|
|
size_t end = value->len;
|
|
while (start < end &&
|
|
(value->data[start] == ' ' || value->data[start] == '\t')) {
|
|
start++;
|
|
}
|
|
while (end > start &&
|
|
(value->data[end - 1] == ' ' || value->data[end - 1] == '\t')) {
|
|
end--;
|
|
}
|
|
return sf_header_name_ci(
|
|
value->data + start, end - start, want);
|
|
}
|
|
|
|
static bool sf_request_header_ok(
|
|
const char *name, size_t name_len, const ScrStr *value) {
|
|
if (sf_header_name_ci(name, name_len, "connection")) {
|
|
return sf_header_value_ci(value, "close") ||
|
|
sf_header_value_ci(value, "keep-alive");
|
|
}
|
|
return
|
|
!sf_header_name_ci(name, name_len, "transfer-encoding") &&
|
|
!sf_header_name_ci(name, name_len, "keep-alive") &&
|
|
!sf_header_name_ci(name, name_len, "upgrade") &&
|
|
!sf_header_name_ci(name, name_len, "expect");
|
|
}
|
|
|
|
static ScrStr *sf_trim_header_value(const ScrStr *value) {
|
|
size_t start = 0;
|
|
size_t end = value->len;
|
|
while (start < end &&
|
|
(value->data[start] == ' ' || value->data[start] == '\t')) {
|
|
start++;
|
|
}
|
|
while (end > start &&
|
|
(value->data[end - 1] == ' ' || value->data[end - 1] == '\t')) {
|
|
end--;
|
|
}
|
|
return scr_str_new(value->data + start, end - start);
|
|
}
|
|
|
|
static bool sf_push_header(ScrArr *pairs, const char *name, size_t name_len,
|
|
ScrStr *value) {
|
|
if (!sf_header_name_ok(name, name_len)) {
|
|
sf_type_error("fetch failed");
|
|
return false;
|
|
}
|
|
ScrStr *byte_value = sf_header_value_bytestring(value);
|
|
if (!byte_value) return false;
|
|
if (!sf_header_value_ok(byte_value)) {
|
|
scr_str_release(byte_value);
|
|
sf_type_error("fetch failed");
|
|
return false;
|
|
}
|
|
ScrStr *normalized = sf_trim_header_value(byte_value);
|
|
scr_str_release(byte_value);
|
|
if (!sf_header_name_ci(name, name_len, "set-cookie")) {
|
|
size_t n = (size_t)scr_arr_len(pairs);
|
|
for (size_t i = 0; i + 1 < n; i += 2) {
|
|
ScrStr *key = scr_arr_get_ref(pairs, (double)i);
|
|
bool match = key->len == name_len;
|
|
for (size_t j = 0; j < name_len && match; j++) {
|
|
unsigned char a = (unsigned char)key->data[j];
|
|
unsigned char b = (unsigned char)name[j];
|
|
if (a >= 'A' && a <= 'Z') a = (unsigned char)(a - 'A' + 'a');
|
|
if (b >= 'A' && b <= 'Z') b = (unsigned char)(b - 'A' + 'a');
|
|
match = a == b;
|
|
}
|
|
scr_str_release(key);
|
|
if (!match) continue;
|
|
|
|
ScrStr *previous =
|
|
scr_arr_get_ref(pairs, (double)(i + 1));
|
|
const char *separator =
|
|
sf_header_name_ci(name, name_len, "cookie") ? "; " : ", ";
|
|
size_t separator_len = 2;
|
|
size_t joined_len =
|
|
previous->len + separator_len + normalized->len;
|
|
char *joined_data = malloc(joined_len ? joined_len : 1);
|
|
if (!joined_data) sf_oom();
|
|
memcpy(joined_data, previous->data, previous->len);
|
|
memcpy(joined_data + previous->len, separator, separator_len);
|
|
memcpy(joined_data + previous->len + separator_len,
|
|
normalized->data, normalized->len);
|
|
ScrStr *joined = scr_str_new(joined_data, joined_len);
|
|
free(joined_data);
|
|
scr_str_release(previous);
|
|
scr_str_release(normalized);
|
|
if (!sf_request_header_ok(name, name_len, joined)) {
|
|
scr_str_release(joined);
|
|
sf_type_error("fetch failed");
|
|
return false;
|
|
}
|
|
scr_arr_set_ref(pairs, (double)(i + 1), joined);
|
|
return true;
|
|
}
|
|
}
|
|
if (!sf_request_header_ok(name, name_len, normalized)) {
|
|
scr_str_release(normalized);
|
|
sf_type_error("fetch failed");
|
|
return false;
|
|
}
|
|
scr_arr_push_ref(pairs, scr_str_new(name, name_len));
|
|
scr_arr_push_ref(pairs, normalized);
|
|
return true;
|
|
}
|
|
|
|
static bool sf_add_headers(ScrArr *pairs, const ScrDyn *headers) {
|
|
if (!headers || headers->kind == SCR_DYN_UNDEF ||
|
|
headers->kind == SCR_DYN_NULL) {
|
|
return true;
|
|
}
|
|
if (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);
|
|
ScrDyn *source = scr_dyn_retain(e->value);
|
|
ScrStr *value = scr_dyn_string_coerce_js(source);
|
|
scr_dyn_release(source);
|
|
bool ok = value && sf_push_header(pairs, name->data, name->len, value);
|
|
scr_str_release(value);
|
|
scr_str_release(name);
|
|
if (!ok) {
|
|
if (!scr_exc_pending()) sf_type_error("fetch failed");
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
if (headers->kind == SCR_DYN_ARR) {
|
|
for (size_t i = 0; i < headers->v.arr.len; i++) {
|
|
ScrDyn *pair = scr_dyn_retain(headers->v.arr.items[i]);
|
|
if (pair->kind != SCR_DYN_ARR || pair->v.arr.len != 2) {
|
|
scr_dyn_release(pair);
|
|
if (!scr_exc_pending()) sf_type_error("fetch failed");
|
|
return false;
|
|
}
|
|
ScrDyn *name_source = scr_dyn_retain(pair->v.arr.items[0]);
|
|
ScrDyn *value_source = scr_dyn_retain(pair->v.arr.items[1]);
|
|
scr_dyn_release(pair);
|
|
ScrStr *name = scr_dyn_string_coerce_js(name_source);
|
|
scr_dyn_release(name_source);
|
|
ScrStr *value = name ? scr_dyn_string_coerce_js(value_source) : NULL;
|
|
scr_dyn_release(value_source);
|
|
bool ok = name && value &&
|
|
sf_push_header(pairs, name->data, name->len, value);
|
|
scr_str_release(value);
|
|
scr_str_release(name);
|
|
if (!ok) {
|
|
if (!scr_exc_pending()) sf_type_error("fetch failed");
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
if (headers->kind == SCR_DYN_HANDLE &&
|
|
headers->v.handle.tag == SCR_DYNH_FETCH_HEADERS) {
|
|
SfHeaders *source = (SfHeaders *)headers->v.handle.ptr;
|
|
size_t n = (size_t)scr_arr_len(source->pairs);
|
|
for (size_t i = 0; i + 1 < n; i += 2) {
|
|
ScrStr *name = scr_arr_get_ref(source->pairs, (double)i);
|
|
ScrStr *value =
|
|
scr_arr_get_ref(source->pairs, (double)(i + 1));
|
|
bool ok = sf_push_header(pairs, name->data, name->len, value);
|
|
scr_str_release(name);
|
|
scr_str_release(value);
|
|
if (!ok) return false;
|
|
}
|
|
return true;
|
|
}
|
|
sf_type_error("fetch failed");
|
|
return false;
|
|
}
|
|
|
|
static void sf_push_header_text(ScrArr *pairs, const char *name,
|
|
const char *value, size_t value_len) {
|
|
scr_arr_push_ref(pairs, scr_str_new(name, strlen(name)));
|
|
scr_arr_push_ref(pairs, scr_str_new(value, value_len));
|
|
}
|
|
|
|
static void sf_strip_header(ScrArr **headers, const char *name) {
|
|
ScrArr *old = *headers;
|
|
size_t n = (size_t)scr_arr_len(old);
|
|
ScrArr *out = scr_arr_new(SCR_ELEM_STR, n);
|
|
for (size_t i = 0; i + 1 < n; i += 2) {
|
|
ScrStr *key = (ScrStr *)scr_arr_get_ref(old, (double)i);
|
|
if (sf_eq_ci(key, name)) {
|
|
scr_str_release(key);
|
|
continue;
|
|
}
|
|
scr_arr_push_ref(out, key);
|
|
scr_arr_push_ref(out, scr_arr_get_ref(old, (double)(i + 1)));
|
|
}
|
|
scr_arr_release(old);
|
|
*headers = out;
|
|
}
|
|
|
|
static bool sf_content_length(ScrArr *headers, bool *present_out,
|
|
size_t *length_out) {
|
|
bool present = false;
|
|
size_t length = 0;
|
|
size_t n = (size_t)scr_arr_len(headers);
|
|
for (size_t i = 0; i + 1 < n; i += 2) {
|
|
ScrStr *name = scr_arr_get_ref(headers, (double)i);
|
|
ScrStr *value = scr_arr_get_ref(headers, (double)(i + 1));
|
|
if (!sf_eq_ci(name, "content-length")) {
|
|
scr_str_release(name);
|
|
scr_str_release(value);
|
|
continue;
|
|
}
|
|
if (present) {
|
|
scr_str_release(name);
|
|
scr_str_release(value);
|
|
sf_type_error("fetch failed");
|
|
return false;
|
|
}
|
|
size_t start = 0;
|
|
size_t end = value->len;
|
|
while (start < end &&
|
|
(value->data[start] == ' ' || value->data[start] == '\t')) {
|
|
start++;
|
|
}
|
|
while (end > start &&
|
|
(value->data[end - 1] == ' ' ||
|
|
value->data[end - 1] == '\t')) {
|
|
end--;
|
|
}
|
|
bool valid = start < end;
|
|
size_t parsed = 0;
|
|
for (size_t j = start; j < end && valid; j++) {
|
|
unsigned char c = (unsigned char)value->data[j];
|
|
if (c < '0' || c > '9') {
|
|
valid = false;
|
|
break;
|
|
}
|
|
size_t digit = (size_t)(c - '0');
|
|
if (parsed > (SIZE_MAX - digit) / 10) {
|
|
valid = false;
|
|
break;
|
|
}
|
|
parsed = parsed * 10 + digit;
|
|
}
|
|
scr_str_release(name);
|
|
scr_str_release(value);
|
|
if (!valid) {
|
|
sf_type_error("fetch failed");
|
|
return false;
|
|
}
|
|
present = true;
|
|
length = parsed;
|
|
}
|
|
*present_out = present;
|
|
*length_out = length;
|
|
return true;
|
|
}
|
|
|
|
/* RequestInit dictionary conversion coerces every member before Request's
|
|
* method validation runs. Keep ToString separate so later members
|
|
* (redirect and signal) are still observed when the resulting method is
|
|
* syntactically invalid or forbidden. */
|
|
static ScrStr *sf_method_value(const ScrDyn *init) {
|
|
const ScrDyn *value =
|
|
init && init->kind == SCR_DYN_OBJ
|
|
? scr_dyn_obj_get(init, "method", 6)
|
|
: NULL;
|
|
if (!value || value->kind == SCR_DYN_UNDEF) return scr_str_new("GET", 3);
|
|
return scr_dyn_string_coerce_js(value);
|
|
}
|
|
|
|
static bool sf_method_validate(ScrStr **method_out) {
|
|
ScrStr *method = *method_out;
|
|
static const char *normalized[] = {
|
|
"DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT"};
|
|
for (size_t i = 0; i < sizeof normalized / sizeof normalized[0]; i++) {
|
|
const char *candidate = normalized[i];
|
|
size_t n = strlen(candidate);
|
|
if (method->len != n) continue;
|
|
bool same = true;
|
|
for (size_t j = 0; j < n && same; j++) {
|
|
char c = method->data[j];
|
|
if (c >= 'a' && c <= 'z') c = (char)(c - 'a' + 'A');
|
|
same = c == candidate[j];
|
|
}
|
|
if (same) {
|
|
scr_str_release(method);
|
|
*method_out = scr_str_new(candidate, n);
|
|
return true;
|
|
}
|
|
}
|
|
bool valid = method->len > 0;
|
|
for (size_t i = 0; i < method->len && valid; i++) {
|
|
unsigned char c = (unsigned char)method->data[i];
|
|
valid = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
|
(c >= '0' && c <= '9') ||
|
|
strchr("!#$%&'*+-.^_`|~", c) != NULL;
|
|
}
|
|
if (!valid) {
|
|
sf_type_error("fetch failed");
|
|
return false;
|
|
}
|
|
if (sf_eq_ci(method, "connect") || sf_eq_ci(method, "trace") ||
|
|
sf_eq_ci(method, "track")) {
|
|
size_t message_len = method->len + 31;
|
|
char *message = malloc(message_len + 1);
|
|
if (!message) sf_oom();
|
|
int len = snprintf(message, message_len + 1, "'%.*s' HTTP method is unsupported.",
|
|
(int)method->len, method->data);
|
|
if (len < 0 || (size_t)len > message_len) sf_oom();
|
|
sf_type_error(message);
|
|
free(message);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool sf_redirect_mode(const ScrDyn *init, int *out) {
|
|
const ScrDyn *value =
|
|
init && init->kind == SCR_DYN_OBJ
|
|
? scr_dyn_obj_get(init, "redirect", 8)
|
|
: NULL;
|
|
if (!value || value->kind == SCR_DYN_UNDEF) {
|
|
*out = SF_REDIRECT_FOLLOW;
|
|
return true;
|
|
}
|
|
ScrStr *mode = scr_dyn_string_coerce_js(value);
|
|
if (!mode) return false;
|
|
if (mode->len == 6 && memcmp(mode->data, "follow", 6) == 0) {
|
|
*out = SF_REDIRECT_FOLLOW;
|
|
scr_str_release(mode);
|
|
return true;
|
|
}
|
|
if (mode->len == 5 && memcmp(mode->data, "error", 5) == 0) {
|
|
*out = SF_REDIRECT_ERROR;
|
|
scr_str_release(mode);
|
|
return true;
|
|
}
|
|
if (mode->len == 6 && memcmp(mode->data, "manual", 6) == 0) {
|
|
*out = SF_REDIRECT_MANUAL;
|
|
scr_str_release(mode);
|
|
return true;
|
|
}
|
|
static const char prefix[] = "undefined: ";
|
|
static const char suffix[] =
|
|
" is not an accepted type. Expected one of follow, manual, error.";
|
|
size_t message_len =
|
|
sizeof prefix - 1 + mode->len + sizeof suffix - 1;
|
|
char *message = malloc(message_len + 1);
|
|
if (!message) sf_oom();
|
|
memcpy(message, prefix, sizeof prefix - 1);
|
|
memcpy(message + sizeof prefix - 1, mode->data, mode->len);
|
|
memcpy(message + sizeof prefix - 1 + mode->len,
|
|
suffix, sizeof suffix - 1);
|
|
message[message_len] = '\0';
|
|
scr_str_release(mode);
|
|
sf_type_error(message);
|
|
free(message);
|
|
return false;
|
|
}
|
|
|
|
static ScrPromise *sf_reject_now(ScrPromise *promise, const char *message) {
|
|
if (!scr_exc_pending()) sf_type_error(message);
|
|
scr_promise_reject_pending(promise);
|
|
return promise;
|
|
}
|
|
|
|
static void sf_request_length_mismatch(SfTransfer *t) {
|
|
if (t->client) scr_http_client_destroy(t->client);
|
|
sf_reject(t, "fetch failed");
|
|
}
|
|
|
|
static void sf_stream_request_flush(SfStream *s) {
|
|
SfTransfer *t = s->request_owner;
|
|
if (!t || t->done || !t->client) return;
|
|
while (s->head) {
|
|
SfChunk *c = s->head;
|
|
s->head = c->next;
|
|
if (!s->head) s->tail = NULL;
|
|
s->queued--;
|
|
ScrBytes *bytes = sf_chunk_bytes(c->value);
|
|
if (!bytes) {
|
|
sf_chunk_release(c);
|
|
sf_stream_drop_chunks(s);
|
|
ScrCaught *caught = scr_exc_take();
|
|
ScrDyn *reason = scr_caught_to_dyn(caught);
|
|
sf_transfer_stream_error(t, reason);
|
|
scr_dyn_release(reason);
|
|
scr_caught_release(caught);
|
|
return;
|
|
}
|
|
if (t->request_has_content_length &&
|
|
(t->request_body_sent > t->request_content_length ||
|
|
bytes->len >
|
|
t->request_content_length - t->request_body_sent)) {
|
|
scr_bytes_release(bytes);
|
|
sf_chunk_release(c);
|
|
sf_stream_drop_chunks(s);
|
|
sf_request_length_mismatch(t);
|
|
return;
|
|
}
|
|
t->request_body_sent += bytes->len;
|
|
scr_http_client_write_bytes(t->client, bytes);
|
|
scr_bytes_release(bytes);
|
|
sf_chunk_release(c);
|
|
}
|
|
if (s->error) {
|
|
sf_transfer_stream_error(t, s->error);
|
|
} else if (s->close_requested) {
|
|
if (t->request_has_content_length &&
|
|
t->request_body_sent != t->request_content_length) {
|
|
sf_request_length_mismatch(t);
|
|
return;
|
|
}
|
|
sf_stream_finish_close(s);
|
|
scr_http_client_end(t->client);
|
|
}
|
|
}
|
|
|
|
static bool sf_start_hop(SfTransfer *t);
|
|
static bool sf_redirect(SfTransfer *t, int status, const ScrStr *location);
|
|
|
|
static void sf_response_terminate(SfTransfer *t) {
|
|
if (!t || t->done || !t->response_stream) return;
|
|
if (t->client) scr_http_client_destroy(t->client);
|
|
ScrError *e = NULL;
|
|
ScrDyn *reason = sf_type_reason("terminated", &e);
|
|
sf_stream_error(t->response_stream, reason);
|
|
scr_dyn_release(reason);
|
|
scr_error_release(e);
|
|
sf_settle(t);
|
|
}
|
|
|
|
static void sf_inflate_append(
|
|
SfTransfer *t, const unsigned char *data, size_t len) {
|
|
if (len == 0) return;
|
|
size_t prior = t->inflate_pending ? t->inflate_pending->len : 0;
|
|
if (len > SIZE_MAX - prior) sf_oom();
|
|
ScrBytes *combined =
|
|
scr_bytes_new(SCR_BYTES_U8, (double)(prior + len));
|
|
if (prior > 0) {
|
|
memcpy(combined->data, t->inflate_pending->data, prior);
|
|
}
|
|
memcpy(combined->data + prior, data, len);
|
|
scr_bytes_release(t->inflate_pending);
|
|
t->inflate_pending = combined;
|
|
}
|
|
|
|
/*
|
|
* Inflate only while the decoded stream has demand. zlib keeps a small
|
|
* amount of output internally, while the unconsumed compressed suffix lives
|
|
* in inflate_pending so the network callback's borrowed chunk can return.
|
|
*/
|
|
static void sf_inflate_process(SfTransfer *t) {
|
|
if (!t || t->done || t->inflate_draining || !t->response_stream ||
|
|
t->response_stream->discarded ||
|
|
(!t->inflate_pending && !t->inflate_needs_drain) ||
|
|
t->response_stream->queued >= 1) {
|
|
return;
|
|
}
|
|
t->inflate_draining = true;
|
|
ScrBytes *input = t->inflate_pending;
|
|
t->inflate_pending = NULL;
|
|
t->zs.next_in = input ? (Bytef *)input->data : Z_NULL;
|
|
t->zs.avail_in = input ? (uInt)input->len : 0;
|
|
bool drain_internal = t->inflate_needs_drain;
|
|
t->inflate_needs_drain = false;
|
|
unsigned char out[16384];
|
|
|
|
while (!t->done && !t->response_stream->discarded &&
|
|
t->response_stream->queued < 1 &&
|
|
(t->zs.avail_in > 0 || drain_internal)) {
|
|
if (t->inflate_member_end) {
|
|
if (t->zs.avail_in == 0) break;
|
|
if (inflateReset(&t->zs) != Z_OK) {
|
|
sf_response_terminate(t);
|
|
break;
|
|
}
|
|
t->inflate_member_end = false;
|
|
}
|
|
|
|
t->zs.next_out = out;
|
|
t->zs.avail_out = sizeof out;
|
|
int result = inflate(&t->zs, Z_NO_FLUSH);
|
|
size_t produced = sizeof out - t->zs.avail_out;
|
|
drain_internal = false;
|
|
if (produced > 0) {
|
|
ScrBytes *decoded = scr_bytes_new(SCR_BYTES_U8, (double)produced);
|
|
memcpy(decoded->data, out, produced);
|
|
sf_stream_enqueue_bytes(t->response_stream, decoded);
|
|
scr_bytes_release(decoded);
|
|
}
|
|
if (result == Z_STREAM_END) {
|
|
/* RFC 1952 permits concatenated gzip members. */
|
|
t->inflate_member_end = true;
|
|
if (t->zs.avail_in == 0) break;
|
|
continue;
|
|
}
|
|
if (result != Z_OK && result != Z_BUF_ERROR) {
|
|
sf_response_terminate(t);
|
|
break;
|
|
}
|
|
if (t->done || t->response_stream->discarded) break;
|
|
if (t->response_stream->queued >= 1) {
|
|
if (result == Z_OK && t->zs.avail_out == 0) {
|
|
t->inflate_needs_drain = true;
|
|
}
|
|
break;
|
|
}
|
|
if (result == Z_BUF_ERROR && produced == 0) break;
|
|
if (t->zs.avail_in == 0) {
|
|
if (result == Z_OK && t->zs.avail_out == 0) {
|
|
drain_internal = true;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!t->done && !t->response_stream->discarded &&
|
|
t->zs.avail_in > 0) {
|
|
sf_inflate_append(t, t->zs.next_in, t->zs.avail_in);
|
|
}
|
|
t->zs.next_in = Z_NULL;
|
|
t->zs.avail_in = 0;
|
|
if (t->response_stream->discarded) {
|
|
scr_bytes_release(t->inflate_pending);
|
|
t->inflate_pending = NULL;
|
|
t->inflate_needs_drain = false;
|
|
}
|
|
scr_bytes_release(input);
|
|
t->inflate_draining = false;
|
|
}
|
|
|
|
static void sf_inflate_finish_if_ended(SfTransfer *t) {
|
|
if (!t || t->done || !t->response_ended || t->inflate_draining ||
|
|
!t->response_stream) {
|
|
return;
|
|
}
|
|
if (t->response_stream->discarded) {
|
|
sf_stream_close(t->response_stream);
|
|
sf_settle(t);
|
|
return;
|
|
}
|
|
if (t->inflate_pending || t->inflate_needs_drain) {
|
|
if (t->response_stream->queued >= 1) return;
|
|
sf_inflate_process(t);
|
|
if (t->done) return;
|
|
if (t->inflate_pending || t->inflate_needs_drain) {
|
|
/*
|
|
* Node fetch accepts a clean HTTP EOF even when the compressed
|
|
* representation ends before zlib reaches Z_STREAM_END, exposing the
|
|
* decoded prefix. Discard the suffix that still needs wire input.
|
|
*/
|
|
scr_bytes_release(t->inflate_pending);
|
|
t->inflate_pending = NULL;
|
|
t->inflate_needs_drain = false;
|
|
}
|
|
}
|
|
sf_stream_close(t->response_stream);
|
|
sf_settle(t);
|
|
}
|
|
|
|
static void sf_inflate_resume(SfTransfer *t) {
|
|
if (!t || t->done || t->inflate_draining) return;
|
|
sf_retain(t);
|
|
sf_inflate_process(t);
|
|
sf_inflate_finish_if_ended(t);
|
|
if (!t->done && t->response_paused && t->response_socket &&
|
|
t->response_stream && t->response_stream->queued < 1 &&
|
|
!t->response_stream->close_requested &&
|
|
!t->response_stream->closed && !t->response_stream->error) {
|
|
t->response_paused = false;
|
|
scr_net_sock_release(scr_net_sock_resume(t->response_socket));
|
|
}
|
|
sf_release(t);
|
|
}
|
|
|
|
static void sf_on_data(ScrClosure *cb, ScrBytes *chunk) {
|
|
SfTransfer *t = sf_from(cb);
|
|
if (!t) return;
|
|
if (t->done || !t->response_stream) {
|
|
sf_release(t);
|
|
return;
|
|
}
|
|
/*
|
|
* Fetch exposes no body for HEAD/204/205/304 responses. The shared HTTP
|
|
* parser still drains an invalid payload on 205, so discard those bytes
|
|
* here instead of queueing them into an inaccessible stream that would
|
|
* apply backpressure forever.
|
|
*/
|
|
if (t->response_null_body) {
|
|
sf_release(t);
|
|
return;
|
|
}
|
|
if (t->response_stream->discarded) {
|
|
sf_release(t);
|
|
return;
|
|
}
|
|
if (!t->inflating) {
|
|
sf_stream_enqueue_bytes(t->response_stream, chunk);
|
|
sf_release(t);
|
|
return;
|
|
}
|
|
|
|
sf_inflate_append(t, chunk->data, chunk->len);
|
|
sf_inflate_process(t);
|
|
sf_release(t);
|
|
}
|
|
|
|
static void sf_on_end(ScrClosure *cb) {
|
|
SfTransfer *t = sf_from(cb);
|
|
if (!t) return;
|
|
if (!t->done) {
|
|
if (t->inflating && t->response_stream &&
|
|
!t->response_stream->discarded) {
|
|
t->response_ended = true;
|
|
sf_inflate_finish_if_ended(t);
|
|
} else {
|
|
if (t->response_stream) sf_stream_close(t->response_stream);
|
|
sf_settle(t);
|
|
}
|
|
}
|
|
sf_release(t);
|
|
}
|
|
|
|
static void sf_on_res_error(ScrClosure *cb, ScrStr *message) {
|
|
(void)message;
|
|
SfTransfer *t = sf_from(cb);
|
|
if (!t) return;
|
|
if (!t->done && t->response_stream) {
|
|
sf_response_terminate(t);
|
|
} else {
|
|
sf_reject(t, "fetch failed");
|
|
}
|
|
sf_release(t);
|
|
}
|
|
|
|
static bool sf_response_has_null_body(const SfTransfer *t, int status) {
|
|
return sf_eq_ci(t->method, "head") || status == 101 ||
|
|
status == 204 || status == 205 || status == 304;
|
|
}
|
|
|
|
static void sf_on_response(ScrClosure *cb, ScrHttpReq *res) {
|
|
SfTransfer *t = sf_from(cb);
|
|
if (!t) {
|
|
scr_http_req_release(res);
|
|
return;
|
|
}
|
|
if (t->done) {
|
|
scr_http_req_release(res);
|
|
sf_release(t);
|
|
return;
|
|
}
|
|
int status = (int)scr_http_req_status(res);
|
|
if (status == 301 || status == 302 || status == 303 ||
|
|
status == 307 || status == 308) {
|
|
ScrStr *name = scr_str_new("location", 8);
|
|
ScrStr *location = scr_http_req_header(res, name);
|
|
scr_str_release(name);
|
|
if (location) {
|
|
if (t->redirect_mode == SF_REDIRECT_MANUAL) {
|
|
scr_str_release(location);
|
|
} else {
|
|
ScrHttpClientReq *old = t->client;
|
|
t->client = NULL;
|
|
if (old) {
|
|
scr_http_client_destroy(old);
|
|
scr_http_client_release(old);
|
|
}
|
|
if (t->redirect_mode == SF_REDIRECT_ERROR) {
|
|
sf_reject(t, "fetch failed");
|
|
scr_str_release(location);
|
|
scr_http_req_release(res);
|
|
sf_release(t);
|
|
return;
|
|
}
|
|
ScrStr *decoded_location = sf_location_to_utf8(location);
|
|
bool follow = sf_redirect(t, status, decoded_location);
|
|
scr_str_release(decoded_location);
|
|
scr_str_release(location);
|
|
if (follow) sf_start_hop(t);
|
|
scr_http_req_release(res);
|
|
sf_release(t);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
bool null_body = sf_response_has_null_body(t, status);
|
|
t->response_null_body = null_body;
|
|
if (!null_body) {
|
|
ScrStr *name = scr_str_new("content-encoding", 16);
|
|
ScrStr *encoding = scr_http_req_header(res, name);
|
|
scr_str_release(name);
|
|
if (encoding) {
|
|
if (sf_eq_ci(encoding, "gzip") || sf_eq_ci(encoding, "x-gzip") ||
|
|
sf_eq_ci(encoding, "deflate")) {
|
|
memset(&t->zs, 0, sizeof t->zs);
|
|
if (inflateInit2(&t->zs, 15 + 32) != Z_OK) sf_oom();
|
|
t->inflating = true;
|
|
}
|
|
scr_str_release(encoding);
|
|
}
|
|
}
|
|
SfStream *body = sf_stream_new_native();
|
|
body->response_owner = t;
|
|
t->response_stream = sf_stream_retain(body);
|
|
t->response_socket = scr_http_req_socket(res);
|
|
SfResponse *response = calloc(1, sizeof *response);
|
|
if (!response) sf_oom();
|
|
response->rc = 1;
|
|
response->body = sf_stream_retain(body);
|
|
response->headers =
|
|
sf_headers_new_response(scr_http_req_header_pairs(res));
|
|
response->url = sf_url_serialize(t->url);
|
|
ScrStr *raw_status_text = scr_http_req_status_message(res);
|
|
response->status_text =
|
|
raw_status_text ? sf_latin1_to_utf8(raw_status_text)
|
|
: scr_str_new("", 0);
|
|
scr_str_release(raw_status_text);
|
|
response->status = status;
|
|
response->redirected = t->redirected;
|
|
response->null_body = null_body;
|
|
ScrDyn *boxed = scr_dyn_new_handle(response, SCR_DYNH_FETCH_RESPONSE);
|
|
sf_response_release(response);
|
|
sf_stream_release(body);
|
|
|
|
scr_http_req_on_data(res, sf_closure(t, (void *)&sf_on_data),
|
|
&sf_on_data, false);
|
|
scr_http_req_on_end(res, sf_closure(t, (void *)&sf_on_end), false);
|
|
scr_http_req_on_error(res, sf_closure(t, (void *)&sf_on_res_error),
|
|
&sf_on_res_error, false);
|
|
t->response_sent = true;
|
|
ScrPromise *promise = t->promise;
|
|
t->promise = NULL;
|
|
scr_promise_fulfill_ref(promise, boxed, &scr_dyn_retain_v,
|
|
&scr_dyn_release_v, NULL);
|
|
scr_promise_release(promise);
|
|
scr_http_req_release(res);
|
|
sf_release(t);
|
|
}
|
|
|
|
static void sf_on_client_error(ScrClosure *cb, ScrStr *message) {
|
|
(void)message;
|
|
SfTransfer *t = sf_from(cb);
|
|
if (!t) return;
|
|
if (!t->done) {
|
|
if (t->response_sent && t->response_stream) {
|
|
sf_response_terminate(t);
|
|
} else {
|
|
sf_reject(t, "fetch failed");
|
|
}
|
|
}
|
|
sf_release(t);
|
|
}
|
|
|
|
static void sf_on_upgrade(
|
|
ScrClosure *cb, ScrHttpReq *res, ScrNetSocket *socket,
|
|
ScrBytes *head) {
|
|
SfTransfer *t = sf_from(cb);
|
|
scr_http_req_release(res);
|
|
scr_bytes_release(head);
|
|
scr_net_sock_destroy(socket);
|
|
scr_net_sock_release(socket);
|
|
if (!t) return;
|
|
if (!t->done) sf_reject(t, "fetch failed");
|
|
sf_release(t);
|
|
}
|
|
|
|
static bool sf_body_is_stream(const ScrDyn *body) {
|
|
return body &&
|
|
body->kind == SCR_DYN_HANDLE &&
|
|
body->v.handle.tag == SCR_DYNH_WEB_STREAM;
|
|
}
|
|
|
|
static bool sf_redirect(SfTransfer *t, int status,
|
|
const ScrStr *location) {
|
|
if (t->hops >= 20) {
|
|
sf_reject(t, "fetch failed");
|
|
return false;
|
|
}
|
|
ScrUrl *next = sf_resolve_url(t->url, location);
|
|
if (!next ||
|
|
!(sf_eq_ci(next->scheme, "http") ||
|
|
sf_eq_ci(next->scheme, "https")) ||
|
|
next->userinfo->len > 0) {
|
|
scr_url_release(next);
|
|
sf_reject(t, "fetch failed");
|
|
return false;
|
|
}
|
|
|
|
bool is_get = sf_eq_ci(t->method, "get");
|
|
bool is_head = sf_eq_ci(t->method, "head");
|
|
bool rewrite =
|
|
(status == 303 && !is_get && !is_head) ||
|
|
((status == 301 || status == 302) &&
|
|
sf_eq_ci(t->method, "post"));
|
|
if (sf_body_is_stream(t->body) && status != 303) {
|
|
scr_url_release(next);
|
|
sf_reject(t, "fetch failed");
|
|
return false;
|
|
}
|
|
if (rewrite) {
|
|
scr_str_release(t->method);
|
|
t->method = scr_str_new("GET", 3);
|
|
scr_dyn_release(t->body);
|
|
t->body = NULL;
|
|
sf_strip_header(&t->headers, "content-type");
|
|
sf_strip_header(&t->headers, "content-length");
|
|
sf_strip_header(&t->headers, "content-encoding");
|
|
sf_strip_header(&t->headers, "content-language");
|
|
sf_strip_header(&t->headers, "content-location");
|
|
}
|
|
|
|
bool same_origin =
|
|
t->url->host->len == next->host->len &&
|
|
memcmp(t->url->host->data, next->host->data, next->host->len) == 0 &&
|
|
sf_port(t->url, 0) == sf_port(next, 0) &&
|
|
t->url->scheme->len == next->scheme->len &&
|
|
memcmp(t->url->scheme->data, next->scheme->data,
|
|
next->scheme->len) == 0;
|
|
if (!same_origin) {
|
|
sf_strip_header(&t->headers, "authorization");
|
|
sf_strip_header(&t->headers, "proxy-authorization");
|
|
sf_strip_header(&t->headers, "cookie");
|
|
}
|
|
|
|
scr_url_release(t->url);
|
|
t->url = next;
|
|
t->hops++;
|
|
t->redirected = true;
|
|
return true;
|
|
}
|
|
|
|
static bool sf_start_hop(SfTransfer *t) {
|
|
ScrUrl *url = t->url;
|
|
bool https = sf_eq_ci(url->scheme, "https");
|
|
int default_port = https ? 443 : 80;
|
|
int port = sf_port(url, default_port);
|
|
if (sf_bad_port(port)) {
|
|
sf_reject(t, "fetch failed");
|
|
return false;
|
|
}
|
|
bool invalid_proxy = false;
|
|
ScrUrl *proxy = sf_proxy_for(url, https, port, &invalid_proxy);
|
|
bool proxy_http = proxy && sf_eq_ci(proxy->scheme, "http");
|
|
bool proxy_https = proxy && sf_eq_ci(proxy->scheme, "https");
|
|
if (invalid_proxy || (proxy && !proxy_http && !proxy_https) ||
|
|
(https && proxy)) {
|
|
scr_url_release(proxy);
|
|
sf_reject(t, "fetch failed");
|
|
return false;
|
|
}
|
|
size_t user_len = (size_t)scr_arr_len(t->headers);
|
|
ScrArr *headers = scr_arr_new(SCR_ELEM_STR, user_len + 16);
|
|
|
|
char authority[384];
|
|
int authority_len =
|
|
port == default_port
|
|
? snprintf(authority, sizeof authority, "%.*s",
|
|
(int)url->host->len, url->host->data)
|
|
: snprintf(authority, sizeof authority, "%.*s:%d",
|
|
(int)url->host->len, url->host->data, port);
|
|
if (authority_len < 0 ||
|
|
(size_t)authority_len >= sizeof authority) {
|
|
scr_arr_release(headers);
|
|
scr_url_release(proxy);
|
|
sf_reject(t, "fetch failed");
|
|
return false;
|
|
}
|
|
sf_push_header_text(headers, "host", authority,
|
|
(size_t)authority_len);
|
|
if (!sf_pairs_have(t->headers, "connection")) {
|
|
sf_push_header_text(headers, "connection", "keep-alive", 10);
|
|
}
|
|
for (size_t i = 0; i + 1 < user_len; i += 2) {
|
|
ScrStr *name = scr_arr_get_ref(t->headers, (double)i);
|
|
ScrStr *value = scr_arr_get_ref(t->headers, (double)(i + 1));
|
|
if (sf_eq_ci(name, "host")) {
|
|
scr_str_release(name);
|
|
scr_str_release(value);
|
|
continue;
|
|
}
|
|
scr_arr_push_ref(headers, name);
|
|
scr_arr_push_ref(headers, value);
|
|
}
|
|
if (proxy && proxy->userinfo->len > 0 &&
|
|
!sf_pairs_have(t->headers, "proxy-authorization")) {
|
|
ScrStr *authorization = sf_proxy_authorization(proxy->userinfo);
|
|
sf_push_header_text(headers, "proxy-authorization",
|
|
authorization->data, authorization->len);
|
|
scr_str_release(authorization);
|
|
}
|
|
if (!sf_pairs_have(t->headers, "accept")) {
|
|
sf_push_header_text(headers, "accept", "*/*", 3);
|
|
}
|
|
if (!sf_pairs_have(t->headers, "accept-language")) {
|
|
sf_push_header_text(headers, "accept-language", "*", 1);
|
|
}
|
|
if (!sf_pairs_have(t->headers, "sec-fetch-mode")) {
|
|
sf_push_header_text(headers, "sec-fetch-mode", "cors", 4);
|
|
}
|
|
if (!sf_pairs_have(t->headers, "user-agent")) {
|
|
sf_push_header_text(headers, "user-agent", "node", 4);
|
|
}
|
|
if (!sf_pairs_have(t->headers, "accept-encoding")) {
|
|
sf_push_header_text(headers, "accept-encoding", "gzip, deflate", 13);
|
|
}
|
|
|
|
ScrStr *bare =
|
|
sf_bare_host(proxy ? proxy->host : url->host);
|
|
ScrStr *dial = scr_net_blocking_lookup(bare);
|
|
ScrStr *path = proxy ? sf_url_serialize(url) : sf_path(url);
|
|
int dial_port =
|
|
proxy ? sf_port(proxy, proxy_https ? 443 : 80) : port;
|
|
ScrStr *sni =
|
|
https ? sf_bare_host(url->host)
|
|
: proxy_https ? sf_bare_host(proxy->host) : NULL;
|
|
void *tls_ctx =
|
|
sni ? scr_tls_fetch_client_ctx(sni, true) : NULL;
|
|
ScrHttpClientReq *client = scr_http_request_ex(
|
|
dial, dial_port, path, t->method, 0, headers, false, NULL, NULL,
|
|
default_port, sni ? &scr_tls_fetch_client_wrap : NULL, tls_ctx);
|
|
scr_arr_release(headers);
|
|
scr_str_release(path);
|
|
scr_str_release(dial);
|
|
scr_str_release(bare);
|
|
scr_str_release(sni);
|
|
scr_url_release(proxy);
|
|
if (!client) {
|
|
sf_reject_now(t->promise, "fetch failed");
|
|
sf_settle(t);
|
|
return false;
|
|
}
|
|
t->client = client;
|
|
|
|
scr_http_client_on_response(
|
|
client, sf_closure(t, (void *)&sf_on_response),
|
|
&sf_on_response, true);
|
|
scr_http_client_on_upgrade(
|
|
client, sf_closure(t, (void *)&sf_on_upgrade),
|
|
&sf_on_upgrade, true);
|
|
scr_http_client_on_error(
|
|
client, sf_closure(t, (void *)&sf_on_client_error),
|
|
&sf_on_client_error, false);
|
|
|
|
ScrDyn *body = t->body;
|
|
if (body && body->kind == SCR_DYN_STR) {
|
|
scr_http_client_end_str(client, body->v.str);
|
|
} else if (body && body->kind == SCR_DYN_BYTES) {
|
|
scr_http_client_end_bytes(client, body->v.bytes);
|
|
} else if (sf_body_is_stream(body)) {
|
|
SfStream *stream = NULL;
|
|
ScrDyn *seeded = NULL;
|
|
if (body->kind == SCR_DYN_ARR) {
|
|
seeded = scr_fetch_stream_from(body);
|
|
if (!seeded) {
|
|
if (t->client) scr_http_client_destroy(t->client);
|
|
sf_reject_now(t->promise, "fetch failed");
|
|
sf_settle(t);
|
|
return false;
|
|
}
|
|
stream = sf_stream_of(seeded);
|
|
} else {
|
|
stream = sf_stream_of(body);
|
|
}
|
|
if (!stream || stream->reader || stream->internal_lock ||
|
|
stream->disturbed) {
|
|
scr_dyn_release(seeded);
|
|
if (t->client) scr_http_client_destroy(t->client);
|
|
sf_reject(t,
|
|
"Response body object should not be disturbed or locked");
|
|
return false;
|
|
}
|
|
stream->disturbed = true;
|
|
stream->internal_lock = true;
|
|
stream->request_owner = t;
|
|
t->request_stream = sf_stream_retain(stream);
|
|
sf_stream_request_flush(stream);
|
|
if (stream->started && !stream->close_requested && !stream->closed &&
|
|
!stream->error && !t->done && t->client) {
|
|
sf_stream_pull(stream);
|
|
}
|
|
scr_dyn_release(seeded);
|
|
} else {
|
|
scr_http_client_end(client);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init) {
|
|
ScrPromise *promise = scr_promise_new();
|
|
if (init && init->kind != SCR_DYN_OBJ &&
|
|
init->kind != SCR_DYN_UNDEF && init->kind != SCR_DYN_NULL) {
|
|
return sf_reject_now(promise, "fetch failed");
|
|
}
|
|
if (init && init->kind == SCR_DYN_OBJ) {
|
|
for (size_t i = 0; i < init->v.obj.len; i++) {
|
|
const ScrDynEntry *entry = &init->v.obj.entries[i];
|
|
bool supported = sf_name(entry->key, entry->key_len, "method") ||
|
|
sf_name(entry->key, entry->key_len, "headers") ||
|
|
sf_name(entry->key, entry->key_len, "body") ||
|
|
sf_name(entry->key, entry->key_len, "duplex") ||
|
|
sf_name(entry->key, entry->key_len, "redirect") ||
|
|
sf_name(entry->key, entry->key_len, "signal");
|
|
bool recognized_unsupported =
|
|
sf_name(entry->key, entry->key_len, "cache") ||
|
|
sf_name(entry->key, entry->key_len, "credentials") ||
|
|
sf_name(entry->key, entry->key_len, "dispatcher") ||
|
|
sf_name(entry->key, entry->key_len, "integrity") ||
|
|
sf_name(entry->key, entry->key_len, "keepalive") ||
|
|
sf_name(entry->key, entry->key_len, "mode") ||
|
|
sf_name(entry->key, entry->key_len, "priority") ||
|
|
sf_name(entry->key, entry->key_len, "referrer") ||
|
|
sf_name(entry->key, entry->key_len, "referrerPolicy") ||
|
|
sf_name(entry->key, entry->key_len, "window");
|
|
if (!supported && recognized_unsupported && entry->value &&
|
|
entry->value->kind != SCR_DYN_UNDEF) {
|
|
return sf_reject_now(promise, "fetch failed");
|
|
}
|
|
}
|
|
}
|
|
/* WebIDL converts dictionary members by name, not object insertion
|
|
* order: body, duplex, headers, method, redirect, signal. Every
|
|
* observable conversion completes before Request validates the URL,
|
|
* method/body combination, or an already-aborted signal. */
|
|
const ScrDyn *body =
|
|
init && init->kind == SCR_DYN_OBJ
|
|
? scr_dyn_obj_get(init, "body", 4)
|
|
: NULL;
|
|
bool body_present =
|
|
body && body->kind != SCR_DYN_UNDEF && body->kind != SCR_DYN_NULL;
|
|
bool body_stream =
|
|
body_present &&
|
|
body->kind == SCR_DYN_HANDLE &&
|
|
body->v.handle.tag == SCR_DYNH_WEB_STREAM;
|
|
/* BodyInit's USVString fallback applies to every ordinary value that
|
|
* is not already bytes or a ReadableStream. This includes scalars,
|
|
* arrays, and plain objects (with the ordinary JS object coercion
|
|
* protocol), while null and undefined continue to mean no body. */
|
|
ScrDyn *coerced_body = NULL;
|
|
if (body_present && body->kind != SCR_DYN_STR &&
|
|
body->kind != SCR_DYN_BYTES && !body_stream) {
|
|
ScrStr *text = scr_dyn_string_coerce_js(body);
|
|
if (!text) {
|
|
return sf_reject_now(promise, "fetch failed");
|
|
}
|
|
coerced_body = scr_dyn_new_str(text);
|
|
scr_str_release(text);
|
|
body = coerced_body;
|
|
}
|
|
|
|
const ScrDyn *duplex =
|
|
init && init->kind == SCR_DYN_OBJ
|
|
? scr_dyn_obj_get(init, "duplex", 6)
|
|
: NULL;
|
|
bool duplex_present =
|
|
duplex && duplex->kind != SCR_DYN_UNDEF;
|
|
bool duplex_half = false;
|
|
if (duplex_present) {
|
|
ScrStr *duplex_mode = scr_dyn_string_coerce_js(duplex);
|
|
if (!duplex_mode) {
|
|
scr_dyn_release(coerced_body);
|
|
return sf_reject_now(promise, "fetch failed");
|
|
}
|
|
duplex_half =
|
|
duplex_mode->len == 4 &&
|
|
memcmp(duplex_mode->data, "half", 4) == 0;
|
|
scr_str_release(duplex_mode);
|
|
}
|
|
if (duplex_present && !duplex_half) {
|
|
scr_dyn_release(coerced_body);
|
|
return sf_reject_now(
|
|
promise, "RequestInit.duplex must be 'half'");
|
|
}
|
|
|
|
ScrArr *headers = scr_arr_new(SCR_ELEM_STR, 16);
|
|
const ScrDyn *init_headers =
|
|
init && init->kind == SCR_DYN_OBJ
|
|
? scr_dyn_obj_get(init, "headers", 7)
|
|
: NULL;
|
|
if (!sf_add_headers(headers, init_headers)) {
|
|
scr_dyn_release(coerced_body);
|
|
scr_arr_release(headers);
|
|
return sf_reject_now(promise, "fetch failed");
|
|
}
|
|
|
|
ScrStr *method = sf_method_value(init);
|
|
if (!method) {
|
|
scr_dyn_release(coerced_body);
|
|
scr_arr_release(headers);
|
|
return sf_reject_now(promise, "fetch failed");
|
|
}
|
|
|
|
int redirect_mode;
|
|
if (!sf_redirect_mode(init, &redirect_mode)) {
|
|
scr_dyn_release(coerced_body);
|
|
scr_arr_release(headers);
|
|
scr_str_release(method);
|
|
return sf_reject_now(promise, "fetch failed");
|
|
}
|
|
|
|
const ScrDyn *signal_dyn =
|
|
init && init->kind == SCR_DYN_OBJ
|
|
? scr_dyn_obj_get(init, "signal", 6)
|
|
: NULL;
|
|
SfSignal *signal = NULL;
|
|
if (signal_dyn && signal_dyn->kind != SCR_DYN_UNDEF &&
|
|
signal_dyn->kind != SCR_DYN_NULL) {
|
|
signal = sf_signal_of(signal_dyn,
|
|
"Request init.signal must be an AbortSignal");
|
|
if (!signal) {
|
|
scr_dyn_release(coerced_body);
|
|
scr_arr_release(headers);
|
|
scr_str_release(method);
|
|
return sf_reject_now(promise, "fetch failed");
|
|
}
|
|
}
|
|
|
|
ScrUrl *u = scr_url_new(url);
|
|
if (!u) {
|
|
scr_dyn_release(coerced_body);
|
|
scr_arr_release(headers);
|
|
scr_str_release(method);
|
|
scr_promise_reject_pending(promise);
|
|
return promise;
|
|
}
|
|
if (u->userinfo->len > 0) {
|
|
static const char prefix[] =
|
|
"Request cannot be constructed from a URL that includes credentials: ";
|
|
size_t message_len = sizeof prefix - 1 + url->len;
|
|
char *message = malloc(message_len + 1);
|
|
if (!message) sf_oom();
|
|
memcpy(message, prefix, sizeof prefix - 1);
|
|
memcpy(message + sizeof prefix - 1, url->data, url->len);
|
|
message[message_len] = '\0';
|
|
sf_type_error(message);
|
|
free(message);
|
|
scr_dyn_release(coerced_body);
|
|
scr_arr_release(headers);
|
|
scr_str_release(method);
|
|
scr_url_release(u);
|
|
return sf_reject_now(promise, "fetch failed");
|
|
}
|
|
bool https = sf_eq_ci(u->scheme, "https");
|
|
if (!https && !sf_eq_ci(u->scheme, "http")) {
|
|
scr_dyn_release(coerced_body);
|
|
scr_arr_release(headers);
|
|
scr_str_release(method);
|
|
scr_url_release(u);
|
|
return sf_reject_now(promise, "fetch failed");
|
|
}
|
|
if (!sf_method_validate(&method)) {
|
|
scr_dyn_release(coerced_body);
|
|
scr_arr_release(headers);
|
|
scr_str_release(method);
|
|
scr_url_release(u);
|
|
return sf_reject_now(promise, "fetch failed");
|
|
}
|
|
if (body_stream && !duplex_half) {
|
|
scr_dyn_release(coerced_body);
|
|
scr_arr_release(headers);
|
|
scr_str_release(method);
|
|
scr_url_release(u);
|
|
return sf_reject_now(
|
|
promise,
|
|
"RequestInit: duplex option is required when sending a body.");
|
|
}
|
|
if (body_present &&
|
|
((method->len == 3 && memcmp(method->data, "GET", 3) == 0) ||
|
|
(method->len == 4 && memcmp(method->data, "HEAD", 4) == 0))) {
|
|
scr_dyn_release(coerced_body);
|
|
scr_arr_release(headers);
|
|
scr_str_release(method);
|
|
scr_url_release(u);
|
|
return sf_reject_now(
|
|
promise, "Request with GET/HEAD method cannot have body.");
|
|
}
|
|
|
|
/* Fetch Metadata is controlled by the fetch implementation. Undici
|
|
* replaces a caller-provided sec-fetch-mode with "cors". */
|
|
sf_strip_header(&headers, "sec-fetch-mode");
|
|
if (body && body->kind == SCR_DYN_STR &&
|
|
!sf_pairs_have(headers, "content-type")) {
|
|
sf_push_header_text(headers, "content-type",
|
|
"text/plain;charset=UTF-8", 24);
|
|
}
|
|
bool has_content_length = false;
|
|
size_t content_length = 0;
|
|
if (!sf_content_length(headers, &has_content_length, &content_length)) {
|
|
scr_dyn_release(coerced_body);
|
|
scr_arr_release(headers);
|
|
scr_str_release(method);
|
|
scr_url_release(u);
|
|
return sf_reject_now(promise, "fetch failed");
|
|
}
|
|
if (has_content_length && !body_stream) {
|
|
size_t body_length =
|
|
!body_present
|
|
? 0
|
|
: body->kind == SCR_DYN_STR
|
|
? body->v.str->len
|
|
: body->v.bytes->len;
|
|
if (body_length != content_length) {
|
|
scr_dyn_release(coerced_body);
|
|
scr_arr_release(headers);
|
|
scr_str_release(method);
|
|
scr_url_release(u);
|
|
return sf_reject_now(promise, "fetch failed");
|
|
}
|
|
}
|
|
/*
|
|
* Request construction validates URL/method/body/headers before fetch
|
|
* observes an already-aborted signal. Undici does the same: abort wins
|
|
* over starting I/O, but never masks a RequestInit validation error.
|
|
*/
|
|
if (signal && signal->aborted) {
|
|
scr_dyn_release(coerced_body);
|
|
scr_arr_release(headers);
|
|
scr_str_release(method);
|
|
scr_url_release(u);
|
|
sf_reject_promise_reason(promise, signal->reason);
|
|
return promise;
|
|
}
|
|
|
|
SfTransfer *t = calloc(1, sizeof *t);
|
|
if (!t) sf_oom();
|
|
t->rc = 1; /* registry */
|
|
t->promise = scr_promise_retain(promise);
|
|
t->url = u;
|
|
t->method = method;
|
|
t->headers = headers;
|
|
t->body = body_present ? scr_dyn_retain((ScrDyn *)body) : NULL;
|
|
scr_dyn_release(coerced_body);
|
|
t->redirect_mode = redirect_mode;
|
|
t->request_has_content_length = has_content_length;
|
|
t->request_content_length = content_length;
|
|
if (signal) {
|
|
t->signal = sf_signal_retain(signal);
|
|
t->signal_watch =
|
|
sf_signal_watch(signal, t, &sf_transfer_abort_watch);
|
|
}
|
|
t->next = sf_live;
|
|
sf_live = t;
|
|
sf_start_hop(t);
|
|
return promise;
|
|
}
|
|
|
|
/* ── handle dispatch tables + teardown ───────────────────────────── */
|
|
|
|
static bool sf_no_set(void *ptr, const char *key, size_t len,
|
|
const ScrDyn *value) {
|
|
(void)ptr;
|
|
(void)key;
|
|
(void)len;
|
|
(void)value;
|
|
return false;
|
|
}
|
|
|
|
static const ScrDynHandleOps sf_signal_ops = {
|
|
"AbortSignal", &sf_signal_retain_v, &sf_signal_release_v,
|
|
&sf_signal_invoke, &sf_signal_get, &sf_signal_set, NULL};
|
|
static const ScrDynHandleOps sf_stream_ops = {
|
|
"ReadableStream", &sf_stream_retain_v, &sf_stream_release_v,
|
|
&sf_stream_invoke, &sf_stream_get, &sf_no_set, NULL};
|
|
static const ScrDynHandleOps sf_reader_ops = {
|
|
"ReadableStreamDefaultReader", &sf_reader_handle_retain_v,
|
|
&sf_reader_handle_release_v, &sf_reader_invoke, &sf_reader_get,
|
|
&sf_no_set, NULL};
|
|
static const ScrDynHandleOps sf_controller_ops = {
|
|
"ReadableStreamDefaultController", &sf_stream_retain_v,
|
|
&sf_stream_release_v, &sf_controller_invoke, &sf_controller_get,
|
|
&sf_no_set, NULL};
|
|
static const ScrDynHandleOps sf_response_ops = {
|
|
"Response", &sf_response_retain_v, &sf_response_release_v,
|
|
&sf_response_invoke, &sf_response_get, &sf_no_set, NULL};
|
|
static const ScrDynHandleOps sf_headers_ops = {
|
|
"Headers", &sf_headers_retain_v, &sf_headers_release_v,
|
|
&sf_headers_invoke, &sf_headers_get, &sf_no_set, NULL};
|
|
static const ScrDynHandleOps sf_event_ops = {
|
|
"Event", &sf_event_retain_v, &sf_event_release_v,
|
|
&sf_event_invoke, &sf_event_get, &sf_event_set, NULL};
|
|
|
|
static void sf_teardown(void) {
|
|
while (sf_live) {
|
|
SfTransfer *t = sf_live;
|
|
if (t->client) scr_http_client_destroy(t->client);
|
|
if (t->response_stream) sf_stream_close(t->response_stream);
|
|
sf_settle(t);
|
|
}
|
|
/*
|
|
* Dyn function boxes deliberately do not participate in trial deletion.
|
|
* At process/session teardown no Web object remains observable, so sever
|
|
* every native callback edge before the collector and RC audit run.
|
|
* Releasing a callback may destroy and unlink another tracked owner.
|
|
*/
|
|
while (sf_callback_streams) {
|
|
sf_stream_drop_source_callbacks(sf_callback_streams, true);
|
|
}
|
|
while (sf_callback_signals) {
|
|
sf_signal_drop_callbacks(sf_callback_signals);
|
|
}
|
|
sf_proxy_snapshot_free();
|
|
}
|
|
|
|
void scr_fetch_install(void) {
|
|
static bool installed;
|
|
if (installed) return;
|
|
installed = true;
|
|
scr_tls_ca_install();
|
|
sf_proxy_snapshot();
|
|
scr_net_install();
|
|
scr_dyn_handle_install(SCR_DYNH_ABORT_SIGNAL, &sf_signal_ops);
|
|
scr_dyn_handle_install(SCR_DYNH_WEB_STREAM, &sf_stream_ops);
|
|
scr_dyn_handle_install(SCR_DYNH_WEB_READER, &sf_reader_ops);
|
|
scr_dyn_handle_install(SCR_DYNH_WEB_CONTROLLER, &sf_controller_ops);
|
|
scr_dyn_handle_install(SCR_DYNH_FETCH_RESPONSE, &sf_response_ops);
|
|
scr_dyn_handle_install(SCR_DYNH_FETCH_HEADERS, &sf_headers_ops);
|
|
scr_dyn_handle_install(SCR_DYNH_EVENT, &sf_event_ops);
|
|
scr_atexit(sf_teardown);
|
|
}
|
|
|
|
#else
|
|
|
|
#include "scr_runtime.h"
|
|
#include "scr_url_internal.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <zlib.h>
|
|
|
|
#include "quickjs.h"
|
|
|
|
static void fx_oom(void) {
|
|
fputs("scriptc: out of memory\n", stderr);
|
|
abort();
|
|
}
|
|
|
|
/* ── small string helpers (ScrStr names are not NUL-guaranteed at the
|
|
* lengths we compare, so everything is len-aware, ASCII-case-folded) ── */
|
|
|
|
static bool fx_eq_ci(const char *a, size_t alen, const char *b) {
|
|
size_t blen = strlen(b);
|
|
if (alen != blen) return false;
|
|
for (size_t i = 0; i < alen; i++) {
|
|
char c = a[i];
|
|
if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
|
|
if (c != b[i]) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* HTTP field values and reason phrases are ByteStrings: each wire octet
|
|
* becomes the same U+00XX code point in JavaScript. QuickJS string creation
|
|
* consumes UTF-8, so expand Latin-1 before crossing into the engine. */
|
|
static ScrStr *fx_latin1_to_utf8(const ScrStr *raw) {
|
|
size_t extra = 0;
|
|
for (size_t i = 0; i < raw->len; i++) {
|
|
if ((unsigned char)raw->data[i] >= 0x80) extra++;
|
|
}
|
|
if (extra == 0) return scr_str_retain((ScrStr *)raw);
|
|
char *utf8 = malloc(raw->len + extra);
|
|
if (!utf8) fx_oom();
|
|
size_t at = 0;
|
|
for (size_t i = 0; i < raw->len; i++) {
|
|
unsigned char byte = (unsigned char)raw->data[i];
|
|
if (byte < 0x80) {
|
|
utf8[at++] = (char)byte;
|
|
} else {
|
|
utf8[at++] = (char)(0xc0 | (byte >> 6));
|
|
utf8[at++] = (char)(0x80 | (byte & 0x3f));
|
|
}
|
|
}
|
|
ScrStr *out = scr_str_new(utf8, at);
|
|
free(utf8);
|
|
return out;
|
|
}
|
|
|
|
static bool fx_str_is(const ScrStr *s, const char *lit) { return fx_eq_ci(s->data, s->len, lit); }
|
|
|
|
static ScrStr *fx_location_to_utf8(const ScrStr *raw) {
|
|
ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)raw->len);
|
|
if (raw->len > 0) memcpy(bytes->data, raw->data, raw->len);
|
|
ScrStr *encoding = scr_str_new("utf8", 4);
|
|
ScrStr *out = scr_bytes_to_str(bytes, encoding);
|
|
scr_str_release(encoding);
|
|
scr_bytes_release(bytes);
|
|
return out;
|
|
}
|
|
|
|
/* Does the flat [name, value, ...] pairs array carry `name`? */
|
|
static bool fx_pairs_have(ScrArr *pairs, const char *name) {
|
|
size_t n = (size_t)scr_arr_len(pairs);
|
|
bool found = false;
|
|
for (size_t i = 0; i + 1 < n && !found; i += 2) {
|
|
ScrStr *nm = (ScrStr *)scr_arr_get_ref(pairs, (double)i);
|
|
found = fx_eq_ci(nm->data, nm->len, name);
|
|
scr_str_release(nm);
|
|
}
|
|
return found;
|
|
}
|
|
|
|
static void fx_pairs_push(ScrArr *pairs, const char *name, const char *value, size_t vlen) {
|
|
scr_arr_push_ref(pairs, scr_str_new(name, strlen(name)));
|
|
scr_arr_push_ref(pairs, scr_str_new(value, vlen));
|
|
}
|
|
|
|
static bool fx_content_length(ScrArr *headers, bool *present_out,
|
|
size_t *length_out) {
|
|
bool present = false;
|
|
size_t length = 0;
|
|
size_t n = (size_t)scr_arr_len(headers);
|
|
for (size_t i = 0; i + 1 < n; i += 2) {
|
|
ScrStr *name = scr_arr_get_ref(headers, (double)i);
|
|
ScrStr *value = scr_arr_get_ref(headers, (double)(i + 1));
|
|
if (!fx_eq_ci(name->data, name->len, "content-length")) {
|
|
scr_str_release(name);
|
|
scr_str_release(value);
|
|
continue;
|
|
}
|
|
if (present) {
|
|
scr_str_release(name);
|
|
scr_str_release(value);
|
|
return false;
|
|
}
|
|
size_t start = 0;
|
|
size_t end = value->len;
|
|
while (start < end &&
|
|
(value->data[start] == ' ' || value->data[start] == '\t')) {
|
|
start++;
|
|
}
|
|
while (end > start &&
|
|
(value->data[end - 1] == ' ' ||
|
|
value->data[end - 1] == '\t')) {
|
|
end--;
|
|
}
|
|
bool valid = start < end;
|
|
size_t parsed = 0;
|
|
for (size_t j = start; j < end && valid; j++) {
|
|
unsigned char c = (unsigned char)value->data[j];
|
|
if (c < '0' || c > '9') {
|
|
valid = false;
|
|
break;
|
|
}
|
|
size_t digit = (size_t)(c - '0');
|
|
if (parsed > (SIZE_MAX - digit) / 10) {
|
|
valid = false;
|
|
break;
|
|
}
|
|
parsed = parsed * 10 + digit;
|
|
}
|
|
scr_str_release(name);
|
|
scr_str_release(value);
|
|
if (!valid) return false;
|
|
present = true;
|
|
length = parsed;
|
|
}
|
|
*present_out = present;
|
|
*length_out = length;
|
|
return true;
|
|
}
|
|
|
|
/* ── transfers ───────────────────────────────────────────────────────── */
|
|
|
|
enum {
|
|
FX_REDIRECT_FOLLOW,
|
|
FX_REDIRECT_ERROR,
|
|
FX_REDIRECT_MANUAL,
|
|
};
|
|
|
|
typedef struct FxTransfer {
|
|
size_t rc; /* the live registry's +1 plus one per minted listener box */
|
|
int id;
|
|
JSContext *ctx;
|
|
JSValue cbs; /* { onResponse, onData, onEnd, onError } — owned while live */
|
|
bool cbs_live;
|
|
/* the request, as it evolves across redirect hops */
|
|
ScrStr *method; /* owned */
|
|
ScrArr *headers; /* flat [name, value, ...] user pairs — owned */
|
|
ScrBytes *body; /* owned, NULL = none */
|
|
ScrUrl *url; /* the CURRENT hop's URL — owned */
|
|
int redirect_mode;
|
|
int hops;
|
|
bool redirected;
|
|
bool request_streaming;
|
|
bool use_env_proxy;
|
|
bool request_ended;
|
|
bool request_has_content_length;
|
|
size_t request_content_length;
|
|
size_t request_body_sent;
|
|
/* the live hop */
|
|
ScrHttpClientReq *client; /* +1, NULL between/after hops */
|
|
bool responded; /* onResponse fired (the final response) */
|
|
bool cancelled; /* island aborted/cancelled: die silently */
|
|
bool done; /* settled: no callback ever fires again */
|
|
/* response decompression */
|
|
bool inflating;
|
|
bool inflate_member_end;
|
|
z_stream zs;
|
|
struct FxTransfer *next;
|
|
} FxTransfer;
|
|
|
|
static FxTransfer *fx_live = NULL; /* registry: +1 each */
|
|
static size_t fx_nlive = 0;
|
|
static int fx_next_id = 1;
|
|
|
|
static FxTransfer *fx_retain(FxTransfer *t) {
|
|
t->rc++;
|
|
return t;
|
|
}
|
|
|
|
static void fx_release(FxTransfer *t) {
|
|
if (--t->rc > 0) return;
|
|
scr_str_release(t->method);
|
|
scr_arr_release(t->headers);
|
|
if (t->body) scr_bytes_release(t->body);
|
|
if (t->url) scr_url_release(t->url);
|
|
if (t->client) scr_http_client_release(t->client);
|
|
if (t->inflating) inflateEnd(&t->zs);
|
|
if (t->cbs_live) JS_FreeValue(t->ctx, t->cbs);
|
|
free(t);
|
|
}
|
|
|
|
static void *fx_retain_v(void *p) { return fx_retain((FxTransfer *)p); }
|
|
static void fx_release_v(void *p) { fx_release((FxTransfer *)p); }
|
|
|
|
/* Settle: the transfer is over (delivered, failed, or aborted) — drop the
|
|
* hop client, free the engine callbacks, and leave the registry. Idempotent. */
|
|
static void fx_settle(FxTransfer *t) {
|
|
if (t->done) return;
|
|
t->done = true;
|
|
if (t->client) {
|
|
scr_http_client_release(t->client);
|
|
t->client = NULL;
|
|
}
|
|
if (t->cbs_live) {
|
|
JS_FreeValue(t->ctx, t->cbs);
|
|
t->cbs = JS_UNDEFINED;
|
|
t->cbs_live = false;
|
|
}
|
|
for (FxTransfer **link = &fx_live; *link; link = &(*link)->next) {
|
|
if (*link == t) {
|
|
*link = t->next;
|
|
fx_nlive--;
|
|
fx_release(t); /* the registry's +1 */
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Calls one callback off the cbs object, swallowing (but reporting) any
|
|
* engine exception — the callbacks are our own glue and must not throw.
|
|
* Entered from the net dispatch station (the main stack): re-anchor the
|
|
* engine's stack-overflow check first, the every-island-entry rule. */
|
|
static void fx_call(FxTransfer *t, const char *name, int argc, JSValueConst *argv) {
|
|
if (!t->cbs_live) return;
|
|
scr_island_host_enter();
|
|
JSContext *ctx = t->ctx;
|
|
JSValue fn = JS_GetPropertyStr(ctx, t->cbs, name);
|
|
JSValue r = JS_Call(ctx, fn, JS_UNDEFINED, argc, argv);
|
|
JS_FreeValue(ctx, fn);
|
|
if (JS_IsException(r)) {
|
|
JSValue e = JS_GetException(ctx);
|
|
const char *msg = JS_ToCString(ctx, e);
|
|
fprintf(stderr, "scriptc: fetch internal callback '%s' threw: %s\n", name,
|
|
msg ? msg : "?");
|
|
if (msg) JS_FreeCString(ctx, msg);
|
|
JS_FreeValue(ctx, e);
|
|
return;
|
|
}
|
|
JS_FreeValue(ctx, r);
|
|
}
|
|
|
|
/* Network failure → onError(detail, code) → the glue's TypeError "fetch
|
|
* failed" with the classified cause. Settles the transfer. */
|
|
static void fx_error(FxTransfer *t, const char *detail, const char *code) {
|
|
if (t->done || t->cancelled) {
|
|
fx_settle(t);
|
|
return;
|
|
}
|
|
JSContext *ctx = t->ctx;
|
|
JSValue args[2];
|
|
args[0] = JS_NewString(ctx, detail);
|
|
args[1] = code ? JS_NewString(ctx, code) : JS_UNDEFINED;
|
|
fx_call(t, "onError", 2, (JSValueConst *)args);
|
|
JS_FreeValue(ctx, args[0]);
|
|
JS_FreeValue(ctx, args[1]);
|
|
fx_settle(t);
|
|
}
|
|
|
|
static void fx_request_length_mismatch(FxTransfer *t) {
|
|
if (t->client) scr_http_client_destroy(t->client);
|
|
fx_error(t, "Request body length does not match content-length header",
|
|
"UND_ERR_REQ_CONTENT_LENGTH_MISMATCH");
|
|
}
|
|
|
|
/* ── minted listener closures (caps[0] boxes the transfer) ───────────── */
|
|
|
|
static ScrClosure *fx_closure(FxTransfer *t, void *fn) {
|
|
/* fn doubles as the closure's own entry: zero-payload lists (req 'end')
|
|
* call cb->fn(cb) directly, payload lists call the fn stored beside the
|
|
* registration — both routes land on the same handler here. */
|
|
ScrClosure *cb = scr_closure_new(fn, 1);
|
|
ScrBox *box = scr_box_new_obj(&fx_retain_v, &fx_release_v, NULL);
|
|
scr_box_set_ref(box, fx_retain(t));
|
|
cb->caps[0] = box;
|
|
return cb;
|
|
}
|
|
|
|
/* +1 — release after use. */
|
|
static FxTransfer *fx_from(ScrClosure *cb) { return (FxTransfer *)scr_box_get_ref(cb->caps[0]); }
|
|
|
|
/* ── URL helpers ─────────────────────────────────────────────────────── */
|
|
|
|
/* scheme://[userinfo@]host[:port]path[?query][#fragment] — response.url
|
|
* and the proxy's absolute-URI form exclude the fragment (Node). */
|
|
static ScrStr *fx_url_serialize(const ScrUrl *u, bool with_fragment) {
|
|
size_t cap = u->scheme->len + u->userinfo->len + u->host->len + u->port->len +
|
|
u->path->len + u->query->len + u->fragment->len + 8;
|
|
char *buf = malloc(cap);
|
|
if (!buf) fx_oom();
|
|
size_t n = 0;
|
|
memcpy(buf + n, u->scheme->data, u->scheme->len);
|
|
n += u->scheme->len;
|
|
buf[n++] = ':';
|
|
if (u->has_authority) {
|
|
buf[n++] = '/';
|
|
buf[n++] = '/';
|
|
if (u->userinfo->len > 0) {
|
|
memcpy(buf + n, u->userinfo->data, u->userinfo->len);
|
|
n += u->userinfo->len;
|
|
buf[n++] = '@';
|
|
}
|
|
memcpy(buf + n, u->host->data, u->host->len);
|
|
n += u->host->len;
|
|
if (u->port->len > 0) {
|
|
buf[n++] = ':';
|
|
memcpy(buf + n, u->port->data, u->port->len);
|
|
n += u->port->len;
|
|
}
|
|
}
|
|
if (u->path->len > 0) {
|
|
memcpy(buf + n, u->path->data, u->path->len);
|
|
n += u->path->len;
|
|
} else {
|
|
buf[n++] = '/';
|
|
}
|
|
memcpy(buf + n, u->query->data, u->query->len);
|
|
n += u->query->len;
|
|
if (with_fragment) {
|
|
memcpy(buf + n, u->fragment->data, u->fragment->len);
|
|
n += u->fragment->len;
|
|
}
|
|
ScrStr *out = scr_str_new(buf, n);
|
|
free(buf);
|
|
return out;
|
|
}
|
|
|
|
/* Parse an absolute URL, answering NULL (static exception CLEARED) instead
|
|
* of throwing — this unit runs inside engine host calls and the net
|
|
* dispatch, where a pending static exception belongs to nobody. */
|
|
static ScrUrl *fx_url_parse(ScrStr *s) {
|
|
ScrUrl *u = scr_url_new(s);
|
|
if (!u) scr_exc_clear();
|
|
return u;
|
|
}
|
|
|
|
/* Resolve a Location header against the current hop's URL. Handles the
|
|
* absolute form plus the relative shapes real servers send (//authority,
|
|
* /rooted, ?query, plain relative); everything reparses through the
|
|
* WHATWG parser so dot segments and encoding normalize consistently. */
|
|
static bool fx_same_scheme_relative(const ScrUrl *base,
|
|
const ScrStr *loc) {
|
|
if (loc->len <= base->scheme->len ||
|
|
loc->data[base->scheme->len] != ':') {
|
|
return false;
|
|
}
|
|
for (size_t i = 0; i < base->scheme->len; i++) {
|
|
char a = loc->data[i];
|
|
char b = base->scheme->data[i];
|
|
if (a >= 'A' && a <= 'Z') a = (char)(a - 'A' + 'a');
|
|
if (b >= 'A' && b <= 'Z') b = (char)(b - 'A' + 'a');
|
|
if (a != b) return false;
|
|
}
|
|
size_t tail = base->scheme->len + 1;
|
|
return loc->len - tail < 2 ||
|
|
!((loc->data[tail] == '/' || loc->data[tail] == '\\') &&
|
|
(loc->data[tail + 1] == '/' ||
|
|
loc->data[tail + 1] == '\\'));
|
|
}
|
|
|
|
static ScrUrl *fx_resolve(const ScrUrl *base, const ScrStr *loc) {
|
|
bool same_scheme_relative = fx_same_scheme_relative(base, loc);
|
|
const char *ref_data =
|
|
loc->data + (same_scheme_relative ? base->scheme->len + 1 : 0);
|
|
size_t ref_len =
|
|
loc->len - (same_scheme_relative ? base->scheme->len + 1 : 0);
|
|
if (!same_scheme_relative) {
|
|
ScrStr *abs_try = scr_str_new(loc->data, loc->len);
|
|
ScrUrl *u = fx_url_parse(abs_try);
|
|
scr_str_release(abs_try);
|
|
if (u) return u;
|
|
}
|
|
|
|
/*
|
|
* Empty and fragment-only references preserve the complete base path
|
|
* and query. The request serializer omits the fragment, but resolving it
|
|
* first keeps the redirect on the current resource rather than its parent.
|
|
*/
|
|
if (ref_len == 0 || ref_data[0] == '#') {
|
|
ScrStr *base_text = fx_url_serialize(base, false);
|
|
size_t len = base_text->len + ref_len;
|
|
char *joined = malloc(len);
|
|
if (!joined) fx_oom();
|
|
memcpy(joined, base_text->data, base_text->len);
|
|
memcpy(joined + base_text->len, ref_data, ref_len);
|
|
ScrStr *text = scr_str_new(joined, len);
|
|
free(joined);
|
|
scr_str_release(base_text);
|
|
ScrUrl *out = fx_url_parse(text);
|
|
scr_str_release(text);
|
|
return out;
|
|
}
|
|
|
|
/* relative: assemble scheme://authority + resolved path/query */
|
|
size_t cap = base->scheme->len + base->userinfo->len + base->host->len + base->port->len +
|
|
base->path->len + base->query->len + ref_len + 16;
|
|
char *buf = malloc(cap);
|
|
if (!buf) fx_oom();
|
|
size_t n = 0;
|
|
memcpy(buf + n, base->scheme->data, base->scheme->len);
|
|
n += base->scheme->len;
|
|
buf[n++] = ':';
|
|
if (ref_len >= 2 &&
|
|
(ref_data[0] == '/' || ref_data[0] == '\\') &&
|
|
(ref_data[1] == '/' || ref_data[1] == '\\')) {
|
|
memcpy(buf + n, ref_data, ref_len);
|
|
n += ref_len;
|
|
} else {
|
|
buf[n++] = '/';
|
|
buf[n++] = '/';
|
|
if (base->userinfo->len > 0) {
|
|
memcpy(buf + n, base->userinfo->data, base->userinfo->len);
|
|
n += base->userinfo->len;
|
|
buf[n++] = '@';
|
|
}
|
|
memcpy(buf + n, base->host->data, base->host->len);
|
|
n += base->host->len;
|
|
if (base->port->len > 0) {
|
|
buf[n++] = ':';
|
|
memcpy(buf + n, base->port->data, base->port->len);
|
|
n += base->port->len;
|
|
}
|
|
if (ref_len > 0 && (ref_data[0] == '/' || ref_data[0] == '\\')) {
|
|
memcpy(buf + n, ref_data, ref_len);
|
|
n += ref_len;
|
|
} else if (ref_len > 0 && ref_data[0] == '?') {
|
|
memcpy(buf + n, base->path->data, base->path->len);
|
|
n += base->path->len;
|
|
memcpy(buf + n, ref_data, ref_len);
|
|
n += ref_len;
|
|
} else {
|
|
/* plain relative: replace everything after the path's last '/' */
|
|
size_t keep = 0;
|
|
for (size_t i = 0; i < base->path->len; i++) {
|
|
if (base->path->data[i] == '/') keep = i + 1;
|
|
}
|
|
memcpy(buf + n, base->path->data, keep);
|
|
n += keep;
|
|
if (keep == 0) buf[n++] = '/';
|
|
memcpy(buf + n, ref_data, ref_len);
|
|
n += ref_len;
|
|
}
|
|
}
|
|
ScrStr *s = scr_str_new(buf, n);
|
|
free(buf);
|
|
ScrUrl *out = fx_url_parse(s);
|
|
scr_str_release(s);
|
|
return out;
|
|
}
|
|
|
|
/* The URL host with IPv6 brackets stripped (the dial/SNI form). +1. */
|
|
static ScrStr *fx_bare_host(const ScrStr *host) {
|
|
if (host->len >= 2 && host->data[0] == '[' && host->data[host->len - 1] == ']') {
|
|
return scr_str_new(host->data + 1, host->len - 2);
|
|
}
|
|
return scr_str_retain((ScrStr *)host);
|
|
}
|
|
|
|
static int fx_url_port(const ScrUrl *u, int deflt) {
|
|
if (u->port->len == 0) return deflt;
|
|
int p = 0;
|
|
for (size_t i = 0; i < u->port->len; i++) p = p * 10 + (u->port->data[i] - '0');
|
|
return p;
|
|
}
|
|
|
|
static bool fx_bad_port(int port) {
|
|
static const int ports[] = {SCR_FETCH_BAD_PORTS};
|
|
for (size_t i = 0; i < sizeof ports / sizeof ports[0]; i++) {
|
|
if (port == ports[i]) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* ── env proxy (NODE_USE_ENV_PROXY=1, undici's EnvHttpProxyAgent) ────── */
|
|
|
|
static bool fx_proxy_enabled;
|
|
static char *fx_http_proxy;
|
|
static char *fx_https_proxy;
|
|
static char *fx_all_proxy;
|
|
static char *fx_no_proxy;
|
|
|
|
static char *fx_env_copy(const char *lower, const char *upper) {
|
|
const char *v = getenv(lower);
|
|
if (v == NULL || v[0] == '\0') v = getenv(upper);
|
|
if (v == NULL || v[0] == '\0') return NULL;
|
|
size_t len = strlen(v);
|
|
char *copy = malloc(len + 1);
|
|
if (!copy) fx_oom();
|
|
memcpy(copy, v, len + 1);
|
|
return copy;
|
|
}
|
|
|
|
static void fx_proxy_snapshot(void) {
|
|
const char *optin = getenv("NODE_USE_ENV_PROXY");
|
|
fx_proxy_enabled = optin != NULL && strcmp(optin, "1") == 0;
|
|
/* Snapshot even without Node's global opt-in: Vercel's dispatcher is a
|
|
* request-local proxy activation over the same startup environment. */
|
|
fx_http_proxy = fx_env_copy("http_proxy", "HTTP_PROXY");
|
|
fx_https_proxy = fx_env_copy("https_proxy", "HTTPS_PROXY");
|
|
fx_all_proxy = fx_env_copy("all_proxy", "ALL_PROXY");
|
|
fx_no_proxy = fx_env_copy("no_proxy", "NO_PROXY");
|
|
}
|
|
|
|
static void fx_proxy_snapshot_free(void) {
|
|
free(fx_http_proxy);
|
|
free(fx_https_proxy);
|
|
free(fx_all_proxy);
|
|
free(fx_no_proxy);
|
|
fx_http_proxy = fx_https_proxy = fx_all_proxy = fx_no_proxy = NULL;
|
|
}
|
|
|
|
static int fx_hex_value(char c) {
|
|
if (c >= '0' && c <= '9') return c - '0';
|
|
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
|
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
|
return -1;
|
|
}
|
|
|
|
static ScrStr *fx_proxy_authorization(const ScrStr *userinfo) {
|
|
char *plain = malloc(userinfo->len + 1);
|
|
if (!plain) fx_oom();
|
|
size_t plain_len = 0;
|
|
bool has_separator = false;
|
|
for (size_t i = 0; i < userinfo->len;) {
|
|
char c = userinfo->data[i];
|
|
if (c == '%' && i + 2 < userinfo->len) {
|
|
int hi = fx_hex_value(userinfo->data[i + 1]);
|
|
int lo = fx_hex_value(userinfo->data[i + 2]);
|
|
if (hi >= 0 && lo >= 0) {
|
|
plain[plain_len++] = (char)((hi << 4) | lo);
|
|
i += 3;
|
|
continue;
|
|
}
|
|
}
|
|
if (c == ':') has_separator = true;
|
|
plain[plain_len++] = c;
|
|
i++;
|
|
}
|
|
if (!has_separator) plain[plain_len++] = ':';
|
|
|
|
static const char b64[] =
|
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
size_t encoded_len = ((plain_len + 2) / 3) * 4;
|
|
char *header = malloc(6 + encoded_len);
|
|
if (!header) fx_oom();
|
|
memcpy(header, "Basic ", 6);
|
|
size_t out = 6;
|
|
for (size_t i = 0; i < plain_len; i += 3) {
|
|
unsigned b0 = (unsigned char)plain[i];
|
|
unsigned b1 = i + 1 < plain_len ? (unsigned char)plain[i + 1] : 0;
|
|
unsigned b2 = i + 2 < plain_len ? (unsigned char)plain[i + 2] : 0;
|
|
unsigned triple = (b0 << 16) | (b1 << 8) | b2;
|
|
header[out++] = b64[(triple >> 18) & 63];
|
|
header[out++] = b64[(triple >> 12) & 63];
|
|
header[out++] = i + 1 < plain_len ? b64[(triple >> 6) & 63] : '=';
|
|
header[out++] = i + 2 < plain_len ? b64[triple & 63] : '=';
|
|
}
|
|
free(plain);
|
|
ScrStr *result = scr_str_new(header, out);
|
|
free(header);
|
|
return result;
|
|
}
|
|
|
|
/* no_proxy: comma/space-separated host suffixes; "*" excludes everything;
|
|
* a leading dot or "*." is stripped; entries match the host exactly or at
|
|
* a dot boundary. Port qualifiers (host:port) require the port to match. */
|
|
static bool fx_no_proxy_match(const char *list, const ScrStr *host, int port) {
|
|
const char *p = list;
|
|
while (*p) {
|
|
while (*p == ',' || *p == ' ' || *p == '\t') p++;
|
|
const char *start = p;
|
|
while (*p && *p != ',' && *p != ' ' && *p != '\t') p++;
|
|
size_t len = (size_t)(p - start);
|
|
if (len == 0) continue;
|
|
if (len == 1 && start[0] == '*') return true;
|
|
/* split a :port qualifier */
|
|
size_t hlen = len;
|
|
long eport = -1;
|
|
if (start[0] == '[') {
|
|
size_t close = 1;
|
|
while (close < len && start[close] != ']') close++;
|
|
if (close < len && close + 1 < len && start[close + 1] == ':') {
|
|
hlen = close + 1;
|
|
eport = strtol(start + close + 2, NULL, 10);
|
|
}
|
|
} else {
|
|
for (size_t i = 0; i < len; i++) {
|
|
if (start[i] == ':') {
|
|
hlen = i;
|
|
eport = strtol(start + i + 1, NULL, 10);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (hlen >= 2 && start[0] == '*' && start[1] == '.') {
|
|
start += 2;
|
|
hlen -= 2;
|
|
} else if (hlen > 0 && start[0] == '.') {
|
|
start++;
|
|
hlen--;
|
|
}
|
|
if (hlen == 0) continue;
|
|
if (eport >= 0 && eport != port) continue;
|
|
if (host->len == hlen) {
|
|
bool eq = true;
|
|
for (size_t i = 0; i < hlen && eq; i++) {
|
|
char a = host->data[i], b = start[i];
|
|
if (a >= 'A' && a <= 'Z') a = (char)(a - 'A' + 'a');
|
|
if (b >= 'A' && b <= 'Z') b = (char)(b - 'A' + 'a');
|
|
eq = a == b;
|
|
}
|
|
if (eq) return true;
|
|
} else if (host->len > hlen && host->data[host->len - hlen - 1] == '.') {
|
|
bool eq = true;
|
|
for (size_t i = 0; i < hlen && eq; i++) {
|
|
char a = host->data[host->len - hlen + i], b = start[i];
|
|
if (a >= 'A' && a <= 'Z') a = (char)(a - 'A' + 'a');
|
|
if (b >= 'A' && b <= 'Z') b = (char)(b - 'A' + 'a');
|
|
eq = a == b;
|
|
}
|
|
if (eq) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* The proxy for this hop's target, parsed (+1), or NULL for direct. */
|
|
static ScrUrl *fx_proxy_for(const ScrUrl *target, bool https, int target_port,
|
|
bool enabled, bool *invalid) {
|
|
*invalid = false;
|
|
if (!enabled) return NULL;
|
|
const char *proxy = https ? fx_https_proxy : fx_http_proxy;
|
|
if (proxy == NULL) proxy = fx_all_proxy;
|
|
if (proxy == NULL) return NULL;
|
|
if (fx_no_proxy != NULL &&
|
|
fx_no_proxy_match(fx_no_proxy, target->host, target_port)) {
|
|
return NULL;
|
|
}
|
|
ScrStr *ps = scr_str_new(proxy, strlen(proxy));
|
|
ScrUrl *u = fx_url_parse(ps);
|
|
scr_str_release(ps);
|
|
*invalid = u == NULL;
|
|
return u;
|
|
}
|
|
|
|
/* ── DNS (the dns.lookup precedent: getaddrinfo at hop start) ────────── */
|
|
|
|
/* The address to DIAL for `host` (+1): brackets strip, then the shared
|
|
* blocking lookup (scr_net.c) — IP literals and localhost pass through,
|
|
* hostnames resolve first-answer, and resolution failure answers the
|
|
* HOSTNAME unchanged so the dial's deferred "getaddrinfo ENOTFOUND host"
|
|
* error is Node's exact cause. */
|
|
static ScrStr *fx_dial_host(const ScrStr *host) {
|
|
ScrStr *bare = fx_bare_host(host);
|
|
ScrStr *out = scr_net_blocking_lookup(bare);
|
|
scr_str_release(bare);
|
|
return out;
|
|
}
|
|
|
|
/* ── the hop ─────────────────────────────────────────────────────────── */
|
|
|
|
static void fx_on_response(ScrClosure *cb, ScrHttpReq *res /*+1*/);
|
|
static void fx_on_client_error(ScrClosure *cb, ScrStr *msg /*borrowed*/);
|
|
static void fx_on_upgrade(
|
|
ScrClosure *cb, ScrHttpReq *res, ScrNetSocket *socket,
|
|
ScrBytes *head);
|
|
|
|
static void fx_start_hop(FxTransfer *t) {
|
|
if (!t->request_streaming && t->request_has_content_length) {
|
|
size_t body_len = t->body ? t->body->len : 0;
|
|
if (body_len != t->request_content_length) {
|
|
fx_request_length_mismatch(t);
|
|
return;
|
|
}
|
|
}
|
|
ScrUrl *u = t->url;
|
|
bool https = fx_str_is(u->scheme, "https");
|
|
int default_port = https ? 443 : 80;
|
|
int port = fx_url_port(u, default_port);
|
|
if (fx_bad_port(port)) {
|
|
fx_error(t, "fetch failed", NULL);
|
|
return;
|
|
}
|
|
bool invalid_proxy = false;
|
|
ScrUrl *proxy = fx_proxy_for(
|
|
u, https, port, fx_proxy_enabled || t->use_env_proxy, &invalid_proxy);
|
|
bool proxy_http = proxy && fx_str_is(proxy->scheme, "http");
|
|
bool proxy_https = proxy && fx_str_is(proxy->scheme, "https");
|
|
if (invalid_proxy || (proxy && !proxy_http && !proxy_https) ||
|
|
(https && proxy)) {
|
|
if (proxy) scr_url_release(proxy);
|
|
fx_error(t, "fetch failed", NULL);
|
|
return;
|
|
}
|
|
|
|
/* undici's request head, in undici's order: host, connection, the user
|
|
* headers, then the fetch defaults for whatever the user left unset. */
|
|
size_t nuser = (size_t)scr_arr_len(t->headers);
|
|
ScrArr *pairs = scr_arr_new(SCR_ELEM_STR, nuser + 16);
|
|
char authority[300];
|
|
int alen;
|
|
if (port != default_port) {
|
|
alen = snprintf(authority, sizeof authority, "%.*s:%d", (int)u->host->len, u->host->data, port);
|
|
} else {
|
|
alen = snprintf(authority, sizeof authority, "%.*s", (int)u->host->len, u->host->data);
|
|
}
|
|
fx_pairs_push(pairs, "host", authority, (size_t)alen);
|
|
if (!fx_pairs_have(t->headers, "connection")) {
|
|
fx_pairs_push(pairs, "connection", "keep-alive", 10);
|
|
}
|
|
for (size_t i = 0; i + 1 < nuser; i += 2) {
|
|
ScrStr *name = scr_arr_get_ref(t->headers, (double)i);
|
|
ScrStr *value = scr_arr_get_ref(t->headers, (double)(i + 1));
|
|
if (fx_eq_ci(name->data, name->len, "host")) {
|
|
scr_str_release(name);
|
|
scr_str_release(value);
|
|
continue;
|
|
}
|
|
scr_arr_push_ref(pairs, name);
|
|
scr_arr_push_ref(pairs, value);
|
|
}
|
|
if (proxy && proxy->userinfo->len > 0 &&
|
|
!fx_pairs_have(t->headers, "proxy-authorization")) {
|
|
ScrStr *authorization = fx_proxy_authorization(proxy->userinfo);
|
|
fx_pairs_push(pairs, "proxy-authorization",
|
|
authorization->data, authorization->len);
|
|
scr_str_release(authorization);
|
|
}
|
|
if (!fx_pairs_have(t->headers, "accept")) fx_pairs_push(pairs, "accept", "*/*", 3);
|
|
if (!fx_pairs_have(t->headers, "accept-language")) fx_pairs_push(pairs, "accept-language", "*", 1);
|
|
if (!fx_pairs_have(t->headers, "sec-fetch-mode")) fx_pairs_push(pairs, "sec-fetch-mode", "cors", 4);
|
|
if (!fx_pairs_have(t->headers, "user-agent")) fx_pairs_push(pairs, "user-agent", "node", 4);
|
|
if (!fx_pairs_have(t->headers, "accept-encoding")) {
|
|
fx_pairs_push(pairs, "accept-encoding", "gzip, deflate", 13);
|
|
}
|
|
|
|
/* the request-target and the wire to dial */
|
|
ScrStr *path;
|
|
ScrStr *dial;
|
|
int dial_port;
|
|
if (proxy != NULL) {
|
|
path = fx_url_serialize(u, false); /* absolute-URI through the proxy */
|
|
dial = fx_dial_host(proxy->host);
|
|
dial_port = fx_url_port(proxy, proxy_https ? 443 : 80);
|
|
} else {
|
|
if (u->query->len > 0) {
|
|
size_t plen = (u->path->len > 0 ? u->path->len : 1) + u->query->len;
|
|
char *pb = malloc(plen);
|
|
if (!pb) fx_oom();
|
|
size_t n = 0;
|
|
if (u->path->len > 0) {
|
|
memcpy(pb, u->path->data, u->path->len);
|
|
n = u->path->len;
|
|
} else {
|
|
pb[n++] = '/';
|
|
}
|
|
memcpy(pb + n, u->query->data, u->query->len);
|
|
n += u->query->len;
|
|
path = scr_str_new(pb, n);
|
|
free(pb);
|
|
} else {
|
|
path = u->path->len > 0 ? scr_str_retain(u->path) : scr_str_new("/", 1);
|
|
}
|
|
dial = fx_dial_host(u->host);
|
|
dial_port = port;
|
|
}
|
|
|
|
ScrHttpClientReq *c;
|
|
if (https || proxy_https) {
|
|
ScrStr *sni =
|
|
fx_bare_host(https ? u->host : proxy->host);
|
|
void *cli = scr_tls_fetch_client_ctx(sni, true);
|
|
scr_str_release(sni);
|
|
c = scr_http_request_ex(dial, dial_port, path, t->method, 0, pairs, false, NULL, NULL,
|
|
443, &scr_tls_fetch_client_wrap, cli);
|
|
} else {
|
|
c = scr_http_request_ex(dial, dial_port, path, t->method, 0, pairs, false, NULL, NULL,
|
|
80, NULL, NULL);
|
|
}
|
|
scr_str_release(dial);
|
|
scr_str_release(path);
|
|
scr_arr_release(pairs);
|
|
if (proxy) scr_url_release(proxy);
|
|
|
|
t->client = c; /* the constructor's +1 */
|
|
t->request_ended = false;
|
|
scr_http_client_on_response(c, fx_closure(t, (void *)&fx_on_response), &fx_on_response, true);
|
|
scr_http_client_on_upgrade(c, fx_closure(t, (void *)&fx_on_upgrade), &fx_on_upgrade, true);
|
|
scr_http_client_on_error(c, fx_closure(t, (void *)&fx_on_client_error), &fx_on_client_error, false);
|
|
if (t->body != NULL) {
|
|
scr_http_client_end_bytes(c, t->body);
|
|
t->request_ended = true;
|
|
} else if (!t->request_streaming) {
|
|
scr_http_client_end(c);
|
|
t->request_ended = true;
|
|
}
|
|
}
|
|
|
|
/* ── redirects ───────────────────────────────────────────────────────── */
|
|
|
|
static void fx_strip_header(ScrArr **headers, const char *name) {
|
|
ScrArr *old = *headers;
|
|
size_t n = (size_t)scr_arr_len(old);
|
|
ScrArr *out = scr_arr_new(SCR_ELEM_STR, n);
|
|
for (size_t i = 0; i + 1 < n; i += 2) {
|
|
ScrStr *nm = (ScrStr *)scr_arr_get_ref(old, (double)i);
|
|
if (fx_eq_ci(nm->data, nm->len, name)) {
|
|
scr_str_release(nm);
|
|
continue;
|
|
}
|
|
scr_arr_push_ref(out, nm);
|
|
scr_arr_push_ref(out, scr_arr_get_ref(old, (double)(i + 1)));
|
|
}
|
|
scr_arr_release(old);
|
|
*headers = out;
|
|
}
|
|
|
|
/* Follows one redirect hop: rewrites the request per the fetch spec and
|
|
* dials the next URL. Consumes nothing; the caller drops res/client. */
|
|
static bool fx_redirect(FxTransfer *t, int status, const ScrStr *loc) {
|
|
if (t->hops >= 20) {
|
|
fx_error(t, "redirect count exceeded", NULL);
|
|
return false;
|
|
}
|
|
ScrUrl *next = fx_resolve(t->url, loc);
|
|
if (next == NULL ||
|
|
!(fx_str_is(next->scheme, "http") || fx_str_is(next->scheme, "https")) ||
|
|
next->userinfo->len > 0) {
|
|
if (next) scr_url_release(next);
|
|
fx_error(t, "invalid redirect URL", NULL);
|
|
return false;
|
|
}
|
|
/* 303 rewrites everything but GET/HEAD to GET; 301/302 rewrite POST —
|
|
* the body and its content-* headers drop with the rewrite. */
|
|
bool is_get = fx_str_is(t->method, "GET");
|
|
bool is_head = fx_str_is(t->method, "HEAD");
|
|
if (t->request_streaming && status != 303) {
|
|
scr_url_release(next);
|
|
fx_error(t, "fetch failed", NULL);
|
|
return false;
|
|
}
|
|
if ((status == 303 && !is_get && !is_head) ||
|
|
((status == 301 || status == 302) && fx_str_is(t->method, "POST"))) {
|
|
scr_str_release(t->method);
|
|
t->method = scr_str_new("GET", 3);
|
|
t->request_streaming = false;
|
|
if (t->body) {
|
|
scr_bytes_release(t->body);
|
|
t->body = NULL;
|
|
}
|
|
fx_strip_header(&t->headers, "content-type");
|
|
fx_strip_header(&t->headers, "content-length");
|
|
fx_strip_header(&t->headers, "content-encoding");
|
|
fx_strip_header(&t->headers, "content-language");
|
|
fx_strip_header(&t->headers, "content-location");
|
|
t->request_has_content_length = false;
|
|
t->request_content_length = 0;
|
|
t->request_body_sent = 0;
|
|
}
|
|
/* cross-origin hop: credentials do not follow (the fetch spec's
|
|
* authorization rule; cookies are not modeled as ambient state). */
|
|
bool same_origin = t->url->host->len == next->host->len &&
|
|
memcmp(t->url->host->data, next->host->data, next->host->len) == 0 &&
|
|
fx_url_port(t->url, 0) == fx_url_port(next, 0) &&
|
|
t->url->scheme->len == next->scheme->len &&
|
|
memcmp(t->url->scheme->data, next->scheme->data, next->scheme->len) == 0;
|
|
if (!same_origin) {
|
|
fx_strip_header(&t->headers, "authorization");
|
|
fx_strip_header(&t->headers, "proxy-authorization");
|
|
fx_strip_header(&t->headers, "cookie");
|
|
}
|
|
scr_url_release(t->url);
|
|
t->url = next;
|
|
t->hops++;
|
|
t->redirected = true;
|
|
return true;
|
|
}
|
|
|
|
/* ── response delivery ───────────────────────────────────────────────── */
|
|
|
|
static void fx_emit_chunk(FxTransfer *t, const uint8_t *data, size_t len) {
|
|
if (len == 0) return;
|
|
JSValue chunk = JS_NewUint8ArrayCopy(t->ctx, data, len);
|
|
fx_call(t, "onData", 1, (JSValueConst *)&chunk);
|
|
JS_FreeValue(t->ctx, chunk);
|
|
}
|
|
|
|
static void fx_on_data(ScrClosure *cb, ScrBytes *chunk /*borrowed*/) {
|
|
FxTransfer *t = fx_from(cb);
|
|
if (t == NULL) return;
|
|
if (t->done || t->cancelled || !t->responded) {
|
|
fx_release(t);
|
|
return;
|
|
}
|
|
if (!t->inflating) {
|
|
fx_emit_chunk(t, chunk->data, chunk->len);
|
|
fx_release(t);
|
|
return;
|
|
}
|
|
t->zs.next_in = (Bytef *)chunk->data;
|
|
t->zs.avail_in = (uInt)chunk->len;
|
|
bool drain_internal = false;
|
|
unsigned char out[16384];
|
|
while (!t->done && (t->zs.avail_in > 0 || drain_internal)) {
|
|
if (t->inflate_member_end) {
|
|
if (t->zs.avail_in == 0) break;
|
|
if (inflateReset(&t->zs) != Z_OK) {
|
|
ScrHttpClientReq *c = t->client;
|
|
if (c) scr_http_client_destroy(c);
|
|
fx_error(t, "invalid compressed body", NULL);
|
|
break;
|
|
}
|
|
t->inflate_member_end = false;
|
|
}
|
|
t->zs.next_out = out;
|
|
t->zs.avail_out = sizeof out;
|
|
int r = inflate(&t->zs, Z_NO_FLUSH);
|
|
size_t produced = sizeof out - t->zs.avail_out;
|
|
drain_internal = false;
|
|
if (produced > 0) fx_emit_chunk(t, out, produced);
|
|
if (r == Z_STREAM_END) {
|
|
/* RFC 1952 permits concatenated gzip members. */
|
|
t->inflate_member_end = true;
|
|
if (t->zs.avail_in == 0) break;
|
|
continue;
|
|
}
|
|
if (r != Z_OK && r != Z_BUF_ERROR) {
|
|
/* corrupt encoding: the body errors (the glue turns a post-resolve
|
|
* onError into controller.error — undici's terminated shape) */
|
|
const char *detail = t->zs.msg ? t->zs.msg : "invalid compressed body";
|
|
ScrHttpClientReq *c = t->client;
|
|
if (c) scr_http_client_destroy(c);
|
|
fx_error(t, detail, NULL);
|
|
break;
|
|
}
|
|
if (r == Z_BUF_ERROR && produced == 0) break; /* need more input */
|
|
if (t->zs.avail_in == 0) {
|
|
if (r == Z_OK && t->zs.avail_out == 0) {
|
|
drain_internal = true;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
fx_release(t);
|
|
}
|
|
|
|
static void fx_on_end(ScrClosure *cb) {
|
|
FxTransfer *t = fx_from(cb);
|
|
if (t == NULL) return;
|
|
if (t->done || t->cancelled) {
|
|
fx_release(t);
|
|
return;
|
|
}
|
|
fx_call(t, "onEnd", 0, NULL);
|
|
fx_settle(t);
|
|
fx_release(t);
|
|
}
|
|
|
|
/* Mid-body death ('aborted' on the response): the body errors. */
|
|
static void fx_on_res_error(ScrClosure *cb, ScrStr *msg /*borrowed*/) {
|
|
FxTransfer *t = fx_from(cb);
|
|
if (t == NULL) return;
|
|
if (t->done || t->cancelled) {
|
|
fx_release(t);
|
|
return;
|
|
}
|
|
fx_error(t, msg->len > 0 ? msg->data : "aborted", NULL);
|
|
fx_release(t);
|
|
}
|
|
|
|
static void fx_on_response(ScrClosure *cb, ScrHttpReq *res /*+1*/) {
|
|
FxTransfer *t = fx_from(cb);
|
|
if (t == NULL) {
|
|
scr_http_req_release(res);
|
|
return;
|
|
}
|
|
if (t->done || t->cancelled) {
|
|
scr_http_req_release(res);
|
|
fx_release(t);
|
|
return;
|
|
}
|
|
int status = (int)scr_http_req_status(res);
|
|
|
|
/* a redirect hop? (Location present, a redirect status) */
|
|
if (status == 301 || status == 302 || status == 303 || status == 307 || status == 308) {
|
|
ScrStr *locname = scr_str_new("location", 8);
|
|
ScrStr *loc = scr_http_req_header(res, locname);
|
|
scr_str_release(locname);
|
|
if (loc != NULL) {
|
|
if (t->redirect_mode == FX_REDIRECT_MANUAL) {
|
|
scr_str_release(loc);
|
|
} else {
|
|
/* drop this hop's connection (its body never reaches the island) */
|
|
ScrHttpClientReq *old = t->client;
|
|
t->client = NULL;
|
|
if (old) {
|
|
scr_http_client_destroy(old);
|
|
scr_http_client_release(old);
|
|
}
|
|
if (t->redirect_mode == FX_REDIRECT_ERROR) {
|
|
fx_error(t, "fetch failed", NULL);
|
|
scr_str_release(loc);
|
|
scr_http_req_release(res);
|
|
fx_release(t);
|
|
return;
|
|
}
|
|
ScrStr *decoded_loc = fx_location_to_utf8(loc);
|
|
bool go = fx_redirect(t, status, decoded_loc);
|
|
scr_str_release(decoded_loc);
|
|
scr_str_release(loc);
|
|
if (go) fx_start_hop(t);
|
|
scr_http_req_release(res);
|
|
fx_release(t);
|
|
return;
|
|
}
|
|
}
|
|
/* a FINAL 3xx (no Location) resolves with its own body, like Node —
|
|
* the curl bridge documented this corner away; this unit delivers it */
|
|
}
|
|
|
|
t->responded = true;
|
|
|
|
/* content-encoding: gzip/x-gzip/deflate inflate transparently (zlib's
|
|
* 15+32 auto-detects the gzip and zlib framings) */
|
|
{
|
|
ScrStr *cename = scr_str_new("content-encoding", 16);
|
|
ScrStr *ce = scr_http_req_header(res, cename);
|
|
scr_str_release(cename);
|
|
if (ce != NULL) {
|
|
if (fx_str_is(ce, "gzip") || fx_str_is(ce, "x-gzip") || fx_str_is(ce, "deflate")) {
|
|
memset(&t->zs, 0, sizeof t->zs);
|
|
if (inflateInit2(&t->zs, 15 + 32) == Z_OK) t->inflating = true;
|
|
}
|
|
scr_str_release(ce);
|
|
}
|
|
}
|
|
|
|
JSContext *ctx = t->ctx;
|
|
ScrArr *raw = scr_http_req_raw_headers(res);
|
|
size_t nraw = (size_t)scr_arr_len(raw);
|
|
JSValue hdrs = JS_NewArray(ctx);
|
|
for (size_t i = 0; i < nraw; i++) {
|
|
ScrStr *s = (ScrStr *)scr_arr_get_ref(raw, (double)i);
|
|
ScrStr *utf8 = fx_latin1_to_utf8(s);
|
|
JS_SetPropertyUint32(
|
|
ctx, hdrs, (uint32_t)i,
|
|
JS_NewStringLen(ctx, utf8->data, utf8->len));
|
|
scr_str_release(utf8);
|
|
scr_str_release(s);
|
|
}
|
|
scr_arr_release(raw);
|
|
ScrStr *stext = scr_http_req_status_message(res);
|
|
ScrStr *stext_utf8 =
|
|
stext ? fx_latin1_to_utf8(stext) : scr_str_new("", 0);
|
|
ScrStr *final_url = fx_url_serialize(t->url, false);
|
|
JSValue argv[5] = {
|
|
JS_NewInt32(ctx, status),
|
|
JS_NewStringLen(ctx, stext_utf8->data, stext_utf8->len),
|
|
hdrs,
|
|
JS_NewStringLen(ctx, final_url->data, final_url->len),
|
|
JS_NewBool(ctx, t->redirected),
|
|
};
|
|
if (stext) scr_str_release(stext);
|
|
scr_str_release(stext_utf8);
|
|
scr_str_release(final_url);
|
|
fx_call(t, "onResponse", 5, argv);
|
|
for (int i = 0; i < 5; i++) JS_FreeValue(ctx, argv[i]);
|
|
|
|
/* body listeners (registered inside the 'response' emit — the parser
|
|
* delivers body bytes only after this returns, ended bodies fire 'end'
|
|
* right behind us) */
|
|
scr_http_req_on_data(res, fx_closure(t, (void *)&fx_on_data), &fx_on_data, false);
|
|
scr_http_req_on_end(res, fx_closure(t, (void *)&fx_on_end), false);
|
|
scr_http_req_on_error(res, fx_closure(t, (void *)&fx_on_res_error), &fx_on_res_error, false);
|
|
|
|
scr_http_req_release(res);
|
|
fx_release(t);
|
|
}
|
|
|
|
/* Pre-response failure on the hop client: classify into Node's causes. */
|
|
static void fx_on_client_error(ScrClosure *cb, ScrStr *msg /*borrowed*/) {
|
|
FxTransfer *t = fx_from(cb);
|
|
if (t == NULL) return;
|
|
if (t->done || t->cancelled || t->responded) {
|
|
fx_release(t);
|
|
return;
|
|
}
|
|
const char *detail = msg->data;
|
|
const char *code = NULL;
|
|
char codebuf[32];
|
|
if (msg->len > 8 && memcmp(detail, "connect E", 9) == 0) {
|
|
/* "connect ECONNREFUSED ip:port" and family: the code is the token */
|
|
const char *s = detail + 8;
|
|
size_t n = 0;
|
|
while (s[n] != '\0' && s[n] != ' ' && n < sizeof codebuf - 1) n++;
|
|
memcpy(codebuf, s, n);
|
|
codebuf[n] = '\0';
|
|
code = codebuf;
|
|
} else if (msg->len > 12 && memcmp(detail, "getaddrinfo E", 13) == 0) {
|
|
const char *s = detail + 12;
|
|
size_t n = 0;
|
|
while (s[n] != '\0' && s[n] != ' ' && n < sizeof codebuf - 1) n++;
|
|
memcpy(codebuf, s, n);
|
|
codebuf[n] = '\0';
|
|
code = codebuf;
|
|
} else if (fx_eq_ci(detail, msg->len, "socket hang up")) {
|
|
/* undici's cause for a server that died before the response head */
|
|
detail = "other side closed";
|
|
code = "UND_ERR_SOCKET";
|
|
}
|
|
fx_error(t, detail, code);
|
|
fx_release(t);
|
|
}
|
|
|
|
static void fx_on_upgrade(
|
|
ScrClosure *cb, ScrHttpReq *res, ScrNetSocket *socket,
|
|
ScrBytes *head) {
|
|
FxTransfer *t = fx_from(cb);
|
|
scr_http_req_release(res);
|
|
scr_bytes_release(head);
|
|
scr_net_sock_destroy(socket);
|
|
scr_net_sock_release(socket);
|
|
if (t == NULL) return;
|
|
if (!t->done && !t->cancelled) {
|
|
fx_error(t, "unexpected server response", NULL);
|
|
} else {
|
|
fx_settle(t);
|
|
}
|
|
fx_release(t);
|
|
}
|
|
|
|
/* ── host functions (called by the fetch glue, inside the engine) ────── */
|
|
|
|
/* host.start({url, method, headers: [n,v,...], body, streaming, redirectMode},
|
|
* cbs) → transfer id. Fixed bodies are copied out of the engine before the
|
|
* net stack sees them; streams arrive later through host.write/end. An
|
|
* unparsable URL throws the engine TypeError the promise executor turns into
|
|
* Node's rejection shape. */
|
|
static JSValue fx_host_start(JSContext *ctx, JSValueConst this_val, int argc,
|
|
JSValueConst *argv) {
|
|
(void)this_val;
|
|
(void)argc;
|
|
JSValueConst req = argv[0];
|
|
|
|
JSValue urlv = JS_GetPropertyStr(ctx, req, "url");
|
|
const char *url = JS_ToCString(ctx, urlv);
|
|
JS_FreeValue(ctx, urlv);
|
|
if (!url) return JS_EXCEPTION;
|
|
ScrStr *urlstr = scr_str_new(url, strlen(url));
|
|
ScrUrl *u = fx_url_parse(urlstr);
|
|
scr_str_release(urlstr);
|
|
if (u == NULL) {
|
|
JSValue e = JS_ThrowTypeError(ctx, "Failed to parse URL from %s", url);
|
|
JS_FreeCString(ctx, url);
|
|
return e;
|
|
}
|
|
JS_FreeCString(ctx, url);
|
|
if (!(fx_str_is(u->scheme, "http") || fx_str_is(u->scheme, "https"))) {
|
|
scr_url_release(u);
|
|
return JS_ThrowTypeError(ctx, "fetch failed");
|
|
}
|
|
|
|
FxTransfer *t = calloc(1, sizeof *t);
|
|
if (!t) fx_oom();
|
|
t->rc = 1; /* the registry's */
|
|
t->ctx = ctx;
|
|
t->id = fx_next_id++;
|
|
t->cbs = JS_DupValue(ctx, argv[1]);
|
|
t->cbs_live = true;
|
|
t->url = u;
|
|
|
|
JSValue redirectv = JS_GetPropertyStr(ctx, req, "redirectMode");
|
|
JS_ToInt32(ctx, &t->redirect_mode, redirectv);
|
|
JS_FreeValue(ctx, redirectv);
|
|
|
|
JSValue streamingv = JS_GetPropertyStr(ctx, req, "streaming");
|
|
t->request_streaming = JS_ToBool(ctx, streamingv) > 0;
|
|
JS_FreeValue(ctx, streamingv);
|
|
|
|
JSValue envproxyv = JS_GetPropertyStr(ctx, req, "useEnvProxy");
|
|
t->use_env_proxy = JS_ToBool(ctx, envproxyv) > 0;
|
|
JS_FreeValue(ctx, envproxyv);
|
|
|
|
JSValue methodv = JS_GetPropertyStr(ctx, req, "method");
|
|
const char *method = JS_ToCString(ctx, methodv);
|
|
JS_FreeValue(ctx, methodv);
|
|
t->method = scr_str_new(method ? method : "GET", method ? strlen(method) : 3);
|
|
if (method) JS_FreeCString(ctx, method);
|
|
|
|
JSValue hdrs = JS_GetPropertyStr(ctx, req, "headers");
|
|
JSValue lenv = JS_GetPropertyStr(ctx, hdrs, "length");
|
|
uint32_t hn = 0;
|
|
JS_ToUint32(ctx, &hn, lenv);
|
|
JS_FreeValue(ctx, lenv);
|
|
t->headers = scr_arr_new(SCR_ELEM_STR, hn);
|
|
for (uint32_t i = 0; i + 1 < hn; i += 2) {
|
|
JSValue nv = JS_GetPropertyUint32(ctx, hdrs, i);
|
|
JSValue vv = JS_GetPropertyUint32(ctx, hdrs, i + 1);
|
|
size_t nlen = 0, vlen = 0;
|
|
const char *hname = JS_ToCStringLen(ctx, &nlen, nv);
|
|
const char *hvalue = JS_ToCStringLen(ctx, &vlen, vv);
|
|
if (hname && hvalue) {
|
|
scr_arr_push_ref(t->headers, scr_str_new(hname, nlen));
|
|
scr_arr_push_ref(t->headers, scr_str_new(hvalue, vlen));
|
|
}
|
|
if (hname) JS_FreeCString(ctx, hname);
|
|
if (hvalue) JS_FreeCString(ctx, hvalue);
|
|
JS_FreeValue(ctx, nv);
|
|
JS_FreeValue(ctx, vv);
|
|
}
|
|
JS_FreeValue(ctx, hdrs);
|
|
/* The island's Request accepts this forbidden Fetch Metadata name, but
|
|
* Node's transport always controls its value. Match that behavior before
|
|
* the native request head is built. */
|
|
fx_strip_header(&t->headers, "sec-fetch-mode");
|
|
bool content_length_valid =
|
|
fx_content_length(t->headers, &t->request_has_content_length,
|
|
&t->request_content_length);
|
|
|
|
JSValue bodyv = JS_GetPropertyStr(ctx, req, "body");
|
|
if (!JS_IsUndefined(bodyv) && !JS_IsNull(bodyv)) {
|
|
size_t blen = 0;
|
|
uint8_t *bytes = JS_GetUint8Array(ctx, &blen, bodyv);
|
|
if (bytes != NULL || blen == 0) {
|
|
t->body = scr_bytes_new(SCR_BYTES_U8, (double)blen);
|
|
if (blen > 0) memcpy(t->body->data, bytes, blen);
|
|
}
|
|
}
|
|
JS_FreeValue(ctx, bodyv);
|
|
|
|
t->next = fx_live;
|
|
fx_live = t;
|
|
fx_nlive++;
|
|
/*
|
|
* A configuration error can reject synchronously in fx_start_hop().
|
|
* That callback settles the transfer and releases the registry's last
|
|
* reference, so preserve the scalar result before the call.
|
|
*/
|
|
int id = t->id;
|
|
if (content_length_valid) {
|
|
fx_start_hop(t);
|
|
} else {
|
|
fx_error(t, "fetch failed", NULL);
|
|
}
|
|
return JS_NewInt32(ctx, id);
|
|
}
|
|
|
|
static FxTransfer *fx_find(int32_t id) {
|
|
for (FxTransfer *t = fx_live; t; t = t->next) {
|
|
if (t->id == id) return t;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* host.write(id, chunk): forwards one byte chunk from a streaming request.
|
|
* The boolean result lets a queued promise job stop pumping after settlement. */
|
|
static JSValue fx_host_write(JSContext *ctx, JSValueConst this_val, int argc,
|
|
JSValueConst *argv) {
|
|
(void)this_val;
|
|
(void)argc;
|
|
int32_t id = 0;
|
|
JS_ToInt32(ctx, &id, argv[0]);
|
|
FxTransfer *t = fx_find(id);
|
|
if (t == NULL || t->done || t->client == NULL ||
|
|
!t->request_streaming || t->request_ended) {
|
|
return JS_NewBool(ctx, false);
|
|
}
|
|
size_t blen = 0;
|
|
uint8_t *bytes = JS_GetUint8Array(ctx, &blen, argv[1]);
|
|
if (bytes == NULL && blen != 0) return JS_NewBool(ctx, false);
|
|
if (t->request_has_content_length &&
|
|
(t->request_body_sent > t->request_content_length ||
|
|
blen > t->request_content_length - t->request_body_sent)) {
|
|
fx_request_length_mismatch(t);
|
|
return JS_NewBool(ctx, false);
|
|
}
|
|
t->request_body_sent += blen;
|
|
if (blen > 0) {
|
|
ScrBytes *chunk = scr_bytes_new(SCR_BYTES_U8, (double)blen);
|
|
memcpy(chunk->data, bytes, blen);
|
|
scr_http_client_write_bytes(t->client, chunk);
|
|
scr_bytes_release(chunk);
|
|
}
|
|
return JS_NewBool(ctx, true);
|
|
}
|
|
|
|
/* host.end(id): closes a streaming request after its reader reaches EOF. */
|
|
static JSValue fx_host_end(JSContext *ctx, JSValueConst this_val, int argc,
|
|
JSValueConst *argv) {
|
|
(void)this_val;
|
|
(void)argc;
|
|
int32_t id = 0;
|
|
JS_ToInt32(ctx, &id, argv[0]);
|
|
FxTransfer *t = fx_find(id);
|
|
if (t == NULL || t->done || t->client == NULL ||
|
|
!t->request_streaming || t->request_ended) {
|
|
return JS_NewBool(ctx, false);
|
|
}
|
|
if (t->request_has_content_length &&
|
|
t->request_body_sent != t->request_content_length) {
|
|
fx_request_length_mismatch(t);
|
|
return JS_NewBool(ctx, false);
|
|
}
|
|
t->request_ended = true;
|
|
scr_http_client_end(t->client);
|
|
return JS_NewBool(ctx, true);
|
|
}
|
|
|
|
/* host.fail(id, detail): a request reader failed or produced a non-byte
|
|
* chunk. Tear down the hop and reject through the ordinary fetch error path. */
|
|
static JSValue fx_host_fail(JSContext *ctx, JSValueConst this_val, int argc,
|
|
JSValueConst *argv) {
|
|
(void)this_val;
|
|
(void)argc;
|
|
int32_t id = 0;
|
|
JS_ToInt32(ctx, &id, argv[0]);
|
|
FxTransfer *t = fx_find(id);
|
|
if (t == NULL || t->done) return JS_NewBool(ctx, false);
|
|
const char *detail = JS_ToCString(ctx, argv[1]);
|
|
if (t->client) scr_http_client_destroy(t->client);
|
|
fx_error(t, detail ? detail : "fetch failed", NULL);
|
|
if (detail) JS_FreeCString(ctx, detail);
|
|
return JS_NewBool(ctx, true);
|
|
}
|
|
|
|
/* host.abort(id): the island aborted the fetch or cancelled the response
|
|
* body stream. The transfer dies quietly — no onError, no onEnd. */
|
|
static JSValue fx_host_abort(JSContext *ctx, JSValueConst this_val, int argc,
|
|
JSValueConst *argv) {
|
|
(void)this_val;
|
|
(void)argc;
|
|
int32_t id = 0;
|
|
JS_ToInt32(ctx, &id, argv[0]);
|
|
FxTransfer *t = fx_find(id);
|
|
if (t != NULL) {
|
|
t->cancelled = true;
|
|
if (t->client) scr_http_client_destroy(t->client);
|
|
fx_settle(t);
|
|
}
|
|
return JS_UNDEFINED;
|
|
}
|
|
|
|
/* ── the JS half: island surface, request streaming, abort wiring, and
|
|
* error shaping ──────────────────────────────────────────────────────── */
|
|
|
|
static const char fx_glue[] =
|
|
"(host) => {\n"
|
|
" 'use strict';\n"
|
|
" const g = globalThis;\n"
|
|
" const NULL_BODY = { 204: true, 205: true, 304: true };\n"
|
|
" g.fetch = function fetch(input, init) {\n"
|
|
" return new Promise((resolve, reject) => {\n"
|
|
" let url;\n"
|
|
" let method = 'GET';\n"
|
|
" let headers = null;\n"
|
|
" let body = null;\n"
|
|
" let signal = null;\n"
|
|
" let redirect = 'follow';\n"
|
|
" if (input instanceof g.Request) {\n"
|
|
" url = input.url;\n"
|
|
" method = input.method;\n"
|
|
" headers = new g.Headers(input.headers);\n"
|
|
" body = input._body instanceof Uint8Array || input._body instanceof g.ReadableStream ? input._body : null;\n"
|
|
" signal = input._signal;\n"
|
|
" redirect = input.redirect;\n"
|
|
" } else {\n"
|
|
" url = String(input);\n"
|
|
" }\n"
|
|
" init = init === undefined || init === null ? {} : init;\n"
|
|
" // WebIDL observes dictionary members in lexical order, exactly\n"
|
|
" // once each, and converts each value before reading the next.\n"
|
|
" const initBody = init.body;\n"
|
|
" let initBodyValue = null;\n"
|
|
" let initBodyContentType = null;\n"
|
|
" if (initBody !== undefined && initBody !== null) {\n"
|
|
" const convertedBody = new g.Request('http://localhost/', { method: 'POST', body: initBody, duplex: 'half' });\n"
|
|
" initBodyValue = convertedBody._body;\n"
|
|
" initBodyContentType = convertedBody.headers.get('content-type');\n"
|
|
" }\n"
|
|
" const initCache = init.cache;\n"
|
|
" if (initCache !== undefined) throw new TypeError('unsupported RequestInit option: cache');\n"
|
|
" const initCredentials = init.credentials;\n"
|
|
" if (initCredentials !== undefined) throw new TypeError('unsupported RequestInit option: credentials');\n"
|
|
" let useEnvProxy = g.process?.env?.NODE_USE_ENV_PROXY === '1';\n"
|
|
" const initDispatcher = init.dispatcher;\n"
|
|
" // Vercel's dynamic CLI installs its EnvProxyDispatcher when\n"
|
|
" // ordinary proxy environment variables are present. Validate the\n"
|
|
" // known public shape and activate equivalent native proxy routing\n"
|
|
" // for this request. Do not silently accept arbitrary custom\n"
|
|
" // dispatchers whose routing behavior the bridge cannot preserve.\n"
|
|
" if (initDispatcher !== undefined) {\n"
|
|
" const dispatcher = initDispatcher;\n"
|
|
" const envProxyMethods = ['dispatch', 'close', 'destroy', 'agents', 'getAgent', 'shouldProxy', 'parseNoProxy'];\n"
|
|
" const isVercelEnvProxy = dispatcher !== null && typeof dispatcher === 'object' &&\n"
|
|
" dispatcher.constructor?.name === 'EnvProxyDispatcher' &&\n"
|
|
" envProxyMethods.every((member) => typeof dispatcher[member] === 'function');\n"
|
|
" if (!isVercelEnvProxy) {\n"
|
|
" throw new TypeError('unsupported RequestInit option: dispatcher');\n"
|
|
" }\n"
|
|
" useEnvProxy = true;\n"
|
|
" }\n"
|
|
" const initDuplex = init.duplex;\n"
|
|
" let duplex;\n"
|
|
" if (initDuplex !== undefined) {\n"
|
|
" duplex = String(initDuplex);\n"
|
|
" if (duplex !== 'half') throw new TypeError(\"RequestInit.duplex must be 'half'\");\n"
|
|
" }\n"
|
|
" const initHeaders = init.headers;\n"
|
|
" const convertedHeaders = initHeaders === undefined ? undefined : new g.Headers(initHeaders);\n"
|
|
" const initIntegrity = init.integrity;\n"
|
|
" if (initIntegrity !== undefined) throw new TypeError('unsupported RequestInit option: integrity');\n"
|
|
" const initKeepalive = init.keepalive;\n"
|
|
" if (initKeepalive !== undefined) throw new TypeError('unsupported RequestInit option: keepalive');\n"
|
|
" const initMethod = init.method;\n"
|
|
" const convertedMethod = initMethod === undefined ? undefined : String(initMethod);\n"
|
|
" const initMode = init.mode;\n"
|
|
" if (initMode !== undefined) throw new TypeError('unsupported RequestInit option: mode');\n"
|
|
" const initPriority = init.priority;\n"
|
|
" if (initPriority !== undefined) throw new TypeError('unsupported RequestInit option: priority');\n"
|
|
" const initRedirect = init.redirect;\n"
|
|
" if (initRedirect !== undefined) {\n"
|
|
" redirect = String(initRedirect);\n"
|
|
" if (redirect !== 'follow' && redirect !== 'error' && redirect !== 'manual') {\n"
|
|
" throw new TypeError(`undefined: ${redirect} is not an accepted type. Expected one of follow, manual, error.`);\n"
|
|
" }\n"
|
|
" }\n"
|
|
" const initReferrer = init.referrer;\n"
|
|
" if (initReferrer !== undefined) throw new TypeError('unsupported RequestInit option: referrer');\n"
|
|
" const initReferrerPolicy = init.referrerPolicy;\n"
|
|
" if (initReferrerPolicy !== undefined) throw new TypeError('unsupported RequestInit option: referrerPolicy');\n"
|
|
" const initSignal = init.signal;\n"
|
|
" // An explicit init.signal overrides the Request's, null included.\n"
|
|
" if (initSignal !== undefined) {\n"
|
|
" if (initSignal !== null && !(initSignal instanceof g.AbortSignal)) {\n"
|
|
" throw new TypeError('fetch init.signal must be an AbortSignal or null');\n"
|
|
" }\n"
|
|
" signal = initSignal;\n"
|
|
" }\n"
|
|
" const initWindow = init.window;\n"
|
|
" if (initWindow !== undefined) throw new TypeError('unsupported RequestInit option: window');\n"
|
|
" // Request validation follows the complete dictionary conversion.\n"
|
|
" if (convertedMethod !== undefined) {\n"
|
|
" method = new g.Request('http://localhost/', { method: convertedMethod }).method;\n"
|
|
" }\n"
|
|
" if (convertedHeaders !== undefined) headers = convertedHeaders;\n"
|
|
" if (headers === null) headers = new g.Headers();\n"
|
|
" let redirectMode = 0;\n"
|
|
" if (redirect === 'error') redirectMode = 1;\n"
|
|
" else if (redirect === 'manual') redirectMode = 2;\n"
|
|
" // The effective body includes one inherited from an input Request.\n"
|
|
" // A GET/HEAD method override must reject that body as well.\n"
|
|
" if ((body !== null || initBodyValue !== null) && (method === 'GET' || method === 'HEAD')) {\n"
|
|
" throw new TypeError('Request with GET/HEAD method cannot have body.');\n"
|
|
" }\n"
|
|
" if (initBodyValue !== null) {\n"
|
|
" if (initBodyValue instanceof g.ReadableStream && duplex !== 'half') {\n"
|
|
" throw new TypeError('RequestInit: duplex option is required when sending a body.');\n"
|
|
" }\n"
|
|
" body = initBodyValue;\n"
|
|
" if (initBodyContentType !== null && !headers.has('content-type')) {\n"
|
|
" headers.set('content-type', initBodyContentType);\n"
|
|
" }\n"
|
|
" }\n"
|
|
" if (body !== null && !(body instanceof Uint8Array) && !(body instanceof g.ReadableStream)) {\n"
|
|
" throw new TypeError('unsupported request body in the scriptc island');\n"
|
|
" }\n"
|
|
" // An already-aborted signal rejects with ITS reason before any\n"
|
|
" // transfer starts (Node: validation above still throws first).\n"
|
|
" if (signal !== null && signal.aborted) {\n"
|
|
" reject(signal.reason);\n"
|
|
" return;\n"
|
|
" }\n"
|
|
" let controller = null;\n"
|
|
" let resolved = false;\n"
|
|
" let done = false;\n"
|
|
" let id = 0;\n"
|
|
" let requestReader = body instanceof g.ReadableStream ? body.getReader() : null;\n"
|
|
" const stopRequest = (reason) => {\n"
|
|
" const reader = requestReader;\n"
|
|
" requestReader = null;\n"
|
|
" if (reader === null) return;\n"
|
|
" try {\n"
|
|
" const cancelled = reader.cancel(reason);\n"
|
|
" if (cancelled !== undefined && cancelled !== null && typeof cancelled.catch === 'function') cancelled.catch(() => {});\n"
|
|
" } catch (_e) { /* already released or errored */ }\n"
|
|
" try { reader.releaseLock(); } catch (_e) { /* pending read */ }\n"
|
|
" };\n"
|
|
" // The transfer is over (delivered, failed, cancelled, or aborted):\n"
|
|
" // a later signal abort must not touch it.\n"
|
|
" const finish = (reason) => {\n"
|
|
" if (done) return;\n"
|
|
" done = true;\n"
|
|
" stopRequest(reason);\n"
|
|
" if (signal !== null) signal.removeEventListener('abort', onAbort);\n"
|
|
" };\n"
|
|
" const onAbort = () => {\n"
|
|
" if (done) return;\n"
|
|
" finish(signal.reason);\n"
|
|
" host.abort(id); // the C side dies quietly; rejection is ours\n"
|
|
" if (!resolved) {\n"
|
|
" resolved = true;\n"
|
|
" reject(signal.reason);\n"
|
|
" } else if (controller !== null) {\n"
|
|
" // Mid-stream: pending and future reads reject with the reason,\n"
|
|
" // exactly Node's aborted-body behavior.\n"
|
|
" try { controller.error(signal.reason); } catch (_e) { /* already closed */ }\n"
|
|
" controller = null;\n"
|
|
" }\n"
|
|
" };\n"
|
|
" const pumpRequest = () => {\n"
|
|
" const reader = requestReader;\n"
|
|
" if (done || reader === null) return;\n"
|
|
" let pending;\n"
|
|
" try { pending = reader.read(); }\n"
|
|
" catch (error) {\n"
|
|
" requestReader = null;\n"
|
|
" try { reader.releaseLock(); } catch (_e) { /* already released */ }\n"
|
|
" host.fail(id, error instanceof Error ? error.message : String(error));\n"
|
|
" return;\n"
|
|
" }\n"
|
|
" pending.then(\n"
|
|
" (result) => {\n"
|
|
" if (done || requestReader !== reader) {\n"
|
|
" try { reader.releaseLock(); } catch (_e) { /* cancel still pending */ }\n"
|
|
" return;\n"
|
|
" }\n"
|
|
" if (result.done) {\n"
|
|
" requestReader = null;\n"
|
|
" try { reader.releaseLock(); } catch (_e) { /* already released */ }\n"
|
|
" host.end(id);\n"
|
|
" return;\n"
|
|
" }\n"
|
|
" let chunk = result.value;\n"
|
|
" if (typeof chunk === 'string') chunk = new g.TextEncoder().encode(chunk);\n"
|
|
" if (!(chunk instanceof Uint8Array)) {\n"
|
|
" const error = new TypeError('Received non-Uint8Array chunk');\n"
|
|
" requestReader = null;\n"
|
|
" try {\n"
|
|
" const cancelled = reader.cancel(error);\n"
|
|
" if (cancelled !== undefined && cancelled !== null && typeof cancelled.catch === 'function') cancelled.catch(() => {});\n"
|
|
" } catch (_e) { /* source already errored */ }\n"
|
|
" try { reader.releaseLock(); } catch (_e) { /* pending cancel */ }\n"
|
|
" host.fail(id, error.message);\n"
|
|
" return;\n"
|
|
" }\n"
|
|
" if (host.write(id, chunk)) pumpRequest();\n"
|
|
" else stopRequest();\n"
|
|
" },\n"
|
|
" (error) => {\n"
|
|
" if (done || requestReader !== reader) {\n"
|
|
" try { reader.releaseLock(); } catch (_e) { /* cancel still pending */ }\n"
|
|
" return;\n"
|
|
" }\n"
|
|
" requestReader = null;\n"
|
|
" try { reader.releaseLock(); } catch (_e) { /* already released */ }\n"
|
|
" host.fail(id, error instanceof Error ? error.message : String(error));\n"
|
|
" },\n"
|
|
" );\n"
|
|
" };\n"
|
|
" const flat = [];\n"
|
|
" for (const pair of headers) { flat.push(pair[0], pair[1]); }\n"
|
|
" try {\n"
|
|
" id = host.start(\n"
|
|
" {\n"
|
|
" url,\n"
|
|
" method,\n"
|
|
" headers: flat,\n"
|
|
" body: body instanceof Uint8Array ? body : undefined,\n"
|
|
" streaming: body instanceof g.ReadableStream,\n"
|
|
" useEnvProxy,\n"
|
|
" redirectMode,\n"
|
|
" },\n"
|
|
" {\n"
|
|
" onResponse(status, statusText, raw, finalUrl, redirected) {\n"
|
|
" resolved = true;\n"
|
|
" const h = new g.Headers();\n"
|
|
" for (let i = 0; i + 1 < raw.length; i += 2) {\n"
|
|
" try { h.append(raw[i], raw[i + 1]); } catch (_e) { /* junk line */ }\n"
|
|
" }\n"
|
|
" let stream = null;\n"
|
|
" if (method !== 'HEAD' && NULL_BODY[status] !== true) {\n"
|
|
" stream = new g.ReadableStream({\n"
|
|
" start(c) { controller = c; },\n"
|
|
" cancel() { controller = null; finish(); host.abort(id); },\n"
|
|
" });\n"
|
|
" }\n"
|
|
" resolve(g.__scr_mk_response(status, statusText, h, finalUrl, redirected, stream));\n"
|
|
" },\n"
|
|
" onData(chunk) {\n"
|
|
" if (controller !== null) {\n"
|
|
" try { controller.enqueue(chunk); } catch (_e) { /* consumer went away */ }\n"
|
|
" }\n"
|
|
" },\n"
|
|
" onEnd() {\n"
|
|
" finish();\n"
|
|
" if (controller !== null) {\n"
|
|
" try { controller.close(); } catch (_e) { /* already errored */ }\n"
|
|
" controller = null;\n"
|
|
" }\n"
|
|
" },\n"
|
|
" onError(detail, code) {\n"
|
|
" finish();\n"
|
|
" if (!resolved) {\n"
|
|
" // Node's shape: TypeError 'fetch failed' with a CAUSE the\n"
|
|
" // wrappers classify (message + code for the network cases).\n"
|
|
" const cause = new Error(String(detail));\n"
|
|
" if (code !== undefined) cause.code = code;\n"
|
|
" if (code === 'ECONNREFUSED') cause.syscall = 'connect';\n"
|
|
" else if (code === 'ENOTFOUND') cause.syscall = 'getaddrinfo';\n"
|
|
" const err = new TypeError('fetch failed');\n"
|
|
" err.cause = cause;\n"
|
|
" reject(err);\n"
|
|
" } else if (controller !== null) {\n"
|
|
" try { controller.error(new TypeError('terminated')); } catch (_e) { /* already closed */ }\n"
|
|
" controller = null;\n"
|
|
" }\n"
|
|
" },\n"
|
|
" },\n"
|
|
" );\n"
|
|
" } catch (error) {\n"
|
|
" stopRequest(error);\n"
|
|
" throw error;\n"
|
|
" }\n"
|
|
" if (!done && signal !== null) signal.addEventListener('abort', onAbort, { once: true });\n"
|
|
" if (!done && requestReader !== null) pumpRequest();\n"
|
|
" });\n"
|
|
" };\n"
|
|
"}\n";
|
|
|
|
/* ── boot / teardown (registered with the island) ────────────────────── */
|
|
|
|
static void fx_boot(void *jsctx) {
|
|
JSContext *ctx = (JSContext *)jsctx;
|
|
JSValue fn = JS_Eval(ctx, fx_glue, sizeof fx_glue - 1, "<scr-fetch>", JS_EVAL_TYPE_GLOBAL);
|
|
if (JS_IsException(fn)) {
|
|
fprintf(stderr, "scriptc: island fetch glue failed to evaluate\n");
|
|
abort();
|
|
}
|
|
JSValue host = JS_NewObject(ctx);
|
|
JS_SetPropertyStr(ctx, host, "start", JS_NewCFunction(ctx, fx_host_start, "start", 2));
|
|
JS_SetPropertyStr(ctx, host, "write", JS_NewCFunction(ctx, fx_host_write, "write", 2));
|
|
JS_SetPropertyStr(ctx, host, "end", JS_NewCFunction(ctx, fx_host_end, "end", 1));
|
|
JS_SetPropertyStr(ctx, host, "fail", JS_NewCFunction(ctx, fx_host_fail, "fail", 2));
|
|
JS_SetPropertyStr(ctx, host, "abort", JS_NewCFunction(ctx, fx_host_abort, "abort", 1));
|
|
JSValue r = JS_Call(ctx, fn, JS_UNDEFINED, 1, (JSValueConst *)&host);
|
|
JS_FreeValue(ctx, host);
|
|
JS_FreeValue(ctx, fn);
|
|
if (JS_IsException(r)) {
|
|
fprintf(stderr, "scriptc: island fetch glue failed to run\n");
|
|
abort();
|
|
}
|
|
JS_FreeValue(ctx, r);
|
|
}
|
|
|
|
/* Engine teardown with transfers still live (process.exit mid-request, or
|
|
* an exit with an abandoned body stream): free their engine values FIRST
|
|
* so the island's counting allocator returns to zero. The net/http exit
|
|
* cleanups (registered later, so they run earlier — atexit LIFO) have
|
|
* already dropped the listener closures; whatever the registry still
|
|
* holds settles here. */
|
|
static void fx_teardown(void) {
|
|
while (fx_live) fx_settle(fx_live);
|
|
}
|
|
|
|
/* The emitted main calls this (before any island entry) in builds whose
|
|
* embedded graph references fetch. No pending/poll hooks: transfers live
|
|
* on real sockets the loop's own poller sleeps on, and armed island
|
|
* timers cap that sleep through the loop's island-deadline hook. */
|
|
void scr_fetch_install(void) {
|
|
static bool installed = false;
|
|
if (installed) return;
|
|
installed = true;
|
|
scr_tls_ca_install();
|
|
fx_proxy_snapshot();
|
|
scr_atexit(fx_proxy_snapshot_free);
|
|
/* The net unit's loop hooks (pending/dispatch/pollfd): the emitted main
|
|
* registers them only for programs that USE node:net — a fetch-only
|
|
* build must register them itself or its sockets would neither keep
|
|
* the loop alive nor dispatch. */
|
|
scr_net_install();
|
|
/* The island's node:http/https client bridge rides the same units and
|
|
* is always compiled beside the native fetch (cc.ts) — embedded graphs
|
|
* that require node:http/https get working clients, not refusals. */
|
|
scr_net_island_install();
|
|
scr_island_set_fetch(fx_boot, NULL, NULL, fx_teardown);
|
|
}
|
|
|
|
#endif /* SCR_DYNAMIC */
|
|
|
|
#undef SCR_FETCH_BAD_PORTS
|