diff --git a/nginx/config b/nginx/config index 2edf7a3d7..17672b7bf 100644 --- a/nginx/config +++ b/nginx/config @@ -150,6 +150,7 @@ NJS_ENGINE_LIB="$ngx_addon_dir/../build/libnjs.a" if [ "$NJS_HAVE_QUICKJS" = "YES" ]; then NJS_ENGINE_DEP="$ngx_addon_dir/../build/libqjs.a" NJS_ENGINE_LIB="$ngx_addon_dir/../build/libnjs.a $ngx_addon_dir/../build/libqjs.a" + QJS_SRCS="$QJS_SRCS $ngx_addon_dir/ngx_qjs_fetch.c" fi if [ $HTTP != NO ]; then diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c index 0c8215c56..c6455c551 100644 --- a/nginx/ngx_http_js_module.c +++ b/nginx/ngx_http_js_module.c @@ -1133,6 +1133,7 @@ static JSClassDef ngx_http_qjs_headers_out_class = { qjs_module_t *njs_http_qjs_addon_modules[] = { &ngx_qjs_ngx_module, &ngx_qjs_ngx_shared_dict_module, + &ngx_qjs_ngx_fetch_module, /* * Shared addons should be in the same order and the same positions * in all nginx modules. diff --git a/nginx/ngx_js.c b/nginx/ngx_js.c index c91a55308..0c4575b0f 100644 --- a/nginx/ngx_js.c +++ b/nginx/ngx_js.c @@ -436,6 +436,7 @@ static const JSCFunctionListEntry ngx_qjs_ext_ngx[] = { JS_CGETSET_MAGIC_DEF("ERR", ngx_qjs_ext_constant_integer, NULL, NGX_LOG_ERR), JS_CGETSET_DEF("error_log_path", ngx_qjs_ext_error_log_path, NULL), + JS_CFUNC_DEF("fetch", 2, ngx_qjs_ext_fetch), JS_CGETSET_MAGIC_DEF("INFO", ngx_qjs_ext_constant_integer, NULL, NGX_LOG_INFO), JS_CFUNC_MAGIC_DEF("log", 1, ngx_qjs_ext_log, 0), @@ -2184,6 +2185,31 @@ ngx_qjs_core_init(JSContext *cx, const char *name) return m; } + +int +ngx_qjs_array_length(JSContext *cx, uint32_t *plen, JSValueConst arr) +{ + int ret; + JSValue value; + uint32_t len; + + value = JS_GetPropertyStr(cx, arr, "length"); + if (JS_IsException(value)) { + return -1; + } + + ret = JS_ToUint32(cx, &len, value); + JS_FreeValue(cx, value); + + if (ret) { + return -1; + } + + *plen = len; + + return 0; +} + #endif diff --git a/nginx/ngx_js.h b/nginx/ngx_js.h index ba4cbdf96..ea2adc88b 100644 --- a/nginx/ngx_js.h +++ b/nginx/ngx_js.h @@ -63,6 +63,9 @@ #define NGX_QJS_CLASS_ID_SHARED (NGX_QJS_CLASS_ID_OFFSET + 11) #define NGX_QJS_CLASS_ID_SHARED_DICT (NGX_QJS_CLASS_ID_OFFSET + 12) #define NGX_QJS_CLASS_ID_SHARED_DICT_ERROR (NGX_QJS_CLASS_ID_OFFSET + 13) +#define NGX_QJS_CLASS_ID_FETCH_HEADERS (NGX_QJS_CLASS_ID_OFFSET + 14) +#define NGX_QJS_CLASS_ID_FETCH_REQUEST (NGX_QJS_CLASS_ID_OFFSET + 15) +#define NGX_QJS_CLASS_ID_FETCH_RESPONSE (NGX_QJS_CLASS_ID_OFFSET + 16) typedef struct ngx_js_loc_conf_s ngx_js_loc_conf_t; @@ -345,6 +348,10 @@ ngx_int_t ngx_qjs_call(JSContext *cx, JSValue function, JSValue *argv, ngx_int_t ngx_qjs_exception(JSContext *cx, ngx_str_t *s); ngx_int_t ngx_qjs_integer(JSContext *cx, JSValueConst val, ngx_int_t *n); ngx_int_t ngx_qjs_string(JSContext *cx, JSValueConst val, ngx_str_t *str); +int ngx_qjs_array_length(JSContext *cx, uint32_t *plen, JSValueConst arr); + +JSValue ngx_qjs_ext_fetch(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv); #define ngx_qjs_prop(cx, type, start, len) \ ((type == NGX_JS_STRING) ? qjs_string_create(cx, start, len) \ @@ -381,6 +388,7 @@ extern qjs_module_t qjs_webcrypto_module; extern qjs_module_t qjs_zlib_module; extern qjs_module_t ngx_qjs_ngx_module; extern qjs_module_t ngx_qjs_ngx_shared_dict_module; +extern qjs_module_t ngx_qjs_ngx_fetch_module; #endif diff --git a/nginx/ngx_qjs_fetch.c b/nginx/ngx_qjs_fetch.c new file mode 100644 index 000000000..a5da5e0a9 --- /dev/null +++ b/nginx/ngx_qjs_fetch.c @@ -0,0 +1,3903 @@ + +/* + * Copyright (C) Dmitry Volyntsev + * Copyright (C) hongzhidao + * Copyright (C) Antoine Bonavita + * Copyright (C) NGINX, Inc. + */ + + +#include <ngx_config.h> +#include <ngx_core.h> +#include <ngx_event.h> +#include <ngx_event_connect.h> +#include "ngx_js.h" + + +typedef struct ngx_qjs_tb_elt_s ngx_qjs_tb_elt_t; + +struct ngx_qjs_tb_elt_s { + ngx_uint_t hash; + ngx_str_t key; + ngx_str_t value; + ngx_qjs_tb_elt_t *next; +}; + + +typedef struct { + enum { + GUARD_NONE = 0, + GUARD_REQUEST, + GUARD_IMMUTABLE, + GUARD_RESPONSE, + } guard; + ngx_list_t header_list; + ngx_qjs_tb_elt_t *content_type; +} ngx_qjs_headers_t; + + +typedef struct { + enum { + CACHE_MODE_DEFAULT = 0, + CACHE_MODE_NO_STORE, + CACHE_MODE_RELOAD, + CACHE_MODE_NO_CACHE, + CACHE_MODE_FORCE_CACHE, + CACHE_MODE_ONLY_IF_CACHED, + } cache_mode; + enum { + CREDENTIALS_SAME_ORIGIN = 0, + CREDENTIALS_INCLUDE, + CREDENTIALS_OMIT, + } credentials; + enum { + MODE_NO_CORS = 0, + MODE_SAME_ORIGIN, + MODE_CORS, + MODE_NAVIGATE, + MODE_WEBSOCKET, + } mode; + ngx_str_t url; + ngx_str_t method; + u_char m[8]; + uint8_t body_used; + ngx_str_t body; + ngx_qjs_headers_t headers; + JSValue header_value; +} ngx_qjs_request_t; + + +typedef struct { + ngx_str_t url; + ngx_int_t code; + ngx_str_t status_text; + uint8_t body_used; + njs_chb_t chain; + ngx_qjs_headers_t headers; + JSValue header_value; +} ngx_qjs_response_t; + + +typedef struct { + njs_str_t name; + njs_int_t value; +} ngx_qjs_entry_t; + + +typedef struct { + ngx_uint_t state; + ngx_uint_t code; + u_char *status_text; + u_char *status_text_end; + ngx_uint_t count; + ngx_flag_t chunked; + off_t content_length_n; + + u_char *header_name_start; + u_char *header_name_end; + u_char *header_start; + u_char *header_end; +} ngx_qjs_http_parse_t; + + +typedef struct { + u_char *pos; + uint64_t chunk_size; + uint8_t state; + uint8_t last; +} ngx_qjs_http_chunk_parse_t; + + +typedef struct ngx_qjs_http_s ngx_qjs_http_t; + +struct ngx_qjs_http_s { + ngx_log_t *log; + ngx_pool_t *pool; + + JSContext *cx; + ngx_qjs_event_t *event; + + ngx_resolver_ctx_t *ctx; + ngx_addr_t addr; + ngx_addr_t *addrs; + ngx_uint_t naddrs; + ngx_uint_t naddr; + in_port_t port; + + ngx_peer_connection_t peer; + ngx_msec_t timeout; + + ngx_int_t buffer_size; + ngx_int_t max_response_body_size; + + unsigned header_only; + +#if (NGX_SSL) + ngx_str_t tls_name; + ngx_ssl_t *ssl; + njs_bool_t ssl_verify; +#endif + + ngx_buf_t *buffer; + ngx_buf_t *chunk; + njs_chb_t chain; + + ngx_qjs_response_t response; + JSValue response_value; + + JSValue promise; + JSValue promise_callbacks[2]; + + uint8_t done; + ngx_qjs_http_parse_t http_parse; + ngx_qjs_http_chunk_parse_t http_chunk_parse; + ngx_int_t (*process)(ngx_qjs_http_t *http); +}; + + +static njs_int_t ngx_qjs_headers_fill_header_free(JSContext *cx, + ngx_qjs_headers_t *headers, JSValue prop_name, JSValue prop_value); +static njs_int_t ngx_qjs_headers_append(JSContext *cx, + ngx_qjs_headers_t *headers, u_char *name, size_t len, u_char *value, + size_t vlen); +static JSValue ngx_qjs_headers_get(JSContext *cx, JSValue this_val, + ngx_str_t *name, njs_bool_t as_array); +static JSValue ngx_qjs_fetch_flag(JSContext *cx, const ngx_qjs_entry_t *entries, + njs_int_t value); +static njs_int_t ngx_qjs_request_constructor(JSContext *cx, + ngx_qjs_request_t *request, ngx_url_t *u, int argc, JSValueConst *argv); +static void ngx_qjs_http_connect(ngx_qjs_http_t *http); +static void ngx_qjs_http_close_connection(ngx_connection_t *c); +static void ngx_qjs_http_fetch_done(ngx_qjs_http_t *http, JSValue retval, + njs_int_t rc); + +#if (NGX_SSL) +static void ngx_qjs_http_ssl_init_connection(ngx_qjs_http_t *http); +static void ngx_qjs_http_ssl_handshake_handler(ngx_connection_t *c); +static void ngx_qjs_http_ssl_handshake(ngx_qjs_http_t *http); +static njs_int_t ngx_qjs_http_ssl_name(ngx_qjs_http_t *http); +#endif + + +static const ngx_qjs_entry_t ngx_qjs_fetch_cache_modes[] = { + { njs_str("default"), CACHE_MODE_DEFAULT }, + { njs_str("no-store"), CACHE_MODE_NO_STORE }, + { njs_str("reload"), CACHE_MODE_RELOAD }, + { njs_str("no-cache"), CACHE_MODE_NO_CACHE }, + { njs_str("force-cache"), CACHE_MODE_FORCE_CACHE }, + { njs_str("only-if-cached"), CACHE_MODE_ONLY_IF_CACHED }, + { njs_null_str, 0 }, +}; + +static const ngx_qjs_entry_t ngx_qjs_fetch_credentials[] = { + { njs_str("same-origin"), CREDENTIALS_SAME_ORIGIN }, + { njs_str("omit"), CREDENTIALS_OMIT }, + { njs_str("include"), CREDENTIALS_INCLUDE }, + { njs_null_str, 0 }, +}; + +static const ngx_qjs_entry_t ngx_qjs_fetch_modes[] = { + { njs_str("no-cors"), MODE_NO_CORS }, + { njs_str("cors"), MODE_CORS }, + { njs_str("same-origin"), MODE_SAME_ORIGIN }, + { njs_str("navigate"), MODE_NAVIGATE }, + { njs_str("websocket"), MODE_WEBSOCKET }, + { njs_null_str, 0 }, +}; + + +#define NGX_QJS_BODY_ARRAY_BUFFER 0 +#define NGX_QJS_BODY_JSON 1 +#define NGX_QJS_BODY_TEXT 2 + + +static void +njs_qjs_http_destructor(ngx_qjs_event_t *event) +{ + JSContext *cx; + ngx_qjs_http_t *http; + + cx = event->ctx; + http = event->data; + + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, "qjs fetch destructor:%p", + http); + + if (http->ctx != NULL) { + ngx_resolve_name_done(http->ctx); + http->ctx = NULL; + } + + if (http->peer.connection != NULL) { + ngx_qjs_http_close_connection(http->peer.connection); + http->peer.connection = NULL; + } + + njs_chb_destroy(&http->chain); + + JS_FreeValue(cx, http->promise_callbacks[0]); + JS_FreeValue(cx, http->promise_callbacks[1]); + JS_FreeValue(cx, http->promise); + JS_FreeValue(cx, http->response_value); +} + + +static ngx_qjs_http_t * +ngx_qjs_http_alloc(JSContext *cx, ngx_pool_t *pool, ngx_log_t *log) +{ + ngx_js_ctx_t *ctx; + ngx_qjs_http_t *http; + ngx_qjs_event_t *event; + + http = ngx_pcalloc(pool, sizeof(ngx_qjs_http_t)); + if (http == NULL) { + return NULL; + } + + http->pool = pool; + http->log = log; + http->cx = cx; + + http->response.header_value = JS_UNDEFINED; + + http->timeout = 10000; + + http->http_parse.content_length_n = -1; + + http->promise = JS_NewPromiseCapability(cx, http->promise_callbacks); + if (JS_IsException(http->promise)) { + return NULL; + } + + event = ngx_palloc(pool, sizeof(ngx_qjs_event_t)); + if (njs_slow_path(event == NULL)) { + goto fail; + } + + ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx)); + + event->ctx = cx; + event->destructor = njs_qjs_http_destructor; + event->fd = ctx->event_id++; + event->data = http; + + ngx_js_add_event(ctx, event); + + http->event = event; + + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "qjs fetch alloc:%p", http); + + return http; + +fail: + + JS_FreeValue(cx, http->promise); + JS_FreeValue(cx, http->promise_callbacks[0]); + JS_FreeValue(cx, http->promise_callbacks[1]); + + JS_ThrowInternalError(cx, "internal error"); + + return NULL; +} + + +#define ngx_qjs_http_error(http, err, fmt, ...) \ + do { \ + JS_ThrowInternalError((http)->cx, fmt, ##__VA_ARGS__); \ + (http)->response_value = JS_GetException((http)->cx); \ + ngx_qjs_http_fetch_done(http, (http)->response_value, NJS_ERROR); \ + } while (0) + +static void +ngx_qjs_http_next(ngx_qjs_http_t *http) +{ + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, http->log, 0, "qjs fetch next addr"); + + if (++http->naddr >= http->naddrs) { + ngx_qjs_http_error(http, 0, "connect failed"); + return; + } + + if (http->peer.connection != NULL) { + ngx_qjs_http_close_connection(http->peer.connection); + http->peer.connection = NULL; + } + + http->buffer = NULL; + + ngx_qjs_http_connect(http); +} + + +static void +ngx_qjs_http_dummy_handler(ngx_event_t *ev) +{ + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, 0, "qjs fetch dummy handler"); +} + + +static void +ngx_qjs_http_write_handler(ngx_event_t *wev) +{ + ssize_t n, size; + ngx_buf_t *b; + ngx_qjs_http_t *http; + ngx_connection_t *c; + + c = wev->data; + http = c->data; + + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, wev->log, 0, "qjs fetch write handler"); + + if (wev->timedout) { + ngx_qjs_http_error(http, NGX_ETIMEDOUT, "write timed out"); + return; + } + +#if (NGX_SSL) + if (http->ssl != NULL && http->peer.connection->ssl == NULL) { + ngx_qjs_http_ssl_init_connection(http); + return; + } +#endif + + b = http->buffer; + + if (b == NULL) { + size = njs_chb_size(&http->chain); + if (size < 0) { + ngx_qjs_http_error(http, 0, "memory error"); + return; + } + + b = ngx_create_temp_buf(http->pool, size); + if (b == NULL) { + ngx_qjs_http_error(http, 0, "memory error"); + return; + } + + njs_chb_join_to(&http->chain, b->last); + b->last += size; + + http->buffer = b; + } + + size = b->last - b->pos; + + n = c->send(c, b->pos, size); + + if (n == NGX_ERROR) { + ngx_qjs_http_next(http); + return; + } + + if (n > 0) { + b->pos += n; + + if (n == size) { + wev->handler = ngx_qjs_http_dummy_handler; + + http->buffer = NULL; + + if (wev->timer_set) { + ngx_del_timer(wev); + } + + if (ngx_handle_write_event(wev, 0) != NGX_OK) { + ngx_qjs_http_error(http, 0, "write failed"); + } + + return; + } + } + + if (!wev->timer_set) { + ngx_add_timer(wev, http->timeout); + } +} + + +static void +ngx_qjs_http_read_handler(ngx_event_t *rev) +{ + ssize_t n, size; + ngx_int_t rc; + ngx_buf_t *b; + ngx_qjs_http_t *http; + ngx_connection_t *c; + + c = rev->data; + http = c->data; + + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, rev->log, 0, "qjs fetch read handler"); + + if (rev->timedout) { + ngx_qjs_http_error(http, NGX_ETIMEDOUT, "read timed out"); + return; + } + + if (http->buffer == NULL) { + b = ngx_create_temp_buf(http->pool, http->buffer_size); + if (b == NULL) { + ngx_qjs_http_error(http, 0, "memory error"); + return; + } + + http->buffer = b; + } + + for ( ;; ) { + b = http->buffer; + size = b->end - b->last; + + n = c->recv(c, b->last, size); + + if (n > 0) { + b->last += n; + + rc = http->process(http); + + if (rc == NGX_ERROR) { + return; + } + + continue; + } + + if (n == NGX_AGAIN) { + if (ngx_handle_read_event(rev, 0) != NGX_OK) { + ngx_qjs_http_error(http, 0, "read failed"); + } + + return; + } + + if (n == NGX_ERROR) { + ngx_qjs_http_next(http); + return; + } + + break; + } + + http->done = 1; + + rc = http->process(http); + + if (rc == NGX_DONE) { + /* handler was called */ + return; + } + + if (rc == NGX_AGAIN) { + ngx_qjs_http_error(http, 0, "prematurely closed connection"); + } +} + + +static ngx_int_t +ngx_qjs_http_parse_status_line(ngx_qjs_http_parse_t *hp, ngx_buf_t *b) +{ + u_char ch; + u_char *p; + enum { + sw_start = 0, + sw_H, + sw_HT, + sw_HTT, + sw_HTTP, + sw_first_major_digit, + sw_major_digit, + sw_first_minor_digit, + sw_minor_digit, + sw_status, + sw_space_after_status, + sw_status_text, + sw_almost_done + } state; + + state = hp->state; + + for (p = b->pos; p < b->last; p++) { + ch = *p; + + switch (state) { + + /* "HTTP/" */ + case sw_start: + switch (ch) { + case 'H': + state = sw_H; + break; + default: + return NGX_ERROR; + } + break; + + case sw_H: + switch (ch) { + case 'T': + state = sw_HT; + break; + default: + return NGX_ERROR; + } + break; + + case sw_HT: + switch (ch) { + case 'T': + state = sw_HTT; + break; + default: + return NGX_ERROR; + } + break; + + case sw_HTT: + switch (ch) { + case 'P': + state = sw_HTTP; + break; + default: + return NGX_ERROR; + } + break; + + case sw_HTTP: + switch (ch) { + case '/': + state = sw_first_major_digit; + break; + default: + return NGX_ERROR; + } + break; + + /* the first digit of major HTTP version */ + case sw_first_major_digit: + if (ch < '1' || ch > '9') { + return NGX_ERROR; + } + + state = sw_major_digit; + break; + + /* the major HTTP version or dot */ + case sw_major_digit: + if (ch == '.') { + state = sw_first_minor_digit; + break; + } + + if (ch < '0' || ch > '9') { + return NGX_ERROR; + } + + break; + + /* the first digit of minor HTTP version */ + case sw_first_minor_digit: + if (ch < '0' || ch > '9') { + return NGX_ERROR; + } + + state = sw_minor_digit; + break; + + /* the minor HTTP version or the end of the request line */ + case sw_minor_digit: + if (ch == ' ') { + state = sw_status; + break; + } + + if (ch < '0' || ch > '9') { + return NGX_ERROR; + } + + break; + + /* HTTP status code */ + case sw_status: + if (ch == ' ') { + break; + } + + if (ch < '0' || ch > '9') { + return NGX_ERROR; + } + + hp->code = hp->code * 10 + (ch - '0'); + + if (++hp->count == 3) { + state = sw_space_after_status; + } + + break; + + /* space or end of line */ + case sw_space_after_status: + switch (ch) { + case ' ': + state = sw_status_text; + break; + case '.': /* IIS may send 403.1, 403.2, etc */ + state = sw_status_text; + break; + case CR: + break; + case LF: + goto done; + default: + return NGX_ERROR; + } + break; + + /* any text until end of line */ + case sw_status_text: + switch (ch) { + case CR: + hp->status_text_end = p; + state = sw_almost_done; + break; + case LF: + hp->status_text_end = p; + goto done; + } + + if (hp->status_text == NULL) { + hp->status_text = p; + } + + break; + + /* end of status line */ + case sw_almost_done: + switch (ch) { + case LF: + goto done; + default: + return NGX_ERROR; + } + } + } + + b->pos = p; + hp->state = state; + + return NGX_AGAIN; + +done: + + b->pos = p + 1; + hp->state = sw_start; + + return NGX_OK; +} + + +static ngx_int_t +ngx_qjs_http_parse_header_line(ngx_qjs_http_parse_t *hp, ngx_buf_t *b) +{ + u_char c, ch, *p; + enum { + sw_start = 0, + sw_name, + sw_space_before_value, + sw_value, + sw_space_after_value, + sw_almost_done, + sw_header_almost_done + } state; + + state = hp->state; + + for (p = b->pos; p < b->last; p++) { + ch = *p; + + switch (state) { + + /* first char */ + case sw_start: + + switch (ch) { + case CR: + hp->header_end = p; + state = sw_header_almost_done; + break; + case LF: + hp->header_end = p; + goto header_done; + default: + state = sw_name; + hp->header_name_start = p; + + c = (u_char) (ch | 0x20); + if (c >= 'a' && c <= 'z') { + break; + } + + if (ch >= '0' && ch <= '9') { + break; + } + + return NGX_ERROR; + } + break; + + /* header name */ + case sw_name: + c = (u_char) (ch | 0x20); + if (c >= 'a' && c <= 'z') { + break; + } + + if (ch == ':') { + hp->header_name_end = p; + state = sw_space_before_value; + break; + } + + if (ch == '-' || ch == '_') { + break; + } + + if (ch >= '0' && ch <= '9') { + break; + } + + if (ch == CR) { + hp->header_name_end = p; + hp->header_start = p; + hp->header_end = p; + state = sw_almost_done; + break; + } + + if (ch == LF) { + hp->header_name_end = p; + hp->header_start = p; + hp->header_end = p; + goto done; + } + + return NGX_ERROR; + + /* space* before header value */ + case sw_space_before_value: + switch (ch) { + case ' ': + break; + case CR: + hp->header_start = p; + hp->header_end = p; + state = sw_almost_done; + break; + case LF: + hp->header_start = p; + hp->header_end = p; + goto done; + default: + hp->header_start = p; + state = sw_value; + break; + } + break; + + /* header value */ + case sw_value: + switch (ch) { + case ' ': + hp->header_end = p; + state = sw_space_after_value; + break; + case CR: + hp->header_end = p; + state = sw_almost_done; + break; + case LF: + hp->header_end = p; + goto done; + } + break; + + /* space* before end of header line */ + case sw_space_after_value: + switch (ch) { + case ' ': + break; + case CR: + state = sw_almost_done; + break; + case LF: + goto done; + default: + state = sw_value; + break; + } + break; + + /* end of header line */ + case sw_almost_done: + switch (ch) { + case LF: + goto done; + default: + return NGX_ERROR; + } + + /* end of header */ + case sw_header_almost_done: + switch (ch) { + case LF: + goto header_done; + default: + return NGX_ERROR; + } + } + } + + b->pos = p; + hp->state = state; + + return NGX_AGAIN; + +done: + + b->pos = p + 1; + hp->state = sw_start; + + return NGX_OK; + +header_done: + + b->pos = p + 1; + hp->state = sw_start; + + return NGX_DONE; +} + + +#define \ +ngx_size_is_sufficient(cs) \ + (cs < ((__typeof__(cs)) 1 << (sizeof(cs) * 8 - 4))) + + +#define NGX_QJS_HTTP_CHUNK_MIDDLE 0 +#define NGX_QJS_HTTP_CHUNK_ON_BORDER 1 +#define NGX_QJS_HTTP_CHUNK_END 2 + + +static ngx_int_t +ngx_qjs_http_chunk_buffer(ngx_qjs_http_chunk_parse_t *hcp, ngx_buf_t *b, + njs_chb_t *chain) +{ + size_t size; + + size = b->last - hcp->pos; + + if (hcp->chunk_size < size) { + njs_chb_append(chain, hcp->pos, hcp->chunk_size); + hcp->pos += hcp->chunk_size; + + return NGX_QJS_HTTP_CHUNK_END; + } + + njs_chb_append(chain, hcp->pos, size); + hcp->pos += size; + + hcp->chunk_size -= size; + + if (hcp->chunk_size == 0) { + return NGX_QJS_HTTP_CHUNK_ON_BORDER; + } + + return NGX_QJS_HTTP_CHUNK_MIDDLE; +} + + +static ngx_int_t +ngx_qjs_http_parse_chunked(ngx_qjs_http_chunk_parse_t *hcp, ngx_buf_t *b, + njs_chb_t *chain) +{ + u_char c, ch; + ngx_int_t rc; + + enum { + sw_start = 0, + sw_chunk_size, + sw_chunk_size_linefeed, + sw_chunk_end_newline, + sw_chunk_end_linefeed, + sw_chunk, + } state; + + state = hcp->state; + + hcp->pos = b->pos; + + while (hcp->pos < b->last) { + /* + * The sw_chunk state is tested outside the switch + * to preserve hcp->pos and to not touch memory. + */ + if (state == sw_chunk) { + rc = ngx_qjs_http_chunk_buffer(hcp, b, chain); + if (rc == NGX_ERROR) { + return rc; + } + + if (rc == NGX_QJS_HTTP_CHUNK_MIDDLE) { + break; + } + + state = sw_chunk_end_newline; + + if (rc == NGX_QJS_HTTP_CHUNK_ON_BORDER) { + break; + } + + /* rc == NGX_JS_HTTP_CHUNK_END */ + } + + ch = *hcp->pos++; + + switch (state) { + + case sw_start: + state = sw_chunk_size; + + c = ch - '0'; + + if (c <= 9) { + hcp->chunk_size = c; + continue; + } + + c = (ch | 0x20) - 'a'; + + if (c <= 5) { + hcp->chunk_size = 0x0A + c; + continue; + } + + return NGX_ERROR; + + case sw_chunk_size: + + c = ch - '0'; + + if (c > 9) { + c = (ch | 0x20) - 'a'; + + if (c <= 5) { + c += 0x0A; + + } else if (ch == '\r') { + state = sw_chunk_size_linefeed; + continue; + + } else { + return NGX_ERROR; + } + } + + if (ngx_size_is_sufficient(hcp->chunk_size)) { + hcp->chunk_size = (hcp->chunk_size << 4) + c; + continue; + } + + return NGX_ERROR; + + case sw_chunk_size_linefeed: + if (ch == '\n') { + + if (hcp->chunk_size != 0) { + state = sw_chunk; + continue; + } + + hcp->last = 1; + state = sw_chunk_end_newline; + continue; + } + + return NGX_ERROR; + + case sw_chunk_end_newline: + if (ch == '\r') { + state = sw_chunk_end_linefeed; + continue; + } + + return NGX_ERROR; + + case sw_chunk_end_linefeed: + if (ch == '\n') { + + if (!hcp->last) { + state = sw_start; + continue; + } + + return NGX_OK; + } + + return NGX_ERROR; + + case sw_chunk: + /* + * This state is processed before the switch. + * It added here just to suppress a warning. + */ + continue; + } + } + + hcp->state = state; + + return NGX_AGAIN; +} + + +static ngx_int_t +ngx_qjs_http_process_body(ngx_qjs_http_t *http) +{ + ssize_t size, chsize, need; + ngx_int_t rc; + ngx_buf_t *b; + + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, + "qjs fetch process body done:%ui", (ngx_uint_t) http->done); + + if (http->done) { + size = njs_chb_size(&http->response.chain); + if (size < 0) { + ngx_qjs_http_error(http, 0, "memory error"); + return NGX_ERROR; + } + + if (!http->header_only + && http->http_parse.chunked + && http->http_parse.content_length_n == -1) + { + ngx_qjs_http_error(http, 0, "invalid fetch chunked response"); + return NGX_ERROR; + } + + if (http->header_only + || http->http_parse.content_length_n == -1 + || size == http->http_parse.content_length_n) + { + http->response_value = JS_NewObjectClass(http->cx, + NGX_QJS_CLASS_ID_FETCH_RESPONSE); + if (JS_IsException(http->response_value)) { + return NGX_ERROR; + } + + JS_SetOpaque(http->response_value, &http->response); + + ngx_qjs_http_fetch_done(http, http->response_value, NJS_OK); + return NGX_DONE; + } + + if (size < http->http_parse.content_length_n) { + return NGX_AGAIN; + } + + ngx_qjs_http_error(http, 0, "fetch trailing data"); + return NGX_ERROR; + } + + b = http->buffer; + + if (http->http_parse.chunked) { + rc = ngx_qjs_http_parse_chunked(&http->http_chunk_parse, b, + &http->response.chain); + if (rc == NGX_ERROR) { + ngx_qjs_http_error(http, 0, "invalid fetch chunked response"); + return NGX_ERROR; + } + + size = njs_chb_size(&http->response.chain); + + if (rc == NGX_OK) { + http->http_parse.content_length_n = size; + } + + if (size > http->max_response_body_size * 10) { + ngx_qjs_http_error(http, 0, "very large fetch chunked response"); + return NGX_ERROR; + } + + b->pos = http->http_chunk_parse.pos; + + } else { + size = njs_chb_size(&http->response.chain); + + if (http->header_only) { + need = 0; + + } else if (http->http_parse.content_length_n == -1) { + need = http->max_response_body_size - size; + + } else { + need = http->http_parse.content_length_n - size; + } + + chsize = ngx_min(need, b->last - b->pos); + + if (size + chsize > http->max_response_body_size) { + ngx_qjs_http_error(http, 0, "fetch response body is too large"); + return NGX_ERROR; + } + + if (chsize > 0) { + njs_chb_append(&http->response.chain, b->pos, chsize); + b->pos += chsize; + } + + rc = (need > chsize) ? NGX_AGAIN : NGX_DONE; + } + + if (b->pos == b->end) { + if (http->chunk == NULL) { + b = ngx_create_temp_buf(http->pool, http->buffer_size); + if (b == NULL) { + ngx_qjs_http_error(http, 0, "memory error"); + return NGX_ERROR; + } + + http->buffer = b; + http->chunk = b; + + } else { + b->last = b->start; + b->pos = b->start; + } + } + + return rc; +} + + +static ngx_int_t +ngx_qjs_http_process_headers(ngx_qjs_http_t *http) +{ + size_t len, vlen; + ngx_int_t rc; + njs_int_t ret; + ngx_qjs_http_parse_t *hp; + + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, http->log, 0, + "qjs fetch process headers"); + + hp = &http->http_parse; + + if (http->response.headers.header_list.size == 0) { + rc = ngx_list_init(&http->response.headers.header_list, http->pool, 4, + sizeof(ngx_qjs_tb_elt_t)); + if (rc != NGX_OK) { + ngx_qjs_http_error(http, 0, "alloc failed"); + return NGX_ERROR; + } + } + + for ( ;; ) { + rc = ngx_qjs_http_parse_header_line(hp, http->buffer); + + if (rc == NGX_OK) { + len = hp->header_name_end - hp->header_name_start; + vlen = hp->header_end - hp->header_start; + + ret = ngx_qjs_headers_append(http->cx, &http->response.headers, + hp->header_name_start, len, + hp->header_start, vlen); + + if (ret == NJS_ERROR) { + ngx_qjs_http_error(http, 0, "cannot add respose header"); + return NGX_ERROR; + } + + ngx_log_debug4(NGX_LOG_DEBUG_EVENT, http->log, 0, + "qjs fetch header \"%*s: %*s\"", + len, hp->header_name_start, vlen, hp->header_start); + + if (len == njs_strlen("Transfer-Encoding") + && vlen == njs_strlen("chunked") + && ngx_strncasecmp(hp->header_name_start, + (u_char *) "Transfer-Encoding", len) == 0 + && ngx_strncasecmp(hp->header_start, (u_char *) "chunked", + vlen) == 0) + { + hp->chunked = 1; + } + + if (len == njs_strlen("Content-Length") + && ngx_strncasecmp(hp->header_name_start, + (u_char *) "Content-Length", len) == 0) + { + hp->content_length_n = ngx_atoof(hp->header_start, vlen); + if (hp->content_length_n == NGX_ERROR) { + ngx_qjs_http_error(http, 0, "invalid fetch content length"); + return NGX_ERROR; + } + + if (!http->header_only + && hp->content_length_n + > (off_t) http->max_response_body_size) + { + ngx_qjs_http_error(http, 0, + "fetch content length is too large"); + return NGX_ERROR; + } + } + + continue; + } + + if (rc == NGX_DONE) { + http->response.headers.guard = GUARD_IMMUTABLE; + break; + } + + if (rc == NGX_AGAIN) { + return NGX_AGAIN; + } + + /* rc == NGX_ERROR */ + + ngx_qjs_http_error(http, 0, "invalid fetch header"); + + return NGX_ERROR; + } + + http->process = ngx_qjs_http_process_body; + + return http->process(http); +} + + +static ngx_int_t +ngx_qjs_http_process_status_line(ngx_qjs_http_t *http) +{ + ngx_int_t rc; + ngx_qjs_http_parse_t *hp; + + hp = &http->http_parse; + + rc = ngx_qjs_http_parse_status_line(hp, http->buffer); + + if (rc == NGX_OK) { + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, + "qjs fetch status %ui", hp->code); + + http->response.code = hp->code; + http->response.status_text.data = hp->status_text; + http->response.status_text.len = hp->status_text_end - hp->status_text; + http->process = ngx_qjs_http_process_headers; + + return http->process(http); + } + + if (rc == NGX_AGAIN) { + return NGX_AGAIN; + } + + /* rc == NGX_ERROR */ + + ngx_qjs_http_error(http, 0, "invalid fetch status line"); + + return NGX_ERROR; +} + + +static void +ngx_qjs_http_close_connection(ngx_connection_t *c) +{ + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, + "qjs fetch close connection: %d", c->fd); + +#if (NGX_SSL) + if (c->ssl) { + c->ssl->no_wait_shutdown = 1; + + if (ngx_ssl_shutdown(c) == NGX_AGAIN) { + c->ssl->handler = ngx_qjs_http_close_connection; + return; + } + } +#endif + + c->destroyed = 1; + + ngx_close_connection(c); +} + + +static void +ngx_qjs_http_connect(ngx_qjs_http_t *http) +{ + ngx_int_t rc; + ngx_addr_t *addr; + + addr = &http->addrs[http->naddr]; + + ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0, + "qjs fetch connect %ui/%ui", http->naddr, http->naddrs); + + http->peer.sockaddr = addr->sockaddr; + http->peer.socklen = addr->socklen; + http->peer.name = &addr->name; + http->peer.get = ngx_event_get_peer; + http->peer.log = http->log; + http->peer.log_error = NGX_ERROR_ERR; + + rc = ngx_event_connect_peer(&http->peer); + + if (rc == NGX_ERROR) { + ngx_qjs_http_error(http, 0, "connect failed"); + return; + } + + if (rc == NGX_BUSY || rc == NGX_DECLINED) { + ngx_qjs_http_next(http); + return; + } + + http->peer.connection->data = http; + http->peer.connection->pool = http->pool; + + http->peer.connection->write->handler = ngx_qjs_http_write_handler; + http->peer.connection->read->handler = ngx_qjs_http_read_handler; + + http->process = ngx_qjs_http_process_status_line; + + ngx_add_timer(http->peer.connection->read, http->timeout); + ngx_add_timer(http->peer.connection->write, http->timeout); + +#if (NGX_SSL) + if (http->ssl != NULL && http->peer.connection->ssl == NULL) { + ngx_qjs_http_ssl_init_connection(http); + return; + } +#endif + + if (rc == NGX_OK) { + ngx_qjs_http_write_handler(http->peer.connection->write); + } +} + + +static void +ngx_qjs_http_fetch_done(ngx_qjs_http_t *http, JSValue retval, njs_int_t rc) +{ + void *external; + JSValue action; + JSContext *cx; + ngx_js_ctx_t *ctx; + ngx_qjs_event_t *event; + + ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0, + "qjs fetch done http:%p rc:%i", http, (ngx_int_t) rc); + + if (http->peer.connection != NULL) { + ngx_qjs_http_close_connection(http->peer.connection); + http->peer.connection = NULL; + } + + if (http->event != NULL) { + action = http->promise_callbacks[(rc != NJS_OK)]; + + cx = http->cx; + event = http->event; + + rc = ngx_qjs_call(cx, action, &retval, 1); + + external = JS_GetContextOpaque(cx); + ctx = ngx_qjs_external_ctx(cx, external); + ngx_js_del_event(ctx, event); + + ngx_qjs_external_event_finalize(cx)(external, rc); + } +} + + +static void +ngx_qjs_resolve_handler(ngx_resolver_ctx_t *ctx) +{ + u_char *p; + size_t len; + socklen_t socklen; + ngx_uint_t i; + ngx_qjs_http_t *http; + struct sockaddr *sockaddr; + + http = ctx->data; + + if (ctx->state) { + ngx_qjs_http_error(http, 0, "\"%.*s\" could not be resolved (%i: %s)", + (int) ctx->name.len, ctx->name.data, (int) ctx->state, + ngx_resolver_strerror(ctx->state)); + return; + } + + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, + "http fetch resolved: \"%V\"", &ctx->name); + +#if (NGX_DEBUG) + { + u_char text[NGX_SOCKADDR_STRLEN]; + ngx_str_t addr; + ngx_uint_t i; + + addr.data = text; + + for (i = 0; i < ctx->naddrs; i++) { + addr.len = ngx_sock_ntop(ctx->addrs[i].sockaddr, ctx->addrs[i].socklen, + text, NGX_SOCKADDR_STRLEN, 0); + + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, + "name was resolved to \"%V\"", &addr); + } + } +#endif + + http->naddrs = ctx->naddrs; + http->addrs = ngx_pcalloc(http->pool, http->naddrs * sizeof(ngx_addr_t)); + + if (http->addrs == NULL) { + goto failed; + } + + for (i = 0; i < ctx->naddrs; i++) { + socklen = ctx->addrs[i].socklen; + + sockaddr = ngx_palloc(http->pool, socklen); + if (sockaddr == NULL) { + goto failed; + } + + ngx_memcpy(sockaddr, ctx->addrs[i].sockaddr, socklen); + ngx_inet_set_port(sockaddr, http->port); + + http->addrs[i].sockaddr = sockaddr; + http->addrs[i].socklen = socklen; + + p = ngx_pnalloc(http->pool, NGX_SOCKADDR_STRLEN); + if (p == NULL) { + goto failed; + } + + len = ngx_sock_ntop(sockaddr, socklen, p, NGX_SOCKADDR_STRLEN, 1); + http->addrs[i].name.len = len; + http->addrs[i].name.data = p; + } + + ngx_resolve_name_done(ctx); + http->ctx = NULL; + + ngx_qjs_http_connect(http); + + return; + +failed: + + ngx_qjs_http_error(http, 0, "memory error"); +} + + +#if (NGX_SSL) + +static void +ngx_qjs_http_ssl_init_connection(ngx_qjs_http_t *http) +{ + ngx_int_t rc; + ngx_connection_t *c; + + c = http->peer.connection; + + ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0, + "qjs fetch secure connect %ui/%ui", http->naddr, + http->naddrs); + + if (ngx_ssl_create_connection(http->ssl, c, NGX_SSL_BUFFER|NGX_SSL_CLIENT) + != NGX_OK) + { + ngx_qjs_http_error(http, 0, "failed to create ssl connection"); + return; + } + + c->sendfile = 0; + + if (ngx_qjs_http_ssl_name(http) != NGX_OK) { + ngx_qjs_http_error(http, 0, "failed to create ssl connection"); + return; + } + + c->log->action = "SSL handshaking to fetch target"; + + rc = ngx_ssl_handshake(c); + + if (rc == NGX_AGAIN) { + c->data = http; + c->ssl->handler = ngx_qjs_http_ssl_handshake_handler; + return; + } + + ngx_qjs_http_ssl_handshake(http); +} + + +static njs_int_t +ngx_qjs_http_ssl_name(ngx_qjs_http_t *http) +{ +#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME + u_char *p; + + /* as per RFC 6066, literal IPv4 and IPv6 addresses are not permitted */ + ngx_str_t *name = &http->tls_name; + + if (name->len == 0 || *name->data == '[') { + goto done; + } + + if (ngx_inet_addr(name->data, name->len) != INADDR_NONE) { + goto done; + } + + /* + * SSL_set_tlsext_host_name() needs a null-terminated string, + * hence we explicitly null-terminate name here + */ + + p = ngx_pnalloc(http->pool, name->len + 1); + if (p == NULL) { + return NGX_ERROR; + } + + (void) ngx_cpystrn(p, name->data, name->len + 1); + + name->data = p; + + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, + "qjs fetch SSL server name: \"%s\"", name->data); + + if (SSL_set_tlsext_host_name(http->peer.connection->ssl->connection, + (char *) name->data) + == 0) + { + ngx_ssl_error(NGX_LOG_ERR, http->log, 0, + "SSL_set_tlsext_host_name(\"%s\") failed", name->data); + return NGX_ERROR; + } + +#endif + +done: + + return NJS_OK; +} + + +static void +ngx_qjs_http_ssl_handshake_handler(ngx_connection_t *c) +{ + ngx_qjs_http_t *http; + + http = c->data; + + http->peer.connection->write->handler = ngx_qjs_http_write_handler; + http->peer.connection->read->handler = ngx_qjs_http_read_handler; + + ngx_qjs_http_ssl_handshake(http); +} + + +static void +ngx_qjs_http_ssl_handshake(ngx_qjs_http_t *http) +{ + long rc; + ngx_connection_t *c; + + c = http->peer.connection; + + if (c->ssl->handshaked) { + if (http->ssl_verify) { + rc = SSL_get_verify_result(c->ssl->connection); + + if (rc != X509_V_OK) { + ngx_log_error(NGX_LOG_ERR, c->log, 0, + "js fetch SSL certificate verify error: (%l:%s)", + rc, X509_verify_cert_error_string(rc)); + goto failed; + } + + if (ngx_ssl_check_host(c, &http->tls_name) != NGX_OK) { + ngx_log_error(NGX_LOG_ERR, c->log, 0, + "js fetch SSL certificate does not match \"%V\"", + &http->tls_name); + goto failed; + } + } + + c->write->handler = ngx_qjs_http_write_handler; + c->read->handler = ngx_qjs_http_read_handler; + + if (c->read->ready) { + ngx_post_event(c->read, &ngx_posted_events); + } + + http->process = ngx_qjs_http_process_status_line; + ngx_qjs_http_write_handler(c->write); + + return; + } + +failed: + + ngx_qjs_http_next(http); +} + +#endif + + +JSValue +ngx_qjs_ext_fetch(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + void *external; + JSValue init, value; + njs_int_t ret; + njs_str_t str; + ngx_url_t u; + ngx_uint_t i; + njs_bool_t has_host; + ngx_pool_t *pool; + ngx_qjs_http_t *http; + ngx_list_part_t *part; + ngx_qjs_tb_elt_t *h; + ngx_connection_t *c; + ngx_qjs_request_t request; + ngx_resolver_ctx_t *ctx; + + external = JS_GetContextOpaque(cx); + c = ngx_qjs_external_connection(cx, external); + pool = ngx_qjs_external_pool(cx, external); + + http = ngx_qjs_http_alloc(cx, pool, c->log); + if (http == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + ret = ngx_qjs_request_constructor(cx, &request, &u, argc, argv); + if (ret != NJS_OK) { + goto fail; + } + + http->response.url = request.url; + http->timeout = ngx_qjs_external_fetch_timeout(cx, external); + http->buffer_size = ngx_qjs_external_buffer_size(cx, external); + http->max_response_body_size = + ngx_qjs_external_max_response_buffer_size(cx, external); + +#if (NGX_SSL) + if (u.default_port == 443) { + http->ssl = ngx_qjs_external_ssl(cx, external); + http->ssl_verify = ngx_qjs_external_ssl_verify(cx, external); + } +#endif + + if (argc > 1 && JS_IsObject(argv[1])) { + init = argv[1]; + value = JS_GetPropertyStr(cx, init, "buffer_size"); + if (JS_IsException(value)) { + goto fail; + } + + if (!JS_IsUndefined(value)) { + ret = JS_ToInt64(cx, &http->buffer_size, value); + JS_FreeValue(cx, value); + + if (ret < 0) { + goto fail; + } + } + + value = JS_GetPropertyStr(cx, init, "max_response_body_size"); + if (JS_IsException(value)) { + goto fail; + } + + if (!JS_IsUndefined(value)) { + ret = JS_ToInt64(cx, &http->max_response_body_size, value); + JS_FreeValue(cx, value); + + if (ret < 0) { + goto fail; + } + } + +#if (NGX_SSL) + value = JS_GetPropertyStr(cx, init, "verify"); + if (JS_IsException(value)) { + goto fail; + } + + if (!JS_IsUndefined(value)) { + http->ssl_verify = JS_ToBool(cx, value); + } +#endif + } + + str.start = request.method.data; + str.length = request.method.len; + http->header_only = njs_strstr_eq(&str, &njs_str_value("HEAD")); + + NJS_CHB_CTX_INIT(&http->chain, cx); + NJS_CHB_CTX_INIT(&http->response.chain, cx); + + njs_chb_append(&http->chain, request.method.data, request.method.len); + njs_chb_append_literal(&http->chain, " "); + + if (u.uri.len == 0 || u.uri.data[0] != '/') { + njs_chb_append_literal(&http->chain, "/"); + } + + njs_chb_append(&http->chain, u.uri.data, u.uri.len); + njs_chb_append_literal(&http->chain, " HTTP/1.1" CRLF); + + has_host = 0; + part = &request.headers.header_list.part; + h = part->elts; + + for (i = 0; /* void */; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + h = part->elts; + i = 0; + } + + if (h[i].hash == 0) { + continue; + } + + if (h[i].key.len == 4 + && ngx_strncasecmp(h[i].key.data, (u_char *) "Host", 4) == 0) + { + has_host = 1; + njs_chb_append_literal(&http->chain, "Host: "); + njs_chb_append(&http->chain, h[i].value.data, h[i].value.len); + njs_chb_append_literal(&http->chain, CRLF); + break; + } + } + + if (!has_host) { + njs_chb_append_literal(&http->chain, "Host: "); + njs_chb_append(&http->chain, u.host.data, u.host.len); + + if (!u.no_port) { + njs_chb_sprintf(&http->chain, 32, ":%d", u.port); + } + + njs_chb_append_literal(&http->chain, CRLF); + } + + part = &request.headers.header_list.part; + h = part->elts; + + for (i = 0; /* void */; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + h = part->elts; + i = 0; + } + + if (h[i].hash == 0) { + continue; + } + + if (h[i].key.len == 4 + && ngx_strncasecmp(h[i].key.data, (u_char *) "Host", 4) == 0) + { + continue; + } + + njs_chb_append(&http->chain, h[i].key.data, h[i].key.len); + njs_chb_append_literal(&http->chain, ": "); + njs_chb_append(&http->chain, h[i].value.data, h[i].value.len); + njs_chb_append_literal(&http->chain, CRLF); + } + + njs_chb_append_literal(&http->chain, "Connection: close" CRLF); + +#if (NGX_SSL) + http->tls_name.data = u.host.data; + http->tls_name.len = u.host.len; +#endif + + if (request.body.len != 0) { + njs_chb_sprintf(&http->chain, 32, "Content-Length: %uz" CRLF CRLF, + request.body.len); + njs_chb_append(&http->chain, request.body.data, request.body.len); + + } else { + njs_chb_append_literal(&http->chain, CRLF); + } + + if (u.addrs == NULL) { + ctx = ngx_resolve_start(ngx_qjs_external_resolver(cx, external), NULL); + if (ctx == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + if (ctx == NGX_NO_RESOLVER) { + JS_ThrowInternalError(cx, "no resolver defined"); + goto fail; + } + + http->ctx = ctx; + http->port = u.port; + + ctx->name = u.host; + ctx->handler = ngx_qjs_resolve_handler; + ctx->data = http; + ctx->timeout = ngx_qjs_external_resolver_timeout(cx, external); + + ret = ngx_resolve_name(http->ctx); + if (ret != NGX_OK) { + http->ctx = NULL; + return JS_ThrowOutOfMemory(cx); + } + + return JS_DupValue(cx, http->promise); + } + + http->naddrs = 1; + ngx_memcpy(&http->addr, &u.addrs[0], sizeof(ngx_addr_t)); + http->addrs = &http->addr; + + ngx_qjs_http_connect(http); + + return JS_DupValue(cx, http->promise); + +fail: + + http->response_value = JS_GetException(cx); + + ngx_qjs_http_fetch_done(http, http->response_value, NJS_ERROR); + + return JS_DupValue(cx, http->promise); +} + + +static int +ngx_qjs_fetch_headers_own_property(JSContext *cx, JSPropertyDescriptor *desc, + JSValueConst obj, JSAtom prop) +{ + JSValue value; + ngx_str_t name; + + name.data = (u_char *) JS_AtomToCString(cx, prop); + if (name.data == NULL) { + return -1; + } + + name.len = ngx_strlen(name.data); + + value = ngx_qjs_headers_get(cx, obj, &name, 0); + JS_FreeCString(cx, (char *) name.data); + + if (JS_IsException(value)) { + return -1; + } + + if (JS_IsNull(value)) { + return 0; + } + + if (desc == NULL) { + JS_FreeValue(cx, value); + + } else { + desc->flags = JS_PROP_ENUMERABLE; + desc->getter = JS_UNDEFINED; + desc->setter = JS_UNDEFINED; + desc->value = value; + } + + return 1; +} + + +static int +ngx_qjs_fetch_headers_own_property_names(JSContext *cx, JSPropertyEnum **ptab, + uint32_t *plen, JSValueConst obj) +{ + int ret; + JSAtom key; + JSValue keys; + ngx_uint_t i; + ngx_list_part_t *part; + ngx_qjs_tb_elt_t *h; + ngx_qjs_headers_t *headers; + + headers = JS_GetOpaque2(cx, obj, NGX_QJS_CLASS_ID_FETCH_HEADERS); + if (headers == NULL) { + (void) JS_ThrowInternalError(cx, "\"this\" is not a Headers object"); + return -1; + } + + keys = JS_NewObject(cx); + if (JS_IsException(keys)) { + return -1; + } + + part = &headers->header_list.part; + h = part->elts; + + for (i = 0; /* void */; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + h = part->elts; + i = 0; + } + + if (h[i].hash == 0) { + continue; + } + + key = JS_NewAtomLen(cx, (const char *) h[i].key.data, h[i].key.len); + if (key == JS_ATOM_NULL) { + goto fail; + } + + if (JS_DefinePropertyValue(cx, keys, key, JS_UNDEFINED, + JS_PROP_ENUMERABLE) < 0) + { + JS_FreeAtom(cx, key); + goto fail; + } + + JS_FreeAtom(cx, key); + } + + ret = JS_GetOwnPropertyNames(cx, ptab, plen, keys, JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY); + + JS_FreeValue(cx, keys); + + return ret; + +fail: + + JS_FreeValue(cx, keys); + + return -1; +} + + +static void +ngx_qjs_fetch_request_finalizer(JSRuntime *rt, JSValue val) +{ + ngx_qjs_request_t *request; + + request = JS_GetOpaque(val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + + JS_FreeValueRT(rt, request->header_value); +} + + +static const JSClassDef ngx_qjs_fetch_headers_class = { + "Headers", + .finalizer = NULL, + .exotic = & (JSClassExoticMethods) { + .get_own_property = ngx_qjs_fetch_headers_own_property, + .get_own_property_names = ngx_qjs_fetch_headers_own_property_names, + }, +}; + +static const JSClassDef ngx_qjs_fetch_request_class = { + "Request", + .finalizer = ngx_qjs_fetch_request_finalizer, +}; + + +static void +ngx_qjs_fetch_response_finalizer(JSRuntime *rt, JSValue val) +{ + ngx_qjs_response_t *response; + + response = JS_GetOpaque(val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + + njs_chb_destroy(&response->chain); + + JS_FreeValueRT(rt, response->header_value); +} + +static const JSClassDef ngx_qjs_fetch_response_class = { + "Response", + .finalizer = ngx_qjs_fetch_response_finalizer, +}; + + +static JSValue +ngx_qjs_headers_get(JSContext *cx, JSValue this_val, ngx_str_t *name, + njs_bool_t as_array) +{ + int ret; + JSValue retval, value; + njs_chb_t chain; + ngx_uint_t i; + ngx_list_part_t *part; + ngx_qjs_tb_elt_t *h, *ph; + ngx_qjs_headers_t *headers; + + headers = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_HEADERS); + if (headers == NULL) { + return JS_NULL; + } + + part = &headers->header_list.part; + h = part->elts; + ph = NULL; + + for (i = 0; /* void */; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + h = part->elts; + i = 0; + } + + if (h[i].hash == 0) { + continue; + } + + if (h[i].key.len == name->len + && njs_strncasecmp(h[i].key.data, name->data, name->len) == 0) + { + ph = &h[i]; + break; + } + } + + if (as_array) { + retval = JS_NewArray(cx); + if (JS_IsException(retval)) { + return JS_EXCEPTION; + } + + i = 0; + while (ph != NULL) { + value = JS_NewStringLen(cx, (const char *) ph->value.data, + ph->value.len); + if (JS_IsException(value)) { + JS_FreeValue(cx, retval); + return JS_EXCEPTION; + } + + ret = JS_DefinePropertyValueUint32(cx, retval, i, value, + JS_PROP_C_W_E); + if (ret < 0) { + JS_FreeValue(cx, retval); + JS_FreeValue(cx, value); + return JS_EXCEPTION; + } + + i++; + ph = ph->next; + } + + return retval; + } + + if (ph == NULL) { + return JS_NULL; + } + + NJS_CHB_CTX_INIT(&chain, cx); + + h = ph; + + for ( ;; ) { + njs_chb_append(&chain, h->value.data, h->value.len); + + if (h->next == NULL) { + break; + } + + njs_chb_append_literal(&chain, ", "); + h = h->next; + } + + retval = qjs_string_create_chb(cx, &chain); + + return retval; +} + + +static JSValue +ngx_qjs_ext_fetch_headers_get(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv, int magic) +{ + njs_int_t ret; + ngx_str_t name; + + ret = ngx_qjs_string(cx, argv[0], &name); + if (ret != NJS_OK) { + return JS_EXCEPTION; + } + + return ngx_qjs_headers_get(cx, this_val, &name, magic); +} + + +static JSValue +ngx_qjs_ext_fetch_headers_has(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + JSValue retval; + njs_int_t ret; + ngx_str_t name; + + ret = ngx_qjs_string(cx, argv[0], &name); + if (ret != NJS_OK) { + return JS_EXCEPTION; + } + + retval = ngx_qjs_headers_get(cx, this_val, &name, 0); + if (JS_IsException(retval)) { + return JS_EXCEPTION; + } + + ret = !JS_IsNull(retval); + JS_FreeValue(cx, retval); + + return JS_NewBool(cx, ret); +} + + +static JSValue +ngx_qjs_ext_fetch_headers_set(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + njs_int_t ret; + ngx_str_t name, value; + ngx_uint_t i; + ngx_list_part_t *part; + ngx_qjs_tb_elt_t *h, **ph, **pp; + ngx_qjs_headers_t *headers; + + headers = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_HEADERS); + if (headers == NULL) { + return JS_ThrowInternalError(cx, + "\"this\" is not fetch headers object"); + } + + ret = ngx_qjs_string(cx, argv[0], &name); + if (ret != NJS_OK) { + return JS_EXCEPTION; + } + + ret = ngx_qjs_string(cx, argv[1], &value); + if (ret != NJS_OK) { + return JS_EXCEPTION; + } + + part = &headers->header_list.part; + h = part->elts; + + for (i = 0; /* void */; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + h = part->elts; + i = 0; + } + + if (h[i].hash == 0) { + continue; + } + + if (name.len == h[i].key.len + && (njs_strncasecmp(name.data, h[i].key.data, name.len) == 0)) + { + h[i].value.len = value.len; + h[i].value.data = value.data; + + ph = &h[i].next; + + while (*ph) { + pp = ph; + ph = &(*ph)->next; + *pp = NULL; + } + + return JS_UNDEFINED; + } + } + + ret = ngx_qjs_headers_append(cx, headers, name.data, name.len, + value.data, value.len); + if (ret != NJS_OK) { + return JS_EXCEPTION; + } + + return JS_UNDEFINED; +} + + +static JSValue +ngx_qjs_ext_fetch_headers_append(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + njs_int_t ret; + ngx_qjs_headers_t *headers; + + headers = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_HEADERS); + if (headers == NULL) { + return JS_ThrowInternalError(cx, + "\"this\" is not fetch headers object"); + } + + ret = ngx_qjs_headers_fill_header_free(cx, headers, + JS_DupValue(cx, argv[0]), + JS_DupValue(cx, argv[1])); + if (ret != NJS_OK) { + return JS_EXCEPTION; + } + + return JS_UNDEFINED; +} + + +static JSValue +ngx_qjs_ext_fetch_headers_delete(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + njs_int_t ret; + ngx_str_t name; + ngx_uint_t i; + ngx_list_part_t *part; + ngx_qjs_tb_elt_t *h; + ngx_qjs_headers_t *headers; + + headers = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_HEADERS); + if (headers == NULL) { + return JS_ThrowInternalError(cx, + "\"this\" is not fetch headers object"); + } + + ret = ngx_qjs_string(cx, argv[0], &name); + if (ret != NJS_OK) { + return JS_EXCEPTION; + } + + part = &headers->header_list.part; + h = part->elts; + + for (i = 0; /* void */; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + h = part->elts; + i = 0; + } + + if (h[i].hash == 0) { + continue; + } + + if (name.len == h[i].key.len + && (njs_strncasecmp(name.data, h[i].key.data, name.len) == 0)) + { + h[i].hash = 0; + } + } + + if (name.len == njs_strlen("Content-Type") + && ngx_strncasecmp(name.data, (u_char *) "Content-Type", name.len) + == 0) + { + headers->content_type = NULL; + } + + return JS_UNDEFINED; +} + + +static JSValue +ngx_qjs_headers_ext_keys(JSContext *cx, JSValue value) +{ + int ret; + uint32_t length; + JSValue keys, key, item, func, retval; + njs_str_t hdr; + njs_bool_t found; + ngx_uint_t i, k, n; + ngx_list_part_t *part; + ngx_qjs_tb_elt_t *h; + ngx_qjs_headers_t *headers; + + headers = JS_GetOpaque2(cx, value, NGX_QJS_CLASS_ID_FETCH_HEADERS); + if (headers == NULL) { + return JS_NULL; + } + + keys = JS_NewArray(cx); + if (JS_IsException(keys)) { + return JS_EXCEPTION; + } + + n = 0; + + part = &headers->header_list.part; + h = part->elts; + + for (i = 0; /* void */; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + h = part->elts; + i = 0; + } + + if (h[i].hash == 0) { + continue; + } + + if (ngx_qjs_array_length(cx, &length, keys)) { + goto fail; + } + + for (k = 0; k < length; k++) { + key = JS_GetPropertyUint32(cx, keys, k); + if (JS_IsException(key)) { + goto fail; + } + + hdr.start = (u_char *) JS_ToCStringLen(cx, &hdr.length, key); + JS_FreeValue(cx, key); + + found = h[i].key.len == hdr.length + && njs_strncasecmp(h[i].key.data, + hdr.start, hdr.length) == 0; + + JS_FreeCString(cx, (const char *) hdr.start); + + if (found) { + break; + } + } + + if (k == n) { + item = JS_NewStringLen(cx, (const char *) h[i].key.data, + h[i].key.len); + if (JS_IsException(value)) { + goto fail; + } + + ret = JS_DefinePropertyValueUint32(cx, keys, n, item, + JS_PROP_C_W_E); + if (ret < 0) { + JS_FreeValue(cx, item); + goto fail; + } + + n++; + } + } + + func = JS_GetPropertyStr(cx, keys, "sort"); + + retval = JS_Call(cx, func, keys, 0, NULL); + + JS_FreeValue(cx, func); + JS_FreeValue(cx, keys); + + return retval; + +fail: + + JS_FreeValue(cx, keys); + + return JS_EXCEPTION; +} + + +static JSValue +ngx_qjs_ext_fetch_headers_foreach(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + int ret; + JSValue callback, keys, key; + JSValue header, retval, arguments[2]; + uint32_t length;; + ngx_str_t name; + ngx_uint_t i; + ngx_qjs_headers_t *headers; + + headers = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_HEADERS); + if (headers == NULL) { + return JS_ThrowInternalError(cx, + "\"this\" is not fetch headers object"); + } + + callback = argv[0]; + + if (!JS_IsFunction(cx, callback)) { + return JS_ThrowInternalError(cx, "\"callback\" is not a function"); + } + + keys = ngx_qjs_headers_ext_keys(cx, this_val); + if (JS_IsException(keys)) { + return JS_EXCEPTION; + } + + if (ngx_qjs_array_length(cx, &length, keys)) { + goto fail; + } + + for (i = 0; i < length; i++) { + key = JS_GetPropertyUint32(cx, keys, i); + if (JS_IsException(key)) { + goto fail; + } + + ret = ngx_qjs_string(cx, key, &name); + if (ret != NJS_OK) { + JS_FreeValue(cx, key); + goto fail; + } + + header = ngx_qjs_headers_get(cx, this_val, &name, 0); + if (JS_IsException(header)) { + JS_FreeValue(cx, key); + goto fail; + } + + arguments[0] = key; + arguments[1] = header; + + retval = JS_Call(cx, callback, JS_UNDEFINED, 2, arguments); + + JS_FreeValue(cx, key); + JS_FreeValue(cx, header); + JS_FreeValue(cx, retval); + } + + JS_FreeValue(cx, keys); + + return JS_UNDEFINED; + +fail: + + JS_FreeValue(cx, keys); + + return JS_EXCEPTION; +} + + +static JSValue +ngx_qjs_ext_fetch_request_field(JSContext *cx, JSValueConst this_val, int magic) +{ + ngx_str_t *field; + ngx_qjs_request_t *request; + + request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + if (request == NULL) { + return JS_UNDEFINED; + } + + field = (ngx_str_t *) ((u_char *) request + magic); + + return qjs_string_create(cx, field->data, field->len); +} + + +static JSValue +ngx_qjs_ext_fetch_request_headers(JSContext *cx, JSValueConst this_val) +{ + ngx_qjs_request_t *request; + + request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + if (request == NULL) { + return JS_UNDEFINED; + } + + if (JS_IsUndefined(request->header_value)) { + request->header_value = JS_NewObjectClass(cx, + NGX_QJS_CLASS_ID_FETCH_HEADERS); + if (JS_IsException(request->header_value)) { + return JS_ThrowInternalError(cx, "fetch header creation failed"); + } + + JS_SetOpaque(request->header_value, &request->headers); + } + + return JS_DupValue(cx, request->header_value); +} + + +static JSValue +ngx_qjs_ext_fetch_request_bodyused(JSContext *cx, JSValueConst this_val) +{ + ngx_qjs_request_t *request; + + request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + if (request == NULL) { + return JS_UNDEFINED; + } + + return JS_NewBool(cx, request->body_used); +} + + +static JSValue +ngx_qjs_ext_fetch_request_cache(JSContext *cx, JSValueConst this_val) +{ + ngx_qjs_request_t *request; + + request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + if (request == NULL) { + return JS_UNDEFINED; + } + + return ngx_qjs_fetch_flag(cx, ngx_qjs_fetch_cache_modes, + (njs_int_t) request->cache_mode); +} + + +static JSValue +ngx_qjs_ext_fetch_request_credentials(JSContext *cx, JSValueConst this_val) +{ + ngx_qjs_request_t *request; + + request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + if (request == NULL) { + return JS_UNDEFINED; + } + + return ngx_qjs_fetch_flag(cx, ngx_qjs_fetch_credentials, + (njs_int_t) request->credentials); +} + + +static JSValue +ngx_qjs_ext_fetch_request_mode(JSContext *cx, JSValueConst this_val) +{ + ngx_qjs_request_t *request; + + request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + if (request == NULL) { + return JS_UNDEFINED; + } + + return ngx_qjs_fetch_flag(cx, ngx_qjs_fetch_modes, + (njs_int_t) request->mode); +} + + +static JSValue +ngx_qjs_ext_fetch_request_body(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv, int magic) +{ + JSValue result; + ngx_qjs_request_t *request; + + request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + if (request == NULL) { + return JS_UNDEFINED; + } + + if (request->body_used) { + return JS_ThrowInternalError(cx, "body stream already read"); + } + + request->body_used = 1; + + switch (magic) { + + case NGX_QJS_BODY_TEXT: + default: + result = qjs_string_create(cx, request->body.data, request->body.len); + if (JS_IsException(result)) { + return JS_ThrowOutOfMemory(cx); + } + } + + return qjs_promise_result(cx, result); +} + + +static JSValue +ngx_qjs_ext_fetch_response_field(JSContext *cx, JSValueConst this_val, int magic) +{ + ngx_str_t *field; + ngx_qjs_response_t *response; + + response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + if (response == NULL) { + return JS_UNDEFINED; + } + + field = (ngx_str_t *) ((u_char *) response + magic); + + return qjs_string_create(cx, field->data, field->len); +} + + +static JSValue +ngx_qjs_ext_fetch_response_body(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv, int magic) +{ + JSValue result; + njs_int_t ret; + njs_str_t string; + ngx_qjs_response_t *response; + + response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + if (response == NULL) { + return JS_UNDEFINED; + } + + if (response->body_used) { + return JS_ThrowInternalError(cx, "body stream already read"); + } + + response->body_used = 1; + + ret = njs_chb_join(&response->chain, &string); + if (ret != NJS_OK) { + return JS_ThrowOutOfMemory(cx); + } + + switch (magic) { + case NGX_QJS_BODY_ARRAY_BUFFER: + result = qjs_new_array_buffer(cx, string.start, string.length); + if (JS_IsException(result)) { + return JS_ThrowOutOfMemory(cx); + } + + break; + + case NGX_QJS_BODY_JSON: + case NGX_QJS_BODY_TEXT: + default: + result = qjs_string_create(cx, string.start, string.length); + if (JS_IsException(result)) { + response->chain.free(cx, string.start); + return JS_ThrowOutOfMemory(cx); + } + + response->chain.free(cx, string.start); + + if (magic == NGX_QJS_BODY_JSON) { + string.start = (u_char *) JS_ToCStringLen(cx, &string.length, + result); + + JS_FreeValue(cx, result); + result = JS_UNDEFINED; + + if (string.start == NULL) { + JS_FreeCString(cx, (const char *) string.start); + ret = NJS_ERROR; + break; + } + + result = JS_ParseJSON(cx, (const char *) string.start, + string.length, "<input>"); + JS_FreeCString(cx, (const char *) string.start); + if (JS_IsException(result)) { + ret = NJS_ERROR; + break; + } + } + } + + return qjs_promise_result(cx, result); +} + + +static JSValue +ngx_qjs_ext_fetch_response_status(JSContext *cx, JSValueConst this_val) +{ + ngx_qjs_response_t *response; + + response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + if (response == NULL) { + return JS_UNDEFINED; + } + + return JS_NewUint32(cx, response->code); +} + + +static JSValue +ngx_qjs_ext_fetch_response_status_text(JSContext *cx, JSValueConst this_val) +{ + ngx_qjs_response_t *response; + + response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + if (response == NULL) { + return JS_UNDEFINED; + } + + return qjs_string_create(cx, response->status_text.data, + response->status_text.len); +} + + +static JSValue +ngx_qjs_ext_fetch_response_headers(JSContext *cx, JSValueConst this_val) +{ + ngx_qjs_response_t *response; + + response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + if (response == NULL) { + return JS_UNDEFINED; + } + + if (JS_IsUndefined(response->header_value)) { + response->header_value = JS_NewObjectClass(cx, + NGX_QJS_CLASS_ID_FETCH_HEADERS); + if (JS_IsException(response->header_value)) { + return JS_ThrowInternalError(cx, "fetch header creation failed"); + } + + JS_SetOpaque(response->header_value, &response->headers); + } + + return JS_DupValue(cx, response->header_value); +} + + +static JSValue +ngx_qjs_ext_fetch_response_bodyused(JSContext *cx, JSValueConst this_val) +{ + ngx_qjs_response_t *response; + + response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + if (response == NULL) { + return JS_UNDEFINED; + } + + return JS_NewBool(cx, response->body_used); +} + + +static JSValue +ngx_qjs_ext_fetch_response_ok(JSContext *cx, JSValueConst this_val) +{ + ngx_qjs_response_t *response; + + response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + if (response == NULL) { + return JS_UNDEFINED; + } + + return JS_NewBool(cx, response->code >= 200 && response->code < 300); +} + + +static JSValue +ngx_qjs_ext_fetch_response_type(JSContext *cx, JSValueConst this_val) +{ + ngx_qjs_response_t *response; + + response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + if (response == NULL) { + return JS_UNDEFINED; + } + + return JS_NewString(cx, "basic"); +} + + +static JSValue +ngx_qjs_ext_fetch_response_redirected(JSContext *cx, JSValueConst this_val) +{ + ngx_qjs_response_t *response; + + response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + if (response == NULL) { + return JS_UNDEFINED; + } + + return JS_NewBool(cx, 0); +} + + +static const JSCFunctionListEntry ngx_qjs_ext_fetch_headers_funcs[] = { + JS_CFUNC_MAGIC_DEF("get", 1, ngx_qjs_ext_fetch_headers_get, 0), + JS_CFUNC_MAGIC_DEF("getAll", 1, ngx_qjs_ext_fetch_headers_get, 1), + JS_CFUNC_DEF("has", 1, ngx_qjs_ext_fetch_headers_has), + JS_CFUNC_DEF("set", 2, ngx_qjs_ext_fetch_headers_set), + JS_CFUNC_DEF("append", 2, ngx_qjs_ext_fetch_headers_append), + JS_CFUNC_DEF("delete", 1, ngx_qjs_ext_fetch_headers_delete), + JS_CFUNC_DEF("forEach", 1, ngx_qjs_ext_fetch_headers_foreach), +}; + +static const JSCFunctionListEntry ngx_qjs_ext_fetch_request_funcs[] = { + JS_CGETSET_MAGIC_DEF("url", ngx_qjs_ext_fetch_request_field, NULL, + offsetof(ngx_qjs_request_t, url) ), + JS_CGETSET_MAGIC_DEF("method", ngx_qjs_ext_fetch_request_field, NULL, + offsetof(ngx_qjs_request_t, method) ), + JS_CGETSET_DEF("headers", ngx_qjs_ext_fetch_request_headers, NULL ), + JS_CGETSET_DEF("bodyUsed", ngx_qjs_ext_fetch_request_bodyused, NULL), + JS_CGETSET_DEF("cache", ngx_qjs_ext_fetch_request_cache, NULL), + JS_CGETSET_DEF("credentials", ngx_qjs_ext_fetch_request_credentials, NULL), + JS_CGETSET_DEF("mode", ngx_qjs_ext_fetch_request_mode, NULL), + JS_CFUNC_MAGIC_DEF("text", 1, ngx_qjs_ext_fetch_request_body, + NGX_QJS_BODY_TEXT), +}; + +static const JSCFunctionListEntry ngx_qjs_ext_fetch_response_funcs[] = { + JS_CGETSET_MAGIC_DEF("url", ngx_qjs_ext_fetch_response_field, NULL, + offsetof(ngx_qjs_response_t, url) ), + JS_CFUNC_MAGIC_DEF("arrayBuffer", 1, ngx_qjs_ext_fetch_response_body, + NGX_QJS_BODY_ARRAY_BUFFER), + JS_CFUNC_MAGIC_DEF("text", 1, ngx_qjs_ext_fetch_response_body, + NGX_QJS_BODY_TEXT), + JS_CFUNC_MAGIC_DEF("json", 1, ngx_qjs_ext_fetch_response_body, + NGX_QJS_BODY_JSON), + JS_CGETSET_DEF("status", ngx_qjs_ext_fetch_response_status, NULL), + JS_CGETSET_DEF("statusText", ngx_qjs_ext_fetch_response_status_text, NULL), + JS_CGETSET_DEF("headers", ngx_qjs_ext_fetch_response_headers, NULL ), + JS_CGETSET_DEF("bodyUsed", ngx_qjs_ext_fetch_response_bodyused, NULL), + JS_CGETSET_DEF("ok", ngx_qjs_ext_fetch_response_ok, NULL), + JS_CGETSET_DEF("type", ngx_qjs_ext_fetch_response_type, NULL), + JS_CGETSET_DEF("redirected", ngx_qjs_ext_fetch_response_redirected, NULL), +}; + + +static const uint32_t token_map[] = { + 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ + + /* ?>=< ;:98 7654 3210 /.-, +*)( '&%$ #"! */ + 0x03ff6cfa, /* 0000 0011 1111 1111 0110 1100 1111 1010 */ + + /* _^]\ [ZYX WVUT SRQP ONML KJIH GFED CBA@ */ + 0xc7fffffe, /* 1100 0111 1111 1111 1111 1111 1111 1110 */ + + /* ~}| {zyx wvut srqp onml kjih gfed cba` */ + 0x57ffffff, /* 0101 0111 1111 1111 1111 1111 1111 1111 */ + + 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ + 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ + 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ + 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ +}; + + +njs_inline njs_bool_t +njs_is_token(uint32_t byte) +{ + return ((token_map[byte >> 5] & ((uint32_t) 1 << (byte & 0x1f))) != 0); +} + + +njs_inline njs_int_t +ngx_qjs_http_whitespace(u_char c) +{ + switch (c) { + case 0x09: /* <TAB> */ + case 0x0A: /* <LF> */ + case 0x0D: /* <CR> */ + case 0x20: /* <SP> */ + return 1; + + default: + return 0; + } +} + + +static void +ngx_qjs_http_trim(u_char **value, size_t *len, + njs_bool_t trim_c0_control_or_space) +{ + u_char *start, *end; + + start = *value; + end = start + *len; + + for ( ;; ) { + if (start == end) { + break; + } + + if (ngx_qjs_http_whitespace(*start) + || (trim_c0_control_or_space && *start <= ' ')) + { + start++; + continue; + } + + break; + } + + for ( ;; ) { + if (start == end) { + break; + } + + end--; + + if (ngx_qjs_http_whitespace(*end) + || (trim_c0_control_or_space && *end <= ' ')) + { + continue; + } + + end++; + break; + } + + *value = start; + *len = end - start; +} + + +static njs_int_t +ngx_qjs_headers_append(JSContext *cx, ngx_qjs_headers_t *headers, + u_char *name, size_t len, u_char *value, size_t vlen) +{ + u_char *p, *end; + ngx_uint_t i; + ngx_list_part_t *part; + ngx_qjs_tb_elt_t *h, **ph; + + ngx_qjs_http_trim(&value, &vlen, 0); + + p = name; + end = p + len; + + while (p < end) { + if (!njs_is_token(*p)) { + JS_ThrowInternalError(cx, "invalid header name"); + return NJS_ERROR; + } + + p++; + } + + p = value; + end = p + vlen; + + while (p < end) { + if (*p == '\0') { + JS_ThrowInternalError(cx, "invalid header value"); + return NJS_ERROR; + } + + p++; + } + + if (headers->guard == GUARD_IMMUTABLE) { + JS_ThrowInternalError(cx, "cannot append to immutable object"); + return NJS_ERROR; + } + + ph = NULL; + part = &headers->header_list.part; + h = part->elts; + + for (i = 0; /* void */; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + h = part->elts; + i = 0; + } + + if (h[i].hash == 0) { + continue; + } + + if (len == h[i].key.len + && (njs_strncasecmp(name, h[i].key.data, len) == 0)) + { + ph = &h[i].next; + while (*ph) { ph = &(*ph)->next; } + break; + } + } + + h = ngx_list_push(&headers->header_list); + if (h == NULL) { + JS_ThrowOutOfMemory(cx); + return NJS_ERROR; + } + + if (ph != NULL) { + *ph = h; + } + + h->hash = 1; + h->key.data = name; + h->key.len = len; + h->value.data = value; + h->value.len = vlen; + h->next = NULL; + + if (len == njs_strlen("Content-Type") + && ngx_strncasecmp(name, (u_char *) "Content-Type", len) == 0) + { + headers->content_type = h; + } + + return NJS_OK; +} + + +static njs_int_t +ngx_qjs_headers_fill_header_free(JSContext *cx, ngx_qjs_headers_t *headers, + JSValue prop_name, JSValue prop_value) +{ + int ret; + ngx_str_t name, value; + + if (ngx_qjs_string(cx, prop_name, &name) != NGX_OK) { + JS_FreeValue(cx, prop_name); + JS_FreeValue(cx, prop_value); + return NJS_ERROR; + } + + if (ngx_qjs_string(cx, prop_value, &value) != NGX_OK) { + JS_FreeValue(cx, prop_name); + JS_FreeValue(cx, prop_value); + return NJS_ERROR; + } + + ret = ngx_qjs_headers_append(cx, headers, name.data, name.len, + value.data, value.len); + + JS_FreeValue(cx, prop_name); + JS_FreeValue(cx, prop_value); + + return ret; +} + + +static njs_int_t +ngx_qjs_headers_inherit(JSContext *cx, ngx_qjs_headers_t *headers, + ngx_qjs_headers_t *orig) +{ + njs_int_t ret; + ngx_uint_t i; + ngx_list_part_t *part; + ngx_qjs_tb_elt_t *h; + + part = &orig->header_list.part; + h = part->elts; + + for (i = 0; /* void */; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + h = part->elts; + i = 0; + } + + if (h[i].hash == 0) { + continue; + } + + ret = ngx_qjs_headers_append(cx, headers, h[i].key.data, h[i].key.len, + h[i].value.data, h[i].value.len); + if (ret != NJS_OK) { + return NJS_ERROR; + } + } + + return NJS_OK; +} + + +static njs_int_t +ngx_qjs_headers_fill(JSContext *cx, ngx_qjs_headers_t *headers, JSValue init) +{ + int ret; + JSValue header, prop_name, prop_value; + uint32_t i, len, length; + JSPropertyEnum *tab; + ngx_qjs_headers_t *hh; + + hh = JS_GetOpaque2(cx, init, NGX_QJS_CLASS_ID_FETCH_HEADERS); + if (hh != NULL) { + return ngx_qjs_headers_inherit(cx, headers, hh); + } + + if (JS_GetOwnPropertyNames(cx, &tab, &len, init, + JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) < 0) { + return NJS_ERROR; + } + + if (qjs_is_array(cx, init)) { + + for (i = 0; i < len; i++) { + header = JS_GetPropertyUint32(cx, init, i); + if (JS_IsException(header)) { + goto fail; + } + + if (ngx_qjs_array_length(cx, &length, header)) { + JS_FreeValue(cx, header); + goto fail; + } + + if (length != 2) { + JS_FreeValue(cx, header); + JS_ThrowInternalError(cx, + "header does not contain exactly two items"); + goto fail; + } + + prop_name = JS_GetPropertyUint32(cx, header, 0); + prop_value = JS_GetPropertyUint32(cx, header, 1); + + JS_FreeValue(cx, header); + + ret = ngx_qjs_headers_fill_header_free(cx, headers, prop_name, + prop_value); + if (ret != NJS_OK) { + goto fail; + } + } + + } else { + + for (i = 0; i < len; i++) { + prop_name = JS_AtomToString(cx, tab[i].atom); + + prop_value = JS_GetProperty(cx, init, tab[i].atom); + if (JS_IsException(prop_value)) { + JS_FreeValue(cx, prop_name); + goto fail; + } + + ret = ngx_qjs_headers_fill_header_free(cx, headers, prop_name, + prop_value); + if (ret != NJS_OK) { + goto fail; + } + } + } + + qjs_free_prop_enum(cx, tab, len); + + return NJS_OK; + +fail: + + qjs_free_prop_enum(cx, tab, len); + + return NJS_ERROR; +} + + +static JSValue +ngx_qjs_fetch_headers_ctor(JSContext *cx, JSValueConst new_target, int argc, + JSValueConst *argv) +{ + JSValue init, proto, obj; + ngx_int_t rc; + njs_int_t ret; + ngx_pool_t *pool; + ngx_qjs_headers_t *headers; + + pool = ngx_qjs_external_pool(cx, JS_GetContextOpaque(cx)); + + headers = ngx_pcalloc(pool, sizeof(ngx_qjs_headers_t)); + if (headers == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + headers->guard = GUARD_NONE; + + rc = ngx_list_init(&headers->header_list, pool, 4, + sizeof(ngx_qjs_tb_elt_t)); + if (rc != NGX_OK) { + return JS_ThrowOutOfMemory(cx); + } + + init = argv[0]; + + if (JS_IsObject(init)) { + ret = ngx_qjs_headers_fill(cx, headers, init); + if (ret != NJS_OK) { + return JS_EXCEPTION; + } + } + + proto = JS_GetPropertyStr(cx, new_target, "prototype"); + if (JS_IsException(proto)) { + return JS_EXCEPTION; + } + + obj = JS_NewObjectProtoClass(cx, proto, NGX_QJS_CLASS_ID_FETCH_HEADERS); + JS_FreeValue(cx, proto); + + if (JS_IsException(obj)) { + return JS_EXCEPTION; + } + + JS_SetOpaque(obj, headers); + + return obj; +} + + +static njs_int_t +ngx_qjs_method_process(JSContext *cx, ngx_qjs_request_t *request) +{ + u_char *s, *p; + njs_str_t method; + const njs_str_t *m; + + static const njs_str_t forbidden[] = { + njs_str("CONNECT"), + njs_str("TRACE"), + njs_str("TRACK"), + njs_null_str, + }; + + static const njs_str_t to_normalize[] = { + njs_str("DELETE"), + njs_str("GET"), + njs_str("HEAD"), + njs_str("OPTIONS"), + njs_str("POST"), + njs_str("PUT"), + njs_null_str, + }; + + method.start = request->method.data; + method.length = request->method.len; + + for (m = &forbidden[0]; m->length != 0; m++) { + if (njs_strstr_case_eq(&method, m)) { + JS_ThrowInternalError(cx, "forbidden method: %.*s", + (int) m->length, m->start); + return NJS_ERROR; + } + } + + for (m = &to_normalize[0]; m->length != 0; m++) { + if (njs_strstr_case_eq(&method, m)) { + s = &request->m[0]; + p = m->start; + + while (*p != '\0') { + *s++ = njs_upper_case(*p++); + } + + request->method.data = &request->m[0]; + request->method.len = m->length; + break; + } + } + + return NJS_OK; +} + + +static njs_int_t +ngx_qjs_fetch_flag_set(JSContext *cx, const ngx_qjs_entry_t *entries, + JSValue value, const char *type) +{ + njs_int_t ret; + njs_str_t str; + ngx_str_t flag; + const ngx_qjs_entry_t *e; + + ret = ngx_qjs_string(cx, value, &flag); + if (ret != NJS_OK) { + return NJS_ERROR; + } + + str.start = flag.data; + str.length = flag.len; + + for (e = entries; e->name.length != 0; e++) { + if (njs_strstr_case_eq(&str, &e->name)) { + return e->value; + } + } + + JS_ThrowInternalError(cx, "unknown %s type: %.*s", + type, (int) flag.len, flag.data); + + return NJS_ERROR; +} + + +static JSValue +ngx_qjs_fetch_flag(JSContext *cx, const ngx_qjs_entry_t *entries, + njs_int_t value) +{ + const ngx_qjs_entry_t *e; + + for (e = entries; e->name.length != 0; e++) { + if (e->value == value) { + return qjs_string_create(cx, e->name.start, e->name.length); + } + } + + return JS_EXCEPTION; +} + + +static njs_int_t +ngx_qjs_request_constructor(JSContext *cx, ngx_qjs_request_t *request, + ngx_url_t *u, int argc, JSValueConst *argv) +{ + JSValue input, init, value; + njs_int_t ret; + ngx_uint_t rc; + ngx_pool_t *pool; + ngx_qjs_request_t *orig; + + input = argv[0]; + if (JS_IsUndefined(input)) { + JS_ThrowInternalError(cx, "1st argument is required"); + return NJS_ERROR; + } + + /* + * set by ngx_memzero(): + * + * request->url.len = 0; + * request->body.length = 0; + * request->cache_mode = CACHE_MODE_DEFAULT; + * request->credentials = CREDENTIALS_SAME_ORIGIN; + * request->mode = MODE_NO_CORS; + * request->headers.content_type = NULL; + */ + + ngx_memzero(request, sizeof(ngx_qjs_request_t)); + + ngx_str_set(&request->method, "GET"); + ngx_str_null(&request->body); + request->headers.guard = GUARD_REQUEST; + request->header_value = JS_UNDEFINED; + + pool = ngx_qjs_external_pool(cx, JS_GetContextOpaque(cx)); + + rc = ngx_list_init(&request->headers.header_list, pool, 4, + sizeof(ngx_qjs_tb_elt_t)); + if (rc != NGX_OK) { + JS_ThrowOutOfMemory(cx); + return NJS_ERROR; + } + + if (JS_IsString(input)) { + ret = ngx_qjs_string(cx, input, &request->url); + if (ret != NJS_OK) { + JS_ThrowInternalError(cx, "failed to convert url arg"); + return NJS_ERROR; + } + + } else { + orig = JS_GetOpaque2(cx, input, NGX_QJS_CLASS_ID_FETCH_REQUEST); + if (orig == NULL) { + JS_ThrowInternalError(cx, + "input is not string or a Request object"); + return NJS_ERROR; + } + + request->url = orig->url; + request->method = orig->method; + request->body = orig->body; + request->body_used = orig->body_used; + request->cache_mode = orig->cache_mode; + request->credentials = orig->credentials; + request->mode = orig->mode; + + ret = ngx_qjs_headers_inherit(cx, &request->headers, &orig->headers); + if (ret != NJS_OK) { + return NJS_ERROR; + } + } + + ngx_qjs_http_trim(&request->url.data, &request->url.len, 1); + + ngx_memzero(u, sizeof(ngx_url_t)); + + u->url.len = request->url.len; + u->url.data = request->url.data; + u->default_port = 80; + u->uri_part = 1; + u->no_resolve = 1; + + if (u->url.len > 7 + && njs_strncasecmp(u->url.data, (u_char *) "http://", 7) == 0) + { + u->url.len -= 7; + u->url.data += 7; + +#if (NGX_SSL) + } else if (u->url.len > 8 + && njs_strncasecmp(u->url.data, (u_char *) "https://", 8) == 0) + { + u->url.len -= 8; + u->url.data += 8; + u->default_port = 443; +#endif + + } else { + JS_ThrowInternalError(cx, "unsupported URL schema (only http or https" + " are supported)"); + return NJS_ERROR; + } + + if (ngx_parse_url(pool, u) != NGX_OK) { + JS_ThrowInternalError(cx, "invalid url"); + return NJS_ERROR; + } + + if (JS_IsObject(argv[1])) { + init = argv[1]; + value = JS_GetPropertyStr(cx, init, "method"); + if (JS_IsException(value)) { + JS_ThrowInternalError(cx, "invalid Request method"); + return NJS_ERROR; + } + + if (!JS_IsUndefined(value)) { + ret = ngx_qjs_string(cx, value, &request->method); + JS_FreeValue(cx, value); + + if (ret != NJS_OK) { + JS_ThrowInternalError(cx, "invalid Request method"); + return NJS_ERROR; + } + } + + ret = ngx_qjs_method_process(cx, request); + if (ret != NJS_OK) { + return NJS_ERROR; + } + + value = JS_GetPropertyStr(cx, init, "cache"); + if (JS_IsException(value)) { + JS_ThrowInternalError(cx, "invalid Request cache"); + return NJS_ERROR; + } + + if (!JS_IsUndefined(value)) { + ret = ngx_qjs_fetch_flag_set(cx, ngx_qjs_fetch_cache_modes, value, + "cache"); + JS_FreeValue(cx, value); + + if (ret == NJS_ERROR) { + return NJS_ERROR; + } + + request->cache_mode = ret; + } + + value = JS_GetPropertyStr(cx, init, "credentials"); + if (JS_IsException(value)) { + JS_ThrowInternalError(cx, "invalid Request credentials"); + return NJS_ERROR; + } + + if (!JS_IsUndefined(value)) { + ret = ngx_qjs_fetch_flag_set(cx, ngx_qjs_fetch_credentials, value, + "credentials"); + JS_FreeValue(cx, value); + + if (ret == NJS_ERROR) { + return NJS_ERROR; + } + + request->credentials = ret; + } + + value = JS_GetPropertyStr(cx, init, "mode"); + if (JS_IsException(value)) { + JS_ThrowInternalError(cx, "invalid Request mode"); + return NJS_ERROR; + } + + if (!JS_IsUndefined(value)) { + ret = ngx_qjs_fetch_flag_set(cx, ngx_qjs_fetch_modes, value, + "mode"); + JS_FreeValue(cx, value); + + if (ret == NJS_ERROR) { + return NJS_ERROR; + } + + request->mode = ret; + } + + value = JS_GetPropertyStr(cx, init, "headers"); + if (JS_IsException(value)) { + JS_ThrowInternalError(cx, "invalid Request headers"); + return NJS_ERROR; + } + + if (!JS_IsUndefined(value)) { + + if (!JS_IsObject(value)) { + JS_ThrowInternalError(cx, "Headers is not an object"); + return NJS_ERROR; + } + + /* + * There are no API to reset or destroy ngx_list, + * just allocating a new one. + */ + + ngx_memset(&request->headers, 0, sizeof(ngx_qjs_headers_t)); + request->headers.guard = GUARD_REQUEST; + + rc = ngx_list_init(&request->headers.header_list, pool, 4, + sizeof(ngx_qjs_tb_elt_t)); + if (rc != NGX_OK) { + JS_FreeValue(cx, value); + JS_ThrowOutOfMemory(cx); + return NJS_ERROR; + } + + ret = ngx_qjs_headers_fill(cx, &request->headers, value); + JS_FreeValue(cx, value); + + if (ret != NJS_OK) { + return NJS_ERROR; + } + } + + value = JS_GetPropertyStr(cx, init, "body"); + if (JS_IsException(value)) { + JS_ThrowInternalError(cx, "invalid Request body"); + return NJS_ERROR; + } + + if (!JS_IsUndefined(value)) { + + if (ngx_qjs_string(cx, value, &request->body) != NGX_OK) { + JS_FreeValue(cx, value); + JS_ThrowInternalError(cx, "invalid Request body"); + return NJS_ERROR; + } + + if (request->headers.content_type == NULL && JS_IsString(value)) { + ret = ngx_qjs_headers_append(cx, &request->headers, + (u_char *) "Content-Type", + njs_length("Content-Type"), + (u_char *) "text/plain;charset=UTF-8", + njs_length("text/plain;charset=UTF-8")); + if (ret != NJS_OK) { + JS_FreeValue(cx, value); + return NJS_ERROR; + } + } + + JS_FreeValue(cx, value); + } + } + + return NJS_OK; +} + + +static JSValue +ngx_qjs_fetch_request_ctor(JSContext *cx, JSValueConst new_target, int argc, + JSValueConst *argv) +{ + JSValue proto, obj; + njs_int_t ret; + ngx_url_t u; + ngx_pool_t *pool; + ngx_qjs_request_t *request; + + pool = ngx_qjs_external_pool(cx, JS_GetContextOpaque(cx)); + + request = ngx_pcalloc(pool, sizeof(ngx_qjs_request_t)); + if (request == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + ret = ngx_qjs_request_constructor(cx, request, &u, argc, argv); + if (ret != NJS_OK) { + return JS_EXCEPTION; + } + + proto = JS_GetPropertyStr(cx, new_target, "prototype"); + if (JS_IsException(proto)) { + return JS_EXCEPTION; + } + + obj = JS_NewObjectProtoClass(cx, proto, NGX_QJS_CLASS_ID_FETCH_REQUEST); + JS_FreeValue(cx, proto); + + if (JS_IsException(obj)) { + return JS_EXCEPTION; + } + + JS_SetOpaque(obj, request); + + return obj; +} + + +static JSValue +ngx_qjs_fetch_response_ctor(JSContext *cx, JSValueConst new_target, int argc, + JSValueConst *argv) +{ + int ret; + u_char *p, *end; + JSValue init, value, body, proto, obj; + ngx_str_t bd; + ngx_pool_t *pool; + ngx_qjs_response_t *response; + + pool = ngx_qjs_external_pool(cx, JS_GetContextOpaque(cx)); + + response = ngx_pcalloc(pool, sizeof(ngx_qjs_response_t)); + if (response == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + /* + * set by njs_mp_zalloc(): + * + * response->url.length = 0; + * response->status_text.length = 0; + */ + + response->code = 200; + response->headers.guard = GUARD_RESPONSE; + response->header_value = JS_UNDEFINED; + + pool = ngx_qjs_external_pool(cx, JS_GetContextOpaque(cx)); + + ret = ngx_list_init(&response->headers.header_list, pool, 4, + sizeof(ngx_qjs_tb_elt_t)); + if (ret != NGX_OK) { + JS_ThrowOutOfMemory(cx); + } + + init = argv[1]; + + if (JS_IsObject(init)) { + value = JS_GetPropertyStr(cx, init, "status"); + if (JS_IsException(value)) { + return JS_ThrowInternalError(cx, "invalid Response status"); + } + + if (!JS_IsUndefined(value)) { + ret = JS_ToInt64(cx, &response->code, value); + JS_FreeValue(cx, value); + + if (ret < 0) { + return JS_EXCEPTION; + } + + if (response->code < 200 || response->code > 599) { + return JS_ThrowInternalError(cx, "status provided (%d) is " + "outside of [200, 599] range", + (int) response->code); + } + } + + value = JS_GetPropertyStr(cx, init, "statusText"); + if (JS_IsException(value)) { + return JS_ThrowInternalError(cx, "invalid Response statusText"); + } + + if (!JS_IsUndefined(value)) { + ret = ngx_qjs_string(cx, value, &response->status_text); + JS_FreeValue(cx, value); + + if (ret < 0) { + return JS_EXCEPTION; + } + + p = response->status_text.data; + end = p + response->status_text.len; + + while (p < end) { + if (*p != '\t' && *p < ' ') { + return JS_ThrowInternalError(cx, "invalid Response statusText"); + } + + p++; + } + } + + value = JS_GetPropertyStr(cx, init, "headers"); + if (JS_IsException(value)) { + return JS_ThrowInternalError(cx, "invalid Response headers"); + } + + if (!JS_IsUndefined(value)) { + + if (!JS_IsObject(value)) { + JS_FreeValue(cx, value); + return JS_ThrowInternalError(cx, "Headers is not an object"); + } + + ret = ngx_qjs_headers_fill(cx, &response->headers, value); + JS_FreeValue(cx, value); + + if (ret != NJS_OK) { + return JS_EXCEPTION; + } + } + } + + NJS_CHB_CTX_INIT(&response->chain, cx); + + body = argv[0]; + + if (!JS_IsNullOrUndefined(body)) { + if (ngx_qjs_string(cx, body, &bd) != NGX_OK) { + return JS_ThrowInternalError(cx, "invalid Response body"); + } + + njs_chb_append(&response->chain, bd.data, bd.len); + + if (JS_IsString(body)) { + ret = ngx_qjs_headers_append(cx, &response->headers, + (u_char *) "Content-Type", + njs_length("Content-Type"), + (u_char *) "text/plain;charset=UTF-8", + njs_length("text/plain;charset=UTF-8")); + if (ret != NJS_OK) { + return JS_EXCEPTION; + } + } + } + + proto = JS_GetPropertyStr(cx, new_target, "prototype"); + if (JS_IsException(proto)) { + return JS_EXCEPTION; + } + + obj = JS_NewObjectProtoClass(cx, proto, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + JS_FreeValue(cx, proto); + + if (JS_IsException(obj)) { + return JS_EXCEPTION; + } + + JS_SetOpaque(obj, response); + + return obj; +} + + +static const JSClassDef *const ngx_qjs_fetch_class_ptr[3] = { + &ngx_qjs_fetch_headers_class, + &ngx_qjs_fetch_request_class, + &ngx_qjs_fetch_response_class, +}; + +static JSCFunction *ngx_qjs_fetch_class_ctor_ptr[3] = { + ngx_qjs_fetch_headers_ctor, + ngx_qjs_fetch_request_ctor, + ngx_qjs_fetch_response_ctor, +}; + +static const JSCFunctionListEntry *const ngx_qjs_fetch_proto_funcs_ptr[6] = { + ngx_qjs_ext_fetch_headers_funcs, + ngx_qjs_ext_fetch_request_funcs, + ngx_qjs_ext_fetch_response_funcs, +}; + +static const uint8_t ngx_qjs_fetch_proto_funcs_count[3] = { + njs_nitems(ngx_qjs_ext_fetch_headers_funcs), + njs_nitems(ngx_qjs_ext_fetch_request_funcs), + njs_nitems(ngx_qjs_ext_fetch_response_funcs), +}; + + +static JSModuleDef * +ngx_qjs_fetch_init(JSContext *cx, const char *name) +{ + int i, class_id; + JSValue global_obj, proto, class; + + global_obj = JS_GetGlobalObject(cx); + + for (i = 0; i < 3; i++) { + class_id = NGX_QJS_CLASS_ID_FETCH_HEADERS + i; + JS_NewClass(JS_GetRuntime(cx), class_id, ngx_qjs_fetch_class_ptr[i]); + + proto = JS_NewObject(cx); + if (JS_IsException(proto)) { + JS_FreeValue(cx, global_obj); + return NULL; + } + + JS_SetPropertyFunctionList(cx, proto, + ngx_qjs_fetch_proto_funcs_ptr[i], + ngx_qjs_fetch_proto_funcs_count[i]); + + class = JS_NewCFunction2(cx, ngx_qjs_fetch_class_ctor_ptr[i], + ngx_qjs_fetch_class_ptr[i]->class_name, 2, + JS_CFUNC_constructor, 0); + + JS_SetConstructor(cx, class, proto); + JS_SetClassProto(cx, class_id, proto); + + JS_SetPropertyStr(cx, global_obj, + ngx_qjs_fetch_class_ptr[i]->class_name, class); + } + + JS_FreeValue(cx, global_obj); + + return JS_NewCModule(cx, name, NULL); +} + + +qjs_module_t ngx_qjs_ngx_fetch_module = { + .name = "fetch", + .init = ngx_qjs_fetch_init, +}; diff --git a/nginx/t/js_fetch.t b/nginx/t/js_fetch.t index ae9d1f614..7ee1a6026 100644 --- a/nginx/t/js_fetch.t +++ b/nginx/t/js_fetch.t @@ -342,6 +342,8 @@ $t->write_file('test.js', <<EOF); } } + out.sort(); + r.return(200, JSON.stringify(out)); } @@ -411,8 +413,6 @@ EOF $t->try_run('no njs.fetch'); -plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; - $t->plan(37); $t->run_daemon(\&http_daemon, port(8082)); diff --git a/nginx/t/js_fetch_https.t b/nginx/t/js_fetch_https.t index 9a44a3390..8ede10489 100644 --- a/nginx/t/js_fetch_https.t +++ b/nginx/t/js_fetch_https.t @@ -196,8 +196,6 @@ foreach my $name ('default.example.com', '1.example.com') { $t->try_run('no njs.fetch'); -plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; - $t->plan(7); $t->run_daemon(\&dns_daemon, port(8981), $t); diff --git a/nginx/t/js_fetch_objects.t b/nginx/t/js_fetch_objects.t index 67cabdfcb..82eb93fe8 100644 --- a/nginx/t/js_fetch_objects.t +++ b/nginx/t/js_fetch_objects.t @@ -271,7 +271,9 @@ $t->write_file('test.js', <<EOF); }) } catch (e) { - if (!e.message.startsWith('Cannot assign to read-only p')) { + if (!e.message.startsWith('Cannot assign to read-only p') + && !e.message.startsWith('no setter for property')) + { throw e; } } @@ -515,8 +517,6 @@ EOF $t->try_run('no njs'); -plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; - $t->plan(5); ############################################################################### diff --git a/nginx/t/js_fetch_resolver.t b/nginx/t/js_fetch_resolver.t index 7cea33867..031ff43c9 100644 --- a/nginx/t/js_fetch_resolver.t +++ b/nginx/t/js_fetch_resolver.t @@ -146,8 +146,6 @@ EOF $t->try_run('no njs.fetch'); -plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; - $t->plan(5); $t->run_daemon(\&dns_daemon, port(8981), $t); diff --git a/nginx/t/js_fetch_timeout.t b/nginx/t/js_fetch_timeout.t index 5b207b90b..ab1ba24ad 100644 --- a/nginx/t/js_fetch_timeout.t +++ b/nginx/t/js_fetch_timeout.t @@ -116,8 +116,6 @@ EOF $t->try_run('no js_fetch_timeout'); -plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; - $t->plan(2); ############################################################################### diff --git a/nginx/t/js_fetch_verify.t b/nginx/t/js_fetch_verify.t index 4c97e04d7..f98b4d8c0 100644 --- a/nginx/t/js_fetch_verify.t +++ b/nginx/t/js_fetch_verify.t @@ -114,8 +114,6 @@ foreach my $name ('localhost') { $t->try_run('no js_fetch_verify'); -plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; - $t->plan(2); $t->run_daemon(\&dns_daemon, port(8981), $t);