diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c index 286a0a780..f9d9721aa 100644 --- a/nginx/ngx_http_js_module.c +++ b/nginx/ngx_http_js_module.c @@ -358,10 +358,8 @@ static void ngx_http_qjs_periodic_finalizer(JSRuntime *rt, JSValue val); static ngx_pool_t *ngx_http_js_pool(ngx_http_request_t *r); static ngx_resolver_t *ngx_http_js_resolver(ngx_http_request_t *r); static ngx_msec_t ngx_http_js_resolver_timeout(ngx_http_request_t *r); -static ngx_msec_t ngx_http_js_fetch_timeout(ngx_http_request_t *r); -static size_t ngx_http_js_buffer_size(ngx_http_request_t *r); -static size_t ngx_http_js_max_response_buffer_size(ngx_http_request_t *r); static void ngx_http_js_event_finalize(ngx_http_request_t *r, ngx_int_t rc); +static ngx_js_loc_conf_t *ngx_http_js_loc_conf(ngx_http_request_t *r); static ngx_js_ctx_t *ngx_http_js_ctx(ngx_http_request_t *r); static void ngx_http_js_periodic_handler(ngx_event_t *ev); @@ -391,9 +389,6 @@ static void *ngx_http_js_create_loc_conf(ngx_conf_t *cf); static char *ngx_http_js_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child); -static ngx_ssl_t *ngx_http_js_ssl(ngx_http_request_t *r); -static ngx_flag_t ngx_http_js_ssl_verify(ngx_http_request_t *r); - static ngx_int_t ngx_http_js_parse_unsafe_uri(ngx_http_request_t *r, njs_str_t *uri, njs_str_t *args); @@ -570,6 +565,34 @@ static ngx_command_t ngx_http_js_commands[] = { 0, NULL }, + { ngx_string("js_fetch_keepalive"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_num_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_js_loc_conf_t, fetch_keepalive), + NULL }, + + { ngx_string("js_fetch_keepalive_requests"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_num_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_js_loc_conf_t, fetch_keepalive_requests), + NULL }, + + { ngx_string("js_fetch_keepalive_time"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_msec_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_js_loc_conf_t, fetch_keepalive_time), + NULL }, + + { ngx_string("js_fetch_keepalive_timeout"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_msec_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_js_loc_conf_t, fetch_keepalive_timeout), + NULL }, + ngx_null_command }; @@ -972,13 +995,9 @@ static uintptr_t ngx_http_js_uptr[] = { (uintptr_t) ngx_http_js_resolver, (uintptr_t) ngx_http_js_resolver_timeout, (uintptr_t) ngx_http_js_event_finalize, - (uintptr_t) ngx_http_js_ssl, - (uintptr_t) ngx_http_js_ssl_verify, - (uintptr_t) ngx_http_js_fetch_timeout, - (uintptr_t) ngx_http_js_buffer_size, - (uintptr_t) ngx_http_js_max_response_buffer_size, - (uintptr_t) 0 /* main_conf ptr */, + (uintptr_t) ngx_http_js_loc_conf, (uintptr_t) ngx_http_js_ctx, + (uintptr_t) 0 /* main_conf ptr */, }; @@ -4666,39 +4685,6 @@ ngx_http_js_resolver_timeout(ngx_http_request_t *r) } -static ngx_msec_t -ngx_http_js_fetch_timeout(ngx_http_request_t *r) -{ - ngx_http_js_loc_conf_t *jlcf; - - jlcf = ngx_http_get_module_loc_conf(r, ngx_http_js_module); - - return jlcf->timeout; -} - - -static size_t -ngx_http_js_buffer_size(ngx_http_request_t *r) -{ - ngx_http_js_loc_conf_t *jlcf; - - jlcf = ngx_http_get_module_loc_conf(r, ngx_http_js_module); - - return jlcf->buffer_size; -} - - -static size_t -ngx_http_js_max_response_buffer_size(ngx_http_request_t *r) -{ - ngx_http_js_loc_conf_t *jlcf; - - jlcf = ngx_http_get_module_loc_conf(r, ngx_http_js_module); - - return jlcf->max_response_body_size; -} - - static void ngx_http_js_event_finalize(ngx_http_request_t *r, ngx_int_t rc) { @@ -4721,6 +4707,13 @@ ngx_http_js_event_finalize(ngx_http_request_t *r, ngx_int_t rc) } +static ngx_js_loc_conf_t * +ngx_http_js_loc_conf(ngx_http_request_t *r) +{ + return ngx_http_get_module_loc_conf(r, ngx_http_js_module); +} + + static ngx_js_ctx_t * ngx_http_js_ctx(ngx_http_request_t *r) { @@ -7724,7 +7717,7 @@ ngx_http_js_init_conf_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf) options.engine = conf->type; jmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_js_module); - ngx_http_js_uptr[NGX_JS_MAIN_CONF_INDEX] = (uintptr_t) jmcf; + ngx_http_js_uptr[NGX_JS_EXTERNAL_MAIN_CONF] = (uintptr_t) jmcf; if (conf->type == NGX_ENGINE_NJS) { options.u.njs.metas = &ngx_http_js_metas; @@ -8236,36 +8229,6 @@ ngx_http_js_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) } -static ngx_ssl_t * -ngx_http_js_ssl(ngx_http_request_t *r) -{ -#if (NGX_HTTP_SSL) - ngx_http_js_loc_conf_t *jlcf; - - jlcf = ngx_http_get_module_loc_conf(r, ngx_http_js_module); - - return jlcf->ssl; -#else - return NULL; -#endif -} - - -static ngx_flag_t -ngx_http_js_ssl_verify(ngx_http_request_t *r) -{ -#if (NGX_HTTP_SSL) - ngx_http_js_loc_conf_t *jlcf; - - jlcf = ngx_http_get_module_loc_conf(r, ngx_http_js_module); - - return jlcf->ssl_verify; -#else - return 0; -#endif -} - - static ngx_int_t ngx_http_js_parse_unsafe_uri(ngx_http_request_t *r, njs_str_t *uri, njs_str_t *args) diff --git a/nginx/ngx_js.c b/nginx/ngx_js.c index c385e16ec..efef34674 100644 --- a/nginx/ngx_js.c +++ b/nginx/ngx_js.c @@ -10,6 +10,7 @@ #include #include #include "ngx_js.h" +#include "ngx_js_http.h" typedef struct { @@ -3986,6 +3987,11 @@ ngx_js_create_conf(ngx_conf_t *cf, size_t size) conf->max_response_body_size = NGX_CONF_UNSET_SIZE; conf->timeout = NGX_CONF_UNSET_MSEC; + conf->fetch_keepalive = NGX_CONF_UNSET_UINT; + conf->fetch_keepalive_requests = NGX_CONF_UNSET_UINT; + conf->fetch_keepalive_time = NGX_CONF_UNSET_MSEC; + conf->fetch_keepalive_timeout = NGX_CONF_UNSET_MSEC; + return conf; } @@ -4097,6 +4103,17 @@ ngx_js_merge_conf(ngx_conf_t *cf, void *parent, void *child, ngx_conf_merge_size_value(conf->max_response_body_size, prev->max_response_body_size, 1048576); + ngx_conf_merge_uint_value(conf->fetch_keepalive, prev->fetch_keepalive, 0); + ngx_conf_merge_uint_value(conf->fetch_keepalive_requests, + prev->fetch_keepalive_requests, 1000); + ngx_conf_merge_msec_value(conf->fetch_keepalive_time, + prev->fetch_keepalive_time, 3600000); + ngx_conf_merge_msec_value(conf->fetch_keepalive_timeout, + prev->fetch_keepalive_timeout, 60000); + + ngx_queue_init(&conf->fetch_keepalive_cache); + ngx_queue_init(&conf->fetch_keepalive_free); + if (ngx_js_merge_vm(cf, (ngx_js_loc_conf_t *) conf, (ngx_js_loc_conf_t *) prev, init_vm) diff --git a/nginx/ngx_js.h b/nginx/ngx_js.h index 032bd4151..af19e0076 100644 --- a/nginx/ngx_js.h +++ b/nginx/ngx_js.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -80,9 +81,7 @@ typedef ngx_pool_t *(*ngx_external_pool_pt)(njs_external_ptr_t e); typedef void (*ngx_js_event_finalize_pt)(njs_external_ptr_t e, ngx_int_t rc); typedef ngx_resolver_t *(*ngx_external_resolver_pt)(njs_external_ptr_t e); typedef ngx_msec_t (*ngx_external_timeout_pt)(njs_external_ptr_t e); -typedef ngx_flag_t (*ngx_external_flag_pt)(njs_external_ptr_t e); -typedef ngx_flag_t (*ngx_external_size_pt)(njs_external_ptr_t e); -typedef ngx_ssl_t *(*ngx_external_ssl_pt)(njs_external_ptr_t e); +typedef ngx_js_loc_conf_t *(*ngx_js_external_loc_conf_pt)(njs_external_ptr_t e); typedef ngx_js_ctx_t *(*ngx_js_external_ctx_pt)(njs_external_ptr_t e); @@ -135,7 +134,14 @@ typedef struct { \ size_t buffer_size; \ size_t max_response_body_size; \ - ngx_msec_t timeout + ngx_msec_t timeout; \ + \ + ngx_uint_t fetch_keepalive; \ + ngx_uint_t fetch_keepalive_requests; \ + ngx_msec_t fetch_keepalive_time; \ + ngx_msec_t fetch_keepalive_timeout; \ + ngx_queue_t fetch_keepalive_cache; \ + ngx_queue_t fetch_keepalive_free #if defined(NGX_HTTP_SSL) || defined(NGX_STREAM_SSL) @@ -272,31 +278,35 @@ struct ngx_engine_s { }; +enum { + NGX_JS_EXTERNAL_CONNECTION = 0, + NGX_JS_EXTERNAL_POOL, + NGX_JS_EXTERNAL_RESOLVER, + NGX_JS_EXTERNAL_RESOLVER_TIMEOUT, + NGX_JS_EXTERNAL_EVENT_FINALIZE, + NGX_JS_EXTERNAL_LOC_CONF, + NGX_JS_EXTERNAL_CTX, + NGX_JS_EXTERNAL_MAIN_CONF, +}; + #define ngx_external_connection(vm, e) \ - (*((ngx_connection_t **) ((u_char *) (e) + njs_vm_meta(vm, 0)))) + (*((ngx_connection_t **) \ + ((u_char *) (e) + njs_vm_meta(vm, NGX_JS_EXTERNAL_CONNECTION)))) #define ngx_external_pool(vm, e) \ - ((ngx_external_pool_pt) njs_vm_meta(vm, 1))(e) + ((ngx_external_pool_pt) njs_vm_meta(vm, NGX_JS_EXTERNAL_POOL))(e) #define ngx_external_resolver(vm, e) \ - ((ngx_external_resolver_pt) njs_vm_meta(vm, 2))(e) + ((ngx_external_resolver_pt) njs_vm_meta(vm, NGX_JS_EXTERNAL_RESOLVER))(e) #define ngx_external_resolver_timeout(vm, e) \ - ((ngx_external_timeout_pt) njs_vm_meta(vm, 3))(e) + ((ngx_external_timeout_pt) \ + njs_vm_meta(vm, NGX_JS_EXTERNAL_RESOLVER_TIMEOUT))(e) #define ngx_external_event_finalize(vm) \ - ((ngx_js_event_finalize_pt) njs_vm_meta(vm, 4)) -#define ngx_external_ssl(vm, e) \ - ((ngx_external_ssl_pt) njs_vm_meta(vm, 5))(e) -#define ngx_external_ssl_verify(vm, e) \ - ((ngx_external_flag_pt) njs_vm_meta(vm, 6))(e) -#define ngx_external_fetch_timeout(vm, e) \ - ((ngx_external_timeout_pt) njs_vm_meta(vm, 7))(e) -#define ngx_external_buffer_size(vm, e) \ - ((ngx_external_size_pt) njs_vm_meta(vm, 8))(e) -#define ngx_external_max_response_buffer_size(vm, e) \ - ((ngx_external_size_pt) njs_vm_meta(vm, 9))(e) -#define NGX_JS_MAIN_CONF_INDEX 10 -#define ngx_main_conf(vm) \ - ((ngx_js_main_conf_t *) njs_vm_meta(vm, NGX_JS_MAIN_CONF_INDEX)) + ((ngx_js_event_finalize_pt) njs_vm_meta(vm, NGX_JS_EXTERNAL_EVENT_FINALIZE)) +#define ngx_external_loc_conf(vm, e) \ + ((ngx_js_external_loc_conf_pt) njs_vm_meta(vm, NGX_JS_EXTERNAL_LOC_CONF))(e) #define ngx_external_ctx(vm, e) \ - ((ngx_js_external_ctx_pt) njs_vm_meta(vm, 11))(e) + ((ngx_js_external_ctx_pt) njs_vm_meta(vm, NGX_JS_EXTERNAL_CTX))(e) +#define ngx_main_conf(vm) \ + ((ngx_js_main_conf_t *) njs_vm_meta(vm, NGX_JS_EXTERNAL_MAIN_CONF)) #define ngx_js_prop(vm, type, value, start, len) \ @@ -367,29 +377,33 @@ JSValue ngx_qjs_ext_fetch(JSContext *cx, JSValueConst this_val, int argc, #define ngx_qjs_meta(cx, i) \ ((uintptr_t *) JS_GetRuntimeOpaque(JS_GetRuntime(cx)))[i] #define ngx_qjs_external_connection(cx, e) \ - (*((ngx_connection_t **) ((u_char *) (e) + ngx_qjs_meta(cx, 0)))) + (*((ngx_connection_t **) \ + ((u_char *) (e) + ngx_qjs_meta(cx, NGX_JS_EXTERNAL_CONNECTION)))) #define ngx_qjs_external_pool(cx, e) \ - ((ngx_external_pool_pt) ngx_qjs_meta(cx, 1))(e) + ((ngx_external_pool_pt) ngx_qjs_meta(cx, NGX_JS_EXTERNAL_POOL))(e) #define ngx_qjs_external_resolver(cx, e) \ - ((ngx_external_resolver_pt) ngx_qjs_meta(cx, 2))(e) + ((ngx_external_resolver_pt) ngx_qjs_meta(cx, NGX_JS_EXTERNAL_RESOLVER))(e) #define ngx_qjs_external_resolver_timeout(cx, e) \ - ((ngx_external_timeout_pt) ngx_qjs_meta(cx, 3))(e) + ((ngx_external_timeout_pt) \ + ngx_qjs_meta(cx, NGX_JS_EXTERNAL_RESOLVER_TIMEOUT))(e) #define ngx_qjs_external_event_finalize(cx) \ - ((ngx_js_event_finalize_pt) ngx_qjs_meta(cx, 4)) + ((ngx_js_event_finalize_pt) ngx_qjs_meta(cx, NGX_JS_EXTERNAL_EVENT_FINALIZE)) +#define ngx_qjs_external_loc_conf(cx, e) \ + ((ngx_js_external_loc_conf_pt) ngx_qjs_meta(cx, NGX_JS_EXTERNAL_LOC_CONF))(e) #define ngx_qjs_external_ssl(cx, e) \ - ((ngx_external_ssl_pt) ngx_qjs_meta(cx, 5))(e) + (ngx_qjs_external_loc_conf(cx, e)->ssl) #define ngx_qjs_external_ssl_verify(cx, e) \ - ((ngx_external_flag_pt) ngx_qjs_meta(cx, 6))(e) + (ngx_qjs_external_loc_conf(cx, e)->ssl_verify) #define ngx_qjs_external_fetch_timeout(cx, e) \ - ((ngx_external_timeout_pt) ngx_qjs_meta(cx, 7))(e) + (ngx_qjs_external_loc_conf(cx, e)->timeout) #define ngx_qjs_external_buffer_size(cx, e) \ - ((ngx_external_size_pt) ngx_qjs_meta(cx, 8))(e) + (ngx_qjs_external_loc_conf(cx, e)->buffer_size) #define ngx_qjs_external_max_response_buffer_size(cx, e) \ - ((ngx_external_size_pt) ngx_qjs_meta(cx, 9))(e) -#define ngx_qjs_main_conf(cx) \ - ((ngx_js_main_conf_t *) ngx_qjs_meta(cx, NGX_JS_MAIN_CONF_INDEX)) + (ngx_qjs_external_loc_conf(cx, e)->max_response_body_size) #define ngx_qjs_external_ctx(cx, e) \ - ((ngx_js_external_ctx_pt) ngx_qjs_meta(cx, 11))(e) + ((ngx_js_external_ctx_pt) ngx_qjs_meta(cx, NGX_JS_EXTERNAL_CTX))(e) +#define ngx_qjs_main_conf(cx) \ + ((ngx_js_main_conf_t *) ngx_qjs_meta(cx, NGX_JS_EXTERNAL_MAIN_CONF)) extern qjs_module_t qjs_webcrypto_module; extern qjs_module_t qjs_xml_module; diff --git a/nginx/ngx_js_fetch.c b/nginx/ngx_js_fetch.c index faa38aab1..ac1c1a27b 100644 --- a/nginx/ngx_js_fetch.c +++ b/nginx/ngx_js_fetch.c @@ -7,10 +7,6 @@ */ -#include -#include -#include -#include #include "ngx_js.h" #include "ngx_js_http.h" @@ -40,7 +36,7 @@ static njs_int_t ngx_js_headers_inherit(njs_vm_t *vm, ngx_js_headers_t *headers, static njs_int_t ngx_js_headers_fill(njs_vm_t *vm, ngx_js_headers_t *headers, njs_value_t *init); static ngx_js_fetch_t *ngx_js_fetch_alloc(njs_vm_t *vm, ngx_pool_t *pool, - ngx_log_t *log); + ngx_log_t *log, ngx_js_loc_conf_t *conf); static void ngx_js_fetch_error(ngx_js_http_t *http, const char *err); static void ngx_js_fetch_destructor(ngx_js_event_t *event); static njs_int_t ngx_js_fetch_promissified_result(njs_vm_t *vm, @@ -537,7 +533,8 @@ ngx_js_ext_fetch(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, c = ngx_external_connection(vm, external); pool = ngx_external_pool(vm, external); - fetch = ngx_js_fetch_alloc(vm, pool, c->log); + fetch = ngx_js_fetch_alloc(vm, pool, c->log, + ngx_external_loc_conf(vm, external)); if (fetch == NULL) { return NJS_ERROR; } @@ -549,16 +546,21 @@ ngx_js_ext_fetch(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, goto fail; } + if (u.host.len >= NGX_JS_HOST_MAX_LEN) { + njs_vm_error(vm, "Host name too long"); + goto fail; + } + + http->host = u.host; + http->port = u.port; http->response.url = request.url; - http->timeout = ngx_external_fetch_timeout(vm, external); - http->buffer_size = ngx_external_buffer_size(vm, external); - http->max_response_body_size = - ngx_external_max_response_buffer_size(vm, external); + http->buffer_size = http->conf->buffer_size; + http->max_response_body_size = http->conf->max_response_body_size; #if (NGX_SSL) if (u.default_port == 443) { - http->ssl = ngx_external_ssl(vm, external); - http->ssl_verify = ngx_external_ssl_verify(vm, external); + http->ssl = http->conf->ssl; + http->ssl_verify = http->conf->ssl_verify; } #endif @@ -682,18 +684,22 @@ ngx_js_ext_fetch(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, continue; } + if (h[i].key.len == 10 + && ngx_strncasecmp(h[i].key.data, (u_char *) "Connection", 10) + == 0) + { + continue; + } + njs_chb_append(&http->chain, h[i].key.data, h[i].key.len); njs_chb_append_literal(&http->chain, ": "); njs_chb_append(&http->chain, h[i].value.data, h[i].value.len); njs_chb_append_literal(&http->chain, CRLF); } - njs_chb_append_literal(&http->chain, "Connection: close" CRLF); - -#if (NGX_SSL) - http->tls_name.data = u.host.data; - http->tls_name.len = u.host.len; -#endif + if (!http->keepalive) { + njs_chb_append_literal(&http->chain, "Connection: close" CRLF); + } if (request.body.len != 0) { njs_chb_sprintf(&http->chain, 32, "Content-Length: %uz" CRLF CRLF, @@ -1134,7 +1140,8 @@ ngx_js_headers_fill(njs_vm_t *vm, ngx_js_headers_t *headers, njs_value_t *init) static ngx_js_fetch_t * -ngx_js_fetch_alloc(njs_vm_t *vm, ngx_pool_t *pool, ngx_log_t *log) +ngx_js_fetch_alloc(njs_vm_t *vm, ngx_pool_t *pool, ngx_log_t *log, + ngx_js_loc_conf_t *conf) { njs_int_t ret; ngx_js_ctx_t *ctx; @@ -1152,10 +1159,10 @@ ngx_js_fetch_alloc(njs_vm_t *vm, ngx_pool_t *pool, ngx_log_t *log) http->pool = pool; http->log = log; + http->conf = conf; - http->timeout = 10000; - - http->http_parse.content_length_n = -1; + http->content_length_n = -1; + http->keepalive = (conf->fetch_keepalive > 0); http->append_headers = ngx_js_fetch_append_headers; http->ready_handler = ngx_js_fetch_process_done; diff --git a/nginx/ngx_js_http.c b/nginx/ngx_js_http.c index 4555a7ac3..f07cccebc 100644 --- a/nginx/ngx_js_http.c +++ b/nginx/ngx_js_http.c @@ -7,19 +7,35 @@ */ -#include -#include -#include -#include #include "ngx_js.h" #include "ngx_js_http.h" +typedef struct { + ngx_js_loc_conf_t *conf; + ngx_queue_t queue; + ngx_connection_t *connection; + + ngx_flag_t ssl; + size_t host_len; + u_char host[NGX_JS_HOST_MAX_LEN]; + in_port_t port; +} ngx_js_http_keepalive_cache_t; + + +#define ngx_js_http_version(major, minor) ((major) * 1000 + (minor)) + + static void ngx_js_http_resolve_handler(ngx_resolver_ctx_t *ctx); static void ngx_js_http_next(ngx_js_http_t *http); static void ngx_js_http_write_handler(ngx_event_t *wev); static void ngx_js_http_read_handler(ngx_event_t *rev); static void ngx_js_http_dummy_handler(ngx_event_t *ev); +static void ngx_js_http_keepalive_close_handler(ngx_event_t *ev); +static void ngx_js_http_keepalive_dummy_handler(ngx_event_t *ev); + +static ngx_int_t ngx_js_http_get_keepalive_connection(ngx_js_http_t *http); +static ngx_int_t ngx_js_http_free_keepalive_connection(ngx_js_http_t *http); 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); @@ -177,12 +193,13 @@ ngx_js_http_resolve_handler(ngx_resolver_ctx_t *ctx) static void ngx_js_http_close_connection(ngx_connection_t *c) { - ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, - "js http close connection: %d", c->fd); + ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0, + "js http close connection: %p:%d", c, c->fd); #if (NGX_SSL) if (c->ssl) { c->ssl->no_wait_shutdown = 1; + c->ssl->no_send_shutdown = 1; if (ngx_ssl_shutdown(c) == NGX_AGAIN) { c->ssl->handler = ngx_js_http_close_connection; @@ -193,6 +210,7 @@ ngx_js_http_close_connection(ngx_connection_t *c) c->destroyed = 1; + ngx_destroy_pool(c->pool); ngx_close_connection(c); } @@ -210,18 +228,32 @@ ngx_js_http_resolve_done(ngx_js_http_t *http) void ngx_js_http_close_peer(ngx_js_http_t *http) { - if (http->peer.connection != NULL) { + if (http->peer.connection == NULL) { + return; + } + + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, http->log, 0, + "js http close peer"); + + if (http->keepalive) { + if (ngx_js_http_free_keepalive_connection(http) != NGX_OK) { + ngx_js_http_close_connection(http->peer.connection); + } + + } else { ngx_js_http_close_connection(http->peer.connection); - http->peer.connection = NULL; } + + http->peer.connection = NULL; } void ngx_js_http_connect(ngx_js_http_t *http) { - ngx_int_t rc; - ngx_addr_t *addr; + ngx_int_t rc; + ngx_addr_t *addr; + ngx_connection_t *c; addr = &http->addrs[http->naddr]; @@ -235,38 +267,51 @@ ngx_js_http_connect(ngx_js_http_t *http) http->peer.log = http->log; http->peer.log_error = NGX_ERROR_ERR; - rc = ngx_event_connect_peer(&http->peer); + rc = ngx_js_http_get_keepalive_connection(http); + if (rc != NGX_OK) { + rc = ngx_event_connect_peer(&http->peer); + if (rc == NGX_ERROR) { + ngx_js_http_error(http, "connect failed"); + return; + } - if (rc == NGX_ERROR) { - ngx_js_http_error(http, "connect failed"); - return; + if (rc == NGX_BUSY || rc == NGX_DECLINED) { + ngx_js_http_next(http); + return; + } } - if (rc == NGX_BUSY || rc == NGX_DECLINED) { - ngx_js_http_next(http); - return; - } + c = http->peer.connection; - http->peer.connection->data = http; - http->peer.connection->pool = http->pool; + c->requests++; + c->data = http; - http->peer.connection->write->handler = ngx_js_http_write_handler; - http->peer.connection->read->handler = ngx_js_http_read_handler; + if (c->pool == NULL) { + /* we need separate pool here to be able to cache SSL connections */ + c->pool = ngx_create_pool(128, http->log); + if (c->pool == NULL) { + ngx_js_http_error(http, "create pool failed"); + return; + } + } + + c->write->handler = ngx_js_http_write_handler; + c->read->handler = ngx_js_http_read_handler; http->process = ngx_js_http_process_status_line; - ngx_add_timer(http->peer.connection->read, http->timeout); - ngx_add_timer(http->peer.connection->write, http->timeout); + ngx_add_timer(c->read, http->conf->timeout); + ngx_add_timer(c->write, http->conf->timeout); #if (NGX_SSL) - if (http->ssl != NULL && http->peer.connection->ssl == NULL) { + if (http->ssl != NULL && c->ssl == NULL) { ngx_js_http_ssl_init_connection(http); return; } #endif if (rc == NGX_OK) { - ngx_js_http_write_handler(http->peer.connection->write); + ngx_js_http_write_handler(c->write); } } @@ -346,10 +391,10 @@ ngx_js_http_ssl_handshake(ngx_js_http_t *http) goto failed; } - if (ngx_ssl_check_host(c, &http->tls_name) != NGX_OK) { + if (ngx_ssl_check_host(c, &http->host) != NGX_OK) { ngx_log_error(NGX_LOG_ERR, c->log, 0, "js http SSL certificate does not match \"%V\"", - &http->tls_name); + &http->host); goto failed; } } @@ -380,7 +425,7 @@ ngx_js_http_ssl_name(ngx_js_http_t *http) u_char *p; /* as per RFC 6066, literal IPv4 and IPv6 addresses are not permitted */ - ngx_str_t *name = &http->tls_name; + ngx_str_t *name = &http->host; if (name->len == 0 || *name->data == '[') { goto done; @@ -522,7 +567,7 @@ ngx_js_http_write_handler(ngx_event_t *wev) } if (!wev->timer_set) { - ngx_add_timer(wev, http->timeout); + ngx_add_timer(wev, http->conf->timeout); } } @@ -571,6 +616,10 @@ ngx_js_http_read_handler(ngx_event_t *rev) return; } + if (rc == NGX_DONE) { + break; + } + continue; } @@ -631,6 +680,10 @@ ngx_js_http_process_status_line(ngx_js_http_t *http) http->response.status_text.len = hp->status_text_end - hp->status_text; http->process = ngx_js_http_process_headers; + if (http->keepalive) { + http->keepalive = (hp->http_version >= ngx_js_http_version(1, 1)); + } + return http->process(http); } @@ -694,21 +747,33 @@ ngx_js_http_process_headers(ngx_js_http_t *http) && ngx_strncasecmp(hp->header_start, (u_char *) "chunked", vlen) == 0) { - hp->chunked = 1; + http->chunked = 1; + } + + if (len == (sizeof("Connection") - 1) + && ngx_strncasecmp(hp->header_name_start, + (u_char *) "Connection", len) == 0) + { + if (vlen == (sizeof("close") - 1) + && ngx_strncasecmp(hp->header_start, (u_char *) "close", + vlen) == 0) + { + http->keepalive = 0; + } } if (len == (sizeof("Content-Length") - 1) && ngx_strncasecmp(hp->header_name_start, (u_char *) "Content-Length", len) == 0) { - hp->content_length_n = ngx_atoof(hp->header_start, vlen); - if (hp->content_length_n == NGX_ERROR) { + http->content_length_n = ngx_atoof(hp->header_start, vlen); + if (http->content_length_n == NGX_ERROR) { ngx_js_http_error(http, "invalid http content length"); return NGX_ERROR; } if (!http->header_only - && hp->content_length_n + && http->content_length_n > (off_t) http->max_response_body_size) { ngx_js_http_error(http, @@ -762,22 +827,22 @@ ngx_js_http_process_body(ngx_js_http_t *http) } if (!http->header_only - && http->http_parse.chunked - && http->http_parse.content_length_n == -1) + && http->chunked + && http->content_length_n == -1) { ngx_js_http_error(http, "invalid http chunked response"); return NGX_ERROR; } if (http->header_only - || http->http_parse.content_length_n == -1 - || size == http->http_parse.content_length_n) + || http->content_length_n == -1 + || size == http->content_length_n) { http->ready_handler(http); return NGX_DONE; } - if (size < http->http_parse.content_length_n) { + if (size < http->content_length_n) { return NGX_AGAIN; } @@ -787,7 +852,7 @@ ngx_js_http_process_body(ngx_js_http_t *http) b = http->buffer; - if (http->http_parse.chunked) { + if (http->chunked) { rc = ngx_js_http_parse_chunked(&http->http_chunk_parse, b, &http->response.chain); if (rc == NGX_ERROR) { @@ -798,7 +863,7 @@ ngx_js_http_process_body(ngx_js_http_t *http) size = njs_chb_size(&http->response.chain); if (rc == NGX_OK) { - http->http_parse.content_length_n = size; + http->content_length_n = size; } if (size > http->max_response_body_size * 10) { @@ -814,11 +879,11 @@ ngx_js_http_process_body(ngx_js_http_t *http) if (http->header_only) { need = 0; - } else if (http->http_parse.content_length_n == -1) { + } else if (http->content_length_n == -1) { need = http->max_response_body_size - size; } else { - need = http->http_parse.content_length_n - size; + need = http->content_length_n - size; } chsize = ngx_min(need, b->last - b->pos); @@ -942,6 +1007,12 @@ ngx_js_http_parse_status_line(ngx_js_http_parse_t *hp, ngx_buf_t *b) return NGX_ERROR; } + hp->http_major = ch - '0'; + + if (hp->http_major > 1) { + return NGX_ERROR; + } + state = sw_major_digit; break; @@ -956,6 +1027,12 @@ ngx_js_http_parse_status_line(ngx_js_http_parse_t *hp, ngx_buf_t *b) return NGX_ERROR; } + hp->http_major = hp->http_major * 10 + (ch - '0'); + + if (hp->http_major > 1) { + return NGX_ERROR; + } + break; /* the first digit of minor HTTP version */ @@ -964,6 +1041,7 @@ ngx_js_http_parse_status_line(ngx_js_http_parse_t *hp, ngx_buf_t *b) return NGX_ERROR; } + hp->http_minor = ch - '0'; state = sw_minor_digit; break; @@ -978,6 +1056,12 @@ ngx_js_http_parse_status_line(ngx_js_http_parse_t *hp, ngx_buf_t *b) return NGX_ERROR; } + if (hp->http_minor > 99) { + return NGX_ERROR; + } + + hp->http_minor = hp->http_minor * 10 + (ch - '0'); + break; /* HTTP status code */ @@ -1055,6 +1139,8 @@ ngx_js_http_parse_status_line(ngx_js_http_parse_t *hp, ngx_buf_t *b) b->pos = p + 1; hp->state = sw_start; + hp->http_version = ngx_js_http_version(hp->http_major, hp->http_minor); + return NGX_OK; } @@ -1528,3 +1614,242 @@ ngx_js_check_header_name(u_char *name, size_t len) return NGX_OK; } + + +static ngx_int_t +ngx_js_http_get_keepalive_connection(ngx_js_http_t *http) +{ + ngx_str_t *host; + ngx_queue_t *q; + ngx_connection_t *c; + ngx_js_loc_conf_t *conf; + ngx_js_http_keepalive_cache_t *cache; + + if (!http->keepalive) { + return NGX_DECLINED; + } + + conf = http->conf; + + host = &http->host; + + for (q = ngx_queue_head(&conf->fetch_keepalive_cache); + q != ngx_queue_sentinel(&conf->fetch_keepalive_cache); + q = ngx_queue_next(q)) + { + cache = ngx_queue_data(q, ngx_js_http_keepalive_cache_t, queue); + + if (host->len != cache->host_len) { + continue; + } + + if ((http->ssl != NULL) != (cache->ssl != 0)) { + continue; + } + + if (ngx_strncasecmp(host->data, cache->host, host->len) != 0) { + continue; + } + + if (http->port != cache->port) { + continue; + } + + c = cache->connection; + ngx_queue_remove(q); + ngx_queue_insert_head(&conf->fetch_keepalive_free, q); + + goto found; + } + + return NGX_DECLINED; + +found: + + ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0, + "js http keepalive using cached connection: %p:%d", + c, c->fd); + + c->idle = 0; + c->sent = 0; + c->data = NULL; + c->log = http->log; + c->pool->log = http->log; + c->read->log = http->log; + c->write->log = http->log; + + if (c->read->timer_set) { + ngx_del_timer(c->read); + } + + http->peer.cached = 1; + http->peer.connection = c; + + return NGX_OK; +} + + +static ngx_int_t +ngx_js_http_free_keepalive_connection(ngx_js_http_t *http) +{ + ngx_uint_t i; + ngx_queue_t *q; + ngx_connection_t *c; + ngx_js_loc_conf_t *conf; + ngx_js_http_keepalive_cache_t *cache; + + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, http->log, 0, + "js http free keepalive connection"); + + c = http->peer.connection; + + if (c == NULL + || c->read->eof + || c->read->error + || c->read->timedout + || c->write->error + || c->write->timedout) + { + return NGX_ERROR; + } + + if (c->requests >= http->conf->fetch_keepalive_requests) { + return NGX_DONE; + } + + if (ngx_current_msec - c->start_time > http->conf->fetch_keepalive_time) { + return NGX_DONE; + } + + if (ngx_terminate || ngx_exiting) { + return NGX_DONE; + } + + if (ngx_handle_read_event(c->read, 0) != NGX_OK) { + return NGX_ERROR; + } + + ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0, + "js http free keepalive connection, " + "saving connection: %p:%d", c, c->fd); + + conf = http->conf; + + if (ngx_queue_empty(&conf->fetch_keepalive_cache) + && ngx_queue_empty(&conf->fetch_keepalive_free)) + { + cache = ngx_pcalloc(ngx_cycle->pool, + sizeof(ngx_js_http_keepalive_cache_t) * conf->fetch_keepalive); + if (cache == NULL) { + return NGX_ERROR; + } + + for (i = 0; i < conf->fetch_keepalive; i++) { + ngx_queue_insert_head(&conf->fetch_keepalive_free, + &cache[i].queue); + cache[i].conf = conf; + } + } + + if (ngx_queue_empty(&conf->fetch_keepalive_free)) { + /* evict from cache */ + q = ngx_queue_last(&conf->fetch_keepalive_cache); + ngx_queue_remove(q); + + cache = ngx_queue_data(q, ngx_js_http_keepalive_cache_t, queue); + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, + "js http free keepalive connection, evicting: %d", + cache->connection->fd); + ngx_js_http_close_connection(cache->connection); + + } else { + q = ngx_queue_head(&conf->fetch_keepalive_free); + ngx_queue_remove(q); + cache = ngx_queue_data(q, ngx_js_http_keepalive_cache_t, queue); + } + + ngx_queue_insert_head(&conf->fetch_keepalive_cache, q); + + c = http->peer.connection; + http->peer.connection = NULL; + + cache->connection = c; + + cache->ssl = (http->ssl != NULL); + ngx_memcpy(cache->host, http->host.data, http->host.len); + cache->host_len = http->host.len; + cache->port = http->port; + + c->read->delayed = 0; + ngx_add_timer(c->read, conf->fetch_keepalive_timeout); + + if (c->write->timer_set) { + ngx_del_timer(c->write); + } + + c->data = cache; + c->write->handler = ngx_js_http_keepalive_dummy_handler; + c->read->handler = ngx_js_http_keepalive_close_handler; + + c->idle = 1; + c->log = ngx_cycle->log; + c->pool->log = ngx_cycle->log; + c->read->log = ngx_cycle->log; + c->write->log = ngx_cycle->log; + + if (c->read->ready) { + ngx_js_http_keepalive_close_handler(c->read); + } + + return NGX_OK; +} + + +static void +ngx_js_http_keepalive_dummy_handler(ngx_event_t *ev) +{ + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, 0, + "js http keepalive dummy handler"); +} + + +static void +ngx_js_http_keepalive_close_handler(ngx_event_t *ev) +{ + ssize_t n; + ngx_connection_t *c; + ngx_js_loc_conf_t *conf; + ngx_js_http_keepalive_cache_t *cache; + u_char buf[1]; + + c = ev->data; + + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, ev->log, 0, + "js http keepalive close handler: %d", c->fd); + + if (c->close || ev->timedout) { + goto close; + } + + n = recv(c->fd, buf, 1, MSG_PEEK); + + if (n == -1 && ngx_socket_errno == NGX_EAGAIN) { + ev->ready = 0; + + if (ngx_handle_read_event(c->read, 0) != NGX_OK) { + goto close; + } + + return; + } + +close: + + cache = c->data; + conf = cache->conf; + + ngx_js_http_close_connection(c); + + ngx_queue_remove(&cache->queue); + ngx_queue_insert_head(&conf->fetch_keepalive_free, &cache->queue); +} diff --git a/nginx/ngx_js_http.h b/nginx/ngx_js_http.h index 63d0f035f..acaa58027 100644 --- a/nginx/ngx_js_http.h +++ b/nginx/ngx_js_http.h @@ -11,17 +11,20 @@ #define _NGX_JS_HTTP_H_INCLUDED_ -typedef struct ngx_js_http_s ngx_js_http_t; +#define NGX_JS_HOST_MAX_LEN 256 + +typedef struct ngx_js_http_s ngx_js_http_t; typedef struct { ngx_uint_t state; + unsigned http_major:16; + unsigned http_minor:16; + ngx_uint_t http_version; 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; @@ -111,18 +114,22 @@ struct ngx_js_http_s { ngx_addr_t *addrs; ngx_uint_t naddrs; ngx_uint_t naddr; + ngx_str_t host; in_port_t port; ngx_peer_connection_t peer; - ngx_msec_t timeout; + ngx_js_loc_conf_t *conf; ngx_int_t buffer_size; ngx_int_t max_response_body_size; unsigned header_only; + ngx_flag_t chunked; + ngx_flag_t keepalive; + off_t content_length_n; + #if (NGX_SSL) - ngx_str_t tls_name; ngx_ssl_t *ssl; njs_bool_t ssl_verify; #endif diff --git a/nginx/ngx_qjs_fetch.c b/nginx/ngx_qjs_fetch.c index 46b54eab2..a211e0fef 100644 --- a/nginx/ngx_qjs_fetch.c +++ b/nginx/ngx_qjs_fetch.c @@ -5,10 +5,6 @@ */ -#include -#include -#include -#include #include "ngx_js.h" #include "ngx_js_http.h" @@ -39,7 +35,7 @@ static ngx_int_t ngx_qjs_headers_inherit(JSContext *cx, static ngx_int_t ngx_qjs_headers_fill(JSContext *cx, ngx_js_headers_t *headers, JSValue init); static ngx_qjs_fetch_t *ngx_qjs_fetch_alloc(JSContext *cx, ngx_pool_t *pool, - ngx_log_t *log); + ngx_log_t *log, ngx_js_loc_conf_t *conf); static void ngx_qjs_fetch_error(ngx_js_http_t *http, const char *err); static void ngx_qjs_fetch_destructor(ngx_qjs_event_t *event); static void ngx_qjs_fetch_done(ngx_qjs_fetch_t *fetch, JSValue retval, @@ -257,7 +253,8 @@ ngx_qjs_ext_fetch(JSContext *cx, JSValueConst this_val, int argc, c = ngx_qjs_external_connection(cx, external); pool = ngx_qjs_external_pool(cx, external); - fetch = ngx_qjs_fetch_alloc(cx, pool, c->log); + fetch = ngx_qjs_fetch_alloc(cx, pool, c->log, + ngx_qjs_external_loc_conf(cx, external)); if (fetch == NULL) { return JS_ThrowOutOfMemory(cx); } @@ -269,9 +266,15 @@ ngx_qjs_ext_fetch(JSContext *cx, JSValueConst this_val, int argc, goto fail; } + if (u.host.len >= NGX_JS_HOST_MAX_LEN) { + JS_ThrowInternalError(cx, "Host name too long"); + goto fail; + } + http = &fetch->http; + http->host = u.host; + http->port = u.port; http->response.url = request.url; - http->timeout = ngx_qjs_external_fetch_timeout(cx, external); http->buffer_size = ngx_qjs_external_buffer_size(cx, external); http->max_response_body_size = ngx_qjs_external_max_response_buffer_size(cx, external); @@ -418,18 +421,22 @@ ngx_qjs_ext_fetch(JSContext *cx, JSValueConst this_val, int argc, continue; } + if (h[i].key.len == 10 + && ngx_strncasecmp(h[i].key.data, (u_char *) "Connection", 10) + == 0) + { + continue; + } + njs_chb_append(&http->chain, h[i].key.data, h[i].key.len); njs_chb_append_literal(&http->chain, ": "); njs_chb_append(&http->chain, h[i].value.data, h[i].value.len); njs_chb_append_literal(&http->chain, CRLF); } - njs_chb_append_literal(&http->chain, "Connection: close" CRLF); - -#if (NGX_SSL) - http->tls_name.data = u.host.data; - http->tls_name.len = u.host.len; -#endif + if (!http->keepalive) { + njs_chb_append_literal(&http->chain, "Connection: close" CRLF); + } if (request.body.len != 0) { njs_chb_sprintf(&http->chain, 32, "Content-Length: %uz" CRLF CRLF, @@ -1193,7 +1200,8 @@ ngx_qjs_headers_fill(JSContext *cx, ngx_js_headers_t *headers, JSValue init) static ngx_qjs_fetch_t * -ngx_qjs_fetch_alloc(JSContext *cx, ngx_pool_t *pool, ngx_log_t *log) +ngx_qjs_fetch_alloc(JSContext *cx, ngx_pool_t *pool, ngx_log_t *log, + ngx_js_loc_conf_t *conf) { ngx_js_ctx_t *ctx; ngx_js_http_t *http; @@ -1210,9 +1218,10 @@ ngx_qjs_fetch_alloc(JSContext *cx, ngx_pool_t *pool, ngx_log_t *log) http->pool = pool; http->log = log; - http->timeout = 10000; + http->conf = conf; - http->http_parse.content_length_n = -1; + http->content_length_n = -1; + http->keepalive = (conf->fetch_keepalive > 0); ngx_qjs_arg(http->response.header_value) = JS_UNDEFINED; diff --git a/nginx/ngx_stream_js_module.c b/nginx/ngx_stream_js_module.c index 5ccc08a2a..ab4e787d6 100644 --- a/nginx/ngx_stream_js_module.c +++ b/nginx/ngx_stream_js_module.c @@ -194,10 +194,8 @@ static void ngx_stream_qjs_periodic_finalizer(JSRuntime *rt, JSValue val); static ngx_pool_t *ngx_stream_js_pool(ngx_stream_session_t *s); static ngx_resolver_t *ngx_stream_js_resolver(ngx_stream_session_t *s); static ngx_msec_t ngx_stream_js_resolver_timeout(ngx_stream_session_t *s); -static ngx_msec_t ngx_stream_js_fetch_timeout(ngx_stream_session_t *s); -static size_t ngx_stream_js_buffer_size(ngx_stream_session_t *s); -static size_t ngx_stream_js_max_response_buffer_size(ngx_stream_session_t *s); static void ngx_stream_js_event_finalize(ngx_stream_session_t *s, ngx_int_t rc); +static ngx_js_loc_conf_t *ngx_stream_js_srv_conf(ngx_stream_session_t *s); static ngx_js_ctx_t *ngx_stream_js_ctx(ngx_stream_session_t *s); static void ngx_stream_js_periodic_handler(ngx_event_t *ev); @@ -225,8 +223,6 @@ static char *ngx_stream_js_merge_srv_conf(ngx_conf_t *cf, void *parent, static char *ngx_stream_js_shared_dict_zone(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); -static ngx_ssl_t *ngx_stream_js_ssl(ngx_stream_session_t *s); -static ngx_flag_t ngx_stream_js_ssl_verify(ngx_stream_session_t *s); static ngx_conf_bitmask_t ngx_stream_js_engines[] = { { ngx_string("njs"), NGX_ENGINE_NJS }, @@ -355,6 +351,34 @@ static ngx_command_t ngx_stream_js_commands[] = { offsetof(ngx_stream_js_srv_conf_t, timeout), NULL }, + { ngx_string("js_fetch_keepalive"), + NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1, + ngx_conf_set_num_slot, + NGX_STREAM_SRV_CONF_OFFSET, + offsetof(ngx_stream_js_srv_conf_t, fetch_keepalive), + NULL }, + + { ngx_string("js_fetch_keepalive_requests"), + NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1, + ngx_conf_set_num_slot, + NGX_STREAM_SRV_CONF_OFFSET, + offsetof(ngx_stream_js_srv_conf_t, fetch_keepalive_requests), + NULL }, + + { ngx_string("js_fetch_keepalive_time"), + NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1, + ngx_conf_set_msec_slot, + NGX_STREAM_SRV_CONF_OFFSET, + offsetof(ngx_stream_js_srv_conf_t, fetch_keepalive_time), + NULL }, + + { ngx_string("js_fetch_keepalive_timeout"), + NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1, + ngx_conf_set_msec_slot, + NGX_STREAM_SRV_CONF_OFFSET, + offsetof(ngx_stream_js_srv_conf_t, fetch_keepalive_timeout), + NULL }, + #if (NGX_STREAM_SSL) { ngx_string("js_fetch_ciphers"), @@ -710,13 +734,9 @@ static uintptr_t ngx_stream_js_uptr[] = { (uintptr_t) ngx_stream_js_resolver, (uintptr_t) ngx_stream_js_resolver_timeout, (uintptr_t) ngx_stream_js_event_finalize, - (uintptr_t) ngx_stream_js_ssl, - (uintptr_t) ngx_stream_js_ssl_verify, - (uintptr_t) ngx_stream_js_fetch_timeout, - (uintptr_t) ngx_stream_js_buffer_size, - (uintptr_t) ngx_stream_js_max_response_buffer_size, - (uintptr_t) 0 /* main_conf ptr */, + (uintptr_t) ngx_stream_js_srv_conf, (uintptr_t) ngx_stream_js_ctx, + (uintptr_t) 0 /* main_conf ptr */, }; @@ -1891,39 +1911,6 @@ ngx_stream_js_resolver_timeout(ngx_stream_session_t *s) } -static ngx_msec_t -ngx_stream_js_fetch_timeout(ngx_stream_session_t *s) -{ - ngx_stream_js_srv_conf_t *jscf; - - jscf = ngx_stream_get_module_srv_conf(s, ngx_stream_js_module); - - return jscf->timeout; -} - - -static size_t -ngx_stream_js_buffer_size(ngx_stream_session_t *s) -{ - ngx_stream_js_srv_conf_t *jscf; - - jscf = ngx_stream_get_module_srv_conf(s, ngx_stream_js_module); - - return jscf->buffer_size; -} - - -static size_t -ngx_stream_js_max_response_buffer_size(ngx_stream_session_t *s) -{ - ngx_stream_js_srv_conf_t *jscf; - - jscf = ngx_stream_get_module_srv_conf(s, ngx_stream_js_module); - - return jscf->max_response_body_size; -} - - static void ngx_stream_js_event_finalize(ngx_stream_session_t *s, ngx_int_t rc) { @@ -1946,6 +1933,13 @@ ngx_stream_js_event_finalize(ngx_stream_session_t *s, ngx_int_t rc) } +static ngx_js_loc_conf_t * +ngx_stream_js_srv_conf(ngx_stream_session_t *s) +{ + return ngx_stream_get_module_srv_conf(s, ngx_stream_js_module); +} + + static ngx_js_ctx_t * ngx_stream_js_ctx(ngx_stream_session_t *s) { @@ -3025,7 +3019,7 @@ ngx_stream_js_init_conf_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf) options.engine = conf->type; jmcf = ngx_stream_conf_get_module_main_conf(cf, ngx_stream_js_module); - ngx_stream_js_uptr[NGX_JS_MAIN_CONF_INDEX] = (uintptr_t) jmcf; + ngx_stream_js_uptr[NGX_JS_EXTERNAL_MAIN_CONF] = (uintptr_t) jmcf; if (conf->type == NGX_ENGINE_NJS) { options.u.njs.metas = &ngx_stream_js_metas; @@ -3716,33 +3710,3 @@ ngx_stream_js_init(ngx_conf_t *cf) return NGX_OK; } - - -static ngx_ssl_t * -ngx_stream_js_ssl(ngx_stream_session_t *s) -{ -#if (NGX_STREAM_SSL) - ngx_stream_js_srv_conf_t *jscf; - - jscf = ngx_stream_get_module_srv_conf(s, ngx_stream_js_module); - - return jscf->ssl; -#else - return NULL; -#endif -} - - -static ngx_flag_t -ngx_stream_js_ssl_verify(ngx_stream_session_t *s) -{ -#if (NGX_STREAM_SSL) - ngx_stream_js_srv_conf_t *jscf; - - jscf = ngx_stream_get_module_srv_conf(s, ngx_stream_js_module); - - return jscf->ssl_verify; -#else - return 0; -#endif -} diff --git a/nginx/t/js_fetch_https_keepalive.t b/nginx/t/js_fetch_https_keepalive.t new file mode 100644 index 000000000..86f72d0a0 --- /dev/null +++ b/nginx/t/js_fetch_https_keepalive.t @@ -0,0 +1,345 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) F5, Inc. + +# Tests for http njs module, fetch method, https keepalive support. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http http_ssl rewrite/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_import test.js; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + resolver 127.0.0.1:%%PORT_8981_UDP%%; + resolver_timeout 1s; + + location /njs { + js_content test.njs; + } + + location /https { + js_content test.https; + + js_fetch_keepalive 4; + js_fetch_ciphers HIGH:!aNull:!MD5; + js_fetch_protocols TLSv1.1 TLSv1.2; + js_fetch_trusted_certificate myca.crt; + } + + location /sni_isolation { + js_content test.sni_isolation; + + js_fetch_keepalive 4; + js_fetch_ciphers HIGH:!aNull:!MD5; + js_fetch_protocols TLSv1.1 TLSv1.2; + js_fetch_trusted_certificate myca.crt; + } + + location /plain_vs_https_isolation { + js_content test.plain_vs_https_isolation; + + js_fetch_keepalive 4; + js_fetch_ciphers HIGH:!aNull:!MD5; + js_fetch_protocols TLSv1.1 TLSv1.2; + js_fetch_trusted_certificate myca.crt; + } + } + + server { + listen 127.0.0.1:8081 ssl; + server_name ka.example.com; + + keepalive_requests 100; + + ssl_certificate ka.example.com.chained.crt; + ssl_certificate_key ka.example.com.key; + + location /loc { + return 200 CONN:$connection_requests; + } + } + + server { + listen 127.0.0.1:8081 ssl; + server_name 1.example.com; + + ssl_certificate 1.example.com.chained.crt; + ssl_certificate_key 1.example.com.key; + + location /loc { + return 200 "You are at 1.example.com."; + } + } + + server { + listen 127.0.0.1:8082; + server_name plain.example.com; + + keepalive_requests 100; + + location /loc { + return 200 PLAIN:$connection_requests; + } + } +} + +EOF + +my $p1 = port(8081); +my $p2 = port(8082); + +$t->write_file('test.js', < reply.text()) + .then(body => r.return(200, body)) + .catch(e => r.return(501, e.message)) + } + + async function sni_isolation(r) { + try { + let resp = await ngx.fetch(`https://ka.example.com:$p1/loc`); + let body1 = await resp.text(); + + resp = await ngx.fetch(`https://1.example.com:$p1/loc`); + let body2 = await resp.text(); + + resp = await ngx.fetch(`https://ka.example.com:$p1/loc`); + let body3 = await resp.text(); + + r.return(200, `\${body1}|\${body2}|\${body3}`); + + } catch (e) { + r.return(501, e.message); + } + } + + async function plain_vs_https_isolation(r) { + try { + let resp = await ngx.fetch(`https://ka.example.com:$p1/loc`); + let body1 = await resp.text(); + + resp = await ngx.fetch(`http://plain.example.com:$p2/loc`); + let body2 = await resp.text(); + + resp = await ngx.fetch(`https://ka.example.com:$p1/loc`); + let body3 = await resp.text(); + + r.return(200, `\${body1}|\${body2}|\${body3}`); + + } catch (e) { + r.return(501, e.message); + } + } + + export default {njs: test_njs, https, sni_isolation, + plain_vs_https_isolation}; +EOF + +my $d = $t->testdir(); + +$t->write_file('openssl.conf', <write_file('myca.conf', <>$d/openssl.out 2>&1") == 0 + or die "Can't create self-signed certificate for CA: $!\n"; + +foreach my $name ('intermediate', '1.example.com', 'ka.example.com') { + system("openssl req -new " + . "-config $d/openssl.conf -subj /CN=$name/ " + . "-out $d/$name.csr -keyout $d/$name.key " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create certificate signing req for $name: $!\n"; +} + +$t->write_file('certserial', '1000'); +$t->write_file('certindex', ''); + +system("openssl ca -batch -config $d/myca.conf " + . "-keyfile $d/myca.key -cert $d/myca.crt " + . "-subj /CN=intermediate/ -in $d/intermediate.csr " + . "-out $d/intermediate.crt " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't sign certificate for intermediate: $!\n"; + +foreach my $name ('1.example.com', 'ka.example.com') { + system("openssl ca -batch -config $d/myca.conf " + . "-keyfile $d/intermediate.key -cert $d/intermediate.crt " + . "-subj /CN=$name/ -in $d/$name.csr -out $d/$name.crt " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't sign certificate for $name $!\n"; + $t->write_file("$name.chained.crt", $t->read_file("$name.crt") + . $t->read_file('intermediate.crt')); +} + +$t->try_run('no njs.fetch'); + +$t->plan(5); + +$t->run_daemon(\&dns_daemon, port(8981), $t); +$t->waitforfile($t->testdir . '/' . port(8981)); + +############################################################################### + +like(http_get('/https?domain=localhost'), + qr/connect failed/s, 'fetch https wrong CN certificate'); +like(http_get('/https?domain=ka.example.com'), + qr/CONN:1$/s, 'fetch https keepalive'); +like(http_get('/https?domain=ka.example.com'), + qr/CONN:2$/s, 'fetch https keepalive reused'); +like(http_get('/sni_isolation'), + qr/CONN:1\|You are at 1\.example\.com\.\|CONN:2$/s, + 'fetch https keepalive SNI isolation'); +like(http_get('/plain_vs_https_isolation'), + qr/CONN:1\|PLAIN:1\|CONN:2$/s, + 'fetch https->plain->https keepalive isolation'); + +############################################################################### + +sub reply_handler { + my ($recv_data, $port, %extra) = @_; + + my (@name, @rdata); + + use constant NOERROR => 0; + use constant A => 1; + use constant IN => 1; + + # default values + + my ($hdr, $rcode, $ttl) = (0x8180, NOERROR, 3600); + + # decode name + + my ($len, $offset) = (undef, 12); + while (1) { + $len = unpack("\@$offset C", $recv_data); + last if $len == 0; + $offset++; + push @name, unpack("\@$offset A$len", $recv_data); + $offset += $len; + } + + $offset -= 1; + my ($id, $type, $class) = unpack("n x$offset n2", $recv_data); + + my $name = join('.', @name); + + if ($type == A) { + push @rdata, rd_addr($ttl, '127.0.0.1'); + } + + $len = @name; + pack("n6 (C/a*)$len x n2", $id, $hdr | $rcode, 1, scalar @rdata, + 0, 0, @name, $type, $class) . join('', @rdata); +} + +sub rd_addr { + my ($ttl, $addr) = @_; + + my $code = 'split(/\./, $addr)'; + + return pack 'n3N', 0xc00c, A, IN, $ttl if $addr eq ''; + + pack 'n3N nC4', 0xc00c, A, IN, $ttl, eval "scalar $code", eval($code); +} + +sub dns_daemon { + my ($port, $t) = @_; + + my ($data, $recv_data); + my $socket = IO::Socket::INET->new( + LocalAddr => '127.0.0.1', + LocalPort => $port, + Proto => 'udp', + ) + or die "Can't create listening socket: $!\n"; + + local $SIG{PIPE} = 'IGNORE'; + + # signal we are ready + + open my $fh, '>', $t->testdir() . '/' . $port; + close $fh; + + while (1) { + $socket->recv($recv_data, 65536); + $data = reply_handler($recv_data, $port); + $socket->send($data); + } +} + +############################################################################### diff --git a/nginx/t/js_fetch_keepalive.t b/nginx/t/js_fetch_keepalive.t new file mode 100755 index 000000000..100be40c6 --- /dev/null +++ b/nginx/t/js_fetch_keepalive.t @@ -0,0 +1,289 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) F5, Inc. + +# Tests for http njs module, fetch method keepalive. + +############################################################################### + +use warnings; +use strict; + +use Test::More; +use IO::Socket::INET; + +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_import test.js; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /engine { + js_content test.engine; + } + + location /keepalive { + js_fetch_keepalive 4; + js_fetch_keepalive_requests 100; + js_fetch_keepalive_time 60s; + js_fetch_keepalive_timeout 60s; + js_content test.keepalive; + } + + location /keepalive_simultaneous { + js_fetch_keepalive 4; + js_content test.keepalive_simultaneous; + } + + location /keepalive_requests { + js_fetch_keepalive 4; + js_fetch_keepalive_requests 2; + js_content test.keepalive; + } + + location /keepalive_time { + js_fetch_keepalive 4; + js_fetch_keepalive_time 100ms; + js_content test.keepalive; + } + + location /keepalive_timeout { + js_fetch_keepalive 4; + js_fetch_keepalive_timeout 100ms; + js_content test.keepalive; + } + + location /no_keepalive { + js_fetch_keepalive 0; + js_content test.keepalive; + } + } + + server { + listen 127.0.0.1:8081; + keepalive_requests 100; + keepalive_timeout 60s; + + location /count { + add_header Connection-ID $connection_requests; + return 200 $connection_requests; + } + + location /count_close { + add_header Connection close; + add_header Connection-ID $connection_requests; + return 200 $connection_requests; + } + + location /count_close_mixed { + add_header cOnNeCtiOn ClOsE; + add_header Connection-ID $connection_requests; + return 200 $connection_requests; + } + } +} + +EOF + +my $p1 = port(8081); +my $p2 = port(8082); + +$t->write_file('test.js', < setTimeout(resolve, milliseconds)); + } + + async function keepalive(r) { + const path = r.args.path; + let port = $p1; + if (r.args.port) { + port = r.args.port; + } + + let responses = []; + for (let i = 0; i < 3; i++) { + let resp = await ngx.fetch(`http://127.0.0.1:\${port}/\${path}`) + .then(resp => resp.text()) + .catch(err => err.message); + responses.push(resp.trim()); + + if (r.args.sleep) { + await sleep(Number(r.args.sleep)); + } + } + + r.return(200, responses.toString()); + } + + async function keepalive_simultaneous(r) { + let promises = []; + for (let i = 0; i < Number(r.args.n); i++) { + promises.push(ngx.fetch('http://127.0.0.1:$p1/count')); + } + + let results = await Promise.all(promises); + let bodies = await Promise.all(results.map(r => r.text())); + let responses = bodies.map(b => parseInt(b.trim())); + + r.return(200, JSON.stringify(responses)); + } + + export default {engine, keepalive, keepalive_simultaneous}; +EOF + +$t->try_run('no js_fetch_keepalive'); + +$t->run_daemon(\&http_daemon, $p2); +$t->waitforsocket('127.0.0.1:' . $p2); + +$t->plan(16); + +############################################################################### + +like(http_get('/no_keepalive?path=count'), qr/1,1,1/, + 'no keepalive connections'); +like(http_get('/keepalive?path=count_close'), qr/1,1,1/, + 'upstream Connection: close (HTTP/1.1)'); +like(http_get('/keepalive?path=count_close_mixed'), qr/1,1,1/, + 'upstream Connection: close, mixed-case (HTTP/1.1)'); +like(http_get('/keepalive?path=count'), qr/1,2,3/, + 'keepalive reuses connection'); +like(http_get('/keepalive?path=count'), qr/4,5,6/, + 'keepalive reuses connection across requests'); +like(http_get('/keepalive_simultaneous?n=8'), qr/1,1,1,1,1,1,1,1/, + 'keepalive simultaneous requests'); +like(http_get('/keepalive_simultaneous?n=8'), qr/2,2,2,2,1,1,1,1/, + 'keepalive simultaneous requests reused connections'); +like(http_get('/keepalive_requests?path=count'), qr/1,2,1/, + 'keepalive with limited requests per connection'); + +like(http_get('/keepalive_time?path=count'), qr/1,2,3/, + 'keepalive with time limit, first round'); + +select undef, undef, undef, 0.15; + +like(http_get('/keepalive_time?path=count'), qr/4,1,2/, + 'keepalive with time limit, second round'); + +like(http_get('/keepalive_timeout?path=count'), qr/1,2,3/, + 'keepalive with timeout limit, first round'); + +select undef, undef, undef, 0.15; + +like(http_get('/keepalive_timeout?path=count'), qr/1,2,3/, + 'keepalive with timeout limit, second round'); + +like(http_get("/keepalive?path=broken_keepalive&port=$p2&sleep=1"), qr/1,1,1/, + 'upstream broken keepalive (connection closed by upstream)'); +like(http_get("/keepalive?path=http10&port=$p2"), qr/1,1,1/, + 'upstream HTTP/1.0 (no keepalive)'); +like(http_get("/keepalive?path=count&port=$p2&sleep=1"), qr/1,2,3/, + 'normal keepalive'); +like(http_get("/keepalive?path=assumed_keepalive&port=$p2&sleep=1"), qr/4,5,6/, + 'assumed keepalive'); + +############################################################################### + +sub http_daemon { + my $port = shift; + + my $server = IO::Socket::INET->new( + Proto => 'tcp', + LocalAddr => '127.0.0.1:' . $port, + Listen => 5, + Reuse => 1 + ) or die "Can't create listening socket: $!\n"; + + my $ccount = 0; + my $rcount = 0; + + # dumb server which is able to keep connections alive + + while (my $client = $server->accept()) { + Test::Nginx::log_core('||', + "connection from " . $client->peerhost()); + $client->autoflush(1); + $ccount++; + $rcount = 0; + + while (1) { + my $headers = ''; + my $uri = ''; + + while (<$client>) { + Test::Nginx::log_core('||', $_); + $headers .= $_; + last if (/^\x0d?\x0a?$/); + } + + last if $headers eq ''; + $rcount++; + + $uri = $1 if $headers =~ /^\S+\s+([^ ]+)\s+HTTP/i; + my $body = $rcount; + + if ($uri eq '/broken_keepalive') { + print $client + "HTTP/1.1 200 OK" . CRLF . + "Content-Length: " . length($body) . CRLF . + "Connection: keep-alive" . CRLF . CRLF . + $body; + + last; + + } elsif ($uri eq '/assumed_keepalive') { + print $client + "HTTP/1.1 200 OK" . CRLF . + "Content-Length: " . length($body) . CRLF . CRLF . + $body; + + } elsif ($uri eq '/count') { + print $client + "HTTP/1.1 200 OK" . CRLF . + "Content-Length: " . length($body) . CRLF . + "Connection: keep-alive" . CRLF . CRLF . + $body; + + } elsif ($uri eq '/http10') { + print $client + "HTTP/1.0 200 OK" . CRLF . + "Content-Length: " . length($body) . CRLF . CRLF . + $body; + } + } + + close $client; + } +} + +############################################################################### diff --git a/nginx/t/stream_js_fetch_keepalive.t b/nginx/t/stream_js_fetch_keepalive.t new file mode 100644 index 000000000..e940eea5f --- /dev/null +++ b/nginx/t/stream_js_fetch_keepalive.t @@ -0,0 +1,200 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) F5, Inc. + +# Tests for stream njs module, fetch method keepalive. + +############################################################################### + +use warnings; +use strict; + +use Test::More; +use IO::Socket::INET; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http stream/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_import test.js; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /engine { + js_content test.engine; + } + } + + server { + listen 127.0.0.1:8081; + keepalive_requests 100; + keepalive_timeout 60s; + + location /count { + add_header Connection-ID $connection_requests; + return 200 $connection_requests; + } + + location /headers { + return 200 "Connection: $http_connection"; + } + } +} + +stream { + %%TEST_GLOBALS_STREAM%% + + js_import test.js; + js_var $message; + + server { + listen 127.0.0.1:8082; + js_fetch_keepalive 4; + js_fetch_keepalive_requests 100; + js_fetch_keepalive_time 60s; + js_fetch_keepalive_timeout 60s; + js_preread test.keepalive; + return $message; + } + + server { + listen 127.0.0.1:8083; + js_fetch_keepalive 0; + js_preread test.keepalive; + return $message; + } + + server { + listen 127.0.0.1:8084; + js_fetch_keepalive 4; + js_fetch_keepalive_requests 2; + js_preread test.keepalive; + return $message; + } + + server { + listen 127.0.0.1:8085; + js_fetch_keepalive 4; + js_fetch_keepalive_time 100ms; + js_preread test.keepalive; + return $message; + } + + server { + listen 127.0.0.1:8086; + js_fetch_keepalive 4; + js_fetch_keepalive_timeout 100ms; + js_preread test.keepalive; + return $message; + } + + server { + listen 127.0.0.1:8087; + js_fetch_keepalive 4; + js_preread test.keepalive_simultaneous; + return $message; + } +} + +EOF + +my $p1 = port(8081); + +$t->write_file('test.js', < r.text())); + let responses = bodies.map(b => parseInt(b.trim())); + + s.variables.message = JSON.stringify(responses); + s.done(); + } + + export default {engine, keepalive, keepalive_simultaneous}; +EOF + +$t->try_run('no stream js_fetch_keepalive'); + +$t->plan(10); + +############################################################################### + +like(stream('127.0.0.1:' . port(8083))->io('GO'), qr/\[1,1,1]/, + 'no keepalive connections'); +like(stream('127.0.0.1:' . port(8082))->io('GO'), qr/\[1,2,3]/, + 'keepalive reuses connection'); +like(stream('127.0.0.1:' . port(8082))->io('GO'), qr/\[4,5,6]/, + 'keepalive reuses connection across sessions'); + +like(stream('127.0.0.1:' . port(8087))->io('GO'), qr/^\[(1,){7}1\]$/, + 'keepalive simultaneous requests'); +like(stream('127.0.0.1:' . port(8087))->io('GO'), + qr/\[2,2,2,2,1,1,1,1\]/, + 'keepalive simultaneous requests reused connections'); + +like(stream('127.0.0.1:' . port(8084))->io('GO'), qr/\[1,2,1]/, + 'keepalive with limited requests per connection'); + +like(stream('127.0.0.1:' . port(8085))->io('GO'), qr/\[1,2,3]/, + 'keepalive with time limit, first round'); + +select undef, undef, undef, 0.15; + +like(stream('127.0.0.1:' . port(8085))->io('GO'), qr/\[4,1,2]/, + 'keepalive with time limit, second round'); + +like(stream('127.0.0.1:' . port(8086))->io('GO'), qr/\[1,2,3]/, + 'keepalive with timeout limit, first round'); + +select undef, undef, undef, 0.15; + +like(stream('127.0.0.1:' . port(8086))->io('GO'), qr/\[1,2,3]/, + 'keepalive with timeout limit, second round'); + +###############################################################################