diff --git a/nginx/config b/nginx/config index b927c9d5b..5b9012bba 100644 --- a/nginx/config +++ b/nginx/config @@ -1,7 +1,8 @@ ngx_addon_name="ngx_js_module" NJS_DEPS="$ngx_addon_dir/ngx_js.h" -NJS_SRCS="$ngx_addon_dir/ngx_js.c" +NJS_SRCS="$ngx_addon_dir/ngx_js.c \ + $ngx_addon_dir/ngx_js_fetch.c" if [ $HTTP != NO ]; then ngx_module_type=HTTP diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c index 5bf28d3cb..560017896 100644 --- a/nginx/ngx_http_js_module.c +++ b/nginx/ngx_http_js_module.c @@ -179,6 +179,13 @@ static njs_host_event_t ngx_http_js_set_timer(njs_external_ptr_t external, static void ngx_http_js_clear_timer(njs_external_ptr_t external, njs_host_event_t event); static void ngx_http_js_timer_handler(ngx_event_t *ev); +static ngx_pool_t *ngx_http_js_pool(njs_vm_t *vm, ngx_http_request_t *r); +static ngx_resolver_t *ngx_http_js_resolver(njs_vm_t *vm, + ngx_http_request_t *r); +static ngx_msec_t ngx_http_js_resolver_timeout(njs_vm_t *vm, + ngx_http_request_t *r); +static void ngx_http_js_handle_vm_event(ngx_http_request_t *r, + njs_vm_event_t vm_event, njs_value_t *args, njs_uint_t nargs); static void ngx_http_js_handle_event(ngx_http_request_t *r, njs_vm_event_t vm_event, njs_value_t *args, njs_uint_t nargs); @@ -576,11 +583,15 @@ static njs_vm_ops_t ngx_http_js_ops = { static uintptr_t ngx_http_js_uptr[] = { offsetof(ngx_http_request_t, connection), + (uintptr_t) ngx_http_js_pool, + (uintptr_t) ngx_http_js_resolver, + (uintptr_t) ngx_http_js_resolver_timeout, + (uintptr_t) ngx_http_js_handle_event, }; static njs_vm_meta_t ngx_http_js_metas = { - .size = 1, + .size = 5, .values = ngx_http_js_uptr }; @@ -2754,7 +2765,7 @@ ngx_http_js_subrequest_done(ngx_http_request_t *r, void *data, ngx_int_t rc) return NGX_ERROR; } - ngx_http_js_handle_event(r->parent, vm_event, njs_value_arg(&reply), 1); + ngx_http_js_handle_vm_event(r->parent, vm_event, njs_value_arg(&reply), 1); return NGX_OK; } @@ -2895,7 +2906,6 @@ ngx_http_js_clear_timer(njs_external_ptr_t external, njs_host_event_t event) static void ngx_http_js_timer_handler(ngx_event_t *ev) { - ngx_connection_t *c; ngx_http_request_t *r; ngx_http_js_event_t *js_event; @@ -2903,16 +2913,41 @@ ngx_http_js_timer_handler(ngx_event_t *ev) r = js_event->request; - c = r->connection; - ngx_http_js_handle_event(r, js_event->vm_event, NULL, 0); +} + + +static ngx_pool_t * +ngx_http_js_pool(njs_vm_t *vm, ngx_http_request_t *r) +{ + return r->pool; +} - ngx_http_run_posted_requests(c); + +static ngx_resolver_t * +ngx_http_js_resolver(njs_vm_t *vm, ngx_http_request_t *r) +{ + ngx_http_core_loc_conf_t *clcf; + + clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); + + return clcf->resolver; +} + + +static ngx_msec_t +ngx_http_js_resolver_timeout(njs_vm_t *vm, ngx_http_request_t *r) +{ + ngx_http_core_loc_conf_t *clcf; + + clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); + + return clcf->resolver_timeout; } static void -ngx_http_js_handle_event(ngx_http_request_t *r, njs_vm_event_t vm_event, +ngx_http_js_handle_vm_event(ngx_http_request_t *r, njs_vm_event_t vm_event, njs_value_t *args, njs_uint_t nargs) { njs_int_t rc; @@ -2925,6 +2960,10 @@ ngx_http_js_handle_event(ngx_http_request_t *r, njs_vm_event_t vm_event, rc = njs_vm_run(ctx->vm); + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http js post event handler rc: %i event: %p", + (ngx_int_t) rc, vm_event); + if (rc == NJS_ERROR) { njs_vm_retval_string(ctx->vm, &exception); @@ -2940,6 +2979,16 @@ ngx_http_js_handle_event(ngx_http_request_t *r, njs_vm_event_t vm_event, } +static void +ngx_http_js_handle_event(ngx_http_request_t *r, njs_vm_event_t vm_event, + njs_value_t *args, njs_uint_t nargs) +{ + ngx_http_js_handle_vm_event(r, vm_event, args, nargs); + + ngx_http_run_posted_requests(r->connection); +} + + static char * ngx_http_js_init_main_conf(ngx_conf_t *cf, void *conf) { diff --git a/nginx/ngx_js.c b/nginx/ngx_js.c index f47b916fd..e2c083a91 100644 --- a/nginx/ngx_js.c +++ b/nginx/ngx_js.c @@ -9,6 +9,7 @@ #include #include #include "ngx_js.h" +#include "ngx_js_fetch.h" static njs_external_t ngx_js_ext_core[] = { @@ -50,6 +51,17 @@ static njs_external_t ngx_js_ext_core[] = { .magic32 = NGX_LOG_ERR, } }, + + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("fetch"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = ngx_js_ext_fetch, + } + }, }; @@ -117,10 +129,16 @@ ngx_js_string(njs_vm_t *vm, njs_value_t *value, njs_str_t *str) ngx_int_t ngx_js_core_init(njs_vm_t *vm, ngx_log_t *log) { + ngx_int_t rc; njs_int_t ret, proto_id; njs_str_t name; njs_opaque_value_t value; + rc = ngx_js_fetch_init(vm, log); + if (rc != NGX_OK) { + return NGX_ERROR; + } + proto_id = njs_vm_external_prototype(vm, ngx_js_ext_core, njs_nitems(ngx_js_ext_core)); if (proto_id < 0) { @@ -177,6 +195,16 @@ ngx_js_ext_constant(njs_vm_t *vm, njs_object_prop_t *prop, } +njs_int_t +ngx_js_ext_boolean(njs_vm_t *vm, njs_object_prop_t *prop, + njs_value_t *value, njs_value_t *setval, njs_value_t *retval) +{ + njs_value_boolean_set(retval, njs_vm_prop_magic32(prop)); + + return NJS_OK; +} + + njs_int_t ngx_js_ext_log(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t level) diff --git a/nginx/ngx_js.h b/nginx/ngx_js.h index fe3112449..b2165e2dd 100644 --- a/nginx/ngx_js.h +++ b/nginx/ngx_js.h @@ -20,10 +20,28 @@ #define NGX_JS_BUFFER 2 #define NGX_JS_PROTO_MAIN 0 +#define NGX_JS_PROTO_RESPONSE 1 -#define ngx_external_connection(vm, ext) \ - (*((ngx_connection_t **) ((u_char *) ext + njs_vm_meta(vm, 0)))) +typedef ngx_pool_t *(*ngx_external_pool_pt)(njs_vm_t *vm, njs_external_ptr_t e); +typedef void (*ngx_js_event_handler_pt)(njs_external_ptr_t e, + njs_vm_event_t vm_event, njs_value_t *args, njs_uint_t nargs); +typedef ngx_resolver_t *(*ngx_external_resolver_pt)(njs_vm_t *vm, + njs_external_ptr_t e); +typedef ngx_msec_t (*ngx_external_resolver_timeout_pt)(njs_vm_t *vm, + njs_external_ptr_t e); + + +#define ngx_external_connection(vm, e) \ + (*((ngx_connection_t **) ((u_char *) (e) + njs_vm_meta(vm, 0)))) +#define ngx_external_pool(vm, e) \ + ((ngx_external_pool_pt) njs_vm_meta(vm, 1))(vm, e) +#define ngx_external_resolver(vm, e) \ + ((ngx_external_resolver_pt) njs_vm_meta(vm, 2))(vm, e) +#define ngx_external_resolver_timeout(vm, e) \ + ((ngx_external_resolver_timeout_pt) njs_vm_meta(vm, 3))(vm, e) +#define ngx_external_event_handler(vm, e) \ + ((ngx_js_event_handler_pt) njs_vm_meta(vm, 4)) #define ngx_js_prop(vm, type, value, start, len) \ @@ -41,6 +59,8 @@ njs_int_t ngx_js_ext_string(njs_vm_t *vm, njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval, njs_value_t *retval); njs_int_t ngx_js_ext_constant(njs_vm_t *vm, njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval, njs_value_t *retval); +njs_int_t ngx_js_ext_boolean(njs_vm_t *vm, njs_object_prop_t *prop, + njs_value_t *value, njs_value_t *setval, njs_value_t *retval); ngx_int_t ngx_js_core_init(njs_vm_t *vm, ngx_log_t *log); diff --git a/nginx/ngx_js_fetch.c b/nginx/ngx_js_fetch.c new file mode 100644 index 000000000..bcaf8565b --- /dev/null +++ b/nginx/ngx_js_fetch.c @@ -0,0 +1,2212 @@ + +/* + * Copyright (C) Dmitry Volyntsev + * Copyright (C) hongzhidao + * Copyright (C) NGINX, Inc. + */ + + +#include +#include +#include +#include +#include "ngx_js.h" + + +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; + + +struct ngx_js_http_s { + ngx_log_t *log; + ngx_pool_t *pool; + + njs_vm_t *vm; + njs_external_ptr_t external; + njs_vm_event_t vm_event; + ngx_js_event_handler_pt event_handler; + + 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; + + njs_str_t url; + ngx_array_t headers; + + ngx_buf_t *buffer; + ngx_buf_t *chunk; + njs_chb_t chain; + + njs_opaque_value_t reply; + njs_opaque_value_t promise; + njs_opaque_value_t promise_callbacks[2]; + + uint8_t done; + uint8_t body_used; + 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); +}; + + +#define ngx_js_http_error(http, err, fmt, ...) \ + do { \ + njs_vm_value_error_set((http)->vm, njs_value_arg(&(http)->reply), \ + fmt, ##__VA_ARGS__); \ + ngx_js_http_fetch_done(http, &(http)->reply, NJS_ERROR); \ + } while (0) + + +static ngx_js_http_t *ngx_js_http_alloc(njs_vm_t *vm, ngx_pool_t *pool, + ngx_log_t *log); +static void ngx_js_resolve_handler(ngx_resolver_ctx_t *ctx); +static njs_int_t ngx_js_fetch_result(njs_vm_t *vm, ngx_js_http_t *http, + njs_value_t *result, njs_int_t rc); +static njs_int_t ngx_js_fetch_promissified_result(njs_vm_t *vm, + njs_value_t *result, njs_int_t rc); +static void ngx_js_http_fetch_done(ngx_js_http_t *http, + 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); +static njs_int_t ngx_js_http_connect(ngx_js_http_t *http); +static njs_int_t 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 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_response_js_ext_headers_get(njs_vm_t *vm, + njs_value_t *args, njs_uint_t nargs, njs_index_t as_array); +static njs_int_t ngx_response_js_ext_headers_has(njs_vm_t *vm, + njs_value_t *args, njs_uint_t nargs, njs_index_t unused); +static njs_int_t ngx_response_js_ext_header(njs_vm_t *vm, + njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval, + njs_value_t *retval); +static njs_int_t ngx_response_js_ext_keys(njs_vm_t *vm, njs_value_t *value, + njs_value_t *keys); +static njs_int_t ngx_response_js_ext_status(njs_vm_t *vm, + njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval, + njs_value_t *retval); +static njs_int_t ngx_response_js_ext_status_text(njs_vm_t *vm, + njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval, + njs_value_t *retval); +static njs_int_t ngx_response_js_ext_ok(njs_vm_t *vm, + njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval, + njs_value_t *retval); +static njs_int_t ngx_response_js_ext_body_used(njs_vm_t *vm, + njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval, + njs_value_t *retval); +static njs_int_t ngx_response_js_ext_type(njs_vm_t *vm, + njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval, + njs_value_t *retval); +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); + + +static njs_external_t ngx_js_ext_http_response_headers[] = { + + { + .flags = NJS_EXTERN_PROPERTY | NJS_EXTERN_SYMBOL, + .name.symbol = NJS_SYMBOL_TO_STRING_TAG, + .u.property = { + .value = "Headers", + } + }, + + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("get"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = ngx_response_js_ext_headers_get, + } + }, + + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("getAll"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = ngx_response_js_ext_headers_get, + .magic8 = 1 + } + }, + + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("has"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = ngx_response_js_ext_headers_has, + } + }, + +}; + + +static njs_external_t ngx_js_ext_http_response[] = { + + { + .flags = NJS_EXTERN_PROPERTY | NJS_EXTERN_SYMBOL, + .name.symbol = NJS_SYMBOL_TO_STRING_TAG, + .u.property = { + .value = "Response", + } + }, + + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("arrayBuffer"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = ngx_response_js_ext_body, +#define NGX_JS_BODY_ARRAY_BUFFER 0 +#define NGX_JS_BODY_JSON 1 +#define NGX_JS_BODY_TEXT 2 + .magic8 = NGX_JS_BODY_ARRAY_BUFFER + } + }, + + { + .flags = NJS_EXTERN_PROPERTY, + .name.string = njs_str("bodyUsed"), + .enumerable = 1, + .u.property = { + .handler = ngx_response_js_ext_body_used, + } + }, + + { + .flags = NJS_EXTERN_OBJECT, + .name.string = njs_str("headers"), + .enumerable = 1, + .u.object = { + .enumerable = 1, + .properties = ngx_js_ext_http_response_headers, + .nproperties = njs_nitems(ngx_js_ext_http_response_headers), + .prop_handler = ngx_response_js_ext_header, + .keys = ngx_response_js_ext_keys, + } + }, + + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("json"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = ngx_response_js_ext_body, + .magic8 = NGX_JS_BODY_JSON + } + }, + + { + .flags = NJS_EXTERN_PROPERTY, + .name.string = njs_str("ok"), + .enumerable = 1, + .u.property = { + .handler = ngx_response_js_ext_ok, + } + }, + + { + .flags = NJS_EXTERN_PROPERTY, + .name.string = njs_str("redirected"), + .enumerable = 1, + .u.property = { + .handler = ngx_js_ext_boolean, + .magic32 = 0, + } + }, + + { + .flags = NJS_EXTERN_PROPERTY, + .name.string = njs_str("status"), + .enumerable = 1, + .u.property = { + .handler = ngx_response_js_ext_status, + } + }, + + { + .flags = NJS_EXTERN_PROPERTY, + .name.string = njs_str("statusText"), + .enumerable = 1, + .u.property = { + .handler = ngx_response_js_ext_status_text, + } + }, + + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("text"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = ngx_response_js_ext_body, + .magic8 = NGX_JS_BODY_TEXT + } + }, + + { + .flags = NJS_EXTERN_PROPERTY, + .name.string = njs_str("type"), + .enumerable = 1, + .u.property = { + .handler = ngx_response_js_ext_type, + } + }, + + { + .flags = NJS_EXTERN_PROPERTY, + .name.string = njs_str("url"), + .enumerable = 1, + .u.property = { + .handler = ngx_js_ext_string, + .magic32 = offsetof(ngx_js_http_t, url), + } + }, +}; + + +njs_int_t +ngx_js_ext_fetch(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, + njs_index_t unused) +{ + int64_t i, length; + njs_int_t ret; + njs_str_t method, body, name, header; + ngx_url_t u; + njs_bool_t has_host; + ngx_pool_t *pool; + njs_value_t *init, *value, *headers, *keys; + ngx_js_http_t *http; + ngx_connection_t *c; + ngx_resolver_ctx_t *ctx; + njs_external_ptr_t external; + njs_opaque_value_t *start, lvalue, headers_value; + + static const njs_str_t body_key = njs_str("body"); + static const njs_str_t headers_key = njs_str("headers"); + static const njs_str_t buffer_size_key = njs_str("buffer_size"); + static const njs_str_t body_size_key = njs_str("max_response_body_size"); + static const njs_str_t method_key = njs_str("method"); + + external = njs_vm_external(vm, njs_argument(args, 0)); + if (external == NULL) { + njs_vm_error(vm, "\"this\" is not an external"); + return NJS_ERROR; + } + + c = ngx_external_connection(vm, external); + pool = ngx_external_pool(vm, external); + + http = ngx_js_http_alloc(vm, pool, c->log); + if (http == NULL) { + return NJS_ERROR; + } + + http->external = external; + http->event_handler = ngx_external_event_handler(vm, external); + http->buffer_size = 4096; + http->max_response_body_size = 32 * 1024; + + ret = ngx_js_string(vm, njs_arg(args, nargs, 1), &http->url); + if (ret != NJS_OK) { + njs_vm_error(vm, "failed to convert url arg"); + goto fail; + } + + ngx_memzero(&u, sizeof(ngx_url_t)); + + u.url.len = http->url.length; + u.url.data = http->url.start; + u.default_port = 80; + u.uri_part = 1; + u.no_resolve = 1; + + if (u.url.len > 7 + && ngx_strncasecmp(u.url.data, (u_char *) "http://", 7) == 0) + { + u.url.len -= 7; + u.url.data += 7; + + } else { + njs_vm_error(vm, "unsupported URL prefix"); + goto fail; + } + + if (ngx_parse_url(pool, &u) != NGX_OK) { + njs_vm_error(vm, "invalid url"); + goto fail; + } + + init = njs_arg(args, nargs, 2); + + method = njs_str_value("GET"); + body = njs_str_value(""); + headers = NULL; + + if (njs_value_is_object(init)) { + value = njs_vm_object_prop(vm, init, &method_key, &lvalue); + if (value != NULL && ngx_js_string(vm, value, &method) != NGX_OK) { + goto fail; + } + + headers = njs_vm_object_prop(vm, init, &headers_key, &headers_value); + if (headers != NULL && !njs_value_is_object(headers)) { + njs_vm_error(vm, "headers is not an object"); + goto fail; + } + + value = njs_vm_object_prop(vm, init, &body_key, &lvalue); + if (value != NULL && ngx_js_string(vm, value, &body) != NGX_OK) { + goto fail; + } + + value = njs_vm_object_prop(vm, init, &buffer_size_key, &lvalue); + if (value != NULL + && ngx_js_integer(vm, value, &http->buffer_size) + != NGX_OK) + { + goto fail; + } + + value = njs_vm_object_prop(vm, init, &body_size_key, &lvalue); + if (value != NULL + && ngx_js_integer(vm, value, &http->max_response_body_size) + != NGX_OK) + { + goto fail; + } + } + + njs_chb_init(&http->chain, njs_vm_memory_pool(vm)); + + njs_chb_append(&http->chain, method.start, method.length); + 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); + njs_chb_append_literal(&http->chain, "Connection: close" CRLF); + + has_host = 0; + + if (headers != NULL) { + keys = njs_vm_object_keys(vm, headers, njs_value_arg(&lvalue)); + if (keys == NULL) { + goto fail; + } + + start = (njs_opaque_value_t *) njs_vm_array_start(vm, keys); + if (start == NULL) { + goto fail; + } + + (void) njs_vm_array_length(vm, keys, &length); + + for (i = 0; i < length; i++) { + if (ngx_js_string(vm, njs_value_arg(start), &name) != NGX_OK) { + goto fail; + } + + start++; + + value = njs_vm_object_prop(vm, headers, &name, &lvalue); + if (ret != NJS_OK) { + goto fail; + } + + if (njs_value_is_null_or_undefined(value)) { + continue; + } + + if (ngx_js_string(vm, value, &header) != NGX_OK) { + goto fail; + } + + if (name.length == 4 + && ngx_strncasecmp(name.start, (u_char *) "Host", 4) == 0) + { + has_host = 1; + } + + njs_chb_append(&http->chain, name.start, name.length); + njs_chb_append_literal(&http->chain, ": "); + njs_chb_append(&http->chain, header.start, header.length); + njs_chb_append_literal(&http->chain, CRLF); + } + } + + if (!has_host) { + njs_chb_append_literal(&http->chain, "Host: "); + njs_chb_append(&http->chain, u.host.data, u.host.len); + njs_chb_append_literal(&http->chain, CRLF); + } + + if (body.length != 0) { + njs_chb_sprintf(&http->chain, 32, "Content-Length: %uz" CRLF CRLF, + body.length); + njs_chb_append(&http->chain, body.start, body.length); + + } else { + njs_chb_append_literal(&http->chain, CRLF); + } + + if (u.addrs == NULL) { + ctx = ngx_resolve_start(ngx_external_resolver(vm, external), NULL); + if (ctx == NULL) { + njs_vm_memory_error(vm); + return NJS_ERROR; + } + + if (ctx == NGX_NO_RESOLVER) { + njs_vm_error(vm, "no resolver defined"); + goto fail; + } + + http->ctx = ctx; + http->port = u.port; + + ctx->name = u.host; + ctx->handler = ngx_js_resolve_handler; + ctx->data = http; + ctx->timeout = ngx_external_resolver_timeout(vm, external); + + ret = ngx_resolve_name(http->ctx); + if (ret != NGX_OK) { + http->ctx = NULL; + njs_vm_memory_error(vm); + return NJS_ERROR; + } + + } else { + http->naddrs = 1; + ngx_memcpy(&http->addr, &u.addrs[0], sizeof(ngx_addr_t)); + http->addrs = &http->addr; + + ret = ngx_js_http_connect(http); + } + + return ngx_js_fetch_result(vm, http, njs_value_arg(&http->reply), ret); + +fail: + + return ngx_js_fetch_result(vm, http, njs_vm_retval(vm), NJS_ERROR); +} + + +static ngx_js_http_t * +ngx_js_http_alloc(njs_vm_t *vm, ngx_pool_t *pool, ngx_log_t *log) +{ + ngx_js_http_t *http; + + http = ngx_pcalloc(pool, sizeof(ngx_js_http_t)); + if (http == NULL) { + goto failed; + } + + http->pool = pool; + http->log = log; + http->vm = vm; + + http->timeout = 10000; + + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "js http alloc:%p", http); + + return http; + +failed: + + njs_vm_error(vm, "internal error"); + + return NULL; +} + + +static void +ngx_js_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, 0, "\"%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_resolve_name_done(ctx); + http->ctx = NULL; + + (void) ngx_js_http_connect(http); + + return; + +failed: + + ngx_js_http_error(http, 0, "memory error"); +} + + +static void +njs_js_http_destructor(njs_external_ptr_t external, njs_host_event_t host) +{ + ngx_js_http_t *http; + + http = host; + + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, "js http destructor:%p", + http); + + if (http->ctx != NULL) { + ngx_resolve_name_done(http->ctx); + http->ctx = NULL; + } + + if (http->peer.connection != NULL) { + ngx_close_connection(http->peer.connection); + http->peer.connection = NULL; + } +} + + +static njs_int_t +ngx_js_fetch_result(njs_vm_t *vm, ngx_js_http_t *http, njs_value_t *result, + njs_int_t rc) +{ + njs_int_t ret; + njs_function_t *callback; + njs_vm_event_t vm_event; + njs_opaque_value_t arguments[2]; + + ret = njs_vm_promise_create(vm, njs_value_arg(&http->promise), + njs_value_arg(&http->promise_callbacks)); + if (ret != NJS_OK) { + goto error; + } + + callback = njs_vm_function_alloc(vm, ngx_js_http_promise_trampoline); + if (callback == NULL) { + goto error; + } + + vm_event = njs_vm_add_event(vm, callback, 1, http, njs_js_http_destructor); + if (vm_event == NULL) { + goto error; + } + + http->vm_event = vm_event; + + if (rc == NJS_ERROR) { + njs_value_assign(&arguments[0], &http->promise_callbacks[1]); + njs_value_assign(&arguments[1], result); + + ret = njs_vm_post_event(vm, vm_event, njs_value_arg(&arguments), 2); + if (ret == NJS_ERROR) { + goto error; + } + } + + njs_vm_retval_set(vm, njs_value_arg(&http->promise)); + + return NJS_OK; + +error: + + njs_vm_error(vm, "internal error"); + + return NJS_ERROR; +} + + +static njs_int_t +ngx_js_fetch_promissified_result(njs_vm_t *vm, njs_value_t *result, + njs_int_t rc) +{ + njs_int_t ret; + njs_function_t *callback; + njs_vm_event_t vm_event; + njs_opaque_value_t retval, arguments[2]; + + ret = njs_vm_promise_create(vm, njs_value_arg(&retval), + njs_value_arg(&arguments)); + if (ret != NJS_OK) { + goto error; + } + + callback = njs_vm_function_alloc(vm, ngx_js_http_promise_trampoline); + if (callback == NULL) { + goto error; + } + + vm_event = njs_vm_add_event(vm, callback, 1, NULL, NULL); + if (vm_event == NULL) { + goto error; + } + + njs_value_assign(&arguments[0], &arguments[(rc != NJS_OK)]); + njs_value_assign(&arguments[1], result); + + ret = njs_vm_post_event(vm, vm_event, njs_value_arg(&arguments), 2); + if (ret == NJS_ERROR) { + goto error; + } + + njs_vm_retval_set(vm, njs_value_arg(&retval)); + + return NJS_OK; + +error: + + njs_vm_error(vm, "internal error"); + + return NJS_ERROR; +} + + +static void +ngx_js_http_fetch_done(ngx_js_http_t *http, njs_opaque_value_t *retval, + njs_int_t rc) +{ + njs_opaque_value_t arguments[2], *action; + + ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0, + "js fetch done http:%p rc:%i", http, (ngx_int_t) rc); + + if (http->peer.connection != NULL) { + ngx_close_connection(http->peer.connection); + http->peer.connection = NULL; + } + + if (http->vm_event != NULL) { + action = &http->promise_callbacks[(rc != NJS_OK)]; + njs_value_assign(&arguments[0], action); + njs_value_assign(&arguments[1], retval); + http->event_handler(http->external, http->vm_event, + njs_value_arg(&arguments), 2); + } +} + + +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_function_t *callback; + + callback = njs_value_function(njs_argument(args, 1)); + + if (callback != NULL) { + return njs_vm_call(vm, callback, njs_argument(args, 2), 1); + } + + return NJS_OK; +} + + +static njs_int_t +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, 0, "connect failed"); + return NJS_ERROR; + } + + if (rc == NGX_BUSY || rc == NGX_DECLINED) { + return ngx_js_http_next(http); + } + + 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; + + if (http->timeout) { + ngx_add_timer(http->peer.connection->read, http->timeout); + ngx_add_timer(http->peer.connection->write, http->timeout); + } + + if (rc == NGX_OK) { + ngx_js_http_write_handler(http->peer.connection->write); + } + + return NJS_OK; +} + + +static njs_int_t +ngx_js_http_next(ngx_js_http_t *http) +{ + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, http->log, 0, "js http next"); + + if (++http->naddr >= http->naddrs) { + ngx_js_http_error(http, 0, "connect failed"); + return NJS_ERROR; + } + + if (http->peer.connection != NULL) { + ngx_close_connection(http->peer.connection); + http->peer.connection = NULL; + } + + http->buffer = NULL; + + return 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, NGX_ETIMEDOUT, "write timed out"); + return; + } + + b = http->buffer; + + if (b == NULL) { + size = njs_chb_size(&http->chain); + if (size < 0) { + ngx_js_http_error(http, 0, "memory error"); + return; + } + + b = ngx_create_temp_buf(http->pool, size); + if (b == NULL) { + ngx_js_http_error(http, 0, "memory error"); + return; + } + + njs_chb_join_to(&http->chain, b->last); + b->last += size; + + http->buffer = b; + } + + size = b->last - b->pos; + + n = ngx_send(c, b->pos, size); + + if (n == NGX_ERROR) { + (void) 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, 0, "write failed"); + } + + return; + } + } + + if (!wev->timer_set && http->timeout) { + 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, NGX_ETIMEDOUT, "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, 0, "memory error"); + return; + } + + http->buffer = b; + } + + for ( ;; ) { + b = http->buffer; + size = b->end - b->last; + + n = ngx_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, 0, "read failed"); + } + + return; + } + + if (n == NGX_ERROR) { + (void) 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, 0, "prematurely closed connection"); + } +} + + +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->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, 0, "invalid fetch status line"); + + return NGX_ERROR; +} + + +static ngx_int_t +ngx_js_http_process_headers(ngx_js_http_t *http) +{ + ngx_int_t rc; + ngx_table_elt_t *h; + 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->headers.size == 0) { + rc = ngx_array_init(&http->headers, http->pool, 4, + sizeof(ngx_table_elt_t)); + if (rc != NGX_OK) { + return NGX_ERROR; + } + } + + for ( ;; ) { + rc = ngx_js_http_parse_header_line(hp, http->buffer); + + if (rc == NGX_OK) { + h = ngx_array_push(&http->headers); + if (h == NULL) { + return NGX_ERROR; + } + + h->hash = 1; + h->key.data = hp->header_name_start; + h->key.len = hp->header_name_end - hp->header_name_start; + + h->value.data = hp->header_start; + h->value.len = hp->header_end - hp->header_start; + + ngx_log_debug4(NGX_LOG_DEBUG_EVENT, http->log, 0, + "js http header \"%*s: %*s\"", + h->key.len, h->key.data, h->value.len, + h->value.data); + + if (h->key.len == njs_strlen("Transfer-Encoding") + && h->value.len == njs_strlen("chunked") + && ngx_strncasecmp(h->key.data, (u_char *) "Transfer-Encoding", + h->key.len) == 0 + && ngx_strncasecmp(h->value.data, (u_char *) "chunked", + h->value.len) == 0) + { + hp->chunked = 1; + } + + if (h->key.len == njs_strlen("Content-Length") + && ngx_strncasecmp(h->key.data, (u_char *) "Content-Length", + h->key.len) == 0) + { + hp->content_length_n = ngx_atoof(h->value.data, h->value.len); + if (hp->content_length_n == NGX_ERROR) { + ngx_js_http_error(http, 0, "invalid fetch content length"); + return NGX_ERROR; + } + + if (hp->content_length_n + > (off_t) http->max_response_body_size) + { + ngx_js_http_error(http, 0, + "fetch content length is too large"); + return NGX_ERROR; + } + } + + continue; + } + + if (rc == NGX_DONE) { + break; + } + + if (rc == NGX_AGAIN) { + return NGX_AGAIN; + } + + /* rc == NGX_ERROR */ + + ngx_js_http_error(http, 0, "invalid fetch header"); + + return NGX_ERROR; + } + + njs_chb_destroy(&http->chain); + njs_chb_init(&http->chain, njs_vm_memory_pool(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, need; + ngx_int_t rc; + njs_int_t ret; + 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->chain); + if (size < 0) { + ngx_js_http_error(http, 0, "memory error"); + return NGX_ERROR; + } + + if (size == http->http_parse.content_length_n) { + ret = njs_vm_external_create(http->vm, njs_value_arg(&http->reply), + NGX_JS_PROTO_RESPONSE, http, 0); + if (ret != NJS_OK) { + ngx_js_http_error(http, 0, "fetch object creation failed"); + return NGX_ERROR; + } + + ngx_js_http_fetch_done(http, &http->reply, NJS_OK); + return NGX_DONE; + } + + if (http->http_parse.chunked + && http->http_parse.content_length_n == 0) + { + ngx_js_http_error(http, 0, "invalid fetch chunked response"); + return NGX_ERROR; + } + + if (size < http->http_parse.content_length_n) { + return NGX_AGAIN; + } + + ngx_js_http_error(http, 0, "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->chain); + if (rc == NGX_ERROR) { + ngx_js_http_error(http, 0, "invalid fetch chunked response"); + return NGX_ERROR; + } + + size = njs_chb_size(&http->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, 0, "very large fetch chunked response"); + return NGX_ERROR; + } + + b->pos = http->http_chunk_parse.pos; + + } else { + need = http->http_parse.content_length_n - njs_chb_size(&http->chain); + size = ngx_min(need, b->last - b->pos); + + if (size > 0) { + njs_chb_append(&http->chain, b->pos, size); + b->pos += size; + rc = NGX_AGAIN; + + } else { + rc = 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, 0, "memory error"); + return NGX_ERROR; + } + + http->buffer = b; + http->chunk = b; + + } else { + b->last = b->start; + b->pos = b->start; + } + } + + return rc; +} + + +static ngx_int_t +ngx_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 == '-') { + 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 http dummy handler"); +} + + +static njs_int_t +ngx_response_js_ext_header_get(njs_vm_t *vm, njs_value_t *value, + njs_str_t *name, njs_value_t *retval, njs_bool_t as_array) +{ + u_char *data, *p, *start, *end; + size_t len; + njs_int_t rc; + ngx_uint_t i; + ngx_js_http_t *http; + ngx_table_elt_t *header, *h; + + http = njs_vm_external(vm, value); + if (http == NULL) { + njs_value_null_set(retval); + return NJS_DECLINED; + } + + p = NULL; + start = NULL; + end = NULL; + + if (as_array) { + rc = njs_vm_array_alloc(vm, retval, 2); + if (rc != NJS_OK) { + return NJS_ERROR; + } + } + + header = http->headers.elts; + + for (i = 0; i < http->headers.nelts; i++) { + h = &header[i]; + + if (h->hash == 0 + || h->key.len != name->length + || ngx_strncasecmp(h->key.data, name->start, name->length) != 0) + { + continue; + } + + if (as_array) { + value = njs_vm_array_push(vm, retval); + if (value == NULL) { + return NJS_ERROR; + } + + rc = njs_vm_value_string_set(vm, value, h->value.data, + h->value.len); + if (rc != NJS_OK) { + return NJS_ERROR; + } + + continue; + } + + if (p == NULL) { + start = h->value.data; + end = h->value.data + h->value.len; + p = end; + continue; + } + + if (p + h->value.len + 1 > end) { + len = njs_max(p + h->value.len + 1 - start, 2 * (end - start)); + + data = ngx_pnalloc(http->pool, len); + if (data == NULL) { + njs_vm_memory_error(vm); + return NJS_ERROR; + } + + p = ngx_cpymem(data, start, p - start); + start = data; + end = data + len; + } + + *p++ = ','; + p = ngx_cpymem(p, h->value.data, h->value.len); + } + + if (as_array) { + return NJS_OK; + } + + if (p == NULL) { + njs_value_null_set(retval); + return NJS_DECLINED; + } + + return njs_vm_value_string_set(vm, retval, start, p - start); +} + + +static njs_int_t +ngx_response_js_ext_headers_get(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t as_array) +{ + njs_int_t ret; + njs_str_t name; + + ret = ngx_js_string(vm, njs_arg(args, nargs, 1), &name); + if (ret != NJS_OK) { + return NJS_ERROR; + } + + ret = ngx_response_js_ext_header_get(vm, njs_argument(args, 0), + &name, njs_vm_retval(vm), as_array); + + return (ret != NJS_ERROR) ? NJS_OK : NJS_ERROR; +} + + +static njs_int_t +ngx_response_js_ext_headers_has(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t unused) +{ + njs_int_t ret; + njs_str_t name; + + ret = ngx_js_string(vm, njs_arg(args, nargs, 1), &name); + if (ret != NJS_OK) { + return NJS_ERROR; + } + + ret = ngx_response_js_ext_header_get(vm, njs_argument(args, 0), + &name, njs_vm_retval(vm), 0); + if (ret == NJS_ERROR) { + return NJS_ERROR; + } + + njs_value_boolean_set(njs_vm_retval(vm), ret == NJS_OK); + + return NJS_OK; +} + + +static njs_int_t +ngx_response_js_ext_header(njs_vm_t *vm, njs_object_prop_t *prop, + njs_value_t *value, njs_value_t *setval, njs_value_t *retval) +{ + njs_int_t ret; + njs_str_t name; + + ret = njs_vm_prop_name(vm, prop, &name); + if (ret != NJS_OK) { + return NJS_ERROR; + } + + return ngx_response_js_ext_header_get(vm, value, &name, njs_vm_retval(vm), + 0); +} + + +static njs_int_t +ngx_response_js_ext_keys(njs_vm_t *vm, njs_value_t *value, njs_value_t *keys) +{ + njs_int_t rc; + njs_str_t hdr; + ngx_uint_t i, k, length; + njs_value_t *start; + ngx_js_http_t *http; + ngx_table_elt_t *h, *headers; + + http = njs_vm_external(vm, value); + if (http == NULL) { + njs_value_undefined_set(keys); + return NJS_DECLINED; + } + + rc = njs_vm_array_alloc(vm, keys, 8); + if (rc != NJS_OK) { + return NJS_ERROR; + } + + length = 0; + headers = http->headers.elts; + start = njs_vm_array_start(vm, keys); + + for (i = 0; i < http->headers.nelts; i++) { + h = &headers[i]; + + for (k = 0; k < length; k++) { + njs_value_string_get(njs_argument(start, k), &hdr); + + if (h->key.len == hdr.length + && ngx_strncasecmp(h->key.data, hdr.start, hdr.length) == 0) + { + break; + } + } + } + + return NJS_OK; +} + + +static njs_int_t +ngx_response_js_ext_body(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t type) +{ + njs_int_t ret; + njs_str_t string; + ngx_js_http_t *http; + njs_opaque_value_t retval; + + http = njs_vm_external(vm, njs_argument(args, 0)); + if (http == NULL) { + njs_value_undefined_set(njs_vm_retval(vm)); + return NJS_DECLINED; + } + + if (http->body_used) { + njs_vm_error(vm, "body stream already read"); + return NJS_ERROR; + } + + http->body_used = 1; + + ret = njs_chb_join(&http->chain, &string); + if (ret != NJS_OK) { + njs_vm_memory_error(http->vm); + return NJS_ERROR; + } + + switch (type) { + case NGX_JS_BODY_ARRAY_BUFFER: + ret = njs_vm_value_array_buffer_set(http->vm, njs_value_arg(&retval), + string.start, string.length); + if (ret != NJS_OK) { + njs_vm_memory_error(http->vm); + return NJS_ERROR; + } + + break; + + case NGX_JS_BODY_JSON: + case NGX_JS_BODY_TEXT: + default: + ret = njs_vm_value_string_set(http->vm, njs_value_arg(&retval), + string.start, string.length); + if (ret != NJS_OK) { + njs_vm_memory_error(http->vm); + return NJS_ERROR; + } + + if (type == NGX_JS_BODY_JSON) { + ret = njs_vm_json_parse(vm, njs_value_arg(&retval), 1); + njs_value_assign(&retval, njs_vm_retval(vm)); + } + } + + return ngx_js_fetch_promissified_result(http->vm, njs_value_arg(&retval), + ret); +} + + +static njs_int_t +ngx_response_js_ext_body_used(njs_vm_t *vm, njs_object_prop_t *prop, + njs_value_t *value, njs_value_t *setval, njs_value_t *retval) +{ + ngx_js_http_t *http; + + http = njs_vm_external(vm, value); + if (http == NULL) { + njs_value_undefined_set(retval); + return NJS_DECLINED; + } + + njs_value_boolean_set(retval, http->body_used); + + return NJS_OK; +} + + +static njs_int_t +ngx_response_js_ext_ok(njs_vm_t *vm, njs_object_prop_t *prop, + njs_value_t *value, njs_value_t *setval, njs_value_t *retval) +{ + ngx_uint_t code; + ngx_js_http_t *http; + + http = njs_vm_external(vm, value); + if (http == NULL) { + njs_value_undefined_set(retval); + return NJS_DECLINED; + } + + code = http->http_parse.code; + + njs_value_boolean_set(retval, code >= 200 && code < 300); + + return NJS_OK; +} + + +static njs_int_t +ngx_response_js_ext_status(njs_vm_t *vm, njs_object_prop_t *prop, + njs_value_t *value, njs_value_t *setval, njs_value_t *retval) +{ + ngx_js_http_t *http; + + http = njs_vm_external(vm, value); + if (http == NULL) { + njs_value_undefined_set(retval); + return NJS_DECLINED; + } + + njs_value_number_set(retval, http->http_parse.code); + + return NJS_OK; +} + + +static njs_int_t +ngx_response_js_ext_status_text(njs_vm_t *vm, njs_object_prop_t *prop, + njs_value_t *value, njs_value_t *setval, njs_value_t *retval) +{ + ngx_js_http_t *http; + + http = njs_vm_external(vm, value); + if (http == NULL) { + njs_value_undefined_set(retval); + return NJS_DECLINED; + } + + njs_vm_value_string_set(vm, retval, http->http_parse.status_text, + http->http_parse.status_text_end + - http->http_parse.status_text); + + return NJS_OK; +} + + +static njs_int_t +ngx_response_js_ext_type(njs_vm_t *vm, njs_object_prop_t *prop, + njs_value_t *value, njs_value_t *setval, njs_value_t *retval) +{ + ngx_js_http_t *http; + + http = njs_vm_external(vm, value); + if (http == NULL) { + njs_value_undefined_set(retval); + return NJS_DECLINED; + } + + return njs_vm_value_string_set(vm, retval, (u_char *) "basic", + njs_length("basic")); +} + + +ngx_int_t +ngx_js_fetch_init(njs_vm_t *vm, ngx_log_t *log) +{ + njs_int_t proto_id; + + proto_id = njs_vm_external_prototype(vm, ngx_js_ext_http_response, + njs_nitems(ngx_js_ext_http_response)); + if (proto_id != NGX_JS_PROTO_RESPONSE) { + ngx_log_error(NGX_LOG_EMERG, log, 0, + "failed to add js http.response proto"); + return NGX_ERROR; + } + + return NGX_OK; +} diff --git a/nginx/ngx_js_fetch.h b/nginx/ngx_js_fetch.h new file mode 100644 index 000000000..7f107c41a --- /dev/null +++ b/nginx/ngx_js_fetch.h @@ -0,0 +1,18 @@ + +/* + * Copyright (C) Dmitry Volyntsev + * Copyright (C) NGINX, Inc. + */ + + +#ifndef _NGX_JS_FETCH_H_INCLUDED_ +#define _NGX_JS_FETCH_H_INCLUDED_ + + +njs_int_t ngx_js_ext_fetch(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, + njs_index_t level); + +ngx_int_t ngx_js_fetch_init(njs_vm_t *vm, ngx_log_t *log); + + +#endif /* _NGX_JS_FETCH_H_INCLUDED_ */ diff --git a/nginx/ngx_stream_js_module.c b/nginx/ngx_stream_js_module.c index 91852ba4d..b4e338818 100644 --- a/nginx/ngx_stream_js_module.c +++ b/nginx/ngx_stream_js_module.c @@ -111,6 +111,11 @@ static njs_host_event_t ngx_stream_js_set_timer(njs_external_ptr_t external, static void ngx_stream_js_clear_timer(njs_external_ptr_t external, njs_host_event_t event); static void ngx_stream_js_timer_handler(ngx_event_t *ev); +static ngx_pool_t *ngx_stream_js_pool(njs_vm_t *vm, ngx_stream_session_t *s); +static ngx_resolver_t *ngx_stream_js_resolver(njs_vm_t *vm, + ngx_stream_session_t *s); +static ngx_msec_t ngx_stream_js_resolver_timeout(njs_vm_t *vm, + ngx_stream_session_t *s); static void ngx_stream_js_handle_event(ngx_stream_session_t *s, njs_vm_event_t vm_event, njs_value_t *args, njs_uint_t nargs); @@ -379,11 +384,15 @@ static njs_vm_ops_t ngx_stream_js_ops = { static uintptr_t ngx_stream_js_uptr[] = { offsetof(ngx_stream_session_t, connection), + (uintptr_t) ngx_stream_js_pool, + (uintptr_t) ngx_stream_js_resolver, + (uintptr_t) ngx_stream_js_resolver_timeout, + (uintptr_t) ngx_stream_js_handle_event, }; static njs_vm_meta_t ngx_stream_js_metas = { - .size = 1, + .size = 5, .values = ngx_stream_js_uptr }; @@ -1266,6 +1275,35 @@ ngx_stream_js_timer_handler(ngx_event_t *ev) } +static ngx_pool_t * +ngx_stream_js_pool(njs_vm_t *vm, ngx_stream_session_t *s) +{ + return s->connection->pool; +} + + +static ngx_resolver_t * +ngx_stream_js_resolver(njs_vm_t *vm, ngx_stream_session_t *s) +{ + ngx_stream_core_srv_conf_t *cscf; + + cscf = ngx_stream_get_module_srv_conf(s, ngx_stream_core_module); + + return cscf->resolver; +} + + +static ngx_msec_t +ngx_stream_js_resolver_timeout(njs_vm_t *vm, ngx_stream_session_t *s) +{ + ngx_stream_core_srv_conf_t *cscf; + + cscf = ngx_stream_get_module_srv_conf(s, ngx_stream_core_module); + + return cscf->resolver_timeout; +} + + static void ngx_stream_js_handle_event(ngx_stream_session_t *s, njs_vm_event_t vm_event, njs_value_t *args, njs_uint_t nargs)