--- /dev/null
+
+/*
+ * Copyright (C) Dmitry Volyntsev
+ * Copyright (C) hongzhidao
+ * Copyright (C) NGINX, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include <ngx_event.h>
+#include <ngx_event_connect.h>
+#include "ngx_js.h"
+
+
+typedef struct ngx_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;
+}