diff options
-rw-r--r-- | nginx/config | 1 | ||||
-rw-r--r-- | nginx/ngx_http_js_module.c | 3 | ||||
-rw-r--r-- | nginx/ngx_js.c | 1 | ||||
-rw-r--r-- | nginx/ngx_js.h | 7 | ||||
-rw-r--r-- | nginx/ngx_js_fetch.c | 6 | ||||
-rw-r--r-- | nginx/ngx_qjs_fetch.c | 2520 | ||||
-rw-r--r-- | nginx/ngx_stream_js_module.c | 1 | ||||
-rw-r--r-- | nginx/t/js_fetch.t | 4 | ||||
-rw-r--r-- | nginx/t/js_fetch_https.t | 2 | ||||
-rw-r--r-- | nginx/t/js_fetch_objects.t | 23 | ||||
-rw-r--r-- | nginx/t/js_fetch_resolver.t | 2 | ||||
-rw-r--r-- | nginx/t/js_fetch_timeout.t | 2 | ||||
-rw-r--r-- | nginx/t/js_fetch_verify.t | 2 | ||||
-rw-r--r-- | nginx/t/js_periodic_fetch.t | 2 | ||||
-rw-r--r-- | nginx/t/stream_js_fetch.t | 2 | ||||
-rw-r--r-- | nginx/t/stream_js_fetch_https.t | 2 | ||||
-rw-r--r-- | nginx/t/stream_js_fetch_init.t | 2 | ||||
-rw-r--r-- | nginx/t/stream_js_periodic_fetch.t | 1 | ||||
-rw-r--r-- | src/qjs.c | 25 | ||||
-rw-r--r-- | src/qjs.h | 2 |
20 files changed, 2585 insertions, 25 deletions
diff --git a/nginx/config b/nginx/config index b994f97f..1c303d9c 100644 --- a/nginx/config +++ b/nginx/config @@ -156,6 +156,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 3ac95478..40bb83a5 100644 --- a/nginx/ngx_http_js_module.c +++ b/nginx/ngx_http_js_module.c @@ -1134,6 +1134,9 @@ 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, +#ifdef NJS_HAVE_QUICKJS + &ngx_qjs_ngx_fetch_module, +#endif /* * 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 34221d28..e4bae32a 100644 --- a/nginx/ngx_js.c +++ b/nginx/ngx_js.c @@ -441,6 +441,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), diff --git a/nginx/ngx_js.h b/nginx/ngx_js.h index e13efc41..bb7c1d26 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; @@ -346,6 +349,9 @@ ngx_int_t ngx_qjs_exception(ngx_engine_t *e, 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); +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) \ : qjs_buffer_create(cx, (u_char *) start, len)) @@ -382,6 +388,7 @@ extern qjs_module_t qjs_xml_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_js_fetch.c b/nginx/ngx_js_fetch.c index 05d220b9..45f2dc10 100644 --- a/nginx/ngx_js_fetch.c +++ b/nginx/ngx_js_fetch.c @@ -1171,7 +1171,7 @@ ngx_js_fetch_alloc(njs_vm_t *vm, ngx_pool_t *pool, ngx_log_t *log) fetch->vm = vm; fetch->event = event; - ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "js fetch alloc:%p", fetch); + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "js http alloc:%p", fetch); return fetch; @@ -1207,7 +1207,7 @@ ngx_js_fetch_destructor(ngx_js_event_t *event) fetch = event->data; http = &fetch->http; - ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, "js fetch destructor:%p", + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, "js http destructor:%p", fetch); ngx_js_http_resolve_done(http); @@ -1273,7 +1273,7 @@ ngx_js_fetch_done(ngx_js_fetch_t *fetch, njs_opaque_value_t *retval, http = &fetch->http; ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0, - "js fetch done fetch:%p rc:%i", fetch, (ngx_int_t) rc); + "js http done fetch:%p rc:%i", fetch, (ngx_int_t) rc); ngx_js_http_close_peer(http); diff --git a/nginx/ngx_qjs_fetch.c b/nginx/ngx_qjs_fetch.c new file mode 100644 index 00000000..084162ba --- /dev/null +++ b/nginx/ngx_qjs_fetch.c @@ -0,0 +1,2520 @@ + +/* + * Copyright (C) hongzhidao + * Copyright (C) F5, Inc. + */ + + +#include <ngx_config.h> +#include <ngx_core.h> +#include <ngx_event.h> +#include <ngx_event_connect.h> +#include "ngx_js.h" +#include "ngx_js_http.h" + + +typedef struct { + ngx_str_t name; + ngx_int_t value; +} ngx_qjs_entry_t; + + +typedef struct { + ngx_js_http_t http; + + JSContext *cx; + ngx_qjs_event_t *event; + + JSValue response_value; + + JSValue promise; + JSValue promise_callbacks[2]; +} ngx_qjs_fetch_t; + + +static ngx_int_t ngx_qjs_method_process(JSContext *cx, + ngx_js_request_t *request); +static ngx_int_t ngx_qjs_headers_inherit(JSContext *cx, + ngx_js_headers_t *headers, ngx_js_headers_t *orig); +static ngx_int_t ngx_qjs_headers_fill(JSContext *cx, ngx_js_headers_t *headers, + JSValue init); +static ngx_qjs_fetch_t *ngx_qjs_fetch_alloc(JSContext *cx, ngx_pool_t *pool, + ngx_log_t *log); +static void ngx_qjs_fetch_error(ngx_js_http_t *http, const char *err); +static void ngx_qjs_fetch_destructor(ngx_qjs_event_t *event); +static void ngx_qjs_fetch_done(ngx_qjs_fetch_t *fetch, JSValue retval, + ngx_int_t rc); + +static ngx_int_t ngx_qjs_request_ctor(JSContext *cx, ngx_js_request_t *request, + ngx_url_t *u, int argc, JSValueConst *argv); + +static ngx_int_t ngx_qjs_fetch_append_headers(ngx_js_http_t *http, + ngx_js_headers_t *headers, u_char *name, size_t len, u_char *value, + size_t vlen); +static void ngx_qjs_fetch_process_done(ngx_js_http_t *http); +static ngx_int_t ngx_qjs_headers_append(JSContext *cx, + ngx_js_headers_t *headers, u_char *name, size_t len, u_char *value, + size_t vlen); + +static JSValue ngx_qjs_fetch_headers_ctor(JSContext *cx, + JSValueConst new_target, int argc, JSValueConst *argv); +static int ngx_qjs_fetch_headers_own_property(JSContext *cx, + JSPropertyDescriptor *desc, JSValueConst obj, JSAtom prop); +static int ngx_qjs_fetch_headers_own_property_names(JSContext *cx, + JSPropertyEnum **ptab, uint32_t *plen, JSValueConst obj); +static JSValue ngx_qjs_ext_fetch_headers_append(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_qjs_ext_fetch_headers_delete(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_qjs_ext_fetch_headers_foreach(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_qjs_ext_fetch_headers_get(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv, int magic); +static JSValue ngx_qjs_ext_fetch_headers_has(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_qjs_ext_fetch_headers_set(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); + +static JSValue ngx_qjs_fetch_request_ctor(JSContext *cx, + JSValueConst new_target, int argc, JSValueConst *argv); +static JSValue ngx_qjs_ext_fetch_request_body(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv, int magic); +static JSValue ngx_qjs_ext_fetch_request_body_used(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_qjs_ext_fetch_request_cache(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_qjs_ext_fetch_request_credentials(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_qjs_ext_fetch_request_headers(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_qjs_ext_fetch_request_field(JSContext *cx, + JSValueConst this_val, int magic); +static JSValue ngx_qjs_ext_fetch_request_mode(JSContext *cx, + JSValueConst this_val); +static void ngx_qjs_fetch_request_finalizer(JSRuntime *rt, JSValue val); + +static JSValue ngx_qjs_fetch_response_ctor(JSContext *cx, + JSValueConst new_target, int argc, JSValueConst *argv); +static JSValue ngx_qjs_ext_fetch_response_status(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_qjs_ext_fetch_response_status_text(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_qjs_ext_fetch_response_ok(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_qjs_ext_fetch_response_body_used(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_qjs_ext_fetch_response_headers(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_qjs_ext_fetch_response_type(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_qjs_ext_fetch_response_body(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv, int magic); +static JSValue ngx_qjs_ext_fetch_response_redirected(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_qjs_ext_fetch_response_field(JSContext *cx, + JSValueConst this_val, int magic); +static void ngx_qjs_fetch_response_finalizer(JSRuntime *rt, JSValue val); + +static JSValue ngx_qjs_fetch_flag(JSContext *cx, const ngx_qjs_entry_t *entries, + ngx_int_t value); +static ngx_int_t ngx_qjs_fetch_flag_set(JSContext *cx, + const ngx_qjs_entry_t *entries, JSValue object, const char *prop); + +static JSModuleDef *ngx_qjs_fetch_init(JSContext *cx, const char *name); + + +static const JSCFunctionListEntry ngx_qjs_ext_fetch_headers_proto[] = { + 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), + 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), +}; + + +static const JSCFunctionListEntry ngx_qjs_ext_fetch_request_proto[] = { +#define NGX_QJS_BODY_ARRAY_BUFFER 0 +#define NGX_QJS_BODY_JSON 1 +#define NGX_QJS_BODY_TEXT 2 + JS_CFUNC_MAGIC_DEF("arrayBuffer", 0, ngx_qjs_ext_fetch_request_body, + NGX_QJS_BODY_ARRAY_BUFFER), + JS_CGETSET_DEF("bodyUsed", ngx_qjs_ext_fetch_request_body_used, NULL), + JS_CGETSET_DEF("cache", ngx_qjs_ext_fetch_request_cache, NULL), + JS_CGETSET_DEF("credentials", ngx_qjs_ext_fetch_request_credentials, NULL), + JS_CFUNC_MAGIC_DEF("json", 0, ngx_qjs_ext_fetch_request_body, + NGX_QJS_BODY_JSON), + JS_CGETSET_DEF("headers", ngx_qjs_ext_fetch_request_headers, NULL ), + JS_CGETSET_MAGIC_DEF("method", ngx_qjs_ext_fetch_request_field, NULL, + offsetof(ngx_js_request_t, method) ), + JS_CGETSET_DEF("mode", ngx_qjs_ext_fetch_request_mode, NULL), + JS_CFUNC_MAGIC_DEF("text", 0, ngx_qjs_ext_fetch_request_body, + NGX_QJS_BODY_TEXT), + JS_CGETSET_MAGIC_DEF("url", ngx_qjs_ext_fetch_request_field, NULL, + offsetof(ngx_js_request_t, url) ), +}; + + +static const JSCFunctionListEntry ngx_qjs_ext_fetch_response_proto[] = { + JS_CFUNC_MAGIC_DEF("arrayBuffer", 0, ngx_qjs_ext_fetch_response_body, + NGX_QJS_BODY_ARRAY_BUFFER), + JS_CGETSET_DEF("bodyUsed", ngx_qjs_ext_fetch_response_body_used, NULL), + JS_CGETSET_DEF("headers", ngx_qjs_ext_fetch_response_headers, NULL ), + JS_CFUNC_MAGIC_DEF("json", 0, ngx_qjs_ext_fetch_response_body, + NGX_QJS_BODY_JSON), + JS_CGETSET_DEF("ok", ngx_qjs_ext_fetch_response_ok, NULL), + JS_CGETSET_DEF("redirected", ngx_qjs_ext_fetch_response_redirected, NULL), + JS_CGETSET_DEF("status", ngx_qjs_ext_fetch_response_status, NULL), + JS_CGETSET_DEF("statusText", ngx_qjs_ext_fetch_response_status_text, NULL), + JS_CFUNC_MAGIC_DEF("text", 0, ngx_qjs_ext_fetch_response_body, + NGX_QJS_BODY_TEXT), + JS_CGETSET_DEF("type", ngx_qjs_ext_fetch_response_type, NULL), + JS_CGETSET_MAGIC_DEF("url", ngx_qjs_ext_fetch_response_field, NULL, + offsetof(ngx_js_response_t, url) ), +}; + + +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 const JSClassDef ngx_qjs_fetch_response_class = { + "Response", + .finalizer = ngx_qjs_fetch_response_finalizer, +}; + + +static const ngx_qjs_entry_t ngx_qjs_fetch_cache_modes[] = { + { ngx_string("default"), CACHE_MODE_DEFAULT }, + { ngx_string("no-store"), CACHE_MODE_NO_STORE }, + { ngx_string("reload"), CACHE_MODE_RELOAD }, + { ngx_string("no-cache"), CACHE_MODE_NO_CACHE }, + { ngx_string("force-cache"), CACHE_MODE_FORCE_CACHE }, + { ngx_string("only-if-cached"), CACHE_MODE_ONLY_IF_CACHED }, + { ngx_null_string, 0 }, +}; + + +static const ngx_qjs_entry_t ngx_qjs_fetch_credentials[] = { + { ngx_string("same-origin"), CREDENTIALS_SAME_ORIGIN }, + { ngx_string("omit"), CREDENTIALS_OMIT }, + { ngx_string("include"), CREDENTIALS_INCLUDE }, + { ngx_null_string, 0 }, +}; + + +static const ngx_qjs_entry_t ngx_qjs_fetch_modes[] = { + { ngx_string("no-cors"), MODE_NO_CORS }, + { ngx_string("cors"), MODE_CORS }, + { ngx_string("same-origin"), MODE_SAME_ORIGIN }, + { ngx_string("navigate"), MODE_NAVIGATE }, + { ngx_string("websocket"), MODE_WEBSOCKET }, + { ngx_null_string, 0 }, +}; + + +qjs_module_t ngx_qjs_ngx_fetch_module = { + .name = "fetch", + .init = ngx_qjs_fetch_init, +}; + + +JSValue +ngx_qjs_ext_fetch(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + int has_host; + void *external; + JSValue init, value, promise; + ngx_int_t rc; + ngx_url_t u; + ngx_uint_t i; + ngx_pool_t *pool; + ngx_js_ctx_t *ctx; + ngx_js_http_t *http; + ngx_qjs_fetch_t *fetch; + ngx_list_part_t *part; + ngx_js_tb_elt_t *h; + ngx_connection_t *c; + ngx_js_request_t request; + ngx_resolver_ctx_t *rs; + + external = JS_GetContextOpaque(cx); + c = ngx_qjs_external_connection(cx, external); + pool = ngx_qjs_external_pool(cx, external); + + fetch = ngx_qjs_fetch_alloc(cx, pool, c->log); + if (fetch == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + promise = JS_DupValue(cx, fetch->promise); + + rc = ngx_qjs_request_ctor(cx, &request, &u, argc, argv); + if (rc != NGX_OK) { + goto fail; + } + + http = &fetch->http; + 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 (JS_IsObject(argv[1])) { + init = argv[1]; + value = JS_GetPropertyStr(cx, init, "buffer_size"); + if (JS_IsException(value)) { + goto fail; + } + + if (!JS_IsUndefined(value)) { + if (JS_ToInt64(cx, (int64_t *) &http->buffer_size, value) < 0) { + JS_FreeValue(cx, value); + goto fail; + } + } + + value = JS_GetPropertyStr(cx, init, "max_response_body_size"); + if (JS_IsException(value)) { + goto fail; + } + + if (!JS_IsUndefined(value)) { + if (JS_ToInt64(cx, (int64_t *) &http->max_response_body_size, + value) < 0) + { + JS_FreeValue(cx, value); + 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 + } + + if (request.method.len == 4 + && ngx_strncasecmp(request.method.data, (u_char *) "HEAD", 4) == 0) + { + http->header_only = 1; + } + + ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx)); + + NJS_CHB_MP_INIT(&http->chain, ctx->engine->pool); + NJS_CHB_MP_INIT(&http->response.chain, ctx->engine->pool); + + 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) { + rs = ngx_js_http_resolve(http, ngx_qjs_external_resolver(cx, external), + &u.host, u.port, + ngx_qjs_external_resolver_timeout(cx, external)); + if (rs == NULL) { + JS_FreeValue(cx, promise); + return JS_ThrowOutOfMemory(cx); + } + + if (rs == NGX_NO_RESOLVER) { + JS_ThrowInternalError(cx, "no resolver defined"); + goto fail; + } + + return promise; + } + + http->naddrs = 1; + ngx_memcpy(&http->addr, &u.addrs[0], sizeof(ngx_addr_t)); + http->addrs = &http->addr; + + ngx_js_http_connect(http); + + return promise; + +fail: + + fetch->response_value = JS_GetException(cx); + + ngx_qjs_fetch_done(fetch, fetch->response_value, NGX_ERROR); + + return promise; +} + + +static JSValue +ngx_qjs_fetch_headers_ctor(JSContext *cx, JSValueConst new_target, int argc, + JSValueConst *argv) +{ + JSValue init, proto, obj; + ngx_int_t rc; + ngx_pool_t *pool; + ngx_js_headers_t *headers; + + pool = ngx_qjs_external_pool(cx, JS_GetContextOpaque(cx)); + + headers = ngx_pcalloc(pool, sizeof(ngx_js_headers_t)); + if (headers == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + headers->guard = GUARD_NONE; + + rc = ngx_list_init(&headers->header_list, pool, 4, + sizeof(ngx_js_tb_elt_t)); + if (rc != NGX_OK) { + return JS_ThrowOutOfMemory(cx); + } + + init = argv[0]; + + if (JS_IsObject(init)) { + rc = ngx_qjs_headers_fill(cx, headers, init); + if (rc != NGX_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 JSValue +ngx_qjs_fetch_request_ctor(JSContext *cx, JSValueConst new_target, int argc, + JSValueConst *argv) +{ + JSValue proto, obj; + ngx_int_t rc; + ngx_url_t u; + ngx_pool_t *pool; + ngx_js_request_t *request; + + pool = ngx_qjs_external_pool(cx, JS_GetContextOpaque(cx)); + + request = ngx_pcalloc(pool, sizeof(ngx_js_request_t)); + if (request == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + rc = ngx_qjs_request_ctor(cx, request, &u, argc, argv); + if (rc != NGX_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 ngx_int_t +ngx_qjs_request_ctor(JSContext *cx, ngx_js_request_t *request, + ngx_url_t *u, int argc, JSValueConst *argv) +{ + JSValue input, init, value; + ngx_int_t rc; + ngx_pool_t *pool; + ngx_js_request_t *orig; + + input = argv[0]; + if (JS_IsUndefined(input)) { + JS_ThrowInternalError(cx, "1st argument is required"); + return NGX_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_js_request_t)); + + request->method.data = (u_char *) "GET"; + request->method.len = 3; + request->body.data = NULL; + request->body.len = 0; + request->headers.guard = GUARD_REQUEST; + ngx_qjs_arg(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_js_tb_elt_t)); + if (rc != NGX_OK) { + JS_ThrowOutOfMemory(cx); + return NGX_ERROR; + } + + if (JS_IsString(input)) { + rc = ngx_qjs_string(cx, input, &request->url); + if (rc != NGX_OK) { + JS_ThrowInternalError(cx, "failed to convert url arg"); + return NGX_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 NGX_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; + + rc = ngx_qjs_headers_inherit(cx, &request->headers, &orig->headers); + if (rc != NGX_OK) { + return NGX_ERROR; + } + } + + ngx_js_http_trim(&request->url.data, &request->url.len, 1); + + ngx_memzero(u, sizeof(ngx_url_t)); + + u->url = request->url; + u->default_port = 80; + u->uri_part = 1; + u->no_resolve = 1; + + if (u->url.len > 7 + && ngx_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 + && ngx_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 NGX_ERROR; + } + + if (ngx_parse_url(pool, u) != NGX_OK) { + JS_ThrowInternalError(cx, "invalid url"); + return NGX_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 NGX_ERROR; + } + + if (!JS_IsUndefined(value)) { + rc = ngx_qjs_string(cx, value, &request->method); + JS_FreeValue(cx, value); + + if (rc != NGX_OK) { + JS_ThrowInternalError(cx, "invalid Request method"); + return NGX_ERROR; + } + } + + rc = ngx_qjs_method_process(cx, request); + if (rc != NGX_OK) { + return NGX_ERROR; + } + + rc = ngx_qjs_fetch_flag_set(cx, ngx_qjs_fetch_cache_modes, init, + "cache"); + if (rc == NGX_ERROR) { + return NGX_ERROR; + } + + request->cache_mode = rc; + + rc = ngx_qjs_fetch_flag_set(cx, ngx_qjs_fetch_credentials, init, + "credentials"); + if (rc == NGX_ERROR) { + return NGX_ERROR; + } + + request->credentials = rc; + + rc = ngx_qjs_fetch_flag_set(cx, ngx_qjs_fetch_modes, init, "mode"); + if (rc == NGX_ERROR) { + return NGX_ERROR; + } + + request->mode = rc; + + value = JS_GetPropertyStr(cx, init, "headers"); + if (JS_IsException(value)) { + JS_ThrowInternalError(cx, "invalid Request headers"); + return NGX_ERROR; + } + + if (!JS_IsUndefined(value)) { + if (!JS_IsObject(value)) { + JS_ThrowInternalError(cx, "Headers is not an object"); + return NGX_ERROR; + } + + /* + * There are no API to reset or destroy ngx_list, + * just allocating a new one. + */ + + ngx_memset(&request->headers, 0, sizeof(ngx_js_headers_t)); + request->headers.guard = GUARD_REQUEST; + + rc = ngx_list_init(&request->headers.header_list, pool, 4, + sizeof(ngx_js_tb_elt_t)); + if (rc != NGX_OK) { + JS_FreeValue(cx, value); + JS_ThrowOutOfMemory(cx); + return NGX_ERROR; + } + + rc = ngx_qjs_headers_fill(cx, &request->headers, value); + JS_FreeValue(cx, value); + + if (rc != NGX_OK) { + return NGX_ERROR; + } + } + + value = JS_GetPropertyStr(cx, init, "body"); + if (JS_IsException(value)) { + JS_ThrowInternalError(cx, "invalid Request body"); + return NGX_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 NGX_ERROR; + } + + if (request->headers.content_type == NULL && JS_IsString(value)) { + rc = ngx_qjs_headers_append(cx, &request->headers, + (u_char *) "Content-Type", + sizeof("Content-Type") - 1, + (u_char *) "text/plain;charset=UTF-8", + sizeof("text/plain;charset=UTF-8") - 1); + if (rc != NGX_OK) { + JS_FreeValue(cx, value); + return NGX_ERROR; + } + } + + JS_FreeValue(cx, value); + } + } + + return NGX_OK; +} + + +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_int_t rc; + ngx_pool_t *pool; + ngx_js_ctx_t *ctx; + ngx_js_response_t *response; + + pool = ngx_qjs_external_pool(cx, JS_GetContextOpaque(cx)); + + response = ngx_pcalloc(pool, sizeof(ngx_js_response_t)); + if (response == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + /* + * set by ngx_pcalloc(): + * + * response->url.length = 0; + * response->status_text.length = 0; + */ + + response->code = 200; + response->headers.guard = GUARD_RESPONSE; + ngx_qjs_arg(response->header_value) = JS_UNDEFINED; + + ret = ngx_list_init(&response->headers.header_list, pool, 4, + sizeof(ngx_js_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, (int64_t *) &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"); + } + + rc = ngx_qjs_headers_fill(cx, &response->headers, value); + JS_FreeValue(cx, value); + + if (ret != NGX_OK) { + return JS_EXCEPTION; + } + } + } + + ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx)); + + NJS_CHB_MP_INIT(&response->chain, ctx->engine->pool); + + 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)) { + rc = ngx_qjs_headers_append(cx, &response->headers, + (u_char *) "Content-Type", + sizeof("Content-Type") - 1, + (u_char *) "text/plain;charset=UTF-8", + sizeof("text/plain;charset=UTF-8") - 1); + if (rc != NGX_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 u_char +ngx_js_upper_case(u_char c) +{ + return (u_char) ((c >= 'a' && c <= 'z') ? c & 0xDF : c); +} + + +static ngx_int_t +ngx_qjs_method_process(JSContext *cx, ngx_js_request_t *request) +{ + u_char *s; + const u_char *p; + const ngx_str_t *m; + + static const ngx_str_t forbidden[] = { + ngx_string("CONNECT"), + ngx_string("TRACE"), + ngx_string("TRACK"), + ngx_null_string, + }; + + static const ngx_str_t to_normalize[] = { + ngx_string("DELETE"), + ngx_string("GET"), + ngx_string("HEAD"), + ngx_string("OPTIONS"), + ngx_string("POST"), + ngx_string("PUT"), + ngx_null_string, + }; + + for (m = &forbidden[0]; m->len != 0; m++) { + if (request->method.len == m->len + && ngx_strncasecmp(request->method.data, m->data, m->len) == 0) + { + JS_ThrowInternalError(cx, "forbidden method: %.*s", + (int) m->len, m->data); + return NGX_ERROR; + } + } + + for (m = &to_normalize[0]; m->len != 0; m++) { + if (request->method.len == m->len + && ngx_strncasecmp(request->method.data, m->data, m->len) == 0) + { + s = &request->m[0]; + p = m->data; + + while (*p != '\0') { + *s++ = ngx_js_upper_case(*p++); + } + + request->method.data = &request->m[0]; + request->method.len = m->len; + break; + } + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_qjs_headers_inherit(JSContext *cx, ngx_js_headers_t *headers, + ngx_js_headers_t *orig) +{ + ngx_int_t rc; + ngx_uint_t i; + ngx_list_part_t *part; + ngx_js_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; + } + + rc = ngx_qjs_headers_append(cx, headers, h[i].key.data, h[i].key.len, + h[i].value.data, h[i].value.len); + if (rc != NGX_OK) { + return NGX_ERROR; + } + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_qjs_headers_fill_header_free(JSContext *cx, ngx_js_headers_t *headers, + JSValue prop_name, JSValue prop_value) +{ + ngx_int_t rc; + 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 NGX_ERROR; + } + + if (ngx_qjs_string(cx, prop_value, &value) != NGX_OK) { + JS_FreeValue(cx, prop_name); + JS_FreeValue(cx, prop_value); + return NGX_ERROR; + } + + rc = 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 rc; +} + + +static ngx_int_t +ngx_qjs_headers_fill(JSContext *cx, ngx_js_headers_t *headers, JSValue init) +{ + JSValue header, prop_name, prop_value; + uint32_t i, len, length; + ngx_int_t rc; + JSPropertyEnum *tab; + ngx_js_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 NGX_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 (qjs_array_length(cx, header, &length)) { + 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); + + rc = ngx_qjs_headers_fill_header_free(cx, headers, prop_name, + prop_value); + if (rc != NGX_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; + } + + rc = ngx_qjs_headers_fill_header_free(cx, headers, prop_name, + prop_value); + if (rc != NGX_OK) { + goto fail; + } + } + } + + qjs_free_prop_enum(cx, tab, len); + + return NGX_OK; + +fail: + + qjs_free_prop_enum(cx, tab, len); + + return NGX_ERROR; +} + + +static ngx_qjs_fetch_t * +ngx_qjs_fetch_alloc(JSContext *cx, ngx_pool_t *pool, ngx_log_t *log) +{ + ngx_js_ctx_t *ctx; + ngx_js_http_t *http; + ngx_qjs_fetch_t *fetch; + ngx_qjs_event_t *event; + + fetch = ngx_pcalloc(pool, sizeof(ngx_qjs_fetch_t)); + if (fetch == NULL) { + return NULL; + } + + http = &fetch->http; + + http->pool = pool; + http->log = log; + + http->timeout = 10000; + + http->http_parse.content_length_n = -1; + + ngx_qjs_arg(http->response.header_value) = JS_UNDEFINED; + + http->append_headers = ngx_qjs_fetch_append_headers; + http->ready_handler = ngx_qjs_fetch_process_done; + http->error_handler = ngx_qjs_fetch_error; + + fetch->promise = JS_NewPromiseCapability(cx, fetch->promise_callbacks); + if (JS_IsException(fetch->promise)) { + return NULL; + } + + event = ngx_palloc(pool, sizeof(ngx_qjs_event_t)); + if (event == NULL) { + goto fail; + } + + ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx)); + + event->ctx = cx; + event->destructor = ngx_qjs_fetch_destructor; + event->fd = ctx->event_id++; + event->data = fetch; + + ngx_js_add_event(ctx, event); + + fetch->cx = cx; + fetch->event = event; + + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "js http alloc:%p", fetch); + + return fetch; + +fail: + + JS_FreeValue(cx, fetch->promise); + JS_FreeValue(cx, fetch->promise_callbacks[0]); + JS_FreeValue(cx, fetch->promise_callbacks[1]); + + JS_ThrowInternalError(cx, "internal error"); + + return NULL; +} + + +static void +ngx_qjs_fetch_error(ngx_js_http_t *http, const char *err) +{ + ngx_qjs_fetch_t *fetch; + + fetch = (ngx_qjs_fetch_t *) http; + + JS_ThrowInternalError(fetch->cx, "%s", err); + + fetch->response_value = JS_GetException(fetch->cx); + + ngx_qjs_fetch_done(fetch, fetch->response_value, NGX_ERROR); +} + + +static void +ngx_qjs_fetch_destructor(ngx_qjs_event_t *event) +{ + JSContext *cx; + ngx_js_http_t *http; + ngx_qjs_fetch_t *fetch; + + cx = event->ctx; + fetch = event->data; + http = &fetch->http; + + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, "js http destructor:%p", + fetch); + + ngx_js_http_resolve_done(http); + ngx_js_http_close_peer(http); + + JS_FreeValue(cx, fetch->promise_callbacks[0]); + JS_FreeValue(cx, fetch->promise_callbacks[1]); + JS_FreeValue(cx, fetch->promise); + JS_FreeValue(cx, fetch->response_value); +} + + +static void +ngx_qjs_fetch_done(ngx_qjs_fetch_t *fetch, JSValue retval, ngx_int_t rc) +{ + void *external; + JSValue action; + JSContext *cx; + ngx_js_ctx_t *ctx; + ngx_js_http_t *http; + ngx_qjs_event_t *event; + + http = &fetch->http; + + ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0, + "js http done fetch:%p rc:%i", fetch, rc); + + ngx_js_http_close_peer(http); + + if (fetch->event != NULL) { + action = fetch->promise_callbacks[(rc != NGX_OK)]; + + cx = fetch->cx; + event = fetch->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 ngx_int_t +ngx_qjs_fetch_append_headers(ngx_js_http_t *http, ngx_js_headers_t *headers, + u_char *name, size_t len, u_char *value, size_t vlen) +{ + ngx_qjs_fetch_t *fetch; + + fetch = (ngx_qjs_fetch_t *) http; + + return ngx_qjs_headers_append(fetch->cx, &http->response.headers, + name, len, value, vlen); +} + + +static void +ngx_qjs_fetch_process_done(ngx_js_http_t *http) +{ + ngx_qjs_fetch_t *fetch; + + fetch = (ngx_qjs_fetch_t *) http; + + fetch->response_value = JS_NewObjectClass(fetch->cx, + NGX_QJS_CLASS_ID_FETCH_RESPONSE); + if (JS_IsException(fetch->response_value)) { + ngx_qjs_fetch_error(http, "fetch response creation failed"); + return; + } + + JS_SetOpaque(fetch->response_value, &http->response); + + ngx_qjs_fetch_done(fetch, fetch->response_value, NGX_OK); +} + + +static ngx_int_t +ngx_qjs_headers_append(JSContext *cx, ngx_js_headers_t *headers, + u_char *name, size_t len, u_char *value, size_t vlen) +{ + u_char *p, *end; + ngx_int_t ret; + ngx_uint_t i; + ngx_list_part_t *part; + ngx_js_tb_elt_t *h, **ph; + + ngx_js_http_trim(&value, &vlen, 0); + + ret = ngx_js_check_header_name(name, len); + if (ret != NGX_OK) { + JS_ThrowInternalError(cx, "invalid header name"); + return NGX_ERROR; + } + + p = value; + end = p + vlen; + + while (p < end) { + if (*p == '\0') { + JS_ThrowInternalError(cx, "invalid header value"); + return NGX_ERROR; + } + + p++; + } + + if (headers->guard == GUARD_IMMUTABLE) { + JS_ThrowInternalError(cx, "cannot append to immutable object"); + return NGX_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 + && (ngx_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 NGX_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 == (sizeof("Content-Type") - 1) + && ngx_strncasecmp(name, (u_char *) "Content-Type", len) == 0) + { + headers->content_type = h; + } + + return NGX_OK; +} + + +static JSValue +ngx_qjs_headers_ext_keys(JSContext *cx, JSValue value) +{ + int ret, found; + JSValue keys, key, item, func, retval; + uint32_t length; + ngx_str_t hdr; + ngx_uint_t i, k, n; + ngx_list_part_t *part; + ngx_js_tb_elt_t *h; + ngx_js_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 (qjs_array_length(cx, keys, &length)) { + goto fail; + } + + for (k = 0; k < length; k++) { + key = JS_GetPropertyUint32(cx, keys, k); + if (JS_IsException(key)) { + goto fail; + } + + hdr.data = (u_char *) JS_ToCStringLen(cx, &hdr.len, key); + JS_FreeValue(cx, key); + + found = h[i].key.len == hdr.len + && ngx_strncasecmp(h[i].key.data, + hdr.data, hdr.len) == 0; + + JS_FreeCString(cx, (const char *) hdr.data); + + 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"); + if (JS_IsException(func)) { + JS_ThrowInternalError(cx, "sort function not found"); + goto fail; + } + + 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_headers_get(JSContext *cx, JSValue this_val, ngx_str_t *name, + int as_array) +{ + int ret; + JSValue retval, value; + njs_chb_t chain; + ngx_uint_t i; + ngx_list_part_t *part; + ngx_js_tb_elt_t *h, *ph; + ngx_js_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 + && ngx_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 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_js_tb_elt_t *h; + ngx_js_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 JSValue +ngx_qjs_ext_fetch_headers_append(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + ngx_int_t rc; + ngx_js_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"); + } + + rc = ngx_qjs_headers_fill_header_free(cx, headers, + JS_DupValue(cx, argv[0]), + JS_DupValue(cx, argv[1])); + if (rc != NGX_OK) { + return JS_EXCEPTION; + } + + return JS_UNDEFINED; +} + + +static JSValue +ngx_qjs_ext_fetch_headers_delete(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + ngx_int_t rc; + ngx_str_t name; + ngx_uint_t i; + ngx_list_part_t *part; + ngx_js_tb_elt_t *h; + ngx_js_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"); + } + + rc = ngx_qjs_string(cx, argv[0], &name); + if (rc != NGX_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 + && (ngx_strncasecmp(name.data, h[i].key.data, name.len) == 0)) + { + h[i].hash = 0; + } + } + + if (name.len == (sizeof("Content-Type") - 1) + && ngx_strncasecmp(name.data, (u_char *) "Content-Type", name.len) + == 0) + { + headers->content_type = NULL; + } + + return JS_UNDEFINED; +} + + +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_js_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 (qjs_array_length(cx, keys, &length)) { + 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 != NGX_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_headers_get(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv, int magic) +{ + ngx_int_t rc; + ngx_str_t name; + + rc = ngx_qjs_string(cx, argv[0], &name); + if (rc != NGX_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; + ngx_int_t rc; + ngx_str_t name; + + rc = ngx_qjs_string(cx, argv[0], &name); + if (rc != NGX_OK) { + return JS_EXCEPTION; + } + + retval = ngx_qjs_headers_get(cx, this_val, &name, 0); + if (JS_IsException(retval)) { + return JS_EXCEPTION; + } + + rc = !JS_IsNull(retval); + JS_FreeValue(cx, retval); + + return JS_NewBool(cx, rc); +} + + +static JSValue +ngx_qjs_ext_fetch_headers_set(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + ngx_int_t rc; + ngx_str_t name, value; + ngx_uint_t i; + ngx_list_part_t *part; + ngx_js_tb_elt_t *h, **ph, **pp; + ngx_js_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"); + } + + rc = ngx_qjs_string(cx, argv[0], &name); + if (rc != NGX_OK) { + return JS_EXCEPTION; + } + + rc = ngx_qjs_string(cx, argv[1], &value); + if (rc != NGX_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 + && (ngx_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; + } + } + + rc = ngx_qjs_headers_append(cx, headers, name.data, name.len, + value.data, value.len); + if (rc != NGX_OK) { + return JS_EXCEPTION; + } + + return JS_UNDEFINED; +} + + +static JSValue +ngx_qjs_ext_fetch_request_body(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv, int magic) +{ + char * string; + JSValue result; + ngx_js_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_ARRAY_BUFFER: + /* + * no free_func for JS_NewArrayBuffer() + * because request->body is allocated from e->pool + * and will be freed when context is freed. + */ + result = JS_NewArrayBuffer(cx, request->body.data, request->body.len, + NULL, NULL, 0); + 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, request->body.data, request->body.len); + if (JS_IsException(result)) { + return JS_ThrowOutOfMemory(cx); + } + + if (magic == NGX_QJS_BODY_JSON) { + string = js_malloc(cx, request->body.len + 1); + + JS_FreeValue(cx, result); + result = JS_UNDEFINED; + + if (string == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + ngx_memcpy(string, request->body.data, request->body.len); + string[request->body.len] = '\0'; + + /* 'string' must be zero terminated. */ + result = JS_ParseJSON(cx, string, request->body.len, "<input>"); + js_free(cx, string); + if (JS_IsException(result)) { + break; + } + } + } + + return qjs_promise_result(cx, result); +} + + +static JSValue +ngx_qjs_ext_fetch_request_body_used(JSContext *cx, JSValueConst this_val) +{ + ngx_js_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_js_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, + request->cache_mode); +} + + +static JSValue +ngx_qjs_ext_fetch_request_credentials(JSContext *cx, JSValueConst this_val) +{ + ngx_js_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, + request->credentials); +} + + +static JSValue +ngx_qjs_ext_fetch_request_headers(JSContext *cx, JSValueConst this_val) +{ + JSValue header; + ngx_js_request_t *request; + + request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + if (request == NULL) { + return JS_UNDEFINED; + } + + header = ngx_qjs_arg(request->header_value); + + if (JS_IsUndefined(header)) { + header = JS_NewObjectClass(cx, NGX_QJS_CLASS_ID_FETCH_HEADERS); + if (JS_IsException(header)) { + return JS_ThrowInternalError(cx, "fetch header creation failed"); + } + + JS_SetOpaque(header, &request->headers); + + ngx_qjs_arg(request->header_value) = header; + } + + return JS_DupValue(cx, header); +} + + +static JSValue +ngx_qjs_ext_fetch_request_field(JSContext *cx, JSValueConst this_val, int magic) +{ + ngx_str_t *field; + ngx_js_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_mode(JSContext *cx, JSValueConst this_val) +{ + ngx_js_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, request->mode); +} + + +static void +ngx_qjs_fetch_request_finalizer(JSRuntime *rt, JSValue val) +{ + ngx_js_request_t *request; + + request = JS_GetOpaque(val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + + JS_FreeValueRT(rt, ngx_qjs_arg(request->header_value)); +} + + +static JSValue +ngx_qjs_ext_fetch_response_status(JSContext *cx, JSValueConst this_val) +{ + ngx_js_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_js_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_ok(JSContext *cx, JSValueConst this_val) +{ + ngx_js_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_body_used(JSContext *cx, JSValueConst this_val) +{ + ngx_js_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_headers(JSContext *cx, JSValueConst this_val) +{ + JSValue header; + ngx_js_response_t *response; + + response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + if (response == NULL) { + return JS_UNDEFINED; + } + + header = ngx_qjs_arg(response->header_value); + + if (JS_IsUndefined(header)) { + header = JS_NewObjectClass(cx, NGX_QJS_CLASS_ID_FETCH_HEADERS); + if (JS_IsException(header)) { + return JS_ThrowInternalError(cx, "fetch header creation failed"); + } + + JS_SetOpaque(header, &response->headers); + + ngx_qjs_arg(response->header_value) = header; + } + + return JS_DupValue(cx, header); +} + + +static JSValue +ngx_qjs_ext_fetch_response_type(JSContext *cx, JSValueConst this_val) +{ + ngx_js_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_body(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv, int magic) +{ + JSValue result; + njs_int_t ret; + njs_str_t string; + ngx_js_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; + + switch (magic) { + case NGX_QJS_BODY_ARRAY_BUFFER: + case NGX_QJS_BODY_TEXT: + ret = njs_chb_join(&response->chain, &string); + if (ret != NJS_OK) { + return JS_ThrowOutOfMemory(cx); + } + + if (magic == NGX_QJS_BODY_TEXT) { + result = qjs_string_create(cx, string.start, string.length); + if (JS_IsException(result)) { + return JS_ThrowOutOfMemory(cx); + } + + break; + } + + /* + * no free_func for JS_NewArrayBuffer() + * because string.start is allocated from e->pool + * and will be freed when context is freed. + */ + result = JS_NewArrayBuffer(cx, string.start, string.length, NULL, NULL, + 0); + if (JS_IsException(result)) { + return JS_ThrowOutOfMemory(cx); + } + + break; + + case NGX_QJS_BODY_JSON: + default: + /* 'string.start' must be zero terminated. */ + njs_chb_append_literal(&response->chain, "\0"); + ret = njs_chb_join(&response->chain, &string); + if (ret != NJS_OK) { + return JS_ThrowOutOfMemory(cx); + } + + result = JS_ParseJSON(cx, (char *) string.start, string.length - 1, + "<input>"); + if (JS_IsException(result)) { + break; + } + } + + return qjs_promise_result(cx, result); +} + + +static JSValue +ngx_qjs_ext_fetch_response_redirected(JSContext *cx, JSValueConst this_val) +{ + ngx_js_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 JSValue +ngx_qjs_ext_fetch_response_field(JSContext *cx, JSValueConst this_val, int magic) +{ + ngx_str_t *field; + ngx_js_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 void +ngx_qjs_fetch_response_finalizer(JSRuntime *rt, JSValue val) +{ + ngx_js_response_t *response; + + response = JS_GetOpaque(val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + + JS_FreeValueRT(rt, ngx_qjs_arg(response->header_value)); + njs_chb_destroy(&response->chain); +} + + +static JSValue +ngx_qjs_fetch_flag(JSContext *cx, const ngx_qjs_entry_t *entries, + ngx_int_t value) +{ + const ngx_qjs_entry_t *e; + + for (e = entries; e->name.len != 0; e++) { + if (e->value == value) { + return qjs_string_create(cx, e->name.data, e->name.len); + } + } + + return JS_ThrowInternalError(cx, "unknown fetch flag: %i", (int) value); +} + + +static ngx_int_t +ngx_qjs_fetch_flag_set(JSContext *cx, const ngx_qjs_entry_t *entries, + JSValue object, const char *prop) +{ + JSValue value; + ngx_int_t rc; + ngx_str_t flag; + const ngx_qjs_entry_t *e; + + value = JS_GetPropertyStr(cx, object, prop); + if (JS_IsException(value)) { + JS_ThrowInternalError(cx, "failed to get %s property", prop); + return NGX_ERROR; + } + + if (JS_IsUndefined(value)) { + return entries[0].value; + } + + rc = ngx_qjs_string(cx, value, &flag); + JS_FreeValue(cx, value); + if (rc != NGX_OK) { + return NGX_ERROR; + } + + for (e = entries; e->name.len != 0; e++) { + if (flag.len == e->name.len + && ngx_strncasecmp(e->name.data, flag.data, flag.len) == 0) + { + return e->value; + } + } + + JS_ThrowInternalError(cx, "unknown %s type: %.*s", prop, + (int) flag.len, flag.data); + + return NGX_ERROR; +} + + +static JSModuleDef * +ngx_qjs_fetch_init(JSContext *cx, const char *name) +{ + int i, class_id; + JSValue global_obj, proto, class; + + static const JSClassDef *const classes[] = { + &ngx_qjs_fetch_headers_class, + &ngx_qjs_fetch_request_class, + &ngx_qjs_fetch_response_class, + NULL + }; + + static JSCFunction *ctors[] = { + ngx_qjs_fetch_headers_ctor, + ngx_qjs_fetch_request_ctor, + ngx_qjs_fetch_response_ctor, + NULL + }; + + static const JSCFunctionListEntry *const protos[] = { + ngx_qjs_ext_fetch_headers_proto, + ngx_qjs_ext_fetch_request_proto, + ngx_qjs_ext_fetch_response_proto, + NULL + }; + + static const uint8_t proto_sizes[] = { + njs_nitems(ngx_qjs_ext_fetch_headers_proto), + njs_nitems(ngx_qjs_ext_fetch_request_proto), + njs_nitems(ngx_qjs_ext_fetch_response_proto), + 0 + }; + + global_obj = JS_GetGlobalObject(cx); + + for (i = 0; classes[i] != NULL; i++) { + class_id = NGX_QJS_CLASS_ID_FETCH_HEADERS + i; + + if (JS_NewClass(JS_GetRuntime(cx), class_id, classes[i]) < 0) { + return NULL; + } + + proto = JS_NewObject(cx); + if (JS_IsException(proto)) { + JS_FreeValue(cx, global_obj); + return NULL; + } + + JS_SetPropertyFunctionList(cx, proto, protos[i], proto_sizes[i]); + + class = JS_NewCFunction2(cx, ctors[i], classes[i]->class_name, 2, + JS_CFUNC_constructor, 0); + if (JS_IsException(class)) { + JS_FreeValue(cx, proto); + JS_FreeValue(cx, global_obj); + return NULL; + } + + JS_SetConstructor(cx, class, proto); + JS_SetClassProto(cx, class_id, proto); + + if (JS_SetPropertyStr(cx, global_obj, classes[i]->class_name, class) + < 0) + { + JS_FreeValue(cx, class); + JS_FreeValue(cx, proto); + JS_FreeValue(cx, global_obj); + return NULL; + } + } + + JS_FreeValue(cx, global_obj); + + return JS_NewCModule(cx, name, NULL); +} diff --git a/nginx/ngx_stream_js_module.c b/nginx/ngx_stream_js_module.c index 5c837494..0e022eb0 100644 --- a/nginx/ngx_stream_js_module.c +++ b/nginx/ngx_stream_js_module.c @@ -834,6 +834,7 @@ static JSClassDef ngx_stream_qjs_variables_class = { qjs_module_t *njs_stream_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/t/js_fetch.t b/nginx/t/js_fetch.t index ae9d1f61..7ee1a602 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 9a44a339..8ede1048 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 67cabdfc..bc5cc7ed 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; } } @@ -386,6 +388,17 @@ $t->write_file('test.js', <<EOF); var body = await r.text(); return `\${body}: \${r.headers.get('Content-Type')}`; }, 'ABC: text/plain;charset=UTF-8'], + ['arrayBuffer()', async () => { + var r = new Request("http://nginx.org", {body: 'ABC'}); + var body = await r.arrayBuffer(); + body = new Uint8Array(body); + return new TextDecoder().decode(body); + }, 'ABC'], + ['json()', async () => { + var r = new Request("http://nginx.org", {body: '{"a": 42}'}); + var body = await r.json(); + return body.a; + }, 42], ['user content type', async () => { var r = new Request("http://nginx.org", {body: 'ABC', @@ -443,6 +456,12 @@ $t->write_file('test.js', <<EOF); var body = await r.text(); return `\${r.url}: \${body} \${r.headers.get('b')}`; }, ': Y'], + ['arrayBuffer', async () => { + var r = new Response('foo'); + var body = await r.arrayBuffer(); + body = new Uint8Array(body); + return new TextDecoder().decode(body); + }, 'foo'], ['json', async () => { var r = new Response('{"a": {"b": 42}}'); var json = await r.json(); @@ -515,8 +534,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 7cea3386..031ff43c 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 5b207b90..ab1ba24a 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 4c97e04d..f98b4d8c 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); diff --git a/nginx/t/js_periodic_fetch.t b/nginx/t/js_periodic_fetch.t index d7bcfb76..0231b662 100644 --- a/nginx/t/js_periodic_fetch.t +++ b/nginx/t/js_periodic_fetch.t @@ -121,8 +121,6 @@ EOF $t->try_run('no js_periodic with fetch'); -plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; - $t->plan(3); ############################################################################### diff --git a/nginx/t/stream_js_fetch.t b/nginx/t/stream_js_fetch.t index c57128a8..9a42ae29 100644 --- a/nginx/t/stream_js_fetch.t +++ b/nginx/t/stream_js_fetch.t @@ -171,8 +171,6 @@ EOF $t->try_run('no stream njs available'); -plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; - $t->plan(9); $t->run_daemon(\&stream_daemon, port(8090), port(8091)); diff --git a/nginx/t/stream_js_fetch_https.t b/nginx/t/stream_js_fetch_https.t index 5d7c5c20..987a896a 100644 --- a/nginx/t/stream_js_fetch_https.t +++ b/nginx/t/stream_js_fetch_https.t @@ -273,8 +273,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(6); $t->run_daemon(\&dns_daemon, port(8981), $t); diff --git a/nginx/t/stream_js_fetch_init.t b/nginx/t/stream_js_fetch_init.t index 3f6d7262..f48b9d5e 100644 --- a/nginx/t/stream_js_fetch_init.t +++ b/nginx/t/stream_js_fetch_init.t @@ -92,8 +92,6 @@ EOF $t->try_run('no stream njs available'); -plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; - $t->plan(1); $t->run_daemon(\&stream_daemon, port(8090)); diff --git a/nginx/t/stream_js_periodic_fetch.t b/nginx/t/stream_js_periodic_fetch.t index e88d69d5..60599423 100644 --- a/nginx/t/stream_js_periodic_fetch.t +++ b/nginx/t/stream_js_periodic_fetch.t @@ -147,7 +147,6 @@ EOF $t->run_daemon(\&stream_daemon, port(8090)); $t->try_run('no js_periodic with fetch'); -plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; $t->plan(3); $t->waitforsocket('127.0.0.1:' . port(8090)); @@ -1147,6 +1147,31 @@ qjs_string_base64url(JSContext *cx, const njs_str_t *src) } +int +qjs_array_length(JSContext *cx, JSValueConst arr, uint32_t *plen) +{ + 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; +} + + static JSValue qjs_promise_fill_trampoline(JSContext *cx, int argc, JSValueConst *argv) { @@ -136,6 +136,8 @@ JSValue qjs_string_create_chb(JSContext *cx, njs_chb_t *chain); void qjs_free_prop_enum(JSContext *ctx, JSPropertyEnum *tab, uint32_t len); +int qjs_array_length(JSContext *cx, JSValueConst arr, uint32_t *plen); + JSValue qjs_promise_result(JSContext *cx, JSValue result); JSValue qjs_string_hex(JSContext *cx, const njs_str_t *src); |