diff --git a/nginx/config b/nginx/config index 03ec03d0c..1c303d9c0 100644 --- a/nginx/config +++ b/nginx/config @@ -6,9 +6,11 @@ NJS_ZLIB=${NJS_ZLIB:-YES} NJS_QUICKJS=${NJS_QUICKJS:-YES} NJS_DEPS="$ngx_addon_dir/ngx_js.h \ + $ngx_addon_dir/ngx_js_http.h \ $ngx_addon_dir/ngx_js_fetch.h \ $ngx_addon_dir/ngx_js_shared_dict.h" NJS_SRCS="$ngx_addon_dir/ngx_js.c \ + $ngx_addon_dir/ngx_js_http.c \ $ngx_addon_dir/ngx_js_fetch.c \ $ngx_addon_dir/ngx_js_regex.c \ $ngx_addon_dir/ngx_js_shared_dict.c" @@ -154,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 5a131bc9a..8b22c8e56 100644 --- a/nginx/ngx_http_js_module.c +++ b/nginx/ngx_http_js_module.c @@ -1133,6 +1133,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 316a2076c..06e089c7c 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), @@ -1502,6 +1503,31 @@ ngx_qjs_string(JSContext *cx, JSValueConst val, ngx_str_t *dst) } +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; +} + + static void ngx_qjs_timer_handler(ngx_event_t *ev) { @@ -4487,3 +4513,19 @@ ngx_js_queue_pop(ngx_js_queue_t *queue) return item; } + + +ngx_int_t +ngx_njs_string(njs_vm_t *vm, njs_value_t *value, ngx_str_t *str) +{ + njs_str_t s; + + if (ngx_js_string(vm, value, &s) != NGX_OK) { + return NGX_ERROR; + } + + str->data = s.start; + str->len = s.length; + + return NGX_OK; +} diff --git a/nginx/ngx_js.h b/nginx/ngx_js.h index 0a99a6964..37ac6fdc7 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(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); +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) \ @@ -382,6 +389,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 @@ -422,6 +430,8 @@ ngx_js_queue_t *ngx_js_queue_create(ngx_pool_t *pool, ngx_uint_t capacity); ngx_int_t ngx_js_queue_push(ngx_js_queue_t *queue, void *item); void *ngx_js_queue_pop(ngx_js_queue_t *queue); +ngx_int_t ngx_njs_string(njs_vm_t *vm, njs_value_t *value, ngx_str_t *str); + extern njs_module_t ngx_js_ngx_module; extern njs_module_t njs_webcrypto_module; diff --git a/nginx/ngx_js_fetch.c b/nginx/ngx_js_fetch.c index 9539a687f..063631945 100644 --- a/nginx/ngx_js_fetch.c +++ b/nginx/ngx_js_fetch.c @@ -12,9 +12,7 @@ #include #include #include "ngx_js.h" - - -typedef struct ngx_js_http_s ngx_js_http_t; +#include "ngx_js_http.h" typedef struct { @@ -24,136 +22,16 @@ typedef struct { 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_js_http_parse_t; - - -typedef struct { - u_char *pos; - uint64_t chunk_size; - uint8_t state; - uint8_t last; -} ngx_js_http_chunk_parse_t; - - -typedef struct ngx_js_tb_elt_s ngx_js_tb_elt_t; - -struct ngx_js_tb_elt_s { - ngx_uint_t hash; - ngx_str_t key; - ngx_str_t value; - ngx_js_tb_elt_t *next; -}; - - -typedef struct { - enum { - GUARD_NONE = 0, - GUARD_REQUEST, - GUARD_IMMUTABLE, - GUARD_RESPONSE, - } guard; - ngx_list_t header_list; - ngx_js_tb_elt_t *content_type; -} ngx_js_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; - njs_str_t url; - njs_str_t method; - u_char m[8]; - uint8_t body_used; - njs_str_t body; - ngx_js_headers_t headers; - njs_opaque_value_t header_value; -} ngx_js_request_t; - - -typedef struct { - njs_str_t url; - ngx_int_t code; - njs_str_t status_text; - uint8_t body_used; - njs_chb_t chain; - ngx_js_headers_t headers; - njs_opaque_value_t header_value; -} ngx_js_response_t; - - -struct ngx_js_http_s { - ngx_log_t *log; - ngx_pool_t *pool; + ngx_js_http_t http; njs_vm_t *vm; ngx_js_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_js_response_t response; njs_opaque_value_t response_value; njs_opaque_value_t promise; njs_opaque_value_t promise_callbacks[2]; - - uint8_t done; - ngx_js_http_parse_t http_parse; - ngx_js_http_chunk_parse_t http_chunk_parse; - ngx_int_t (*process)(ngx_js_http_t *http); -}; +} ngx_js_fetch_t; static njs_int_t ngx_js_method_process(njs_vm_t *vm, ngx_js_request_t *r); @@ -161,44 +39,29 @@ static njs_int_t ngx_js_headers_inherit(njs_vm_t *vm, ngx_js_headers_t *headers, ngx_js_headers_t *orig); static njs_int_t ngx_js_headers_fill(njs_vm_t *vm, ngx_js_headers_t *headers, njs_value_t *init); -static ngx_js_http_t *ngx_js_http_alloc(njs_vm_t *vm, ngx_pool_t *pool, +static ngx_js_fetch_t *ngx_js_fetch_alloc(njs_vm_t *vm, ngx_pool_t *pool, ngx_log_t *log); -static void ngx_js_http_resolve_done(ngx_js_http_t *http); -static void ngx_js_http_close_peer(ngx_js_http_t *http); -static void ngx_js_http_destructor(ngx_js_event_t *event); -static ngx_resolver_ctx_t *ngx_js_http_resolve(ngx_js_http_t *http, - ngx_resolver_t *r, ngx_str_t *host, in_port_t port, ngx_msec_t timeout); -static void ngx_js_http_resolve_handler(ngx_resolver_ctx_t *ctx); +static void ngx_js_fetch_error(ngx_js_http_t *http, const char *err); +static void ngx_js_fetch_destructor(ngx_js_event_t *event); static njs_int_t ngx_js_fetch_promissified_result(njs_vm_t *vm, njs_value_t *result, njs_int_t rc, njs_value_t *retval); -static void ngx_js_http_fetch_done(ngx_js_http_t *http, - njs_opaque_value_t *retval, njs_int_t rc); +static void ngx_js_fetch_done(ngx_js_fetch_t *fetch, njs_opaque_value_t *retval, + njs_int_t rc); static njs_int_t ngx_js_http_promise_trampoline(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval); -static void ngx_js_http_connect(ngx_js_http_t *http); -static void ngx_js_http_next(ngx_js_http_t *http); -static void ngx_js_http_write_handler(ngx_event_t *wev); -static void ngx_js_http_read_handler(ngx_event_t *rev); static njs_int_t ngx_js_request_constructor(njs_vm_t *vm, ngx_js_request_t *request, ngx_url_t *u, njs_external_ptr_t external, njs_value_t *args, njs_uint_t nargs); +static ngx_int_t ngx_js_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_js_fetch_process_done(ngx_js_http_t *http); static njs_int_t ngx_js_headers_append(njs_vm_t *vm, ngx_js_headers_t *headers, u_char *name, size_t len, u_char *value, size_t vlen); -static ngx_int_t ngx_js_http_process_status_line(ngx_js_http_t *http); -static ngx_int_t ngx_js_http_process_headers(ngx_js_http_t *http); -static ngx_int_t ngx_js_http_process_body(ngx_js_http_t *http); -static ngx_int_t ngx_js_http_parse_status_line(ngx_js_http_parse_t *hp, - ngx_buf_t *b); -static ngx_int_t ngx_js_http_parse_header_line(ngx_js_http_parse_t *hp, - ngx_buf_t *b); -static ngx_int_t ngx_js_http_parse_chunked(ngx_js_http_chunk_parse_t *hcp, - ngx_buf_t *b, njs_chb_t *chain); -static void ngx_js_http_dummy_handler(ngx_event_t *ev); - static njs_int_t ngx_headers_js_ext_append(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval); static njs_int_t ngx_headers_js_ext_delete(njs_vm_t *vm, njs_value_t *args, @@ -253,15 +116,6 @@ static njs_int_t ngx_response_js_ext_type(njs_vm_t *vm, static njs_int_t ngx_response_js_ext_body(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval); -#if (NGX_SSL) -static void ngx_js_http_ssl_init_connection(ngx_js_http_t *http); -static void ngx_js_http_ssl_handshake_handler(ngx_connection_t *c); -static void ngx_js_http_ssl_handshake(ngx_js_http_t *http); -static njs_int_t ngx_js_http_ssl_name(ngx_js_http_t *http); -#endif - -static void ngx_js_http_trim(u_char **value, size_t *len, - njs_bool_t trim_c0_control_or_space); static njs_int_t ngx_fetch_flag(njs_vm_t *vm, const ngx_js_entry_t *entries, njs_int_t value, njs_value_t *retval); static njs_int_t ngx_fetch_flag_set(njs_vm_t *vm, const ngx_js_entry_t *entries, @@ -656,12 +510,14 @@ ngx_js_ext_fetch(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval) { 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; njs_value_t *init, *value; ngx_js_http_t *http; + ngx_js_fetch_t *fetch; ngx_list_part_t *part; ngx_js_tb_elt_t *h; ngx_js_request_t request; @@ -680,11 +536,13 @@ ngx_js_ext_fetch(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, c = ngx_external_connection(vm, external); pool = ngx_external_pool(vm, external); - http = ngx_js_http_alloc(vm, pool, c->log); - if (http == NULL) { + fetch = ngx_js_fetch_alloc(vm, pool, c->log); + if (fetch == NULL) { return NJS_ERROR; } + http = &fetch->http; + ret = ngx_js_request_constructor(vm, &request, &u, external, args, nargs); if (ret != NJS_OK) { goto fail; @@ -730,11 +588,15 @@ ngx_js_ext_fetch(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, #endif } - http->header_only = njs_strstr_eq(&request.method, &njs_str_value("HEAD")); + str.start = request.method.data; + str.length = request.method.len; + + http->header_only = njs_strstr_eq(&str, &njs_str_value("HEAD")); NJS_CHB_MP_INIT(&http->chain, vm); + NJS_CHB_MP_INIT(&http->response.chain, vm); - njs_chb_append(&http->chain, request.method.start, request.method.length); + 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] != '/') { @@ -824,10 +686,10 @@ ngx_js_ext_fetch(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, http->tls_name.len = u.host.len; #endif - if (request.body.length != 0) { + if (request.body.len != 0) { njs_chb_sprintf(&http->chain, 32, "Content-Length: %uz" CRLF CRLF, - request.body.length); - njs_chb_append(&http->chain, request.body.start, request.body.length); + request.body.len); + njs_chb_append(&http->chain, request.body.data, request.body.len); } else { njs_chb_append_literal(&http->chain, CRLF); @@ -847,7 +709,7 @@ ngx_js_ext_fetch(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, goto fail; } - njs_value_assign(retval, njs_value_arg(&http->promise)); + njs_value_assign(retval, njs_value_arg(&fetch->promise)); return NJS_OK; } @@ -858,18 +720,17 @@ ngx_js_ext_fetch(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, ngx_js_http_connect(http); - njs_value_assign(retval, njs_value_arg(&http->promise)); + njs_value_assign(retval, njs_value_arg(&fetch->promise)); return NJS_OK; fail: - njs_vm_exception_get(vm, njs_value_arg(&lvalue)); - ngx_js_http_fetch_done(http, &lvalue, NJS_ERROR); + ngx_js_fetch_done(fetch, &lvalue, NJS_ERROR); - njs_value_assign(retval, njs_value_arg(&http->promise)); + njs_value_assign(retval, njs_value_arg(&fetch->promise)); return NJS_OK; } @@ -1003,13 +864,13 @@ ngx_js_ext_response_constructor(njs_vm_t *vm, njs_value_t *args, value = njs_vm_object_prop(vm, init, &status_text, &lvalue); if (value != NULL) { - if (ngx_js_string(vm, value, &response->status_text) != NGX_OK) { + if (ngx_njs_string(vm, value, &response->status_text) != NGX_OK) { njs_vm_error(vm, "invalid Response statusText"); return NJS_ERROR; } - p = response->status_text.start; - end = p + response->status_text.length; + p = response->status_text.data; + end = p + response->status_text.len; while (p < end) { if (*p != '\t' && *p < ' ') { @@ -1069,6 +930,7 @@ static njs_int_t ngx_js_method_process(njs_vm_t *vm, ngx_js_request_t *request) { u_char *s, *p; + njs_str_t str; const njs_str_t *m; static const njs_str_t forbidden[] = { @@ -1088,15 +950,18 @@ ngx_js_method_process(njs_vm_t *vm, ngx_js_request_t *request) njs_null_str, }; + str.start = request->method.data; + str.length = request->method.len; + for (m = &forbidden[0]; m->length != 0; m++) { - if (njs_strstr_case_eq(&request->method, m)) { + if (njs_strstr_case_eq(&str, m)) { njs_vm_error(vm, "forbidden method: %V", m); return NJS_ERROR; } } for (m = &to_normalize[0]; m->length != 0; m++) { - if (njs_strstr_case_eq(&request->method, m)) { + if (njs_strstr_case_eq(&str, m)) { s = &request->m[0]; p = m->start; @@ -1104,8 +969,8 @@ ngx_js_method_process(njs_vm_t *vm, ngx_js_request_t *request) *s++ = njs_upper_case(*p++); } - request->method.start = &request->m[0]; - request->method.length = m->length; + request->method.data = &request->m[0]; + request->method.len = m->length; break; } } @@ -1248,30 +1113,36 @@ ngx_js_headers_fill(njs_vm_t *vm, ngx_js_headers_t *headers, njs_value_t *init) } -static ngx_js_http_t * -ngx_js_http_alloc(njs_vm_t *vm, ngx_pool_t *pool, ngx_log_t *log) +static ngx_js_fetch_t * +ngx_js_fetch_alloc(njs_vm_t *vm, ngx_pool_t *pool, ngx_log_t *log) { njs_int_t ret; ngx_js_ctx_t *ctx; ngx_js_http_t *http; + ngx_js_fetch_t *fetch; ngx_js_event_t *event; njs_function_t *callback; - http = ngx_pcalloc(pool, sizeof(ngx_js_http_t)); - if (http == NULL) { + fetch = ngx_pcalloc(pool, sizeof(ngx_js_fetch_t)); + if (fetch == NULL) { goto failed; } + http = &fetch->http; + http->pool = pool; http->log = log; - http->vm = vm; http->timeout = 10000; http->http_parse.content_length_n = -1; - ret = njs_vm_promise_create(vm, njs_value_arg(&http->promise), - njs_value_arg(&http->promise_callbacks)); + http->append_headers = ngx_js_fetch_append_headers; + http->ready_handler = ngx_js_fetch_process_done; + http->error_handler = ngx_js_fetch_error; + + ret = njs_vm_promise_create(vm, njs_value_arg(&fetch->promise), + njs_value_arg(&fetch->promise_callbacks)); if (ret != NJS_OK) { goto failed; } @@ -1290,17 +1161,18 @@ ngx_js_http_alloc(njs_vm_t *vm, ngx_pool_t *pool, ngx_log_t *log) event->ctx = vm; njs_value_function_set(njs_value_arg(&event->function), callback); - event->destructor = ngx_js_http_destructor; + event->destructor = ngx_js_fetch_destructor; event->fd = ctx->event_id++; - event->data = http; + event->data = fetch; ngx_js_add_event(ctx, event); - http->event = event; + fetch->vm = vm; + fetch->event = event; - ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "js fetch alloc:%p", http); + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "js fetch alloc:%p", fetch); - return http; + return fetch; failed: @@ -1311,194 +1183,31 @@ ngx_js_http_alloc(njs_vm_t *vm, ngx_pool_t *pool, ngx_log_t *log) static void -ngx_js_http_error(ngx_js_http_t *http, const char *fmt, ...) -{ - u_char *p, *end; - va_list args; - u_char err[NGX_MAX_ERROR_STR]; - - end = err + NGX_MAX_ERROR_STR - 1; - - va_start(args, fmt); - p = njs_vsprintf(err, end, fmt, args); - *p = '\0'; - va_end(args); - - njs_vm_error(http->vm, (const char *) err); - njs_vm_exception_get(http->vm, njs_value_arg(&http->response_value)); - ngx_js_http_fetch_done(http, &http->response_value, NJS_ERROR); -} - - -static ngx_resolver_ctx_t * -ngx_js_http_resolve(ngx_js_http_t *http, ngx_resolver_t *r, ngx_str_t *host, - in_port_t port, ngx_msec_t timeout) -{ - ngx_int_t ret; - ngx_resolver_ctx_t *ctx; - - ctx = ngx_resolve_start(r, NULL); - if (ctx == NULL) { - return NULL; - } - - if (ctx == NGX_NO_RESOLVER) { - return ctx; - } - - http->ctx = ctx; - http->port = port; - - ctx->name = *host; - ctx->handler = ngx_js_http_resolve_handler; - ctx->data = http; - ctx->timeout = timeout; - - ret = ngx_resolve_name(ctx); - if (ret != NGX_OK) { - http->ctx = NULL; - return NULL; - } - - return ctx; -} - - -static void -ngx_js_http_resolve_handler(ngx_resolver_ctx_t *ctx) -{ - u_char *p; - size_t len; - socklen_t socklen; - ngx_uint_t i; - ngx_js_http_t *http; - struct sockaddr *sockaddr; - - http = ctx->data; - - if (ctx->state) { - ngx_js_http_error(http, "\"%V\" could not be resolved (%i: %s)", - &ctx->name, 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_js_http_resolve_done(http); - - ngx_js_http_connect(http); - - return; - -failed: - - ngx_js_http_error(http, "memory error"); -} - - -static void -ngx_js_http_close_connection(ngx_connection_t *c) +ngx_js_fetch_error(ngx_js_http_t *http, const char *err) { - ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, - "js 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_js_http_close_connection; - return; - } - } -#endif - - c->destroyed = 1; - - ngx_close_connection(c); -} + ngx_js_fetch_t *fetch; + fetch = (ngx_js_fetch_t *) http; -static void -ngx_js_http_resolve_done(ngx_js_http_t *http) -{ - if (http->ctx != NULL) { - ngx_resolve_name_done(http->ctx); - http->ctx = NULL; - } -} + njs_vm_error(fetch->vm, err); + njs_vm_exception_get(fetch->vm, njs_value_arg(&fetch->response_value)); -static void -ngx_js_http_close_peer(ngx_js_http_t *http) -{ - if (http->peer.connection != NULL) { - ngx_js_http_close_connection(http->peer.connection); - http->peer.connection = NULL; - } + ngx_js_fetch_done(fetch, &fetch->response_value, NJS_ERROR); } static void -ngx_js_http_destructor(ngx_js_event_t *event) +ngx_js_fetch_destructor(ngx_js_event_t *event) { - ngx_js_http_t *http; + ngx_js_http_t *http; + ngx_js_fetch_t *fetch; - http = event->data; + fetch = event->data; + http = &fetch->http; ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, "js fetch destructor:%p", - http); + fetch); ngx_js_http_resolve_done(http); ngx_js_http_close_peer(http); @@ -1551,26 +1260,29 @@ ngx_js_fetch_promissified_result(njs_vm_t *vm, njs_value_t *result, static void -ngx_js_http_fetch_done(ngx_js_http_t *http, njs_opaque_value_t *retval, +ngx_js_fetch_done(ngx_js_fetch_t *fetch, njs_opaque_value_t *retval, njs_int_t rc) { njs_vm_t *vm; ngx_js_ctx_t *ctx; + ngx_js_http_t *http; ngx_js_event_t *event; njs_opaque_value_t arguments[2], *action; + http = &fetch->http; + ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0, - "js fetch done http:%p rc:%i", http, (ngx_int_t) rc); + "js fetch done fetch:%p rc:%i", fetch, (ngx_int_t) rc); ngx_js_http_close_peer(http); - if (http->event != NULL) { - action = &http->promise_callbacks[(rc != NJS_OK)]; + if (fetch->event != NULL) { + action = &fetch->promise_callbacks[(rc != NJS_OK)]; njs_value_assign(&arguments[0], action); njs_value_assign(&arguments[1], retval); - vm = http->vm; - event = http->event; + vm = fetch->vm; + event = fetch->event; rc = ngx_js_call(vm, njs_value_function(njs_value_arg(&event->function)), &arguments[0], 2); @@ -1599,1627 +1311,354 @@ ngx_js_http_promise_trampoline(njs_vm_t *vm, njs_value_t *args, } -static void -ngx_js_http_connect(ngx_js_http_t *http) +static njs_int_t +ngx_js_request_constructor(njs_vm_t *vm, ngx_js_request_t *request, + ngx_url_t *u, njs_external_ptr_t external, njs_value_t *args, + njs_uint_t nargs) { - ngx_int_t rc; - ngx_addr_t *addr; - - addr = &http->addrs[http->naddr]; - - ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0, - "js 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); + njs_int_t ret; + ngx_uint_t rc; + ngx_pool_t *pool; + njs_value_t *input, *init, *value, *headers; + ngx_js_request_t *orig; + njs_opaque_value_t lvalue; - if (rc == NGX_ERROR) { - ngx_js_http_error(http, "connect failed"); - return; - } + static const njs_str_t body_key = njs_str("body"); + static const njs_str_t cache_key = njs_str("cache"); + static const njs_str_t cred_key = njs_str("credentials"); + static const njs_str_t headers_key = njs_str("headers"); + static const njs_str_t mode_key = njs_str("mode"); + static const njs_str_t method_key = njs_str("method"); - if (rc == NGX_BUSY || rc == NGX_DECLINED) { - ngx_js_http_next(http); - return; + input = njs_arg(args, nargs, 1); + if (njs_value_is_undefined(input)) { + njs_vm_error(vm, "1st argument is required"); + return NJS_ERROR; } - http->peer.connection->data = http; - http->peer.connection->pool = http->pool; + /* + * set by ngx_memzero(): + * + * request->url.length = 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; + */ - http->peer.connection->write->handler = ngx_js_http_write_handler; - http->peer.connection->read->handler = ngx_js_http_read_handler; + ngx_memzero(request, sizeof(ngx_js_request_t)); - http->process = ngx_js_http_process_status_line; + request->method.data = (u_char *) "GET"; + request->method.len = 3; + request->body.data = (u_char *) ""; + request->body.len = 0; + request->headers.guard = GUARD_REQUEST; - ngx_add_timer(http->peer.connection->read, http->timeout); - ngx_add_timer(http->peer.connection->write, http->timeout); + pool = ngx_external_pool(vm, external); -#if (NGX_SSL) - if (http->ssl != NULL && http->peer.connection->ssl == NULL) { - ngx_js_http_ssl_init_connection(http); - return; + rc = ngx_list_init(&request->headers.header_list, pool, 4, + sizeof(ngx_js_tb_elt_t)); + if (rc != NGX_OK) { + njs_vm_memory_error(vm); + return NJS_ERROR; } -#endif - if (rc == NGX_OK) { - ngx_js_http_write_handler(http->peer.connection->write); - } -} + if (njs_value_is_string(input)) { + ret = ngx_njs_string(vm, input, &request->url); + if (ret != NJS_OK) { + njs_vm_error(vm, "failed to convert url arg"); + return NJS_ERROR; + } + } else { + orig = njs_vm_external(vm, ngx_http_js_fetch_request_proto_id, input); + if (orig == NULL) { + njs_vm_error(vm, "input is not string or a Request object"); + return NJS_ERROR; + } -#if (NGX_SSL) + 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; -static void -ngx_js_http_ssl_init_connection(ngx_js_http_t *http) -{ - ngx_int_t rc; - ngx_connection_t *c; + ret = ngx_js_headers_inherit(vm, &request->headers, &orig->headers); + if (ret != NJS_OK) { + return NJS_ERROR; + } + } - c = http->peer.connection; + ngx_js_http_trim(&request->url.data, &request->url.len, 1); - ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0, - "js fetch secure connect %ui/%ui", http->naddr, - http->naddrs); + 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 (ngx_ssl_create_connection(http->ssl, c, NGX_SSL_BUFFER|NGX_SSL_CLIENT) - != NGX_OK) + if (u->url.len > 7 + && njs_strncasecmp(u->url.data, (u_char *) "http://", 7) == 0) { - ngx_js_http_error(http, "failed to create ssl connection"); - return; - } + u->url.len -= 7; + u->url.data += 7; - c->sendfile = 0; +#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 - if (ngx_js_http_ssl_name(http) != NGX_OK) { - ngx_js_http_error(http, "failed to create ssl connection"); - return; + } else { + njs_vm_error(vm, "unsupported URL schema (only http or https are" + " supported)"); + return NJS_ERROR; } - c->log->action = "SSL handshaking to fetch target"; - - rc = ngx_ssl_handshake(c); - - if (rc == NGX_AGAIN) { - c->data = http; - c->ssl->handler = ngx_js_http_ssl_handshake_handler; - return; + if (ngx_parse_url(pool, u) != NGX_OK) { + njs_vm_error(vm, "invalid url"); + return NJS_ERROR; } - ngx_js_http_ssl_handshake(http); -} + init = njs_arg(args, nargs, 2); + if (njs_value_is_object(init)) { + value = njs_vm_object_prop(vm, init, &method_key, &lvalue); + if (value != NULL && ngx_njs_string(vm, value, &request->method) + != NGX_OK) + { + njs_vm_error(vm, "invalid Request method"); + return NJS_ERROR; + } -static void -ngx_js_http_ssl_handshake_handler(ngx_connection_t *c) -{ - ngx_js_http_t *http; + ret = ngx_js_method_process(vm, request); + if (ret != NJS_OK) { + return NJS_ERROR; + } - http = c->data; + value = njs_vm_object_prop(vm, init, &cache_key, &lvalue); + if (value != NULL) { + ret = ngx_fetch_flag_set(vm, ngx_js_fetch_cache_modes, value, + "cache"); + if (ret == NJS_ERROR) { + return NJS_ERROR; + } - http->peer.connection->write->handler = ngx_js_http_write_handler; - http->peer.connection->read->handler = ngx_js_http_read_handler; + request->cache_mode = ret; + } - ngx_js_http_ssl_handshake(http); -} - - -static void -ngx_js_http_ssl_handshake(ngx_js_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; + value = njs_vm_object_prop(vm, init, &cred_key, &lvalue); + if (value != NULL) { + ret = ngx_fetch_flag_set(vm, ngx_js_fetch_credentials, value, + "credentials"); + if (ret == NJS_ERROR) { + return NJS_ERROR; } - 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; - } + request->credentials = ret; } - c->write->handler = ngx_js_http_write_handler; - c->read->handler = ngx_js_http_read_handler; + value = njs_vm_object_prop(vm, init, &mode_key, &lvalue); + if (value != NULL) { + ret = ngx_fetch_flag_set(vm, ngx_js_fetch_modes, value, "mode"); + if (ret == NJS_ERROR) { + return NJS_ERROR; + } - if (c->read->ready) { - ngx_post_event(c->read, &ngx_posted_events); + request->mode = ret; } - http->process = ngx_js_http_process_status_line; - ngx_js_http_write_handler(c->write); - - return; - } - -failed: - - ngx_js_http_next(http); - } - - -static njs_int_t -ngx_js_http_ssl_name(ngx_js_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; - } + headers = njs_vm_object_prop(vm, init, &headers_key, &lvalue); + if (headers != NULL) { + if (!njs_value_is_object(headers)) { + njs_vm_error(vm, "Headers is not an object"); + return NJS_ERROR; + } - /* - * SSL_set_tlsext_host_name() needs a null-terminated string, - * hence we explicitly null-terminate name here - */ + /* + * There are no API to reset or destroy ngx_list, + * just allocating a new one. + */ - p = ngx_pnalloc(http->pool, name->len + 1); - if (p == NULL) { - return NGX_ERROR; - } + ngx_memset(&request->headers, 0, sizeof(ngx_js_headers_t)); + request->headers.guard = GUARD_REQUEST; - (void) ngx_cpystrn(p, name->data, name->len + 1); + rc = ngx_list_init(&request->headers.header_list, pool, 4, + sizeof(ngx_js_tb_elt_t)); + if (rc != NGX_OK) { + njs_vm_memory_error(vm); + return NJS_ERROR; + } - name->data = p; + ret = ngx_js_headers_fill(vm, &request->headers, headers); + if (ret != NJS_OK) { + return NJS_ERROR; + } + } - ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, - "js fetch SSL server name: \"%s\"", name->data); + value = njs_vm_object_prop(vm, init, &body_key, &lvalue); + if (value != NULL) { + if (ngx_njs_string(vm, value, &request->body) != NGX_OK) { + njs_vm_error(vm, "invalid Request body"); + return NJS_ERROR; + } - 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; + if (request->headers.content_type == NULL + && njs_value_is_string(value)) + { + ret = ngx_js_headers_append(vm, &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) { + return NJS_ERROR; + } + } + } } -#endif -done: - return NJS_OK; } -#endif - -static void -ngx_js_http_next(ngx_js_http_t *http) +static ngx_int_t +ngx_js_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_log_debug0(NGX_LOG_DEBUG_EVENT, http->log, 0, "js fetch next addr"); - - if (++http->naddr >= http->naddrs) { - ngx_js_http_error(http, "connect failed"); - return; - } - - if (http->peer.connection != NULL) { - ngx_js_http_close_connection(http->peer.connection); - http->peer.connection = NULL; - } + ngx_js_fetch_t *fetch; - http->buffer = NULL; + fetch = (ngx_js_fetch_t *) http; - ngx_js_http_connect(http); + return ngx_js_headers_append(fetch->vm, headers, name, len, value, vlen); } static void -ngx_js_http_write_handler(ngx_event_t *wev) +ngx_js_fetch_process_done(ngx_js_http_t *http) { - ssize_t n, size; - ngx_buf_t *b; - ngx_js_http_t *http; - ngx_connection_t *c; - - c = wev->data; - http = c->data; - - ngx_log_debug0(NGX_LOG_DEBUG_EVENT, wev->log, 0, "js fetch write handler"); - - if (wev->timedout) { - ngx_js_http_error(http, "write timed out"); - return; - } - -#if (NGX_SSL) - if (http->ssl != NULL && http->peer.connection->ssl == NULL) { - ngx_js_http_ssl_init_connection(http); - return; - } -#endif - - b = http->buffer; - - if (b == NULL) { - size = njs_chb_size(&http->chain); - if (size < 0) { - ngx_js_http_error(http, "memory error"); - return; - } - - b = ngx_create_temp_buf(http->pool, size); - if (b == NULL) { - ngx_js_http_error(http, "memory error"); - return; - } + njs_int_t ret; + ngx_js_fetch_t *fetch; - njs_chb_join_to(&http->chain, b->last); - b->last += size; + fetch = (ngx_js_fetch_t *) http; - http->buffer = b; - } - - size = b->last - b->pos; - - n = c->send(c, b->pos, size); - - if (n == NGX_ERROR) { - ngx_js_http_next(http); + ret = njs_vm_external_create(fetch->vm, + njs_value_arg(&fetch->response_value), + ngx_http_js_fetch_response_proto_id, + &fetch->http.response, 0); + if (ret != NJS_OK) { + ngx_js_fetch_error(http, "fetch response creation failed"); return; } - if (n > 0) { - b->pos += n; + ngx_js_fetch_done(fetch, &fetch->response_value, NJS_OK); +} - if (n == size) { - wev->handler = ngx_js_http_dummy_handler; - http->buffer = NULL; +static njs_int_t +ngx_js_headers_append(njs_vm_t *vm, ngx_js_headers_t *headers, + u_char *name, size_t len, u_char *value, size_t vlen) +{ + u_char *p, *end; + ngx_uint_t i; + ngx_js_tb_elt_t *h, **ph; + ngx_list_part_t *part; - if (wev->timer_set) { - ngx_del_timer(wev); - } + ngx_js_http_trim(&value, &vlen, 0); - if (ngx_handle_write_event(wev, 0) != NGX_OK) { - ngx_js_http_error(http, "write failed"); - } + p = name; + end = p + len; - return; + while (p < end) { + if (!njs_is_token(*p)) { + njs_vm_error(vm, "invalid header name"); + return NJS_ERROR; } - } - if (!wev->timer_set) { - ngx_add_timer(wev, http->timeout); + p++; } -} - - -static void -ngx_js_http_read_handler(ngx_event_t *rev) -{ - ssize_t n, size; - ngx_int_t rc; - ngx_buf_t *b; - ngx_js_http_t *http; - ngx_connection_t *c; - - c = rev->data; - http = c->data; - ngx_log_debug0(NGX_LOG_DEBUG_EVENT, rev->log, 0, "js fetch read handler"); - - if (rev->timedout) { - ngx_js_http_error(http, "read timed out"); - return; - } + p = value; + end = p + vlen; - if (http->buffer == NULL) { - b = ngx_create_temp_buf(http->pool, http->buffer_size); - if (b == NULL) { - ngx_js_http_error(http, "memory error"); - return; + while (p < end) { + if (*p == '\0') { + njs_vm_error(vm, "invalid header value"); + return NJS_ERROR; } - http->buffer = b; + p++; } - for ( ;; ) { - b = http->buffer; - size = b->end - b->last; - - n = c->recv(c, b->last, size); + if (headers->guard == GUARD_IMMUTABLE) { + njs_vm_error(vm, "cannot append to immutable object"); + return NJS_ERROR; + } - if (n > 0) { - b->last += n; + ph = NULL; + part = &headers->header_list.part; + h = part->elts; - rc = http->process(http); + for (i = 0; /* void */; i++) { - if (rc == NGX_ERROR) { - return; + if (i >= part->nelts) { + if (part->next == NULL) { + break; } - continue; + part = part->next; + h = part->elts; + i = 0; } - if (n == NGX_AGAIN) { - if (ngx_handle_read_event(rev, 0) != NGX_OK) { - ngx_js_http_error(http, "read failed"); - } - - return; + if (h[i].hash == 0) { + continue; } - if (n == NGX_ERROR) { - ngx_js_http_next(http); - return; + 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; } - - break; - } - - http->done = 1; - - rc = http->process(http); - - if (rc == NGX_DONE) { - /* handler was called */ - return; } - if (rc == NGX_AGAIN) { - ngx_js_http_error(http, "prematurely closed connection"); - } -} - - -static njs_int_t -ngx_js_request_constructor(njs_vm_t *vm, ngx_js_request_t *request, - ngx_url_t *u, njs_external_ptr_t external, njs_value_t *args, - njs_uint_t nargs) -{ - njs_int_t ret; - ngx_uint_t rc; - ngx_pool_t *pool; - njs_value_t *input, *init, *value, *headers; - ngx_js_request_t *orig; - njs_opaque_value_t lvalue; - - static const njs_str_t body_key = njs_str("body"); - static const njs_str_t cache_key = njs_str("cache"); - static const njs_str_t cred_key = njs_str("credentials"); - static const njs_str_t headers_key = njs_str("headers"); - static const njs_str_t mode_key = njs_str("mode"); - static const njs_str_t method_key = njs_str("method"); - - input = njs_arg(args, nargs, 1); - if (njs_value_is_undefined(input)) { - njs_vm_error(vm, "1st argument is required"); + h = ngx_list_push(&headers->header_list); + if (h == NULL) { + njs_vm_memory_error(vm); return NJS_ERROR; } - /* - * set by ngx_memzero(): - * - * request->url.length = 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 = njs_str_value("GET"); - request->body = njs_str_value(""); - request->headers.guard = GUARD_REQUEST; + if (ph != NULL) { + *ph = h; + } - pool = ngx_external_pool(vm, external); + h->hash = 1; + h->key.data = name; + h->key.len = len; + h->value.data = value; + h->value.len = vlen; + h->next = NULL; - rc = ngx_list_init(&request->headers.header_list, pool, 4, - sizeof(ngx_js_tb_elt_t)); - if (rc != NGX_OK) { - njs_vm_memory_error(vm); - return NJS_ERROR; - } - - if (njs_value_is_string(input)) { - ret = ngx_js_string(vm, input, &request->url); - if (ret != NJS_OK) { - njs_vm_error(vm, "failed to convert url arg"); - return NJS_ERROR; - } - - } else { - orig = njs_vm_external(vm, ngx_http_js_fetch_request_proto_id, input); - if (orig == NULL) { - njs_vm_error(vm, "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_js_headers_inherit(vm, &request->headers, &orig->headers); - if (ret != NJS_OK) { - return NJS_ERROR; - } - } - - ngx_js_http_trim(&request->url.start, &request->url.length, 1); - - ngx_memzero(u, sizeof(ngx_url_t)); - - u->url.len = request->url.length; - u->url.data = request->url.start; - 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 { - njs_vm_error(vm, "unsupported URL schema (only http or https are" - " supported)"); - return NJS_ERROR; - } - - if (ngx_parse_url(pool, u) != NGX_OK) { - njs_vm_error(vm, "invalid url"); - return NJS_ERROR; - } - - init = njs_arg(args, nargs, 2); - - if (njs_value_is_object(init)) { - value = njs_vm_object_prop(vm, init, &method_key, &lvalue); - if (value != NULL && ngx_js_string(vm, value, &request->method) - != NGX_OK) - { - njs_vm_error(vm, "invalid Request method"); - return NJS_ERROR; - } - - ret = ngx_js_method_process(vm, request); - if (ret != NJS_OK) { - return NJS_ERROR; - } - - value = njs_vm_object_prop(vm, init, &cache_key, &lvalue); - if (value != NULL) { - ret = ngx_fetch_flag_set(vm, ngx_js_fetch_cache_modes, value, - "cache"); - if (ret == NJS_ERROR) { - return NJS_ERROR; - } - - request->cache_mode = ret; - } - - value = njs_vm_object_prop(vm, init, &cred_key, &lvalue); - if (value != NULL) { - ret = ngx_fetch_flag_set(vm, ngx_js_fetch_credentials, value, - "credentials"); - if (ret == NJS_ERROR) { - return NJS_ERROR; - } - - request->credentials = ret; - } - - value = njs_vm_object_prop(vm, init, &mode_key, &lvalue); - if (value != NULL) { - ret = ngx_fetch_flag_set(vm, ngx_js_fetch_modes, value, "mode"); - if (ret == NJS_ERROR) { - return NJS_ERROR; - } - - request->mode = ret; - } - - headers = njs_vm_object_prop(vm, init, &headers_key, &lvalue); - if (headers != NULL) { - if (!njs_value_is_object(headers)) { - njs_vm_error(vm, "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_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) { - njs_vm_memory_error(vm); - return NJS_ERROR; - } - - ret = ngx_js_headers_fill(vm, &request->headers, headers); - if (ret != NJS_OK) { - return NJS_ERROR; - } - } - - value = njs_vm_object_prop(vm, init, &body_key, &lvalue); - if (value != NULL) { - if (ngx_js_string(vm, value, &request->body) != NGX_OK) { - njs_vm_error(vm, "invalid Request body"); - return NJS_ERROR; - } - - if (request->headers.content_type == NULL - && njs_value_is_string(value)) - { - ret = ngx_js_headers_append(vm, &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) { - return NJS_ERROR; - } - } - } + if (len == njs_strlen("Content-Type") + && ngx_strncasecmp(name, (u_char *) "Content-Type", len) == 0) + { + headers->content_type = h; } return NJS_OK; } -njs_inline njs_int_t -ngx_js_http_whitespace(u_char c) -{ - switch (c) { - case 0x09: /* */ - case 0x0A: /* */ - case 0x0D: /* */ - case 0x20: /* */ - return 1; - - default: - return 0; - } -} - - -static void -ngx_js_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_js_http_whitespace(*start) - || (trim_c0_control_or_space && *start <= ' ')) - { - start++; - continue; - } - - break; - } - - for ( ;; ) { - if (start == end) { - break; - } - - end--; - - if (ngx_js_http_whitespace(*end) - || (trim_c0_control_or_space && *end <= ' ')) - { - continue; - } - - end++; - break; - } - - *value = start; - *len = end - start; -} - - -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); -} - - -static njs_int_t -ngx_js_headers_append(njs_vm_t *vm, ngx_js_headers_t *headers, - u_char *name, size_t len, u_char *value, size_t vlen) -{ - u_char *p, *end; - ngx_uint_t i; - ngx_js_tb_elt_t *h, **ph; - ngx_list_part_t *part; - - ngx_js_http_trim(&value, &vlen, 0); - - p = name; - end = p + len; - - while (p < end) { - if (!njs_is_token(*p)) { - njs_vm_error(vm, "invalid header name"); - return NJS_ERROR; - } - - p++; - } - - p = value; - end = p + vlen; - - while (p < end) { - if (*p == '\0') { - njs_vm_error(vm, "invalid header value"); - return NJS_ERROR; - } - - p++; - } - - if (headers->guard == GUARD_IMMUTABLE) { - njs_vm_error(vm, "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) { - njs_vm_memory_error(vm); - 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 ngx_int_t -ngx_js_http_process_status_line(ngx_js_http_t *http) -{ - ngx_int_t rc; - ngx_js_http_parse_t *hp; - - hp = &http->http_parse; - - rc = ngx_js_http_parse_status_line(hp, http->buffer); - - if (rc == NGX_OK) { - ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, "js fetch status %ui", - hp->code); - - http->response.code = hp->code; - http->response.status_text.start = hp->status_text; - http->response.status_text.length = hp->status_text_end - - hp->status_text; - http->process = ngx_js_http_process_headers; - - return http->process(http); - } - - if (rc == NGX_AGAIN) { - return NGX_AGAIN; - } - - /* rc == NGX_ERROR */ - - ngx_js_http_error(http, "invalid fetch status line"); - - return NGX_ERROR; -} - - -static ngx_int_t -ngx_js_http_process_headers(ngx_js_http_t *http) -{ - size_t len, vlen; - ngx_int_t rc; - njs_int_t ret; - ngx_js_http_parse_t *hp; - - ngx_log_debug0(NGX_LOG_DEBUG_EVENT, http->log, 0, - "js 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_js_tb_elt_t)); - if (rc != NGX_OK) { - ngx_js_http_error(http, "alloc failed"); - return NGX_ERROR; - } - } - - for ( ;; ) { - rc = ngx_js_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_js_headers_append(http->vm, &http->response.headers, - hp->header_name_start, len, - hp->header_start, vlen); - - if (ret == NJS_ERROR) { - ngx_js_http_error(http, "cannot add respose header"); - return NGX_ERROR; - } - - ngx_log_debug4(NGX_LOG_DEBUG_EVENT, http->log, 0, - "js 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_js_http_error(http, "invalid fetch content length"); - return NGX_ERROR; - } - - if (!http->header_only - && hp->content_length_n - > (off_t) http->max_response_body_size) - { - ngx_js_http_error(http, - "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_js_http_error(http, "invalid fetch header"); - - return NGX_ERROR; - } - - njs_chb_destroy(&http->chain); - - NJS_CHB_MP_INIT(&http->response.chain, http->vm); - - http->process = ngx_js_http_process_body; - - return http->process(http); -} - - -static ngx_int_t -ngx_js_http_process_body(ngx_js_http_t *http) -{ - ssize_t size, chsize, need; - ngx_int_t rc; - njs_int_t ret; - ngx_buf_t *b; - - ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, - "js fetch process body done:%ui", (ngx_uint_t) http->done); - - if (http->done) { - size = njs_chb_size(&http->response.chain); - if (size < 0) { - ngx_js_http_error(http, "memory error"); - return NGX_ERROR; - } - - if (!http->header_only - && http->http_parse.chunked - && http->http_parse.content_length_n == -1) - { - ngx_js_http_error(http, "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) - { - ret = njs_vm_external_create(http->vm, - njs_value_arg(&http->response_value), - ngx_http_js_fetch_response_proto_id, - &http->response, 0); - if (ret != NJS_OK) { - ngx_js_http_error(http, "fetch response creation failed"); - return NGX_ERROR; - } - - ngx_js_http_fetch_done(http, &http->response_value, NJS_OK); - return NGX_DONE; - } - - if (size < http->http_parse.content_length_n) { - return NGX_AGAIN; - } - - ngx_js_http_error(http, "fetch trailing data"); - return NGX_ERROR; - } - - b = http->buffer; - - if (http->http_parse.chunked) { - rc = ngx_js_http_parse_chunked(&http->http_chunk_parse, b, - &http->response.chain); - if (rc == NGX_ERROR) { - ngx_js_http_error(http, "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_js_http_error(http, "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_js_http_error(http, "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_js_http_error(http, "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_js_http_parse_status_line(ngx_js_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_js_http_parse_header_line(ngx_js_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_JS_HTTP_CHUNK_MIDDLE 0 -#define NGX_JS_HTTP_CHUNK_ON_BORDER 1 -#define NGX_JS_HTTP_CHUNK_END 2 - - -static ngx_int_t -ngx_js_http_chunk_buffer(ngx_js_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_JS_HTTP_CHUNK_END; - } - - njs_chb_append(chain, hcp->pos, size); - hcp->pos += size; - - hcp->chunk_size -= size; - - if (hcp->chunk_size == 0) { - return NGX_JS_HTTP_CHUNK_ON_BORDER; - } - - return NGX_JS_HTTP_CHUNK_MIDDLE; -} - - -static ngx_int_t -ngx_js_http_parse_chunked(ngx_js_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_js_http_chunk_buffer(hcp, b, chain); - if (rc == NGX_ERROR) { - return rc; - } - - if (rc == NGX_JS_HTTP_CHUNK_MIDDLE) { - break; - } - - state = sw_chunk_end_newline; - - if (rc == NGX_JS_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 void -ngx_js_http_dummy_handler(ngx_event_t *ev) -{ - ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, 0, "js fetch dummy handler"); -} - - static njs_int_t ngx_headers_js_get(njs_vm_t *vm, njs_value_t *value, njs_str_t *name, njs_value_t *retval, njs_bool_t as_array) @@ -3729,8 +2168,8 @@ ngx_request_js_ext_body(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, switch (type) { case NGX_JS_BODY_ARRAY_BUFFER: ret = njs_vm_value_array_buffer_set(vm, njs_value_arg(&result), - request->body.start, - request->body.length); + request->body.data, + request->body.len); if (ret != NJS_OK) { njs_vm_memory_error(vm); return NJS_ERROR; @@ -3742,8 +2181,8 @@ ngx_request_js_ext_body(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, case NGX_JS_BODY_TEXT: default: ret = njs_vm_value_string_create(vm, njs_value_arg(&result), - request->body.start, - request->body.length); + request->body.data, + request->body.len); if (ret != NJS_OK) { njs_vm_memory_error(vm); return NJS_ERROR; @@ -3825,8 +2264,8 @@ ngx_request_js_ext_headers(njs_vm_t *vm, njs_object_prop_t *prop, return NJS_DECLINED; } - if (njs_value_is_null(njs_value_arg(&request->header_value))) { - ret = njs_vm_external_create(vm, njs_value_arg(&request->header_value), + if (njs_value_is_null(njs_value_arg(&request->u.njs_headers))) { + ret = njs_vm_external_create(vm, njs_value_arg(&request->u.njs_headers), ngx_http_js_fetch_headers_proto_id, &request->headers, 0); if (ret != NJS_OK) { @@ -3835,7 +2274,7 @@ ngx_request_js_ext_headers(njs_vm_t *vm, njs_object_prop_t *prop, } } - njs_value_assign(retval, &request->header_value); + njs_value_assign(retval, &request->u.njs_headers); return NJS_OK; } @@ -3950,8 +2389,8 @@ ngx_response_js_ext_headers(njs_vm_t *vm, njs_object_prop_t *prop, return NJS_DECLINED; } - if (njs_value_is_null(njs_value_arg(&response->header_value))) { - ret = njs_vm_external_create(vm, njs_value_arg(&response->header_value), + if (njs_value_is_null(njs_value_arg(&response->u.njs_headers))) { + ret = njs_vm_external_create(vm, njs_value_arg(&response->u.njs_headers), ngx_http_js_fetch_headers_proto_id, &response->headers, 0); if (ret != NJS_OK) { @@ -3960,7 +2399,7 @@ ngx_response_js_ext_headers(njs_vm_t *vm, njs_object_prop_t *prop, } } - njs_value_assign(retval, &response->header_value); + njs_value_assign(retval, &response->u.njs_headers); return NJS_OK; } @@ -4017,8 +2456,8 @@ ngx_response_js_ext_status_text(njs_vm_t *vm, njs_object_prop_t *prop, return NJS_DECLINED; } - njs_vm_value_string_create(vm, retval, response->status_text.start, - response->status_text.length); + njs_vm_value_string_create(vm, retval, response->status_text.data, + response->status_text.len); return NJS_OK; } diff --git a/nginx/ngx_js_http.c b/nginx/ngx_js_http.c new file mode 100644 index 000000000..42b673cdc --- /dev/null +++ b/nginx/ngx_js_http.c @@ -0,0 +1,1486 @@ + +/* + * Copyright (C) Dmitry Volyntsev + * Copyright (C) hongzhidao + * Copyright (C) Antoine Bonavita + * Copyright (C) NGINX, Inc. + */ + + +#include +#include +#include +#include +#include "ngx_js.h" +#include "ngx_js_http.h" + + +static void ngx_js_http_resolve_handler(ngx_resolver_ctx_t *ctx); +static void ngx_js_http_next(ngx_js_http_t *http); +static void ngx_js_http_write_handler(ngx_event_t *wev); +static void ngx_js_http_read_handler(ngx_event_t *rev); +static void ngx_js_http_dummy_handler(ngx_event_t *ev); + +static ngx_int_t ngx_js_http_process_status_line(ngx_js_http_t *http); +static ngx_int_t ngx_js_http_process_headers(ngx_js_http_t *http); +static ngx_int_t ngx_js_http_process_body(ngx_js_http_t *http); +static ngx_int_t ngx_js_http_parse_status_line(ngx_js_http_parse_t *hp, + ngx_buf_t *b); +static ngx_int_t ngx_js_http_parse_header_line(ngx_js_http_parse_t *hp, + ngx_buf_t *b); +static ngx_int_t ngx_js_http_parse_chunked(ngx_js_http_chunk_parse_t *hcp, + ngx_buf_t *b, njs_chb_t *chain); + +#if (NGX_SSL) +static void ngx_js_http_ssl_init_connection(ngx_js_http_t *http); +static void ngx_js_http_ssl_handshake_handler(ngx_connection_t *c); +static void ngx_js_http_ssl_handshake(ngx_js_http_t *http); +static njs_int_t ngx_js_http_ssl_name(ngx_js_http_t *http); +#endif + + +static void +ngx_js_http_error(ngx_js_http_t *http, const char *fmt, ...) +{ + u_char *p, *end; + va_list args; + u_char err[NGX_MAX_ERROR_STR]; + + end = err + NGX_MAX_ERROR_STR - 1; + + va_start(args, fmt); + p = njs_vsprintf(err, end, fmt, args); + *p = '\0'; + va_end(args); + + http->error_handler(http, (const char *) err); +} + + +ngx_resolver_ctx_t * +ngx_js_http_resolve(ngx_js_http_t *http, ngx_resolver_t *r, ngx_str_t *host, + in_port_t port, ngx_msec_t timeout) +{ + ngx_int_t ret; + ngx_resolver_ctx_t *ctx; + + ctx = ngx_resolve_start(r, NULL); + if (ctx == NULL) { + return NULL; + } + + if (ctx == NGX_NO_RESOLVER) { + return ctx; + } + + http->ctx = ctx; + http->port = port; + + ctx->name = *host; + ctx->handler = ngx_js_http_resolve_handler; + ctx->data = http; + ctx->timeout = timeout; + + ret = ngx_resolve_name(ctx); + if (ret != NGX_OK) { + http->ctx = NULL; + return NULL; + } + + return ctx; +} + + +static void +ngx_js_http_resolve_handler(ngx_resolver_ctx_t *ctx) +{ + u_char *p; + size_t len; + socklen_t socklen; + ngx_uint_t i; + ngx_js_http_t *http; + struct sockaddr *sockaddr; + + http = ctx->data; + + if (ctx->state) { + ngx_js_http_error(http, "\"%V\" could not be resolved (%i: %s)", + &ctx->name, ctx->state, + ngx_resolver_strerror(ctx->state)); + return; + } + + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, + "http 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_js_http_resolve_done(http); + + ngx_js_http_connect(http); + + return; + +failed: + + ngx_js_http_error(http, "memory error"); +} + + +static void +ngx_js_http_close_connection(ngx_connection_t *c) +{ + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, + "js http 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_js_http_close_connection; + return; + } + } +#endif + + c->destroyed = 1; + + ngx_close_connection(c); +} + + +void +ngx_js_http_resolve_done(ngx_js_http_t *http) +{ + if (http->ctx != NULL) { + ngx_resolve_name_done(http->ctx); + http->ctx = NULL; + } +} + + +void +ngx_js_http_close_peer(ngx_js_http_t *http) +{ + if (http->peer.connection != NULL) { + ngx_js_http_close_connection(http->peer.connection); + http->peer.connection = NULL; + } +} + + +void +ngx_js_http_connect(ngx_js_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, + "js http 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_js_http_error(http, "connect failed"); + return; + } + + if (rc == NGX_BUSY || rc == NGX_DECLINED) { + ngx_js_http_next(http); + return; + } + + http->peer.connection->data = http; + http->peer.connection->pool = http->pool; + + http->peer.connection->write->handler = ngx_js_http_write_handler; + http->peer.connection->read->handler = ngx_js_http_read_handler; + + http->process = ngx_js_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_js_http_ssl_init_connection(http); + return; + } +#endif + + if (rc == NGX_OK) { + ngx_js_http_write_handler(http->peer.connection->write); + } +} + + +#if (NGX_SSL) + +static void +ngx_js_http_ssl_init_connection(ngx_js_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, + "js http 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_js_http_error(http, "failed to create ssl connection"); + return; + } + + c->sendfile = 0; + + if (ngx_js_http_ssl_name(http) != NGX_OK) { + ngx_js_http_error(http, "failed to create ssl connection"); + return; + } + + c->log->action = "SSL handshaking to http target"; + + rc = ngx_ssl_handshake(c); + + if (rc == NGX_AGAIN) { + c->data = http; + c->ssl->handler = ngx_js_http_ssl_handshake_handler; + return; + } + + ngx_js_http_ssl_handshake(http); +} + + +static void +ngx_js_http_ssl_handshake_handler(ngx_connection_t *c) +{ + ngx_js_http_t *http; + + http = c->data; + + http->peer.connection->write->handler = ngx_js_http_write_handler; + http->peer.connection->read->handler = ngx_js_http_read_handler; + + ngx_js_http_ssl_handshake(http); +} + + +static void +ngx_js_http_ssl_handshake(ngx_js_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 http 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 http SSL certificate does not match \"%V\"", + &http->tls_name); + goto failed; + } + } + + c->write->handler = ngx_js_http_write_handler; + c->read->handler = ngx_js_http_read_handler; + + if (c->read->ready) { + ngx_post_event(c->read, &ngx_posted_events); + } + + http->process = ngx_js_http_process_status_line; + ngx_js_http_write_handler(c->write); + + return; + } + +failed: + + ngx_js_http_next(http); +} + + +static njs_int_t +ngx_js_http_ssl_name(ngx_js_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, + "js http 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; +} + +#endif + + +static void +ngx_js_http_next(ngx_js_http_t *http) +{ + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, http->log, 0, "js http next addr"); + + if (++http->naddr >= http->naddrs) { + ngx_js_http_error(http, "connect failed"); + return; + } + + if (http->peer.connection != NULL) { + ngx_js_http_close_connection(http->peer.connection); + http->peer.connection = NULL; + } + + http->buffer = NULL; + + ngx_js_http_connect(http); +} + + +static void +ngx_js_http_write_handler(ngx_event_t *wev) +{ + ssize_t n, size; + ngx_buf_t *b; + ngx_js_http_t *http; + ngx_connection_t *c; + + c = wev->data; + http = c->data; + + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, wev->log, 0, "js http write handler"); + + if (wev->timedout) { + ngx_js_http_error(http, "write timed out"); + return; + } + +#if (NGX_SSL) + if (http->ssl != NULL && http->peer.connection->ssl == NULL) { + ngx_js_http_ssl_init_connection(http); + return; + } +#endif + + b = http->buffer; + + if (b == NULL) { + size = njs_chb_size(&http->chain); + if (size < 0) { + ngx_js_http_error(http, "memory error"); + return; + } + + b = ngx_create_temp_buf(http->pool, size); + if (b == NULL) { + ngx_js_http_error(http, "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_js_http_next(http); + return; + } + + if (n > 0) { + b->pos += n; + + if (n == size) { + wev->handler = ngx_js_http_dummy_handler; + + http->buffer = NULL; + + if (wev->timer_set) { + ngx_del_timer(wev); + } + + if (ngx_handle_write_event(wev, 0) != NGX_OK) { + ngx_js_http_error(http, "write failed"); + } + + return; + } + } + + if (!wev->timer_set) { + ngx_add_timer(wev, http->timeout); + } +} + + +static void +ngx_js_http_read_handler(ngx_event_t *rev) +{ + ssize_t n, size; + ngx_int_t rc; + ngx_buf_t *b; + ngx_js_http_t *http; + ngx_connection_t *c; + + c = rev->data; + http = c->data; + + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, rev->log, 0, "js http read handler"); + + if (rev->timedout) { + ngx_js_http_error(http, "read timed out"); + return; + } + + if (http->buffer == NULL) { + b = ngx_create_temp_buf(http->pool, http->buffer_size); + if (b == NULL) { + ngx_js_http_error(http, "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_js_http_error(http, "read failed"); + } + + return; + } + + if (n == NGX_ERROR) { + ngx_js_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_js_http_error(http, "prematurely closed connection"); + } +} + + +static void +ngx_js_http_dummy_handler(ngx_event_t *ev) +{ + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, 0, "js http dummy handler"); +} + + +static ngx_int_t +ngx_js_http_process_status_line(ngx_js_http_t *http) +{ + ngx_int_t rc; + ngx_js_http_parse_t *hp; + + hp = &http->http_parse; + + rc = ngx_js_http_parse_status_line(hp, http->buffer); + + if (rc == NGX_OK) { + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, "js http 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_js_http_process_headers; + + return http->process(http); + } + + if (rc == NGX_AGAIN) { + return NGX_AGAIN; + } + + /* rc == NGX_ERROR */ + + ngx_js_http_error(http, "invalid http status line"); + + return NGX_ERROR; +} + + +static ngx_int_t +ngx_js_http_process_headers(ngx_js_http_t *http) +{ + size_t len, vlen; + ngx_int_t rc; + njs_int_t ret; + ngx_js_http_parse_t *hp; + + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, http->log, 0, + "js http 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_js_tb_elt_t)); + if (rc != NGX_OK) { + ngx_js_http_error(http, "alloc failed"); + return NGX_ERROR; + } + } + + for ( ;; ) { + rc = ngx_js_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 = http->append_headers(http, &http->response.headers, + hp->header_name_start, len, + hp->header_start, vlen); + + if (ret == NJS_ERROR) { + ngx_js_http_error(http, "cannot add respose header"); + return NGX_ERROR; + } + + ngx_log_debug4(NGX_LOG_DEBUG_EVENT, http->log, 0, + "js http 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_js_http_error(http, "invalid http content length"); + return NGX_ERROR; + } + + if (!http->header_only + && hp->content_length_n + > (off_t) http->max_response_body_size) + { + ngx_js_http_error(http, + "http 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_js_http_error(http, "invalid http header"); + + return NGX_ERROR; + } + + njs_chb_destroy(&http->chain); + + http->process = ngx_js_http_process_body; + + return http->process(http); +} + + +static ngx_int_t +ngx_js_http_process_body(ngx_js_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, + "js http process body done:%ui", (ngx_uint_t) http->done); + + if (http->done) { + size = njs_chb_size(&http->response.chain); + if (size < 0) { + ngx_js_http_error(http, "memory error"); + return NGX_ERROR; + } + + if (!http->header_only + && http->http_parse.chunked + && http->http_parse.content_length_n == -1) + { + ngx_js_http_error(http, "invalid http chunked response"); + return NGX_ERROR; + } + + if (http->header_only + || http->http_parse.content_length_n == -1 + || size == http->http_parse.content_length_n) + { + http->ready_handler(http); + return NGX_DONE; + } + + if (size < http->http_parse.content_length_n) { + return NGX_AGAIN; + } + + ngx_js_http_error(http, "http trailing data"); + return NGX_ERROR; + } + + b = http->buffer; + + if (http->http_parse.chunked) { + rc = ngx_js_http_parse_chunked(&http->http_chunk_parse, b, + &http->response.chain); + if (rc == NGX_ERROR) { + ngx_js_http_error(http, "invalid http 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_js_http_error(http, "very large http 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_js_http_error(http, "http 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_js_http_error(http, "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_js_http_parse_status_line(ngx_js_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_js_http_parse_header_line(ngx_js_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_JS_HTTP_CHUNK_MIDDLE 0 +#define NGX_JS_HTTP_CHUNK_ON_BORDER 1 +#define NGX_JS_HTTP_CHUNK_END 2 + + +static ngx_int_t +ngx_js_http_chunk_buffer(ngx_js_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_JS_HTTP_CHUNK_END; + } + + njs_chb_append(chain, hcp->pos, size); + hcp->pos += size; + + hcp->chunk_size -= size; + + if (hcp->chunk_size == 0) { + return NGX_JS_HTTP_CHUNK_ON_BORDER; + } + + return NGX_JS_HTTP_CHUNK_MIDDLE; +} + + +static ngx_int_t +ngx_js_http_parse_chunked(ngx_js_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_js_http_chunk_buffer(hcp, b, chain); + if (rc == NGX_ERROR) { + return rc; + } + + if (rc == NGX_JS_HTTP_CHUNK_MIDDLE) { + break; + } + + state = sw_chunk_end_newline; + + if (rc == NGX_JS_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; +} + + +njs_inline njs_int_t +ngx_js_http_whitespace(u_char c) +{ + switch (c) { + case 0x09: /* */ + case 0x0A: /* */ + case 0x0D: /* */ + case 0x20: /* */ + return 1; + + default: + return 0; + } +} + + +void +ngx_js_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_js_http_whitespace(*start) + || (trim_c0_control_or_space && *start <= ' ')) + { + start++; + continue; + } + + break; + } + + for ( ;; ) { + if (start == end) { + break; + } + + end--; + + if (ngx_js_http_whitespace(*end) + || (trim_c0_control_or_space && *end <= ' ')) + { + continue; + } + + end++; + break; + } + + *value = start; + *len = end - start; +} diff --git a/nginx/ngx_js_http.h b/nginx/ngx_js_http.h new file mode 100644 index 000000000..85b1fcaba --- /dev/null +++ b/nginx/ngx_js_http.h @@ -0,0 +1,195 @@ + +/* + * Copyright (C) Dmitry Volyntsev + * Copyright (C) hongzhidao + * Copyright (C) Antoine Bonavita + * Copyright (C) NGINX, Inc. + */ + + +#ifndef _NGX_JS_HTTP_H_INCLUDED_ +#define _NGX_JS_HTTP_H_INCLUDED_ + + +typedef struct ngx_js_http_s ngx_js_http_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_js_http_parse_t; + + +typedef struct { + u_char *pos; + uint64_t chunk_size; + uint8_t state; + uint8_t last; +} ngx_js_http_chunk_parse_t; + + +typedef struct ngx_js_tb_elt_s ngx_js_tb_elt_t; + +struct ngx_js_tb_elt_s { + ngx_uint_t hash; + ngx_str_t key; + ngx_str_t value; + ngx_js_tb_elt_t *next; +}; + + +typedef struct { + enum { + GUARD_NONE = 0, + GUARD_REQUEST, + GUARD_IMMUTABLE, + GUARD_RESPONSE, + } guard; + ngx_list_t header_list; + ngx_js_tb_elt_t *content_type; +} ngx_js_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_js_headers_t headers; + union { + njs_opaque_value_t njs_headers; +#if (NJS_HAVE_QUICKJS) + JSValue qjs_headers; +#endif + } u; +} ngx_js_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_js_headers_t headers; + union { + njs_opaque_value_t njs_headers; +#if (NJS_HAVE_QUICKJS) + JSValue qjs_headers; +#endif + } u; +} ngx_js_response_t; + + +struct ngx_js_http_s { + ngx_log_t *log; + ngx_pool_t *pool; + + 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_js_response_t response; + + uint8_t done; + ngx_js_http_parse_t http_parse; + ngx_js_http_chunk_parse_t http_chunk_parse; + ngx_int_t (*process)(ngx_js_http_t *http); + ngx_int_t (*append_headers)(ngx_js_http_t *http, + ngx_js_headers_t *headers, + u_char *name, size_t len, + u_char *value, size_t vlen); + void (*ready_handler)(ngx_js_http_t *http); + void (*error_handler)(ngx_js_http_t *http, + const char *err); +}; + + +ngx_resolver_ctx_t *ngx_js_http_resolve(ngx_js_http_t *http, ngx_resolver_t *r, + ngx_str_t *host, in_port_t port, ngx_msec_t timeout); +void ngx_js_http_connect(ngx_js_http_t *http); +void ngx_js_http_resolve_done(ngx_js_http_t *http); +void ngx_js_http_close_peer(ngx_js_http_t *http); +void ngx_js_http_trim(u_char **value, size_t *len, + njs_bool_t trim_c0_control_or_space); + + +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); +} + + +#endif /* _NGX_JS_HTTP_H_INCLUDED_ */ diff --git a/nginx/ngx_qjs_fetch.c b/nginx/ngx_qjs_fetch.c new file mode 100644 index 000000000..94a04e5ef --- /dev/null +++ b/nginx/ngx_qjs_fetch.c @@ -0,0 +1,2413 @@ + +/* + * Copyright (C) Dmitry Volyntsev + * Copyright (C) hongzhidao + * Copyright (C) Antoine Bonavita + * Copyright (C) NGINX, Inc. + */ + + +#include +#include +#include +#include +#include "ngx_js.h" +#include "ngx_js_http.h" + + +typedef struct { + njs_str_t name; + njs_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 njs_int_t ngx_qjs_headers_fill_header_free(JSContext *cx, + ngx_js_headers_t *headers, JSValue prop_name, JSValue prop_value); +static njs_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_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_js_request_t *request, ngx_url_t *u, int argc, JSValueConst *argv); +static void ngx_qjs_fetch_done(ngx_qjs_fetch_t *fetch, JSValue retval, + njs_int_t 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); +static void ngx_qjs_fetch_process_done(ngx_js_http_t *http); + + +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_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, "qjs fetch destructor:%p", + fetch); + + ngx_js_http_resolve_done(http); + ngx_js_http_close_peer(http); + + /* TODO */ + //njs_chb_destroy(&http->chain); + + 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_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, NJS_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; + + http->response.u.qjs_headers = 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 (njs_slow_path(event == NULL)) { + goto fail; + } + + ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx)); + + event->ctx = cx; + event->destructor = njs_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, "qjs fetch 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_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, NJS_OK); +} + + +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_done(ngx_qjs_fetch_t *fetch, JSValue retval, njs_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, + "qjs fetch done fetch:%p rc:%i", fetch, (ngx_int_t) rc); + + ngx_js_http_close_peer(http); + + if (fetch->event != NULL) { + action = fetch->promise_callbacks[(rc != NJS_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); + } +} + + +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_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 *ctx; + + 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); + } + + http = &fetch->http; + + 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")); + + ngx_js_ctx_t *js_ctx; + ngx_engine_t *e; + + js_ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx)); + e = js_ctx->engine; + + NJS_CHB_MP_INIT2(&http->chain, e->pool); + NJS_CHB_MP_INIT2(&http->response.chain, e->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) { + ctx = ngx_js_http_resolve(http, ngx_qjs_external_resolver(cx, external), + &u.host, u.port, + ngx_qjs_external_resolver_timeout(cx, external)); + if (ctx == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + if (ctx == NGX_NO_RESOLVER) { + JS_ThrowInternalError(cx, "no resolver defined"); + goto fail; + } + + return JS_DupValue(cx, fetch->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 JS_DupValue(cx, fetch->promise); + +fail: + + fetch->response_value = JS_GetException(cx); + + ngx_qjs_fetch_done(fetch, fetch->response_value, NJS_ERROR); + + return JS_DupValue(cx, fetch->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_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 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, request->u.qjs_headers); +} + + +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_js_response_t *response; + + response = JS_GetOpaque(val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + + /* TODO */ + //njs_chb_destroy(&response->chain); + + JS_FreeValueRT(rt, response->u.qjs_headers); +} + +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_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 + && 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_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"); + } + + 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_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"); + } + + 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_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"); + } + + 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_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 (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_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 (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_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_headers(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; + } + + if (JS_IsUndefined(request->u.qjs_headers)) { + request->u.qjs_headers = JS_NewObjectClass(cx, + NGX_QJS_CLASS_ID_FETCH_HEADERS); + if (JS_IsException(request->u.qjs_headers)) { + return JS_ThrowInternalError(cx, "fetch header creation failed"); + } + + JS_SetOpaque(request->u.qjs_headers, &request->headers); + } + + return JS_DupValue(cx, request->u.qjs_headers); +} + + +static JSValue +ngx_qjs_ext_fetch_request_bodyused(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, + (njs_int_t) 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, + (njs_int_t) request->credentials); +} + + +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, + (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_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_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_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 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; + + 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_copy(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, ""); + 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_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_headers(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; + } + + if (JS_IsUndefined(response->u.qjs_headers)) { + response->u.qjs_headers = JS_NewObjectClass(cx, + NGX_QJS_CLASS_ID_FETCH_HEADERS); + if (JS_IsException(response->u.qjs_headers)) { + return JS_ThrowInternalError(cx, "fetch header creation failed"); + } + + JS_SetOpaque(response->u.qjs_headers, &response->headers); + } + + return JS_DupValue(cx, response->u.qjs_headers); +} + + +static JSValue +ngx_qjs_ext_fetch_response_bodyused(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_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_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_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 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_js_request_t, url) ), + JS_CGETSET_MAGIC_DEF("method", ngx_qjs_ext_fetch_request_field, NULL, + offsetof(ngx_js_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_js_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 njs_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_uint_t i; + ngx_list_part_t *part; + ngx_js_tb_elt_t *h, **ph; + + ngx_js_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_js_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_js_headers_t *headers, + ngx_js_headers_t *orig) +{ + njs_int_t ret; + 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; + } + + 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_js_headers_t *headers, JSValue init) +{ + int ret; + JSValue header, prop_name, prop_value; + uint32_t i, len, length; + 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 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_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)) { + 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_js_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_js_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_js_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_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; + request->u.qjs_headers = 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 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_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 + && 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_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 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_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); + } + + 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_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 njs_mp_zalloc(): + * + * response->url.length = 0; + * response->status_text.length = 0; + */ + + response->code = 200; + response->headers.guard = GUARD_RESPONSE; + response->u.qjs_headers = JS_UNDEFINED; + + pool = ngx_qjs_external_pool(cx, JS_GetContextOpaque(cx)); + + 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, &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', <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..2579309f1 100644 --- a/nginx/t/js_fetch_objects.t +++ b/nginx/t/js_fetch_objects.t @@ -271,7 +271,9 @@ $t->write_file('test.js', <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); diff --git a/src/njs_chb.h b/src/njs_chb.h index 3dbe5ac9e..5d030443e 100644 --- a/src/njs_chb.h +++ b/src/njs_chb.h @@ -37,6 +37,9 @@ void njs_chb_init(njs_chb_t *chain, void *pool, njs_chb_alloc_t alloc, #define NJS_CHB_MP_INIT(chain, vm) \ njs_chb_init(chain, njs_vm_memory_pool(vm), (njs_chb_alloc_t) njs_mp_alloc,\ (njs_chb_free_t) njs_mp_free) +#define NJS_CHB_MP_INIT2(chain, mp) \ + njs_chb_init(chain, mp, (njs_chb_alloc_t) njs_mp_alloc, \ + (njs_chb_free_t) njs_mp_free) #define NJS_CHB_CTX_INIT(chain, ctx) \ njs_chb_init(chain, ctx, (njs_chb_alloc_t) js_malloc, \ (njs_chb_free_t) js_free) diff --git a/src/qjs.c b/src/qjs.c index a941ba71d..0642960e5 100644 --- a/src/qjs.c +++ b/src/qjs.c @@ -1009,6 +1009,13 @@ qjs_new_array_buffer(JSContext *cx, uint8_t *src, size_t len) } +JSValue +qjs_new_array_buffer_copy(JSContext *cx, uint8_t *src, size_t len) +{ + return JS_NewArrayBufferCopy(cx, src, len); +} + + JSValue qjs_string_create_chb(JSContext *cx, njs_chb_t *chain) { diff --git a/src/qjs.h b/src/qjs.h index d3bbc0e86..f6c92b871 100644 --- a/src/qjs.h +++ b/src/qjs.h @@ -67,6 +67,7 @@ JSContext *qjs_new_context(JSRuntime *rt, qjs_module_t **addons); JSValue qjs_new_uint8_array(JSContext *ctx, int argc, JSValueConst *argv); JSValue qjs_new_array_buffer(JSContext *cx, uint8_t *src, size_t len); +JSValue qjs_new_array_buffer_copy(JSContext *cx, uint8_t *src, size_t len); JSValue qjs_buffer_alloc(JSContext *ctx, size_t size); JSValue qjs_buffer_create(JSContext *ctx, u_char *start, size_t size); JSValue qjs_buffer_chb_alloc(JSContext *ctx, njs_chb_t *chain);