From: Dmitry Volyntsev Date: Sat, 16 May 2026 22:14:37 +0000 (-0700) Subject: Modules: added jsVarNames() method. X-Git-Tag: 0.9.9~1 X-Git-Url: http://git.kaiwu.me/postgresql/log/contrib/postgres_fdw/NGINX-js-1660x332.png%20%22NGINX%20JavaScript%20Banner%22?a=commitdiff_plain;h=f9ff315e6ad3cafdaaae3847fde79115f7a1143f;p=njs.git Modules: added jsVarNames() method. The method returns names of variables declared with js_var. An optional string argument filters returned names by prefix. This lets JavaScript code discover a configuration-defined variable schema without duplicating every variable name in the script. For example: js_var $extract_name; js_var $extract_arguments_workspace; function handler(r) { r.jsVarNames("extract_").forEach(function(name) { r.variables[name] = extract(name.slice(8)); }); } The same API is available as s.jsVarNames() in stream. Variables created by js_set or by the core modules are not returned. --- diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c index 2ac39637..530bea32 100644 --- a/nginx/ngx_http_js_module.c +++ b/nginx/ngx_http_js_module.c @@ -366,6 +366,9 @@ static njs_int_t ngx_http_js_ext_variables(njs_vm_t *vm, static njs_int_t ngx_http_js_periodic_session_variables(njs_vm_t *vm, njs_object_prop_t *prop, uint32_t atom_id, njs_value_t *value, njs_value_t *setval, njs_value_t *retval); +static njs_int_t ngx_http_js_ext_js_var_names(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_subrequest(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval); static ngx_int_t ngx_http_js_subrequest_done(ngx_http_request_t *r, @@ -461,6 +464,8 @@ static JSValue ngx_http_qjs_ext_raw_headers(JSContext *cx, JSValueConst this_val, int out); static JSValue ngx_http_qjs_ext_variables(JSContext *cx, JSValueConst this_val, int type); +static JSValue ngx_http_qjs_ext_js_var_names(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); static int ngx_http_qjs_variables_own_property(JSContext *cx, JSPropertyDescriptor *pdesc, JSValueConst obj, JSAtom prop); @@ -1115,6 +1120,17 @@ static njs_external_t ngx_http_js_ext_request[] = { } }, + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("jsVarNames"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = ngx_http_js_ext_js_var_names, + } + }, + { .flags = NJS_EXTERN_PROPERTY, .name.string = njs_str("status"), @@ -1407,6 +1423,7 @@ static const JSCFunctionListEntry ngx_http_qjs_ext_request[] = { JS_CFUNC_DEF("sendBuffer", 2, ngx_http_qjs_ext_send_buffer), JS_CFUNC_DEF("sendHeader", 0, ngx_http_qjs_ext_send_header), JS_CFUNC_DEF("setReturnValue", 1, ngx_http_qjs_ext_set_return_value), + JS_CFUNC_DEF("jsVarNames", 1, ngx_http_qjs_ext_js_var_names), JS_CGETSET_DEF("status", ngx_http_qjs_ext_status_get, ngx_http_qjs_ext_status_set), JS_CFUNC_MAGIC_DEF("readRequestArrayBuffer", 0, @@ -4439,6 +4456,88 @@ ngx_http_js_ext_keys_header_in(njs_vm_t *vm, njs_value_t *value, } +static ngx_uint_t +ngx_http_js_var_name_matches(ngx_str_t *name, const u_char *prefix, + size_t prefix_len) +{ + if (prefix_len == 0) { + return 1; + } + + return name->len >= prefix_len + && ngx_memcmp(name->data, prefix, prefix_len) == 0; +} + + +static njs_int_t +ngx_http_js_ext_js_var_names(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t unused, njs_value_t *retval) +{ + njs_int_t rc; + njs_str_t prefix; + njs_value_t *arg, *value; + ngx_uint_t i; + ngx_http_request_t *r; + ngx_http_variable_t *v; + ngx_http_core_main_conf_t *cmcf; + + 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; + } + + prefix.start = NULL; + prefix.length = 0; + + arg = njs_arg(args, nargs, 1); + + if (!njs_value_is_undefined(arg)) { + if (!njs_value_is_string(arg)) { + njs_vm_type_error(vm, "\"prefix\" must be a string"); + return NJS_ERROR; + } + + njs_value_string_get(vm, arg, &prefix); + } + + cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module); + + rc = njs_vm_array_alloc(vm, retval, 4); + if (rc != NJS_OK) { + return NJS_ERROR; + } + + v = cmcf->variables.elts; + + for (i = 0; i < cmcf->variables.nelts; i++) { + if (v[i].get_handler != ngx_http_js_variable_var) { + continue; + } + + if (!ngx_http_js_var_name_matches(&v[i].name, prefix.start, + prefix.length)) + { + continue; + } + + value = njs_vm_array_push(vm, retval); + if (value == NULL) { + return NJS_ERROR; + } + + rc = njs_vm_value_string_create(vm, value, v[i].name.data, + v[i].name.len); + if (rc != NJS_OK) { + return NJS_ERROR; + } + } + + return NJS_OK; +} + + static njs_int_t ngx_http_js_request_variables(njs_vm_t *vm, njs_object_prop_t *prop, uint32_t atom_id, ngx_http_request_t *r, njs_value_t *setval, @@ -7910,6 +8009,81 @@ ngx_http_qjs_ext_variables(JSContext *cx, JSValueConst this_val, int type) } +static JSValue +ngx_http_qjs_ext_js_var_names(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + JSValue array, value; + uint32_t n; + size_t prefix_len; + const char *prefix; + ngx_uint_t i; + ngx_http_request_t *r; + ngx_http_variable_t *v; + ngx_http_core_main_conf_t *cmcf; + + r = ngx_http_qjs_request(this_val); + if (r == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + prefix = NULL; + prefix_len = 0; + + if (argc > 0 && !JS_IsUndefined(argv[0])) { + if (!JS_IsString(argv[0])) { + return JS_ThrowTypeError(cx, "\"prefix\" must be a string"); + } + + prefix = JS_ToCStringLen(cx, &prefix_len, argv[0]); + if (prefix == NULL) { + return JS_EXCEPTION; + } + } + + array = JS_NewArray(cx); + if (JS_IsException(array)) { + JS_FreeCString(cx, prefix); + return JS_EXCEPTION; + } + + n = 0; + cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module); + v = cmcf->variables.elts; + + for (i = 0; i < cmcf->variables.nelts; i++) { + if (v[i].get_handler != ngx_http_js_variable_var) { + continue; + } + + if (!ngx_http_js_var_name_matches(&v[i].name, (u_char *) prefix, + prefix_len)) + { + continue; + } + + value = qjs_string_create(cx, v[i].name.data, v[i].name.len); + if (JS_IsException(value)) { + JS_FreeCString(cx, prefix); + JS_FreeValue(cx, array); + return JS_EXCEPTION; + } + + if (JS_DefinePropertyValueUint32(cx, array, n++, value, + JS_PROP_C_W_E) < 0) + { + JS_FreeCString(cx, prefix); + JS_FreeValue(cx, array); + return JS_EXCEPTION; + } + } + + JS_FreeCString(cx, prefix); + + return array; +} + + static int ngx_http_qjs_variables_own_property(JSContext *cx, JSPropertyDescriptor *pdesc, JSValueConst obj, JSAtom prop) diff --git a/nginx/ngx_stream_js_module.c b/nginx/ngx_stream_js_module.c index 3b12a335..5e550c57 100644 --- a/nginx/ngx_stream_js_module.c +++ b/nginx/ngx_stream_js_module.c @@ -138,6 +138,9 @@ static njs_int_t ngx_stream_js_ext_send(njs_vm_t *vm, njs_value_t *args, static njs_int_t ngx_stream_js_ext_set_return_value(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_stream_js_ext_js_var_names(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_stream_js_ext_variables(njs_vm_t *vm, njs_object_prop_t *prop, uint32_t atom_id, njs_value_t *value, @@ -164,6 +167,8 @@ static JSValue ngx_stream_qjs_ext_send(JSContext *cx, JSValueConst this_val, int argc, JSValueConst *argv, int from_upstream); static JSValue ngx_stream_qjs_ext_set_return_value(JSContext *cx, JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_stream_qjs_ext_js_var_names(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); static JSValue ngx_stream_qjs_ext_variables(JSContext *cx, JSValueConst this_val, int type); static JSValue ngx_stream_qjs_ext_uint(JSContext *cx, JSValueConst this_val, @@ -676,6 +681,17 @@ static njs_external_t ngx_stream_js_ext_session[] = { } }, + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("jsVarNames"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = ngx_stream_js_ext_js_var_names, + } + }, + { .flags = NJS_EXTERN_PROPERTY, .name.string = njs_str("status"), @@ -837,6 +853,7 @@ static const JSCFunctionListEntry ngx_stream_qjs_ext_session[] = { JS_CFUNC_MAGIC_DEF("sendUpstream", 1, ngx_stream_qjs_ext_send, NGX_JS_BOOL_FALSE), JS_CFUNC_DEF("setReturnValue", 1, ngx_stream_qjs_ext_set_return_value), + JS_CFUNC_DEF("jsVarNames", 1, ngx_stream_qjs_ext_js_var_names), JS_CGETSET_MAGIC_DEF("status", ngx_stream_qjs_ext_uint, NULL, offsetof(ngx_stream_session_t, status)), JS_CGETSET_MAGIC_DEF("variables", ngx_stream_qjs_ext_variables, @@ -1751,6 +1768,88 @@ ngx_stream_js_ext_set_return_value(njs_vm_t *vm, njs_value_t *args, } +static ngx_uint_t +ngx_stream_js_var_name_matches(ngx_str_t *name, const u_char *prefix, + size_t prefix_len) +{ + if (prefix_len == 0) { + return 1; + } + + return name->len >= prefix_len + && ngx_memcmp(name->data, prefix, prefix_len) == 0; +} + + +static njs_int_t +ngx_stream_js_ext_js_var_names(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t unused, njs_value_t *retval) +{ + njs_int_t rc; + njs_str_t prefix; + njs_value_t *arg, *value; + ngx_uint_t i; + ngx_stream_session_t *s; + ngx_stream_variable_t *v; + ngx_stream_core_main_conf_t *cmcf; + + s = njs_vm_external(vm, ngx_stream_js_session_proto_id, + njs_argument(args, 0)); + if (s == NULL) { + njs_vm_error(vm, "\"this\" is not an external"); + return NJS_ERROR; + } + + prefix.start = NULL; + prefix.length = 0; + + arg = njs_arg(args, nargs, 1); + + if (!njs_value_is_undefined(arg)) { + if (!njs_value_is_string(arg)) { + njs_vm_type_error(vm, "\"prefix\" must be a string"); + return NJS_ERROR; + } + + njs_value_string_get(vm, arg, &prefix); + } + + cmcf = ngx_stream_get_module_main_conf(s, ngx_stream_core_module); + + rc = njs_vm_array_alloc(vm, retval, 4); + if (rc != NJS_OK) { + return NJS_ERROR; + } + + v = cmcf->variables.elts; + + for (i = 0; i < cmcf->variables.nelts; i++) { + if (v[i].get_handler != ngx_stream_js_variable_var) { + continue; + } + + if (!ngx_stream_js_var_name_matches(&v[i].name, prefix.start, + prefix.length)) + { + continue; + } + + value = njs_vm_array_push(vm, retval); + if (value == NULL) { + return NJS_ERROR; + } + + rc = njs_vm_value_string_create(vm, value, v[i].name.data, + v[i].name.len); + if (rc != NJS_OK) { + return NJS_ERROR; + } + } + + return NJS_OK; +} + + static njs_int_t ngx_stream_js_session_variables(njs_vm_t *vm, njs_object_prop_t *prop, uint32_t atom_id, ngx_stream_session_t *s, njs_value_t *setval, @@ -2520,6 +2619,81 @@ ngx_stream_qjs_ext_variables(JSContext *cx, JSValueConst this_val, int type) } +static JSValue +ngx_stream_qjs_ext_js_var_names(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + JSValue array, value; + uint32_t n; + size_t prefix_len; + const char *prefix; + ngx_uint_t i; + ngx_stream_session_t *s; + ngx_stream_variable_t *v; + ngx_stream_core_main_conf_t *cmcf; + + s = ngx_stream_qjs_session(this_val); + if (s == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a session object"); + } + + prefix = NULL; + prefix_len = 0; + + if (argc > 0 && !JS_IsUndefined(argv[0])) { + if (!JS_IsString(argv[0])) { + return JS_ThrowTypeError(cx, "\"prefix\" must be a string"); + } + + prefix = JS_ToCStringLen(cx, &prefix_len, argv[0]); + if (prefix == NULL) { + return JS_EXCEPTION; + } + } + + array = JS_NewArray(cx); + if (JS_IsException(array)) { + JS_FreeCString(cx, prefix); + return JS_EXCEPTION; + } + + n = 0; + cmcf = ngx_stream_get_module_main_conf(s, ngx_stream_core_module); + v = cmcf->variables.elts; + + for (i = 0; i < cmcf->variables.nelts; i++) { + if (v[i].get_handler != ngx_stream_js_variable_var) { + continue; + } + + if (!ngx_stream_js_var_name_matches(&v[i].name, (u_char *) prefix, + prefix_len)) + { + continue; + } + + value = qjs_string_create(cx, v[i].name.data, v[i].name.len); + if (JS_IsException(value)) { + JS_FreeCString(cx, prefix); + JS_FreeValue(cx, array); + return JS_EXCEPTION; + } + + if (JS_DefinePropertyValueUint32(cx, array, n++, value, + JS_PROP_C_W_E) < 0) + { + JS_FreeCString(cx, prefix); + JS_FreeValue(cx, array); + return JS_EXCEPTION; + } + } + + JS_FreeCString(cx, prefix); + + return array; +} + + static JSValue ngx_stream_qjs_ext_uint(JSContext *cx, JSValueConst this_val, int offset) { diff --git a/nginx/t/js_var_names.t b/nginx/t/js_var_names.t new file mode 100644 index 00000000..0054d5b3 --- /dev/null +++ b/nginx/t/js_var_names.t @@ -0,0 +1,178 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) F5, Inc. + +# Tests for http njs module, r.jsVarNames() method. + +############################################################################### + +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/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_import test.js; + + js_var $foo; + js_var $test_method; + js_var $test_params_name; + js_var $test_params_arguments_workspace; + js_var $other_name; + js_set $js_set_var test.set_var; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + set $set_var conf_set; + + location /all { + js_content test.all; + } + + location /prefix { + js_content test.prefix; + } + + location /empty_prefix { + js_content test.empty_prefix; + } + + location /none { + js_content test.none; + } + + location /array { + js_content test.array; + } + + location /excludes { + js_content test.excludes; + } + + location /fresh { + js_content test.fresh; + } + + location /type { + js_content test.type; + } + } +} + +EOF + +$t->write_file('test.js', <<'EOF'); + function render(names) { + return names.sort().join('|'); + } + + function all(r) { + r.return(200, render(r.jsVarNames())); + } + + function prefix(r) { + r.return(200, render(r.jsVarNames('test_'))); + } + + function empty_prefix(r) { + r.return(200, render(r.jsVarNames(''))); + } + + function none(r) { + r.return(200, String(r.jsVarNames('none_').length)); + } + + function array(r) { + r.return(200, String(Array.isArray(r.jsVarNames()))); + } + + function excludes(r) { + let names = r.jsVarNames(); + + r.return(200, String(names.indexOf('js_set_var') == -1 + && names.indexOf('set_var') == -1 + && names.indexOf('remote_addr') == -1)); + } + + function fresh(r) { + let names = r.jsVarNames('test_'); + names.push('test_bad'); + + r.return(200, r.jsVarNames('test_').indexOf('test_bad') == -1 + ? 'fresh' : 'shared'); + } + + function type(r) { + try { + r.jsVarNames(1); + r.return(200, 'no error'); + + } catch (e) { + r.return(200, e.name + ':' + e.message); + } + } + + function set_var(r) { + return 'set'; + } + + export default {all, prefix, empty_prefix, none, array, excludes, fresh, + type, set_var}; +EOF + +$t->try_run('no r.jsVarNames')->plan(8); + +############################################################################### + +my $all = 'foo|other_name|test_method|' + . 'test_params_arguments_workspace|test_params_name'; + +my $prefix = 'test_method|test_params_arguments_workspace|test_params_name'; + +is(http_get_body('/all'), $all, 'jsVarNames all js_var names'); +is(http_get_body('/prefix'), $prefix, 'jsVarNames prefix'); +is(http_get_body('/empty_prefix'), $all, 'jsVarNames empty prefix'); +is(http_get_body('/none'), '0', 'jsVarNames prefix no match'); +is(http_get_body('/array'), 'true', 'jsVarNames returns an array'); +is(http_get_body('/excludes'), 'true', 'jsVarNames excludes other variables'); +is(http_get_body('/fresh'), 'fresh', 'jsVarNames fresh array'); +like(http_get_body('/type'), qr/^TypeError:.*prefix.*string/, + 'jsVarNames prefix type'); + +$t->stop(); + +############################################################################### + +sub http_get_body { + my ($uri) = @_; + + http_get($uri) =~ /\x0d\x0a?\x0d\x0a?(.*)/ms; + return $1; +} + +############################################################################### diff --git a/nginx/t/stream_js_var_names.t b/nginx/t/stream_js_var_names.t new file mode 100644 index 00000000..9c949195 --- /dev/null +++ b/nginx/t/stream_js_var_names.t @@ -0,0 +1,184 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) F5, Inc. + +# Tests for stream njs module, s.jsVarNames() method. + +############################################################################### + +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/stream stream_return/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +stream { + %%TEST_GLOBALS_STREAM%% + + js_import test.js; + + js_var $foo; + js_var $test_method; + js_var $test_params_name; + js_var $test_params_arguments_workspace; + js_var $other_name; + js_set $js_set_var test.set_var; + js_set $all test.all; + js_set $prefix test.prefix; + js_set $empty_prefix test.empty_prefix; + js_set $none test.none; + js_set $array test.array; + js_set $excludes test.excludes; + js_set $fresh test.fresh; + js_set $type test.type; + server { + listen 127.0.0.1:8081; + return $all; + } + + server { + listen 127.0.0.1:8082; + return $prefix; + } + + server { + listen 127.0.0.1:8083; + return $empty_prefix; + } + + server { + listen 127.0.0.1:8084; + return $none; + } + + server { + listen 127.0.0.1:8085; + return $array; + } + + server { + listen 127.0.0.1:8086; + return $excludes; + } + + server { + listen 127.0.0.1:8087; + return $fresh; + } + + server { + listen 127.0.0.1:8088; + return $type; + } +} + +EOF + +$t->write_file('test.js', <<'EOF'); + function render(names) { + return names.sort().join('|'); + } + + function all(s) { + return render(s.jsVarNames()); + } + + function prefix(s) { + return render(s.jsVarNames('test_')); + } + + function empty_prefix(s) { + return render(s.jsVarNames('')); + } + + function none(s) { + return String(s.jsVarNames('none_').length); + } + + function array(s) { + return String(Array.isArray(s.jsVarNames())); + } + + function excludes(s) { + let names = s.jsVarNames(); + + return String(names.indexOf('js_set_var') == -1 + && names.indexOf('remote_addr') == -1); + } + + function fresh(s) { + let names = s.jsVarNames('test_'); + names.push('test_bad'); + + return s.jsVarNames('test_').indexOf('test_bad') == -1 + ? 'fresh' : 'shared'; + } + + function type(s) { + try { + s.jsVarNames(1); + return 'no error'; + + } catch (e) { + return e.name + ':' + e.message; + } + } + + function set_var(s) { + return 'set'; + } + + export default {all, prefix, empty_prefix, none, array, excludes, fresh, + type, set_var}; +EOF + +$t->try_run('no s.jsVarNames')->plan(8); + +############################################################################### + +my $all = 'foo|other_name|test_method|' + . 'test_params_arguments_workspace|test_params_name'; + +my $prefix = 'test_method|test_params_arguments_workspace|test_params_name'; + +is(stream('127.0.0.1:' . port(8081))->read(), $all, + 'jsVarNames all js_var names'); +is(stream('127.0.0.1:' . port(8082))->read(), $prefix, + 'jsVarNames prefix'); +is(stream('127.0.0.1:' . port(8083))->read(), $all, + 'jsVarNames empty prefix'); +is(stream('127.0.0.1:' . port(8084))->read(), '0', + 'jsVarNames prefix no match'); +is(stream('127.0.0.1:' . port(8085))->read(), 'true', + 'jsVarNames returns an array'); +is(stream('127.0.0.1:' . port(8086))->read(), 'true', + 'jsVarNames excludes other variables'); +is(stream('127.0.0.1:' . port(8087))->read(), 'fresh', + 'jsVarNames fresh array'); +like(stream('127.0.0.1:' . port(8088))->read(), + qr/^TypeError:.*prefix.*string/, 'jsVarNames prefix type'); + +$t->stop(); + +###############################################################################