From: Dmitry Volyntsev Date: Tue, 10 Nov 2026 20:17:46 +0000 (-0800) Subject: Modules: bind QuickJS native views to their context lifetime X-Git-Url: https://git.kaiwu.me/ngx_http_auth_basic_module.html?a=commitdiff_plain;h=aaaf571211d59e7b7365f7e038c6dca7d9536e08;p=njs.git Modules: bind QuickJS native views to their context lifetime HTTP and stream views can outlive their request or connection pool in a reused runtime. Detaching only the root object leaves variables, headers, forms and replies with dangling native pointers. Direct session accessors and cold body getters can dereference detached roots, while populated request caches hide their expiry. Share a runtime-owned revocable token across each binding and its views. Reject expired access before native dereferences, including cache access, without retaining pools or copying native data. Keep reply tokens tied to the originating subrequest operation so completed replies remain usable until the parent binding is detached. The header and variable lifetime gap dates back to 201b1276, which introduces QuickJS integration. RequestForm uses the same unguarded model since 0ffc96df. Extracted JS values and stream callback flags remain independent; RequestForm remains a guarded request-bound view. --- diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c index 93a94225..8a9c0033 100644 --- a/nginx/ngx_http_js_module.c +++ b/nginx/ngx_http_js_module.c @@ -165,6 +165,18 @@ typedef struct { JSValue response_body; } ngx_http_qjs_request_t; + +typedef struct { + ngx_js_opaque_t opaque; + ngx_uint_t buffer_type; +} ngx_http_qjs_variables_t; + + +typedef struct { + ngx_qjs_owner_t *owner; + ngx_http_js_ctx_t *ctx; +} ngx_http_qjs_subrequest_t; + #endif @@ -236,13 +248,13 @@ static JSValue ngx_http_qjs_ext_read_request_body(JSContext *cx, static ngx_int_t ngx_http_qjs_body_resolve(ngx_http_request_t *r, ngx_http_js_ctx_t *ctx, void *event); static JSValue ngx_http_qjs_form_to_value(JSContext *cx, ngx_http_request_t *r, - ngx_http_js_ctx_t *ctx, ngx_uint_t max_keys); + ngx_http_js_ctx_t *ctx, ngx_uint_t max_keys, JSValueConst request); static JSValue ngx_http_qjs_request_form_entry_value(JSContext *cx, ngx_js_form_entry_t *entry); static ngx_int_t ngx_http_qjs_request_form_max_keys(JSContext *cx, JSValueConst options, ngx_uint_t *max_keys); static JSValue ngx_http_qjs_request_form_make(JSContext *cx, - ngx_js_form_t *form); + JSValueConst request, ngx_js_form_t *form); static JSValue ngx_http_qjs_ext_read_request_form(JSContext *cx, JSValueConst this_val, int argc, JSValueConst *argv); static JSValue ngx_http_qjs_ext_request_form_get(JSContext *cx, @@ -491,9 +503,14 @@ static int ngx_http_qjs_headers_out_define_own_property(JSContext *cx, static int ngx_http_qjs_headers_out_delete_property(JSContext *cx, JSValueConst obj, JSAtom prop); -static ngx_http_request_t *ngx_http_qjs_request(JSValueConst val); +static ngx_http_request_t *ngx_http_qjs_request(JSContext *cx, + JSValueConst val); static JSValue ngx_http_qjs_request_make(JSContext *cx, ngx_int_t proto_id, - ngx_http_request_t *r); + ngx_http_request_t *r, ngx_qjs_owner_t *owner); +static JSValue ngx_http_qjs_view_make(JSContext *cx, JSValueConst request, + JSClassID request_class, JSClassID view_class, void *external); +static JSValue ngx_http_qjs_variables_make(JSContext *cx, JSValueConst request, + JSClassID request_class, int type); static void ngx_http_qjs_request_mark(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func); static void ngx_http_qjs_request_finalizer(JSRuntime *rt, JSValue val); @@ -1483,13 +1500,13 @@ static JSClassDef ngx_http_qjs_periodic_class = { static JSClassDef ngx_http_qjs_request_form_class = { "RequestForm", - .finalizer = NULL, + .finalizer = ngx_qjs_opaque_finalizer, }; static JSClassDef ngx_http_qjs_variables_class = { "Variables", - .finalizer = NULL, + .finalizer = ngx_qjs_opaque_finalizer, .exotic = & (JSClassExoticMethods) { .get_own_property = ngx_http_qjs_variables_own_property, .set_property = ngx_http_qjs_variables_set_property, @@ -1499,7 +1516,7 @@ static JSClassDef ngx_http_qjs_variables_class = { static JSClassDef ngx_http_qjs_headers_in_class = { "headersIn", - .finalizer = NULL, + .finalizer = ngx_qjs_opaque_finalizer, .exotic = & (JSClassExoticMethods) { .get_own_property = ngx_http_qjs_headers_in_own_property, .get_own_property_names = ngx_http_qjs_headers_in_own_property_names, @@ -1509,7 +1526,7 @@ static JSClassDef ngx_http_qjs_headers_in_class = { static JSClassDef ngx_http_qjs_headers_out_class = { "headersOut", - .finalizer = NULL, + .finalizer = ngx_qjs_opaque_finalizer, .exotic = & (JSClassExoticMethods) { .get_own_property = ngx_http_qjs_headers_out_own_property, .get_own_property_names = ngx_http_qjs_headers_out_own_property_names, @@ -6157,9 +6174,10 @@ ngx_http_qjs_ext_args(JSContext *cx, JSValueConst this_val) ngx_http_request_t *r; ngx_http_qjs_request_t *req; - req = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_REQUEST); + req = (ngx_http_qjs_request_t *) ngx_qjs_opaque_get(cx, this_val, + NGX_QJS_CLASS_ID_HTTP_REQUEST); if (req == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); + return JS_EXCEPTION; } if (!JS_IsUndefined(req->args)) { @@ -6313,9 +6331,9 @@ ngx_http_qjs_ext_done(JSContext *cx, JSValueConst this_val, ngx_http_js_ctx_t *ctx; ngx_http_request_t *r; - r = ngx_http_qjs_request(this_val); + r = ngx_http_qjs_request(cx, this_val); if (r == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); + return JS_EXCEPTION; } ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); @@ -6337,9 +6355,9 @@ ngx_http_qjs_ext_finish(JSContext *cx, JSValueConst this_val, ngx_http_js_ctx_t *ctx; ngx_http_request_t *r; - r = ngx_http_qjs_request(this_val); + r = ngx_http_qjs_request(cx, this_val); if (r == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); + return JS_EXCEPTION; } if (ngx_http_send_special(r, NGX_HTTP_LAST) == NGX_ERROR) { @@ -6357,39 +6375,18 @@ ngx_http_qjs_ext_finish(JSContext *cx, JSValueConst this_val, static JSValue ngx_http_qjs_ext_headers_in(JSContext *cx, JSValueConst this_val) { - JSValue obj; - ngx_http_request_t *r; - - r = ngx_http_qjs_request(this_val); - if (r == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); - } - - obj = JS_NewObjectProtoClass(cx, JS_NULL, NGX_QJS_CLASS_ID_HTTP_HEADERS_IN); - - JS_SetOpaque(obj, r); - - return obj; + return ngx_http_qjs_view_make(cx, this_val, + NGX_QJS_CLASS_ID_HTTP_REQUEST, + NGX_QJS_CLASS_ID_HTTP_HEADERS_IN, NULL); } static JSValue ngx_http_qjs_ext_headers_out(JSContext *cx, JSValueConst this_val) { - JSValue obj; - ngx_http_request_t *r; - - r = ngx_http_qjs_request(this_val); - if (r == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); - } - - obj = JS_NewObjectProtoClass(cx, JS_NULL, - NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT); - - JS_SetOpaque(obj, r); - - return obj; + return ngx_http_qjs_view_make(cx, this_val, + NGX_QJS_CLASS_ID_HTTP_REQUEST, + NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT, NULL); } @@ -6399,9 +6396,9 @@ ngx_http_qjs_ext_http_version(JSContext *cx, JSValueConst this_val) ngx_str_t v; ngx_http_request_t *r; - r = ngx_http_qjs_request(this_val); + r = ngx_http_qjs_request(cx, this_val); if (r == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); + return JS_EXCEPTION; } switch (r->http_version) { @@ -6441,9 +6438,9 @@ ngx_http_qjs_ext_internal(JSContext *cx, JSValueConst this_val) { ngx_http_request_t *r; - r = ngx_http_qjs_request(this_val); + r = ngx_http_qjs_request(cx, this_val); if (r == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); + return JS_EXCEPTION; } return JS_NewBool(cx, r->internal); @@ -6457,9 +6454,9 @@ ngx_http_qjs_ext_internal_redirect(JSContext *cx, JSValueConst this_val, ngx_http_js_ctx_t *ctx; ngx_http_request_t *r; - r = ngx_http_qjs_request(this_val); + r = ngx_http_qjs_request(cx, this_val); if (r == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); + return JS_EXCEPTION; } if (r->parent != NULL) { @@ -6492,9 +6489,9 @@ ngx_http_qjs_ext_log(JSContext *cx, JSValueConst this_val, int argc, const char *msg; ngx_http_request_t *r; - r = ngx_http_qjs_request(this_val); + r = ngx_http_qjs_request(cx, this_val); if (r == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); + return JS_EXCEPTION; } for (n = 0; n < argc; n++) { @@ -6516,24 +6513,8 @@ static JSValue ngx_http_qjs_ext_periodic_variables(JSContext *cx, JSValueConst this_val, int type) { - JSValue obj; - ngx_http_qjs_request_t *req; - - req = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_PERIODIC); - if (req == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a periodic object"); - } - - obj = JS_NewObjectProtoClass(cx, JS_NULL, NGX_QJS_CLASS_ID_HTTP_VARS); - - /* - * Using lowest bit of the pointer to store the buffer type. - */ - type = (type == NGX_JS_BUFFER) ? 1 : 0; - JS_SetOpaque(obj, (void *) ((uintptr_t) req->opaque.external - | (uintptr_t) type)); - - return obj; + return ngx_http_qjs_variables_make(cx, this_val, + NGX_QJS_CLASS_ID_HTTP_PERIODIC, type); } @@ -6543,9 +6524,9 @@ ngx_http_qjs_ext_parent(JSContext *cx, JSValueConst this_val) ngx_http_js_ctx_t *ctx; ngx_http_request_t *r; - r = ngx_http_qjs_request(this_val); + r = ngx_http_qjs_request(cx, this_val); if (r == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); + return JS_EXCEPTION; } ctx = r->parent ? ngx_http_get_module_ctx(r->parent, ngx_http_js_module) @@ -6565,9 +6546,9 @@ ngx_http_qjs_ext_remote_address(JSContext *cx, JSValueConst this_val) ngx_connection_t *c; ngx_http_request_t *r; - r = ngx_http_qjs_request(this_val); + r = ngx_http_qjs_request(cx, this_val); if (r == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); + return JS_EXCEPTION; } c = r->connection; @@ -6587,9 +6568,10 @@ ngx_http_qjs_ext_response_body(JSContext *cx, JSValueConst this_val, int type) ngx_http_request_t *r; ngx_http_qjs_request_t *req; - req = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_REQUEST); + req = (ngx_http_qjs_request_t *) ngx_qjs_opaque_get(cx, this_val, + NGX_QJS_CLASS_ID_HTTP_REQUEST); if (req == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); + return JS_EXCEPTION; } buffer_type = ngx_js_buffer_type(type); @@ -6642,9 +6624,10 @@ ngx_http_qjs_ext_request_body(JSContext *cx, JSValueConst this_val, int type) ngx_http_js_ctx_t *ctx; ngx_http_qjs_request_t *req; - req = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_REQUEST); + req = (ngx_http_qjs_request_t *) ngx_qjs_opaque_get(cx, this_val, + NGX_QJS_CLASS_ID_HTTP_REQUEST); if (req == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); + return JS_EXCEPTION; } buffer_type = ngx_js_buffer_type(type); @@ -6739,7 +6722,8 @@ ngx_http_qjs_body_resolve(ngx_http_request_t *r, ngx_http_js_ctx_t *ctx, cx = ctx->engine->u.qjs.ctx; if (ngx_http_js_body_read_is_form(ctx->body_read_state)) { - result = ngx_http_qjs_form_to_value(cx, r, ctx, (uintptr_t) ev->data); + result = ngx_http_qjs_form_to_value(cx, r, ctx, (uintptr_t) ev->data, + ev->args[2]); } else { result = ngx_http_qjs_body_to_value(cx, ctx, (uintptr_t) ev->data); @@ -6774,9 +6758,10 @@ ngx_http_qjs_ext_read_request_body(JSContext *cx, JSValueConst this_val, ngx_http_request_t *r; ngx_http_qjs_request_t *req; - req = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_REQUEST); + req = (ngx_http_qjs_request_t *) ngx_qjs_opaque_get(cx, this_val, + NGX_QJS_CLASS_ID_HTTP_REQUEST); if (req == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); + return JS_EXCEPTION; } r = req->opaque.external; @@ -6790,7 +6775,7 @@ ngx_http_qjs_ext_read_request_body(JSContext *cx, JSValueConst this_val, return JS_ThrowTypeError(cx, "request body is already being read"); } - event = ngx_pcalloc(r->pool, sizeof(ngx_qjs_event_t) + sizeof(JSValue) * 2); + event = ngx_pcalloc(r->pool, sizeof(ngx_qjs_event_t) + sizeof(JSValue) * 3); if (event == NULL) { return JS_ThrowOutOfMemory(cx); } @@ -6806,6 +6791,7 @@ ngx_http_qjs_ext_read_request_body(JSContext *cx, JSValueConst this_val, } event->function = JS_DupValue(cx, event->args[0]); + event->args[2] = JS_DupValue(cx, this_val); event->destructor = ngx_http_js_read_body_event_destructor; ngx_js_add_event(ctx, event); @@ -6872,24 +6858,18 @@ ngx_http_qjs_request_form_max_keys(JSContext *cx, JSValueConst options, static JSValue -ngx_http_qjs_request_form_make(JSContext *cx, ngx_js_form_t *form) +ngx_http_qjs_request_form_make(JSContext *cx, JSValueConst request, + ngx_js_form_t *form) { - JSValue obj; - - obj = JS_NewObjectClass(cx, NGX_QJS_CLASS_ID_HTTP_FORM); - if (JS_IsException(obj)) { - return JS_EXCEPTION; - } - - JS_SetOpaque(obj, form); - - return obj; + return ngx_http_qjs_view_make(cx, request, + NGX_QJS_CLASS_ID_HTTP_REQUEST, + NGX_QJS_CLASS_ID_HTTP_FORM, form); } static JSValue ngx_http_qjs_form_to_value(JSContext *cx, ngx_http_request_t *r, - ngx_http_js_ctx_t *ctx, ngx_uint_t max_keys) + ngx_http_js_ctx_t *ctx, ngx_uint_t max_keys, JSValueConst request) { ngx_int_t rc; ngx_str_t error; @@ -6897,7 +6877,7 @@ ngx_http_qjs_form_to_value(JSContext *cx, ngx_http_request_t *r, rc = ngx_http_js_request_form(r, ctx, max_keys, &form, &error); if (rc == NGX_OK) { - return ngx_http_qjs_request_form_make(cx, form); + return ngx_http_qjs_request_form_make(cx, request, form); } if (rc == NGX_JS_FORM_TYPE_ERROR) { @@ -6923,15 +6903,22 @@ ngx_http_qjs_ext_read_request_form(JSContext *cx, JSValueConst this_val, ngx_http_request_t *r; ngx_http_qjs_request_t *req; - req = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_REQUEST); + req = (ngx_http_qjs_request_t *) ngx_qjs_opaque_get(cx, this_val, + NGX_QJS_CLASS_ID_HTTP_REQUEST); if (req == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); + return JS_EXCEPTION; } if (ngx_http_qjs_request_form_max_keys(cx, argv[0], &max_keys) != NGX_OK) { return JS_EXCEPTION; } + if (ngx_qjs_opaque_get(cx, this_val, NGX_QJS_CLASS_ID_HTTP_REQUEST) + == NULL) + { + return JS_EXCEPTION; + } + r = req->opaque.external; ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); @@ -6943,7 +6930,7 @@ ngx_http_qjs_ext_read_request_form(JSContext *cx, JSValueConst this_val, return JS_ThrowTypeError(cx, "request body is already being read"); } - event = ngx_pcalloc(r->pool, sizeof(ngx_qjs_event_t) + sizeof(JSValue) * 2); + event = ngx_pcalloc(r->pool, sizeof(ngx_qjs_event_t) + sizeof(JSValue) * 3); if (event == NULL) { return JS_ThrowOutOfMemory(cx); } @@ -6959,6 +6946,7 @@ ngx_http_qjs_ext_read_request_form(JSContext *cx, JSValueConst this_val, } event->function = JS_DupValue(cx, event->args[0]); + event->args[2] = JS_DupValue(cx, this_val); event->destructor = ngx_http_js_read_body_event_destructor; ngx_js_add_event(ctx, event); @@ -6975,7 +6963,7 @@ resolve: return JS_ThrowInternalError(cx, "failed to read request body"); } - return ngx_http_qjs_form_to_value(cx, r, ctx, max_keys); + return ngx_http_qjs_form_to_value(cx, r, ctx, max_keys, this_val); } @@ -6988,18 +6976,25 @@ ngx_http_qjs_ext_request_form_get(JSContext *cx, JSValueConst this_val, const char *name; ngx_uint_t i, n; ngx_js_form_t *form; + ngx_js_opaque_t *opaque; ngx_js_form_entry_t *entry; - form = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_FORM); - if (form == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a RequestForm"); + opaque = ngx_qjs_opaque_get(cx, this_val, NGX_QJS_CLASS_ID_HTTP_FORM); + if (opaque == NULL) { + return JS_EXCEPTION; } + form = opaque->external; name = JS_ToCStringLen(cx, &name_len, argv[0]); if (name == NULL) { return JS_ThrowTypeError(cx, "\"name\" must be a string"); } + if (ngx_qjs_opaque_get(cx, this_val, NGX_QJS_CLASS_ID_HTTP_FORM) == NULL) { + JS_FreeCString(cx, name); + return JS_EXCEPTION; + } + if (as_array) { array = JS_NewArray(cx); if (JS_IsException(array)) { @@ -7060,18 +7055,24 @@ ngx_http_qjs_ext_request_form_has(JSContext *cx, JSValueConst this_val, const char *name; ngx_uint_t i; ngx_js_form_t *form; + ngx_js_opaque_t *opaque; ngx_js_form_entry_t *entry; - form = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_FORM); - if (form == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a RequestForm"); + opaque = ngx_qjs_opaque_get(cx, this_val, NGX_QJS_CLASS_ID_HTTP_FORM); + if (opaque == NULL) { + return JS_EXCEPTION; } + form = opaque->external; name = JS_ToCStringLen(cx, &name_len, argv[0]); if (name == NULL) { return JS_ThrowTypeError(cx, "\"name\" must be a string"); } + if (ngx_qjs_opaque_get(cx, this_val, NGX_QJS_CLASS_ID_HTTP_FORM) == NULL) { + JS_FreeCString(cx, name); + return JS_EXCEPTION; + } entry = form->entries.elts; for (i = 0; i < form->entries.nelts; i++) { @@ -7096,13 +7097,15 @@ ngx_http_qjs_ext_request_form_for_each(JSContext *cx, JSValueConst this_val, JSValue args[3], ret; ngx_uint_t i; ngx_js_form_t *form; + ngx_js_opaque_t *opaque; ngx_js_form_entry_t *entry; - form = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_FORM); - if (form == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a RequestForm"); + opaque = ngx_qjs_opaque_get(cx, this_val, NGX_QJS_CLASS_ID_HTTP_FORM); + if (opaque == NULL) { + return JS_EXCEPTION; } + form = opaque->external; if (!JS_IsFunction(cx, argv[0])) { return JS_ThrowTypeError(cx, "\"callback\" is not a function"); } @@ -7134,6 +7137,12 @@ ngx_http_qjs_ext_request_form_for_each(JSContext *cx, JSValueConst this_val, } JS_FreeValue(cx, ret); + + if (ngx_qjs_opaque_get(cx, this_val, NGX_QJS_CLASS_ID_HTTP_FORM) + == NULL) + { + return JS_EXCEPTION; + } } return JS_UNDEFINED; @@ -7174,13 +7183,15 @@ static JSValue ngx_http_qjs_ext_request_form_has_files(JSContext *cx, JSValueConst this_val, int argc, JSValueConst *argv) { - ngx_js_form_t *form; + ngx_js_form_t *form; + ngx_js_opaque_t *opaque; - form = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_FORM); - if (form == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a RequestForm"); + opaque = ngx_qjs_opaque_get(cx, this_val, NGX_QJS_CLASS_ID_HTTP_FORM); + if (opaque == NULL) { + return JS_EXCEPTION; } + form = opaque->external; return JS_NewBool(cx, form->has_files); } @@ -7195,6 +7206,7 @@ ngx_http_js_read_body_event_destructor(ngx_qjs_event_t *event) JS_FreeValue(cx, event->function); JS_FreeValue(cx, event->args[0]); JS_FreeValue(cx, event->args[1]); + JS_FreeValue(cx, event->args[2]); } @@ -7208,9 +7220,9 @@ ngx_http_qjs_ext_return(JSContext *cx, JSValueConst this_val, ngx_http_request_t *r; ngx_http_complex_value_t cv; - r = ngx_http_qjs_request(this_val); + r = ngx_http_qjs_request(cx, this_val); if (r == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); + return JS_EXCEPTION; } if (ngx_qjs_integer(cx, argv[0], &status) != NGX_OK) { @@ -7261,9 +7273,9 @@ ngx_http_qjs_ext_decline(JSContext *cx, JSValueConst this_val, ngx_http_js_ctx_t *ctx; ngx_http_request_t *r; - r = ngx_http_qjs_request(this_val); + r = ngx_http_qjs_request(cx, this_val); if (r == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); + return JS_EXCEPTION; } ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); @@ -7279,9 +7291,9 @@ ngx_http_qjs_ext_status_get(JSContext *cx, JSValueConst this_val) { ngx_http_request_t *r; - r = ngx_http_qjs_request(this_val); + r = ngx_http_qjs_request(cx, this_val); if (r == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); + return JS_EXCEPTION; } return JS_NewInt32(cx, r->headers_out.status); @@ -7295,9 +7307,9 @@ ngx_http_qjs_ext_status_set(JSContext *cx, JSValueConst this_val, ngx_int_t n; ngx_http_request_t *r; - r = ngx_http_qjs_request(this_val); + r = ngx_http_qjs_request(cx, this_val); if (r == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); + return JS_EXCEPTION; } if (ngx_qjs_integer(cx, value, &n) != NGX_OK) { @@ -7317,9 +7329,9 @@ ngx_http_qjs_ext_string(JSContext *cx, JSValueConst this_val, int offset) ngx_str_t *field; ngx_http_request_t *r; - r = ngx_http_qjs_request(this_val); + r = ngx_http_qjs_request(cx, this_val); if (r == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); + return JS_EXCEPTION; } field = (ngx_str_t *) ((u_char *) r + offset); @@ -7339,9 +7351,9 @@ ngx_http_qjs_ext_send(JSContext *cx, JSValueConst this_val, ngx_http_js_ctx_t *ctx; ngx_http_request_t *r; - r = ngx_http_qjs_request(this_val); + r = ngx_http_qjs_request(cx, this_val); if (r == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); + return JS_EXCEPTION; } ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); @@ -7408,9 +7420,9 @@ ngx_http_qjs_ext_send_buffer(JSContext *cx, JSValueConst this_val, ngx_http_js_ctx_t *ctx; ngx_http_request_t *r; - r = ngx_http_qjs_request(this_val); + r = ngx_http_qjs_request(cx, this_val); if (r == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); + return JS_EXCEPTION; } ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); @@ -7551,9 +7563,9 @@ ngx_http_qjs_ext_send_header(JSContext *cx, JSValueConst this_val, { ngx_http_request_t *r; - r = ngx_http_qjs_request(this_val); + r = ngx_http_qjs_request(cx, this_val); if (r == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); + return JS_EXCEPTION; } if (ngx_http_set_content_type(r) != NGX_OK) { @@ -7577,9 +7589,9 @@ ngx_http_qjs_ext_set_return_value(JSContext *cx, JSValueConst this_val, ngx_js_ctx_t *ctx; ngx_http_request_t *r; - r = ngx_http_qjs_request(this_val); + r = ngx_http_qjs_request(cx, this_val); if (r == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); + return JS_EXCEPTION; } ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); @@ -7596,9 +7608,15 @@ ngx_http_qjs_subrequest_done(ngx_http_request_t *r, void *data, ngx_int_t rc) { ngx_qjs_event_t *event = data; - JSValue reply; - JSContext *cx; - ngx_http_js_ctx_t *ctx, *sctx; + JSValue reply; + JSContext *cx; + ngx_http_js_ctx_t *ctx, *sctx; + ngx_http_qjs_subrequest_t *subrequest; + + subrequest = event->data; + if (subrequest->owner == NULL || !subrequest->owner->alive) { + return NGX_OK; + } if (rc != NGX_OK || r->connection->error || r->buffered) { return rc; @@ -7623,22 +7641,25 @@ ngx_http_qjs_subrequest_done(ngx_http_request_t *r, void *data, ngx_int_t rc) sctx->done = 1; - ctx = ngx_http_get_module_ctx(r->parent, ngx_http_js_module); + ctx = subrequest->ctx; ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "js subrequest done s: %ui parent ctx: %p", r->headers_out.status, ctx); - if (ctx == NULL) { + if (ctx == NULL + || ngx_http_get_module_ctx(r->parent, ngx_http_js_module) != ctx) + { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "js subrequest: failed to get the parent context"); return NGX_ERROR; } - cx = ctx->engine->u.qjs.ctx; + cx = event->ctx; - reply = ngx_http_qjs_request_make(cx, NGX_QJS_CLASS_ID_HTTP_REQUEST, r); + reply = ngx_http_qjs_request_make(cx, NGX_QJS_CLASS_ID_HTTP_REQUEST, r, + subrequest->owner); if (JS_IsException(reply)) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "js subrequest reply creation failed"); @@ -7668,13 +7689,17 @@ ngx_http_qjs_subrequest_done(ngx_http_request_t *r, void *data, ngx_int_t rc) static void ngx_http_js_subrequest_event_destructor(ngx_qjs_event_t *event) { - JSContext *cx; + JSContext *cx; + ngx_http_qjs_subrequest_t *subrequest; cx = event->ctx; + subrequest = event->data; JS_FreeValue(cx, event->function); JS_FreeValue(cx, event->args[0]); JS_FreeValue(cx, event->args[1]); + ngx_qjs_owner_release(JS_GetRuntime(cx), subrequest->owner); + subrequest->owner = NULL; } @@ -7690,15 +7715,18 @@ ngx_http_qjs_ext_subrequest(JSContext *cx, JSValueConst this_val, ngx_qjs_event_t *event; ngx_http_js_ctx_t *ctx; ngx_http_request_t *r, *sr; + ngx_http_qjs_request_t *req; ngx_http_request_body_t *rb; + ngx_http_qjs_subrequest_t *subrequest; ngx_http_post_subrequest_t *ps; - r = ngx_http_qjs_request(this_val); + r = ngx_http_qjs_request(cx, this_val); if (r == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); + return JS_EXCEPTION; } - ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + ctx = (ngx_http_js_ctx_t *) ngx_qjs_external_ctx(cx, + JS_GetContextOpaque(cx)); if (r->subrequest_in_memory) { return JS_ThrowTypeError(cx, "subrequest can only be created for " @@ -7847,7 +7875,8 @@ ngx_http_qjs_ext_subrequest(JSContext *cx, JSValueConst this_val, promise = !!JS_IsUndefined(callback); event = ngx_pcalloc(r->pool, sizeof(ngx_qjs_event_t) - + sizeof(JSValue) * 2); + + sizeof(JSValue) * 2 + + sizeof(ngx_http_qjs_subrequest_t)); if (event == NULL) { return JS_ThrowOutOfMemory(cx); } @@ -7856,6 +7885,8 @@ ngx_http_qjs_ext_subrequest(JSContext *cx, JSValueConst this_val, event->fd = ctx->event_id++; event->args = (JSValue *) &event[1]; event->destructor = ngx_http_js_subrequest_event_destructor; + subrequest = (ngx_http_qjs_subrequest_t *) &event->args[2]; + event->data = subrequest; if (promise) { retval = JS_NewPromiseCapability(cx, &event->args[0]); @@ -7871,6 +7902,10 @@ ngx_http_qjs_ext_subrequest(JSContext *cx, JSValueConst this_val, } event->function = JS_DupValue(cx, callback); + req = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_REQUEST); + subrequest->owner = req->opaque.owner; + subrequest->owner->references++; + subrequest->ctx = ctx; ps->handler = ngx_http_qjs_subrequest_done; ps->data = event; @@ -7885,6 +7920,11 @@ ngx_http_qjs_ext_subrequest(JSContext *cx, JSValueConst this_val, if (ngx_http_subrequest(r, &uri, args.len ? &args : NULL, &sr, ps, flags) != NGX_OK) { + if (event != NULL) { + ngx_http_js_subrequest_event_destructor(event); + } + + JS_FreeValue(cx, retval); return JS_ThrowInternalError(cx, "subrequest creation failed"); } @@ -7953,9 +7993,9 @@ ngx_http_qjs_ext_raw_headers(JSContext *cx, JSValueConst this_val, int out) ngx_table_elt_t *header, *h; ngx_http_request_t *r; - r = ngx_http_qjs_request(this_val); + r = ngx_http_qjs_request(cx, this_val); if (r == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); + return JS_EXCEPTION; } headers = (out) ? &r->headers_out.headers : &r->headers_in.headers; @@ -8030,23 +8070,8 @@ 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) { - JSValue obj; - ngx_http_request_t *r; - - r = ngx_http_qjs_request(this_val); - if (r == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); - } - - obj = JS_NewObjectProtoClass(cx, JS_NULL, NGX_QJS_CLASS_ID_HTTP_VARS); - - /* - * Using lowest bit of the pointer to store the buffer type. - */ - type = (type == NGX_JS_BUFFER) ? 1 : 0; - JS_SetOpaque(obj, (void *) ((uintptr_t) r | (uintptr_t) type)); - - return obj; + return ngx_http_qjs_variables_make(cx, this_val, + NGX_QJS_CLASS_ID_HTTP_REQUEST, type); } @@ -8063,9 +8088,9 @@ ngx_http_qjs_ext_js_var_names(JSContext *cx, JSValueConst this_val, int argc, ngx_http_variable_t *v; ngx_http_core_main_conf_t *cmcf; - r = ngx_http_qjs_request(this_val); + r = ngx_http_qjs_request(cx, this_val); if (r == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a request object"); + return JS_EXCEPTION; } prefix = NULL; @@ -8133,19 +8158,19 @@ ngx_http_qjs_variables_own_property(JSContext *cx, JSPropertyDescriptor *pdesc, ngx_str_t name, name_lc; ngx_uint_t i, key, start, length, is_capture; ngx_http_request_t *r; + ngx_http_qjs_variables_t *variables; ngx_http_variable_value_t *vv; u_char storage[64]; - r = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_HTTP_VARS); - - buffer_type = ((uintptr_t) r & 1) ? NGX_JS_BUFFER : NGX_JS_STRING; - r = (ngx_http_request_t *) ((uintptr_t) r & ~(uintptr_t) 1); - - if (r == NULL) { - (void) JS_ThrowTypeError(cx, "\"this\" is not a request object"); + variables = (ngx_http_qjs_variables_t *) ngx_qjs_opaque_get(cx, obj, + NGX_QJS_CLASS_ID_HTTP_VARS); + if (variables == NULL) { return -1; } + buffer_type = variables->buffer_type; + r = variables->opaque.external; + name.data = (u_char *) JS_AtomToCString(cx, prop); if (name.data == NULL) { return -1; @@ -8203,6 +8228,10 @@ ngx_http_qjs_variables_own_property(JSContext *cx, JSPropertyDescriptor *pdesc, vv = ngx_http_get_variable(r, &name_lc, key); JS_FreeCString(cx, (char *) name.data); + if (ngx_qjs_opaque_get(cx, obj, NGX_QJS_CLASS_ID_HTTP_VARS) == NULL) { + return -1; + } + if (vv == NULL || vv->not_found) { return 0; } @@ -8227,19 +8256,19 @@ ngx_http_qjs_variables_set_property(JSContext *cx, JSValueConst obj, ngx_uint_t key; ngx_http_request_t *r; ngx_http_variable_t *v; + ngx_http_qjs_variables_t *variables; ngx_http_variable_value_t *vv; ngx_http_core_main_conf_t *cmcf; u_char storage[64]; - r = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_HTTP_VARS); - - r = (ngx_http_request_t *) ((uintptr_t) r & ~(uintptr_t) 1); - - if (r == NULL) { - (void) JS_ThrowTypeError(cx, "\"this\" is not a request object"); + variables = (ngx_http_qjs_variables_t *) ngx_qjs_opaque_get(cx, obj, + NGX_QJS_CLASS_ID_HTTP_VARS); + if (variables == NULL) { return -1; } + r = variables->opaque.external; + name.data = (u_char *) JS_AtomToCString(cx, prop); if (name.data == NULL) { return -1; @@ -8275,6 +8304,10 @@ ngx_http_qjs_variables_set_property(JSContext *cx, JSValueConst obj, return -1; } + if (ngx_qjs_opaque_get(cx, obj, NGX_QJS_CLASS_ID_HTTP_VARS) == NULL) { + return -1; + } + if (v->set_handler != NULL) { vv = ngx_pcalloc(r->pool, sizeof(ngx_http_variable_value_t)); if (vv == NULL) { @@ -8367,14 +8400,16 @@ ngx_http_qjs_headers_in_own_property_names(JSContext *cx, { int ret; JSValue keys; + ngx_js_opaque_t *opaque; ngx_http_request_t *r; - r = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_IN); - if (r == NULL) { - (void) JS_ThrowTypeError(cx, "\"this\" is not a headers_in object"); + opaque = ngx_qjs_opaque_get(cx, obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_IN); + if (opaque == NULL) { return -1; } + r = opaque->external; + keys = JS_NewObject(cx); if (JS_IsException(keys)) { return -1; @@ -8522,6 +8557,7 @@ ngx_http_qjs_headers_in_own_property(JSContext *cx, JSPropertyDescriptor *pdesc, int ret; unsigned flags; ngx_str_t name; + ngx_js_opaque_t *opaque; ngx_http_request_t *r; ngx_http_js_header_t *h; @@ -8539,12 +8575,13 @@ ngx_http_qjs_headers_in_own_property(JSContext *cx, JSPropertyDescriptor *pdesc, #undef header }; - r = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_IN); - if (r == NULL) { - (void) JS_ThrowTypeError(cx, "\"this\" is not a headers_in object"); + opaque = ngx_qjs_opaque_get(cx, obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_IN); + if (opaque == NULL) { return -1; } + r = opaque->external; + name.data = (u_char *) JS_AtomToCString(cx, prop); if (name.data == NULL) { return -1; @@ -8577,14 +8614,16 @@ ngx_http_qjs_headers_out_own_property_names(JSContext *cx, int ret; JSAtom key; JSValue keys; + ngx_js_opaque_t *opaque; ngx_http_request_t *r; - r = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT); - if (r == NULL) { - (void) JS_ThrowTypeError(cx, "\"this\" is not a headers_out object"); + opaque = ngx_qjs_opaque_get(cx, obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT); + if (opaque == NULL) { return -1; } + r = opaque->external; + keys = JS_NewObject(cx); if (JS_IsException(keys)) { return -1; @@ -9182,14 +9221,16 @@ ngx_http_qjs_headers_out_own_property(JSContext *cx, { int ret; ngx_str_t name; + ngx_js_opaque_t *opaque; ngx_http_request_t *r; - r = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT); - if (r == NULL) { - (void) JS_ThrowTypeError(cx, "\"this\" is not a headers_out object"); + opaque = ngx_qjs_opaque_get(cx, obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT); + if (opaque == NULL) { return -1; } + r = opaque->external; + name.data = (u_char *) JS_AtomToCString(cx, prop); if (name.data == NULL) { return -1; @@ -9221,14 +9262,16 @@ ngx_http_qjs_headers_out_define_own_property(JSContext *cx, { int ret; ngx_str_t name; + ngx_js_opaque_t *opaque; ngx_http_request_t *r; - r = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT); - if (r == NULL) { - (void) JS_ThrowTypeError(cx, "\"this\" is not a headers_out object"); + opaque = ngx_qjs_opaque_get(cx, obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT); + if (opaque == NULL) { return -1; } + r = opaque->external; + if (!JS_IsUndefined(setter) || !JS_IsUndefined(getter)) { (void) JS_ThrowTypeError(cx, "cannot define getter or setter"); return -1; @@ -9267,14 +9310,16 @@ ngx_http_qjs_headers_out_delete_property(JSContext *cx, { int ret; ngx_str_t name; + ngx_js_opaque_t *opaque; ngx_http_request_t *r; - r = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT); - if (r == NULL) { - (void) JS_ThrowTypeError(cx, "\"this\" is not a headers_out object"); + opaque = ngx_qjs_opaque_get(cx, obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT); + if (opaque == NULL) { return -1; } + r = opaque->external; + name.data = (u_char *) JS_AtomToCString(cx, prop); if (name.data == NULL) { return -1; @@ -9390,23 +9435,94 @@ ngx_http_qjs_body_filter(ngx_http_request_t *r, ngx_http_js_loc_conf_t *jlcf, } +static JSValue +ngx_http_qjs_view_make(JSContext *cx, JSValueConst request, + JSClassID request_class, JSClassID view_class, void *external) +{ + JSValue obj; + ngx_js_opaque_t *parent, *opaque; + + parent = ngx_qjs_opaque_get(cx, request, request_class); + if (parent == NULL) { + return JS_EXCEPTION; + } + + if (view_class == NGX_QJS_CLASS_ID_HTTP_FORM) { + obj = JS_NewObjectClass(cx, view_class); + + } else { + obj = JS_NewObjectProtoClass(cx, JS_NULL, view_class); + } + + if (JS_IsException(obj)) { + return obj; + } + + opaque = ngx_qjs_opaque_alloc(cx, sizeof(ngx_js_opaque_t), + external != NULL ? external : parent->external, + parent->owner); + if (opaque == NULL) { + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } + + JS_SetOpaque(obj, opaque); + + return obj; +} + + +static JSValue +ngx_http_qjs_variables_make(JSContext *cx, JSValueConst request, + JSClassID request_class, int type) +{ + JSValue obj; + ngx_js_opaque_t *parent; + ngx_http_qjs_variables_t *variables; + + parent = ngx_qjs_opaque_get(cx, request, request_class); + if (parent == NULL) { + return JS_EXCEPTION; + } + + obj = JS_NewObjectProtoClass(cx, JS_NULL, NGX_QJS_CLASS_ID_HTTP_VARS); + if (JS_IsException(obj)) { + return obj; + } + + variables = (ngx_http_qjs_variables_t *) ngx_qjs_opaque_alloc(cx, + sizeof(ngx_http_qjs_variables_t), + parent->external, parent->owner); + if (variables == NULL) { + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } + + variables->buffer_type = type == NGX_JS_BUFFER ? NGX_JS_BUFFER + : NGX_JS_STRING; + JS_SetOpaque(obj, variables); + + return obj; +} + + static ngx_http_request_t * -ngx_http_qjs_request(JSValueConst val) +ngx_http_qjs_request(JSContext *cx, JSValueConst val) { - ngx_http_qjs_request_t *req; + ngx_js_opaque_t *opaque; - req = JS_GetOpaque(val, NGX_QJS_CLASS_ID_HTTP_REQUEST); - if (req == NULL) { + opaque = ngx_qjs_opaque_get(cx, val, NGX_QJS_CLASS_ID_HTTP_REQUEST); + if (opaque == NULL) { return NULL; } - return req->opaque.external; + return opaque->external; } static JSValue ngx_http_qjs_request_make(JSContext *cx, ngx_int_t proto_id, - ngx_http_request_t *r) + ngx_http_request_t *r, ngx_qjs_owner_t *owner) { JSValue request; ngx_http_qjs_request_t *req; @@ -9416,13 +9532,13 @@ ngx_http_qjs_request_make(JSContext *cx, ngx_int_t proto_id, return JS_EXCEPTION; } - req = js_malloc(cx, sizeof(ngx_http_qjs_request_t)); + req = (ngx_http_qjs_request_t *) ngx_qjs_opaque_alloc(cx, + sizeof(ngx_http_qjs_request_t), r, owner); if (req == NULL) { JS_FreeValue(cx, request); - return JS_ThrowOutOfMemory(cx); + return JS_EXCEPTION; } - req->opaque.external = r; req->args = JS_UNDEFINED; req->request_body = JS_UNDEFINED; req->response_body = JS_UNDEFINED; @@ -9462,7 +9578,7 @@ ngx_http_qjs_request_finalizer(JSRuntime *rt, JSValue val) JS_FreeValueRT(rt, req->request_body); JS_FreeValueRT(rt, req->response_body); - js_free_rt(rt, req); + ngx_qjs_opaque_free(rt, &req->opaque); } @@ -9476,7 +9592,7 @@ ngx_http_qjs_periodic_finalizer(JSRuntime *rt, JSValue val) return; } - js_free_rt(rt, req); + ngx_qjs_opaque_free(rt, &req->opaque); } @@ -9574,7 +9690,7 @@ ngx_engine_qjs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, } ngx_qjs_arg(ctx->args[0]) = ngx_http_qjs_request_make(cx, proto_id, - external); + external, NULL); if (JS_IsException(ngx_qjs_arg(ctx->args[0]))) { goto failed; } diff --git a/nginx/ngx_js.c b/nginx/ngx_js.c index a85251b9..78cbe5b6 100644 --- a/nginx/ngx_js.c +++ b/nginx/ngx_js.c @@ -1301,6 +1301,10 @@ ngx_qjs_detach_ctx(ngx_js_ctx_t *ctx, JSContext *cx) opaque = JS_GetOpaque(ngx_qjs_arg(ctx->args[0]), class_id); if (opaque != NULL) { + if (opaque->owner != NULL) { + opaque->owner->alive = 0; + } + opaque->external = NULL; } } @@ -1832,6 +1836,103 @@ ngx_qjs_value_bytes_copy(JSContext *cx, JSValueConst value, ngx_str_t *str) } +ngx_js_opaque_t * +ngx_qjs_opaque_alloc(JSContext *cx, size_t size, void *external, + ngx_qjs_owner_t *owner) +{ + ngx_js_opaque_t *opaque; + + if (owner != NULL && owner->runtime != JS_GetRuntime(cx)) { + JS_ThrowTypeError(cx, "native object belongs to a different runtime"); + return NULL; + } + + opaque = js_malloc(cx, size); + if (opaque == NULL) { + return NULL; + } + + if (owner == NULL) { + owner = js_mallocz(cx, sizeof(ngx_qjs_owner_t)); + if (owner == NULL) { + js_free(cx, opaque); + return NULL; + } + + owner->alive = 1; + owner->runtime = JS_GetRuntime(cx); + } + + owner->references++; + opaque->external = external; + opaque->owner = owner; + + return opaque; +} + + +ngx_js_opaque_t * +ngx_qjs_opaque_get(JSContext *cx, JSValueConst value, JSClassID class_id) +{ + ngx_js_opaque_t *opaque; + + opaque = JS_GetOpaque2(cx, value, class_id); + if (opaque == NULL) { + return NULL; + } + + if (opaque->owner != NULL + && opaque->owner->runtime != JS_GetRuntime(cx)) + { + JS_ThrowTypeError(cx, "native object belongs to a different runtime"); + return NULL; + } + + if (opaque->external == NULL + || (opaque->owner != NULL && !opaque->owner->alive)) + { + JS_ThrowTypeError(cx, "native object is no longer valid"); + return NULL; + } + + return opaque; +} + + +void +ngx_qjs_owner_release(JSRuntime *rt, ngx_qjs_owner_t *owner) +{ + if (owner != NULL) { + njs_assert(owner->runtime == rt); + njs_assert(owner->references != 0); + + if (--owner->references == 0) { + js_free_rt(rt, owner); + } + } +} + + +void +ngx_qjs_opaque_free(JSRuntime *rt, ngx_js_opaque_t *opaque) +{ + ngx_qjs_owner_release(rt, opaque->owner); + js_free_rt(rt, opaque); +} + + +void +ngx_qjs_opaque_finalizer(JSRuntime *rt, JSValue value) +{ + ngx_js_opaque_t *opaque; + + opaque = JS_GetOpaque(value, JS_GetClassID(value)); + if (opaque != NULL) { + ngx_qjs_opaque_free(rt, opaque); + } +} + + static void ngx_qjs_timer_handler(ngx_event_t *ev) { diff --git a/nginx/ngx_js.h b/nginx/ngx_js.h index aa866d42..2b847b90 100644 --- a/nginx/ngx_js.h +++ b/nginx/ngx_js.h @@ -235,8 +235,20 @@ struct ngx_js_ctx_s { }; +#if (NJS_HAVE_QUICKJS) +typedef struct { + JSRuntime *runtime; + ngx_uint_t references; + unsigned alive:1; +} ngx_qjs_owner_t; +#endif + + typedef struct { void *external; +#if (NJS_HAVE_QUICKJS) + ngx_qjs_owner_t *owner; +#endif } ngx_js_opaque_t; @@ -408,6 +420,13 @@ ngx_int_t ngx_qjs_bytes_copy(JSContext *cx, ngx_str_t *dst, const ngx_str_t *src); ngx_int_t ngx_qjs_value_bytes_copy(JSContext *cx, JSValueConst value, ngx_str_t *str); +ngx_js_opaque_t *ngx_qjs_opaque_alloc(JSContext *cx, size_t size, + void *external, ngx_qjs_owner_t *owner); +ngx_js_opaque_t *ngx_qjs_opaque_get(JSContext *cx, JSValueConst value, + JSClassID class_id); +void ngx_qjs_owner_release(JSRuntime *rt, ngx_qjs_owner_t *owner); +void ngx_qjs_opaque_free(JSRuntime *rt, ngx_js_opaque_t *opaque); +void ngx_qjs_opaque_finalizer(JSRuntime *rt, JSValue value); ngx_int_t ngx_qjs_header_value(JSContext *cx, ngx_pool_t *pool, JSValueConst val, ngx_str_t *dst); diff --git a/nginx/ngx_stream_js_module.c b/nginx/ngx_stream_js_module.c index bd6e6a79..334c04f8 100644 --- a/nginx/ngx_stream_js_module.c +++ b/nginx/ngx_stream_js_module.c @@ -91,6 +91,12 @@ typedef struct { JSValue callbacks[NGX_JS_EVENT_MAX]; } ngx_stream_qjs_session_t; + +typedef struct { + ngx_js_opaque_t opaque; + ngx_uint_t buffer_type; +} ngx_stream_qjs_variables_t; + #endif #define ngx_stream_pending(ctx) \ @@ -191,7 +197,10 @@ static ngx_int_t ngx_stream_qjs_run_event(ngx_stream_session_t *s, static ngx_int_t ngx_stream_qjs_body_filter(ngx_stream_session_t *s, ngx_stream_js_ctx_t *ctx, ngx_chain_t *in, ngx_uint_t from_upstream); -static ngx_stream_session_t *ngx_stream_qjs_session(JSValueConst val); +static ngx_stream_session_t *ngx_stream_qjs_session(JSContext *cx, + JSValueConst val); +static JSValue ngx_stream_qjs_variables_make(JSContext *cx, + JSValueConst session, JSClassID class_id, int type); static JSValue ngx_stream_qjs_session_make(JSContext *cx, ngx_int_t proto_id, ngx_stream_session_t *s); static void ngx_stream_qjs_session_mark(JSRuntime *rt, JSValueConst val, @@ -902,7 +911,7 @@ static JSClassDef ngx_stream_qjs_flags_class = { static JSClassDef ngx_stream_qjs_variables_class = { "Variables", - .finalizer = NULL, + .finalizer = ngx_qjs_opaque_finalizer, .exotic = & (JSClassExoticMethods) { .get_own_property = ngx_stream_qjs_variables_own_property, .set_property = ngx_stream_qjs_variables_set_property, @@ -2137,9 +2146,9 @@ ngx_stream_qjs_ext_done(JSContext *cx, JSValueConst this_val, int argc, ngx_stream_js_ctx_t *ctx; ngx_stream_session_t *s; - s = ngx_stream_qjs_session(this_val); + s = ngx_stream_qjs_session(cx, this_val); if (s == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a session object"); + return JS_EXCEPTION; } status = (ngx_int_t) magic; @@ -2184,9 +2193,9 @@ ngx_stream_qjs_ext_log(JSContext *cx, JSValueConst this_val, int argc, const char *msg; ngx_stream_session_t *s; - s = ngx_stream_qjs_session(this_val); + s = ngx_stream_qjs_session(cx, this_val); if (s == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a session object"); + return JS_EXCEPTION; } for (n = 0; n < argc; n++) { @@ -2291,9 +2300,10 @@ ngx_stream_qjs_ext_on(JSContext *cx, JSValueConst this_val, int argc, ngx_stream_qjs_session_t *ses; const ngx_stream_qjs_event_t *e; - ses = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_STREAM_SESSION); + ses = (ngx_stream_qjs_session_t *) ngx_qjs_opaque_get(cx, this_val, + NGX_QJS_CLASS_ID_STREAM_SESSION); if (ses == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a session object"); + return JS_EXCEPTION; } s = ses->opaque.external; @@ -2304,6 +2314,10 @@ ngx_stream_qjs_ext_on(JSContext *cx, JSValueConst this_val, int argc, return JS_EXCEPTION; } + if (ngx_stream_qjs_session(cx, this_val) == NULL) { + return JS_EXCEPTION; + } + if (JS_IsFunction(cx, ngx_qjs_arg(ctx->events[e->id].function))) { return JS_ThrowTypeError(cx, "event handler \"%s\" is already set", e->name.data); @@ -2330,9 +2344,9 @@ ngx_stream_qjs_ext_off(JSContext *cx, JSValueConst this_val, int argc, ngx_stream_session_t *s; const ngx_stream_qjs_event_t *e; - s = ngx_stream_qjs_session(this_val); + s = ngx_stream_qjs_session(cx, this_val); if (s == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a session object"); + return JS_EXCEPTION; } ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module); @@ -2342,6 +2356,10 @@ ngx_stream_qjs_ext_off(JSContext *cx, JSValueConst this_val, int argc, return JS_EXCEPTION; } + if (ngx_stream_qjs_session(cx, this_val) == NULL) { + return JS_EXCEPTION; + } + ngx_qjs_arg(ctx->events[e->id].function) = JS_NULL; ctx->events[e->id].data_type = NGX_JS_UNSET; @@ -2353,24 +2371,8 @@ static JSValue ngx_stream_qjs_ext_periodic_variables(JSContext *cx, JSValueConst this_val, int type) { - JSValue obj; - ngx_stream_qjs_session_t *ses; - - ses = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_STREAM_PERIODIC); - if (ses == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a periodic object"); - } - - obj = JS_NewObjectProtoClass(cx, JS_NULL, NGX_QJS_CLASS_ID_STREAM_VARS); - - /* - * Using lowest bit of the pointer to store the buffer type. - */ - type = (type == NGX_JS_BUFFER) ? 1 : 0; - JS_SetOpaque(obj, (void *) ((uintptr_t) ses->opaque.external - | (uintptr_t) type)); - - return obj; + return ngx_stream_qjs_variables_make(cx, this_val, + NGX_QJS_CLASS_ID_STREAM_PERIODIC, type); } @@ -2380,9 +2382,9 @@ ngx_stream_qjs_ext_remote_address(JSContext *cx, JSValueConst this_val) ngx_connection_t *c; ngx_stream_session_t *s; - s = ngx_stream_qjs_session(this_val); + s = ngx_stream_qjs_session(cx, this_val); if (s == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a session object"); + return JS_EXCEPTION; } c = s->connection; @@ -2406,9 +2408,9 @@ ngx_stream_qjs_ext_send(JSContext *cx, JSValueConst this_val, int argc, ngx_stream_js_ctx_t *ctx; ngx_stream_session_t *s; - s = ngx_stream_qjs_session(this_val); + s = ngx_stream_qjs_session(cx, this_val); if (s == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a session object"); + return JS_EXCEPTION; } c = s->connection; @@ -2598,9 +2600,9 @@ ngx_stream_qjs_ext_set_return_value(JSContext *cx, JSValueConst this_val, ngx_js_ctx_t *ctx; ngx_stream_session_t *s; - s = ngx_stream_qjs_session(this_val); + s = ngx_stream_qjs_session(cx, this_val); if (s == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a session object"); + return JS_EXCEPTION; } ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module); @@ -2615,23 +2617,8 @@ ngx_stream_qjs_ext_set_return_value(JSContext *cx, JSValueConst this_val, static JSValue ngx_stream_qjs_ext_variables(JSContext *cx, JSValueConst this_val, int type) { - JSValue obj; - ngx_stream_session_t *s; - - s = ngx_stream_qjs_session(this_val); - if (s == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a session object"); - } - - obj = JS_NewObjectProtoClass(cx, JS_NULL, NGX_QJS_CLASS_ID_STREAM_VARS); - - /* - * Using lowest bit of the pointer to store the buffer type. - */ - type = (type == NGX_JS_BUFFER) ? 1 : 0; - JS_SetOpaque(obj, (void *) ((uintptr_t) s | (uintptr_t) type)); - - return obj; + return ngx_stream_qjs_variables_make(cx, this_val, + NGX_QJS_CLASS_ID_STREAM_SESSION, type); } @@ -2648,9 +2635,9 @@ ngx_stream_qjs_ext_js_var_names(JSContext *cx, JSValueConst this_val, int argc, ngx_stream_variable_t *v; ngx_stream_core_main_conf_t *cmcf; - s = ngx_stream_qjs_session(this_val); + s = ngx_stream_qjs_session(cx, this_val); if (s == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a session object"); + return JS_EXCEPTION; } prefix = NULL; @@ -2716,9 +2703,9 @@ ngx_stream_qjs_ext_uint(JSContext *cx, JSValueConst this_val, int offset) ngx_uint_t *field; ngx_stream_session_t *s; - s = ngx_stream_qjs_session(this_val); + s = ngx_stream_qjs_session(cx, this_val); if (s == NULL) { - return JS_ThrowTypeError(cx, "\"this\" is not a session object"); + return JS_EXCEPTION; } field = (ngx_uint_t *) ((u_char *) s + offset); @@ -2746,19 +2733,19 @@ ngx_stream_qjs_variables_own_property(JSContext *cx, ngx_str_t name, name_lc; ngx_uint_t key; ngx_stream_session_t *s; + ngx_stream_qjs_variables_t *variables; ngx_stream_variable_value_t *vv; u_char storage[64]; - s = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_STREAM_VARS); - - buffer_type = ((uintptr_t) s & 1) ? NGX_JS_BUFFER : NGX_JS_STRING; - s = (ngx_stream_session_t *) ((uintptr_t) s & ~(uintptr_t) 1); - - if (s == NULL) { - (void) JS_ThrowTypeError(cx, "\"this\" is not a session object"); + variables = (ngx_stream_qjs_variables_t *) ngx_qjs_opaque_get(cx, obj, + NGX_QJS_CLASS_ID_STREAM_VARS); + if (variables == NULL) { return -1; } + buffer_type = variables->buffer_type; + s = variables->opaque.external; + name.data = (u_char *) JS_AtomToCString(cx, prop); if (name.data == NULL) { return -1; @@ -2784,6 +2771,10 @@ ngx_stream_qjs_variables_own_property(JSContext *cx, vv = ngx_stream_get_variable(s, &name_lc, key); JS_FreeCString(cx, (char *) name.data); + if (ngx_qjs_opaque_get(cx, obj, NGX_QJS_CLASS_ID_STREAM_VARS) == NULL) { + return -1; + } + if (vv == NULL || vv->not_found) { return 0; } @@ -2807,19 +2798,19 @@ ngx_stream_qjs_variables_set_property(JSContext *cx, JSValueConst obj, ngx_uint_t key; ngx_stream_session_t *s; ngx_stream_variable_t *v; + ngx_stream_qjs_variables_t *variables; ngx_stream_variable_value_t *vv; ngx_stream_core_main_conf_t *cmcf; u_char storage[64]; - s = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_STREAM_VARS); - - s = (ngx_stream_session_t *) ((uintptr_t) s & ~(uintptr_t) 1); - - if (s == NULL) { - (void) JS_ThrowTypeError(cx, "\"this\" is not a session object"); + variables = (ngx_stream_qjs_variables_t *) ngx_qjs_opaque_get(cx, obj, + NGX_QJS_CLASS_ID_STREAM_VARS); + if (variables == NULL) { return -1; } + s = variables->opaque.external; + name.data = (u_char *) JS_AtomToCString(cx, prop); if (name.data == NULL) { return -1; @@ -2855,6 +2846,10 @@ ngx_stream_qjs_variables_set_property(JSContext *cx, JSValueConst obj, return -1; } + if (ngx_qjs_opaque_get(cx, obj, NGX_QJS_CLASS_ID_STREAM_VARS) == NULL) { + return -1; + } + if (v->set_handler != NULL) { vv = ngx_pcalloc(s->connection->pool, sizeof(ngx_stream_variable_value_t)); @@ -3008,17 +3003,51 @@ ngx_stream_qjs_body_filter(ngx_stream_session_t *s, ngx_stream_js_ctx_t *ctx, } +static JSValue +ngx_stream_qjs_variables_make(JSContext *cx, JSValueConst session, + JSClassID class_id, int type) +{ + JSValue obj; + ngx_js_opaque_t *parent; + ngx_stream_qjs_variables_t *variables; + + parent = ngx_qjs_opaque_get(cx, session, class_id); + if (parent == NULL) { + return JS_EXCEPTION; + } + + obj = JS_NewObjectProtoClass(cx, JS_NULL, NGX_QJS_CLASS_ID_STREAM_VARS); + if (JS_IsException(obj)) { + return obj; + } + + variables = (ngx_stream_qjs_variables_t *) ngx_qjs_opaque_alloc(cx, + sizeof(ngx_stream_qjs_variables_t), + parent->external, parent->owner); + if (variables == NULL) { + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } + + variables->buffer_type = type == NGX_JS_BUFFER ? NGX_JS_BUFFER + : NGX_JS_STRING; + JS_SetOpaque(obj, variables); + + return obj; +} + + static ngx_stream_session_t * -ngx_stream_qjs_session(JSValueConst val) +ngx_stream_qjs_session(JSContext *cx, JSValueConst val) { - ngx_stream_qjs_session_t *ses; + ngx_js_opaque_t *opaque; - ses = JS_GetOpaque(val, NGX_QJS_CLASS_ID_STREAM_SESSION); - if (ses == NULL) { + opaque = ngx_qjs_opaque_get(cx, val, NGX_QJS_CLASS_ID_STREAM_SESSION); + if (opaque == NULL) { return NULL; } - return ses->opaque.external; + return opaque->external; } @@ -3035,14 +3064,13 @@ ngx_stream_qjs_session_make(JSContext *cx, ngx_int_t proto_id, return JS_EXCEPTION; } - ses = js_malloc(cx, sizeof(ngx_stream_qjs_session_t)); + ses = (ngx_stream_qjs_session_t *) ngx_qjs_opaque_alloc(cx, + sizeof(ngx_stream_qjs_session_t), s, NULL); if (ses == NULL) { JS_FreeValue(cx, session); - return JS_ThrowOutOfMemory(cx); + return JS_EXCEPTION; } - ses->opaque.external = s; - for (i = 0; i < NGX_JS_EVENT_MAX; i++) { ses->callbacks[i] = JS_UNDEFINED; } @@ -3084,7 +3112,7 @@ ngx_stream_qjs_session_finalizer(JSRuntime *rt, JSValue val) JS_FreeValueRT(rt, ses->callbacks[i]); } - js_free_rt(rt, ses); + ngx_qjs_opaque_free(rt, &ses->opaque); } @@ -3098,7 +3126,7 @@ ngx_stream_qjs_periodic_finalizer(JSRuntime *rt, JSValue val) return; } - js_free_rt(rt, ses); + ngx_qjs_opaque_free(rt, &ses->opaque); } diff --git a/nginx/t/js_http_lifetime_scopes.t b/nginx/t/js_http_lifetime_scopes.t new file mode 100644 index 00000000..c9ae9c10 --- /dev/null +++ b/nginx/t/js_http_lifetime_scopes.t @@ -0,0 +1,199 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) F5, Inc. + +# Tests for QuickJS lifetime scopes during redirects and periodic cleanup. + +############################################################################### + +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; +worker_processes 1; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_context_reuse 1; + js_import test.js; + js_var $test_value original; + js_shared_dict_zone zone=scopes:32k; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /engine { + js_content test.engine; + } + + location /source { + js_import source.js; + js_content source.content; + } + + location /destination { + js_import destination.js; + js_content destination.content; + } + + location @periodic { + js_periodic test.periodic interval=40ms; + } + + location @forced { + js_periodic test.forced interval=50ms; + } + + location /results { + js_content test.results; + } + } +} + +EOF + +$t->write_file('source.js', <<'EOF'); + let headers; + + function content(r) { + let expired = headers === undefined; + if (headers !== undefined) { + try { + headers.host; + } catch (e) { + expired = e instanceof TypeError; + } + } + + headers = r.headersIn; + njs.on('exit', () => { + ngx.shared.scopes.set('source_exit', + String(headers.host === 'localhost')); + }); + r.internalRedirect('/destination?source=' + expired); + } + + export default {content}; +EOF + +$t->write_file('destination.js', <<'EOF'); + let variables; + + function content(r) { + let expired = variables === undefined; + if (variables !== undefined) { + try { + variables.test_value; + } catch (e) { + expired = e instanceof TypeError; + } + } + + variables = r.variables; + njs.on('exit', () => { + ngx.shared.scopes.set('destination_exit', + String(variables.test_value === 'original')); + }); + r.return(200, r.args.source + ':' + expired); + } + + export default {content}; +EOF + +$t->write_file('test.js', <<'EOF'); + let previous, previousForced; + + function engine(r) { + r.return(200, njs.engine); + } + + function check(value) { + try { + value.test_value; + } catch (e) { + return e instanceof TypeError; + } + + return false; + } + + function periodic(s) { + if (njs.engine !== 'QuickJS') { + return; + } + + if (previous !== undefined) { + ngx.shared.scopes.set('periodic', String(check(previous))); + } + + previous = s.variables; + } + + function forced(s) { + if (njs.engine !== 'QuickJS') { + return; + } + + if (previousForced === undefined) { + previousForced = s.rawVariables; + setTimeout(() => {}, 100000); + return; + } + + ngx.shared.scopes.set('forced', String(check(previousForced))); + } + + function results(r) { + const d = ngx.shared.scopes; + r.return(200, ['periodic', 'forced', 'source_exit', 'destination_exit'] + .map(name => d.get(name)).join(':')); + } + + export default {engine, periodic, forced, results}; +EOF + +$t->try_run('no njs available'); + +plan(skip_all => 'QuickJS-specific lifetime tests') + if http_get('/engine') =~ /njs$/m; + +$t->plan(3); + +############################################################################### + +like(http_get('/source'), qr/true:true$/, 'initial redirect scopes'); +like(http_get('/source'), qr/true:true$/, 'old scopes expire independently'); + +my $result; +for (1 .. 50) { + $result = http_get('/results'); + last if $result =~ /true:true:true:true$/; + select undef, undef, undef, 0.02; +} + +like($result, qr/true:true:true:true$/, + 'periodic expiry and synchronous exit-hook access'); + +############################################################################### diff --git a/nginx/t/js_http_object_lifetime.t b/nginx/t/js_http_object_lifetime.t new file mode 100644 index 00000000..97dba293 --- /dev/null +++ b/nginx/t/js_http_object_lifetime.t @@ -0,0 +1,227 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) F5, Inc. + +# Tests for QuickJS HTTP object lifetime after request cleanup. + +############################################################################### + +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; +worker_processes 1; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_context_reuse 1; + js_import test.js; + js_var $test_value original; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /engine { + js_content test.engine; + } + + location /a { + js_content test.content; + } + + location /child { + return 200 'child'; + } + } +} + +EOF + +$t->write_file('test.js', <<'EOF'); + let saved, snapshots; + + function engine(r) { + r.return(200, njs.engine); + } + + const checks = { + headers_in: () => saved.host, + headers_in_keys: () => Object.keys(saved), + headers_out: () => saved['X-Test'], + headers_out_set: () => { saved['X-Test'] = 'changed'; }, + headers_out_delete: () => { delete saved['X-Test']; }, + headers_out_define: () => { + Object.defineProperty(saved, 'X-Test', {value: 'changed'}); + }, + headers_out_keys: () => Object.keys(saved), + variables: () => saved.test_value, + variables_set: () => { saved.test_value = 'changed'; }, + raw_variables: () => saved.test_value, + form: () => saved.get('x'), + form_all: () => saved.getAll('x'), + form_has: () => saved.has('x'), + form_each: () => saved.forEach(() => {}), + form_files: () => saved.hasFiles(), + form_file: () => saved.get('upload'), + request: () => saved.uri, + args_hot: () => saved.args, + body_cold: () => saved.requestText, + body_hot: () => saved.requestText, + body_switch: () => saved.requestBuffer, + read_text: () => saved.readRequestText(), + read_form: () => saved.readRequestForm(), + reply: () => saved.status, + reply_callback: () => saved.status, + reply_headers: () => saved.headersOut, + reply_header_view: () => saved['Content-Type'], + reply_variables: () => saved.test_value, + reply_body: () => saved.responseBuffer + }; + + async function content(r) { + const name = r.args.case; + if (r.args.keep) { + snapshots = {args: r.args, uri: r.uri}; + + if (name.startsWith('headers_in')) { + saved = r.headersIn; + } else if (name.startsWith('headers_out')) { + r.headersOut['X-Test'] = 'original'; + saved = r.headersOut; + } else if (name.startsWith('variables')) { + saved = r.variables; + } else if (name === 'raw_variables') { + saved = r.rawVariables; + snapshots.buffer = saved.test_value; + } else if (name.startsWith('form')) { + saved = await r.readRequestForm(); + snapshots.form = saved.get('x'); + if (name === 'form_file') { + snapshots.filename = saved.get('upload').name; + } + } else if (name.startsWith('reply')) { + if (name === 'reply_callback') { + saved = await new Promise(resolve => { + r.subrequest('/child', resolve); + }); + } else { + saved = await r.subrequest('/child'); + } + + if (saved.status !== 200 || saved.headersOut === undefined) + { + throw new Error('reply expired before parent cleanup'); + } + + snapshots.reply = saved.responseText; + if (name === 'reply_header_view') { + saved = saved.headersOut; + } else if (name === 'reply_variables') { + saved = saved.variables; + } + } else { + saved = r; + if (name === 'body_hot' || name === 'body_switch') { + snapshots.body = r.requestText; + } + } + + r.return(200, 'kept'); + return; + } + + let expired = false; + try { + checks[name](); + } catch (e) { + expired = e instanceof TypeError; + } + + const independent = snapshots.args.keep === '1' + && snapshots.uri === '/a' + && (snapshots.form === undefined + || snapshots.form === 'original') + && (snapshots.body === undefined + || snapshots.body === 'x=original') + && (snapshots.filename === undefined + || snapshots.filename === 'test.txt') + && (snapshots.reply === undefined + || snapshots.reply === 'child') + && (snapshots.buffer === undefined + || snapshots.buffer.toString() === 'original'); + + saved = null; + snapshots = null; + r.return(200, expired + ':' + independent); + } + + export default {content, engine}; + +EOF + +my @cases = qw/ + headers_in headers_in_keys + headers_out headers_out_set headers_out_delete headers_out_define + headers_out_keys variables variables_set raw_variables + form form_all form_has form_each form_files form_file + request args_hot body_cold body_hot body_switch read_text read_form + reply reply_callback reply_headers reply_header_view reply_variables + reply_body +/; + +@cases = grep { $_ eq $ENV{NJS_LIFETIME_CASE} } @cases + if defined $ENV{NJS_LIFETIME_CASE}; + +$t->try_run('no njs available'); + +plan(skip_all => 'QuickJS-specific lifetime tests') + if http_get('/engine') =~ /njs$/m; + +$t->plan(2 * @cases); + +############################################################################### + +for my $case (@cases) { + my $type = 'application/x-www-form-urlencoded'; + my $body = 'x=original'; + + if ($case eq 'form_file') { + $type = 'multipart/form-data; boundary=A'; + $body = "--A\r\nContent-Disposition: form-data; name=\"x\"\r\n\r\n" + . "original\r\n--A\r\n" + . "Content-Disposition: form-data; name=\"upload\"; " + . "filename=\"test.txt\"\r\n\r\npayload\r\n--A--\r\n"; + } + + like(http("POST /a?keep=1&case=$case HTTP/1.0\r\n" + . "Host: localhost\r\n" + . "Content-Type: $type\r\n" + . "Content-Length: " . length($body) . "\r\n\r\n$body"), + qr/kept$/, "$case before cleanup"); + like(http_get("/a?case=$case"), qr/true:true$/, + "$case expires without invalidating extracted values"); +} + +############################################################################### diff --git a/nginx/t/stream_js_object_lifetime.t b/nginx/t/stream_js_object_lifetime.t new file mode 100644 index 00000000..82901db1 --- /dev/null +++ b/nginx/t/stream_js_object_lifetime.t @@ -0,0 +1,207 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) F5, Inc. + +# Tests for QuickJS stream objects after session and periodic cleanup. + +############################################################################### + +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; +worker_processes 1; + +events { +} + +stream { + %%TEST_GLOBALS_STREAM%% + + js_context_reuse 1; + js_import test.js; + js_var $test_value original; + js_var $result; + js_set $engine test.engine; + js_shared_dict_zone zone=scopes:32k; + + server { + listen 127.0.0.1:8081; + js_preread test.preread; + return $result; + } + + server { + listen 127.0.0.1:8082; + return $engine; + } + + server { + listen 127.0.0.1:8083; + js_periodic test.periodic interval=40ms; + return OK; + } + + server { + listen 127.0.0.1:8084; + js_preread test.results; + return $result; + } +} + +EOF + +$t->write_file('test.js', <<'EOF'); + let saved, snapshot, flags, address, previous; + + function engine() { + return njs.engine; + } + + const checks = { + variables: () => saved.test_value, + variables_set: () => { saved.test_value = 'changed'; }, + variables_define: () => { + Object.defineProperty(saved, 'test_value', {value: 'changed'}); + }, + raw_variables: () => saved.test_value, + session: () => saved.remoteAddress, + status: () => saved.status, + on: () => saved.on('upload', () => {}), + off: () => saved.off('upload'), + done: () => saved.done(), + send: () => saved.sendUpstream('x'), + return_value: () => saved.setReturnValue('x'), + var_names: () => saved.jsVarNames() + }; + + function preread(s) { + s.on('upload', (data, f) => { + if (data.length === 0) { + return; + } + + const parts = data.split(':'); + const action = parts[0], name = parts[1]; + if (action === 'keep') { + snapshot = s.rawVariables.test_value; + flags = f; + address = s.remoteAddress; + if (name.startsWith('variables')) { + saved = s.variables; + } else if (name === 'raw_variables') { + saved = s.rawVariables; + } else { + saved = s; + } + + s.variables.result = 'kept'; + } else { + let expired = false; + try { + checks[name](); + } catch (e) { + expired = e instanceof TypeError; + } + + const independent = snapshot.toString() === 'original' + && flags.from_upstream === false + && address === '127.0.0.1'; + saved = null; + snapshot = null; + flags = null; + s.variables.result = expired + ':' + independent; + } + + s.done(); + }); + } + + function periodic(s) { + if (njs.engine !== 'QuickJS' + || ngx.shared.scopes.get('enabled') !== '1') + { + return; + } + + if (previous !== undefined) { + let expired = false; + try { + previous.test_value; + } catch (e) { + expired = e instanceof TypeError; + } + + ngx.shared.scopes.set('periodic', String(expired)); + } + + previous = s.variables; + } + + function results(s) { + s.on('upload', data => { + if (data.length === 0) { + return; + } + + ngx.shared.scopes.set('enabled', '1'); + s.variables.result = ngx.shared.scopes.get('periodic') || '-'; + s.done(); + }); + } + + export default {engine, preread, periodic, results}; +EOF + +my @cases = qw/variables variables_set variables_define raw_variables + session status on off done send return_value var_names/; +@cases = grep { $_ eq $ENV{NJS_LIFETIME_CASE} } @cases + if defined $ENV{NJS_LIFETIME_CASE}; +my $periodic = !defined $ENV{NJS_LIFETIME_CASE} + || $ENV{NJS_LIFETIME_CASE} eq 'periodic'; + +$t->try_run('no stream njs available'); +plan(skip_all => 'QuickJS-specific lifetime tests') + if stream('127.0.0.1:' . port(8082))->read() =~ /njs$/; +$t->plan(2 * @cases + ($periodic ? 1 : 0)); + +############################################################################### + +for my $case (@cases) { + is(stream('127.0.0.1:' . port(8081))->io("keep:$case"), 'kept', + "$case before cleanup"); + is(stream('127.0.0.1:' . port(8081))->io("read:$case"), 'true:true', + "$case expires without invalidating extracted values"); +} + +if ($periodic) { + my $result; + for (1 .. 50) { + $result = stream('127.0.0.1:' . port(8084))->io('results'); + last if $result eq 'true'; + select undef, undef, undef, 0.02; + } + + is($result, 'true', + 'periodic variables expire with their originating session'); +} + +###############################################################################