Skip to content

Commit 84bd841

Browse files
committed
Fetch: added forward proxy support with HTTPS tunneling.
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.
1 parent 0dc3b77 commit 84bd841

14 files changed

+2119
-24
lines changed

nginx/ngx_http_js_module.c

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
typedef struct {
1616
NGX_JS_COMMON_LOC_CONF;
1717

18+
ngx_http_complex_value_t fetch_proxy_cv;
19+
1820
ngx_str_t content;
1921
ngx_str_t header_filter;
2022
ngx_str_t body_filter;
@@ -380,6 +382,8 @@ static char *ngx_http_js_content(ngx_conf_t *cf, ngx_command_t *cmd,
380382
void *conf);
381383
static char *ngx_http_js_shared_dict_zone(ngx_conf_t *cf, ngx_command_t *cmd,
382384
void *conf);
385+
static char *ngx_http_js_fetch_proxy(ngx_conf_t *cf, ngx_command_t *cmd,
386+
void *conf);
383387
static char *ngx_http_js_body_filter_set(ngx_conf_t *cf, ngx_command_t *cmd,
384388
void *conf);
385389
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[] = {
593597
offsetof(ngx_http_js_loc_conf_t, fetch_keepalive_timeout),
594598
NULL },
595599

600+
{ ngx_string("js_fetch_proxy"),
601+
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
602+
ngx_http_js_fetch_proxy,
603+
NGX_HTTP_LOC_CONF_OFFSET,
604+
0,
605+
NULL },
606+
596607
ngx_null_command
597608
};
598609

@@ -818,6 +829,16 @@ static njs_external_t ngx_http_js_ext_request[] = {
818829
}
819830
},
820831

832+
{
833+
.flags = NJS_EXTERN_PROPERTY,
834+
.name.string = njs_str("requestLine"),
835+
.enumerable = 1,
836+
.u.property = {
837+
.handler = ngx_js_ext_string,
838+
.magic32 = offsetof(ngx_http_request_t, request_line),
839+
}
840+
},
841+
821842
{
822843
.flags = NJS_EXTERN_PROPERTY,
823844
.name.string = njs_str("requestText"),
@@ -1080,6 +1101,8 @@ static const JSCFunctionListEntry ngx_http_qjs_ext_request[] = {
10801101
JS_CGETSET_DEF("remoteAddress", ngx_http_qjs_ext_remote_address, NULL),
10811102
JS_CGETSET_MAGIC_DEF("requestBuffer", ngx_http_qjs_ext_request_body, NULL,
10821103
NGX_JS_BUFFER),
1104+
JS_CGETSET_MAGIC_DEF("requestLine", ngx_http_qjs_ext_string, NULL,
1105+
offsetof(ngx_http_request_t, request_line)),
10831106
JS_CGETSET_MAGIC_DEF("requestText", ngx_http_qjs_ext_request_body, NULL,
10841107
NGX_JS_STRING),
10851108
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)
80168039
}
80178040

80188041

