Supports Basic authentication via Proxy-Authorization header.
- js_fetch_proxy - configures forward proxy URL. It takes proxy URL
as a parameter. The URL may optionally contain user and password.
Parameter value can contain variables. If value is empty,
forward proxy is disabled.
example.conf:
...
http {
js_import main.js;
server {
listen 8080;
resolver 127.0.0.1:5353;
location /api {
js_fetch_proxy http://user:pass@proxy.example.com:3128;
js_content main.fetch_handler;
}
}
}
This implements #956 feature request on Github.
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;
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,
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
};
}
},
+ {
+ .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"),
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,
}
+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)
{
}
+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)
{
* set by ngx_pcalloc():
*
* conf->reuse_queue = NULL;
+ * conf->fetch_proxy_auth_header = { 0, NULL };
*/
conf->paths = NGX_CONF_UNSET_PTR;
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;
}
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)
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)
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));
{
njs_int_t ret;
ngx_url_t u;
+ ngx_str_t *resolve_host;
ngx_pool_t *pool;
njs_value_t *init, *value;
ngx_js_http_t *http;
NJS_CHB_MP_INIT(&http->chain, njs_vm_memory_pool(vm));
NJS_CHB_MP_INIT(&http->response.chain, njs_vm_memory_pool(vm));
- ngx_js_fetch_build_request(http, &request, &u.uri, &u);
+ resolve_host = NULL;
+ http->connect_port = http->port;
+
+ 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;
+ }
+
+ } else {
+ http->proxy.url = http->conf->fetch_proxy_url;
+ http->proxy.auth = http->conf->fetch_proxy_auth_header;
+ }
+
+ 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 (!ngx_js_http_proxy(http) && u.addrs == NULL) {
+ resolve_host = &u.host;
+ }
+
+ ngx_js_fetch_build_request(http, &request, &u.uri, &u,
+ ngx_js_http_proxy(http) && !ngx_js_https(&u));
+
+ 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,
+ resolve_host,
ngx_external_resolver_timeout(vm, external));
+
if (ctx == NULL) {
njs_vm_memory_error(vm);
return NJS_ERROR;
}
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));
goto failed;
}
+ /*
+ * set by ngx_pcalloc():
+ *
+ * fetch->http.proxy.state = HTTP_STATE_DIRECT;
+ */
+
http = &fetch->http;
http->pool = pool;
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);
+ 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
}
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;
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) {
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);
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
}
#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;
}
static void
ngx_js_fetch_append_request_headers(njs_chb_t *chain,
- ngx_js_request_t *request)
+ ngx_js_request_t *request, njs_bool_t is_proxy)
{
ngx_uint_t i;
ngx_list_part_t *part;
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);
void
ngx_js_fetch_build_request(ngx_js_http_t *http, ngx_js_request_t *request,
- ngx_str_t *path, ngx_url_t *u)
+ ngx_str_t *path, ngx_url_t *u, njs_bool_t is_proxy)
{
ngx_str_t method;
ngx_uint_t i;
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_literal(&http->chain, CRLF);
}
- ngx_js_fetch_append_request_headers(&http->chain, request);
+ 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);
ngx_uint_t naddr;
ngx_str_t host;
in_port_t port;
+ in_port_t connect_port;
ngx_peer_connection_t peer;
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_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);
+ ngx_str_t *path, ngx_url_t *u, njs_bool_t is_proxy);
#endif /* _NGX_JS_HTTP_H_INCLUDED_ */
JSValue init, value, promise;
ngx_int_t rc;
ngx_url_t u;
+ ngx_str_t *resolve_host;
ngx_pool_t *pool;
ngx_js_ctx_t *ctx;
ngx_js_http_t *http;
NJS_CHB_MP_INIT(&http->chain, ctx->engine->pool);
NJS_CHB_MP_INIT(&http->response.chain, ctx->engine->pool);
- ngx_js_fetch_build_request(http, &request, &u.uri, &u);
+ resolve_host = NULL;
+ http->connect_port = http->port;
+
+ 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;
+ }
+
+ } else {
+ http->proxy.url = http->conf->fetch_proxy_url;
+ http->proxy.auth = http->conf->fetch_proxy_auth_header;
+ }
+
+ 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 (!ngx_js_http_proxy(http) && u.addrs == NULL) {
+ resolve_host = &u.host;
+ }
+
+ ngx_js_fetch_build_request(http, &request, &u.uri, &u,
+ ngx_js_http_proxy(http) && !ngx_js_https(&u));
+
+ 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,
+ resolve_host,
ngx_qjs_external_resolver_timeout(cx, external));
+
if (rs == NULL) {
JS_FreeValue(cx, promise);
return JS_ThrowOutOfMemory(cx);
}
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;
return NULL;
}
+ /*
+ * set by ngx_pcalloc():
+ *
+ * fetch->http.proxy.state = HTTP_STATE_DIRECT;
+ */
+
http = &fetch->http;
http->pool = pool;
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;
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[] = {
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
};
}
+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)
{
}
like(http_get('/user_agent_header'),
- qr/200 OK.*nginx-js\/1.0$/s,
+ 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');
--- /dev/null
+#!/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', <<EOF);
+ function engine(r) {
+ r.return(200, njs.engine);
+ }
+
+ function endpoint(r) {
+ let proxy_auth = r.headersIn['Proxy-Authorization'] || '';
+ let valid_creds = [
+ 'Basic dGVzdHVzZXI6dGVzdHBhc3M=',
+ 'Basic dXNlckBkb21haW46cEBzczp3b3Jk'
+ ];
+
+ if (proxy_auth && !valid_creds.includes(proxy_auth)) {
+ r.return(407, 'Proxy Authentication Required');
+ return;
+ }
+
+ let s = `METHOD: \${r.method}\\n`;
+ s += `URI: \${r.requestLine.split(" ")[1]}\\n`;
+
+ if (proxy_auth) {
+ s += `PROXY-AUTH: \${proxy_auth}\\n`;
+ }
+
+ s += 'BODY: ' + (r.requestText || '') + '\\n';
+
+ r.return(200, s + 'ORIGIN:response');
+ }
+
+ async function http_fetch(r) {
+ try {
+ let domain = decodeURIComponent(r.args.domain);
+ let method = r.method;
+ let body = r.requestText;
+
+ let reply = await ngx.fetch(`http://\${domain}:$p/test`,
+ {method, body});
+ body = await reply.text();
+ r.return(200, body);
+ } catch (e) {
+ r.return(500, e.message);
+ }
+ }
+
+ async function http_fetch_status(r) {
+ try {
+ let reply = await ngx.fetch('http://127.0.0.1:$p/test');
+ r.return(200, 'STATUS:' + reply.status);
+ } catch (e) {
+ r.return(500, e.message);
+ }
+ }
+
+ export default {engine, endpoint, http_fetch, http_fetch_status};
+
+EOF
+
+$t->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);
+}
+
+###############################################################################
--- /dev/null
+#!/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', <<EOF);
+ function engine(r) {
+ r.return(200, njs.engine);
+ }
+
+ function endpoint(r) {
+ let s = `METHOD: \${r.method}\\n`;
+ s += `URI: \${r.requestLine.split(" ")[1]}\\n`;
+ s += 'BODY: ' + (r.requestText || '') + '\\n';
+
+ r.return(200, s + 'ORIGIN');
+ }
+
+ async function https_fetch(r) {
+ try {
+ let domain = decodeURIComponent(r.args.domain);
+ let method = r.method;
+ let body = r.requestText;
+ let reply = await ngx.fetch(`https://\${domain}:$p3/test`,
+ {method, body});
+ body = await reply.text();
+ r.return(200, body);
+
+ } catch (e) {
+ r.return(500, e.message);
+ }
+ }
+
+ export default {engine, endpoint, https_fetch};
+
+EOF
+
+my $d = $t->testdir();
+
+$t->write_file('openssl.conf', <<EOF);
+[ req ]
+default_bits = 2048
+encrypt_key = no
+distinguished_name = req_distinguished_name
+x509_extensions = myca_extensions
+[ req_distinguished_name ]
+[ myca_extensions ]
+basicConstraints = critical,CA:TRUE
+EOF
+
+$t->write_file('myca.conf', <<EOF);
+[ ca ]
+default_ca = myca
+
+[ myca ]
+new_certs_dir = $d
+database = $d/certindex
+default_md = sha256
+policy = myca_policy
+serial = $d/certserial
+default_days = 1
+x509_extensions = myca_extensions
+
+[ myca_policy ]
+commonName = supplied
+
+[ myca_extensions ]
+basicConstraints = critical,CA:TRUE
+EOF
+
+system('openssl req -x509 -new '
+ . "-config $d/openssl.conf -subj /CN=myca/ "
+ . "-out $d/myca.crt -keyout $d/myca.key "
+ . ">>$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);
+}
+
+###############################################################################
--- /dev/null
+#!/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', <<EOF);
+ function engine(r) {
+ r.return(200, njs.engine);
+ }
+
+ async function https_fetch(r) {
+ try {
+ let domain = decodeURIComponent(r.args.domain);
+ let p = r.args.port || $p3;
+ let scheme = r.args.scheme || 'https';
+ let reply = await ngx.fetch(`\${scheme}://\${domain}:\${p}/test`);
+ let body = await reply.text();
+ r.return(200, body);
+
+ } catch (e) {
+ r.return(500, e.message);
+ }
+ }
+
+ export default {engine, https_fetch};
+
+EOF
+
+my $d = $t->testdir();
+
+$t->write_file('openssl.conf', <<EOF);
+[ req ]
+default_bits = 2048
+encrypt_key = no
+distinguished_name = req_distinguished_name
+x509_extensions = myca_extensions
+[ req_distinguished_name ]
+[ myca_extensions ]
+basicConstraints = critical,CA:TRUE
+EOF
+
+$t->write_file('myca.conf', <<EOF);
+[ ca ]
+default_ca = myca
+
+[ myca ]
+new_certs_dir = $d
+database = $d/certindex
+default_md = sha256
+policy = myca_policy
+serial = $d/certserial
+default_days = 1
+x509_extensions = myca_extensions
+
+[ myca_policy ]
+commonName = supplied
+
+[ myca_extensions ]
+basicConstraints = critical,CA:TRUE
+EOF
+
+system('openssl req -x509 -new '
+ . "-config $d/openssl.conf -subj /CN=myca/ "
+ . "-out $d/myca.crt -keyout $d/myca.key "
+ . ">>$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);
+ }
+}
+
+###############################################################################
--- /dev/null
+#!/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', <<EOF);
+ function proxy_endpoint(r) {
+ let proxy_auth = r.headersIn['Proxy-Authorization'] || '';
+ let expected = 'Basic dGVzdHVzZXI6dGVzdHBhc3M=';
+
+ if (!proxy_auth) {
+ r.return(500, 'PROXY:NO-AUTH');
+ return;
+ }
+
+ if (proxy_auth !== expected) {
+ r.return(407, 'PROXY:BAD-AUTH');
+ return;
+ }
+
+ r.return(200, 'PROXY:' + proxy_auth);
+ }
+
+ function origin_endpoint(r) {
+ let proxy_auth = r.headersIn['Proxy-Authorization'] || '';
+
+ if (proxy_auth) {
+ r.return(500, 'ORIGIN:HAS-PROXY-AUTH');
+ return;
+ }
+
+ r.return(200, 'ORIGIN:OK');
+ }
+
+ async function http_fetch(r) {
+ try {
+ let reply = await ngx.fetch('http://127.0.0.1:$p2/test');
+ let body = await reply.text();
+ r.return(200, body);
+ } catch (e) {
+ r.return(500, e.message);
+ }
+ }
+
+ export default {proxy_endpoint, origin_endpoint, http_fetch};
+
+EOF
+
+$t->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');
+
+###############################################################################
--- /dev/null
+#!/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', <<EOF);
+ function origin_endpoint(r) {
+ let proxy_auth = r.headersIn['Proxy-Authorization'] || '';
+
+ if (proxy_auth) {
+ r.return(500, 'ORIGIN:HAS-PROXY-AUTH');
+ return;
+ }
+
+ r.return(200, 'ORIGIN:OK');
+ }
+
+ function proxy_endpoint(r) {
+ let proxy_auth = r.headersIn['Proxy-Authorization'] || '';
+ let expected = 'Basic dGVzdHVzZXI6dGVzdHBhc3M=';
+
+ if (!proxy_auth) {
+ r.return(500, 'PROXY:NO-AUTH');
+ return;
+ }
+
+ if (proxy_auth !== expected) {
+ r.return(407, 'PROXY:BAD-AUTH');
+ return;
+ }
+
+ r.return(200, 'PROXY:' + proxy_auth);
+ }
+
+ function http_fetch(s) {
+ var collect = '';
+
+ s.on('upload', async function (data, flags) {
+ collect += data;
+
+ if (collect.length > 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('||', @_); }
+
+###############################################################################