From: Dmitry Volyntsev Date: Sat, 28 Mar 2026 00:33:37 +0000 (-0700) Subject: HTTP: added js_access directive. X-Git-Url: http://www.kaiwu.me/sitemap.xml?a=commitdiff_plain;h=8d571dd98677d69a27e3e0428bbaba3fe81ea792;p=njs.git HTTP: added js_access directive. The directive registers a JavaScript handler in the access phase, running after built-in access checkers (allow/deny, auth_basic, auth_request). r.subrequest(), ngx.fetch() and other async operations are supported. The handler defaults to NGX_OK (access granted) on normal completion, matching the behavior of other access phase modules. The r.decline() method allows the handler to return NGX_DECLINED (no opinion), deferring the decision to other access checkers under "satisfy any". The r.return() method can send any HTTP response from the access phase, including 3xx redirects for authentication flows. --- diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c index d1908ff4..1654b310 100644 --- a/nginx/ngx_http_js_module.c +++ b/nginx/ngx_http_js_module.c @@ -18,6 +18,7 @@ typedef struct { ngx_http_complex_value_t fetch_proxy_cv; + ngx_str_t access; ngx_str_t content; ngx_str_t header_filter; ngx_str_t body_filter; @@ -74,6 +75,8 @@ struct ngx_http_js_ctx_s { ngx_chain_t *in); ngx_js_periodic_t *periodic; + + unsigned in_progress:1; }; @@ -112,6 +115,8 @@ typedef struct { } ngx_http_js_entry_t; +static ngx_int_t ngx_http_js_access_handler(ngx_http_request_t *r); +static void ngx_http_js_access_write_event_handler(ngx_http_request_t *r); static ngx_int_t ngx_http_js_content_handler(ngx_http_request_t *r); static void ngx_http_js_content_event_handler(ngx_http_request_t *r); static void ngx_http_js_content_write_event_handler(ngx_http_request_t *r); @@ -192,6 +197,8 @@ static njs_int_t ngx_http_js_ext_finish(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval); static njs_int_t ngx_http_js_ext_return(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval); +static njs_int_t ngx_http_js_ext_decline(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t unused, njs_value_t *retval); static njs_int_t ngx_http_js_ext_internal_redirect(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval); @@ -307,6 +314,8 @@ static JSValue ngx_http_qjs_ext_response_body(JSContext *cx, JSValueConst this_val, int type); static JSValue ngx_http_qjs_ext_return(JSContext *cx, JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_http_qjs_ext_decline(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv); static JSValue ngx_http_qjs_ext_send(JSContext *cx, JSValueConst this_val, int argc, JSValueConst *argv); static JSValue ngx_http_qjs_ext_send_buffer(JSContext *cx, @@ -379,6 +388,8 @@ static char *ngx_http_js_periodic(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); static char *ngx_http_js_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); static char *ngx_http_js_var(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); +static char *ngx_http_js_access(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); 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, @@ -482,6 +493,13 @@ static ngx_command_t ngx_http_js_commands[] = { 0, NULL }, + { ngx_string("js_access"), + NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF|NGX_HTTP_LMT_CONF|NGX_CONF_TAKE1, + ngx_http_js_access, + NGX_HTTP_LOC_CONF_OFFSET, + 0, + NULL }, + { ngx_string("js_content"), NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF|NGX_HTTP_LMT_CONF|NGX_CONF_TAKE1, ngx_http_js_content, @@ -916,6 +934,17 @@ static njs_external_t ngx_http_js_ext_request[] = { } }, + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("decline"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = ngx_http_js_ext_decline, + } + }, + { .flags = NJS_EXTERN_METHOD, .name.string = njs_str("send"), @@ -1132,6 +1161,7 @@ static const JSCFunctionListEntry ngx_http_qjs_ext_request[] = { JS_CGETSET_MAGIC_DEF("responseText", ngx_http_qjs_ext_response_body, NULL, NGX_JS_STRING), JS_CFUNC_DEF("return", 2, ngx_http_qjs_ext_return), + JS_CFUNC_DEF("decline", 0, ngx_http_qjs_ext_decline), JS_CFUNC_DEF("send", 1, ngx_http_qjs_ext_send), JS_CFUNC_DEF("sendBuffer", 2, ngx_http_qjs_ext_send_buffer), JS_CFUNC_DEF("sendHeader", 0, ngx_http_qjs_ext_send_header), @@ -1211,6 +1241,85 @@ qjs_module_t *njs_http_qjs_addon_modules[] = { #endif +static ngx_int_t +ngx_http_js_access_handler(ngx_http_request_t *r) +{ + ngx_int_t rc; + ngx_http_js_ctx_t *ctx; + ngx_http_js_loc_conf_t *jlcf; + + jlcf = ngx_http_get_module_loc_conf(r, ngx_http_js_module); + + if (jlcf->access.len == 0) { + return NGX_DECLINED; + } + + if (r != r->main) { + return NGX_DECLINED; + } + + rc = ngx_http_js_init_vm(r, ngx_http_js_request_proto_id); + if (rc != NGX_OK) { + return rc; + } + + ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + + if (ctx->in_progress) { + if (ngx_js_ctx_pending(ctx)) { + return NGX_AGAIN; + } + + ctx->in_progress = 0; + + if (ctx->rejected_promises != NULL + && ctx->rejected_promises->items > 0) + { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + return ctx->status; + } + + ctx->status = NGX_OK; + + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http js access handler \"%V\"", &jlcf->access); + + rc = ctx->engine->call((ngx_js_ctx_t *) ctx, &jlcf->access, &ctx->args[0], + 1); + + if (rc == NGX_ERROR) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + if (ngx_js_ctx_pending(ctx)) { + ctx->in_progress = 1; + r->write_event_handler = ngx_http_js_access_write_event_handler; + return NGX_AGAIN; + } + + return ctx->status; +} + + +static void +ngx_http_js_access_write_event_handler(ngx_http_request_t *r) +{ + ngx_http_js_ctx_t *ctx; + + ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http js access write event handler"); + + if (!ngx_js_ctx_pending(ctx)) { + ngx_http_core_run_phases(r); + return; + } +} + + static ngx_int_t ngx_http_js_content_handler(ngx_http_request_t *r) { @@ -2822,6 +2931,30 @@ ngx_http_js_ext_return(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, } +static njs_int_t +ngx_http_js_ext_decline(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, + njs_index_t unused, njs_value_t *retval) +{ + ngx_http_js_ctx_t *ctx; + ngx_http_request_t *r; + + r = njs_vm_external(vm, ngx_http_js_request_proto_id, + njs_argument(args, 0)); + if (r == NULL) { + njs_vm_error(vm, "\"this\" is not an external"); + return NJS_ERROR; + } + + ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + + ctx->status = NGX_DECLINED; + + njs_value_undefined_set(retval); + + return NJS_OK; +} + + static njs_int_t ngx_http_js_ext_internal_redirect(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval) @@ -5502,6 +5635,26 @@ ngx_http_qjs_ext_return(JSContext *cx, JSValueConst this_val, } +static JSValue +ngx_http_qjs_ext_decline(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + ngx_http_js_ctx_t *ctx; + ngx_http_request_t *r; + + r = ngx_http_qjs_request(this_val); + if (r == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + + ctx->status = NGX_DECLINED; + + return JS_UNDEFINED; +} + + static JSValue ngx_http_qjs_ext_status_get(JSContext *cx, JSValueConst this_val) { @@ -7773,12 +7926,24 @@ ngx_http_js_init_conf_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf) static ngx_int_t ngx_http_js_init(ngx_conf_t *cf) { + ngx_http_handler_pt *h; + ngx_http_core_main_conf_t *cmcf; + ngx_http_next_header_filter = ngx_http_top_header_filter; ngx_http_top_header_filter = ngx_http_js_header_filter; ngx_http_next_body_filter = ngx_http_top_body_filter; ngx_http_top_body_filter = ngx_http_js_body_filter; + cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module); + + h = ngx_array_push(&cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers); + if (h == NULL) { + return NGX_ERROR; + } + + *h = ngx_http_js_access_handler; + return NGX_OK; } @@ -8156,6 +8321,24 @@ ngx_http_js_var(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) } +static char * +ngx_http_js_access(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_http_js_loc_conf_t *jlcf = conf; + + ngx_str_t *value; + + if (jlcf->access.data) { + return "is duplicate"; + } + + value = cf->args->elts; + jlcf->access = value[1]; + + return NGX_CONF_OK; +} + + static char * ngx_http_js_content(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { @@ -8275,6 +8458,7 @@ ngx_http_js_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) ngx_http_js_loc_conf_t *prev = parent; ngx_http_js_loc_conf_t *conf = child; + ngx_conf_merge_str_value(conf->access, prev->access, ""); ngx_conf_merge_str_value(conf->content, prev->content, ""); ngx_conf_merge_str_value(conf->header_filter, prev->header_filter, ""); ngx_conf_merge_str_value(conf->body_filter, prev->body_filter, ""); @@ -8287,6 +8471,15 @@ ngx_http_js_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) return NGX_CONF_ERROR; } + if (conf->access.len != 0) { + if (conf->imports == NGX_CONF_UNSET_PTR) { + ngx_log_error(NGX_LOG_EMERG, cf->log, 0, + "no imports defined for \"js_access\" \"%V\", " + "use \"js_import\" directive", &conf->access); + return NGX_CONF_ERROR; + } + } + if (conf->content.len != 0) { if (conf->imports == NGX_CONF_UNSET_PTR) { ngx_log_error(NGX_LOG_EMERG, cf->log, 0, diff --git a/nginx/t/js_access.t b/nginx/t/js_access.t new file mode 100644 index 00000000..29ac026e --- /dev/null +++ b/nginx/t/js_access.t @@ -0,0 +1,400 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) F5, Inc. + +# Tests for http njs module, js_access directive. + +############################################################################### + +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 rewrite proxy/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_import test.js; + + js_var $foo; + js_var $upstream; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /var { + js_content test.content; + } + + location /deny { + js_access test.deny; + js_content test.content; + } + + location /exception { + js_access test.exception; + js_content test.content; + } + + location /noop { + js_access test.noop; + js_content test.content; + } + + location /decline { + js_access test.decline; + js_content test.content; + } + + location /override { + js_access test.override; + js_content test.content; + } + + location /content_only { + js_content test.content_only; + } + + location /async_timeout { + js_access test.async_timeout; + js_content test.content; + } + + location /async_deny { + js_access test.async_deny; + js_content test.content; + } + + location /async_exception { + js_access test.async_exception; + js_content test.content; + } + + location /sr_skip { + js_content test.sr_skip; + } + + location /sub { + js_access test.deny; + js_content test.content; + } + + location /sr { + js_access test.sr; + js_content test.content; + } + + location /fetch { + js_access test.fetch; + js_content test.content; + } + + location /route { + js_access test.route; + proxy_pass http://$upstream; + } + + location /auth_check { + js_content test.auth_check; + } + + location /redirect { + js_access test.redirect; + js_content test.content; + } + + location /redirect_async { + js_access test.redirect_async; + js_content test.content; + } + + location /callback { + js_content test.content; + } + } + + server { + listen 127.0.0.1:8080; + server_name noaccess; + + location /no_access { + js_content test.content_only; + } + } + + server { + listen 127.0.0.1:8081; + + location / { + return 200 "backend1"; + } + } + + server { + listen 127.0.0.1:8082; + + location / { + return 200 "backend2"; + } + } +} + +EOF + +my $p0 = port(8080); +my $p1 = port(8081); +my $p2 = port(8082); + +$t->write_file('test.js', < setTimeout(resolve, 5)); + r.variables.foo = 'timeout_ok'; + } + + async function async_deny(r) { + await new Promise(resolve => setTimeout(resolve, 5)); + r.return(403); + } + + async function async_exception(r) { + await new Promise(resolve => setTimeout(resolve, 5)); + throw new Error("async_access_error"); + } + + async function sr_skip(r) { + let reply = await r.subrequest('/sub'); + r.return(reply.status, reply.responseText); + } + + async function fetch(r) { + let resp = await ngx.fetch( + \`http://127.0.0.1:$p0/auth_check?token=\${r.variables.arg_token}\`); + + if (resp.status != 200) { + r.return(resp.status); + return; + } + + r.variables.foo = await resp.text(); + } + + async function sr(r) { + let reply = await r.subrequest('/auth_check?token=' + + r.variables.arg_token); + if (reply.status != 200) { + r.return(reply.status); + return; + } + + r.variables.foo = reply.responseText; + } + + function route(r) { + let dest = r.variables.arg_dest; + r.variables.upstream = (dest === 'one') + ? '127.0.0.1:$p1' : '127.0.0.1:$p2'; + } + + function auth_check(r) { + let token = r.variables.arg_token; + + if (token === 'valid') { + r.return(200, 'authenticated'); + } else { + r.return(403); + } + } + + function redirect(r) { + r.return(302, 'http://127.0.0.1:$p0/callback'); + } + + async function redirect_async(r) { + await new Promise(resolve => setTimeout(resolve, 5)); + r.return(302, 'http://127.0.0.1:$p0/callback'); + } + + export default { content, deny, exception, noop, override, + decline, content_only, async_timeout, async_deny, + async_exception, sr_skip, sr, fetch, route, + auth_check, redirect, redirect_async }; + +EOF + +$t->write_file_expand('duplicate.conf', bad_conf( + http => 'js_import test.js;', + location => 'js_access test.noop; js_access test.noop;')); + +$t->write_file_expand('no_import.conf', bad_conf( + location => 'js_access test.noop;')); + +$t->try_run('no js_access')->plan(26); + +############################################################################### + +like(http_get('/deny'), qr/403 Forbidden/, + 'js_access sync r.return(403) rejects'); +like(http_post('/deny'), qr/403 Forbidden/, + 'js_access deny with request body'); +like(http_get('/exception'), qr/500 Internal Server Error/, + 'js_access sync exception returns 500'); +like(http_get('/noop'), qr/var:/, + 'js_access noop continues to content'); +like(http_get('/decline'), qr/var:/, + 'js_access decline continues to content'); +like(http_get('/override'), qr/var:overridden/, + 'js_access override in location'); +like(http_get('/content_only'), qr/content_only/, + 'js_content without js_access'); +like(http("GET /no_access HTTP/1.0" . CRLF . + "Host: noaccess" . CRLF . CRLF), + qr/content_only/, + 'js_access not inherited in sibling server'); +like(http_get('/async_timeout'), qr/var:timeout_ok/, + 'async js_access with setTimeout'); +like(http_get('/async_deny'), qr/403 Forbidden/, + 'async js_access r.return(403) rejects'); +like(http_get('/async_exception'), qr/500 Internal Server Error/, + 'async js_access exception returns 500'); +like(http_get('/sr_skip'), qr/var:/, + 'js_access skipped for subrequests'); +like(http_get('/sr?token=valid'), qr/var:authenticated/, + 'subrequest access allow'); +like(http_get('/sr?token=invalid'), qr/403 Forbidden/, + 'subrequest access deny'); +like(http_get('/fetch?token=valid'), qr/var:authenticated/, + 'fetch access allow'); +like(http_get('/fetch?token=invalid'), qr/403 Forbidden/, + 'fetch access deny'); +like(http_get('/route?dest=one'), qr/backend1/, + 'variable routing to backend1'); +like(http_get('/route?dest=two'), qr/backend2/, + 'variable routing to backend2'); +like(http_get('/redirect'), qr/302 Moved/, + 'js_access sync redirect'); +like(http_get('/redirect'), qr!Location: http://127.0.0.1:$p0/callback!, + 'js_access sync redirect Location header'); +like(http_get('/redirect_async'), qr/302 Moved/, + 'js_access async redirect'); +like(http_get('/redirect_async'), qr!Location: http://127.0.0.1:$p0/callback!, + 'js_access async redirect Location header'); + +my ($rc, $out) = nginx_test_conf($t, 'duplicate.conf'); + +isnt($rc, 0, 'duplicate js_access fails'); +like($out, qr/"js_access" directive is duplicate/, + 'duplicate js_access error'); + +($rc, $out) = nginx_test_conf($t, 'no_import.conf'); + +isnt($rc, 0, 'js_access without js_import fails'); +like($out, qr/no imports defined for "js_access" "test\.noop"/, + 'js_access without js_import error'); + +############################################################################### + +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); +} + +sub nginx_test_conf { + my ($t, $conf) = @_; + my $testdir = $t->testdir(); + my $cmd = "$Test::Nginx::NGINX -p $testdir/ -c $conf -t " + . "-e error.log 2>&1"; + + my $out = `$cmd`; + + return ($? >> 8, $out); +} + +sub bad_conf { + my %args = @_; + my $http = $args{http} // ''; + my $loc = $args{location} // ''; + + return <<"EOF"; + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + $http + + server { + listen 127.0.0.1:8080; + + location / { + $loc + } + } +} + +EOF +} diff --git a/nginx/t/js_access_satisfy.t b/nginx/t/js_access_satisfy.t new file mode 100644 index 00000000..926a496c --- /dev/null +++ b/nginx/t/js_access_satisfy.t @@ -0,0 +1,175 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) F5, Inc. + +# Tests for http njs module, js_access directive with satisfy. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http rewrite access/) + ->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 /all_decline_allow { + satisfy all; + allow all; + js_access test.decline; + js_content test.content; + } + + location /all_decline_deny { + satisfy all; + deny all; + js_access test.decline; + js_content test.content; + } + + location /any_allow_deny { + satisfy any; + deny all; + js_access test.allow; + js_content test.content; + } + + location /any_deny_allow { + satisfy any; + allow all; + js_access test.deny; + js_content test.content; + } + + location /any_both_deny { + satisfy any; + deny all; + js_access test.deny; + js_content test.content; + } + + location /any_decline_deny { + satisfy any; + deny all; + js_access test.decline; + js_content test.content; + } + + location /any_decline_allow { + satisfy any; + allow all; + js_access test.decline; + js_content test.content; + } + + location /any_async_allow_deny { + satisfy any; + deny all; + js_access test.async_allow; + js_content test.content; + } + + location /any_async_decline_deny { + satisfy any; + deny all; + js_access test.async_decline; + js_content test.content; + } + } +} + +EOF + +$t->write_file('test.js', <<'EOF'); + function allow(r) { + /* default: normal return yields NGX_OK */ + } + + function deny(r) { + r.return(403); + } + + function decline(r) { + r.decline(); + } + + async function async_allow(r) { + await new Promise(resolve => setTimeout(resolve, 5)); + } + + async function async_decline(r) { + await new Promise(resolve => setTimeout(resolve, 5)); + r.decline(); + } + + function content(r) { + r.return(200, 'PASSED'); + } + + export default { allow, deny, decline, async_allow, async_decline, + content }; +EOF + +$t->try_run('no js_access')->plan(9); + +############################################################################### + +# satisfy all + decline: ip decides +like(http_get('/all_decline_allow'), qr/PASSED/, + 'satisfy all: js declines + ip allows'); +like(http_get('/all_decline_deny'), qr/403 Forbidden/, + 'satisfy all: js declines + ip denies'); + +# satisfy any: js allows overrides ip deny +like(http_get('/any_allow_deny'), qr/PASSED/, + 'satisfy any: js allows + ip denies'); + +# satisfy any: ip allows overrides js deny +like(http_get('/any_deny_allow'), qr/PASSED/, + 'satisfy any: js denies + ip allows'); + +# satisfy any: both deny +like(http_get('/any_both_deny'), qr/403 Forbidden/, + 'satisfy any: both deny'); + +# satisfy any + decline: js has no opinion, ip decides +like(http_get('/any_decline_deny'), qr/403 Forbidden/, + 'satisfy any: js declines + ip denies'); +like(http_get('/any_decline_allow'), qr/PASSED/, + 'satisfy any: js declines + ip allows'); + +# async variants +like(http_get('/any_async_allow_deny'), qr/PASSED/, + 'satisfy any: async js allows + ip denies'); +like(http_get('/any_async_decline_deny'), qr/403 Forbidden/, + 'satisfy any: async js declines + ip denies'); + +############################################################################### diff --git a/ts/ngx_http_js_module.d.ts b/ts/ngx_http_js_module.d.ts index d7dd1c9e..b0af0c5a 100644 --- a/ts/ngx_http_js_module.d.ts +++ b/ts/ngx_http_js_module.d.ts @@ -414,6 +414,13 @@ interface NginxHTTPRequest { * @param body Respose body. */ return(status: number, body?: NjsStringOrBuffer): void; + /** + * Signals that the handler has no opinion about whether access + * should be allowed or denied. Useful with the ``satisfy any`` + * directive: without this call the handler implicitly allows + * access (returns NGX_OK to the access phase checker). + */ + decline(): void; /** * Sends a part of the response body to the client. */