8042+
static ngx_int_t
8043+
ngx_http_js_eval_proxy_url(ngx_pool_t *pool, void *request,
8044+
void *module_conf, ngx_url_t **url_out, ngx_str_t *auth_out)
8045+
{
8046+
ngx_str_t value;
8047+
ngx_http_request_t *r;
8048+
ngx_http_js_loc_conf_t *jlcf;
8049+
8050+
r = request;
8051+
jlcf = module_conf;
8052+
8053+
if (ngx_http_complex_value(r, &jlcf->fetch_proxy_cv, &value) != NGX_OK) {
8054+
return NGX_ERROR;
8055+
}
8056+
8057+
return ngx_js_parse_proxy_url(pool, r->connection->log, &value,
8058+
url_out, auth_out);
8059+
}
8060+
8061+
8062+
static char *
8063+
ngx_http_js_fetch_proxy(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
8064+
{
8065+
ngx_uint_t n;
8066+
ngx_str_t *value;
8067+
ngx_http_js_loc_conf_t *jlcf;
8068+
ngx_http_compile_complex_value_t ccv;
8069+
8070+
jlcf = conf;
8071+
8072+
value = cf->args->elts;
8073+
8074+
n = ngx_http_script_variables_count(&value[1]);
8075+
8076+
if (n) {
8077+
ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t));
8078+
8079+
ccv.cf = cf;
8080+
ccv.value = &value[1];
8081+
ccv.complex_value = &jlcf->fetch_proxy_cv;
8082+
8083+
if (ngx_http_compile_complex_value(&ccv) != NGX_OK) {
8084+
return NGX_CONF_ERROR;
8085+
}
8086+
8087+
jlcf->eval_proxy_url = ngx_http_js_eval_proxy_url;
8088+
8089+
return NGX_CONF_OK;
8090+
}
8091+
8092+
return ngx_js_fetch_proxy(cf, cmd, conf);
8093+
}
8094+
8095+
80198096
static char *
80208097
ngx_http_js_var(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
80218098
{

nginx/ngx_js.c

Lines changed: 199 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3305,6 +3305,196 @@ ngx_js_preload_object(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
33053305
}
33063306

33073307

3308+
static ngx_int_t
3309+
ngx_js_build_proxy_auth_header(ngx_pool_t *pool, ngx_str_t *auth_header,
3310+
ngx_str_t *user, ngx_str_t *pass)
3311+
{
3312+
u_char *p;
3313+
size_t len;
3314+
ngx_str_t userpass, b64;
3315+
3316+
userpass.len = user->len + 1 + pass->len;
3317+
userpass.data = ngx_pnalloc(pool, userpass.len);
3318+
if (userpass.data == NULL) {
3319+
return NGX_ERROR;
3320+
}
3321+
3322+
p = ngx_cpymem(userpass.data, user->data, user->len);
3323+
*p++ = ':';
3324+
ngx_memcpy(p, pass->data, pass->len);
3325+
3326+
b64.len = ngx_base64_encoded_length(userpass.len);
3327+
b64.data = ngx_pnalloc(pool, b64.len);
3328+
if (b64.data == NULL) {
3329+
return NGX_ERROR;
3330+
}
3331+
3332+
ngx_encode_base64(&b64, &userpass);
3333+
3334+
len = sizeof("Proxy-Authorization: Basic \r\n") - 1 + b64.len;
3335+
p = ngx_pnalloc(pool, len);
3336+
if (p == NULL) {
3337+
return NGX_ERROR;
3338+
}
3339+
3340+
ngx_sprintf(p, "Proxy-Authorization: Basic %V\r\n", &b64);
3341+
auth_header->data = p;
3342+
auth_header->len = len;
3343+
3344+
return NGX_OK;
3345+
}
3346+
3347+
3348+
ngx_int_t
3349+
ngx_js_parse_proxy_url(ngx_pool_t *pool, ngx_log_t *log, ngx_str_t *url_str,
3350+
ngx_url_t **url_out, ngx_str_t *auth_header_out)
3351+
{
3352+
u_char *p, *at, *colon, *host_start, *user_start, *pass_start;
3353+
u_char *decoded_user, *decoded_pass, *decoded_end;
3354+
size_t user_len, pass_len;
3355+
ngx_url_t *u;
3356+
ngx_str_t user, pass;
3357+
3358+
if (url_str->len == 0) {
3359+
*url_out = NULL;
3360+
ngx_str_null(auth_header_out);
3361+
return NGX_OK;
3362+
}
3363+
3364+
if (ngx_strncmp(url_str->data, "http://", sizeof("http://") - 1) != 0) {
3365+
ngx_log_error(NGX_LOG_ERR, log, 0,
3366+
"js_fetch_proxy URL must use http:// scheme");
3367+
return NGX_ERROR;
3368+
}
3369+
3370+
host_start = url_str->data + (sizeof("http://") - 1);
3371+
at = ngx_strlchr(host_start, url_str->data + url_str->len, '@');
3372+
3373+
ngx_str_null(auth_header_out);
3374+
3375+
if (at != NULL) {
3376+
colon = NULL;
3377+
3378+
for (p = at - 1; p > host_start; p--) {
3379+
if (*p == ':') {
3380+
colon = p;
3381+
break;
3382+
}
3383+
}
3384+
3385+
if (colon == NULL) {
3386+
ngx_log_error(NGX_LOG_ERR, log, 0,
3387+
"js_fetch_proxy URL credentials must be in "
3388+
"user:password format");
3389+
return NGX_ERROR;
3390+
}
3391+
3392+
user_start = host_start;
3393+
user_len = colon - host_start;
3394+
pass_start = colon + 1;
3395+
pass_len = at - pass_start;
3396+
3397+
decoded_user = ngx_pnalloc(pool, 128);
3398+
if (decoded_user == NULL) {
3399+
return NGX_ERROR;
3400+
}
3401+
3402+
decoded_pass = ngx_pnalloc(pool, 128);
3403+
if (decoded_pass == NULL) {
3404+
return NGX_ERROR;
3405+
}
3406+
3407+
p = user_start;
3408+
decoded_end = decoded_user;
3409+
ngx_unescape_uri(&decoded_end, &p, user_len, NGX_UNESCAPE_URI);
3410+
3411+
user_len = decoded_end - decoded_user;
3412+
if (user_len == 0 || user_len > 127) {
3413+
ngx_log_error(NGX_LOG_ERR, log, 0,
3414+
"js_fetch_proxy username invalid or too long "
3415+
"(max 127 bytes after decoding)");
3416+
return NGX_ERROR;
3417+
}
3418+
3419+
p = pass_start;
3420+
decoded_end = decoded_pass;
3421+
ngx_unescape_uri(&decoded_end, &p, pass_len, NGX_UNESCAPE_URI);
3422+
3423+
pass_len = decoded_end - decoded_pass;
3424+
if (pass_len == 0 || pass_len > 127) {
3425+
ngx_log_error(NGX_LOG_ERR, log, 0,
3426+
"js_fetch_proxy password invalid or too long "
3427+
"(max 127 bytes after decoding)");
3428+
return NGX_ERROR;
3429+
}
3430+
3431+
user.data = decoded_user;
3432+
user.len = user_len;
3433+
pass.data = decoded_pass;
3434+
pass.len = pass_len;
3435+
3436+
if (ngx_js_build_proxy_auth_header(pool, auth_header_out,
3437+
&user, &pass)
3438+
!= NGX_OK)
3439+
{
3440+
return NGX_ERROR;
3441+
}
3442+
3443+
host_start = at + 1;
3444+
}
3445+
3446+
u = ngx_pcalloc(pool, sizeof(ngx_url_t));
3447+
if (u == NULL) {
3448+
return NGX_ERROR;
3449+
}
3450+
3451+
u->url.data = host_start;
3452+
u->url.len = url_str->data + url_str->len - host_start;
3453+
u->default_port = 3128;
3454+
u->no_resolve = 1;
3455+
3456+
if (ngx_parse_url(pool, u) != NGX_OK) {
3457+
ngx_log_error(NGX_LOG_ERR, log, 0,
3458+
"invalid proxy URL: %V", url_str);
3459+
return NGX_ERROR;
3460+
}
3461+
3462+
*url_out = u;
3463+
3464+
return NGX_OK;
3465+
}
3466+
3467+
3468+
char *
3469+
ngx_js_fetch_proxy(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
3470+
{
3471+
ngx_str_t *value;
3472+
ngx_js_loc_conf_t *jscf;
3473+
3474+
jscf = conf;
3475+
3476+
value = cf->args->elts;
3477+
3478+
if (ngx_js_parse_proxy_url(cf->pool, cf->log, &value[1],
3479+
&jscf->fetch_proxy_url,
3480+
&jscf->fetch_proxy_auth_header)
3481+
!= NGX_OK)
3482+
{
3483+
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
3484+
"invalid proxy URL: %V", &value[1]);
3485+
return NGX_CONF_ERROR;
3486+
}
3487+
3488+
if (jscf->fetch_proxy_url == NULL) {
3489+
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
3490+
"proxy host is empty in URL: %V", &value[1]);
3491+
return NGX_CONF_ERROR;
3492+
}
3493+
3494+
return NGX_CONF_OK;
3495+
}
3496+
3497+
33083498
static ngx_int_t
33093499
ngx_js_init_preload_vm(njs_vm_t *vm, ngx_js_loc_conf_t *conf)
33103500
{
@@ -3984,6 +4174,7 @@ ngx_js_create_conf(ngx_conf_t *cf, size_t size)
39844174
* set by ngx_pcalloc():
39854175
*
39864176
* conf->reuse_queue = NULL;
4177+
* conf->fetch_proxy_auth_header = { 0, NULL };
39874178
*/
39884179

39894180
conf->paths = NGX_CONF_UNSET_PTR;
@@ -4001,6 +4192,8 @@ ngx_js_create_conf(ngx_conf_t *cf, size_t size)
40014192
conf->fetch_keepalive_requests = NGX_CONF_UNSET_UINT;
40024193
conf->fetch_keepalive_time = NGX_CONF_UNSET_MSEC;
40034194
conf->fetch_keepalive_timeout = NGX_CONF_UNSET_MSEC;
4195+
conf->fetch_proxy_url = NGX_CONF_UNSET_PTR;
4196+
conf->eval_proxy_url = NGX_CONF_UNSET_PTR;
40044197

40054198
return conf;
40064199
}
@@ -4120,10 +4313,15 @@ ngx_js_merge_conf(ngx_conf_t *cf, void *parent, void *child,
41204313
prev->fetch_keepalive_time, 3600000);
41214314
ngx_conf_merge_msec_value(conf->fetch_keepalive_timeout,
41224315
prev->fetch_keepalive_timeout, 60000);
4123-
41244316
ngx_queue_init(&conf->fetch_keepalive_cache);
41254317
ngx_queue_init(&conf->fetch_keepalive_free);
41264318

4319+
ngx_conf_merge_ptr_value(conf->fetch_proxy_url, prev->fetch_proxy_url,
4320+
NULL);
4321+
ngx_conf_merge_ptr_value(conf->eval_proxy_url, prev->eval_proxy_url, NULL);
4322+
ngx_conf_merge_str_value(conf->fetch_proxy_auth_header,
4323+
prev->fetch_proxy_auth_header, "");
4324+
41274325
if (ngx_js_merge_vm(cf, (ngx_js_loc_conf_t *) conf,
41284326
(ngx_js_loc_conf_t *) prev,
41294327
init_vm)

nginx/ngx_js.h

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,24 @@ typedef struct {
141141
ngx_msec_t fetch_keepalive_time; \
142142
ngx_msec_t fetch_keepalive_timeout; \
143143
ngx_queue_t fetch_keepalive_cache; \
144-
ngx_queue_t fetch_keepalive_free
144+
ngx_queue_t fetch_keepalive_free; \
145+
\
146+
ngx_url_t *fetch_proxy_url; \
147+
ngx_str_t fetch_proxy_auth_header; \
148+
\
149+
ngx_int_t (*eval_proxy_url)(ngx_pool_t *pool, \
150+
void *request, \
151+
void *module_conf, \
152+
ngx_url_t **url_out, \
153+
ngx_str_t *auth_out)
154+
155+
#define ngx_js_conf_dynamic_proxy(conf) \
156+
((conf)->eval_proxy_url != NULL)
157+
158+
#define ngx_js_conf_proxy(conf) \
159+
(((conf)->fetch_proxy_url != NULL \
160+
&& (conf)->fetch_proxy_url->host.len > 0) \
161+
|| ngx_js_conf_dynamic_proxy(conf))
145162

146163

147164
#if (NGX_SSL)
@@ -423,6 +440,9 @@ void ngx_js_logger(ngx_connection_t *c, ngx_uint_t level,
423440
char * ngx_js_import(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
424441
char * ngx_js_engine(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
425442
char * ngx_js_preload_object(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
443+
char * ngx_js_fetch_proxy(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
444+
ngx_int_t ngx_js_parse_proxy_url(ngx_pool_t *pool, ngx_log_t *log,
445+
ngx_str_t *url_str, ngx_url_t **url_out, ngx_str_t *auth_header_out);
426446
ngx_int_t ngx_js_merge_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf,
427447
ngx_js_loc_conf_t *prev,
428448
ngx_int_t (*init_vm)(ngx_conf_t *cf, ngx_js_loc_conf_t *conf));

0 commit comments

Comments
 (0)