blob: a01b5895d6fde508b8986afb132ed49f935dd0b5 [file] [log] [blame]
/*
* Copyright (C) Dmitry Volyntsev
* Copyright (C) hongzhidao
* Copyright (C) Antoine Bonavita
* Copyright (C) NGINX, Inc.
*/
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_event.h>
#include <ngx_event_connect.h>
#include "ngx_js.h"
typedef struct ngx_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;
#if (NGX_SSL)
ngx_str_t tls_name;
ngx_ssl_t *ssl;
njs_bool_t ssl_verify;
#endif
ngx_buf_t *buffer;
ngx_buf_t *chunk;
njs_chb_t chain;
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 njs_js_http_destructor(njs_external_ptr_t external,
njs_host_event_t host);
static void ngx_js_resolve_handler(ngx_resolver_ctx_t *ctx);
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 void ngx_js_http_connect(ngx_js_http_t *http);
static void ngx_js_http_next(ngx_js_http_t *http);
static void ngx_js_http_write_handler(ngx_event_t *wev);
static void ngx_js_http_read_handler(ngx_event_t *rev);
static 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);
#if (NGX_SSL)
static void ngx_js_http_ssl_init_connection(ngx_js_http_t *http);
static void ngx_js_http_ssl_handshake_handler(ngx_connection_t *c);
static void ngx_js_http_ssl_handshake(ngx_js_http_t *http);
static njs_int_t ngx_js_http_ssl_name(ngx_js_http_t *http);
#endif
static 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),
}
},
};
static njs_int_t ngx_http_js_fetch_proto_id;
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");
#if (NGX_SSL)
static const njs_str_t verify_key = njs_str("verify");
#endif
external = njs_vm_external(vm, NJS_PROTO_ID_ANY, 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->timeout = ngx_external_fetch_timeout(vm, external);
http->event_handler = ngx_external_event_handler(vm, external);
http->buffer_size = ngx_external_buffer_size(vm, external);
http->max_response_body_size =
ngx_external_max_response_buffer_size(vm, external);
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;
#if (NGX_SSL)
} else if (u.url.len > 8
&& ngx_strncasecmp(u.url.data, (u_char *) "https://", 8) == 0)
{
u.url.len -= 8;
u.url.data += 8;
u.default_port = 443;
http->ssl = ngx_external_ssl(vm, external);
http->ssl_verify = ngx_external_ssl_verify(vm, external);
#endif
} 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;
}
#if (NGX_SSL)
value = njs_vm_object_prop(vm, init, &verify_key, &lvalue);
if (value != NULL) {
http->ssl_verify = njs_value_bool(value);
}
#endif
}
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 (value == NULL) {
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 (NGX_SSL)
http->tls_name.data = u.host.data;
http->tls_name.len = u.host.len;
#endif
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;
}
njs_vm_retval_set(vm, njs_value_arg(&http->promise));
return NJS_OK;
}
http->naddrs = 1;
ngx_memcpy(&http->addr, &u.addrs[0], sizeof(ngx_addr_t));
http->addrs = &http->addr;
ngx_js_http_connect(http);
njs_vm_retval_set(vm, njs_value_arg(&http->promise));
return NJS_OK;
fail:
ngx_js_http_fetch_done(http, (njs_opaque_value_t *) njs_vm_retval(vm),
NJS_ERROR);
njs_vm_retval_set(vm, njs_value_arg(&http->promise));
return NJS_OK;
}
static ngx_js_http_t *
ngx_js_http_alloc(njs_vm_t *vm, ngx_pool_t *pool, ngx_log_t *log)
{
njs_int_t ret;
ngx_js_http_t *http;
njs_vm_event_t vm_event;
njs_function_t *callback;
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;
ret = njs_vm_promise_create(vm, njs_value_arg(&http->promise),
njs_value_arg(&http->promise_callbacks));
if (ret != NJS_OK) {
goto failed;
}
callback = njs_vm_function_alloc(vm, ngx_js_http_promise_trampoline);
if (callback == NULL) {
goto failed;
}
vm_event = njs_vm_add_event(vm, callback, 1, http, njs_js_http_destructor);
if (vm_event == NULL) {
goto failed;
}
http->vm_event = vm_event;
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;
ngx_js_http_connect(http);
return;
failed:
ngx_js_http_error(http, 0, "memory error");
}
static void
ngx_js_http_close_connection(ngx_connection_t *c)
{
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
"close js http connection: %d", c->fd);
#if (NGX_SSL)
if (c->ssl) {
c->ssl->no_wait_shutdown = 1;
if (ngx_ssl_shutdown(c) == NGX_AGAIN) {
c->ssl->handler = ngx_js_http_close_connection;
return;
}
}
#endif
c->destroyed = 1;
ngx_close_connection(c);
}
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_js_http_close_connection(http->peer.connection);
http->peer.connection = NULL;
}
}
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_js_http_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 void
ngx_js_http_connect(ngx_js_http_t *http)
{
ngx_int_t rc;
ngx_addr_t *addr;
addr = &http->addrs[http->naddr];
ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0,
"js http connect %ui/%ui", http->naddr, http->naddrs);
http->peer.sockaddr = addr->sockaddr;
http->peer.socklen = addr->socklen;
http->peer.name = &addr->name;
http->peer.get = ngx_event_get_peer;
http->peer.log = http->log;
http->peer.log_error = NGX_ERROR_ERR;
rc = ngx_event_connect_peer(&http->peer);
if (rc == NGX_ERROR) {
ngx_js_http_error(http, 0, "connect failed");
return;
}
if (rc == NGX_BUSY || rc == NGX_DECLINED) {
ngx_js_http_next(http);
return;
}
http->peer.connection->data = http;
http->peer.connection->pool = http->pool;
http->peer.connection->write->handler = ngx_js_http_write_handler;
http->peer.connection->read->handler = ngx_js_http_read_handler;
http->process = ngx_js_http_process_status_line;
if (http->timeout) {
ngx_add_timer(http->peer.connection->read, http->timeout);
ngx_add_timer(http->peer.connection->write, http->timeout);
}
#if (NGX_SSL)
if (http->ssl != NULL && http->peer.connection->ssl == NULL) {
ngx_js_http_ssl_init_connection(http);
return;
}
#endif
if (rc == NGX_OK) {
ngx_js_http_write_handler(http->peer.connection->write);
}
}
#if (NGX_SSL)
static void
ngx_js_http_ssl_init_connection(ngx_js_http_t *http)
{
ngx_int_t rc;
ngx_connection_t *c;
c = http->peer.connection;
ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0,
"js http secure connect %ui/%ui", http->naddr, http->naddrs);
if (ngx_ssl_create_connection(http->ssl, c, NGX_SSL_BUFFER|NGX_SSL_CLIENT)
!= NGX_OK)
{
ngx_js_http_error(http, 0, "failed to create ssl connection");
return;
}
c->sendfile = 0;
if (ngx_js_http_ssl_name(http) != NGX_OK) {
ngx_js_http_error(http, 0, "failed to create ssl connection");
return;
}
c->log->action = "SSL handshaking to fetch target";
rc = ngx_ssl_handshake(c);
if (rc == NGX_AGAIN) {
c->data = http;
c->ssl->handler = ngx_js_http_ssl_handshake_handler;
return;
}
ngx_js_http_ssl_handshake(http);
}
static void
ngx_js_http_ssl_handshake_handler(ngx_connection_t *c)
{
ngx_js_http_t *http;
http = c->data;
http->peer.connection->write->handler = ngx_js_http_write_handler;
http->peer.connection->read->handler = ngx_js_http_read_handler;
ngx_js_http_ssl_handshake(http);
}
static void
ngx_js_http_ssl_handshake(ngx_js_http_t *http)
{
long rc;
ngx_connection_t *c;
c = http->peer.connection;
if (c->ssl->handshaked) {
if (http->ssl_verify) {
rc = SSL_get_verify_result(c->ssl->connection);
if (rc != X509_V_OK) {
ngx_log_error(NGX_LOG_ERR, c->log, 0,
"js http fetch SSL certificate verify "
"error: (%l:%s)", rc,
X509_verify_cert_error_string(rc));
goto failed;
}
if (ngx_ssl_check_host(c, &http->tls_name) != NGX_OK) {
ngx_log_error(NGX_LOG_ERR, c->log, 0,
"js http SSL certificate does not match \"%V\"",
&http->tls_name);
goto failed;
}
}
c->write->handler = ngx_js_http_write_handler;
c->read->handler = ngx_js_http_read_handler;
if (c->read->ready) {
ngx_post_event(c->read, &ngx_posted_events);
}
http->process = ngx_js_http_process_status_line;
ngx_js_http_write_handler(c->write);
return;
}
failed:
ngx_js_http_next(http);
}
static njs_int_t
ngx_js_http_ssl_name(ngx_js_http_t *http)
{
#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME
u_char *p;
/* as per RFC 6066, literal IPv4 and IPv6 addresses are not permitted */
ngx_str_t *name = &http->tls_name;
if (name->len == 0 || *name->data == '[') {
goto done;
}
if (ngx_inet_addr(name->data, name->len) != INADDR_NONE) {
goto done;
}
/*
* SSL_set_tlsext_host_name() needs a null-terminated string,
* hence we explicitly null-terminate name here
*/
p = ngx_pnalloc(http->pool, name->len + 1);
if (p == NULL) {
return NGX_ERROR;
}
(void) ngx_cpystrn(p, name->data, name->len + 1);
name->data = p;
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, http->log, 0,
"js http SSL server name: \"%s\"", name->data);
if (SSL_set_tlsext_host_name(http->peer.connection->ssl->connection,
(char *) name->data)
== 0)
{
ngx_ssl_error(NGX_LOG_ERR, http->log, 0,
"SSL_set_tlsext_host_name(\"%s\") failed", name->data);
return NGX_ERROR;
}
#endif
done:
return NJS_OK;
}
#endif
static void
ngx_js_http_next(ngx_js_http_t *http)
{
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, http->log, 0, "js http next");
if (++http->naddr >= http->naddrs) {
ngx_js_http_error(http, 0, "connect failed");
return;
}
if (http->peer.connection != NULL) {
ngx_js_http_close_connection(http->peer.connection);
http->peer.connection = NULL;
}
http->buffer = NULL;
ngx_js_http_connect(http);
}
static void
ngx_js_http_write_handler(ngx_event_t *wev)
{
ssize_t n, size;
ngx_buf_t *b;
ngx_js_http_t *http;
ngx_connection_t *c;
c = wev->data;
http = c->data;
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, wev->log, 0, "js http write handler");
if (wev->timedout) {
ngx_js_http_error(http, NGX_ETIMEDOUT, "write timed out");
return;
}
#if (NGX_SSL)
if (http->ssl != NULL && http->peer.connection->ssl == NULL) {
ngx_js_http_ssl_init_connection(http);
return;
}
#endif
b = http->buffer;
if (b == NULL) {
size = njs_chb_size(&http->chain);
if (size < 0) {
ngx_js_http_error(http, 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 = c->send(c, b->pos, size);
if (n == NGX_ERROR) {
ngx_js_http_next(http);
return;
}
if (n > 0) {
b->pos += n;
if (n == size) {
wev->handler = ngx_js_http_dummy_handler;
http->buffer = NULL;
if (wev->timer_set) {
ngx_del_timer(wev);
}
if (ngx_handle_write_event(wev, 0) != NGX_OK) {
ngx_js_http_error(http, 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 = c->recv(c, b->last, size);
if (n > 0) {
b->last += n;
rc = http->process(http);
if (rc == NGX_ERROR) {
return;
}
continue;
}
if (n == NGX_AGAIN) {
if (ngx_handle_read_event(rev, 0) != NGX_OK) {
ngx_js_http_error(http, 0, "read failed");
}
return;
}
if (n == NGX_ERROR) {
ngx_js_http_next(http);
return;
}
break;
}
http->done = 1;
rc = http->process(http);
if (rc == NGX_DONE) {
/* handler was called */
return;
}
if (rc == NGX_AGAIN) {
ngx_js_http_error(http, 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_http_js_fetch_proto_id, 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, ngx_http_js_fetch_proto_id, 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, retval, 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, ngx_http_js_fetch_proto_id, 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;
for (i = 0; i < http->headers.nelts; i++) {
h = &headers[i];
start = njs_vm_array_start(vm, keys);
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;
}
}
if (k == length) {
value = njs_vm_array_push(vm, keys);
if (value == NULL) {
return NJS_ERROR;
}
rc = njs_vm_value_string_set(vm, value, h->key.data, h->key.len);
if (rc != NJS_OK) {
return NJS_ERROR;
}
length++;
}
}
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, ngx_http_js_fetch_proto_id,
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, ngx_http_js_fetch_proto_id, 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, ngx_http_js_fetch_proto_id, 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, ngx_http_js_fetch_proto_id, 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, ngx_http_js_fetch_proto_id, 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, ngx_http_js_fetch_proto_id, 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)
{
ngx_http_js_fetch_proto_id = njs_vm_external_prototype(vm,
ngx_js_ext_http_response,
njs_nitems(ngx_js_ext_http_response));
if (ngx_http_js_fetch_proto_id < 0) {
ngx_log_error(NGX_LOG_EMERG, log, 0,
"failed to add js http.response proto");
return NGX_ERROR;
}
return NGX_OK;
}