From 55165c8b2834ca46901b38a2910f0502a35a9bc3 Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Thu, 5 Mar 2026 20:44:02 -0800 Subject: [PATCH 1/2] Modules: added var alias for variables object. This allows to reference nginx variables with a shorter syntax: `r.var.uri` or `s.var.bytes_sent`. --- nginx/ngx_http_js_module.c | 24 ++++++++++++++++++++++++ nginx/ngx_stream_js_module.c | 24 ++++++++++++++++++++++++ nginx/t/js_variables.t | 30 +++++++++++++++++++++++++++--- nginx/t/stream_js_variables.t | 33 +++++++++++++++++++++++++++++++-- 4 files changed, 106 insertions(+), 5 deletions(-) diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c index 20308a815..20bc1a64a 100644 --- a/nginx/ngx_http_js_module.c +++ b/nginx/ngx_http_js_module.c @@ -990,6 +990,16 @@ static njs_external_t ngx_http_js_ext_request[] = { } }, + { + .flags = NJS_EXTERN_OBJECT, + .name.string = njs_str("var"), + .u.object = { + .writable = 1, + .prop_handler = ngx_http_js_ext_variables, + .magic32 = NGX_JS_STRING, + } + }, + { .flags = NJS_EXTERN_OBJECT, .name.string = njs_str("variables"), @@ -1034,6 +1044,16 @@ static njs_external_t ngx_http_js_ext_periodic_session[] = { } }, + { + .flags = NJS_EXTERN_OBJECT, + .name.string = njs_str("var"), + .u.object = { + .writable = 1, + .prop_handler = ngx_http_js_periodic_session_variables, + .magic32 = NGX_JS_STRING, + } + }, + { .flags = NJS_EXTERN_OBJECT, .name.string = njs_str("variables"), @@ -1155,6 +1175,8 @@ static const JSCFunctionListEntry ngx_http_qjs_ext_request[] = { JS_CFUNC_DEF("subrequest", 3, ngx_http_qjs_ext_subrequest), JS_CGETSET_MAGIC_DEF("uri", ngx_http_qjs_ext_string, NULL, offsetof(ngx_http_request_t, uri)), + JS_CGETSET_MAGIC_DEF("var", ngx_http_qjs_ext_variables, + NULL, NGX_JS_STRING), JS_CGETSET_MAGIC_DEF("variables", ngx_http_qjs_ext_variables, NULL, NGX_JS_STRING), JS_CFUNC_MAGIC_DEF("warn", 1, ngx_http_qjs_ext_log, NGX_LOG_WARN), @@ -1166,6 +1188,8 @@ static const JSCFunctionListEntry ngx_http_qjs_ext_periodic[] = { JS_PROP_CONFIGURABLE), JS_CGETSET_MAGIC_DEF("rawVariables", ngx_http_qjs_ext_periodic_variables, NULL, NGX_JS_BUFFER), + JS_CGETSET_MAGIC_DEF("var", ngx_http_qjs_ext_periodic_variables, + NULL, NGX_JS_STRING), JS_CGETSET_MAGIC_DEF("variables", ngx_http_qjs_ext_periodic_variables, NULL, NGX_JS_STRING), }; diff --git a/nginx/ngx_stream_js_module.c b/nginx/ngx_stream_js_module.c index b21b701dd..76d8907d5 100644 --- a/nginx/ngx_stream_js_module.c +++ b/nginx/ngx_stream_js_module.c @@ -685,6 +685,16 @@ static njs_external_t ngx_stream_js_ext_session[] = { } }, + { + .flags = NJS_EXTERN_OBJECT, + .name.string = njs_str("var"), + .u.object = { + .writable = 1, + .prop_handler = ngx_stream_js_ext_variables, + .magic32 = NGX_JS_STRING, + } + }, + { .flags = NJS_EXTERN_OBJECT, .name.string = njs_str("variables"), @@ -729,6 +739,16 @@ static njs_external_t ngx_stream_js_ext_periodic_session[] = { } }, + { + .flags = NJS_EXTERN_OBJECT, + .name.string = njs_str("var"), + .u.object = { + .writable = 1, + .prop_handler = ngx_stream_js_periodic_variables, + .magic32 = NGX_JS_STRING, + } + }, + { .flags = NJS_EXTERN_OBJECT, .name.string = njs_str("variables"), @@ -853,6 +873,8 @@ static const JSCFunctionListEntry ngx_stream_qjs_ext_session[] = { JS_CFUNC_DEF("setReturnValue", 1, ngx_stream_qjs_ext_set_return_value), JS_CGETSET_MAGIC_DEF("status", ngx_stream_qjs_ext_uint, NULL, offsetof(ngx_stream_session_t, status)), + JS_CGETSET_MAGIC_DEF("var", ngx_stream_qjs_ext_variables, + NULL, NGX_JS_STRING), JS_CGETSET_MAGIC_DEF("variables", ngx_stream_qjs_ext_variables, NULL, NGX_JS_STRING), JS_CFUNC_MAGIC_DEF("warn", 1, ngx_stream_qjs_ext_log, NGX_LOG_WARN), @@ -864,6 +886,8 @@ static const JSCFunctionListEntry ngx_stream_qjs_ext_periodic[] = { JS_PROP_CONFIGURABLE), JS_CGETSET_MAGIC_DEF("rawVariables", ngx_stream_qjs_ext_periodic_variables, NULL, NGX_JS_BUFFER), + JS_CGETSET_MAGIC_DEF("var", ngx_stream_qjs_ext_periodic_variables, + NULL, NGX_JS_STRING), JS_CGETSET_MAGIC_DEF("variables", ngx_stream_qjs_ext_periodic_variables, NULL, NGX_JS_STRING), }; diff --git a/nginx/t/js_variables.t b/nginx/t/js_variables.t index 6f1eb1735..a22bbcedd 100644 --- a/nginx/t/js_variables.t +++ b/nginx/t/js_variables.t @@ -35,7 +35,8 @@ events { http { %%TEST_GLOBALS_HTTP%% - js_set $test_var test.variable; + js_set $test_var test.variable; + js_set $test_short_var test.short_var; js_import test.js; @@ -50,10 +51,18 @@ http { return 200 $test_var$foo; } + location /var_set_short { + return 200 $test_short_var$foo; + } + location /content_set { js_content test.content_set; } + location /content_set_short { + js_content test.short_content_set; + } + location /not_found_set { js_content test.not_found_set; } @@ -72,11 +81,21 @@ $t->write_file('test.js', <write_file('test.js', <try_run('no njs')->plan(5); +$t->try_run('no njs')->plan(7); ############################################################################### like(http_get('/var_set?a=bar'), qr/test_varbar/, 'var set'); +like(http_get('/var_set_short?a=bar'), qr/short_varbar/, + 'short var set via r.var'); like(http_get('/content_set?a=bar'), qr/bar/, 'content set'); +like(http_get('/content_set_short?a=bar'), qr/bar/, + 'short content set via r.var'); like(http_get('/not_found_set'), qr/variable not found/, 'not found exception'); like(http_get('/variable_lowkey'), qr/X{16}/, 'variable name is not overwritten while reading'); diff --git a/nginx/t/stream_js_variables.t b/nginx/t/stream_js_variables.t index 29e6c33ee..ff6804b1a 100644 --- a/nginx/t/stream_js_variables.t +++ b/nginx/t/stream_js_variables.t @@ -37,7 +37,9 @@ stream { %%TEST_GLOBALS_STREAM%% js_set $test_var test.variable; + js_set $test_short_var test.short_var; js_set $test_not_found test.not_found; + js_set $test_short_not_found test.short_not_found; js_import test.js; @@ -50,6 +52,16 @@ stream { listen 127.0.0.1:8082; return $test_not_found; } + + server { + listen 127.0.0.1:8083; + return $test_short_var$status; + } + + server { + listen 127.0.0.1:8084; + return $test_short_not_found; + } } EOF @@ -60,6 +72,11 @@ $t->write_file('test.js', <write_file('test.js', <try_run('no stream njs available')->plan(2); +$t->try_run('no stream njs available')->plan(4); ############################################################################### is(stream('127.0.0.1:' . port(8081))->read(), 'test_var400', 'var set'); is(stream('127.0.0.1:' . port(8082))->read(), 'not_found', 'not found set'); +is(stream('127.0.0.1:' . port(8083))->read(), 'short_var401', + 'short var set via s.var'); +is(stream('127.0.0.1:' . port(8084))->read(), 'short_not_found', + 'short not found set via s.var'); $t->stop(); From 4abe43c4b470cc140e36c0cc949971751d08180f Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Wed, 11 Feb 2026 13:12:44 -0800 Subject: [PATCH 2/2] Modules: js_set inline JavaScript expressions. Added support for inline JavaScript expressions in the js_set directive. Previously, js_set only accepted function references: js_set $var main.handler; Now it also accepts inline expressions: js_set $var '(r.uri)'; js_set $var 'r.headersIn["Host"] || "none"'; Additionally, nginx-style $variable references are expanded to the corresponding JavaScript variable access. For example: js_set $var '$uri.toUpperCase()'; is equivalent to: js_set $var 'r.variables.uri.toUpperCase()'; In stream context, $var expands to s.variables.var. --- nginx/ngx_http_js_module.c | 32 +- nginx/ngx_js.c | 526 ++++++++++++++++++++++++--- nginx/ngx_js.h | 14 + nginx/ngx_stream_js_module.c | 31 +- nginx/t/js_inline.t | 179 +++++++++ nginx/t/js_inline_only.t | 76 ++++ nginx/t/js_variables_location.t | 5 +- nginx/t/stream_js_variables_server.t | 5 +- 8 files changed, 783 insertions(+), 85 deletions(-) create mode 100644 nginx/t/js_inline.t create mode 100644 nginx/t/js_inline_only.t diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c index 20bc1a64a..05aa31214 100644 --- a/nginx/ngx_http_js_module.c +++ b/nginx/ngx_http_js_module.c @@ -1644,8 +1644,9 @@ ngx_http_js_variable_set(ngx_http_request_t *r, ngx_http_variable_value_t *v, if (rc == NGX_DECLINED) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, - "no \"js_import\" directives found for \"js_set\" handler" - " \"%V\" in the current scope", fname); + "no \"js_import\" or inline expression found" + " for \"js_set\" handler \"%V\" at %s:%ui", + fname, vdata->file_name, vdata->line); v->not_found = 1; return NGX_OK; } @@ -1732,6 +1733,7 @@ ngx_http_js_init_vm(ngx_http_request_t *r, njs_int_t proto_id) } ngx_js_ctx_init((ngx_js_ctx_t *) ctx, r->connection->log); + ctx->conf = (ngx_js_loc_conf_t *) jlcf; ngx_http_set_ctx(r, ctx, ngx_http_js_module); } @@ -8040,9 +8042,12 @@ 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) { - ngx_str_t *value; - ngx_js_set_t *data, *prev; - ngx_http_variable_t *v; + ngx_str_t *value; + ngx_js_set_t *data, *prev; + ngx_http_variable_t *v; + ngx_http_js_loc_conf_t *jlcf; + + static ngx_uint_t ngx_http_js_inline_index; value = cf->args->elts; @@ -8065,20 +8070,25 @@ ngx_http_js_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) return NGX_CONF_ERROR; } - data->fname = value[2]; - data->flags = 0; - data->file_name = cf->conf_file->file.name.data; - data->line = cf->conf_file->line; + jlcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_js_module); + + if (ngx_js_set_init(cf, &jlcf->inlines, &ngx_http_js_inline_index, + &value[2], "r", data) + != NGX_OK) + { + return NGX_CONF_ERROR; + } if (v->get_handler == ngx_http_js_variable_set) { prev = (ngx_js_set_t *) v->data; if (data->fname.len != prev->fname.len - || ngx_strncmp(data->fname.data, prev->fname.data, data->fname.len) != 0) + || ngx_strncmp(data->fname.data, prev->fname.data, + data->fname.len) != 0) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "variable \"%V\" is redeclared with " - "different function name", &value[1]); + "different handler", &value[1]); return NGX_CONF_ERROR; } } diff --git a/nginx/ngx_js.c b/nginx/ngx_js.c index bd1b5f237..25f59f778 100644 --- a/nginx/ngx_js.c +++ b/nginx/ngx_js.c @@ -571,6 +571,7 @@ ngx_engine_njs_init(ngx_engine_t *engine, ngx_engine_opts_t *opts) vm_options.argc = ngx_argc; vm_options.init = 1; + vm_options.file.length = opts->file.length; vm_options.file.start = njs_mp_alloc(engine->pool, opts->file.length); if (vm_options.file.start == NULL) { return NGX_ERROR; @@ -599,21 +600,154 @@ ngx_engine_njs_init(ngx_engine_t *engine, ngx_engine_opts_t *opts) } +/* + * Parse line number from JS exception stack trace. + * + * njs format: + * SyntaxError: ...\n at /path:LINE\n + * + * QuickJS format: + * SyntaxError: ...\n at
:LINE:COL\n + * + * Returns 0 if no line number found. + */ +static ngx_uint_t +ngx_js_error_line(u_char *start, size_t len) +{ + u_char *p, *end; + ngx_uint_t line; + + end = start + len; + + p = ngx_strlchr(start, end, '\n'); + if (p == NULL) { + return 0; + } + + p++; + + while (p < end && *p == ' ') { + p++; + } + + if (end - p < 3 || p[0] != 'a' || p[1] != 't' || p[2] != ' ') { + return 0; + } + + p += 3; + + /* find first ':' followed by a digit */ + + while (p < end && *p != '\n') { + if (*p == ':' && p + 1 < end && p[1] >= '0' && p[1] <= '9') { + p++; + + line = 0; + + while (p < end && *p >= '0' && *p <= '9') { + line = line * 10 + (*p - '0'); + p++; + } + + return line; + } + + p++; + } + + return 0; +} + + +static ngx_js_inline_t * +ngx_js_inline_map(ngx_js_loc_conf_t *conf, u_char *start, size_t len) +{ + ngx_uint_t i, line; + ngx_js_inline_t *inl; + + line = ngx_js_error_line(start, len); + + if (line == 0 || conf->inlines == NGX_CONF_UNSET_PTR) { + return NULL; + } + + i = line - 1; + + if (conf->imports != NGX_CONF_UNSET_PTR) { + if (i < conf->imports->nelts) { + return NULL; + } + + i -= conf->imports->nelts; + } + + if (i >= conf->inlines->nelts) { + return NULL; + } + + inl = conf->inlines->elts; + + return &inl[i]; +} + + +static ngx_js_inline_t * +ngx_js_inline_from_stack(ngx_js_loc_conf_t *conf, u_char *start, size_t len) +{ + u_char *p, *end; + ngx_uint_t index; + ngx_js_inline_t *inl; + + static const u_char prefix[] = "__js_set_"; + + if (conf == NULL || conf->inlines == NGX_CONF_UNSET_PTR) { + return NULL; + } + + end = start + len; + p = start; + + while (p + (sizeof(prefix) - 1) <= end) { + if (ngx_strncmp(p, prefix, sizeof(prefix) - 1) == 0) { + p += sizeof(prefix) - 1; + + if (p >= end || *p < '0' || *p > '9') { + return NULL; + } + + index = 0; + + while (p < end && *p >= '0' && *p <= '9') { + index = index * 10 + (*p - '0'); + p++; + } + + if (index >= conf->inlines->nelts) { + return NULL; + } + + inl = conf->inlines->elts; + + return &inl[index]; + } + + p++; + } + + return NULL; +} + + static ngx_int_t ngx_engine_njs_compile(ngx_js_loc_conf_t *conf, ngx_log_t *log, u_char *start, size_t size) { - u_char *end; - njs_vm_t *vm; - njs_int_t rc; - njs_str_t text; - ngx_uint_t i; - njs_value_t *value; - njs_opaque_value_t exception, lvalue; - ngx_js_named_path_t *import; - - static const njs_str_t line_number_key = njs_str("lineNumber"); - static const njs_str_t file_name_key = njs_str("fileName"); + u_char *end; + njs_vm_t *vm; + njs_int_t rc; + njs_str_t text; + ngx_js_inline_t *inl; + njs_opaque_value_t exception; vm = conf->engine->u.njs.vm; @@ -633,26 +767,16 @@ ngx_engine_njs_compile(ngx_js_loc_conf_t *conf, ngx_log_t *log, u_char *start, njs_vm_exception_get(vm, njs_value_arg(&exception)); njs_vm_value_string(vm, &text, njs_value_arg(&exception)); - value = njs_vm_object_prop(vm, njs_value_arg(&exception), - &file_name_key, &lvalue); - if (value == NULL) { - value = njs_vm_object_prop(vm, njs_value_arg(&exception), - &line_number_key, &lvalue); + inl = ngx_js_inline_map(conf, text.start, text.length); + if (inl != NULL) { + ngx_log_error(NGX_LOG_EMERG, log, 0, "%*s, included at %s:%ui", + text.length, text.start, inl->file, inl->line); - if (value != NULL) { - i = njs_value_number(value) - 1; - - if (i < conf->imports->nelts) { - import = conf->imports->elts; - ngx_log_error(NGX_LOG_EMERG, log, 0, - "%*s, included in %s:%ui", text.length, - text.start, import[i].file, import[i].line); - return NGX_ERROR; - } - } + } else { + ngx_log_error(NGX_LOG_EMERG, log, 0, "%*s", + text.length, text.start); } - ngx_log_error(NGX_LOG_EMERG, log, 0, "%*s", text.length, text.start); return NGX_ERROR; } @@ -763,10 +887,12 @@ static ngx_int_t ngx_engine_njs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname, njs_opaque_value_t *args, njs_uint_t nargs) { - njs_vm_t *vm; - njs_int_t ret; - njs_str_t name; - njs_function_t *func; + njs_vm_t *vm; + njs_int_t ret; + njs_str_t name, str; + ngx_str_t s; + ngx_js_inline_t *inl; + njs_function_t *func; name.start = fname->data; name.length = fname->len; @@ -783,7 +909,24 @@ ngx_engine_njs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname, ret = njs_vm_invoke(vm, func, njs_value_arg(args), nargs, njs_value_arg(&ctx->retval)); if (ret == NJS_ERROR) { - ngx_js_log_exception(vm, ctx->log, "exception"); + if (njs_vm_exception_string(vm, &str) != NJS_OK) { + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, + "js exception"); + return NGX_ERROR; + } + + s.data = str.start; + s.len = str.length; + + inl = ngx_js_inline_from_stack(ctx->conf, s.data, s.len); + if (inl != NULL) { + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, + "js exception: %V, included at %s:%ui", + &s, inl->file, inl->line); + } else { + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, + "js exception: %V", &s); + } return NGX_ERROR; } @@ -963,9 +1106,11 @@ static ngx_int_t ngx_engine_qjs_compile(ngx_js_loc_conf_t *conf, ngx_log_t *log, u_char *start, size_t size) { - JSValue code; + ngx_str_t text; + JSValue code, exception; JSContext *cx; ngx_engine_t *engine; + ngx_js_inline_t *inl; ngx_js_code_entry_t *pc; engine = conf->engine; @@ -975,10 +1120,36 @@ ngx_engine_qjs_compile(ngx_js_loc_conf_t *conf, ngx_log_t *log, u_char *start, JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); if (JS_IsException(code)) { - ngx_qjs_log_exception(engine, log, "compile"); + exception = JS_GetException(cx); + + if (ngx_qjs_dump_obj(engine, exception, &text) == NGX_OK) { + inl = ngx_js_inline_map(conf, text.data, text.len); + if (inl != NULL) { + ngx_log_error(NGX_LOG_EMERG, log, 0, "%V, included at %s:%ui", + &text, inl->file, inl->line); + + } else { + ngx_log_error(NGX_LOG_EMERG, log, 0, "js compile: %V", &text); + } + + } else { + ngx_log_error(NGX_LOG_EMERG, log, 0, "js compile error"); + } + + JS_FreeValue(cx, exception); return NGX_ERROR; } + if (engine->precompiled == NULL) { + engine->precompiled = njs_arr_create(engine->pool, 4, + sizeof(ngx_js_code_entry_t)); + if (engine->precompiled == NULL) { + JS_FreeValue(cx, code); + ngx_log_error(NGX_LOG_EMERG, log, 0, "njs_arr_create() failed"); + return NGX_ERROR; + } + } + pc = njs_arr_add(engine->precompiled); if (pc == NULL) { JS_FreeValue(cx, code); @@ -1120,9 +1291,11 @@ static ngx_int_t ngx_engine_qjs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname, njs_opaque_value_t *args, njs_uint_t nargs) { - JSValue fn, val; - ngx_int_t rc; - JSContext *cx; + JSValue fn, val, exc; + ngx_int_t rc; + ngx_str_t s; + JSContext *cx; + ngx_js_inline_t *inl; cx = ctx->engine->u.qjs.ctx; @@ -1138,7 +1311,24 @@ ngx_engine_qjs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname, val = JS_Call(cx, fn, JS_UNDEFINED, nargs, &ngx_qjs_arg(args[0])); JS_FreeValue(cx, fn); if (JS_IsException(val)) { - ngx_qjs_log_exception(ctx->engine, ctx->log, "call exception"); + exc = JS_GetException(cx); + + if (ngx_qjs_dump_obj(ctx->engine, exc, &s) == NGX_OK) { + inl = ngx_js_inline_from_stack(ctx->conf, s.data, s.len); + if (inl != NULL) { + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, + "js exception: %V, included at %s:%ui", + &s, inl->file, inl->line); + } else { + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, + "js exception: %V", &s); + } + + } else { + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "js exception"); + } + + JS_FreeValue(cx, exc); return NGX_ERROR; } @@ -3668,6 +3858,131 @@ ngx_js_init_preload_vm(njs_vm_t *vm, ngx_js_loc_conf_t *conf) } +ngx_int_t +ngx_js_is_function_ref(ngx_str_t *str) +{ + u_char *p, *end; + + p = str->data; + end = p + str->len; + + if (p == end) { + return 0; + } + + for ( ;; ) { + if ((*p < 'a' || *p > 'z') + && (*p < 'A' || *p > 'Z') + && *p != '_' && *p != '$') + { + return 0; + } + + p++; + + while (p < end && *p != '.') { + if ((*p < 'a' || *p > 'z') + && (*p < 'A' || *p > 'Z') + && (*p < '0' || *p > '9') + && *p != '_' && *p != '$') + { + return 0; + } + + p++; + } + + if (p == end) { + return 1; + } + + /* skip '.' */ + p++; + + if (p == end) { + return 0; + } + } +} + + +static ngx_int_t +ngx_js_set_inline(ngx_conf_t *cf, ngx_array_t **inlines, ngx_uint_t *index, + ngx_str_t *code, const char *arg, ngx_str_t *fname) +{ + size_t arg_len; + ngx_js_inline_t *inl; + + if (*inlines == NGX_CONF_UNSET_PTR) { + *inlines = ngx_array_create(cf->pool, 4, sizeof(ngx_js_inline_t)); + if (*inlines == NULL) { + return NGX_ERROR; + } + } + + inl = ngx_array_push(*inlines); + if (inl == NULL) { + return NGX_ERROR; + } + + arg_len = ngx_strlen(arg); + inl->code = *code; + inl->file = cf->conf_file->file.name.data; + inl->line = cf->conf_file->line; + inl->arg.len = arg_len; + inl->arg.data = (u_char *) arg; + + inl->fname.data = ngx_pnalloc(cf->pool, sizeof("__js_set_65535") - 1); + if (inl->fname.data == NULL) { + return NGX_ERROR; + } + + inl->fname.len = ngx_sprintf(inl->fname.data, "__js_set_%ui", (*index)++) + - inl->fname.data; + + *fname = inl->fname; + + return NGX_OK; +} + + +ngx_int_t +ngx_js_set_init(ngx_conf_t *cf, ngx_array_t **inlines, ngx_uint_t *index, + ngx_str_t *handler, const char *arg, ngx_js_set_t *set) +{ + u_char *p, *end; + + set->flags = 0; + set->file_name = cf->conf_file->file.name.data; + set->line = cf->conf_file->line; + + if (ngx_js_is_function_ref(handler)) { + + p = handler->data; + end = p + handler->len; + + while (p < end) { + if (*p == '$' && p + 1 < end + && ((p[1] >= 'a' && p[1] <= 'z') + || (p[1] >= 'A' && p[1] <= 'Z') + || p[1] == '_')) + { + goto inline_expr; + } + + p++; + } + + set->fname = *handler; + return NGX_OK; + } + +inline_expr: + + return ngx_js_set_inline(cf, inlines, index, handler, arg, &set->fname); +} + + /* * Merge configuration values used at configuration time. */ @@ -3686,10 +4001,14 @@ ngx_js_merge_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf, { ngx_str_t *path, *s; ngx_uint_t i; - ngx_array_t *imports, *preload_objects, *paths; + ngx_array_t *imports, *inlines, *preload_objects, *paths; + ngx_js_inline_t *inl, *ili; ngx_js_named_path_t *import, *pi, *pij, *preload; - if (prev->imports != NGX_CONF_UNSET_PTR && prev->engine == NULL) { + if ((prev->imports != NGX_CONF_UNSET_PTR + || prev->inlines != NGX_CONF_UNSET_PTR) + && prev->engine == NULL) + { /* * special handling to preserve conf->engine * in the "http" or "stream" section to inherit it to all servers @@ -3703,6 +4022,7 @@ ngx_js_merge_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf, } if (conf->imports == NGX_CONF_UNSET_PTR + && conf->inlines == NGX_CONF_UNSET_PTR && conf->type == prev->type && conf->paths == NGX_CONF_UNSET_PTR && conf->preload_objects == NGX_CONF_UNSET_PTR) @@ -3710,6 +4030,7 @@ ngx_js_merge_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf, if (prev->engine != NULL) { conf->preload_objects = prev->preload_objects; conf->imports = prev->imports; + conf->inlines = prev->inlines; conf->type = prev->type; conf->paths = prev->paths; conf->engine = prev->engine; @@ -3791,6 +4112,43 @@ ngx_js_merge_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf, } } + if (prev->inlines != NGX_CONF_UNSET_PTR) { + if (conf->inlines == NGX_CONF_UNSET_PTR) { + conf->inlines = prev->inlines; + + } else { + inlines = ngx_array_create(cf->pool, 4, + sizeof(ngx_js_inline_t)); + if (inlines == NULL) { + return NGX_ERROR; + } + + ili = prev->inlines->elts; + + for (i = 0; i < prev->inlines->nelts; i++) { + inl = ngx_array_push(inlines); + if (inl == NULL) { + return NGX_ERROR; + } + + *inl = ili[i]; + } + + ili = conf->inlines->elts; + + for (i = 0; i < conf->inlines->nelts; i++) { + inl = ngx_array_push(inlines); + if (inl == NULL) { + return NGX_ERROR; + } + + *inl = ili[i]; + } + + conf->inlines = inlines; + } + } + if (prev->paths != NGX_CONF_UNSET_PTR) { if (conf->paths == NGX_CONF_UNSET_PTR) { conf->paths = prev->paths; @@ -3827,7 +4185,9 @@ ngx_js_merge_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf, } } - if (conf->imports == NGX_CONF_UNSET_PTR) { + if (conf->imports == NGX_CONF_UNSET_PTR + && conf->inlines == NGX_CONF_UNSET_PTR) + { return NGX_OK; } @@ -4155,6 +4515,7 @@ ngx_js_init_conf_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf, size_t size; ngx_str_t *m, file; ngx_uint_t i; + ngx_js_inline_t *inl; ngx_pool_cleanup_t *cln; ngx_js_named_path_t *import; @@ -4164,14 +4525,34 @@ ngx_js_init_conf_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf, size = 0; - import = conf->imports->elts; - for (i = 0; i < conf->imports->nelts; i++) { + if (conf->imports != NGX_CONF_UNSET_PTR) { + import = conf->imports->elts; + + for (i = 0; i < conf->imports->nelts; i++) { - /* import from ''; globalThis. = ; */ + /* import from ''; globalThis. = ; */ - size += sizeof("import from '';") - 1 + import[i].name.len * 3 - + import[i].path.len - + sizeof(" globalThis. = ;\n") - 1; + size += sizeof("import from '';") - 1 + import[i].name.len * 3 + + import[i].path.len + + sizeof(" globalThis. = ;\n") - 1; + } + } + + if (conf->inlines != NGX_CONF_UNSET_PTR) { + inl = conf->inlines->elts; + + for (i = 0; i < conf->inlines->nelts; i++) { + + /* + * function () { return (); } + * globalThis. = ;\n + */ + + size += sizeof("function () { return (); }") - 1 + + sizeof(" globalThis. = ;\n") - 1 + + inl[i].fname.len * 3 + + inl[i].arg.len + inl[i].code.len; + } } start = ngx_pnalloc(cf->pool, size + 1); @@ -4180,20 +4561,44 @@ ngx_js_init_conf_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf, } p = start; - import = conf->imports->elts; - for (i = 0; i < conf->imports->nelts; i++) { - /* import from ''; globalThis. = ; */ + if (conf->imports != NGX_CONF_UNSET_PTR) { + import = conf->imports->elts; - p = ngx_cpymem(p, "import ", sizeof("import ") - 1); - p = ngx_cpymem(p, import[i].name.data, import[i].name.len); - p = ngx_cpymem(p, " from '", sizeof(" from '") - 1); - p = ngx_cpymem(p, import[i].path.data, import[i].path.len); - p = ngx_cpymem(p, "'; globalThis.", sizeof("'; globalThis.") - 1); - p = ngx_cpymem(p, import[i].name.data, import[i].name.len); - p = ngx_cpymem(p, " = ", sizeof(" = ") - 1); - p = ngx_cpymem(p, import[i].name.data, import[i].name.len); - p = ngx_cpymem(p, ";\n", sizeof(";\n") - 1); + for (i = 0; i < conf->imports->nelts; i++) { + + /* import from ''; globalThis. = ; */ + + p = ngx_cpymem(p, "import ", sizeof("import ") - 1); + p = ngx_cpymem(p, import[i].name.data, import[i].name.len); + p = ngx_cpymem(p, " from '", sizeof(" from '") - 1); + p = ngx_cpymem(p, import[i].path.data, import[i].path.len); + p = ngx_cpymem(p, "'; globalThis.", + sizeof("'; globalThis.") - 1); + p = ngx_cpymem(p, import[i].name.data, import[i].name.len); + p = ngx_cpymem(p, " = ", sizeof(" = ") - 1); + p = ngx_cpymem(p, import[i].name.data, import[i].name.len); + p = ngx_cpymem(p, ";\n", sizeof(";\n") - 1); + } + } + + if (conf->inlines != NGX_CONF_UNSET_PTR) { + inl = conf->inlines->elts; + + for (i = 0; i < conf->inlines->nelts; i++) { + p = ngx_cpymem(p, "function ", sizeof("function ") - 1); + p = ngx_cpymem(p, inl[i].fname.data, inl[i].fname.len); + p = ngx_cpymem(p, "(", 1); + p = ngx_cpymem(p, inl[i].arg.data, inl[i].arg.len); + p = ngx_cpymem(p, ") { return (", sizeof(") { return (") - 1); + p = ngx_cpymem(p, inl[i].code.data, inl[i].code.len); + p = ngx_cpymem(p, "); } globalThis.", + sizeof("); } globalThis.") - 1); + p = ngx_cpymem(p, inl[i].fname.data, inl[i].fname.len); + p = ngx_cpymem(p, " = ", sizeof(" = ") - 1); + p = ngx_cpymem(p, inl[i].fname.data, inl[i].fname.len); + p = ngx_cpymem(p, ";\n", sizeof(";\n") - 1); + } } *p = '\0'; @@ -4288,6 +4693,7 @@ ngx_js_create_conf(ngx_conf_t *cf, size_t size) conf->paths = NGX_CONF_UNSET_PTR; conf->type = NGX_CONF_UNSET_UINT; conf->imports = NGX_CONF_UNSET_PTR; + conf->inlines = NGX_CONF_UNSET_PTR; conf->preload_objects = NGX_CONF_UNSET_PTR; conf->reuse = NGX_CONF_UNSET_SIZE; diff --git a/nginx/ngx_js.h b/nginx/ngx_js.h index a25dc65a0..04f95c22d 100644 --- a/nginx/ngx_js.h +++ b/nginx/ngx_js.h @@ -133,6 +133,7 @@ typedef struct { ngx_js_queue_t *reuse_queue; \ ngx_str_t cwd; \ ngx_array_t *imports; \ + ngx_array_t *inlines; \ ngx_array_t *paths; \ \ ngx_array_t *preload_objects; \ @@ -184,6 +185,7 @@ typedef struct { #define NGX_JS_COMMON_CTX \ ngx_engine_t *engine; \ + ngx_js_loc_conf_t *conf; \ ngx_log_t *log; \ njs_opaque_value_t args[3]; \ njs_opaque_value_t retval; \ @@ -224,6 +226,15 @@ typedef struct { } ngx_js_set_t; +typedef struct { + ngx_str_t code; + ngx_str_t fname; + ngx_str_t arg; + u_char *file; + ngx_uint_t line; +} ngx_js_inline_t; + + struct ngx_js_ctx_s { NGX_JS_COMMON_CTX; }; @@ -452,6 +463,9 @@ 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_is_function_ref(ngx_str_t *str); +ngx_int_t ngx_js_set_init(ngx_conf_t *cf, ngx_array_t **inlines, + ngx_uint_t *index, ngx_str_t *handler, const char *arg, ngx_js_set_t *set); ngx_int_t ngx_js_merge_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf, ngx_js_loc_conf_t *prev, ngx_int_t (*init_vm)(ngx_conf_t *cf, ngx_js_loc_conf_t *conf)); diff --git a/nginx/ngx_stream_js_module.c b/nginx/ngx_stream_js_module.c index 76d8907d5..89699cc80 100644 --- a/nginx/ngx_stream_js_module.c +++ b/nginx/ngx_stream_js_module.c @@ -1157,8 +1157,9 @@ ngx_stream_js_variable_set(ngx_stream_session_t *s, if (rc == NGX_DECLINED) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "no \"js_import\" directives found for \"js_set\" handler" - " \"%V\" in the current scope", fname); + "no \"js_import\" or inline expression found" + " for \"js_set\" handler \"%V\" at %s:%ui", + fname, vdata->file_name, vdata->line); v->not_found = 1; return NGX_OK; } @@ -1245,6 +1246,7 @@ ngx_stream_js_init_vm(ngx_stream_session_t *s, njs_int_t proto_id) } ngx_js_ctx_init((ngx_js_ctx_t *) ctx, s->connection->log); + ctx->conf = (ngx_js_loc_conf_t *) jscf; ngx_stream_set_ctx(s, ctx, ngx_stream_js_module); } @@ -3539,9 +3541,12 @@ ngx_stream_js_periodic(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) static char * ngx_stream_js_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { - ngx_str_t *value; - ngx_js_set_t *data, *prev; - ngx_stream_variable_t *v; + ngx_str_t *value; + ngx_js_set_t *data, *prev; + ngx_stream_variable_t *v; + ngx_stream_js_srv_conf_t *jscf; + + static ngx_uint_t ngx_stream_js_inline_index; value = cf->args->elts; @@ -3564,19 +3569,25 @@ ngx_stream_js_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) return NGX_CONF_ERROR; } - data->fname = value[2]; - data->file_name = cf->conf_file->file.name.data; - data->line = cf->conf_file->line; + jscf = ngx_stream_conf_get_module_srv_conf(cf, ngx_stream_js_module); + + if (ngx_js_set_init(cf, &jscf->inlines, &ngx_stream_js_inline_index, + &value[2], "s", data) + != NGX_OK) + { + return NGX_CONF_ERROR; + } if (v->get_handler == ngx_stream_js_variable_set) { prev = (ngx_js_set_t *) v->data; if (data->fname.len != prev->fname.len - || ngx_strncmp(data->fname.data, prev->fname.data, data->fname.len) != 0) + || ngx_strncmp(data->fname.data, prev->fname.data, + data->fname.len) != 0) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "variable \"%V\" is redeclared with " - "different function name", &value[1]); + "different handler", &value[1]); return NGX_CONF_ERROR; } } diff --git a/nginx/t/js_inline.t b/nginx/t/js_inline.t new file mode 100644 index 000000000..c6de31585 --- /dev/null +++ b/nginx/t/js_inline.t @@ -0,0 +1,179 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) F5, Inc. + +# Tests for http njs module, js_set inline expressions. + +############################################################################### + +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/)->plan(13); + +use constant TEMPLATE_CONF => <<'EOF'; + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + # oridinary variable set (function reference) + js_set $test_var test.variable; + + js_set $var 'r.var.test_var.toUpperCase()'; + js_set $header 'r.headersIn["Foo"] || "none"'; + js_set $template `/p${r.uri}/post`; + js_set $expression '1 + 2'; + js_set $force_expr (r.var.uri); + + js_set $runtime_err '(o.a.a)'; + + %%EXTRA_CONF%% + + js_import test.js; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /inline { + return 200 "uri=$template foo=$header sum=$expression"; + } + + location /var { + return 200 "var=$var"; + } + + location /mixed { + return 200 "test_var=$test_var uri=$template"; + } + + location /force_expr { + return 200 "uri=$force_expr"; + } + + location /runtime_err { + return 200 "err=$runtime_err"; + } + } +} + +EOF + +############################################################################### + +$t->write_file('test.js', <try_run('no njs'); + +############################################################################### + +like(http_get('/inline'), qr/uri=\/p\/inline\/post/, 'template inline'); +like(http_get('/inline'), qr/foo=none/, 'inline headerIn'); +like(http( + 'GET /inline HTTP/1.0' . CRLF + . 'Foo: bar' . CRLF + . 'Host: localhost' . CRLF . CRLF +), qr/foo=bar/, 'inline headerIn with header'); + +like(http_get('/inline'), qr/sum=3/, 'inline sum'); +like(http_get('/var'), qr/var=FROM_FUNC/, 'inline var func ref'); +like(http_get('/mixed'), qr/test_var=from_func/, 'mixed func ref'); +like(http_get('/mixed'), qr/uri=\/p\/mixed\/post/, 'mixed inline'); + +like(http_get('/force_expr'), qr/uri=\/force_expr/, 'forced expression syntax'); + +http_get('/runtime_err'); +like($t->read_file('error.log'), qr/included at.*nginx\.conf:/s, + 'runtime error location'); + +$t->stop(); + +############################################################################### + +like(check($t, "js_set \$bad 'return 1';"), + qr/SyntaxError.*included at.*nginx\.conf:/s, + 'inline syntax error location'); + +like(check($t, "js_set \$bad '1 +';"), + qr/SyntaxError.*included at.*nginx\.conf:/s, + 'inline syntax error unexpected end'); + +$t->write_file('bad.js', 'export default {INVALID SYNTAX'); + +like(check($t, 'js_import bad.js;'), + qr/\[emerg\].*SyntaxError/s, + 'file syntax error'); + +unlike(check($t, 'js_import bad.js;'), + qr/included at/s, + 'file syntax error no inline location'); + +open my $fh, '>', $t->testdir() . '/error.log'; +close $fh; + +############################################################################### + +sub write_conf { + my ($t, $extra) = @_; + + $t->write_file_expand('nginx.conf', + TEMPLATE_CONF =~ s/%%EXTRA_CONF%%/$extra/r); +} + +sub check { + my ($t, $extra) = @_; + + $t->stop(); + unlink $t->testdir() . '/error.log'; + + write_conf($t, $extra); + + eval { + open OLDERR, ">&", \*STDERR; close STDERR; + $t->run(); + open STDERR, ">&", \*OLDERR; + }; + + return unless $@; + + my $log = $t->read_file('error.log'); + + if ($ENV{TEST_NGINX_VERBOSE}) { + map { Test::Nginx::log_core($_) } split(/^/m, $log); + } + + return $log; +} + +############################################################################### diff --git a/nginx/t/js_inline_only.t b/nginx/t/js_inline_only.t new file mode 100644 index 000000000..f21448221 --- /dev/null +++ b/nginx/t/js_inline_only.t @@ -0,0 +1,76 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) F5, Inc. + +# Tests for http njs module, js_set inline expressions without js_import. + +############################################################################### + +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_set $template `/p${r.uri}/post`; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + js_set $a '(r.args.a || "none")'; + js_set $expanded 'r.var.uri.toUpperCase()'; + + location /inline { + js_set $method '(r.method)'; + + return 200 "uri=$template a=$a method=$method"; + } + + location /expanded { + return 200 "expanded=$expanded"; + } + } +} + +EOF + +$t->try_run('no njs')->plan(6); + +############################################################################### + +like(http_get('/inline'), qr/uri=\/p\/inline\/post/, 'inline only uri'); +like(http_get('/inline'), qr/a=none/, 'inline only args'); +like(http_get('/inline?a=1'), qr/a=1/, 'inline only args with value'); +like(http_get('/inline'), qr/method=GET/, 'inline only method'); +like(http_get('/expanded'), qr/expanded=\/EXPANDED/, + 'inline only expanded r.var'); + +$t->stop(); + +ok(index($t->read_file('error.log'), 'SyntaxError') < 0, 'no syntax errors'); + +############################################################################### diff --git a/nginx/t/js_variables_location.t b/nginx/t/js_variables_location.t index 02bd85ef0..92496a509 100644 --- a/nginx/t/js_variables_location.t +++ b/nginx/t/js_variables_location.t @@ -90,7 +90,8 @@ like(http_get('/not_found'), qr/NOT_FOUND:$/, 'not found is empty'); $t->stop(); ok(index($t->read_file('error.log'), - 'no "js_import" directives found for "js_set" handler "main.variable" ' - . 'in the current scope') > 0, 'log error for js_set without js_import'); + 'no "js_import" or inline expression found for "js_set" handler ' + . '"main.variable"') > 0, + 'log error for js_set without js_import'); ############################################################################### diff --git a/nginx/t/stream_js_variables_server.t b/nginx/t/stream_js_variables_server.t index a7d53718e..abf8c1802 100644 --- a/nginx/t/stream_js_variables_server.t +++ b/nginx/t/stream_js_variables_server.t @@ -92,7 +92,8 @@ is(stream('127.0.0.1:' . port(8083))->read(), 'NOT_FOUND:', 'not found var'); $t->stop(); ok(index($t->read_file('error.log'), - 'no "js_import" directives found for "js_set" handler "main.variable" ' - . 'in the current scope') > 0, 'log error for js_set without js_import'); + 'no "js_import" or inline expression found for "js_set" handler ' + . '"main.variable"') > 0, + 'log error for js_set without js_import'); ###############################################################################