diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c index e3ee42719..8f31b80d7 100644 --- a/nginx/ngx_http_js_module.c +++ b/nginx/ngx_http_js_module.c @@ -15,6 +15,8 @@ typedef struct { NGX_JS_COMMON_LOC_CONF; + ngx_http_complex_value_t fetch_proxy_cv; + ngx_str_t content; ngx_str_t header_filter; ngx_str_t body_filter; @@ -380,6 +382,8 @@ static char *ngx_http_js_content(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); static char *ngx_http_js_shared_dict_zone(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); +static char *ngx_http_js_fetch_proxy(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); static char *ngx_http_js_body_filter_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); static ngx_int_t ngx_http_js_init_conf_vm(ngx_conf_t *cf, @@ -593,6 +597,13 @@ static ngx_command_t ngx_http_js_commands[] = { offsetof(ngx_http_js_loc_conf_t, fetch_keepalive_timeout), NULL }, + { ngx_string("js_fetch_proxy"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_http_js_fetch_proxy, + NGX_HTTP_LOC_CONF_OFFSET, + 0, + NULL }, + ngx_null_command }; @@ -818,6 +829,16 @@ static njs_external_t ngx_http_js_ext_request[] = { } }, + { + .flags = NJS_EXTERN_PROPERTY, + .name.string = njs_str("requestLine"), + .enumerable = 1, + .u.property = { + .handler = ngx_js_ext_string, + .magic32 = offsetof(ngx_http_request_t, request_line), + } + }, + { .flags = NJS_EXTERN_PROPERTY, .name.string = njs_str("requestText"), @@ -1080,6 +1101,8 @@ static const JSCFunctionListEntry ngx_http_qjs_ext_request[] = { JS_CGETSET_DEF("remoteAddress", ngx_http_qjs_ext_remote_address, NULL), JS_CGETSET_MAGIC_DEF("requestBuffer", ngx_http_qjs_ext_request_body, NULL, NGX_JS_BUFFER), + JS_CGETSET_MAGIC_DEF("requestLine", ngx_http_qjs_ext_string, NULL, + offsetof(ngx_http_request_t, request_line)), JS_CGETSET_MAGIC_DEF("requestText", ngx_http_qjs_ext_request_body, NULL, NGX_JS_STRING), JS_CGETSET_MAGIC_DEF("responseBuffer", ngx_http_qjs_ext_response_body, NULL, @@ -8016,6 +8039,60 @@ ngx_http_js_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) } +static ngx_int_t +ngx_http_js_eval_proxy_url(ngx_pool_t *pool, void *request, + void *module_conf, ngx_url_t **url_out, ngx_str_t *auth_out) +{ + ngx_str_t value; + ngx_http_request_t *r; + ngx_http_js_loc_conf_t *jlcf; + + r = request; + jlcf = module_conf; + + if (ngx_http_complex_value(r, &jlcf->fetch_proxy_cv, &value) != NGX_OK) { + return NGX_ERROR; + } + + return ngx_js_parse_proxy_url(pool, r->connection->log, &value, + url_out, auth_out); +} + + +static char * +ngx_http_js_fetch_proxy(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_str_t *value; + ngx_uint_t n; + ngx_http_js_loc_conf_t *jlcf; + ngx_http_compile_complex_value_t ccv; + + value = cf->args->elts; + + n = ngx_http_script_variables_count(&value[1]); + + if (n) { + ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); + + jlcf = conf; + + ccv.cf = cf; + ccv.value = &value[1]; + ccv.complex_value = &jlcf->fetch_proxy_cv; + + if (ngx_http_compile_complex_value(&ccv) != NGX_OK) { + return NGX_CONF_ERROR; + } + + jlcf->eval_proxy_url = ngx_http_js_eval_proxy_url; + + return NGX_CONF_OK; + } + + return ngx_js_fetch_proxy(cf, cmd, conf); +} + + static char * ngx_http_js_var(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { diff --git a/nginx/ngx_js.c b/nginx/ngx_js.c index 75a28735d..03f665788 100644 --- a/nginx/ngx_js.c +++ b/nginx/ngx_js.c @@ -3305,6 +3305,195 @@ ngx_js_preload_object(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) } +static ngx_int_t +ngx_js_build_proxy_auth_header(ngx_pool_t *pool, ngx_str_t *auth_header, + ngx_str_t *user, ngx_str_t *pass) +{ + u_char *p; + size_t len; + ngx_str_t userpass, b64; + + userpass.len = user->len + 1 + pass->len; + userpass.data = ngx_pnalloc(pool, userpass.len); + if (userpass.data == NULL) { + return NGX_ERROR; + } + + p = ngx_cpymem(userpass.data, user->data, user->len); + *p++ = ':'; + ngx_memcpy(p, pass->data, pass->len); + + b64.len = ngx_base64_encoded_length(userpass.len); + b64.data = ngx_pnalloc(pool, b64.len); + if (b64.data == NULL) { + return NGX_ERROR; + } + + ngx_encode_base64(&b64, &userpass); + + len = sizeof("Proxy-Authorization: Basic \r\n") - 1 + b64.len; + p = ngx_pnalloc(pool, len); + if (p == NULL) { + return NGX_ERROR; + } + + ngx_sprintf(p, "Proxy-Authorization: Basic %V\r\n", &b64); + auth_header->data = p; + auth_header->len = len; + + return NGX_OK; +} + + +ngx_int_t +ngx_js_parse_proxy_url(ngx_pool_t *pool, ngx_log_t *log, ngx_str_t *url, + ngx_url_t **url_out, ngx_str_t *auth_header_out) +{ + u_char *p, *at, *colon, *host_start, *user_start, *pass_start; + u_char *decoded_user, *decoded_pass, *decoded_end; + size_t user_len, pass_len; + ngx_url_t *u; + ngx_str_t user, pass; + + if (url->len == 0) { + *url_out = NULL; + ngx_str_null(auth_header_out); + return NGX_OK; + } + + if (ngx_strncmp(url->data, "http://", sizeof("http://") - 1) != 0) { + ngx_log_error(NGX_LOG_ERR, log, 0, + "js_fetch_proxy URL must use http:// scheme"); + return NGX_ERROR; + } + + host_start = url->data + (sizeof("http://") - 1); + at = ngx_strlchr(host_start, url->data + url->len, '@'); + + ngx_str_null(auth_header_out); + + if (at != NULL) { + colon = NULL; + + for (p = at - 1; p > host_start; p--) { + if (*p == ':') { + colon = p; + break; + } + } + + if (colon == NULL) { + ngx_log_error(NGX_LOG_ERR, log, 0, + "js_fetch_proxy URL credentials must be in " + "user:password format"); + return NGX_ERROR; + } + + user_start = host_start; + user_len = colon - host_start; + pass_start = colon + 1; + pass_len = at - pass_start; + + decoded_user = ngx_pnalloc(pool, 128); + if (decoded_user == NULL) { + return NGX_ERROR; + } + + decoded_pass = ngx_pnalloc(pool, 128); + if (decoded_pass == NULL) { + return NGX_ERROR; + } + + p = user_start; + decoded_end = decoded_user; + ngx_unescape_uri(&decoded_end, &p, user_len, NGX_UNESCAPE_URI); + + user_len = decoded_end - decoded_user; + if (user_len == 0 || user_len > 127) { + ngx_log_error(NGX_LOG_ERR, log, 0, + "js_fetch_proxy username invalid or too long " + "(max 127 bytes after decoding)"); + return NGX_ERROR; + } + + p = pass_start; + decoded_end = decoded_pass; + ngx_unescape_uri(&decoded_end, &p, pass_len, NGX_UNESCAPE_URI); + + pass_len = decoded_end - decoded_pass; + if (pass_len == 0 || pass_len > 127) { + ngx_log_error(NGX_LOG_ERR, log, 0, + "js_fetch_proxy password invalid or too long " + "(max 127 bytes after decoding)"); + return NGX_ERROR; + } + + user.data = decoded_user; + user.len = user_len; + pass.data = decoded_pass; + pass.len = pass_len; + + if (ngx_js_build_proxy_auth_header(pool, auth_header_out, + &user, &pass) + != NGX_OK) + { + return NGX_ERROR; + } + + host_start = at + 1; + } + + u = ngx_pcalloc(pool, sizeof(ngx_url_t)); + if (u == NULL) { + return NGX_ERROR; + } + + u->url.data = host_start; + u->url.len = url->data + url->len - host_start; + u->default_port = 3128; + u->no_resolve = 1; + + if (ngx_parse_url(pool, u) != NGX_OK) { + ngx_log_error(NGX_LOG_ERR, log, 0, "invalid proxy URL: %V", url); + return NGX_ERROR; + } + + *url_out = u; + + return NGX_OK; +} + + +char * +ngx_js_fetch_proxy(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_str_t *value; + ngx_js_loc_conf_t *jscf; + + jscf = conf; + + value = cf->args->elts; + + if (ngx_js_parse_proxy_url(cf->pool, cf->log, &value[1], + &jscf->fetch_proxy_url, + &jscf->fetch_proxy_auth_header) + != NGX_OK) + { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid proxy URL: %V", &value[1]); + return NGX_CONF_ERROR; + } + + if (jscf->fetch_proxy_url == NULL) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "proxy host is empty in URL: %V", &value[1]); + return NGX_CONF_ERROR; + } + + return NGX_CONF_OK; +} + + static ngx_int_t ngx_js_init_preload_vm(njs_vm_t *vm, ngx_js_loc_conf_t *conf) { @@ -3984,6 +4173,7 @@ ngx_js_create_conf(ngx_conf_t *cf, size_t size) * set by ngx_pcalloc(): * * conf->reuse_queue = NULL; + * conf->fetch_proxy_auth_header = { 0, NULL }; */ conf->paths = NGX_CONF_UNSET_PTR; @@ -4001,6 +4191,8 @@ ngx_js_create_conf(ngx_conf_t *cf, size_t size) conf->fetch_keepalive_requests = NGX_CONF_UNSET_UINT; conf->fetch_keepalive_time = NGX_CONF_UNSET_MSEC; conf->fetch_keepalive_timeout = NGX_CONF_UNSET_MSEC; + conf->fetch_proxy_url = NGX_CONF_UNSET_PTR; + conf->eval_proxy_url = NGX_CONF_UNSET_PTR; return conf; } @@ -4120,10 +4312,15 @@ ngx_js_merge_conf(ngx_conf_t *cf, void *parent, void *child, 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); + ngx_conf_merge_ptr_value(conf->fetch_proxy_url, prev->fetch_proxy_url, + NULL); + ngx_conf_merge_ptr_value(conf->eval_proxy_url, prev->eval_proxy_url, NULL); + ngx_conf_merge_str_value(conf->fetch_proxy_auth_header, + prev->fetch_proxy_auth_header, ""); + 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 20ea85b17..ff3897f98 100644 --- a/nginx/ngx_js.h +++ b/nginx/ngx_js.h @@ -141,7 +141,24 @@ typedef struct { ngx_msec_t fetch_keepalive_time; \ ngx_msec_t fetch_keepalive_timeout; \ ngx_queue_t fetch_keepalive_cache; \ - ngx_queue_t fetch_keepalive_free + ngx_queue_t fetch_keepalive_free; \ + \ + ngx_url_t *fetch_proxy_url; \ + ngx_str_t fetch_proxy_auth_header; \ + \ + ngx_int_t (*eval_proxy_url)(ngx_pool_t *pool, \ + void *request, \ + void *module_conf, \ + ngx_url_t **url_out, \ + ngx_str_t *auth_out) + +#define ngx_js_conf_dynamic_proxy(conf) \ + ((conf)->eval_proxy_url != NULL) + +#define ngx_js_conf_proxy(conf) \ + (((conf)->fetch_proxy_url != NULL \ + && (conf)->fetch_proxy_url->host.len > 0) \ + || ngx_js_conf_dynamic_proxy(conf)) #if (NGX_SSL) @@ -423,6 +440,9 @@ void ngx_js_logger(ngx_connection_t *c, ngx_uint_t level, char * ngx_js_import(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); char * ngx_js_engine(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); char * ngx_js_preload_object(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); +char * ngx_js_fetch_proxy(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); +ngx_int_t ngx_js_parse_proxy_url(ngx_pool_t *pool, ngx_log_t *log, + ngx_str_t *url_str, ngx_url_t **url_out, ngx_str_t *auth_header_out); ngx_int_t ngx_js_merge_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf, ngx_js_loc_conf_t *prev, ngx_int_t (*init_vm)(ngx_conf_t *cf, ngx_js_loc_conf_t *conf)); diff --git a/nginx/ngx_js_fetch.c b/nginx/ngx_js_fetch.c index ac1c1a27b..09d0e1947 100644 --- a/nginx/ngx_js_fetch.c +++ b/nginx/ngx_js_fetch.c @@ -508,15 +508,11 @@ ngx_js_ext_fetch(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, { njs_int_t ret; ngx_url_t u; - ngx_uint_t i; - njs_bool_t has_host; - ngx_str_t method; + ngx_str_t *resolve_host; ngx_pool_t *pool; njs_value_t *init, *value; ngx_js_http_t *http; ngx_js_fetch_t *fetch; - ngx_list_part_t *part; - ngx_js_tb_elt_t *h; ngx_js_request_t request; ngx_connection_t *c; ngx_resolver_ctx_t *ctx; @@ -558,7 +554,7 @@ ngx_js_ext_fetch(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, http->max_response_body_size = http->conf->max_response_body_size; #if (NGX_SSL) - if (u.default_port == 443) { + if (ngx_js_https(&u)) { http->ssl = http->conf->ssl; http->ssl_verify = http->conf->ssl_verify; } @@ -600,131 +596,45 @@ ngx_js_ext_fetch(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, NJS_CHB_MP_INIT(&http->chain, njs_vm_memory_pool(vm)); NJS_CHB_MP_INIT(&http->response.chain, njs_vm_memory_pool(vm)); - njs_chb_append(&http->chain, request.method.data, request.method.len); - njs_chb_append_literal(&http->chain, " "); + resolve_host = NULL; + http->connect_port = http->port; - if (u.uri.len == 0 || u.uri.data[0] != '/') { - njs_chb_append_literal(&http->chain, "/"); - } - - njs_chb_append(&http->chain, u.uri.data, u.uri.len); - njs_chb_append_literal(&http->chain, " HTTP/1.1" CRLF); - - has_host = 0; - part = &request.headers.header_list.part; - h = part->elts; - - for (i = 0; /* void */; i++) { - - if (i >= part->nelts) { - if (part->next == NULL) { - break; - } - - part = part->next; - h = part->elts; - i = 0; - } - - if (h[i].hash == 0) { - continue; - } - - if (h[i].key.len == 4 - && ngx_strncasecmp(h[i].key.data, (u_char *) "Host", 4) == 0) - { - has_host = 1; - njs_chb_append_literal(&http->chain, "Host: "); - njs_chb_append(&http->chain, h[i].value.data, h[i].value.len); - njs_chb_append_literal(&http->chain, CRLF); - break; - } - } - - if (!has_host) { - njs_chb_append_literal(&http->chain, "Host: "); - njs_chb_append(&http->chain, u.host.data, u.host.len); - - if (!u.no_port) { - njs_chb_sprintf(&http->chain, 32, ":%d", u.port); - } - - njs_chb_append_literal(&http->chain, CRLF); - } - - part = &request.headers.header_list.part; - h = part->elts; - - for (i = 0; /* void */; i++) { - - if (i >= part->nelts) { - if (part->next == NULL) { - break; + if (ngx_js_conf_proxy(http->conf)) { + if (ngx_js_conf_dynamic_proxy(http->conf)) { + if (http->conf->eval_proxy_url(http->pool, external, http->conf, + &http->proxy.url, &http->proxy.auth) + != NGX_OK) + { + njs_vm_error(vm, "failed to evaluate proxy URL"); + goto fail; } - part = part->next; - h = part->elts; - i = 0; - } - - if (h[i].hash == 0) { - continue; - } - - if (h[i].key.len == 4 - && ngx_strncasecmp(h[i].key.data, (u_char *) "Host", 4) == 0) - { - continue; + } else { + http->proxy.url = http->conf->fetch_proxy_url; + http->proxy.auth = http->conf->fetch_proxy_auth_header; } - if (h[i].key.len == 14 - && ngx_strncasecmp(h[i].key.data, (u_char *) "Content-Length", 14) - == 0) - { - continue; + if (ngx_js_http_proxy(http) && http->proxy.url->addrs == NULL) { + resolve_host = &http->proxy.url->host; + http->connect_port = http->proxy.url->port; } - - 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); } - if (!http->keepalive) { - njs_chb_append_literal(&http->chain, "Connection: close" CRLF); + if (!ngx_js_http_proxy(http) && u.addrs == NULL) { + resolve_host = &u.host; } - if (request.body.len != 0) { - njs_chb_sprintf(&http->chain, 32, "Content-Length: %uz" CRLF CRLF, - request.body.len); - njs_chb_append(&http->chain, request.body.data, request.body.len); + ngx_js_fetch_build_request(http, &request, &u.uri, &u, + ngx_js_http_proxy(http) && !ngx_js_https(&u)); - } else { - method = request.method; - - if ((method.len == 4 - && (ngx_strncasecmp(method.data, (u_char *) "POST", 4) == 0)) - || (method.len == 3 - && ngx_strncasecmp(method.data, (u_char *) "PUT", 3) == 0)) - { - njs_chb_append_literal(&http->chain, "Content-Length: 0" CRLF CRLF); - - } else { - njs_chb_append_literal(&http->chain, CRLF); - } - } + if (resolve_host != NULL) { + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, http->log, 0, + "js http fetch: resolving"); - if (u.addrs == NULL) { ctx = ngx_js_http_resolve(http, ngx_external_resolver(vm, external), - &u.host, u.port, + resolve_host, ngx_external_resolver_timeout(vm, external)); + if (ctx == NULL) { njs_vm_memory_error(vm); return NJS_ERROR; @@ -741,9 +651,15 @@ ngx_js_ext_fetch(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, } http->naddrs = 1; - ngx_memcpy(&http->addr, &u.addrs[0], sizeof(ngx_addr_t)); http->addrs = &http->addr; + if (ngx_js_http_proxy(http)) { + ngx_memcpy(&http->addr, &http->proxy.url->addrs[0], sizeof(ngx_addr_t)); + + } else { + ngx_memcpy(&http->addr, &u.addrs[0], sizeof(ngx_addr_t)); + } + ngx_js_http_connect(http); njs_value_assign(retval, njs_value_arg(&fetch->promise)); @@ -1155,6 +1071,12 @@ ngx_js_fetch_alloc(njs_vm_t *vm, ngx_pool_t *pool, ngx_log_t *log, goto failed; } + /* + * set by ngx_pcalloc(): + * + * fetch->http.proxy.state = HTTP_STATE_DIRECT; + */ + http = &fetch->http; http->pool = pool; @@ -1425,7 +1347,7 @@ ngx_js_request_constructor(njs_vm_t *vm, ngx_js_request_t *request, u->url.len = request->url.len; u->url.data = request->url.data; - u->default_port = 80; + u->default_port = NGX_JS_HTTP_DEFAULT_PORT; u->uri_part = 1; u->no_resolve = 1; @@ -1441,7 +1363,7 @@ ngx_js_request_constructor(njs_vm_t *vm, ngx_js_request_t *request, { u->url.len -= 8; u->url.data += 8; - u->default_port = 443; + u->default_port = NGX_JS_HTTPS_DEFAULT_PORT; #endif } else { diff --git a/nginx/ngx_js_http.c b/nginx/ngx_js_http.c index 35c67842f..32c594613 100644 --- a/nginx/ngx_js_http.c +++ b/nginx/ngx_js_http.c @@ -24,6 +24,7 @@ typedef struct { #define ngx_js_http_version(major, minor) ((major) * 1000 + (minor)) +#define NGX_JS_USER_AGENT "nginx-js" static void ngx_js_http_resolve_handler(ngx_resolver_ctx_t *ctx); @@ -47,11 +48,16 @@ static ngx_int_t ngx_js_http_parse_header_line(ngx_js_http_parse_t *hp, 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_fetch_append_request_headers(njs_chb_t *chain, + ngx_js_request_t *request, njs_bool_t is_proxy); + #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 ngx_int_t ngx_js_http_ssl_name(ngx_js_http_t *http); +static void ngx_js_http_build_connect_request(ngx_js_http_t *http); +static ngx_int_t ngx_js_http_process_connect_response(ngx_js_http_t *http); #endif @@ -75,7 +81,7 @@ ngx_js_http_error(ngx_js_http_t *http, const char *fmt, ...) ngx_resolver_ctx_t * ngx_js_http_resolve(ngx_js_http_t *http, ngx_resolver_t *r, ngx_str_t *host, - in_port_t port, ngx_msec_t timeout) + ngx_msec_t timeout) { ngx_int_t ret; ngx_resolver_ctx_t *ctx; @@ -90,7 +96,6 @@ ngx_js_http_resolve(ngx_js_http_t *http, ngx_resolver_t *r, ngx_str_t *host, } http->ctx = ctx; - http->port = port; ctx->name = *host; ctx->handler = ngx_js_http_resolve_handler; @@ -163,7 +168,7 @@ ngx_js_http_resolve_handler(ngx_resolver_ctx_t *ctx) } ngx_memcpy(sockaddr, ctx->addrs[i].sockaddr, socklen); - ngx_inet_set_port(sockaddr, http->port); + ngx_inet_set_port(sockaddr, http->connect_port); http->addrs[i].sockaddr = sockaddr; http->addrs[i].socklen = socklen; @@ -298,16 +303,37 @@ ngx_js_http_connect(ngx_js_http_t *http) 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(c->read, http->conf->timeout); ngx_add_timer(c->write, http->conf->timeout); #if (NGX_SSL) - if (http->ssl != NULL && c->ssl == NULL) { + if (ngx_js_conf_proxy(http->conf) && http->ssl != NULL && c->ssl == NULL) { + http->proxy.pending = ngx_js_chain_to_buf(http->pool, &http->chain); + if (http->proxy.pending == NULL) { + ngx_js_http_error(http, "memory error"); + return; + } + + njs_chb_destroy(&http->chain); + ngx_js_http_build_connect_request(http); + + http->proxy.state = HTTP_STATE_PROXY_CONNECT_PENDING; + http->process = ngx_js_http_process_connect_response; + + } else { + if (ngx_js_conf_proxy(http->conf) && http->ssl != NULL && c->ssl != NULL) { + http->proxy.state = HTTP_STATE_PROXY_TUNNEL_READY; + } + + http->process = ngx_js_http_process_status_line; + } + + if (http->ssl != NULL && c->ssl == NULL && !ngx_js_conf_proxy(http->conf)) { ngx_js_http_ssl_init_connection(http); return; } +#else + http->process = ngx_js_http_process_status_line; #endif if (rc == NGX_OK) { @@ -406,6 +432,15 @@ ngx_js_http_ssl_handshake(ngx_js_http_t *http) ngx_post_event(c->read, &ngx_posted_events); } + if (http->proxy.state == HTTP_STATE_PROXY_TUNNEL_READY) { + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, http->log, 0, + "js http send origin request"); + + http->buffer = http->proxy.pending; + http->proxy.pending = NULL; + http->proxy.state = HTTP_STATE_ORIGIN_REQUEST_SENT; + } + http->process = ngx_js_http_process_status_line; ngx_js_http_write_handler(c->write); @@ -467,6 +502,104 @@ ngx_js_http_ssl_name(ngx_js_http_t *http) return NGX_OK; } + +static void +ngx_js_http_build_connect_request(ngx_js_http_t *http) +{ + NGX_CHB_CTX_INIT(&http->chain, http->pool); + + njs_chb_append_literal(&http->chain, "CONNECT "); + + njs_chb_append(&http->chain, http->host.data, http->host.len); + njs_chb_sprintf(&http->chain, 32, ":%d HTTP/1.1" CRLF, http->port); + + njs_chb_append_literal(&http->chain, "Host: "); + + njs_chb_append(&http->chain, http->host.data, http->host.len); + njs_chb_sprintf(&http->chain, 32, ":%d" CRLF, http->port); + + if (http->proxy.auth.len != 0) { + njs_chb_append(&http->chain, http->proxy.auth.data, + http->proxy.auth.len); + njs_chb_append_literal(&http->chain, CRLF); + } +} + + +static ngx_int_t +ngx_js_http_process_connect_response(ngx_js_http_t *http) +{ + ngx_int_t rc; + ngx_buf_t *b; + ngx_js_http_parse_t *hp; + + b = http->buffer; + hp = &http->http_parse; + + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, http->log, 0, + "js http process CONNECT response"); + + rc = ngx_js_http_parse_status_line(hp, b); + + if (rc == NGX_AGAIN) { + return NGX_AGAIN; + } + + if (rc != NGX_OK) { + ngx_js_http_error(http, "proxy CONNECT: invalid status line"); + return NGX_ERROR; + } + + if (hp->code != 200) { + ngx_js_http_error(http, "proxy CONNECT failed with status %ui", + hp->code); + return NGX_ERROR; + } + + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, + "js http proxy CONNECT status: %ui", hp->code); + + for (;;) { + rc = ngx_js_http_parse_header_line(hp, b); + + if (rc == NGX_OK) { + ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0, + "js http CONNECT header: \"%*s\"", + hp->header_end - hp->header_name_start, + hp->header_name_start); + continue; + } + + if (rc == NGX_DONE) { + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, http->log, 0, + "js http proxy tunnel established"); + break; + } + + if (rc == NGX_AGAIN) { + return NGX_AGAIN; + } + + ngx_js_http_error(http, "proxy CONNECT: invalid headers"); + return NGX_ERROR; + } + + http->proxy.state = HTTP_STATE_PROXY_TUNNEL_READY; + + ngx_memzero(hp, sizeof(*hp)); + + if (http->ssl == NULL) { + ngx_js_http_error(http, "proxy CONNECT: SSL not configured"); + return NGX_ERROR; + } + + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, http->log, 0, + "js http init SSL through proxy tunnel"); + + ngx_js_http_ssl_init_connection(http); + return NGX_OK; +} + #endif @@ -510,7 +643,9 @@ ngx_js_http_write_handler(ngx_event_t *wev) } #if (NGX_SSL) - if (http->ssl != NULL && http->peer.connection->ssl == NULL) { + if (http->ssl != NULL && http->peer.connection->ssl == NULL + && !ngx_js_conf_proxy(http->conf)) + { ngx_js_http_ssl_init_connection(http); return; } @@ -519,21 +654,12 @@ ngx_js_http_write_handler(ngx_event_t *wev) b = http->buffer; if (b == NULL) { - size = njs_chb_size(&http->chain); - if (size < 0) { - ngx_js_http_error(http, "memory error"); - return; - } - - b = ngx_create_temp_buf(http->pool, size); + b = ngx_js_chain_to_buf(http->pool, &http->chain); if (b == NULL) { ngx_js_http_error(http, "memory error"); return; } - njs_chb_join_to(&http->chain, b->last); - b->last += size; - http->buffer = b; } @@ -1857,3 +1983,210 @@ ngx_js_http_keepalive_close_handler(ngx_event_t *ev) ngx_queue_remove(&cache->queue); ngx_queue_insert_head(&conf->fetch_keepalive_free, &cache->queue); } + + +ngx_buf_t * +ngx_js_chain_to_buf(ngx_pool_t *pool, njs_chb_t *chain) +{ + ssize_t size; + ngx_buf_t *buf; + + size = njs_chb_size(chain); + if (size < 0) { + return NULL; + } + + buf = ngx_create_temp_buf(pool, size); + if (buf == NULL) { + return NULL; + } + + njs_chb_join_to(chain, buf->last); + buf->last += size; + + return buf; +} + + +static void +ngx_js_fetch_append_request_headers(njs_chb_t *chain, + ngx_js_request_t *request, njs_bool_t is_proxy) +{ + ngx_uint_t i; + ngx_list_part_t *part; + ngx_js_tb_elt_t *h; + + part = &request->headers.header_list.part; + h = part->elts; + + for (i = 0; /* void */; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + h = part->elts; + i = 0; + } + + if (h[i].hash == 0) { + continue; + } + + if (h[i].key.len == 4 + && ngx_strncasecmp(h[i].key.data, (u_char *) "Host", 4) == 0) + { + continue; + } + + if (h[i].key.len == 14 + && ngx_strncasecmp(h[i].key.data, (u_char *) "Content-Length", + 14) == 0) + { + continue; + } + + if (h[i].key.len == 10 + && ngx_strncasecmp(h[i].key.data, (u_char *) "Connection", 10) + == 0) + { + continue; + } + + if (is_proxy && h[i].key.len == 19 + && ngx_strncasecmp(h[i].key.data, (u_char *) "Proxy-Authorization", + 19) == 0) + { + continue; + } + + njs_chb_append(chain, h[i].key.data, h[i].key.len); + njs_chb_append_literal(chain, ": "); + njs_chb_append(chain, h[i].value.data, h[i].value.len); + njs_chb_append_literal(chain, CRLF); + } +} + + +void +ngx_js_fetch_build_request(ngx_js_http_t *http, ngx_js_request_t *request, + ngx_str_t *path, ngx_url_t *u, njs_bool_t is_proxy) +{ + ngx_str_t method; + ngx_uint_t i; + njs_bool_t has_host, has_user_agent; + ngx_list_part_t *part; + ngx_js_tb_elt_t *h; + + njs_chb_append(&http->chain, request->method.data, request->method.len); + njs_chb_append_literal(&http->chain, " "); + + if (is_proxy) { + njs_chb_append_literal(&http->chain, "http://"); + njs_chb_append(&http->chain, http->host.data, http->host.len); + + if (http->port != u->default_port) { + njs_chb_sprintf(&http->chain, 32, ":%d", http->port); + } + } + + if (path->len == 0 || path->data[0] != '/') { + njs_chb_append_literal(&http->chain, "/"); + } + + njs_chb_append(&http->chain, path->data, path->len); + njs_chb_append_literal(&http->chain, " HTTP/1.1" CRLF); + + has_host = 0; + has_user_agent = 0; + part = &request->headers.header_list.part; + h = part->elts; + + for (i = 0; /* void */; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + h = part->elts; + i = 0; + } + + if (h[i].hash == 0) { + continue; + } + + if (h[i].key.len == 4 + && ngx_strncasecmp(h[i].key.data, (u_char *) "Host", 4) == 0) + { + has_host = 1; + njs_chb_append_literal(&http->chain, "Host: "); + njs_chb_append(&http->chain, h[i].value.data, h[i].value.len); + njs_chb_append_literal(&http->chain, CRLF); + continue; + } + + if (h[i].key.len == 10 + && ngx_strncasecmp(h[i].key.data, (u_char *) "User-Agent", 10) == 0) + { + has_user_agent = 1; + njs_chb_append_literal(&http->chain, "User-Agent: "); + njs_chb_append(&http->chain, h[i].value.data, h[i].value.len); + njs_chb_append_literal(&http->chain, CRLF); + continue; + } + } + + if (!has_host) { + njs_chb_append_literal(&http->chain, "Host: "); + njs_chb_append(&http->chain, u->host.data, u->host.len); + + if (!u->no_port) { + njs_chb_sprintf(&http->chain, 32, ":%d", u->port); + } + + njs_chb_append_literal(&http->chain, CRLF); + } + + if (!has_user_agent) { + njs_chb_append_literal(&http->chain, "User-Agent: "); + njs_chb_append(&http->chain, (u_char *) NGX_JS_USER_AGENT, + sizeof(NGX_JS_USER_AGENT) - 1); + njs_chb_append_literal(&http->chain, CRLF); + } + + if (is_proxy && http->proxy.auth.len != 0) { + njs_chb_append(&http->chain, http->proxy.auth.data, + http->proxy.auth.len); + } + + ngx_js_fetch_append_request_headers(&http->chain, request, is_proxy); + + 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, + request->body.len); + njs_chb_append(&http->chain, request->body.data, request->body.len); + + } else { + method = request->method; + + if ((method.len == 4 + && (ngx_strncasecmp(method.data, (u_char *) "POST", 4) == 0)) + || (method.len == 3 + && ngx_strncasecmp(method.data, (u_char *) "PUT", 3) == 0)) + { + njs_chb_append_literal(&http->chain, "Content-Length: 0" CRLF CRLF); + + } else { + njs_chb_append_literal(&http->chain, CRLF); + } + } +} diff --git a/nginx/ngx_js_http.h b/nginx/ngx_js_http.h index acaa58027..93fa1966d 100644 --- a/nginx/ngx_js_http.h +++ b/nginx/ngx_js_http.h @@ -12,6 +12,10 @@ #define NGX_JS_HOST_MAX_LEN 256 +#define NGX_JS_HTTP_DEFAULT_PORT 80 +#define NGX_JS_HTTPS_DEFAULT_PORT 443 + +#define ngx_js_https(u) ((u)->default_port == NGX_JS_HTTPS_DEFAULT_PORT) typedef struct ngx_js_http_s ngx_js_http_t; @@ -116,6 +120,7 @@ struct ngx_js_http_s { ngx_uint_t naddr; ngx_str_t host; in_port_t port; + in_port_t connect_port; ngx_peer_connection_t peer; @@ -151,11 +156,25 @@ struct ngx_js_http_s { void (*ready_handler)(ngx_js_http_t *http); void (*error_handler)(ngx_js_http_t *http, const char *err); + + struct { + enum { + HTTP_STATE_DIRECT = 0, + HTTP_STATE_PROXY_CONNECT_PENDING, + HTTP_STATE_PROXY_TUNNEL_READY, + HTTP_STATE_ORIGIN_REQUEST_SENT + } state; + + ngx_buf_t *pending; +#define ngx_js_http_proxy(http) ((http)->proxy.url != NULL) + ngx_url_t *url; + ngx_str_t auth; + } proxy; }; ngx_resolver_ctx_t *ngx_js_http_resolve(ngx_js_http_t *http, ngx_resolver_t *r, - ngx_str_t *host, in_port_t port, ngx_msec_t timeout); + ngx_str_t *host, ngx_msec_t timeout); void ngx_js_http_connect(ngx_js_http_t *http); void ngx_js_http_resolve_done(ngx_js_http_t *http); void ngx_js_http_close_peer(ngx_js_http_t *http); @@ -163,5 +182,10 @@ void ngx_js_http_trim(u_char **value, size_t *len, int trim_c0_control_or_space); ngx_int_t ngx_js_check_header_name(u_char *name, size_t len); +ngx_buf_t *ngx_js_chain_to_buf(ngx_pool_t *pool, njs_chb_t *chain); + +void ngx_js_fetch_build_request(ngx_js_http_t *http, ngx_js_request_t *request, + ngx_str_t *path, ngx_url_t *u, njs_bool_t is_proxy); + #endif /* _NGX_JS_HTTP_H_INCLUDED_ */ diff --git a/nginx/ngx_qjs_fetch.c b/nginx/ngx_qjs_fetch.c index a211e0fef..0b3f3269c 100644 --- a/nginx/ngx_qjs_fetch.c +++ b/nginx/ngx_qjs_fetch.c @@ -232,19 +232,15 @@ JSValue ngx_qjs_ext_fetch(JSContext *cx, JSValueConst this_val, int argc, JSValueConst *argv) { - int has_host; void *external; JSValue init, value, promise; ngx_int_t rc; ngx_url_t u; - ngx_str_t method; - ngx_uint_t i; + ngx_str_t *resolve_host; ngx_pool_t *pool; ngx_js_ctx_t *ctx; ngx_js_http_t *http; ngx_qjs_fetch_t *fetch; - ngx_list_part_t *part; - ngx_js_tb_elt_t *h; ngx_connection_t *c; ngx_js_request_t request; ngx_resolver_ctx_t *rs; @@ -280,7 +276,7 @@ ngx_qjs_ext_fetch(JSContext *cx, JSValueConst this_val, int argc, ngx_qjs_external_max_response_buffer_size(cx, external); #if (NGX_SSL) - if (u.default_port == 443) { + if (ngx_js_https(&u)) { http->ssl = ngx_qjs_external_ssl(cx, external); http->ssl_verify = ngx_qjs_external_ssl_verify(cx, external); } @@ -337,131 +333,45 @@ ngx_qjs_ext_fetch(JSContext *cx, JSValueConst this_val, int argc, NJS_CHB_MP_INIT(&http->chain, ctx->engine->pool); NJS_CHB_MP_INIT(&http->response.chain, ctx->engine->pool); - njs_chb_append(&http->chain, request.method.data, request.method.len); - njs_chb_append_literal(&http->chain, " "); + resolve_host = NULL; + http->connect_port = http->port; - if (u.uri.len == 0 || u.uri.data[0] != '/') { - njs_chb_append_literal(&http->chain, "/"); - } - - njs_chb_append(&http->chain, u.uri.data, u.uri.len); - njs_chb_append_literal(&http->chain, " HTTP/1.1" CRLF); - - has_host = 0; - part = &request.headers.header_list.part; - h = part->elts; - - for (i = 0; /* void */; i++) { - - if (i >= part->nelts) { - if (part->next == NULL) { - break; - } - - part = part->next; - h = part->elts; - i = 0; - } - - if (h[i].hash == 0) { - continue; - } - - if (h[i].key.len == 4 - && ngx_strncasecmp(h[i].key.data, (u_char *) "Host", 4) == 0) - { - has_host = 1; - njs_chb_append_literal(&http->chain, "Host: "); - njs_chb_append(&http->chain, h[i].value.data, h[i].value.len); - njs_chb_append_literal(&http->chain, CRLF); - break; - } - } - - if (!has_host) { - njs_chb_append_literal(&http->chain, "Host: "); - njs_chb_append(&http->chain, u.host.data, u.host.len); - - if (!u.no_port) { - njs_chb_sprintf(&http->chain, 32, ":%d", u.port); - } - - njs_chb_append_literal(&http->chain, CRLF); - } - - part = &request.headers.header_list.part; - h = part->elts; - - for (i = 0; /* void */; i++) { - - if (i >= part->nelts) { - if (part->next == NULL) { - break; + if (ngx_js_conf_proxy(http->conf)) { + if (ngx_js_conf_dynamic_proxy(http->conf)) { + if (http->conf->eval_proxy_url(http->pool, external, http->conf, + &http->proxy.url, &http->proxy.auth) + != NGX_OK) + { + JS_ThrowInternalError(cx, "failed to evaluate proxy URL"); + return JS_EXCEPTION; } - part = part->next; - h = part->elts; - i = 0; - } - - if (h[i].hash == 0) { - continue; - } - - if (h[i].key.len == 4 - && ngx_strncasecmp(h[i].key.data, (u_char *) "Host", 4) == 0) - { - continue; - } - - if (h[i].key.len == 14 - && ngx_strncasecmp(h[i].key.data, (u_char *) "Content-Length", 14) - == 0) - { - continue; + } else { + http->proxy.url = http->conf->fetch_proxy_url; + http->proxy.auth = http->conf->fetch_proxy_auth_header; } - if (h[i].key.len == 10 - && ngx_strncasecmp(h[i].key.data, (u_char *) "Connection", 10) - == 0) - { - continue; + if (ngx_js_http_proxy(http) && http->proxy.url->addrs == NULL) { + resolve_host = &http->proxy.url->host; + http->connect_port = http->proxy.url->port; } - - 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); } - if (!http->keepalive) { - njs_chb_append_literal(&http->chain, "Connection: close" CRLF); + if (!ngx_js_http_proxy(http) && u.addrs == NULL) { + resolve_host = &u.host; } - if (request.body.len != 0) { - njs_chb_sprintf(&http->chain, 32, "Content-Length: %uz" CRLF CRLF, - request.body.len); - njs_chb_append(&http->chain, request.body.data, request.body.len); - - } else { - method = request.method; - - if ((method.len == 4 - && (ngx_strncasecmp(method.data, (u_char *) "POST", 4) == 0)) - || (method.len == 3 - && ngx_strncasecmp(method.data, (u_char *) "PUT", 3) == 0)) - { - njs_chb_append_literal(&http->chain, "Content-Length: 0" CRLF CRLF); + ngx_js_fetch_build_request(http, &request, &u.uri, &u, + ngx_js_http_proxy(http) && !ngx_js_https(&u)); - } else { - njs_chb_append_literal(&http->chain, CRLF); - } - } + if (resolve_host != NULL) { + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, http->log, 0, + "js http fetch: resolving"); - if (u.addrs == NULL) { rs = ngx_js_http_resolve(http, ngx_qjs_external_resolver(cx, external), - &u.host, u.port, + resolve_host, ngx_qjs_external_resolver_timeout(cx, external)); + if (rs == NULL) { JS_FreeValue(cx, promise); return JS_ThrowOutOfMemory(cx); @@ -476,9 +386,15 @@ ngx_qjs_ext_fetch(JSContext *cx, JSValueConst this_val, int argc, } http->naddrs = 1; - ngx_memcpy(&http->addr, &u.addrs[0], sizeof(ngx_addr_t)); http->addrs = &http->addr; + if (ngx_js_http_proxy(http)) { + ngx_memcpy(&http->addr, &http->proxy.url->addrs[0], sizeof(ngx_addr_t)); + + } else { + ngx_memcpy(&http->addr, &u.addrs[0], sizeof(ngx_addr_t)); + } + ngx_js_http_connect(http); return promise; @@ -662,7 +578,7 @@ ngx_qjs_request_ctor(JSContext *cx, ngx_js_request_t *request, ngx_memzero(u, sizeof(ngx_url_t)); u->url = request->url; - u->default_port = 80; + u->default_port = NGX_JS_HTTP_DEFAULT_PORT; u->uri_part = 1; u->no_resolve = 1; @@ -678,7 +594,7 @@ ngx_qjs_request_ctor(JSContext *cx, ngx_js_request_t *request, { u->url.len -= 8; u->url.data += 8; - u->default_port = 443; + u->default_port = NGX_JS_HTTPS_DEFAULT_PORT; #endif } else { @@ -1213,6 +1129,12 @@ ngx_qjs_fetch_alloc(JSContext *cx, ngx_pool_t *pool, ngx_log_t *log, return NULL; } + /* + * set by ngx_pcalloc(): + * + * fetch->http.proxy.state = HTTP_STATE_DIRECT; + */ + http = &fetch->http; http->pool = pool; diff --git a/nginx/ngx_stream_js_module.c b/nginx/ngx_stream_js_module.c index 212af4003..abc76e842 100644 --- a/nginx/ngx_stream_js_module.c +++ b/nginx/ngx_stream_js_module.c @@ -17,9 +17,11 @@ typedef struct ngx_stream_js_ctx_s ngx_stream_js_ctx_t; typedef struct { NGX_JS_COMMON_LOC_CONF; - ngx_str_t access; - ngx_str_t preread; - ngx_str_t filter; + ngx_stream_complex_value_t fetch_proxy_cv; + + ngx_str_t access; + ngx_str_t preread; + ngx_str_t filter; } ngx_stream_js_srv_conf_t; @@ -222,6 +224,8 @@ static char *ngx_stream_js_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child); static char *ngx_stream_js_shared_dict_zone(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); +static char *ngx_stream_js_fetch_proxy(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); static ngx_conf_bitmask_t ngx_stream_js_engines[] = { @@ -425,6 +429,13 @@ static ngx_command_t ngx_stream_js_commands[] = { 0, NULL }, + { ngx_string("js_fetch_proxy"), + NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1, + ngx_stream_js_fetch_proxy, + NGX_STREAM_SRV_CONF_OFFSET, + 0, + NULL }, + ngx_null_command }; @@ -3683,6 +3694,62 @@ ngx_stream_js_shared_dict_zone(ngx_conf_t *cf, ngx_command_t *cmd, } +static ngx_int_t +ngx_stream_js_eval_proxy_url(ngx_pool_t *pool, void *request, + void *module_conf, ngx_url_t **url_out, ngx_str_t *auth_out) +{ + ngx_str_t value; + ngx_stream_session_t *s; + ngx_stream_js_srv_conf_t *jscf; + + s = request; + jscf = module_conf; + + if (ngx_stream_complex_value(s, &jscf->fetch_proxy_cv, &value) + != NGX_OK) + { + return NGX_ERROR; + } + + return ngx_js_parse_proxy_url(pool, s->connection->log, &value, + url_out, auth_out); +} + + +static char * +ngx_stream_js_fetch_proxy(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_str_t *value; + ngx_uint_t n; + ngx_stream_js_srv_conf_t *jscf; + ngx_stream_compile_complex_value_t ccv; + + value = cf->args->elts; + + n = ngx_stream_script_variables_count(&value[1]); + + if (n) { + ngx_memzero(&ccv, sizeof(ngx_stream_compile_complex_value_t)); + + jscf = conf; + + ccv.cf = cf; + ccv.value = &value[1]; + ccv.complex_value = &jscf->fetch_proxy_cv; + + if (ngx_stream_compile_complex_value(&ccv) != NGX_OK) { + return NGX_CONF_ERROR; + } + + jscf->eval_proxy_url = ngx_stream_js_eval_proxy_url; + + return NGX_CONF_OK; + } + + return ngx_js_fetch_proxy(cf, cmd, conf); +} + + static ngx_int_t ngx_stream_js_init(ngx_conf_t *cf) { diff --git a/nginx/t/js_fetch.t b/nginx/t/js_fetch.t index 76d9238d2..9bbacf5bc 100644 --- a/nginx/t/js_fetch.t +++ b/nginx/t/js_fetch.t @@ -92,6 +92,10 @@ http { js_content test.host_header; } + location /user_agent_header { + js_content test.user_agent_header; + } + location /header_iter { js_content test.header_iter; } @@ -122,6 +126,10 @@ http { location /host { return 200 $http_host; } + + location /user_agent { + return 200 $http_user_agent; + } } } @@ -318,6 +326,17 @@ $t->write_file('test.js', <write_file('test.js', <try_run('no njs.fetch'); -$t->plan(38); +$t->plan(40); $t->run_daemon(\&http_daemon, port(8082)); $t->waitforsocket('127.0.0.1:' . port(8082)); @@ -527,6 +547,12 @@ like(http_get('/body_content_length'), qr/200 OK/s, } +like(http_get('/user_agent_header'), + qr/200 OK.*nginx-js$/s, + 'fetch default user-agent header'); +like(http_get('/user_agent_header?ua=My-User-Agent'), + qr/200 OK.*My-User-Agent$/s, 'fetch user-agent header'); + ############################################################################### sub has_version { diff --git a/nginx/t/js_fetch_https.t b/nginx/t/js_fetch_https.t index 42b5acbb9..43dcb9f80 100644 --- a/nginx/t/js_fetch_https.t +++ b/nginx/t/js_fetch_https.t @@ -241,8 +241,15 @@ sub reply_handler { my $name = join('.', @name); - if ($type == A) { - push @rdata, rd_addr($ttl, '127.0.0.1'); + if ($name eq 'default.example.com' || $name eq '1.example.com') { + if ($type == A) { + push @rdata, rd_addr($ttl, '127.0.0.1'); + } + + } elsif ($name eq 'localhost') { + if ($type == A) { + push @rdata, rd_addr($ttl, '127.0.0.1'); + } } $len = @name; diff --git a/nginx/t/js_fetch_proxy.t b/nginx/t/js_fetch_proxy.t new file mode 100644 index 000000000..8b2a71f11 --- /dev/null +++ b/nginx/t/js_fetch_proxy.t @@ -0,0 +1,193 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) F5, Inc. + +# Tests for http njs module, fetch method with forward proxy. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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 /http_via_proxy { + js_fetch_proxy http://testuser:testpass@127.0.0.1:%%PORT_8081%%; + js_content test.http_fetch; + } + + location /http_no_proxy { + js_content test.http_fetch; + } + + location /http_via_proxy_no_auth { + js_fetch_proxy http://127.0.0.1:%%PORT_8081%%; + js_content test.http_fetch; + } + + location /http_via_proxy_bad_auth { + js_fetch_proxy http://wronguser:wrongpass@127.0.0.1:%%PORT_8081%%; + js_content test.http_fetch_status; + } + + location /http_via_proxy_encoded { + js_fetch_proxy http://user%40domain:p%40ss%3Aword@127.0.0.1:%%PORT_8081%%; + js_content test.http_fetch; + } + } + + server { + listen 127.0.0.1:%%PORT_8081%%; + server_name localhost; + + location = /test { + js_content test.endpoint; + } + } +} + +EOF + +my $p = port(8081); + +$t->write_file('test.js', <try_run('no js_fetch_proxy')->plan(11); + +############################################################################### + +my $resp = http_get('/http_via_proxy?domain=127.0.0.1'); +like($resp, qr/METHOD: GET/, 'proxy received GET method'); +like($resp, qr/URI: http:\/\/127\.0\.0\.1:$p\/test/, + 'proxy received absolute-form URI'); +like($resp, qr/PROXY-AUTH: Basic\s+dGVzdHVzZXI6dGVzdHBhc3M=/, + 'proxy Proxy-Authorization has expected Basic credentials'); + +$resp = http_post('/http_via_proxy?domain=127.0.0.1'); +like($resp, qr/METHOD: POST/, 'proxy received POST method'); +like($resp, qr/BODY: REQ-BODY/, 'proxy received request body'); + +like(http_get('/http_via_proxy?domain=example.com'), + qr/URI: http:\/\/example\.com:/, 'proxy received example.com URI'); + +$resp = http_get('/http_via_proxy_no_auth?domain=127.0.0.1'); +like($resp, qr/URI: http:\/\/127\.0\.0\.1:$p\/test/, + 'proxy received absolute-form URI'); +unlike($resp, qr/PROXY-AUTH:/, 'proxy received no Proxy-Authorization header'); + +like(http_get('/http_via_proxy_bad_auth'), qr/STATUS:407/, + 'Proxy-Authorization is invalid'); + +like(http_get('/http_no_proxy?domain=127.0.0.1'), + qr/ORIGIN:response/, 'origin response without proxy'); + +like(http_get('/http_via_proxy_encoded?domain=127.0.0.1'), + qr/PROXY-AUTH: Basic\s+dXNlckBkb21haW46cEBzczp3b3Jk/, + 'encoded username and password with special chars decoded correctly'); + +############################################################################### + +sub http_post { + my ($url, %extra) = @_; + + my $p = "POST $url HTTP/1.0" . CRLF . + "Host: localhost" . CRLF . + "Content-Length: 8" . CRLF . + CRLF . + "REQ-BODY"; + + return http($p, %extra); +} + +############################################################################### diff --git a/nginx/t/js_fetch_proxy_https.t b/nginx/t/js_fetch_proxy_https.t new file mode 100644 index 000000000..6589cd3bc --- /dev/null +++ b/nginx/t/js_fetch_proxy_https.t @@ -0,0 +1,481 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) F5, Inc. + +# Tests for http njs module, fetch method with HTTPS through forward proxy. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use Socket qw/ CRLF SOCK_STREAM /; +use IO::Select; + +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/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_import test.js; + + resolver 127.0.0.1:%%PORT_8981_UDP%%; + resolver_timeout 1s; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + js_fetch_trusted_certificate myca.crt; + + location /engine { + js_content test.engine; + } + + location /https_via_proxy { + js_fetch_proxy http://user:pass@forward.proxy.net:%%PORT_8082%%; + js_content test.https_fetch; + } + + location /https_via_broken_proxy { + js_fetch_proxy http://user:pass@nonexistent.domain:%%PORT_8082%%; + js_content test.https_fetch; + } + + location /https_no_proxy { + js_content test.https_fetch; + } + } + + server { + listen 127.0.0.1:%%PORT_8083%% ssl; + server_name example.com; + + ssl_certificate example.com.chained.crt; + ssl_certificate_key example.com.key; + + location = /test { + js_content test.endpoint; + } + } +} + +EOF + +my $p2 = port(8082); +my $p3 = port(8083); + +$t->write_file('test.js', <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', '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 ('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 js_fetch_proxy')->plan(8); + +$t->run_daemon(\&https_proxy_daemon, $p2); +$t->run_daemon(\&dns_daemon, port(8981), $t); +$t->waitforsocket('127.0.0.1:' . $p2); +$t->waitforfile($t->testdir . '/' . port(8981)); + +############################################################################### + +my $resp = http_get('/https_via_proxy?domain=example.com'); +like($resp, qr/METHOD: GET/, 'https through proxy received GET method'); +like($resp, qr/URI: \/test/, 'https through proxy received /test URI'); +like($resp, qr/ORIGIN$/, 'https through proxy origin response'); + +$resp = http_post('/https_via_proxy?domain=example.com'); +like($resp, qr/METHOD: POST/, 'https through proxy received POST method'); +like($resp, qr/BODY: REQ-BODY/, 'https through proxy received request body'); + +like(http_get('/https_no_proxy?domain=example.com'), qr/ORIGIN/, + 'https without proxy'); + +like(http_get('/https_via_proxy?domain=nonexistent.dest.domain'), + qr/connect failed/, 'https through proxy nonexistent.dest.domain'); +like(http_get('/https_via_broken_proxy?domain=example.com'), + qr/\"nonexistent.domain\" could not be res/, 'https through broken proxy'); + +############################################################################### + +sub https_proxy_daemon { + my ($port) = @_; + + my $server = IO::Socket::INET->new( + Proto => 'tcp', + LocalAddr => "127.0.0.1:$port", + Listen => 128, + Reuse => 1 + ) or die "Can't create listening socket: $!\n"; + + local $SIG{PIPE} = 'IGNORE'; + + my $s = { + sel => IO::Select->new($server), + pending => {}, # fd -> { sock, buf } + conn => {}, # client fd -> { client, origin } + o2c => {}, # origin fd -> client fd + }; + + while (1) { + my @ready = $s->{sel}->can_read(1.0); + for my $sock (@ready) { + if ($sock == $server) { + my $client = $server->accept(); + next unless $client; + $client->autoflush(1); + $s->{sel}->add($client); + $s->{pending}->{ fileno($client) } = { + sock => $client, + buf => '', + }; + + next; + } + + my $fd = fileno($sock); + next unless defined $fd; + + if (exists $s->{o2c}->{$fd}) { + my $buf; + my $cfd = $s->{o2c}->{$fd}; + my $client = $s->{conn}->{$cfd}{client}; + + my $n = sysread($sock, $buf, 4096); + if (!defined($n) || $n == 0) { + _cleanup($s, $client, $sock); + next; + } + + syswrite($client, $buf); + next; + } + + if (exists $s->{conn}->{$fd}) { + my $buf; + my $origin = $s->{conn}->{$fd}{origin}; + + my $n = sysread($sock, $buf, 4096); + if (!defined($n) || $n == 0) { + _cleanup($s, $sock, $origin); + next; + } + + syswrite($origin, $buf); + next; + } + + if (exists $s->{pending}->{$fd}) { + my $buf; + my $p = $s->{pending}->{$fd}; + + my $n = sysread($sock, $buf, 4096); + if (!defined($n) || $n == 0) { + $s->{sel}->remove($sock); + delete $s->{pending}->{$fd}; + close $sock; + next; + } + + $p->{buf} .= $buf; + + if ($p->{buf} =~ /(\x0d\x0a)\1/s) { + my $method = '', my $proxy_auth = '', my $target = ''; + + my ($headers) = split(/\r\n\r\n/, $p->{buf}, 2); + for my $line (split(/\r\n/, $headers)) { + if ($method eq '' && $line =~ /^(\S+)\s+(\S+)\s+HTTP/i) { + $method = $1; + $target = $2; + next; + } + + if ($line =~ /^Proxy-Authorization:\s*(.+?)\s*$/i) { + $proxy_auth = $1; + next; + } + } + + if (uc($method) eq 'CONNECT' + && $proxy_auth =~ /^Basic\s+dXNlcjpwYXNz$/i) + { + print $sock "HTTP/1.1 200 established" . CRLF . CRLF; + + my ($host, $port) = split(/:/, $target); + + my $origin = IO::Socket::INET->new( + PeerAddr => "127.0.0.1:$port", + Proto => 'tcp', + Type => SOCK_STREAM + ); + + if (!$origin) { + $s->{sel}->remove($sock); + delete $s->{pending}->{$fd}; + close $sock; + next; + } + + $origin->autoflush(1); + $s->{sel}->add($origin); + + $s->{conn}->{$fd} = { + client => $sock, + origin => $origin, + }; + + $s->{o2c}->{ fileno($origin) } = $fd; + delete $s->{pending}->{$fd}; + + } else { + print $sock + "HTTP/1.1 407 Proxy Auth Required" . CRLF . + "Proxy-Authenticate: Basic realm=\"proxy\"" . CRLF . + "Content-Length: 0" . CRLF . + "Connection: close" . CRLF . CRLF; + $s->{sel}->remove($sock); + delete $s->{pending}->{$fd}; + close $sock; + } + } + + next; + } + + $s->{sel}->remove($sock); + close $sock; + } + } +} + +sub _cleanup { + my ($s, $client, $origin) = @_; + + my $cfd = fileno($client); + my $ofd = fileno($origin); + + delete $s->{o2c}->{$ofd} if defined $ofd; + + if (defined $cfd) { + delete $s->{conn}->{$cfd}; + delete $s->{pending}->{$cfd} if exists $s->{pending}->{$cfd}; + } + + if ($client) { + $s->{sel}->remove($client); + close $client; + } + + if ($origin) { + $s->{sel}->remove($origin); + close $origin; + } +} + +############################################################################### + +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 ($name eq 'example.com' || $name eq 'forward.proxy.net') { + 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); + } +} + +############################################################################### + +sub http_post { + my ($url, %extra) = @_; + + my $p = "POST $url HTTP/1.0" . CRLF . + "Host: localhost" . CRLF . + "Content-Length: 8" . CRLF . + CRLF . + "REQ-BODY"; + + return http($p, %extra); +} + +############################################################################### diff --git a/nginx/t/js_fetch_proxy_keepalive.t b/nginx/t/js_fetch_proxy_keepalive.t new file mode 100644 index 000000000..422cf1b71 --- /dev/null +++ b/nginx/t/js_fetch_proxy_keepalive.t @@ -0,0 +1,459 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) F5, Inc. + +# Tests for http njs module, fetch method with keepalive and forward proxy. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use Socket qw/ CRLF SOCK_STREAM /; +use IO::Select; + +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/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_import test.js; + + resolver 127.0.0.1:%%PORT_8981_UDP%%; + resolver_timeout 1s; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + js_fetch_trusted_certificate myca.crt; + + location /engine { + js_content test.engine; + } + + location /https_via_proxy_keepalive { + js_fetch_keepalive 4; + js_fetch_proxy http://user:pass@127.0.0.1:%%PORT_8082%%; + js_content test.https_fetch; + } + } + + server { + listen 127.0.0.1:%%PORT_8083%% ssl; + server_name example.com; + + keepalive_requests 100; + + ssl_certificate example.com.chained.crt; + ssl_certificate_key example.com.key; + + location = /test { + return 200 "COM:$connection_requests"; + } + } + + server { + listen 127.0.0.1:%%PORT_8084%% ssl; + server_name example.org; + + keepalive_requests 100; + + ssl_certificate example.org.chained.crt; + ssl_certificate_key example.org.key; + + location = /test { + return 200 "ORG:$connection_requests"; + } + } +} + +EOF + +my $p2 = port(8082); +my $p3 = port(8083); +my $p4 = port(8084); + +$t->write_file('test.js', <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', 'example.com', 'example.org') { + 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 ('example.com', 'example.org') { + 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 js_fetch_proxy')->plan(4); + +$t->run_daemon(\&https_proxy_daemon, $p2); +$t->run_daemon(\&dns_daemon, port(8981), $t); +$t->waitforsocket('127.0.0.1:' . $p2); +$t->waitforfile($t->testdir . '/' . port(8981)); + +############################################################################### + +like(http_get("/https_via_proxy_keepalive?domain=example.com"), + qr/COM:1$/, 'https keepalive through proxy 1'); +like(http_get("/https_via_proxy_keepalive?domain=example.org&port=$p4"), + qr/ORG:1$/, 'https keepalive through proxy different hostnames'); +like(http_get('/https_via_proxy_keepalive?domain=example.com'), + qr/COM:2$/, 'https keepalive through proxy 2'); +like(http_get("/https_via_proxy_keepalive?domain=example.org&port=$p4"), + qr/ORG:2$/, 'https keepalive through proxy different hostnames 2'); + +############################################################################### + +sub https_proxy_daemon { + my ($port) = @_; + + my $server = IO::Socket::INET->new( + Proto => 'tcp', + LocalAddr => "127.0.0.1:$port", + Listen => 128, + Reuse => 1 + ) or die "Can't create listening socket: $!\n"; + + local $SIG{PIPE} = 'IGNORE'; + + my $s = { + sel => IO::Select->new($server), + pending => {}, # fd -> { sock, buf } + conn => {}, # client fd -> { client, origin } + o2c => {}, # origin fd -> client fd + }; + + while (1) { + my @ready = $s->{sel}->can_read(1.0); + for my $sock (@ready) { + if ($sock == $server) { + my $client = $server->accept(); + next unless $client; + $client->autoflush(1); + $s->{sel}->add($client); + $s->{pending}->{ fileno($client) } = { + sock => $client, + buf => '', + }; + + next; + } + + my $fd = fileno($sock); + next unless defined $fd; + + if (exists $s->{o2c}->{$fd}) { + my $buf; + my $cfd = $s->{o2c}->{$fd}; + my $client = $s->{conn}->{$cfd}{client}; + + my $n = sysread($sock, $buf, 4096); + if (!defined($n) || $n == 0) { + _cleanup($s, $client, $sock); + next; + } + + syswrite($client, $buf); + next; + } + + if (exists $s->{conn}->{$fd}) { + my $buf; + my $origin = $s->{conn}->{$fd}{origin}; + + my $n = sysread($sock, $buf, 4096); + if (!defined($n) || $n == 0) { + _cleanup($s, $sock, $origin); + next; + } + + syswrite($origin, $buf); + next; + } + + if (exists $s->{pending}->{$fd}) { + my $buf; + my $p = $s->{pending}->{$fd}; + + my $n = sysread($sock, $buf, 4096); + if (!defined($n) || $n == 0) { + $s->{sel}->remove($sock); + delete $s->{pending}->{$fd}; + close $sock; + next; + } + + $p->{buf} .= $buf; + + if ($p->{buf} =~ /(\x0d\x0a)\1/s) { + my $method = '', my $proxy_auth = '', my $target = ''; + + my ($headers) = split(/\r\n\r\n/, $p->{buf}, 2); + for my $line (split(/\r\n/, $headers)) { + if ($method eq '' && $line =~ /^(\S+)\s+(\S+)\s+HTTP/i) { + $method = $1; + $target = $2; + next; + } + + if ($line =~ /^Proxy-Authorization:\s*(.+?)\s*$/i) { + $proxy_auth = $1; + next; + } + } + + if (uc($method) eq 'CONNECT' + && $proxy_auth =~ /^Basic\s+dXNlcjpwYXNz$/i) + { + print $sock "HTTP/1.1 200 established" . CRLF . CRLF; + + my ($host, $port) = split(/:/, $target); + + my $origin = IO::Socket::INET->new( + PeerAddr => "127.0.0.1:$port", + Proto => 'tcp', + Type => SOCK_STREAM + ); + + if (!$origin) { + $s->{sel}->remove($sock); + delete $s->{pending}->{$fd}; + close $sock; + next; + } + + $origin->autoflush(1); + $s->{sel}->add($origin); + + $s->{conn}->{$fd} = { + client => $sock, + origin => $origin, + }; + + $s->{o2c}->{ fileno($origin) } = $fd; + delete $s->{pending}->{$fd}; + + } else { + print $sock + "HTTP/1.1 407 Proxy Auth Required" . CRLF . + "Proxy-Authenticate: Basic realm=\"proxy\"" . CRLF . + "Content-Length: 0" . CRLF . + "Connection: close" . CRLF . CRLF; + $s->{sel}->remove($sock); + delete $s->{pending}->{$fd}; + close $sock; + } + } + + next; + } + + $s->{sel}->remove($sock); + close $sock; + } + } +} + +sub _cleanup { + my ($s, $client, $origin) = @_; + + my $cfd = fileno($client); + my $ofd = fileno($origin); + + delete $s->{o2c}->{$ofd} if defined $ofd; + + if (defined $cfd) { + delete $s->{conn}->{$cfd}; + delete $s->{pending}->{$cfd} if exists $s->{pending}->{$cfd}; + } + + if ($client) { + $s->{sel}->remove($client); + close $client; + } + + if ($origin) { + $s->{sel}->remove($origin); + close $origin; + } +} + +############################################################################### + +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 ($name eq 'example.com' || $name eq 'example.org') { + 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_proxy_variable.t b/nginx/t/js_fetch_proxy_variable.t new file mode 100644 index 000000000..7399d28f6 --- /dev/null +++ b/nginx/t/js_fetch_proxy_variable.t @@ -0,0 +1,142 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) F5, Inc. + +# Tests for http njs module, fetch method with variable proxy URLs. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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 /static_proxy { + js_fetch_proxy http://testuser:testpass@127.0.0.1:%%PORT_8081%%; + js_content test.http_fetch; + } + + location /dynamic_proxy { + set $proxy_url http://testuser:testpass@127.0.0.1:%%PORT_8081%%; + js_fetch_proxy $proxy_url; + js_content test.http_fetch; + } + + location /dynamic_empty_proxy { + set $proxy_url ""; + js_fetch_proxy $proxy_url; + js_content test.http_fetch; + } + } + + server { + listen 127.0.0.1:%%PORT_8081%%; + server_name localhost; + + location = /test { + js_content test.proxy_endpoint; + } + } + + server { + listen 127.0.0.1:%%PORT_8082%%; + server_name localhost; + + location = /test { + js_content test.origin_endpoint; + } + } +} + +EOF + +my $p1 = port(8081); +my $p2 = port(8082); + +$t->write_file('test.js', <try_run('no js_fetch_proxy')->plan(3); + +############################################################################### + +like(http_get('/static_proxy'), qr/PROXY:Basic\s+dGVzdHVzZXI6dGVzdHBhc3M=/, + 'static proxy URL with auth'); +like(http_get('/dynamic_proxy'), qr/PROXY:Basic\s+dGVzdHVzZXI6dGVzdHBhc3M=/, + 'dynamic proxy URL with auth'); +like(http_get('/dynamic_empty_proxy'), qr/ORIGIN:OK/, + 'dynamic empty proxy URL bypasses proxy'); + +############################################################################### diff --git a/nginx/t/stream_js_fetch_proxy.t b/nginx/t/stream_js_fetch_proxy.t new file mode 100644 index 000000000..d4ca9fbc8 --- /dev/null +++ b/nginx/t/stream_js_fetch_proxy.t @@ -0,0 +1,198 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) F5, Inc. + +# Tests for stream njs module, fetch method with forward proxy. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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 stream_map/) + ->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:%%PORT_8080%%; + server_name localhost; + + location = /test { + js_content test.origin_endpoint; + } + } + + server { + listen 127.0.0.1:%%PORT_8081%%; + server_name localhost; + + location = /test { + js_content test.proxy_endpoint; + } + } +} + +stream { + %%TEST_GLOBALS_STREAM%% + + js_import test.js; + + server { + listen 127.0.0.1:%%PORT_8091%%; + js_fetch_proxy http://testuser:testpass@127.0.0.1:%%PORT_8081%%; + js_filter test.http_fetch; + proxy_pass 127.0.0.1:%%PORT_8094%%; + } + + server { + listen 127.0.0.1:%%PORT_8092%%; + + set $proxy_url http://testuser:testpass@127.0.0.1:%%PORT_8081%%; + js_fetch_proxy $proxy_url; + js_filter test.http_fetch; + proxy_pass 127.0.0.1:%%PORT_8094%%; + } + + server { + listen 127.0.0.1:%%PORT_8093%%; + + set $proxy_url ""; + js_fetch_proxy $proxy_url; + js_filter test.http_fetch; + proxy_pass 127.0.0.1:%%PORT_8094%%; + } +} + +EOF + +my $p0 = port(8080); +my $p1 = port(8081); + +$t->write_file('test.js', < 0) { + s.off('upload'); + + let reply = await ngx.fetch('http://127.0.0.1:$p0/test'); + let body = await reply.text(); + + s.send(body, flags); + } + }); + } + + export default {origin_endpoint, proxy_endpoint, http_fetch}; + +EOF + +$t->try_run('no js_fetch_proxy available')->plan(3); + +$t->run_daemon(\&stream_daemon, port(8094)); +$t->waitforsocket('127.0.0.1:' . port(8094)); + +############################################################################### + +is(stream('127.0.0.1:' . port(8091))->io('TEST'), + 'PROXY:Basic dGVzdHVzZXI6dGVzdHBhc3M=', 'static proxy'); +is(stream('127.0.0.1:' . port(8092))->io('TEST'), + 'PROXY:Basic dGVzdHVzZXI6dGVzdHBhc3M=', 'dynamic proxy'); +is(stream('127.0.0.1:' . port(8093))->io('TEST'), 'ORIGIN:OK', 'no proxy'); + +############################################################################### + +$t->stop(); + +############################################################################### + +sub stream_daemon { + my ($port) = @_; + + 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"; + + local $SIG{PIPE} = 'IGNORE'; + + while (my $client = $server->accept()) { + $client->autoflush(1); + + log2c("(new connection $client)"); + + $client->sysread(my $buffer, 65536); + + log2i("$client $buffer"); + + $client->syswrite($buffer); + + log2o("$client $buffer"); + + $client->close(); + } +} + +sub log2i { Test::Nginx::log_core('|| <<', @_); } +sub log2o { Test::Nginx::log_core('|| >>', @_); } +sub log2c { Test::Nginx::log_core('||', @_); } + +###############################################################################