diff --git a/nginx/config b/nginx/config index 700ae4abf..436f06cbe 100644 --- a/nginx/config +++ b/nginx/config @@ -3,6 +3,7 @@ ngx_addon_name="ngx_js_module" NJS_OPENSSL=${NJS_OPENSSL:-YES} NJS_LIBXSLT=${NJS_LIBXSLT:-YES} NJS_ZLIB=${NJS_ZLIB:-YES} +NJS_QUICKJS=${NJS_QUICKJS:-YES} NJS_DEPS="$ngx_addon_dir/ngx_js.h \ $ngx_addon_dir/ngx_js_fetch.h \ @@ -12,9 +13,78 @@ NJS_SRCS="$ngx_addon_dir/ngx_js.c \ $ngx_addon_dir/ngx_js_regex.c \ $ngx_addon_dir/ngx_js_shared_dict.c" +QJS_DEPS="" +QJS_SRCS="" + NJS_OPENSSL_LIB= NJS_XSLT_LIB= NJS_ZLIB_LIB= +NJS_QUICKJS_LIB= +NJS_QUICKJS_INC= +NJS_HAVE_QUICKJS= + +if [ $NJS_QUICKJS != NO ]; then + + ngx_feature="QuickJS library -lquickjs.lto" + ngx_feature_name=NJS_HAVE_QUICKJS + ngx_feature_run=yes + ngx_feature_incs="#if defined(__GNUC__) && (__GNUC__ >= 8) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored \"-Wcast-function-type\" + #endif + + #include " + ngx_feature_path="" + ngx_feature_libs="-lquickjs.lto -lm -ldl -lpthread" + ngx_feature_test="JSRuntime *rt; + + rt = JS_NewRuntime(); + (void) JS_GetClassID; + JS_FreeRuntime(rt); + return 0;" + . auto/feature + + if [ $ngx_found = no ]; then + ngx_feature="QuickJS library -lquickjs" + ngx_feature_libs="-lquickjs -lm -ldl -lpthread" + + . auto/feature + fi + + if [ $ngx_found = no ]; then + ngx_feature="QuickJS library -I/usr/include/quickjs/ -L/usr/lib/quickjs/ -lquickjs.lto" + ngx_feature_path="/usr/include/quickjs/" + ngx_feature_libs="-L/usr/lib/quickjs/ -lquickjs.lto -lm -ldl -lpthread" + + . auto/feature + fi + + if [ $ngx_found = no ]; then + ngx_feature="QuickJS library -I/usr/include/quickjs/ -L/usr/lib/quickjs/ -lquickjs" + ngx_feature_libs="-L/usr/lib/quickjs/ -lquickjs -lm -ldl -lpthread" + + . auto/feature + fi + + if [ $ngx_found = yes ]; then + + ngx_feature="QuickJS JS_NewTypedArray()" + ngx_feature_test="(void) JS_NewTypedArray; + return 0;" + + . auto/feature + + if [ $ngx_found = yes ]; then + have=NJS_HAVE_QUICKJS_NEW_TYPED_ARRAY . auto/have + fi + + NJS_HAVE_QUICKJS=YES + NJS_QUICKJS_LIB="$ngx_feature_libs" + NJS_QUICKJS_INC="$ngx_feature_path" + + echo " enabled QuickJS engine" + fi +fi if [ $NJS_OPENSSL != NO ]; then NJS_OPENSSL_LIB=OPENSSL @@ -37,17 +107,30 @@ if [ $NJS_ZLIB != NO ]; then have=NJS_HAVE_ZLIB . auto/have NJS_SRCS="$NJS_SRCS $ngx_addon_dir/../external/njs_zlib_module.c" + if [ "$NJS_HAVE_QUICKJS" = "YES" ]; then + NJS_SRCS="$NJS_SRCS $ngx_addon_dir/../external/qjs_zlib_module.c" + fi + echo " enabled zlib module" fi + +NJS_ENGINE_DEP="$ngx_addon_dir/../build/libnjs.a" +NJS_ENGINE_LIB="$ngx_addon_dir/../build/libnjs.a" +if [ "$NJS_HAVE_QUICKJS" = "YES" ]; then + NJS_ENGINE_DEP="$ngx_addon_dir/../build/libqjs.a" + NJS_ENGINE_LIB="$ngx_addon_dir/../build/libnjs.a $ngx_addon_dir/../build/libqjs.a" +fi + if [ $HTTP != NO ]; then ngx_module_type=HTTP_AUX_FILTER ngx_module_name=ngx_http_js_module - ngx_module_incs="$ngx_addon_dir/../src $ngx_addon_dir/../build" - ngx_module_deps="$ngx_addon_dir/../build/libnjs.a $NJS_DEPS" - ngx_module_srcs="$ngx_addon_dir/ngx_http_js_module.c $NJS_SRCS" + ngx_module_incs="$ngx_addon_dir/../src $ngx_addon_dir/../build \ + $NJS_QUICKJS_INC" + ngx_module_deps="$NJS_ENGINE_DEP $NJS_DEPS $QJS_DEPS" + ngx_module_srcs="$ngx_addon_dir/ngx_http_js_module.c $NJS_SRCS $QJS_SRCS" ngx_module_libs="PCRE $NJS_OPENSSL_LIB $NJS_XSLT_LIB $NJS_ZLIB_LIB \ - $ngx_addon_dir/../build/libnjs.a -lm" + $NJS_QUICKJS_LIB $NJS_ENGINE_LIB -lm" . auto/module @@ -59,11 +142,12 @@ fi if [ $STREAM != NO ]; then ngx_module_type=STREAM ngx_module_name=ngx_stream_js_module - ngx_module_incs="$ngx_addon_dir/../src $ngx_addon_dir/../build" - ngx_module_deps="$ngx_addon_dir/../build/libnjs.a $NJS_DEPS" - ngx_module_srcs="$ngx_addon_dir/ngx_stream_js_module.c $NJS_SRCS" + ngx_module_incs="$ngx_addon_dir/../src $ngx_addon_dir/../build \ + $NJS_QUICKJS_INC" + ngx_module_deps="$NJS_ENGINE_DEP $NJS_DEPS $QJS_DEPS" + ngx_module_srcs="$ngx_addon_dir/ngx_stream_js_module.c $NJS_SRCS $QJS_SRCS" ngx_module_libs="PCRE $NJS_OPENSSL_LIB $NJS_XSLT_LIB $NJS_ZLIB_LIB \ - $ngx_addon_dir/../build/libnjs.a -lm" + $NJS_QUICKJS_LIB $NJS_ENGINE_LIB -lm" . auto/module fi diff --git a/nginx/config.make b/nginx/config.make index cf7859e95..2fa40063f 100644 --- a/nginx/config.make +++ b/nginx/config.make @@ -3,7 +3,15 @@ cat << END >> $NGX_MAKEFILE $ngx_addon_dir/../build/libnjs.a: $NGX_MAKEFILE cd $ngx_addon_dir/.. \\ && if [ -f build/Makefile ]; then \$(MAKE) clean; fi \\ - && CFLAGS="\$(CFLAGS)" CC="\$(CC)" ./configure --no-openssl --no-libxml2 --no-zlib --no-pcre \\ + && CFLAGS="\$(CFLAGS)" CC="\$(CC)" ./configure --no-openssl \\ + --no-libxml2 --no-zlib --no-pcre --no-quickjs \\ && \$(MAKE) libnjs +$ngx_addon_dir/../build/libqjs.a: $NGX_MAKEFILE + cd $ngx_addon_dir/.. \\ + && if [ -f build/Makefile ]; then \$(MAKE) clean; fi \\ + && CFLAGS="\$(CFLAGS)" CC="\$(CC)" ./configure --no-openssl \\ + --no-libxml2 --no-zlib --no-pcre \\ + && \$(MAKE) libnjs libqjs + END diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c index 35f988d08..c434d763a 100644 --- a/nginx/ngx_http_js_module.c +++ b/nginx/ngx_http_js_module.c @@ -46,6 +46,7 @@ typedef struct { #define NJS_HEADER_SEMICOLON 0x1 #define NJS_HEADER_SINGLE 0x2 #define NJS_HEADER_ARRAY 0x4 +#define NJS_HEADER_GET 0x8 typedef struct ngx_http_js_ctx_s ngx_http_js_ctx_t; @@ -86,6 +87,20 @@ typedef njs_int_t (*njs_http_js_header_handler_t)(njs_vm_t *vm, typedef njs_int_t (*njs_http_js_header_handler122_t)(njs_vm_t *vm, ngx_http_request_t *r, ngx_list_t *headers, njs_str_t *name, njs_value_t *setval, njs_value_t *retval); +#if (NJS_HAVE_QUICKJS) +typedef int (*njs_http_qjs_header_handler_t)(JSContext *cx, + ngx_http_request_t *r, ngx_str_t *name, JSPropertyDescriptor *pdesc, + JSValue *value, unsigned flags); + + +typedef struct { + ngx_http_request_t *request; + JSValue args; + JSValue request_body; + JSValue response_body; +} ngx_http_qjs_request_t; + +#endif typedef struct { @@ -260,6 +275,84 @@ static njs_int_t ngx_http_js_server(njs_vm_t *vm, ngx_http_request_t *r, unsigned flags, njs_str_t *name, njs_value_t *setval, njs_value_t *retval); +#if (NJS_HAVE_QUICKJS) +static JSValue ngx_http_qjs_ext_args(JSContext *cx, JSValueConst this_val); +static JSValue ngx_http_qjs_ext_done(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv); +static JSValue ngx_http_qjs_ext_finish(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv); +static JSValue ngx_http_qjs_ext_headers_in(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_http_qjs_ext_headers_out(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_http_qjs_ext_http_version(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_http_qjs_ext_internal(JSContext *cx, JSValueConst this_val); +static JSValue ngx_http_qjs_ext_internal_redirect(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_http_qjs_ext_log(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv, int level); +static JSValue ngx_http_qjs_ext_periodic_variables(JSContext *cx, + JSValueConst this_val, int type); +static JSValue ngx_http_qjs_ext_parent(JSContext *cx, JSValueConst this_val); +static JSValue ngx_http_qjs_ext_remote_address(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_http_qjs_ext_request_body(JSContext *cx, + JSValueConst this_val, int type); +static JSValue ngx_http_qjs_ext_response_body(JSContext *cx, + JSValueConst this_val, int type); +static JSValue ngx_http_qjs_ext_return(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv); +static JSValue ngx_http_qjs_ext_send(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv); +static JSValue ngx_http_qjs_ext_send_buffer(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_http_qjs_ext_send_header(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_http_qjs_ext_set_return_value(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_http_qjs_ext_status_get(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_http_qjs_ext_status_set(JSContext *cx, JSValueConst this_val, + JSValueConst value); +static JSValue ngx_http_qjs_ext_string(JSContext *cx, JSValueConst this_val, + int offset); +static JSValue ngx_http_qjs_ext_subrequest(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv); +static JSValue ngx_http_qjs_ext_raw_headers(JSContext *cx, + JSValueConst this_val, int out); +static JSValue ngx_http_qjs_ext_variables(JSContext *cx, + JSValueConst this_val, int type); + +static int ngx_http_qjs_variables_own_property(JSContext *cx, + JSPropertyDescriptor *pdesc, JSValueConst obj, JSAtom prop); +static int ngx_http_qjs_variables_set_property(JSContext *cx, JSValueConst obj, + JSAtom atom, JSValueConst value, JSValueConst receiver, int flags); + +static int ngx_http_qjs_headers_in_own_property(JSContext *cx, + JSPropertyDescriptor *pdesc, JSValueConst obj, JSAtom prop); +static int ngx_http_qjs_headers_in_own_property_names(JSContext *ctx, + JSPropertyEnum **ptab, uint32_t *plen, JSValueConst obj); + +static int ngx_http_qjs_headers_out_own_property(JSContext *cx, + JSPropertyDescriptor *pdesc, JSValueConst obj, JSAtom prop); +static int ngx_http_qjs_headers_out_own_property_names(JSContext *cx, + JSPropertyEnum **ptab, uint32_t *plen, JSValueConst obj); +static int ngx_http_qjs_headers_out_set_property(JSContext *cx, + JSValueConst obj, JSAtom atom, JSValueConst value, JSValueConst receiver, + int flags); +static int ngx_http_qjs_headers_out_define_own_property(JSContext *cx, + JSValueConst this_obj, JSAtom prop, JSValueConst val, JSValueConst getter, + JSValueConst setter, int flags); +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 JSValue ngx_http_qjs_request_make(JSContext *cx, ngx_int_t proto_id, + ngx_http_request_t *r); +static void ngx_http_qjs_request_finalizer(JSRuntime *rt, JSValue val); +#endif + static ngx_pool_t *ngx_http_js_pool(ngx_http_request_t *r); static ngx_resolver_t *ngx_http_js_resolver(ngx_http_request_t *r); static ngx_msec_t ngx_http_js_resolver_timeout(ngx_http_request_t *r); @@ -304,6 +397,9 @@ static ngx_int_t ngx_http_js_parse_unsafe_uri(ngx_http_request_t *r, static ngx_conf_bitmask_t ngx_http_js_engines[] = { { ngx_string("njs"), NGX_ENGINE_NJS }, +#if (NJS_HAVE_QUICKJS) + { ngx_string("qjs"), NGX_ENGINE_QJS }, +#endif { ngx_null_string, 0 } }; @@ -328,6 +424,13 @@ static ngx_command_t ngx_http_js_commands[] = { offsetof(ngx_http_js_loc_conf_t, type), &ngx_http_js_engines }, + { ngx_string("js_context_reuse"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_size_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_js_loc_conf_t, reuse), + NULL }, + { ngx_string("js_import"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE13, ngx_js_import, @@ -497,8 +600,8 @@ static ngx_http_output_header_filter_pt ngx_http_next_header_filter; static ngx_http_output_body_filter_pt ngx_http_next_body_filter; -static njs_int_t ngx_http_js_request_proto_id; -static njs_int_t ngx_http_js_periodic_session_proto_id; +static njs_int_t ngx_http_js_request_proto_id = 1; +static njs_int_t ngx_http_js_periodic_session_proto_id = 2; static njs_external_t ngx_http_js_ext_request[] = { @@ -924,6 +1027,128 @@ static ngx_http_js_entry_t ngx_http_methods[] = { }; +#if (NJS_HAVE_QUICKJS) + +static JSClassID ngx_http_qjs_request_class_id; +static JSClassID ngx_http_qjs_periodic_class_id; +static JSClassID ngx_http_qjs_variables_class_id; +static JSClassID ngx_http_qjs_headers_in_class_id; +static JSClassID ngx_http_qjs_headers_out_class_id; + + +static const JSCFunctionListEntry ngx_http_qjs_ext_request[] = { + JS_CGETSET_DEF("args", ngx_http_qjs_ext_args, NULL), + JS_CFUNC_DEF("done", 0, ngx_http_qjs_ext_done), + JS_CFUNC_MAGIC_DEF("error", 1, ngx_http_qjs_ext_log, NGX_LOG_ERR), + JS_CFUNC_DEF("finish", 0, ngx_http_qjs_ext_finish), + JS_CGETSET_DEF("headersIn", ngx_http_qjs_ext_headers_in, NULL), + JS_CGETSET_DEF("headersOut", ngx_http_qjs_ext_headers_out, NULL), + JS_CGETSET_DEF("httpVersion", ngx_http_qjs_ext_http_version, NULL), + JS_CGETSET_DEF("internal", ngx_http_qjs_ext_internal, NULL), + JS_CFUNC_DEF("internalRedirect", 1, ngx_http_qjs_ext_internal_redirect), + JS_CFUNC_MAGIC_DEF("log", 1, ngx_http_qjs_ext_log, NGX_LOG_INFO), + JS_CGETSET_MAGIC_DEF("method", ngx_http_qjs_ext_string, NULL, + offsetof(ngx_http_request_t, method_name)), + JS_CGETSET_DEF("parent", ngx_http_qjs_ext_parent, NULL), + JS_CGETSET_MAGIC_DEF("rawHeadersIn", ngx_http_qjs_ext_raw_headers, NULL, 0), + JS_CGETSET_MAGIC_DEF("rawHeadersOut", ngx_http_qjs_ext_raw_headers, NULL, + 1), + JS_CGETSET_MAGIC_DEF("rawVariables", ngx_http_qjs_ext_variables, + NULL, NGX_JS_BUFFER), + JS_CGETSET_DEF("remoteAddress", ngx_http_qjs_ext_remote_address, NULL), + JS_CGETSET_MAGIC_DEF("requestBuffer", ngx_http_qjs_ext_request_body, NULL, + NGX_JS_BUFFER), + JS_CGETSET_MAGIC_DEF("requestText", ngx_http_qjs_ext_request_body, NULL, + NGX_JS_STRING), + JS_CGETSET_MAGIC_DEF("responseBuffer", ngx_http_qjs_ext_response_body, NULL, + NGX_JS_BUFFER), + JS_CGETSET_MAGIC_DEF("responseText", ngx_http_qjs_ext_response_body, NULL, + NGX_JS_STRING), + JS_CFUNC_DEF("return", 2, ngx_http_qjs_ext_return), + JS_CFUNC_DEF("send", 1, ngx_http_qjs_ext_send), + JS_CFUNC_DEF("sendBuffer", 1, ngx_http_qjs_ext_send_buffer), + JS_CFUNC_DEF("sendHeader", 0, ngx_http_qjs_ext_send_header), + JS_CFUNC_DEF("setReturnValue", 1, ngx_http_qjs_ext_set_return_value), + JS_CGETSET_DEF("status", ngx_http_qjs_ext_status_get, + ngx_http_qjs_ext_status_set), + 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("variables", ngx_http_qjs_ext_variables, + NULL, NGX_JS_STRING), + JS_CFUNC_MAGIC_DEF("warn", 1, ngx_http_qjs_ext_log, NGX_LOG_WARN), +}; + + +static const JSCFunctionListEntry ngx_http_qjs_ext_periodic[] = { + JS_CGETSET_MAGIC_DEF("rawVariables", ngx_http_qjs_ext_periodic_variables, + NULL, NGX_JS_BUFFER), + JS_CGETSET_MAGIC_DEF("variables", ngx_http_qjs_ext_periodic_variables, + NULL, NGX_JS_STRING), +}; + + +static JSClassDef ngx_http_qjs_request_class = { + "Request", + .finalizer = ngx_http_qjs_request_finalizer, +}; + + +static JSClassDef ngx_http_qjs_periodic_class = { + "PeriodicSession", + .finalizer = NULL, +}; + + +static JSClassDef ngx_http_qjs_variables_class = { + "Variables", + .finalizer = NULL, + .exotic = & (JSClassExoticMethods) { + .get_own_property = ngx_http_qjs_variables_own_property, + .set_property = ngx_http_qjs_variables_set_property, + }, +}; + + +static JSClassDef ngx_http_qjs_headers_in_class = { + "headersIn", + .finalizer = NULL, + .exotic = & (JSClassExoticMethods) { + .get_own_property = ngx_http_qjs_headers_in_own_property, + .get_own_property_names = ngx_http_qjs_headers_in_own_property_names, + }, +}; + + +static JSClassDef ngx_http_qjs_headers_out_class = { + "headersOut", + .finalizer = NULL, + .exotic = & (JSClassExoticMethods) { + .get_own_property = ngx_http_qjs_headers_out_own_property, + .get_own_property_names = ngx_http_qjs_headers_out_own_property_names, + .set_property = ngx_http_qjs_headers_out_set_property, + .define_own_property = ngx_http_qjs_headers_out_define_own_property, + .delete_property = ngx_http_qjs_headers_out_delete_property, + }, +}; + + +qjs_module_t *njs_http_qjs_addon_modules[] = { + &ngx_qjs_ngx_module, + /* + * Shared addons should be in the same order and the same positions + * in all nginx modules. + */ +#ifdef NJS_HAVE_ZLIB + &qjs_zlib_module, +#endif + NULL, +}; + + +#endif + + static ngx_int_t ngx_http_js_content_handler(ngx_http_request_t *r) { @@ -1426,6 +1651,14 @@ ngx_http_js_cleanup_ctx(void *data) ctx->engine); r = ngx_js_ctx_external(ctx); + + /* + * Restoring the original module context, because it can be reset + * by internalRedirect() method. Proper ctx is required for + * ngx_http_qjs_request_finalizer() to work correctly. + */ + ngx_http_set_ctx(r, ctx, ngx_http_js_module); + jlcf = ngx_http_get_module_loc_conf(r, ngx_http_js_module); ngx_js_ctx_destroy((ngx_js_ctx_t *) ctx, (ngx_js_loc_conf_t *) jlcf); @@ -4447,37 +4680,2887 @@ ngx_engine_njs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, } +#if (NJS_HAVE_QUICKJS) + static ngx_int_t -ngx_http_js_init_conf_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf) +ngx_http_qjs_query_string_decode(njs_chb_t *chain, const u_char *start, + size_t size) { - ngx_engine_opts_t options; - ngx_js_main_conf_t *jmcf; + u_char *dst; + uint32_t cp; + const u_char *p, *end; + njs_unicode_decode_t ctx; - memset(&options, 0, sizeof(ngx_engine_opts_t)); + static const int8_t hex[256] + njs_aligned(32) = + { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; - options.engine = conf->type; + njs_utf8_decode_init(&ctx); - if (conf->type == NGX_ENGINE_NJS) { - jmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_js_module); - ngx_http_js_uptr[NGX_JS_MAIN_CONF_INDEX] = (uintptr_t) jmcf; + cp = 0; - options.u.njs.metas = &ngx_http_js_metas; - options.u.njs.addons = njs_http_js_addon_modules; - options.clone = ngx_engine_njs_clone; + p = start; + end = p + size; + + while (p < end) { + if (*p == '%' && end - p > 2 && hex[p[1]] >= 0 && hex[p[2]] >= 0) { + cp = njs_utf8_consume(&ctx, (hex[p[1]] << 4) | hex[p[2]]); + p += 3; + + } else { + if (*p == '+') { + cp = ' '; + p++; + + } else { + cp = njs_utf8_decode(&ctx, &p, end); + } + } + + if (cp > NJS_UNICODE_MAX_CODEPOINT) { + if (cp == NJS_UNICODE_CONTINUE) { + continue; + } + + cp = NJS_UNICODE_REPLACEMENT; + } + + dst = njs_chb_reserve(chain, 4); + if (dst == NULL) { + return NGX_ERROR; + } + + njs_chb_written(chain, njs_utf8_encode(dst, cp) - dst); } - return ngx_js_init_conf_vm(cf, conf, &options); + if (cp == NJS_UNICODE_CONTINUE) { + dst = njs_chb_reserve(chain, 3); + if (dst == NULL) { + return NGX_ERROR; + } + + njs_chb_written(chain, + njs_utf8_encode(dst, NJS_UNICODE_REPLACEMENT) - dst); + } + + return NGX_OK; } -static ngx_int_t -ngx_http_js_init(ngx_conf_t *cf) +static JSValue +ngx_http_qjs_ext_args(JSContext *cx, JSValueConst this_val) { - ngx_http_next_header_filter = ngx_http_top_header_filter; - ngx_http_top_header_filter = ngx_http_js_header_filter; + u_char *start, *end, *p, *v; + uint32_t len; + JSAtom key; + JSValue args, val, prev, length, arr; + njs_str_t decoded; + njs_int_t ret; + ngx_int_t rc; + njs_chb_t chain; + ngx_http_request_t *r; + ngx_http_qjs_request_t *req; - ngx_http_next_body_filter = ngx_http_top_body_filter; - ngx_http_top_body_filter = ngx_http_js_body_filter; + req = JS_GetOpaque(this_val, ngx_http_qjs_request_class_id); + if (req == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + if (!JS_IsUndefined(req->args)) { + return JS_DupValue(cx, req->args); + } + + args = JS_NewObject(cx); + if (JS_IsException(args)) { + return JS_EXCEPTION; + } + + NJS_CHB_CTX_INIT(&chain, cx); + + r = req->request; + + rc = ngx_http_qjs_query_string_decode(&chain, r->args.data, r->args.len); + if (rc != NGX_OK) { + njs_chb_destroy(&chain); + return JS_ThrowOutOfMemory(cx); + } + + ret = njs_chb_join(&chain, &decoded); + njs_chb_destroy(&chain); + + if (ret != NJS_OK) { + return JS_ThrowOutOfMemory(cx); + } + + start = decoded.start; + end = start + decoded.length; + + while (start < end) { + p = ngx_strlchr(start, end, '&'); + if (p == NULL) { + p = end; + } + + v = ngx_strlchr(start, p, '='); + if (v == NULL) { + v = p; + } + + if (v == start) { + start = p + 1; + continue; + } + + key = JS_NewAtomLen(cx, (const char *) start, v - start); + if (key == JS_ATOM_NULL) { + chain.free(cx, decoded.start); + return JS_EXCEPTION; + } + + val = qjs_string_create(cx, v + 1, p - v - 1); + if (JS_IsException(val)) { + chain.free(cx, decoded.start); + JS_FreeAtom(cx, key); + return JS_EXCEPTION; + } + + prev = JS_GetProperty(cx, args, key); + if (JS_IsException(prev)) { + chain.free(cx, decoded.start); + JS_FreeAtom(cx, key); + JS_FreeValue(cx, val); + return JS_EXCEPTION; + } + + if (JS_IsUndefined(prev)) { + if (JS_SetProperty(cx, args, key, val) < 0) { + goto exception; + } + + } else if (JS_IsArray(cx, prev)) { + length = JS_GetPropertyStr(cx, prev, "length"); + + if (JS_ToUint32(cx, &len, length)) { + goto exception; + } + + JS_FreeValue(cx, length); + + if (JS_SetPropertyUint32(cx, prev, len, val) < 0) { + goto exception; + } + + JS_FreeValue(cx, prev); + + } else { + + arr = JS_NewArray(cx); + if (JS_IsException(arr)) { + goto exception; + } + + if (JS_SetPropertyUint32(cx, arr, 0, prev) < 0) { + goto exception; + } + + if (JS_SetPropertyUint32(cx, arr, 1, val) < 0) { + goto exception; + } + + if (JS_SetProperty(cx, args, key, arr) < 0) { + goto exception; + } + } + + JS_FreeAtom(cx, key); + start = p + 1; + } + + chain.free(cx, decoded.start); + req->args = args; + + return JS_DupValue(cx, args); + +exception: + + chain.free(cx, decoded.start); + JS_FreeAtom(cx, key); + JS_FreeValue(cx, val); + JS_FreeValue(cx, prev); + + return JS_EXCEPTION; +} + + +static JSValue +ngx_http_qjs_ext_done(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + ngx_http_js_ctx_t *ctx; + ngx_http_request_t *r; + + r = ngx_http_qjs_request(this_val); + if (r == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + + if (!ctx->filter) { + return JS_ThrowTypeError(cx, "cannot set done while not filtering"); + } + + ctx->done = 1; + + return JS_UNDEFINED; +} + + +static JSValue +ngx_http_qjs_ext_finish(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + ngx_http_js_ctx_t *ctx; + ngx_http_request_t *r; + + r = ngx_http_qjs_request(this_val); + if (r == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + if (ngx_http_send_special(r, NGX_HTTP_LAST) == NGX_ERROR) { + return JS_ThrowInternalError(cx, "failed to send response"); + } + + ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + + ctx->status = NGX_OK; + + return JS_UNDEFINED; +} + + +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_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + obj = JS_NewObjectProtoClass(cx, JS_NULL, ngx_http_qjs_headers_in_class_id); + + JS_SetOpaque(obj, r); + + return obj; +} + + +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_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + obj = JS_NewObjectProtoClass(cx, JS_NULL, + ngx_http_qjs_headers_out_class_id); + + JS_SetOpaque(obj, r); + + return obj; +} + + +static JSValue +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); + if (r == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + switch (r->http_version) { + case NGX_HTTP_VERSION_9: + ngx_str_set(&v, "0.9"); + break; + + case NGX_HTTP_VERSION_10: + ngx_str_set(&v, "1.0"); + break; + + case NGX_HTTP_VERSION_11: + ngx_str_set(&v, "1.1"); + break; + + case NGX_HTTP_VERSION_20: + ngx_str_set(&v, "2.0"); + break; + +#if (NGX_HTTP_VERSION_30) + case NGX_HTTP_VERSION_30: + ngx_str_set(&v, "3.0"); + break; +#endif + + default: + ngx_str_set(&v, ""); + break; + } + + return qjs_string_create(cx, v.data, v.len); +} + + +static JSValue +ngx_http_qjs_ext_internal(JSContext *cx, JSValueConst this_val) +{ + ngx_http_request_t *r; + + r = ngx_http_qjs_request(this_val); + if (r == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + return JS_NewBool(cx, r->internal); +} + + +static JSValue +ngx_http_qjs_ext_internal_redirect(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + ngx_http_js_ctx_t *ctx; + ngx_http_request_t *r; + + r = ngx_http_qjs_request(this_val); + if (r == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + if (r->parent != NULL) { + return JS_ThrowTypeError(cx, + "internalRedirect cannot be called from a subrequest"); + } + + ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + + if (ctx->filter) { + return JS_ThrowTypeError(cx, + "internalRedirect cannot be called while filtering"); + } + + if (ngx_qjs_string(ctx->engine, argv[0], &ctx->redirect_uri) != NGX_OK) { + return JS_EXCEPTION; + } + + ctx->status = NGX_DONE; + + return JS_UNDEFINED; +} + + +static JSValue +ngx_http_qjs_ext_log(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv, int level) +{ + int n; + const char *msg; + ngx_http_request_t *r; + + r = ngx_http_qjs_request(this_val); + if (r == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + for (n = 0; n < argc; n++) { + msg = JS_ToCString(cx, argv[n]); + + ngx_js_logger(r->connection, level, (u_char *) msg, ngx_strlen(msg)); + + JS_FreeCString(cx, msg); + } + + return JS_UNDEFINED; +} + + +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_http_qjs_periodic_class_id); + if (req == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a periodic object"); + } + + obj = JS_NewObjectProtoClass(cx, JS_NULL, ngx_http_qjs_variables_class_id); + + /* + * ngx_http_request_t is always aligned to 16 bytes, so we can use the + * lower 4 bits of the pointer to store the buffer type. + */ + JS_SetOpaque(obj, (void *) ((uintptr_t) req->request | (uintptr_t) type)); + + return obj; +} + + +static JSValue +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); + if (r == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + ctx = r->parent ? ngx_http_get_module_ctx(r->parent, ngx_http_js_module) + : NULL; + + if (ctx == NULL) { + return JS_UNDEFINED; + } + + return JS_DupValue(cx, ngx_qjs_arg(ctx->args[0])); +} + + +static JSValue +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); + if (r == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + c = r->connection; + + return qjs_string_create(cx, c->addr_text.data, c->addr_text.len); +} + + +static JSValue +ngx_http_qjs_ext_response_body(JSContext *cx, JSValueConst this_val, int type) +{ + u_char *p; + size_t len; + uint32_t buffer_type; + ngx_buf_t *b; + JSValue body; + ngx_http_request_t *r; + ngx_http_qjs_request_t *req; + + req = JS_GetOpaque(this_val, ngx_http_qjs_request_class_id); + if (req == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + buffer_type = ngx_js_buffer_type(type); + + if (!JS_IsUndefined(req->response_body)) { + if ((buffer_type == NGX_JS_STRING) == JS_IsString(req->response_body)) { + return JS_DupValue(cx, req->response_body); + } + } + + r = req->request; + + b = r->out ? r->out->buf : NULL; + + if (b == NULL) { + return JS_UNDEFINED; + } + + len = b->last - b->pos; + + p = ngx_pnalloc(r->pool, len); + if (p == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + if (len) { + ngx_memcpy(p, b->pos, len); + } + + body = ngx_qjs_prop(cx, buffer_type, p, len); + if (JS_IsException(body)) { + return JS_EXCEPTION; + } + + req->response_body = body; + + return JS_DupValue(cx, req->response_body); +} + + +static JSValue +ngx_http_qjs_ext_request_body(JSContext *cx, JSValueConst this_val, int type) +{ + u_char *p, *data; + size_t len; + JSValue body; + uint32_t buffer_type; + ngx_buf_t *buf; + ngx_chain_t *cl; + ngx_http_request_t *r; + ngx_http_qjs_request_t *req; + + req = JS_GetOpaque(this_val, ngx_http_qjs_request_class_id); + if (req == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + buffer_type = ngx_js_buffer_type(type); + + if (!JS_IsUndefined(req->request_body)) { + if ((buffer_type == NGX_JS_STRING) == JS_IsString(req->request_body)) { + return JS_DupValue(cx, req->request_body); + } + } + + r = req->request; + + if (r->request_body == NULL || r->request_body->bufs == NULL) { + return JS_UNDEFINED; + } + + if (r->request_body->temp_file) { + return JS_ThrowTypeError(cx, "request body is in a file"); + } + + cl = r->request_body->bufs; + buf = cl->buf; + + if (cl->next == NULL) { + len = buf->last - buf->pos; + data = buf->pos; + + goto done; + } + + len = buf->last - buf->pos; + cl = cl->next; + + for ( /* void */ ; cl; cl = cl->next) { + buf = cl->buf; + len += buf->last - buf->pos; + } + + p = ngx_pnalloc(r->pool, len); + if (p == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + data = p; + cl = r->request_body->bufs; + + for ( /* void */ ; cl; cl = cl->next) { + buf = cl->buf; + p = ngx_cpymem(p, buf->pos, buf->last - buf->pos); + } + +done: + + body = ngx_qjs_prop(cx, buffer_type, data, len); + if (JS_IsException(body)) { + return JS_EXCEPTION; + } + + req->request_body = body; + + return JS_DupValue(cx, req->request_body); +} + + +static JSValue +ngx_http_qjs_ext_return(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + ngx_str_t body; + ngx_int_t status; + ngx_http_js_ctx_t *ctx; + ngx_http_request_t *r; + ngx_http_complex_value_t cv; + + r = ngx_http_qjs_request(this_val); + if (r == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + if (ngx_qjs_integer(cx, argv[0], &status) != NGX_OK) { + return JS_EXCEPTION; + } + + if (status < 0 || status > 999) { + return JS_ThrowRangeError(cx, "code is out of range"); + } + + ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + + if (ngx_qjs_string(ctx->engine, argv[1], &body) != NGX_OK) { + return JS_ThrowOutOfMemory(cx); + } + + if (status < NGX_HTTP_BAD_REQUEST || body.len) { + ngx_memzero(&cv, sizeof(ngx_http_complex_value_t)); + + cv.value.data = body.data; + cv.value.len = body.len; + + ctx->status = ngx_http_send_response(r, status, NULL, &cv); + + if (ctx->status == NGX_ERROR) { + return JS_ThrowTypeError(cx, "failed to send response"); + } + + } else { + ctx->status = status; + } + + return JS_UNDEFINED; +} + + +static JSValue +ngx_http_qjs_ext_status_get(JSContext *cx, JSValueConst this_val) +{ + ngx_http_request_t *r; + + r = ngx_http_qjs_request(this_val); + if (r == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + return JS_NewInt32(cx, r->headers_out.status); +} + + +static JSValue +ngx_http_qjs_ext_status_set(JSContext *cx, JSValueConst this_val, + JSValueConst value) +{ + ngx_int_t n; + ngx_http_request_t *r; + + r = ngx_http_qjs_request(this_val); + if (r == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + if (ngx_qjs_integer(cx, value, &n) != NGX_OK) { + return JS_EXCEPTION; + } + + r->headers_out.status = n; + r->headers_out.status_line.len = 0; + + return JS_UNDEFINED; +} + + +static JSValue +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); + if (r == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + field = (ngx_str_t *) ((u_char *) r + offset); + + return qjs_string_create(cx, field->data, field->len); +} + + +static JSValue +ngx_http_qjs_ext_send(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + ngx_str_t s; + ngx_buf_t *b; + ngx_uint_t n; + ngx_chain_t *out, *cl, **ll; + ngx_http_js_ctx_t *ctx; + ngx_http_request_t *r; + + r = ngx_http_qjs_request(this_val); + if (r == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + + if (ctx->filter) { + return JS_ThrowTypeError(cx, "cannot send while in body filter"); + } + + out = NULL; + ll = &out; + + for (n = 0; n < (ngx_uint_t) argc; n++) { + if (ngx_qjs_string(ctx->engine, argv[n], &s) != NGX_OK) { + return JS_ThrowTypeError(cx, "failed to convert arg"); + } + + if (s.len == 0) { + continue; + } + + b = ngx_calloc_buf(r->pool); + if (b == NULL) { + return JS_ThrowInternalError(cx, "failed to allocate buffer"); + } + + b->start = s.data; + b->pos = b->start; + b->end = s.data + s.len; + b->last = b->end; + b->memory = 1; + + cl = ngx_alloc_chain_link(r->pool); + if (cl == NULL) { + return JS_ThrowInternalError(cx, "failed to allocate chain link"); + } + + cl->buf = b; + + *ll = cl; + ll = &cl->next; + } + + *ll = NULL; + + if (ngx_http_output_filter(r, out) == NGX_ERROR) { + return JS_ThrowInternalError(cx, "failed to send response"); + } + + return JS_UNDEFINED; +} + + +static JSValue +ngx_http_qjs_ext_send_buffer(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + unsigned last_buf, flush; + JSValue flags, value; + ngx_str_t buffer; + ngx_buf_t *b; + ngx_chain_t *cl; + ngx_http_js_ctx_t *ctx; + ngx_http_request_t *r; + + r = ngx_http_qjs_request(this_val); + if (r == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + + if (!ctx->filter) { + return JS_ThrowTypeError(cx, "cannot send buffer while not filtering"); + } + + if (ngx_qjs_string(ctx->engine, argv[0], &buffer) != NGX_OK) { + return JS_ThrowTypeError(cx, "failed get buffer arg"); + } + + flush = ctx->buf->flush; + last_buf = ctx->buf->last_buf; + + flags = argv[1]; + + if (JS_IsObject(flags)) { + value = JS_GetPropertyStr(cx, flags, "flush"); + if (JS_IsException(value)) { + return JS_EXCEPTION; + } + + flush = JS_ToBool(cx, value); + JS_FreeValue(cx, value); + + value = JS_GetPropertyStr(cx, flags, "last"); + if (JS_IsException(value)) { + return JS_EXCEPTION; + } + + last_buf = JS_ToBool(cx, value); + JS_FreeValue(cx, value); + } + + cl = ngx_chain_get_free_buf(r->pool, &ctx->free); + if (cl == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + b = cl->buf; + + b->flush = flush; + b->last_buf = last_buf; + + b->memory = (buffer.len ? 1 : 0); + b->sync = (buffer.len ? 0 : 1); + b->tag = (ngx_buf_tag_t) &ngx_http_js_module; + + b->start = buffer.data; + b->end = buffer.data + buffer.len; + b->pos = b->start; + b->last = b->end; + + *ctx->last_out = cl; + ctx->last_out = &cl->next; + + return JS_UNDEFINED; +} + + +static JSValue +ngx_http_qjs_ext_send_header(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + ngx_http_request_t *r; + + r = ngx_http_qjs_request(this_val); + if (r == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + if (ngx_http_set_content_type(r) != NGX_OK) { + return JS_ThrowInternalError(cx, "failed to set content type"); + } + + if (ngx_http_send_header(r) == NGX_ERROR) { + return JS_ThrowInternalError(cx, "failed to send header"); + } + + return JS_UNDEFINED; +} + + +static JSValue +ngx_http_qjs_ext_set_return_value(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + ngx_js_ctx_t *ctx; + ngx_http_request_t *r; + + r = ngx_http_qjs_request(this_val); + if (r == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + + JS_FreeValue(cx, ngx_qjs_arg(ctx->retval)); + ngx_qjs_arg(ctx->retval) = JS_DupValue(cx, argv[0]); + + return JS_UNDEFINED; +} + + +static ngx_int_t +ngx_http_qjs_subrequest_done(ngx_http_request_t *r, void *data, ngx_int_t rc) +{ + ngx_js_event_t *event = data; + + JSValue reply; + JSContext *cx; + ngx_http_js_ctx_t *ctx, *sctx; + + if (rc != NGX_OK || r->connection->error || r->buffered) { + return rc; + } + + sctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + + if (sctx && sctx->done) { + return NGX_OK; + } + + if (sctx == NULL) { + sctx = ngx_pcalloc(r->pool, sizeof(ngx_http_js_ctx_t)); + if (sctx == NULL) { + return NGX_ERROR; + } + + ngx_http_set_ctx(r, sctx, ngx_http_js_module); + + ngx_qjs_arg(sctx->response_body) = JS_UNDEFINED; + } + + sctx->done = 1; + + ctx = ngx_http_get_module_ctx(r->parent, ngx_http_js_module); + + 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) { + 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; + + if (!JS_IsObject(ngx_qjs_arg(sctx->args[0]))) { + reply = ngx_http_qjs_request_make(cx, ngx_http_qjs_request_class_id, r); + if (JS_IsException(reply)) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "js subrequest reply creation failed"); + return NGX_ERROR; + } + + + } else { + reply = JS_DupValue(cx, ngx_qjs_arg(sctx->args[0])); + } + + rc = ngx_qjs_call((ngx_js_ctx_t *) ctx, event->function, + &ngx_qjs_arg(reply), 1); + + JS_FreeValue(cx, reply); + ngx_js_del_event(ctx, event); + + ngx_http_js_event_finalize(r->parent, rc); + + return NGX_OK; +} + + +static void +ngx_http_js_subrequest_event_destructor(ngx_js_event_t *event) +{ + JSContext *cx; + + cx = event->ctx; + + JS_FreeValue(cx, ngx_qjs_arg(event->function)); + JS_FreeValue(cx, ngx_qjs_arg(event->args[0])); + JS_FreeValue(cx, ngx_qjs_arg(event->args[1])); +} + + +static JSValue +ngx_http_qjs_ext_subrequest(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + JSValue arg, options, callback, value, retval; + ngx_int_t rc; + ngx_str_t uri, args, method_name, body_arg; + ngx_uint_t method, methods_max, has_body, detached, flags, + promise; + ngx_js_event_t *event; + ngx_http_js_ctx_t *ctx; + ngx_http_request_t *r, *sr; + ngx_http_request_body_t *rb; + ngx_http_post_subrequest_t *ps; + + r = ngx_http_qjs_request(this_val); + if (r == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + + if (r->main != r) { + return JS_ThrowTypeError(cx, "subrequest can only be created for " + "the primary request"); + } + + if (ngx_qjs_string(ctx->engine, argv[0], &uri) != NGX_OK) { + return JS_ThrowTypeError(cx, "failed to convert uri arg"); + } + + if (uri.len == 0) { + return JS_ThrowTypeError(cx, "uri is empty"); + } + + options = JS_UNDEFINED; + callback = JS_UNDEFINED; + + method = 0; + methods_max = sizeof(ngx_http_methods) / sizeof(ngx_http_methods[0]); + + args.len = 0; + args.data = NULL; + + method_name.len = 0; + method_name.data = NULL; + + has_body = 0; + detached = 0; + + arg = argv[1]; + + if (JS_IsString(arg)) { + if (ngx_qjs_string(ctx->engine, arg, &args) != NGX_OK) { + return JS_ThrowTypeError(cx, "failed to convert args"); + } + + } else if (JS_IsFunction(cx, arg)) { + callback = arg; + + } else if (JS_IsObject(arg)) { + options = arg; + + } else if (!JS_IsNullOrUndefined(arg)) { + return JS_ThrowTypeError(cx, "failed to convert args"); + } + + if (!JS_IsUndefined(options)) { + value = JS_GetPropertyStr(cx, options, "args"); + if (JS_IsException(value)) { + return JS_EXCEPTION; + } + + if (!JS_IsUndefined(value)) { + rc = ngx_qjs_string(ctx->engine, value, &args); + JS_FreeValue(cx, value); + + if (rc != NGX_OK) { + return JS_ThrowTypeError(cx, "failed to convert options.args"); + } + } + + value = JS_GetPropertyStr(cx, options, "detached"); + if (JS_IsException(value)) { + return JS_EXCEPTION; + } + + if (!JS_IsUndefined(value)) { + detached = JS_ToBool(cx, value); + JS_FreeValue(cx, value); + } + + value = JS_GetPropertyStr(cx, options, "method"); + if (JS_IsException(value)) { + return JS_EXCEPTION; + } + + if (!JS_IsUndefined(value)) { + rc = ngx_qjs_string(ctx->engine, value, &method_name); + JS_FreeValue(cx, value); + + if (rc != NGX_OK) { + return JS_ThrowTypeError(cx, "failed to convert option.method"); + } + + while (method < methods_max) { + if (method_name.len == ngx_http_methods[method].name.len + && ngx_memcmp(method_name.data, + ngx_http_methods[method].name.data, + method_name.len) + == 0) + { + break; + } + + method++; + } + } + + value = JS_GetPropertyStr(cx, options, "body"); + if (JS_IsException(value)) { + return JS_EXCEPTION; + } + + if (!JS_IsUndefined(value)) { + rc = ngx_qjs_string(ctx->engine, value, &body_arg); + JS_FreeValue(cx, value); + + if (rc != NGX_OK) { + return JS_ThrowTypeError(cx, "failed to convert option.body"); + } + + has_body = 1; + } + } + + flags = NGX_HTTP_LOG_UNSAFE; + + if (ngx_http_parse_unsafe_uri(r, &uri, &args, &flags) != NGX_OK) { + return JS_ThrowTypeError(cx, "unsafe uri"); + } + + arg = argv[2]; + + if (JS_IsUndefined(callback) && !JS_IsNullOrUndefined(arg)) { + if (!JS_IsFunction(cx, arg)) { + return JS_ThrowTypeError(cx, "callback is not a function"); + } + + callback = arg; + } + + if (detached && !JS_IsUndefined(callback)) { + return JS_ThrowTypeError(cx, "detached flag and callback are mutually " + "exclusive"); + } + + promise = 0; + retval = JS_UNDEFINED; + flags = NGX_HTTP_SUBREQUEST_BACKGROUND; + + if (!detached) { + ps = ngx_palloc(r->pool, sizeof(ngx_http_post_subrequest_t)); + if (ps == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + promise = !!JS_IsUndefined(callback); + + event = ngx_pcalloc(r->pool, sizeof(ngx_js_event_t) + + sizeof(njs_opaque_value_t) * 2); + if (event == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + event->ctx = cx; + event->fd = ctx->event_id++; + event->args = (njs_opaque_value_t *) &event[1]; + event->destructor = ngx_http_js_subrequest_event_destructor; + + if (promise) { + retval = JS_NewPromiseCapability(cx, &ngx_qjs_arg(event->args[0])); + if (JS_IsException(retval)) { + return JS_EXCEPTION; + } + + callback = ngx_qjs_arg(event->args[0]); + + } else { + ngx_qjs_arg(event->args[0]) = JS_UNDEFINED; + ngx_qjs_arg(event->args[1]) = JS_UNDEFINED; + } + + JS_DupValue(cx, callback); + memcpy(&event->function, &callback, sizeof(njs_opaque_value_t)); + + ps->handler = ngx_http_qjs_subrequest_done; + ps->data = event; + + flags |= NGX_HTTP_SUBREQUEST_IN_MEMORY; + + } else { + ps = NULL; + event = NULL; + } + + if (ngx_http_subrequest(r, &uri, args.len ? &args : NULL, &sr, ps, flags) + != NGX_OK) + { + return JS_ThrowInternalError(cx, "subrequest creation failed"); + } + + if (event != NULL) { + ngx_js_add_event(ctx, event); + } + + if (method != methods_max) { + sr->method = ngx_http_methods[method].value; + sr->method_name = ngx_http_methods[method].name; + + } else { + sr->method = NGX_HTTP_UNKNOWN; + sr->method_name = method_name; + } + + sr->header_only = (sr->method == NGX_HTTP_HEAD) || JS_IsUndefined(callback); + + if (has_body) { + rb = ngx_pcalloc(r->pool, sizeof(ngx_http_request_body_t)); + if (rb == NULL) { + goto memory_error; + } + + if (body_arg.len != 0) { + rb->bufs = ngx_alloc_chain_link(r->pool); + if (rb->bufs == NULL) { + goto memory_error; + } + + rb->bufs->next = NULL; + + rb->bufs->buf = ngx_calloc_buf(r->pool); + if (rb->bufs->buf == NULL) { + goto memory_error; + } + + rb->bufs->buf->memory = 1; + rb->bufs->buf->last_buf = 1; + + rb->bufs->buf->pos = body_arg.data; + rb->bufs->buf->last = body_arg.data + body_arg.len; + } + + sr->request_body = rb; + sr->headers_in.content_length_n = body_arg.len; + sr->headers_in.chunked = 0; + } + + return retval; + +memory_error: + + return JS_ThrowOutOfMemory(cx); +} + + +static JSValue +ngx_http_qjs_ext_raw_headers(JSContext *cx, JSValueConst this_val, int out) +{ + JSValue array, elem, key, val; + uint32_t idx; + ngx_uint_t i; + ngx_list_t *headers; + ngx_list_part_t *part; + ngx_table_elt_t *header, *h; + ngx_http_request_t *r; + + r = ngx_http_qjs_request(this_val); + if (r == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + headers = (out) ? &r->headers_out.headers : &r->headers_in.headers; + + array = JS_NewArray(cx); + if (JS_IsException(array)) { + return JS_EXCEPTION; + } + + idx = 0; + part = &headers->part; + header = part->elts; + + for (i = 0; /* void */ ; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + header = part->elts; + i = 0; + } + + h = &header[i]; + + if (h->hash == 0) { + continue; + } + + elem = JS_NewArray(cx); + if (JS_IsException(elem)) { + JS_FreeValue(cx, array); + return JS_EXCEPTION; + } + + if (JS_DefinePropertyValueUint32(cx, array, idx++, elem, + JS_PROP_C_W_E) < 0) + { + JS_FreeValue(cx, elem); + JS_FreeValue(cx, array); + return JS_EXCEPTION; + } + + key = qjs_string_create(cx, h->key.data, h->key.len); + if (JS_IsException(key)) { + JS_FreeValue(cx, array); + return JS_EXCEPTION; + } + + if (JS_DefinePropertyValueUint32(cx, elem, 0, key, JS_PROP_C_W_E) < 0) { + JS_FreeValue(cx, key); + JS_FreeValue(cx, array); + return JS_EXCEPTION; + } + + val = qjs_string_create(cx, h->value.data, h->value.len); + if (JS_IsException(val)) { + JS_FreeValue(cx, array); + return JS_EXCEPTION; + } + + if (JS_DefinePropertyValueUint32(cx, elem, 1, val, JS_PROP_C_W_E) < 0) { + JS_FreeValue(cx, val); + JS_FreeValue(cx, array); + return JS_EXCEPTION; + } + } + + return array; +} + + +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_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + obj = JS_NewObjectProtoClass(cx, JS_NULL, ngx_http_qjs_variables_class_id); + + /* + * ngx_http_request_t is always aligned to 16 bytes, so we can use the + * lower 4 bits of the pointer to store the buffer type. + */ + JS_SetOpaque(obj, (void *) ((uintptr_t) r | (uintptr_t) type)); + + return obj; +} + + +static int +ngx_http_qjs_variables_own_property(JSContext *cx, JSPropertyDescriptor *pdesc, + JSValueConst obj, JSAtom prop) +{ + uint32_t buffer_type; + ngx_str_t name; + ngx_uint_t i, key, start, length, is_capture; + ngx_http_request_t *r; + ngx_http_variable_value_t *vv; + + r = JS_GetOpaque(obj, ngx_http_qjs_variables_class_id); + if (r == NULL) { + (void) JS_ThrowInternalError(cx, "\"this\" is not a request object"); + return -1; + } + + buffer_type = ngx_js_buffer_type((uintptr_t) r & 15); + r = (ngx_http_request_t *) ((uintptr_t) r & ~15Ul); + + name.data = (u_char *) JS_AtomToCString(cx, prop); + if (name.data == NULL) { + return -1; + } + + name.len = ngx_strlen(name.data); + + is_capture = 1; + for (i = 0; i < name.len; i++) { + if (name.data[i] < '0' || name.data[i] > '9') { + is_capture = 0; + break; + } + } + + if (is_capture) { + key = ngx_atoi(name.data, name.len) * 2; + JS_FreeCString(cx, (char *) name.data); + if (r->captures == NULL || r->captures_data == NULL + || r->ncaptures <= key) + { + return 0; + } + + + if (pdesc != NULL) { + pdesc->flags = JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE; + pdesc->getter = JS_UNDEFINED; + pdesc->setter = JS_UNDEFINED; + + start = r->captures[key]; + length = r->captures[key + 1] - start; + pdesc->value = ngx_qjs_prop(cx, buffer_type, + &r->captures_data[start], length); + } + + return 1; + } + + key = ngx_hash_strlow(name.data, name.data, name.len); + + vv = ngx_http_get_variable(r, &name, key); + JS_FreeCString(cx, (char *) name.data); + if (vv == NULL || vv->not_found) { + return 0; + } + + if (pdesc != NULL) { + pdesc->flags = JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE; + pdesc->getter = JS_UNDEFINED; + pdesc->setter = JS_UNDEFINED; + pdesc->value = ngx_qjs_prop(cx, buffer_type, vv->data, vv->len); + } + + return 1; +} + + +static int +ngx_http_qjs_variables_set_property(JSContext *cx, JSValueConst obj, + JSAtom prop, JSValueConst value, JSValueConst receiver, int flags) +{ + ngx_str_t name, s; + ngx_uint_t key; + ngx_http_js_ctx_t *ctx; + ngx_http_request_t *r; + ngx_http_variable_t *v; + ngx_http_variable_value_t *vv; + ngx_http_core_main_conf_t *cmcf; + + r = JS_GetOpaque(obj, ngx_http_qjs_variables_class_id); + if (r == NULL) { + (void) JS_ThrowInternalError(cx, "\"this\" is not a request object"); + return -1; + } + + r = (ngx_http_request_t *) ((uintptr_t) r & ~15Ul); + + name.data = (u_char *) JS_AtomToCString(cx, prop); + if (name.data == NULL) { + return -1; + } + + name.len = ngx_strlen(name.data); + + key = ngx_hash_strlow(name.data, name.data, name.len); + + cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module); + + v = ngx_hash_find(&cmcf->variables_hash, key, name.data, name.len); + JS_FreeCString(cx, (char *) name.data); + + if (v == NULL) { + (void) JS_ThrowInternalError(cx, "variable not found"); + return -1; + } + + ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + + if (ngx_qjs_string(ctx->engine, value, &s) != NGX_OK) { + return -1; + } + + if (v->set_handler != NULL) { + vv = ngx_pcalloc(r->pool, sizeof(ngx_http_variable_value_t)); + if (vv == NULL) { + (void) JS_ThrowOutOfMemory(cx); + return -1; + } + + vv->valid = 1; + vv->not_found = 0; + vv->data = s.data; + vv->len = s.len; + + v->set_handler(r, vv, v->data); + + return 1; + } + + if (!(v->flags & NGX_HTTP_VAR_INDEXED)) { + (void) JS_ThrowTypeError(cx, "variable is not writable"); + return -1; + } + + vv = &r->variables[v->index]; + + vv->valid = 1; + vv->not_found = 0; + + vv->data = ngx_pnalloc(r->pool, s.len); + if (vv->data == NULL) { + vv->valid = 0; + (void) JS_ThrowOutOfMemory(cx); + return -1; + } + + vv->len = s.len; + ngx_memcpy(vv->data, s.data, vv->len); + + return 1; +} + + +static int +ngx_http_qjs_ext_keys_header(JSContext *cx, ngx_list_t *headers, JSValue keys, + JSPropertyEnum **ptab, uint32_t *plen) +{ + JSAtom key; + ngx_uint_t item; + ngx_list_part_t *part; + ngx_table_elt_t *header, *h; + + part = &headers->part; + item = 0; + + while (part) { + if (item >= part->nelts) { + part = part->next; + item = 0; + continue; + } + + header = part->elts; + h = &header[item++]; + + if (h->hash == 0) { + continue; + } + + key = JS_NewAtomLen(cx, (const char *) h->key.data, h->key.len); + if (key == JS_ATOM_NULL) { + return -1; + } + + if (JS_DefinePropertyValue(cx, keys, key, JS_UNDEFINED, + JS_PROP_ENUMERABLE) < 0) + { + JS_FreeAtom(cx, key); + return -1; + } + + JS_FreeAtom(cx, key); + } + + return JS_GetOwnPropertyNames(cx, ptab, plen, keys, JS_GPN_STRING_MASK); +} + + +static int +ngx_http_qjs_headers_in_own_property_names(JSContext *cx, + JSPropertyEnum **ptab, uint32_t *plen, JSValueConst obj) +{ + int ret; + JSValue keys; + ngx_http_request_t *r; + + r = JS_GetOpaque(obj, ngx_http_qjs_headers_in_class_id); + if (r == NULL) { + (void) JS_ThrowInternalError(cx, "\"this\" is not a headers_in object"); + return -1; + } + + keys = JS_NewObject(cx); + if (JS_IsException(keys)) { + return -1; + } + + ret = ngx_http_qjs_ext_keys_header(cx, &r->headers_in.headers, keys, ptab, + plen); + JS_FreeValue(cx, keys); + + return ret; +} + + +static njs_int_t +ngx_http_qjs_header_generic(JSContext *cx, ngx_http_request_t *r, + ngx_list_t *headers, ngx_table_elt_t **ph, ngx_str_t *name, + JSPropertyDescriptor *pdesc, unsigned flags) +{ + int ret; + u_char sep; + njs_chb_t chain; + JSValue val; + ngx_uint_t i; + ngx_list_part_t *part; + ngx_table_elt_t *header, *h; + + if (ph == NULL) { + /* iterate over all headers */ + + ph = &header; + part = &headers->part; + h = part->elts; + + for (i = 0; /* void */ ; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + h = part->elts; + i = 0; + } + + if (h[i].hash == 0 + || name->len != h[i].key.len + || ngx_strncasecmp(name->data, h[i].key.data, name->len) + != 0) + { + continue; + } + + *ph = &h[i]; + ph = &h[i].next; + } + + *ph = NULL; + ph = &header; + } + + if (*ph == NULL) { + return 0; + } + + if (flags & NJS_HEADER_ARRAY) { + if (pdesc == NULL) { + return 1; + } + + pdesc->flags = JS_PROP_ENUMERABLE; + pdesc->getter = JS_UNDEFINED; + pdesc->setter = JS_UNDEFINED; + pdesc->value = JS_NewArray(cx); + if (JS_IsException(pdesc->value)) { + return -1; + } + + for (h = *ph, i = 0; h; h = h->next, i++) { + val = qjs_string_create(cx, h->value.data, h->value.len); + if (JS_IsException(val)) { + JS_FreeValue(cx, pdesc->value); + return -1; + } + + if (JS_DefinePropertyValueUint32(cx, pdesc->value, i, val, + JS_PROP_ENUMERABLE) < 0) + { + JS_FreeValue(cx, pdesc->value); + return -1; + } + } + + return 1; + } + + if ((*ph)->next == NULL || flags & NJS_HEADER_SINGLE) { + if (pdesc != NULL) { + pdesc->flags = JS_PROP_ENUMERABLE; + pdesc->getter = JS_UNDEFINED; + pdesc->setter = JS_UNDEFINED; + pdesc->value = qjs_string_create(cx, (*ph)->value.data, + (*ph)->value.len); + if (JS_IsException(pdesc->value)) { + return -1; + } + } + + return 1; + } + + NJS_CHB_CTX_INIT(&chain, cx); + + sep = flags & NJS_HEADER_SEMICOLON ? ';' : ','; + + for (h = *ph; h; h = h->next) { + njs_chb_append(&chain, h->value.data, h->value.len); + njs_chb_append(&chain, &sep, 1); + njs_chb_append_literal(&chain, " "); + } + + ret = 1; + + if (pdesc != NULL) { + pdesc->flags = JS_PROP_ENUMERABLE; + pdesc->getter = JS_UNDEFINED; + pdesc->setter = JS_UNDEFINED; + pdesc->value = qjs_string_create_chb(cx, &chain); + if (JS_IsException(pdesc->value)) { + ret = -1; + goto done; + } + } + +done: + + njs_chb_destroy(&chain); + + return ret; +} + + +static int +ngx_http_qjs_header_in(JSContext *cx, ngx_http_request_t *r, unsigned flags, + ngx_str_t *name, JSPropertyDescriptor *pdesc) +{ + u_char *lowcase_key; + ngx_uint_t hash; + ngx_table_elt_t **ph; + ngx_http_header_t *hh; + ngx_http_core_main_conf_t *cmcf; + + /* look up hashed headers */ + + lowcase_key = ngx_pnalloc(r->pool, name->len); + if (lowcase_key == NULL) { + (void) JS_ThrowOutOfMemory(cx); + return -1; + } + + hash = ngx_hash_strlow(lowcase_key, name->data, name->len); + + cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module); + + hh = ngx_hash_find(&cmcf->headers_in_hash, hash, lowcase_key, + name->len); + + ph = NULL; + + if (hh) { + if (hh->offset == offsetof(ngx_http_headers_in_t, cookie)) { + flags |= NJS_HEADER_SEMICOLON; + } + + ph = (ngx_table_elt_t **) ((char *) &r->headers_in + hh->offset); + } + + return ngx_http_qjs_header_generic(cx, r, &r->headers_in.headers, ph, name, + pdesc, flags); +} + + +static int +ngx_http_qjs_headers_in_own_property(JSContext *cx, JSPropertyDescriptor *pdesc, + JSValueConst obj, JSAtom prop) +{ + int ret; + unsigned flags; + ngx_str_t name, *h; + ngx_http_request_t *r; + + static ngx_str_t single_headers_in[] = { + ngx_string("Content-Type"), + ngx_string("ETag"), + ngx_string("From"), + ngx_string("Max-Forwards"), + ngx_string("Referer"), + ngx_string("Proxy-Authorization"), + ngx_string("User-Agent"), + ngx_string(""), + }; + + r = JS_GetOpaque(obj, ngx_http_qjs_headers_in_class_id); + if (r == NULL) { + (void) JS_ThrowInternalError(cx, "\"this\" is not a headers_in object"); + return -1; + } + + name.data = (u_char *) JS_AtomToCString(cx, prop); + if (name.data == NULL) { + return -1; + } + + name.len = ngx_strlen(name.data); + + flags = 0; + + for (h = single_headers_in; h->len > 0; h++) { + if (h->len == name.len + && ngx_strncasecmp(h->data, name.data, name.len) == 0) + { + flags |= NJS_HEADER_SINGLE; + break; + } + } + + ret = ngx_http_qjs_header_in(cx, r, flags, &name, pdesc); + JS_FreeCString(cx, (char *) name.data); + + return ret; +} + + +static int +ngx_http_qjs_headers_out_own_property_names(JSContext *cx, + JSPropertyEnum **ptab, uint32_t *plen, JSValueConst obj) +{ + int ret; + JSAtom key; + JSValue keys; + ngx_http_request_t *r; + + r = JS_GetOpaque(obj, ngx_http_qjs_headers_out_class_id); + if (r == NULL) { + (void) JS_ThrowInternalError(cx, "\"this\" is not a headers_out" + " object"); + return -1; + } + + keys = JS_NewObject(cx); + if (JS_IsException(keys)) { + return -1; + } + + if (r->headers_out.content_type.len) { + key = JS_NewAtomLen(cx, "Content-Type", njs_length("Content-Type")); + if (key == JS_ATOM_NULL) { + return -1; + } + + if (JS_DefinePropertyValue(cx, keys, key, JS_UNDEFINED, + JS_PROP_ENUMERABLE) < 0) + { + JS_FreeAtom(cx, key); + return -1; + } + + JS_FreeAtom(cx, key); + } + + if (r->headers_out.content_length == NULL + && r->headers_out.content_length_n >= 0) + { + key = JS_NewAtomLen(cx, "Content-Length", njs_length("Content-Length")); + if (key == JS_ATOM_NULL) { + return -1; + } + + if (JS_DefinePropertyValue(cx, keys, key, JS_UNDEFINED, + JS_PROP_ENUMERABLE) < 0) + { + JS_FreeAtom(cx, key); + return -1; + } + + JS_FreeAtom(cx, key); + } + + ret = ngx_http_qjs_ext_keys_header(cx, &r->headers_out.headers, keys, ptab, + plen); + JS_FreeValue(cx, keys); + + return ret; +} + + +static int +ngx_http_qjs_headers_out_handler(JSContext *cx, ngx_http_request_t *r, + ngx_str_t *name, JSPropertyDescriptor *pdesc, JSValue *value, + unsigned flags) +{ + u_char *p; + int64_t length; + uint32_t i; + ngx_int_t rc; + ngx_str_t s; + JSValue v; + ngx_list_part_t *part; + ngx_table_elt_t *header, *h, **ph; + ngx_http_js_ctx_t *ctx; + + if (flags & NJS_HEADER_GET) { + return ngx_http_qjs_header_generic(cx, r, &r->headers_out.headers, NULL, + name, pdesc, flags); + } + + part = &r->headers_out.headers.part; + header = part->elts; + + for (i = 0; /* void */ ; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + header = part->elts; + i = 0; + } + + h = &header[i]; + + if (h->hash == 0 + || h->key.len != name->len + || ngx_strncasecmp(h->key.data, name->data, name->len) != 0) + { + continue; + } + + h->hash = 0; + h->next = NULL; + } + + if (value == NULL) { + return 1; + } + + if (JS_IsArray(cx, *value)) { + v = JS_GetPropertyStr(cx, *value, "length"); + if (JS_IsException(v)) { + return -1; + } + + if (JS_ToInt64(cx, &length, v) < 0) { + JS_FreeValue(cx, v); + return -1; + } + + JS_FreeValue(cx, v); + + } else { + v = *value; + length = 1; + } + + ph = &header; + ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + + for (i = 0; i < (uint32_t) length; i++) { + if (JS_IsArray(cx, *value)) { + v = JS_GetPropertyUint32(cx, *value, i); + if (JS_IsException(v)) { + return -1; + } + } + + rc = ngx_qjs_string(ctx->engine, v, &s); + + if (JS_IsArray(cx, *value)) { + JS_FreeValue(cx, v); + } + + if (rc != NGX_OK) { + return -1; + } + + if (s.len == 0) { + continue; + } + + h = ngx_list_push(&r->headers_out.headers); + if (h == NULL) { + (void) JS_ThrowOutOfMemory(cx); + return -1; + } + + p = ngx_pnalloc(r->pool, name->len); + if (p == NULL) { + h->hash = 0; + (void) JS_ThrowOutOfMemory(cx); + return -1; + } + + ngx_memcpy(p, name->data, name->len); + + h->key.data = p; + h->key.len = name->len; + + p = ngx_pnalloc(r->pool, s.len); + if (p == NULL) { + h->hash = 0; + (void) JS_ThrowOutOfMemory(cx); + return -1; + } + + ngx_memcpy(p, s.data, s.len); + + h->value.data = p; + h->value.len = s.len; + h->hash = 1; + + *ph = h; + ph = &h->next; + } + + *ph = NULL; + + return NJS_OK; +} + + +static int +ngx_http_qjs_headers_out_special_handler(JSContext *cx, ngx_http_request_t *r, + ngx_str_t *name, JSPropertyDescriptor *pdesc, JSValue *value, + unsigned flags, ngx_table_elt_t **hh) +{ + u_char *p; + uint32_t length; + JSValue len, setval; + ngx_str_t s; + ngx_uint_t i, rc; + ngx_list_t *headers; + ngx_list_part_t *part; + ngx_table_elt_t *header, *h; + ngx_http_js_ctx_t *ctx; + + if (flags & NJS_HEADER_GET) { + return ngx_http_qjs_headers_out_handler(cx, r, name, pdesc, NULL, + flags | NJS_HEADER_SINGLE); + } + + if (value != NULL) { + if (JS_IsArray(cx, *value)) { + len = JS_GetPropertyStr(cx, *value, "length"); + if (JS_IsException(len)) { + return -1; + } + + if (JS_ToUint32(cx, &length, len) < 0) { + JS_FreeValue(cx, len); + return -1; + } + + JS_FreeValue(cx, len); + + setval = JS_GetPropertyUint32(cx, *value, length - 1); + if (JS_IsException(setval)) { + return -1; + } + + } else { + setval = *value; + } + + } else { + setval = JS_UNDEFINED; + } + + ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + + rc = ngx_qjs_string(ctx->engine, setval, &s); + + if (value != NULL && JS_IsArray(cx, *value)) { + JS_FreeValue(cx, setval); + } + + if (rc != NGX_OK) { + return -1; + } + + headers = &r->headers_out.headers; + part = &headers->part; + header = part->elts; + + for (i = 0; /* void */ ; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + header = part->elts; + i = 0; + } + + h = &header[i]; + + if (h->hash == 0) { + continue; + } + + if (h->key.len == name->len + && ngx_strncasecmp(h->key.data, name->data, name->len) == 0) + { + goto done; + } + } + + h = NULL; + +done: + + if (h != NULL && s.len == 0) { + h->hash = 0; + h = NULL; + } + + if (h == NULL && s.len != 0) { + h = ngx_list_push(headers); + if (h == NULL) { + (void) JS_ThrowOutOfMemory(cx); + return -1; + } + + p = ngx_pnalloc(r->pool, name->len); + if (p == NULL) { + h->hash = 0; + (void) JS_ThrowOutOfMemory(cx); + return -1; + } + + ngx_memcpy(p, name->data, name->len); + + h->key.data = p; + h->key.len = name->len; + } + + if (h != NULL) { + p = ngx_pnalloc(r->pool, s.len); + if (p == NULL) { + h->hash = 0; + (void) JS_ThrowOutOfMemory(cx); + return -1; + } + + ngx_memcpy(p, s.data, s.len); + + h->value.data = p; + h->value.len = s.len; + h->hash = 1; + } + + if (hh != NULL) { + *hh = h; + } + + return 1; +} + + +static int +ngx_http_qjs_headers_out_content_encoding(JSContext *cx, ngx_http_request_t *r, + ngx_str_t *name, JSPropertyDescriptor *pdesc, JSValue *value, + unsigned flags) +{ + int ret; + ngx_table_elt_t *h; + + ret = ngx_http_qjs_headers_out_special_handler(cx, r, name, pdesc, value, + flags, &h); + if (ret < 0) { + return -1; + } + + if (!(flags & NJS_HEADER_GET)) { + r->headers_out.content_encoding = h; + } + + return ret; +} + + +static int +ngx_http_qjs_headers_out_content_length(JSContext *cx, ngx_http_request_t *r, + ngx_str_t *name, JSPropertyDescriptor *pdesc, JSValue *value, + unsigned flags) +{ + int ret; + u_char *p; + ngx_int_t n; + ngx_table_elt_t *h; + u_char content_len[NGX_OFF_T_LEN]; + + if (flags & NJS_HEADER_GET) { + if (r->headers_out.content_length == NULL + && r->headers_out.content_length_n >= 0) + { + p = ngx_sprintf(content_len, "%O", r->headers_out.content_length_n); + + if (pdesc != NULL) { + pdesc->flags = JS_PROP_C_W_E; + pdesc->getter = JS_UNDEFINED; + pdesc->setter = JS_UNDEFINED; + pdesc->value = qjs_string_create(cx, content_len, + p - content_len); + if (JS_IsException(pdesc->value)) { + return -1; + } + } + + return 1; + } + } + + ret = ngx_http_qjs_headers_out_special_handler(cx, r, name, pdesc, value, + flags, &h); + if (ret < 0) { + return -1; + } + + if (!(flags & NJS_HEADER_GET)) { + if (h != NULL) { + n = ngx_atoi(h->value.data, h->value.len); + if (n == NGX_ERROR) { + h->hash = 0; + (void) JS_ThrowInternalError(cx, "failed converting argument " + "to positive integer"); + return -1; + } + + r->headers_out.content_length = h; + r->headers_out.content_length_n = n; + + } else { + ngx_http_clear_content_length(r); + } + } + + return ret; +} + + +static int +ngx_http_qjs_headers_out_content_type(JSContext *cx, ngx_http_request_t *r, + ngx_str_t *name, JSPropertyDescriptor *pdesc, JSValue *value, + unsigned flags) +{ + uint32_t length; + JSValue len, setval; + ngx_int_t rc; + ngx_str_t *hdr, s; + ngx_http_js_ctx_t *ctx; + + if (flags & NJS_HEADER_GET) { + hdr = &r->headers_out.content_type; + + if (pdesc != NULL) { + pdesc->flags = JS_PROP_C_W_E; + pdesc->getter = JS_UNDEFINED; + pdesc->setter = JS_UNDEFINED; + + if (hdr->len == 0) { + pdesc->value = JS_UNDEFINED; + return 1; + } + + pdesc->value = qjs_string_create(cx, hdr->data, hdr->len); + if (JS_IsException(pdesc->value)) { + return -1; + } + } + + return 1; + } + + if (value == NULL) { + r->headers_out.content_type.len = 0; + r->headers_out.content_type_len = 0; + r->headers_out.content_type.data = NULL; + r->headers_out.content_type_lowcase = NULL; + return 1; + } + + if (JS_IsArray(cx, *value)) { + len = JS_GetPropertyStr(cx, *value, "length"); + if (JS_IsException(len)) { + return -1; + } + + if (JS_ToUint32(cx, &length, len) < 0) { + JS_FreeValue(cx, len); + return -1; + } + + JS_FreeValue(cx, len); + + setval = JS_GetPropertyUint32(cx, *value, length - 1); + if (JS_IsException(setval)) { + return -1; + } + + } else { + setval = *value; + } + + ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + + rc = ngx_qjs_string(ctx->engine, setval, &s); + + if (JS_IsArray(cx, *value)) { + JS_FreeValue(cx, setval); + } + + if (rc != NGX_OK) { + return -1; + } + + r->headers_out.content_type.len = s.len; + r->headers_out.content_type_len = r->headers_out.content_type.len; + r->headers_out.content_type.data = s.data; + r->headers_out.content_type_lowcase = NULL; + + return 1; +} + + +static int +ngx_http_qjs_headers_out_date(JSContext *cx, ngx_http_request_t *r, + ngx_str_t *name, JSPropertyDescriptor *pdesc, JSValue *value, + unsigned flags) +{ + int ret; + ngx_table_elt_t *h; + + ret = ngx_http_qjs_headers_out_special_handler(cx, r, name, pdesc, value, + flags, &h); + if (ret < 0) { + return -1; + } + + if (!(flags & NJS_HEADER_GET)) { + r->headers_out.date = h; + } + + return ret; +} + + +static int +ngx_http_qjs_headers_out_last_modified(JSContext *cx, ngx_http_request_t *r, + ngx_str_t *name, JSPropertyDescriptor *pdesc, JSValue *value, + unsigned flags) +{ + int ret; + ngx_table_elt_t *h; + + ret = ngx_http_qjs_headers_out_special_handler(cx, r, name, pdesc, value, + flags, &h); + if (ret < 0) { + return -1; + } + + if (!(flags & NJS_HEADER_GET)) { + r->headers_out.last_modified = h; + } + + return ret; +} + + +static int +ngx_http_qjs_headers_out_location(JSContext *cx, ngx_http_request_t *r, + ngx_str_t *name, JSPropertyDescriptor *pdesc, JSValue *value, + unsigned flags) +{ + int ret; + ngx_table_elt_t *h; + + ret = ngx_http_qjs_headers_out_special_handler(cx, r, name, pdesc, value, + flags, &h); + if (ret < 0) { + return -1; + } + + if (!(flags & NJS_HEADER_GET)) { + r->headers_out.location = h; + } + + return ret; +} + + +static int +ngx_http_qjs_headers_out_server(JSContext *cx, ngx_http_request_t *r, + ngx_str_t *name, JSPropertyDescriptor *pdesc, JSValue *value, + unsigned flags) +{ + int ret; + ngx_table_elt_t *h; + + ret = ngx_http_qjs_headers_out_special_handler(cx, r, name, pdesc, value, + flags, &h); + if (ret < 0) { + return -1; + } + + if (!(flags & NJS_HEADER_GET)) { + r->headers_out.server = h; + } + + return ret; +} + + +static int +ngx_http_qjs_headers_out(JSContext *cx, ngx_http_request_t *r, + ngx_str_t *name, JSPropertyDescriptor *pdesc, JSValue *value, + unsigned flags) +{ + ngx_http_js_header_t *h; + + static ngx_http_js_header_t headers_out[] = { +#define header(name, fl, h) { njs_str(name), fl, (uintptr_t) h } + header("Age", NJS_HEADER_SINGLE, ngx_http_qjs_headers_out_handler), + header("Content-Encoding", 0, ngx_http_qjs_headers_out_content_encoding), + header("Content-Length", 0, ngx_http_qjs_headers_out_content_length), + header("Content-Type", 0, ngx_http_qjs_headers_out_content_type), + header("Date", 0, ngx_http_qjs_headers_out_date), + header("Etag", NJS_HEADER_SINGLE, ngx_http_qjs_headers_out_handler), + header("Expires", NJS_HEADER_SINGLE, ngx_http_qjs_headers_out_handler), + header("Last-Modified", 0, ngx_http_qjs_headers_out_last_modified), + header("Location", 0, ngx_http_qjs_headers_out_location), + header("Server", 0, ngx_http_qjs_headers_out_server), + header("Set-Cookie", NJS_HEADER_ARRAY, + ngx_http_qjs_headers_out_handler), + header("Retry-After", NJS_HEADER_SINGLE, + ngx_http_qjs_headers_out_handler), + header("", 0, ngx_http_qjs_headers_out_handler), +#undef header + }; + + for (h = headers_out; h->name.len > 0; h++) { + if (h->name.len == name->len + && ngx_strncasecmp(h->name.data, name->data, name->len) == 0) + { + break; + } + } + + return ((njs_http_qjs_header_handler_t) h->handler)(cx, + r, name, pdesc, value, h->flags | flags); +} + + +static int +ngx_http_qjs_headers_out_own_property(JSContext *cx, + JSPropertyDescriptor *pdesc, JSValueConst obj, JSAtom prop) +{ + int ret; + ngx_str_t name; + ngx_http_request_t *r; + + r = JS_GetOpaque(obj, ngx_http_qjs_headers_out_class_id); + if (r == NULL) { + (void) JS_ThrowInternalError(cx, "\"this\" is not a headers_out" + " object"); + return -1; + } + + name.data = (u_char *) JS_AtomToCString(cx, prop); + if (name.data == NULL) { + return -1; + } + + name.len = ngx_strlen(name.data); + + ret = ngx_http_qjs_headers_out(cx, r, &name, pdesc, NULL, NJS_HEADER_GET); + JS_FreeCString(cx, (char *) name.data); + + return ret; +} + + +static int +ngx_http_qjs_headers_out_set_property(JSContext *cx, + JSValueConst obj, JSAtom atom, JSValueConst value, JSValueConst receiver, + int flags) +{ + return ngx_http_qjs_headers_out_define_own_property(cx, obj, atom, value, + JS_UNDEFINED, JS_UNDEFINED, flags); +} + + +static int +ngx_http_qjs_headers_out_define_own_property(JSContext *cx, + JSValueConst obj, JSAtom prop, JSValueConst value, JSValueConst getter, + JSValueConst setter, int flags) +{ + int ret; + ngx_str_t name; + ngx_http_request_t *r; + + r = JS_GetOpaque(obj, ngx_http_qjs_headers_out_class_id); + if (r == NULL) { + (void) JS_ThrowInternalError(cx, "\"this\" is not a headers_out" + " object"); + return -1; + } + + if (!JS_IsUndefined(setter) || !JS_IsUndefined(getter)) { + (void) JS_ThrowTypeError(cx, "cannot define getter or setter"); + return -1; + } + + name.data = (u_char *) JS_AtomToCString(cx, prop); + if (name.data == NULL) { + return -1; + } + + name.len = ngx_strlen(name.data); + + if (r->header_sent) { + ngx_log_error(NGX_LOG_WARN, r->connection->log, 0, + "ignored setting of response header \"%V\" because" + " headers were already sent", &name); + } + + ret = ngx_http_qjs_headers_out(cx, r, &name, NULL, &value, 0); + JS_FreeCString(cx, (char *) name.data); + + return ret; +} + + +static int +ngx_http_qjs_headers_out_delete_property(JSContext *cx, + JSValueConst obj, JSAtom prop) +{ + int ret; + ngx_str_t name; + ngx_http_request_t *r; + + r = JS_GetOpaque(obj, ngx_http_qjs_headers_out_class_id); + if (r == NULL) { + (void) JS_ThrowInternalError(cx, "\"this\" is not a headers_out" + " object"); + return -1; + } + + name.data = (u_char *) JS_AtomToCString(cx, prop); + if (name.data == NULL) { + return -1; + } + + name.len = ngx_strlen(name.data); + + ret = ngx_http_qjs_headers_out(cx, r, &name, NULL, NULL, 0); + JS_FreeCString(cx, (char *) name.data); + + return ret; +} + + +static ngx_int_t +ngx_http_qjs_body_filter(ngx_http_request_t *r, ngx_http_js_loc_conf_t *jlcf, + ngx_http_js_ctx_t *ctx, ngx_chain_t *in) +{ + size_t len; + u_char *p; + JSAtom last_key; + JSValue arguments[3], last; + ngx_int_t rc; + njs_int_t pending; + ngx_buf_t *b; + ngx_chain_t *cl; + JSContext *cx; + ngx_connection_t *c; + + c = r->connection; + cx = ctx->engine->u.qjs.ctx; + + arguments[0] = ngx_qjs_arg(ctx->args[0]); + + last_key = JS_NewAtom(cx, "last"); + if (last_key == JS_ATOM_NULL) { + return NGX_ERROR; + } + + while (in != NULL) { + ctx->buf = in->buf; + b = ctx->buf; + + if (!ctx->done) { + len = b->last - b->pos; + + p = ngx_pnalloc(r->pool, len); + if (p == NULL) { + return NJS_ERROR; + } + + if (len) { + ngx_memcpy(p, b->pos, len); + } + + arguments[1] = ngx_qjs_prop(cx, jlcf->buffer_type, p, len); + if (JS_IsException(arguments[1])) { + JS_FreeAtom(cx, last_key); + return NGX_ERROR; + } + + last = JS_NewBool(cx, b->last_buf); + + arguments[2] = JS_NewObject(cx); + if (JS_IsException(arguments[2])) { + JS_FreeAtom(cx, last_key); + JS_FreeValue(cx, arguments[1]); + return NGX_ERROR; + } + + if (JS_SetProperty(cx, arguments[2], last_key, last) < 0) { + JS_FreeAtom(cx, last_key); + JS_FreeValue(cx, arguments[1]); + JS_FreeValue(cx, arguments[2]); + return NGX_ERROR; + } + + pending = ngx_js_ctx_pending(ctx); + + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0, + "http js body call \"%V\"", &jlcf->body_filter); + + rc = ctx->engine->call((ngx_js_ctx_t *) ctx, &jlcf->body_filter, + (njs_opaque_value_t *) &arguments[0], 3); + + JS_FreeAtom(cx, last_key); + JS_FreeValue(cx, arguments[1]); + JS_FreeValue(cx, arguments[2]); + + if (rc == NGX_ERROR) { + return NGX_ERROR; + } + + if (!pending && rc == NGX_AGAIN) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "async operation inside \"%V\" body filter", + &jlcf->body_filter); + return NGX_ERROR; + } + + ctx->buf->pos = ctx->buf->last; + + } else { + cl = ngx_alloc_chain_link(c->pool); + if (cl == NULL) { + return NGX_ERROR; + } + + cl->buf = b; + + *ctx->last_out = cl; + ctx->last_out = &cl->next; + } + + in = in->next; + } + + return NGX_OK; +} + + +static ngx_http_request_t * +ngx_http_qjs_request(JSValueConst val) +{ + ngx_http_qjs_request_t *req; + + req = JS_GetOpaque(val, ngx_http_qjs_request_class_id); + if (req == NULL) { + return NULL; + } + + return req->request; +} + + +static JSValue +ngx_http_qjs_request_make(JSContext *cx, ngx_int_t proto_id, + ngx_http_request_t *r) +{ + int ret; + JSValue request; + ngx_http_qjs_request_t *req; + + request = JS_NewObjectClass(cx, proto_id); + if (JS_IsException(request)) { + return JS_EXCEPTION; + } + + ret = qjs_set_to_string_tag(cx, request, "Request"); + if (ret == -1) { + JS_FreeValue(cx, request); + return JS_EXCEPTION; + } + + req = js_malloc(cx, sizeof(ngx_http_qjs_request_t)); + if (req == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + req->request = r; + req->args = JS_UNDEFINED; + req->request_body = JS_UNDEFINED; + req->response_body = JS_UNDEFINED; + + JS_SetOpaque(request, req); + + return request; +} + + +static void +ngx_http_qjs_request_finalizer(JSRuntime *rt, JSValue val) +{ + ngx_http_qjs_request_t *req; + + req = JS_GetOpaque(val, ngx_http_qjs_request_class_id); + if (req == NULL) { + return; + } + + JS_FreeValueRT(rt, req->args); + JS_FreeValueRT(rt, req->request_body); + JS_FreeValueRT(rt, req->response_body); + + js_free_rt(rt, req); +} + + +static ngx_engine_t * +ngx_engine_qjs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, + njs_int_t proto_id, void *external) +{ + JSValue proto; + JSContext *cx; + ngx_engine_t *engine; + ngx_http_js_ctx_t *hctx; + + engine = ngx_qjs_clone(ctx, cf, external); + if (engine == NULL) { + return NULL; + } + + cx = engine->u.qjs.ctx; + + if (!JS_IsRegisteredClass(JS_GetRuntime(cx), + ngx_http_qjs_request_class_id)) + { + if (JS_NewClass(JS_GetRuntime(cx), ngx_http_qjs_request_class_id, + &ngx_http_qjs_request_class) < 0) + { + return NULL; + } + + proto = JS_NewObject(cx); + JS_SetPropertyFunctionList(cx, proto, ngx_http_qjs_ext_request, + njs_nitems(ngx_http_qjs_ext_request)); + + JS_SetClassProto(cx, ngx_http_qjs_request_class_id, proto); + + if (JS_NewClass(JS_GetRuntime(cx), ngx_http_qjs_periodic_class_id, + &ngx_http_qjs_periodic_class) < 0) + { + return NULL; + } + + proto = JS_NewObject(cx); + JS_SetPropertyFunctionList(cx, proto, ngx_http_qjs_ext_periodic, + njs_nitems(ngx_http_qjs_ext_periodic)); + + JS_SetClassProto(cx, ngx_http_qjs_periodic_class_id, proto); + + if (JS_NewClass(JS_GetRuntime(cx), ngx_http_qjs_variables_class_id, + &ngx_http_qjs_variables_class) < 0) + { + return NULL; + } + + if (JS_NewClass(JS_GetRuntime(cx), ngx_http_qjs_headers_in_class_id, + &ngx_http_qjs_headers_in_class) < 0) + { + return NULL; + } + + if (JS_NewClass(JS_GetRuntime(cx), ngx_http_qjs_headers_out_class_id, + &ngx_http_qjs_headers_out_class) < 0) + { + return NULL; + } + } + + hctx = (ngx_http_js_ctx_t *) ctx; + hctx->body_filter = ngx_http_qjs_body_filter; + + if (proto_id == ngx_http_js_request_proto_id) { + proto_id = ngx_http_qjs_request_class_id; + + } else if (proto_id == ngx_http_js_periodic_session_proto_id) { + proto_id = ngx_http_qjs_periodic_class_id; + } + + ngx_qjs_arg(hctx->args[0]) = ngx_http_qjs_request_make(cx, proto_id, + external); + if (JS_IsException(ngx_qjs_arg(hctx->args[0]))) { + return NULL; + } + + return engine; +} + +#endif + + +static ngx_int_t +ngx_http_js_init_conf_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf) +{ + ngx_engine_opts_t options; + ngx_js_main_conf_t *jmcf; + + memset(&options, 0, sizeof(ngx_engine_opts_t)); + + options.engine = conf->type; + + jmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_js_module); + ngx_http_js_uptr[NGX_JS_MAIN_CONF_INDEX] = (uintptr_t) jmcf; + + if (conf->type == NGX_ENGINE_NJS) { + options.u.njs.metas = &ngx_http_js_metas; + options.u.njs.addons = njs_http_js_addon_modules; + options.clone = ngx_engine_njs_clone; + } + +#if (NJS_HAVE_QUICKJS) + else if (conf->type == NGX_ENGINE_QJS) { + options.u.qjs.metas = ngx_http_js_uptr; + options.u.qjs.addons = njs_http_qjs_addon_modules; + options.clone = ngx_engine_qjs_clone; + } +#endif + + return ngx_js_init_conf_vm(cf, conf, &options); +} + + +static ngx_int_t +ngx_http_js_init(ngx_conf_t *cf) +{ + ngx_http_next_header_filter = ngx_http_top_header_filter; + ngx_http_top_header_filter = ngx_http_js_header_filter; + + ngx_http_next_body_filter = ngx_http_top_body_filter; + ngx_http_top_body_filter = ngx_http_js_body_filter; + +#if (NJS_HAVE_QUICKJS) + JS_NewClassID(&ngx_http_qjs_request_class_id); + JS_NewClassID(&ngx_http_qjs_periodic_class_id); + JS_NewClassID(&ngx_http_qjs_variables_class_id); + JS_NewClassID(&ngx_http_qjs_headers_in_class_id); + JS_NewClassID(&ngx_http_qjs_headers_out_class_id); +#endif return NGX_OK; } diff --git a/nginx/ngx_js.c b/nginx/ngx_js.c index ce4988f97..9097efea3 100644 --- a/nginx/ngx_js.c +++ b/nginx/ngx_js.c @@ -8,6 +8,7 @@ #include #include +#include #include "ngx_js.h" @@ -57,6 +58,53 @@ static ngx_int_t ngx_engine_njs_string(ngx_engine_t *e, static void ngx_engine_njs_destroy(ngx_engine_t *e, ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *conf); +#if (NJS_HAVE_QUICKJS) +static ngx_int_t ngx_engine_qjs_init(ngx_engine_t *engine, + ngx_engine_opts_t *opts); +static ngx_int_t ngx_engine_qjs_compile(ngx_js_loc_conf_t *conf, ngx_log_t *log, + u_char *start, size_t size); +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); +static void *ngx_engine_qjs_external(ngx_engine_t *engine); +static ngx_int_t ngx_engine_qjs_pending(ngx_engine_t *engine); +static ngx_int_t ngx_engine_qjs_string(ngx_engine_t *e, + njs_opaque_value_t *value, ngx_str_t *str); + +static JSValue ngx_qjs_ext_set_timeout(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv, int immediate); +static JSValue ngx_qjs_ext_clear_timeout(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv); + +static JSValue ngx_qjs_ext_build(JSContext *cx, JSValueConst this_val); +static JSValue ngx_qjs_ext_conf_file_path(JSContext *cx, JSValueConst this_val); +static JSValue ngx_qjs_ext_conf_prefix(JSContext *cx, JSValueConst this_val); +static JSValue ngx_qjs_ext_constant_integer(JSContext *cx, + JSValueConst this_val, int magic); +static JSValue ngx_qjs_ext_error_log_path(JSContext *cx, JSValueConst this_val); +static JSValue ngx_qjs_ext_log(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv, int level); +static JSValue ngx_qjs_ext_console_time(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv); +static JSValue ngx_qjs_ext_console_time_end(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_qjs_ext_prefix(JSContext *cx, JSValueConst this_val); +static JSValue ngx_qjs_ext_worker_id(JSContext *cx, JSValueConst this_val); + +static void ngx_qjs_console_finalizer(JSRuntime *rt, JSValue val); + +static JSModuleDef *ngx_qjs_module_loader(JSContext *ctx, + const char *module_name, void *opaque); +static int ngx_qjs_unhandled_rejection(ngx_js_ctx_t *ctx); +static void ngx_qjs_rejection_tracker(JSContext *ctx, JSValueConst promise, + JSValueConst reason, JS_BOOL is_handled, void *opaque); + +static JSValue ngx_qjs_value(JSContext *cx, const ngx_str_t *path); +static ngx_int_t ngx_qjs_dump_obj(ngx_engine_t *e, JSValueConst val, + ngx_str_t *dst); + +static JSModuleDef *ngx_qjs_core_init(JSContext *cx, const char *name); +#endif + static njs_int_t ngx_js_ext_build(njs_vm_t *vm, njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval, njs_value_t *retval); static njs_int_t ngx_js_ext_conf_file_path(njs_vm_t *vm, @@ -377,6 +425,59 @@ njs_module_t *njs_js_addon_modules_shared[] = { static njs_int_t ngx_js_console_proto_id; +#if (NJS_HAVE_QUICKJS) + +static JSClassID ngx_qjs_console_class_id; + +static const JSCFunctionListEntry ngx_qjs_ext_ngx[] = { + JS_CGETSET_DEF("build", ngx_qjs_ext_build, NULL), + JS_CGETSET_DEF("conf_prefix", ngx_qjs_ext_conf_prefix, NULL), + JS_CGETSET_DEF("conf_file_path", ngx_qjs_ext_conf_file_path, NULL), + JS_CGETSET_MAGIC_DEF("ERR", ngx_qjs_ext_constant_integer, NULL, + NGX_LOG_ERR), + JS_CGETSET_DEF("error_log_path", ngx_qjs_ext_error_log_path, NULL), + JS_CGETSET_MAGIC_DEF("INFO", ngx_qjs_ext_constant_integer, NULL, + NGX_LOG_INFO), + JS_CFUNC_MAGIC_DEF("log", 1, ngx_qjs_ext_log, 0), + JS_CGETSET_DEF("prefix", ngx_qjs_ext_prefix, NULL), + JS_PROP_STRING_DEF("version", NGINX_VERSION, JS_PROP_C_W_E), + JS_PROP_INT32_DEF("version_number", nginx_version, JS_PROP_C_W_E), + JS_CGETSET_MAGIC_DEF("WARN", ngx_qjs_ext_constant_integer, NULL, + NGX_LOG_WARN), + JS_CGETSET_DEF("worker_id", ngx_qjs_ext_worker_id, NULL), +}; + + +static const JSCFunctionListEntry ngx_qjs_ext_console[] = { + JS_CFUNC_MAGIC_DEF("error", 1, ngx_qjs_ext_log, NGX_LOG_ERR), + JS_CFUNC_MAGIC_DEF("info", 1, ngx_qjs_ext_log, NGX_LOG_INFO), + JS_CFUNC_MAGIC_DEF("log", 1, ngx_qjs_ext_log, NGX_LOG_INFO), + JS_CFUNC_DEF("time", 1, ngx_qjs_ext_console_time), + JS_CFUNC_DEF("timeEnd", 1, ngx_qjs_ext_console_time_end), + JS_CFUNC_MAGIC_DEF("warn", 1, ngx_qjs_ext_log, NGX_LOG_WARN), +}; + + +static const JSCFunctionListEntry ngx_qjs_ext_global[] = { + JS_CFUNC_MAGIC_DEF("setTimeout", 1, ngx_qjs_ext_set_timeout, 0), + JS_CFUNC_MAGIC_DEF("setImmediate", 1, ngx_qjs_ext_set_timeout, 1), + JS_CFUNC_DEF("clearTimeout", 1, ngx_qjs_ext_clear_timeout), +}; + + +static JSClassDef ngx_qjs_console_class = { + "Console", + .finalizer = ngx_qjs_console_finalizer, +}; + + +qjs_module_t ngx_qjs_ngx_module = { + .name = "ngx", + .init = ngx_qjs_core_init, +}; + +#endif + static ngx_engine_t * ngx_create_engine(ngx_engine_opts_t *opts) { @@ -415,6 +516,25 @@ ngx_create_engine(ngx_engine_opts_t *opts) : ngx_engine_njs_destroy; break; +#if (NJS_HAVE_QUICKJS) + case NGX_ENGINE_QJS: + rc = ngx_engine_qjs_init(engine, opts); + if (rc != NGX_OK) { + return NULL; + } + + engine->name = "QuickJS"; + engine->type = NGX_ENGINE_QJS; + engine->compile = ngx_engine_qjs_compile; + engine->call = ngx_engine_qjs_call; + engine->external = ngx_engine_qjs_external; + engine->pending = ngx_engine_qjs_pending; + engine->string = ngx_engine_qjs_string; + engine->destroy = opts->destroy ? opts->destroy + : ngx_engine_qjs_destroy; + break; +#endif + default: return NULL; } @@ -503,202 +623,1552 @@ ngx_engine_njs_compile(ngx_js_loc_conf_t *conf, ngx_log_t *log, u_char *start, } } - ngx_log_error(NGX_LOG_EMERG, log, 0, "%*s", text.length, text.start); - return NGX_ERROR; + ngx_log_error(NGX_LOG_EMERG, log, 0, "%*s", text.length, text.start); + return NGX_ERROR; + } + + if (start != end) { + ngx_log_error(NGX_LOG_EMERG, log, 0, + "extra characters in js script: \"%*s\"", + end - start, start); + return NGX_ERROR; + } + + return NGX_OK; +} + + +ngx_engine_t * +ngx_njs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external) +{ + njs_vm_t *vm; + njs_int_t rc; + njs_str_t key; + ngx_str_t exception; + ngx_uint_t i; + ngx_engine_t *engine; + njs_opaque_value_t retval; + ngx_js_named_path_t *preload; + + vm = njs_vm_clone(cf->engine->u.njs.vm, external); + if (vm == NULL) { + return NULL; + } + + engine = njs_mp_alloc(njs_vm_memory_pool(vm), sizeof(ngx_engine_t)); + if (engine == NULL) { + return NULL; + } + + memcpy(engine, cf->engine, sizeof(ngx_engine_t)); + engine->pool = njs_vm_memory_pool(vm); + engine->u.njs.vm = vm; + + /* bind objects from preload vm */ + + if (cf->preload_objects != NGX_CONF_UNSET_PTR) { + preload = cf->preload_objects->elts; + + for (i = 0; i < cf->preload_objects->nelts; i++) { + key.start = preload[i].name.data; + key.length = preload[i].name.len; + + rc = njs_vm_value(cf->preload_vm, &key, njs_value_arg(&retval)); + if (rc != NJS_OK) { + return NULL; + } + + rc = njs_vm_bind(vm, &key, njs_value_arg(&retval), 0); + if (rc != NJS_OK) { + return NULL; + } + } + } + + if (njs_vm_start(vm, njs_value_arg(&retval)) == NJS_ERROR) { + ngx_js_exception(vm, &exception); + + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "js exception: %V", &exception); + + return NULL; + } + + return engine; +} + + +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; + ngx_str_t exception; + njs_function_t *func; + + name.start = fname->data; + name.length = fname->len; + + vm = ctx->engine->u.njs.vm; + + func = njs_vm_function(vm, &name); + if (func == NULL) { + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, + "js function \"%V\" not found", fname); + return NGX_ERROR; + } + + ret = njs_vm_invoke(vm, func, njs_value_arg(args), nargs, + njs_value_arg(&ctx->retval)); + if (ret == NJS_ERROR) { + ngx_js_exception(vm, &exception); + + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, + "js exception: %V", &exception); + + return NGX_ERROR; + } + + for ( ;; ) { + ret = njs_vm_execute_pending_job(vm); + if (ret <= NJS_OK) { + if (ret == NJS_ERROR) { + ngx_js_exception(vm, &exception); + + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, + "js job exception: %V", &exception); + return NGX_ERROR; + } + + break; + } + } + + if (ngx_js_unhandled_rejection(ctx)) { + ngx_js_exception(vm, &exception); + + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "js exception: %V", &exception); + return NGX_ERROR; + } + + return njs_rbtree_is_empty(&ctx->waiting_events) ? NGX_OK : NGX_AGAIN; +} + + +static void * +ngx_engine_njs_external(ngx_engine_t *engine) +{ + return njs_vm_external_ptr(engine->u.njs.vm); +} + +static ngx_int_t +ngx_engine_njs_pending(ngx_engine_t *e) +{ + return njs_vm_pending(e->u.njs.vm); +} + + +static ngx_int_t +ngx_engine_njs_string(ngx_engine_t *e, njs_opaque_value_t *value, + ngx_str_t *str) +{ + ngx_int_t rc; + njs_str_t s; + + rc = ngx_js_string(e->u.njs.vm, njs_value_arg(value), &s); + + str->data = s.start; + str->len = s.length; + + return rc; +} + + +static void +ngx_engine_njs_destroy(ngx_engine_t *e, ngx_js_ctx_t *ctx, + ngx_js_loc_conf_t *conf) +{ + ngx_js_event_t *event; + njs_rbtree_node_t *node; + + if (ctx != NULL) { + node = njs_rbtree_min(&ctx->waiting_events); + + while (njs_rbtree_is_there_successor(&ctx->waiting_events, node)) { + event = (ngx_js_event_t *) ((u_char *) node + - offsetof(ngx_js_event_t, node)); + + if (event->destructor != NULL) { + event->destructor(event); + } + + node = njs_rbtree_node_successor(&ctx->waiting_events, node); + } + } + + njs_vm_destroy(e->u.njs.vm); + + /* + * when ctx !=NULL e->pool is vm pool, in such case it is destroyed + * by njs_vm_destroy(). + */ + + if (ctx == NULL) { + njs_mp_destroy(e->pool); + } +} + + +#if (NJS_HAVE_QUICKJS) + +static ngx_int_t +ngx_engine_qjs_init(ngx_engine_t *engine, ngx_engine_opts_t *opts) +{ + JSRuntime *rt; + + rt = JS_NewRuntime(); + if (rt == NULL) { + return NGX_ERROR; + } + + engine->u.qjs.ctx = qjs_new_context(rt, opts->u.qjs.addons); + if (engine->u.qjs.ctx == NULL) { + return NGX_ERROR; + } + + JS_SetRuntimeOpaque(rt, opts->u.qjs.metas); + JS_SetContextOpaque(engine->u.qjs.ctx, opts->u.qjs.addons); + + JS_SetModuleLoaderFunc(rt, NULL, ngx_qjs_module_loader, opts->conf); + + return NGX_OK; +} + + +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; + JSContext *cx; + ngx_engine_t *engine; + ngx_js_code_entry_t *pc; + + engine = conf->engine; + cx = engine->u.qjs.ctx; + + code = JS_Eval(cx, (char *) start, size, "
", + JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); + + if (JS_IsException(code)) { + ngx_qjs_exception(engine, &text); + ngx_log_error(NGX_LOG_EMERG, log, 0, "js compile %V", &text); + return NGX_ERROR; + } + + pc = njs_arr_add(engine->precompiled); + if (pc == NULL) { + JS_FreeValue(cx, code); + ngx_log_error(NGX_LOG_EMERG, log, 0, "njs_arr_add() failed"); + return NGX_ERROR; + } + + pc->code = JS_WriteObject(cx, &pc->code_size, code, JS_WRITE_OBJ_BYTECODE); + if (pc->code == NULL) { + JS_FreeValue(cx, code); + ngx_log_error(NGX_LOG_EMERG, log, 0, "JS_WriteObject() failed"); + return NGX_ERROR; + } + + JS_FreeValue(cx, code); + + return NGX_OK; +} + + +static JSValue +js_std_await(JSContext *ctx, JSValue obj) +{ + int state, err; + JSValue ret; + JSContext *ctx1; + + for (;;) { + state = JS_PromiseState(ctx, obj); + if (state == JS_PROMISE_FULFILLED) { + ret = JS_PromiseResult(ctx, obj); + JS_FreeValue(ctx, obj); + break; + + } else if (state == JS_PROMISE_REJECTED) { + ret = JS_Throw(ctx, JS_PromiseResult(ctx, obj)); + JS_FreeValue(ctx, obj); + break; + + } else if (state == JS_PROMISE_PENDING) { + err = JS_ExecutePendingJob(JS_GetRuntime(ctx), &ctx1); + if (err < 0) { + /* js_std_dump_error(ctx1); */ + } + + } else { + /* not a promise */ + ret = obj; + break; + } + } + + return ret; +} + + +ngx_engine_t * +ngx_qjs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external) +{ + JSValue rv; + njs_mp_t *mp; + uint32_t i, length; + JSRuntime *rt; + ngx_str_t exception; + JSContext *cx; + ngx_engine_t *engine; + ngx_js_code_entry_t *pc; + + mp = njs_mp_fast_create(2 * getpagesize(), 128, 512, 16); + if (mp == NULL) { + return NULL; + } + + engine = njs_mp_alloc(mp, sizeof(ngx_engine_t)); + if (engine == NULL) { + return NULL; + } + + memcpy(engine, cf->engine, sizeof(ngx_engine_t)); + engine->pool = mp; + + if (cf->reuse_queue != NULL) { + engine->u.qjs.ctx = ngx_js_queue_pop(cf->reuse_queue); + if (engine->u.qjs.ctx != NULL) { + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, ctx->log, 0, + "js reused context: %p", engine->u.qjs.ctx); + JS_SetContextOpaque(engine->u.qjs.ctx, external); + return engine; + } + } + + rt = JS_NewRuntime(); + if (rt == NULL) { + return NULL; + } + + JS_SetRuntimeOpaque(rt, JS_GetRuntimeOpaque( + JS_GetRuntime(cf->engine->u.qjs.ctx))); + + cx = qjs_new_context(rt, JS_GetContextOpaque(cf->engine->u.qjs.ctx)); + if (cx == NULL) { + return NULL; + } + + engine->u.qjs.ctx = cx; + JS_SetContextOpaque(cx, external); + + JS_SetHostPromiseRejectionTracker(rt, ngx_qjs_rejection_tracker, ctx); + + + /* TODO: bind objects from preload vm */ + + rv = JS_UNDEFINED; + pc = engine->precompiled->start; + length = engine->precompiled->items; + + for (i = 0; i < length; i++) { + rv = JS_ReadObject(cx, pc[i].code, pc[i].code_size, + JS_READ_OBJ_BYTECODE); + if (JS_IsException(rv)) { + ngx_qjs_exception(engine, &exception); + + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, + "js load module exception: %V", &exception); + return NULL; + } + } + + if (JS_ResolveModule(cx, rv) < 0) { + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "js resolve module failed"); + return NULL; + } + + rv = JS_EvalFunction(cx, rv); + + if (JS_IsException(rv)) { + ngx_qjs_exception(engine, &exception); + + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "js eval exception: %V", + &exception); + return NULL; + } + + rv = js_std_await(cx, rv); + if (JS_IsException(rv)) { + ngx_qjs_exception(engine, &exception); + + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "js eval exception: %V", + &exception); + return NULL; + } + + JS_FreeValue(cx, rv); + + return engine; +} + + +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) +{ + int rc; + JSValue fn, val; + ngx_str_t exception; + JSRuntime *rt; + JSContext *cx, *cx1; + + cx = ctx->engine->u.qjs.ctx; + + fn = ngx_qjs_value(cx, fname); + if (!JS_IsFunction(cx, fn)) { + JS_FreeValue(cx, fn); + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "js function \"%V\" not found", + fname); + + return NGX_ERROR; + } + + val = JS_Call(cx, fn, JS_UNDEFINED, nargs, &ngx_qjs_arg(args[0])); + JS_FreeValue(cx, fn); + if (JS_IsException(val)) { + ngx_qjs_exception(ctx->engine, &exception); + + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, + "js call exception: %V", &exception); + + return NGX_ERROR; + } + + JS_FreeValue(cx, ngx_qjs_arg(ctx->retval)); + ngx_qjs_arg(ctx->retval) = val; + + rt = JS_GetRuntime(cx); + + for ( ;; ) { + rc = JS_ExecutePendingJob(rt, &cx1); + if (rc <= 0) { + if (rc == -1) { + ngx_qjs_exception(ctx->engine, &exception); + + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, + "js job exception: %V", &exception); + + return NGX_ERROR; + } + + break; + } + } + + if (ngx_qjs_unhandled_rejection(ctx)) { + ngx_qjs_exception(ctx->engine, &exception); + + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "js exception: %V", &exception); + return NGX_ERROR; + } + + return njs_rbtree_is_empty(&ctx->waiting_events) ? NGX_OK : NGX_AGAIN; +} + + +static void * +ngx_engine_qjs_external(ngx_engine_t *e) +{ + return JS_GetContextOpaque(e->u.qjs.ctx); +} + + +static ngx_int_t +ngx_engine_qjs_pending(ngx_engine_t *e) +{ + return JS_IsJobPending(JS_GetRuntime(e->u.qjs.ctx)); +} + + +static ngx_int_t +ngx_engine_qjs_string(ngx_engine_t *e, njs_opaque_value_t *value, + ngx_str_t *str) +{ + return ngx_qjs_dump_obj(e, ngx_qjs_arg(*value), str); +} + + +static void +ngx_js_cleanup_reuse_ctx(void *data) +{ + JSRuntime *rt; + JSContext *cx; + + ngx_js_queue_t *reuse = data; + + for ( ;; ) { + cx = ngx_js_queue_pop(reuse); + if (cx == NULL) { + break; + } + + rt = JS_GetRuntime(cx); + JS_FreeContext(cx); + JS_FreeRuntime(rt); + } +} + + +void +ngx_engine_qjs_destroy(ngx_engine_t *e, ngx_js_ctx_t *ctx, + ngx_js_loc_conf_t *conf) +{ + uint32_t i, length; + JSRuntime *rt; + JSContext *cx; + JSClassID class_id; + ngx_js_event_t *event; + ngx_js_opaque_t *opaque; + njs_rbtree_node_t *node; + ngx_pool_cleanup_t *cln; + ngx_js_code_entry_t *pc; + ngx_js_rejected_promise_t *rejected_promise; + + cx = e->u.qjs.ctx; + + if (ctx != NULL) { + node = njs_rbtree_min(&ctx->waiting_events); + + while (njs_rbtree_is_there_successor(&ctx->waiting_events, node)) { + event = (ngx_js_event_t *) ((u_char *) node + - offsetof(ngx_js_event_t, node)); + + if (event->destructor != NULL) { + event->destructor(event); + } + + node = njs_rbtree_node_successor(&ctx->waiting_events, node); + } + + if (ctx->rejected_promises != NULL) { + rejected_promise = ctx->rejected_promises->start; + + for (i = 0; i < ctx->rejected_promises->items; i++) { + JS_FreeValue(cx, ngx_qjs_arg(rejected_promise[i].promise)); + JS_FreeValue(cx, ngx_qjs_arg(rejected_promise[i].message)); + } + } + + class_id = JS_GetClassID(ngx_qjs_arg(ctx->args[0])); + opaque = JS_GetOpaque(ngx_qjs_arg(ctx->args[0]), class_id); + opaque->external = NULL; + + JS_FreeValue(cx, ngx_qjs_arg(ctx->args[0])); + JS_FreeValue(cx, ngx_qjs_arg(ctx->retval)); + + } else if (e->precompiled != NULL) { + pc = e->precompiled->start; + length = e->precompiled->items; + + for (i = 0; i < length; i++) { + js_free(cx, pc[i].code); + } + } + + njs_mp_destroy(e->pool); + + if (conf != NULL && conf->reuse != 0) { + if (conf->reuse_queue == NULL) { + conf->reuse_queue = ngx_js_queue_create(ngx_cycle->pool, + conf->reuse); + if (conf->reuse_queue == NULL) { + goto free_ctx; + } + + cln = ngx_pool_cleanup_add(ngx_cycle->pool, 0); + if (cln == NULL) { + goto free_ctx; + } + + cln->handler = ngx_js_cleanup_reuse_ctx; + cln->data = conf->reuse_queue; + } + + if (ngx_js_queue_push(conf->reuse_queue, cx) != NGX_OK) { + goto free_ctx; + } + + return; + } + +free_ctx: + + rt = JS_GetRuntime(cx); + JS_FreeContext(cx); + JS_FreeRuntime(rt); +} + + +static JSValue +ngx_qjs_value(JSContext *cx, const ngx_str_t *path) +{ + u_char *start, *p, *end; + JSAtom key; + size_t size; + JSValue value, rv; + + start = path->data; + end = start + path->len; + + value = JS_GetGlobalObject(cx); + + for ( ;; ) { + p = njs_strlchr(start, end, '.'); + + size = ((p != NULL) ? p : end) - start; + if (size == 0) { + JS_FreeValue(cx, value); + return JS_ThrowTypeError(cx, "empty path element"); + } + + key = JS_NewAtomLen(cx, (char *) start, size); + if (key == JS_ATOM_NULL) { + JS_FreeValue(cx, value); + return JS_ThrowInternalError(cx, "could not create atom"); + } + + rv = JS_GetProperty(cx, value, key); + JS_FreeAtom(cx, key); + if (JS_IsException(rv)) { + JS_FreeValue(cx, value); + return JS_EXCEPTION; + } + + JS_FreeValue(cx, value); + + if (p == NULL) { + break; + } + + start = p + 1; + value = rv; + } + + return rv; +} + + +static ngx_int_t +ngx_qjs_dump_obj(ngx_engine_t *e, JSValueConst val, ngx_str_t *dst) +{ + size_t len, byte_offset, byte_length; + u_char *start, *p; + JSValue buffer, stack; + ngx_str_t str, stack_str; + JSContext *cx; + + if (JS_IsNullOrUndefined(val)) { + dst->data = NULL; + dst->len = 0; + return NGX_OK; + } + + cx = e->u.qjs.ctx; + + buffer = JS_GetTypedArrayBuffer(cx, val, &byte_offset, &byte_length, NULL); + if (!JS_IsException(buffer)) { + start = JS_GetArrayBuffer(cx, &dst->len, buffer); + + JS_FreeValue(cx, buffer); + + if (start != NULL) { + start += byte_offset; + dst->len = byte_length; + + dst->data = njs_mp_alloc(e->pool, dst->len); + if (dst->data == NULL) { + return NGX_ERROR; + } + + memcpy(dst->data, start, dst->len); + return NGX_OK; + } + } + + str.data = (u_char *) JS_ToCString(cx, val); + if (str.data != NULL) { + str.len = ngx_strlen(str.data); + + stack = JS_GetPropertyStr(cx, val, "stack"); + + stack_str.len = 0; + stack_str.data = NULL; + + if (!JS_IsException(stack) && !JS_IsUndefined(stack)) { + stack_str.data = (u_char *) JS_ToCString(cx, stack); + if (stack_str.data != NULL) { + stack_str.len = ngx_strlen(stack_str.data); + } + } + + len = str.len; + + if (stack_str.len != 0) { + len += stack_str.len + njs_length("\n"); + } + + start = njs_mp_alloc(e->pool, len); + if (start == NULL) { + JS_FreeCString(cx, (char *) str.data); + JS_FreeValue(cx, stack); + return NGX_ERROR; + } + + p = ngx_cpymem(start, str.data, str.len); + + if (stack_str.len != 0) { + *p++ = '\n'; + (void) ngx_cpymem(p, stack_str.data, stack_str.len); + JS_FreeCString(cx, (char *) stack_str.data); + } + + JS_FreeCString(cx, (char *) str.data); + JS_FreeValue(cx, stack); + + } else { + len = njs_length("[exception]"); + + start = njs_mp_alloc(e->pool, len); + if (start == NULL) { + return NGX_ERROR; + } + + memcpy(start, "[exception]", len); + } + + dst->data = start; + dst->len = len; + + return NGX_OK; +} + + +ngx_int_t +ngx_qjs_call(ngx_js_ctx_t *ctx, njs_opaque_value_t fn, JSValue *argv, int argc) +{ + int rc; + JSValue ret; + ngx_str_t exception; + JSRuntime *rt; + JSContext *cx, *cx1; + + cx = ctx->engine->u.qjs.ctx; + + ret = JS_Call(cx, ngx_qjs_arg(fn), JS_UNDEFINED, argc, argv); + if (JS_IsException(ret)) { + ngx_qjs_exception(ctx->engine, &exception); + + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, + "js call exception: %V", &exception); + + return NGX_ERROR; + } + + JS_FreeValue(cx, ret); + + rt = JS_GetRuntime(cx); + + for ( ;; ) { + rc = JS_ExecutePendingJob(rt, &cx1); + if (rc <= 0) { + if (rc == -1) { + ngx_qjs_exception(ctx->engine, &exception); + + ngx_log_error(NGX_LOG_ERR, ctx->log, 0, + "js job exception: %V", &exception); + + return NGX_ERROR; + } + + break; + } + } + + return NGX_OK; +} + + +ngx_int_t +ngx_qjs_exception(ngx_engine_t *e, ngx_str_t *s) +{ + JSValue exception; + + exception = JS_GetException(e->u.qjs.ctx); + if (ngx_qjs_dump_obj(e, exception, s) != NGX_OK) { + return NGX_ERROR; + } + + JS_FreeValue(e->u.qjs.ctx, exception); + + return NGX_OK; +} + + +ngx_int_t +ngx_qjs_integer(JSContext *cx, JSValueConst val, ngx_int_t *n) +{ + double num; + + if (JS_ToFloat64(cx, &num, val)) { + return NGX_ERROR; + } + + if (isinf(num) || isnan(num)) { + (void) JS_ThrowTypeError(cx, "invalid number"); + return NGX_ERROR; + } + + *n = num; + + return NGX_OK; +} + + +ngx_int_t +ngx_qjs_string(ngx_engine_t *e, JSValueConst val, ngx_str_t *dst) +{ + size_t len, byte_offset, byte_length; + u_char *start; + JSValue buffer; + JSContext *cx; + const char *str; + + if (JS_IsNullOrUndefined(val)) { + dst->data = NULL; + dst->len = 0; + return NGX_OK; + } + + cx = e->u.qjs.ctx; + + buffer = JS_GetTypedArrayBuffer(cx, val, &byte_offset, &byte_length, NULL); + if (!JS_IsException(buffer)) { + start = JS_GetArrayBuffer(cx, &dst->len, buffer); + + JS_FreeValue(cx, buffer); + + if (start != NULL) { + start += byte_offset; + dst->len = byte_length; + + dst->data = njs_mp_alloc(e->pool, dst->len); + if (dst->data == NULL) { + return NGX_ERROR; + } + + memcpy(dst->data, start, dst->len); + return NGX_OK; + } + } + + str = JS_ToCString(cx, val); + if (str == NULL) { + return NGX_ERROR; + } + + len = strlen(str); + + start = njs_mp_alloc(e->pool, len); + if (start == NULL) { + JS_FreeCString(cx, str); + return NGX_ERROR; + } + + memcpy(start, str, len); + + JS_FreeCString(cx, str); + + dst->data = start; + dst->len = len; + + return NGX_OK; +} + + +static void +ngx_qjs_timer_handler(ngx_event_t *ev) +{ + void *external; + JSContext *cx; + ngx_int_t rc; + ngx_js_ctx_t *ctx; + ngx_js_event_t *event; + + event = (ngx_js_event_t *) ((u_char *) ev - offsetof(ngx_js_event_t, ev)); + + cx = event->ctx; + external = JS_GetContextOpaque(cx); + ctx = ngx_qjs_external_ctx(cx, external); + + rc = ngx_qjs_call((ngx_js_ctx_t *) ctx, event->function, + (JSValue *) event->args, event->nargs); + + ngx_js_del_event(ctx, event); + + ngx_qjs_external_event_finalize(cx)(external, rc); +} + + +static void +ngx_qjs_clear_timer(ngx_js_event_t *event) +{ + int i; + JSContext *cx; + + cx = event->ctx; + + if (event->ev.timer_set) { + ngx_del_timer(&event->ev); + } + + JS_FreeValue(cx, ngx_qjs_arg(event->function)); + + for (i = 0; i < (int) event->nargs; i++) { + JS_FreeValue(cx, ngx_qjs_arg(event->args[i])); + } +} + + +static JSValue +ngx_qjs_ext_set_timeout(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv, int immediate) +{ + int i, n; + void *external; + uint32_t delay; + ngx_js_ctx_t *ctx; + ngx_js_event_t *event; + ngx_connection_t *c; + + if (!JS_IsFunction(cx, argv[0])) { + return JS_ThrowTypeError(cx, "first arg must be a function"); + } + + delay = 0; + + if (!immediate && argc >= 2) { + if (JS_ToUint32(cx, &delay, argv[1]) < 0) { + return JS_EXCEPTION; + } + } + + n = immediate ? 1 : 2; + argc = (argc >= n) ? argc - n : 0; + external = JS_GetContextOpaque(cx); + ctx = ngx_qjs_external_ctx(cx, external); + + event = ngx_pcalloc(ngx_qjs_external_pool(cx, external), + sizeof(ngx_js_event_t) + + sizeof(njs_opaque_value_t) * argc); + if (event == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + event->ctx = cx; + + JS_DupValue(cx, argv[0]); + memcpy(&event->function, &argv[0], sizeof(njs_opaque_value_t)); + + event->nargs = argc; + event->args = (njs_opaque_value_t *) &event[1]; + event->destructor = ngx_qjs_clear_timer; + event->fd = ctx->event_id++; + + c = ngx_qjs_external_connection(cx, external); + + event->ev.log = c->log; + event->ev.data = event; + event->ev.handler = ngx_qjs_timer_handler; + + if (event->nargs != 0) { + for (i = 0; i < argc; i++) { + ngx_qjs_arg(event->args[i]) = JS_DupValue(cx, argv[n + i]); + } + } + + ngx_js_add_event(ctx, event); + + ngx_add_timer(&event->ev, delay); + + return JS_NewInt32(cx, event->fd); +} + + +static JSValue +ngx_qjs_ext_clear_timeout(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + uint32_t id; + ngx_js_ctx_t *ctx; + ngx_js_event_t event_lookup, *event; + njs_rbtree_node_t *rb; + + if (JS_ToUint32(cx, &id, argv[0]) < 0) { + return JS_EXCEPTION; + } + + ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx)); + event_lookup.fd = id; + + rb = njs_rbtree_find(&ctx->waiting_events, &event_lookup.node); + if (rb == NULL) { + return JS_ThrowReferenceError(cx, "failed to find timer"); + } + + event = (ngx_js_event_t *) ((u_char *) rb - offsetof(ngx_js_event_t, node)); + + ngx_js_del_event(ctx, event); + + return JS_UNDEFINED; +} + + +static JSValue +ngx_qjs_ext_build(JSContext *cx, JSValueConst this_val) +{ + return JS_NewStringLen(cx, +#ifdef NGX_BUILD + (char *) NGX_BUILD, + njs_strlen(NGX_BUILD) +#else + (char *) "", + 0 +#endif + ); +} + + +static JSValue +ngx_qjs_ext_conf_prefix(JSContext *cx, JSValueConst this_val) +{ + return JS_NewStringLen(cx, (char *) ngx_cycle->prefix.data, + ngx_cycle->prefix.len); +} + + +static JSValue +ngx_qjs_ext_conf_file_path(JSContext *cx, JSValueConst this_val) +{ + return JS_NewStringLen(cx, (char *) ngx_cycle->conf_file.data, + ngx_cycle->conf_file.len); +} + + +static JSValue +ngx_qjs_ext_constant_integer(JSContext *cx, JSValueConst this_val, int magic) +{ + return JS_NewInt32(cx, magic); +} + + +static JSValue +ngx_qjs_ext_error_log_path(JSContext *cx, JSValueConst this_val) +{ + return JS_NewStringLen(cx, (char *) ngx_cycle->error_log.data, + ngx_cycle->error_log.len); +} + + +static JSValue +ngx_qjs_ext_prefix(JSContext *cx, JSValueConst this_val) +{ + return JS_NewStringLen(cx, (char *) ngx_cycle->prefix.data, + ngx_cycle->prefix.len); +} + + +static JSValue +ngx_qjs_ext_worker_id(JSContext *cx, JSValueConst this_val) +{ + return JS_NewInt32(cx, ngx_worker); +} + + +static void +ngx_qjs_console_finalizer(JSRuntime *rt, JSValue val) +{ + ngx_js_console_t *console; + + console = JS_GetOpaque(val, ngx_qjs_console_class_id); + if (console == (void *) 1) { + return; + } + + js_free_rt(rt, console); +} + + +static JSValue +ngx_qjs_ext_log(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv, int magic) +{ + char *p; + uint32_t level; + ngx_str_t msg; + ngx_js_ctx_t *ctx; + ngx_connection_t *c; + + p = JS_GetContextOpaque(cx); + if (p == NULL) { + return JS_ThrowInternalError(cx, "external is not set"); + } + + level = magic & NGX_JS_LOG_MASK; + + if (level == 0) { + if (JS_ToUint32(cx, &level, argv[0]) < 0) { + return JS_EXCEPTION; + } + + argc--; + argv++; + } + + ctx = ngx_qjs_external_ctx(cx, p); + c = ngx_qjs_external_connection(cx, p); + + for ( ; argc > 0; argc--, argv++) { + if (ngx_qjs_dump_obj(ctx->engine, argv[0], &msg) != NGX_OK) { + return JS_EXCEPTION; + } + + ngx_js_logger(c, level, (u_char *) msg.data, msg.len); + } + + return JS_UNDEFINED; +} + + +static JSValue +ngx_qjs_ext_console_time(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + ngx_str_t name; + ngx_queue_t *labels, *q; + ngx_js_ctx_t *ctx; + ngx_js_console_t *console; + ngx_connection_t *c; + ngx_js_timelabel_t *label; + + static const ngx_str_t default_label = ngx_string("default"); + + console = JS_GetOpaque(this_val, ngx_qjs_console_class_id); + if (console == NULL) { + return JS_ThrowInternalError(cx, "this is not a console object"); + } + + if (console == (void *) 1) { + console = js_malloc(cx, sizeof(ngx_js_console_t)); + if (console == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + ngx_queue_init(&console->labels); + + JS_SetOpaque(this_val, console); + } + + if (!JS_IsUndefined(argv[0])) { + name.data = (u_char *) JS_ToCStringLen(cx, &name.len, argv[0]); + if (name.data == NULL) { + return JS_EXCEPTION; + } + + } else { + name = default_label; + } + + labels = &console->labels; + + for (q = ngx_queue_head(labels); + q != ngx_queue_sentinel(labels); + q = ngx_queue_next(q)) + { + label = ngx_queue_data(q, ngx_js_timelabel_t, queue); + + if (name.len == label->name.length + && ngx_strncmp(name.data, label->name.start, name.len) == 0) + { + c = ngx_qjs_external_connection(cx, JS_GetContextOpaque(cx)); + ngx_log_error(NGX_LOG_INFO, c->log, 0, "js: Timer \"%V\" already" + " exists", &name); + + goto done; + } + } + + ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx)); + + label = njs_mp_alloc(ctx->engine->pool, + sizeof(ngx_js_timelabel_t) + name.len); + if (label == NULL) { + if (name.data != default_label.data) { + JS_FreeCString(cx, (char *) name.data); + } + return JS_ThrowOutOfMemory(cx); + } + + label->name.length = name.len; + label->name.start = (u_char *) label + sizeof(ngx_js_timelabel_t); + memcpy(label->name.start, name.data, name.len); + + label->time = ngx_js_monotonic_time(); + + ngx_queue_insert_tail(&console->labels, &label->queue); + +done: + + if (name.data != default_label.data) { + JS_FreeCString(cx, (char *) name.data); + } + + return JS_UNDEFINED; +} + + +static JSValue +ngx_qjs_ext_console_time_end(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + uint64_t ns, ms; + ngx_str_t name; + ngx_queue_t *labels, *q; + ngx_js_console_t *console; + ngx_connection_t *c; + ngx_js_timelabel_t *label; + + static const ngx_str_t default_label = ngx_string("default"); + + ns = ngx_js_monotonic_time(); + + console = JS_GetOpaque(this_val, ngx_qjs_console_class_id); + if (console == NULL) { + return JS_ThrowInternalError(cx, "this is not a console object"); + } + + if (console == (void *) 1) { + goto not_found; + } + + if (!JS_IsUndefined(argv[0])) { + name.data = (u_char *) JS_ToCStringLen(cx, &name.len, argv[0]); + if (name.data == NULL) { + return JS_EXCEPTION; + } + + } else { + name = default_label; + } + + labels = &console->labels; + q = ngx_queue_head(labels); + + for ( ;; ) { + if (q == ngx_queue_sentinel(labels)) { + goto not_found; + } + + label = ngx_queue_data(q, ngx_js_timelabel_t, queue); + + if (name.len == label->name.length + && ngx_strncmp(name.data, label->name.start, name.len) == 0) + { + ngx_queue_remove(&label->queue); + break; + } + + q = ngx_queue_next(q); } - if (start != end) { - ngx_log_error(NGX_LOG_EMERG, log, 0, - "extra characters in js script: \"%*s\"", - end - start, start); - return NGX_ERROR; - } + ns = ns - label->time; - return NGX_OK; + ms = ns / 1000000; + ns = ns % 1000000; + + c = ngx_qjs_external_connection(cx, JS_GetContextOpaque(cx)); + ngx_log_error(NGX_LOG_INFO, c->log, 0, "js: %V: %uL.%06uLms", + &name, ms, ns); + + return JS_UNDEFINED; + +not_found: + + c = ngx_qjs_external_connection(cx, JS_GetContextOpaque(cx)); + ngx_log_error(NGX_LOG_INFO, c->log, 0, "js: Timer \"%V\" doesn't exist", + &name); + + return JS_UNDEFINED; } -ngx_engine_t * -ngx_njs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external) +static JSModuleDef * +ngx_qjs_module_loader(JSContext *cx, const char *module_name, void *opaque) { - njs_vm_t *vm; - njs_int_t rc; - njs_str_t key; - ngx_str_t exception; - ngx_uint_t i; - ngx_engine_t *engine; - njs_opaque_value_t retval; - ngx_js_named_path_t *preload; + JSValue func_val; + njs_int_t ret; + njs_str_t text; + JSModuleDef *m; + njs_module_info_t info; + ngx_js_loc_conf_t *conf; + ngx_js_code_entry_t *pc; - vm = njs_vm_clone(cf->engine->u.njs.vm, external); - if (vm == NULL) { - return NULL; - } + conf = opaque; - engine = njs_mp_alloc(njs_vm_memory_pool(vm), sizeof(ngx_engine_t)); - if (engine == NULL) { + njs_memzero(&info, sizeof(njs_module_info_t)); + + info.name.start = (u_char *) module_name; + info.name.length = njs_strlen(module_name); + + ret = ngx_js_module_lookup(conf, &info); + if (ret != NJS_OK) { + JS_ThrowReferenceError(cx, "could not load module filename '%s'", + module_name); return NULL; } - memcpy(engine, cf->engine, sizeof(ngx_engine_t)); - engine->pool = njs_vm_memory_pool(vm); - engine->u.njs.vm = vm; + ret = ngx_js_module_read(conf->engine->pool, info.fd, &text); - /* bind objects from preload vm */ + (void) close(info.fd); - if (cf->preload_objects != NGX_CONF_UNSET_PTR) { - preload = cf->preload_objects->elts; + if (ret != NJS_OK) { + JS_ThrowInternalError(cx, "while reading \"%*s\" module", + (int) info.file.length, info.file.start); + return NULL; + } - for (i = 0; i < cf->preload_objects->nelts; i++) { - key.start = preload[i].name.data; - key.length = preload[i].name.len; + func_val = JS_Eval(cx, (char *) text.start, text.length, module_name, + JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); - rc = njs_vm_value(cf->preload_vm, &key, njs_value_arg(&retval)); - if (rc != NJS_OK) { - return NULL; - } + njs_mp_free(conf->engine->pool, text.start); - rc = njs_vm_bind(vm, &key, njs_value_arg(&retval), 0); - if (rc != NJS_OK) { - return NULL; - } - } + if (JS_IsException(func_val)) { + return NULL; } - if (njs_vm_start(vm, njs_value_arg(&retval)) == NJS_ERROR) { - ngx_js_exception(vm, &exception); + if (conf->engine->precompiled == NULL) { + conf->engine->precompiled = njs_arr_create(conf->engine->pool, 4, + sizeof(ngx_js_code_entry_t)); + if (conf->engine->precompiled == NULL) { + JS_FreeValue(cx, func_val); + JS_ThrowOutOfMemory(cx); + return NULL; + } + } - ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "js exception: %V", &exception); + pc = njs_arr_add(conf->engine->precompiled); + if (pc == NULL) { + JS_FreeValue(cx, func_val); + JS_ThrowOutOfMemory(cx); + return NULL; + } + pc->code = JS_WriteObject(cx, &pc->code_size, func_val, + JS_WRITE_OBJ_BYTECODE); + if (pc->code == NULL) { + JS_FreeValue(cx, func_val); + JS_ThrowInternalError(cx, "could not write module bytecode"); return NULL; } - return engine; + m = JS_VALUE_GET_PTR(func_val); + JS_FreeValue(cx, func_val); + + return m; } -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) +static int +ngx_qjs_unhandled_rejection(ngx_js_ctx_t *ctx) { - njs_vm_t *vm; - njs_int_t ret; - njs_str_t name; - ngx_str_t exception; - njs_function_t *func; + size_t len; + uint32_t i; + JSContext *cx; + const char *str; + ngx_js_rejected_promise_t *rejected_promise; - name.start = fname->data; - name.length = fname->len; + if (ctx->rejected_promises == NULL + || ctx->rejected_promises->items == 0) + { + return 0; + } - vm = ctx->engine->u.njs.vm; + cx = ctx->engine->u.qjs.ctx; + rejected_promise = ctx->rejected_promises->start; - func = njs_vm_function(vm, &name); - if (func == NULL) { - ngx_log_error(NGX_LOG_ERR, ctx->log, 0, - "js function \"%V\" not found", fname); - return NGX_ERROR; + str = JS_ToCStringLen(cx, &len, ngx_qjs_arg(rejected_promise->message)); + if (njs_slow_path(str == NULL)) { + return -1; } - ret = njs_vm_invoke(vm, func, njs_value_arg(args), nargs, - njs_value_arg(&ctx->retval)); - if (ret == NJS_ERROR) { - ngx_js_exception(vm, &exception); - - ngx_log_error(NGX_LOG_ERR, ctx->log, 0, - "js exception: %V", &exception); + JS_ThrowTypeError(cx, "unhandled promise rejection: %*s", (int) len, str); + JS_FreeCString(cx, str); - return NGX_ERROR; + for (i = 0; i < ctx->rejected_promises->items; i++) { + JS_FreeValue(cx, ngx_qjs_arg(rejected_promise[i].promise)); + JS_FreeValue(cx, ngx_qjs_arg(rejected_promise[i].message)); } - for ( ;; ) { - ret = njs_vm_execute_pending_job(vm); - if (ret <= NJS_OK) { - if (ret == NJS_ERROR) { - ngx_js_exception(vm, &exception); + njs_arr_destroy(ctx->rejected_promises); + ctx->rejected_promises = NULL; - ngx_log_error(NGX_LOG_ERR, ctx->log, 0, - "js job exception: %V", &exception); - return NGX_ERROR; - } + return 1; +} - break; + +static void +ngx_qjs_rejection_tracker(JSContext *cx, JSValueConst promise, + JSValueConst reason, JS_BOOL is_handled, void *opaque) +{ + void *promise_obj; + uint32_t i, length; + ngx_js_ctx_t *ctx; + ngx_js_rejected_promise_t *rejected_promise; + + ctx = opaque; + + if (is_handled && ctx->rejected_promises != NULL) { + rejected_promise = ctx->rejected_promises->start; + length = ctx->rejected_promises->items; + + promise_obj = JS_VALUE_GET_PTR(promise); + + for (i = 0; i < length; i++) { + if (JS_VALUE_GET_PTR(ngx_qjs_arg(rejected_promise[i].promise)) + == promise_obj) + { + JS_FreeValue(cx, ngx_qjs_arg(rejected_promise[i].promise)); + JS_FreeValue(cx, ngx_qjs_arg(rejected_promise[i].message)); + njs_arr_remove(ctx->rejected_promises, &rejected_promise[i]); + + break; + } } + + return; } - if (ngx_js_unhandled_rejection(ctx)) { - ngx_js_exception(vm, &exception); + if (ctx->rejected_promises == NULL) { + if (ctx->engine == NULL) { + /* Do not track rejections during eval stage. The exception + * is lifted by the ngx_qjs_clone() function manually. */ + return; + } - ngx_log_error(NGX_LOG_ERR, ctx->log, 0, "js exception: %V", &exception); - return NGX_ERROR; + ctx->rejected_promises = njs_arr_create(ctx->engine->pool, 4, + sizeof(ngx_js_rejected_promise_t)); + if (ctx->rejected_promises == NULL) { + return; + } } - return njs_rbtree_is_empty(&ctx->waiting_events) ? NGX_OK : NGX_AGAIN; -} - + rejected_promise = njs_arr_add(ctx->rejected_promises); + if (rejected_promise == NULL) { + return; + } -static void * -ngx_engine_njs_external(ngx_engine_t *engine) -{ - return njs_vm_external_ptr(engine->u.njs.vm); + ngx_qjs_arg(rejected_promise->promise) = JS_DupValue(cx, promise); + ngx_qjs_arg(rejected_promise->message) = JS_DupValue(cx, reason); } -static ngx_int_t -ngx_engine_njs_pending(ngx_engine_t *e) + +static JSModuleDef * +ngx_qjs_core_init(JSContext *cx, const char *name) { - return njs_vm_pending(e->u.njs.vm); -} + int ret; + JSValue global_obj, proto, obj; + JSModuleDef *m; + JS_NewClassID(&ngx_qjs_console_class_id); -static ngx_int_t -ngx_engine_njs_string(ngx_engine_t *e, njs_opaque_value_t *value, - ngx_str_t *str) -{ - ngx_int_t rc; - njs_str_t s; + if (!JS_IsRegisteredClass(JS_GetRuntime(cx), + ngx_qjs_console_class_id)) + { + if (JS_NewClass(JS_GetRuntime(cx), ngx_qjs_console_class_id, + &ngx_qjs_console_class) < 0) + { + return NULL; + } - rc = ngx_js_string(e->u.njs.vm, njs_value_arg(value), &s); + proto = JS_NewObject(cx); + if (JS_IsException(proto)) { + return NULL; + } - str->data = s.start; - str->len = s.length; + JS_SetPropertyFunctionList(cx, proto, ngx_qjs_ext_console, + njs_nitems(ngx_qjs_ext_console)); - return rc; -} + JS_SetClassProto(cx, ngx_qjs_console_class_id, proto); + } + obj = JS_NewObject(cx); + if (JS_IsException(obj)) { + return NULL; + } -static void -ngx_engine_njs_destroy(ngx_engine_t *e, ngx_js_ctx_t *ctx, - ngx_js_loc_conf_t *conf) -{ - ngx_js_event_t *event; - njs_rbtree_node_t *node; + JS_SetPropertyFunctionList(cx, obj, ngx_qjs_ext_ngx, + njs_nitems(ngx_qjs_ext_ngx)); - if (ctx != NULL) { - node = njs_rbtree_min(&ctx->waiting_events); + global_obj = JS_GetGlobalObject(cx); - while (njs_rbtree_is_there_successor(&ctx->waiting_events, node)) { - event = (ngx_js_event_t *) ((u_char *) node - - offsetof(ngx_js_event_t, node)); + JS_SetPropertyFunctionList(cx, global_obj, ngx_qjs_ext_global, + njs_nitems(ngx_qjs_ext_global)); - if (event->destructor != NULL) { - event->destructor(event); - } + ret = JS_SetPropertyStr(cx, global_obj, "ngx", obj); + if (ret < 0) { + JS_FreeValue(cx, global_obj); + return NULL; + } - node = njs_rbtree_node_successor(&ctx->waiting_events, node); - } + obj = JS_NewObjectClass(cx, ngx_qjs_console_class_id); + if (JS_IsException(obj)) { + JS_FreeValue(cx, global_obj); + return NULL; } - njs_vm_destroy(e->u.njs.vm); + JS_SetOpaque(obj, (void *) 1); - /* - * when ctx !=NULL e->pool is vm pool, in such case it is destroyed - * by njs_vm_destroy(). - */ + ret = JS_SetPropertyStr(cx, global_obj, "console", obj); + if (ret < 0) { + JS_FreeValue(cx, global_obj); + return NULL; + } - if (ctx == NULL) { - njs_mp_destroy(e->pool); + JS_FreeValue(cx, global_obj); + + m = JS_NewCModule(cx, name, NULL); + if (m == NULL) { + return NULL; } + + return m; } +#endif + ngx_int_t ngx_js_call(njs_vm_t *vm, njs_function_t *func, njs_opaque_value_t *args, @@ -2439,11 +3909,18 @@ ngx_js_create_conf(ngx_conf_t *cf, size_t size) return NULL; } + /* + * set by ngx_pcalloc(): + * + * conf->reuse_queue = NULL; + */ + conf->paths = NGX_CONF_UNSET_PTR; conf->type = NGX_CONF_UNSET_UINT; conf->imports = NGX_CONF_UNSET_PTR; conf->preload_objects = NGX_CONF_UNSET_PTR; + conf->reuse = NGX_CONF_UNSET_SIZE; conf->buffer_size = NGX_CONF_UNSET_SIZE; conf->max_response_body_size = NGX_CONF_UNSET_SIZE; conf->timeout = NGX_CONF_UNSET_MSEC; @@ -2507,6 +3984,7 @@ ngx_js_merge_conf(ngx_conf_t *cf, void *parent, void *child, ngx_conf_merge_uint_value(conf->type, prev->type, NGX_ENGINE_NJS); ngx_conf_merge_msec_value(conf->timeout, prev->timeout, 60000); + ngx_conf_merge_size_value(conf->reuse, prev->reuse, 128); ngx_conf_merge_size_value(conf->buffer_size, prev->buffer_size, 16384); ngx_conf_merge_size_value(conf->max_response_body_size, prev->max_response_body_size, 1048576); @@ -2562,3 +4040,59 @@ ngx_js_monotonic_time(void) return (uint64_t) tv.tv_sec * 1000000000 + tv.tv_usec * 1000; #endif } + + +ngx_js_queue_t * +ngx_js_queue_create(ngx_pool_t *pool, ngx_uint_t capacity) +{ + ngx_js_queue_t *queue; + + queue = ngx_pcalloc(pool, sizeof(ngx_js_queue_t)); + if (queue == NULL) { + return NULL; + } + + queue->data = ngx_pcalloc(pool, sizeof(void *) * capacity); + if (queue->data == NULL) { + return NULL; + } + + queue->head = 0; + queue->tail = 0; + queue->size = 0; + queue->capacity = capacity; + + return queue; +} + + +ngx_int_t +ngx_js_queue_push(ngx_js_queue_t *queue, void *item) +{ + if (queue->size >= queue->capacity) { + return NGX_ERROR; + } + + queue->data[queue->tail] = item; + queue->tail = (queue->tail + 1) % queue->capacity; + queue->size++; + + return NGX_OK; +} + + +void * +ngx_js_queue_pop(ngx_js_queue_t *queue) +{ + void *item; + + if (queue->size == 0) { + return NULL; + } + + item = queue->data[queue->head]; + queue->head = (queue->head + 1) % queue->capacity; + queue->size--; + + return item; +} diff --git a/nginx/ngx_js.h b/nginx/ngx_js.h index 6d59bac17..c621c782b 100644 --- a/nginx/ngx_js.h +++ b/nginx/ngx_js.h @@ -19,8 +19,12 @@ #include "ngx_js_fetch.h" #include "ngx_js_shared_dict.h" +#if (NJS_HAVE_QUICKJS) +#include +#endif #define NGX_ENGINE_NJS 1 +#define NGX_ENGINE_QJS 2 #define NGX_JS_UNSET 0 #define NGX_JS_DEPRECATED 1 @@ -76,6 +80,15 @@ struct ngx_js_event_s { }; +typedef struct { + void **data; + ngx_uint_t head; + ngx_uint_t tail; + ngx_uint_t size; + ngx_uint_t capacity; +} ngx_js_queue_t; + + #define NGX_JS_COMMON_MAIN_CONF \ ngx_js_dict_t *dicts; \ ngx_array_t *periodics \ @@ -84,6 +97,8 @@ struct ngx_js_event_s { #define _NGX_JS_COMMON_LOC_CONF \ ngx_uint_t type; \ ngx_engine_t *engine; \ + ngx_uint_t reuse; \ + ngx_js_queue_t *reuse_queue; \ ngx_str_t cwd; \ ngx_array_t *imports; \ ngx_array_t *paths; \ @@ -157,6 +172,11 @@ struct ngx_js_ctx_s { }; +typedef struct { + void *external; +} ngx_js_opaque_t; + + typedef struct ngx_engine_opts_s { unsigned engine; union { @@ -164,6 +184,12 @@ typedef struct ngx_engine_opts_s { njs_vm_meta_t *metas; njs_module_t **addons; } njs; +#if (NJS_HAVE_QUICKJS) + struct { + uintptr_t *metas; + qjs_module_t **addons; + } qjs; +#endif } u; njs_str_t file; @@ -176,11 +202,22 @@ typedef struct ngx_engine_opts_s { } ngx_engine_opts_t; +typedef struct { + u_char *code; + size_t code_size; +} ngx_js_code_entry_t; + + struct ngx_engine_s { union { struct { njs_vm_t *vm; } njs; +#if (NJS_HAVE_QUICKJS) + struct { + JSContext *ctx; + } qjs; +#endif } u; ngx_int_t (*compile)(ngx_js_loc_conf_t *conf, ngx_log_t *lg, @@ -202,6 +239,7 @@ struct ngx_engine_s { unsigned type; const char *name; njs_mp_t *pool; + njs_arr_t *precompiled; }; @@ -246,6 +284,7 @@ void ngx_js_ctx_init(ngx_js_ctx_t *ctx, ngx_log_t *log); #define ngx_js_ctx_external(ctx) \ ((ctx)->engine->external(ctx->engine)) + void ngx_js_ctx_destroy(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *conf); ngx_int_t ngx_js_call(njs_vm_t *vm, njs_function_t *func, njs_opaque_value_t *args, njs_uint_t nargs); @@ -253,6 +292,60 @@ ngx_int_t ngx_js_exception(njs_vm_t *vm, ngx_str_t *s); ngx_engine_t *ngx_njs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external); +#if (NJS_HAVE_QUICKJS) + +typedef union { + njs_opaque_value_t opaque; + JSValue value; +} ngx_qjs_value_t; + +#define ngx_qjs_arg(val) (((ngx_qjs_value_t *) &(val))->value) +ngx_engine_t *ngx_qjs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, + void *external); +void ngx_engine_qjs_destroy(ngx_engine_t *e, ngx_js_ctx_t *ctx, + ngx_js_loc_conf_t *conf); +ngx_int_t ngx_qjs_call(ngx_js_ctx_t *ctx, njs_opaque_value_t function, + JSValue *argv, int argc); +ngx_int_t ngx_qjs_exception(ngx_engine_t *e, ngx_str_t *s); +ngx_int_t ngx_qjs_integer(JSContext *cx, JSValueConst val, ngx_int_t *n); +ngx_int_t ngx_qjs_string(ngx_engine_t *e, JSValueConst val, ngx_str_t *str); + +#define ngx_qjs_prop(cx, type, start, len) \ + ((type == NGX_JS_STRING) ? qjs_string_create(cx, start, len) \ + : qjs_buffer_create(cx, (u_char *) start, len)) + +#define ngx_qjs_meta(cx, i) \ + ((uintptr_t *) JS_GetRuntimeOpaque(JS_GetRuntime(cx)))[i] +#define ngx_qjs_external_connection(cx, e) \ + (*((ngx_connection_t **) ((u_char *) (e) + ngx_qjs_meta(cx, 0)))) +#define ngx_qjs_external_pool(cx, e) \ + ((ngx_external_pool_pt) ngx_qjs_meta(cx, 1))(e) +#define ngx_qjs_external_resolver(cx, e) \ + ((ngx_external_resolver_pt) ngx_qjs_meta(vm, 2))(e) +#define ngx_qjs_external_resolver_timeout(cx, e) \ + ((ngx_external_timeout_pt) ngx_qjs_meta(cx, 3))(e) +#define ngx_qjs_external_event_finalize(cx) \ + ((ngx_js_event_finalize_pt) ngx_qjs_meta(cx, 4)) +#define ngx_qjs_external_ssl(cx, e) \ + ((ngx_external_ssl_pt) ngx_qjs_meta(cx, 5))(e) +#define ngx_qjs_external_ssl_verify(cx, e) \ + ((ngx_external_flag_pt) ngx_qjs_meta(cx, 6))(e) +#define ngx_qjs_external_fetch_timeout(cx, e) \ + ((ngx_external_timeout_pt) ngx_qjs_meta(cx, 7))(e) +#define ngx_qjs_external_buffer_size(cx, e) \ + ((ngx_external_size_pt) ngx_qjs_meta(cx, 8))(e) +#define ngx_qjs_external_max_response_buffer_size(cx, e) \ + ((ngx_external_size_pt) ngx_qjs_meta(cx, 9))(e) +#define ngx_qjs_main_conf(cx) \ + ((ngx_js_main_conf_t *) ngx_qjs_meta(cx, NGX_JS_MAIN_CONF_INDEX)) +#define ngx_qjs_external_ctx(cx, e) \ + ((ngx_js_external_ctx_pt) ngx_qjs_meta(cx, 11))(e) + +extern qjs_module_t qjs_zlib_module; +extern qjs_module_t ngx_qjs_ngx_module; + +#endif + njs_int_t ngx_js_ext_log(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t level, njs_value_t *retval); void ngx_js_log(njs_vm_t *vm, njs_external_ptr_t external, @@ -286,6 +379,10 @@ njs_int_t ngx_js_ext_flags(njs_vm_t *vm, njs_object_prop_t *prop, ngx_int_t ngx_js_string(njs_vm_t *vm, njs_value_t *value, njs_str_t *str); ngx_int_t ngx_js_integer(njs_vm_t *vm, njs_value_t *value, ngx_int_t *n); +ngx_js_queue_t *ngx_js_queue_create(ngx_pool_t *pool, ngx_uint_t capacity); +ngx_int_t ngx_js_queue_push(ngx_js_queue_t *queue, void *item); +void *ngx_js_queue_pop(ngx_js_queue_t *queue); + extern njs_module_t ngx_js_ngx_module; extern njs_module_t njs_webcrypto_module; diff --git a/nginx/ngx_stream_js_module.c b/nginx/ngx_stream_js_module.c index 565f4e661..cee33add5 100644 --- a/nginx/ngx_stream_js_module.c +++ b/nginx/ngx_stream_js_module.c @@ -75,6 +75,21 @@ struct ngx_stream_js_ctx_s { }; +#if (NJS_HAVE_QUICKJS) + +typedef struct { + ngx_str_t name; + ngx_uint_t data_type; + ngx_uint_t id; +} ngx_stream_qjs_event_t; + +typedef struct { + ngx_stream_session_t *session; + JSValue callbacks[NGX_JS_EVENT_MAX]; +} ngx_stream_qjs_session_t; + +#endif + #define ngx_stream_pending(ctx) \ (ngx_js_ctx_pending(ctx) || ngx_stream_js_pending_events(ctx)) @@ -128,6 +143,53 @@ static njs_int_t ngx_stream_js_periodic_variables(njs_vm_t *vm, njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval, njs_value_t *retval); +#if (NJS_HAVE_QUICKJS) + +static JSValue ngx_stream_qjs_ext_done(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv, int magic); +static JSValue ngx_stream_qjs_ext_log(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv, int level); +static JSValue ngx_stream_qjs_ext_on(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv); +static JSValue ngx_stream_qjs_ext_off(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv); +static JSValue ngx_stream_qjs_ext_periodic_variables(JSContext *cx, + JSValueConst this_val, int type); +static JSValue ngx_stream_qjs_ext_remote_address(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_stream_qjs_ext_send(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv, int from_upstream); +static JSValue ngx_stream_qjs_ext_set_return_value(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_stream_qjs_ext_variables(JSContext *cx, + JSValueConst this_val, int type); +static JSValue ngx_stream_qjs_ext_uint(JSContext *cx, JSValueConst this_val, + int offset); +static JSValue ngx_stream_qjs_ext_flag(JSContext *cx, JSValueConst this_val, + int mask); + +static int ngx_stream_qjs_variables_own_property(JSContext *cx, + JSPropertyDescriptor *pdesc, JSValueConst obj, JSAtom prop); +static int ngx_stream_qjs_variables_set_property(JSContext *cx, + JSValueConst obj, JSAtom atom, JSValueConst value, JSValueConst receiver, + int flags); +static int ngx_stream_qjs_variables_define_own_property(JSContext *cx, + JSValueConst obj, JSAtom prop, JSValueConst value, JSValueConst getter, + JSValueConst setter, int flags); + +static ngx_int_t ngx_stream_qjs_run_event(ngx_stream_session_t *s, + ngx_stream_js_ctx_t *ctx, ngx_stream_js_ev_t *event, + ngx_uint_t from_upstream); +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 JSValue ngx_stream_qjs_session_make(JSContext *cx, ngx_int_t proto_id, + ngx_stream_session_t *s); +static void ngx_stream_qjs_session_finalizer(JSRuntime *rt, JSValue val); + +#endif + static ngx_pool_t *ngx_stream_js_pool(ngx_stream_session_t *s); static ngx_resolver_t *ngx_stream_js_resolver(ngx_stream_session_t *s); static ngx_msec_t ngx_stream_js_resolver_timeout(ngx_stream_session_t *s); @@ -167,6 +229,9 @@ static ngx_flag_t ngx_stream_js_ssl_verify(ngx_stream_session_t *s); static ngx_conf_bitmask_t ngx_stream_js_engines[] = { { ngx_string("njs"), NGX_ENGINE_NJS }, +#if (NJS_HAVE_QUICKJS) + { ngx_string("qjs"), NGX_ENGINE_QJS }, +#endif { ngx_null_string, 0 } }; @@ -191,6 +256,13 @@ static ngx_command_t ngx_stream_js_commands[] = { offsetof(ngx_stream_js_srv_conf_t, type), &ngx_stream_js_engines }, + { ngx_string("js_context_reuse"), + NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1, + ngx_conf_set_size_slot, + NGX_STREAM_SRV_CONF_OFFSET, + offsetof(ngx_stream_js_srv_conf_t, reuse), + NULL }, + { ngx_string("js_import"), NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE13, ngx_js_import, @@ -649,9 +721,9 @@ static njs_vm_meta_t ngx_stream_js_metas = { static ngx_stream_filter_pt ngx_stream_next_filter; -static njs_int_t ngx_stream_js_session_proto_id; -static njs_int_t ngx_stream_js_periodic_session_proto_id; -static njs_int_t ngx_stream_js_session_flags_proto_id; +static njs_int_t ngx_stream_js_session_proto_id = 1; +static njs_int_t ngx_stream_js_periodic_session_proto_id = 2; +static njs_int_t ngx_stream_js_session_flags_proto_id = 3; njs_module_t ngx_js_stream_module = { @@ -682,6 +754,98 @@ njs_module_t *njs_stream_js_addon_modules[] = { NULL, }; +#if (NJS_HAVE_QUICKJS) + +static JSClassID ngx_stream_qjs_session_class_id; +static JSClassID ngx_stream_qjs_periodic_class_id; +static JSClassID ngx_stream_qjs_flags_class_id; +static JSClassID ngx_stream_qjs_variables_class_id; + + +static const JSCFunctionListEntry ngx_stream_qjs_ext_session[] = { + JS_CFUNC_MAGIC_DEF("allow", 1, ngx_stream_qjs_ext_done, NGX_OK), + JS_CFUNC_MAGIC_DEF("decline", 1, ngx_stream_qjs_ext_done, -NGX_DECLINED), + JS_CFUNC_MAGIC_DEF("deny", 1, ngx_stream_qjs_ext_done, -NGX_DONE), + JS_CFUNC_MAGIC_DEF("done", 1, ngx_stream_qjs_ext_done, NGX_OK), + JS_CFUNC_MAGIC_DEF("error", 1, ngx_stream_qjs_ext_log, NGX_LOG_ERR), + JS_CFUNC_MAGIC_DEF("log", 1, ngx_stream_qjs_ext_log, NGX_LOG_INFO), + JS_CFUNC_DEF("on", 2, ngx_stream_qjs_ext_on), + JS_CFUNC_DEF("off", 1, ngx_stream_qjs_ext_off), + JS_CGETSET_MAGIC_DEF("rawVariables", ngx_stream_qjs_ext_variables, + NULL, NGX_JS_BUFFER), + JS_CGETSET_DEF("remoteAddress", ngx_stream_qjs_ext_remote_address, NULL), + JS_CFUNC_MAGIC_DEF("send", 2, ngx_stream_qjs_ext_send, NGX_JS_BOOL_UNSET), + JS_CFUNC_MAGIC_DEF("sendDownstream", 1, ngx_stream_qjs_ext_send, + NGX_JS_BOOL_TRUE), + JS_CFUNC_MAGIC_DEF("sendUpstream", 1, ngx_stream_qjs_ext_send, + NGX_JS_BOOL_FALSE), + JS_CFUNC_DEF("setReturnValue", 1, ngx_stream_qjs_ext_set_return_value), + JS_CGETSET_MAGIC_DEF("status", ngx_stream_qjs_ext_uint, NULL, + offsetof(ngx_stream_session_t, status)), + JS_CGETSET_MAGIC_DEF("variables", ngx_stream_qjs_ext_variables, + NULL, NGX_JS_STRING), + JS_CFUNC_MAGIC_DEF("warn", 1, ngx_stream_qjs_ext_log, NGX_LOG_WARN), +}; + + +static const JSCFunctionListEntry ngx_stream_qjs_ext_periodic[] = { + JS_CGETSET_MAGIC_DEF("rawVariables", ngx_stream_qjs_ext_periodic_variables, + NULL, NGX_JS_BUFFER), + JS_CGETSET_MAGIC_DEF("variables", ngx_stream_qjs_ext_periodic_variables, + NULL, NGX_JS_STRING), +}; + + +static const JSCFunctionListEntry ngx_stream_qjs_ext_flags[] = { + JS_CGETSET_MAGIC_DEF("from_upstream", ngx_stream_qjs_ext_flag, NULL, + 2), + JS_CGETSET_MAGIC_DEF("last", ngx_stream_qjs_ext_flag, NULL, 1), +}; + + +static JSClassDef ngx_stream_qjs_session_class = { + "Session", + .finalizer = ngx_stream_qjs_session_finalizer, +}; + + +static JSClassDef ngx_stream_qjs_periodic_class = { + "Periodic", + .finalizer = NULL, +}; + + +static JSClassDef ngx_stream_qjs_flags_class = { + "Stream Flags", + .finalizer = NULL, +}; + + +static JSClassDef ngx_stream_qjs_variables_class = { + "Variables", + .finalizer = NULL, + .exotic = & (JSClassExoticMethods) { + .get_own_property = ngx_stream_qjs_variables_own_property, + .set_property = ngx_stream_qjs_variables_set_property, + .define_own_property = ngx_stream_qjs_variables_define_own_property, + }, +}; + + +qjs_module_t *njs_stream_qjs_addon_modules[] = { + &ngx_qjs_ngx_module, + /* + * Shared addons should be in the same order and the same positions + * in all nginx modules. + */ +#ifdef NJS_HAVE_ZLIB + &qjs_zlib_module, +#endif + NULL, +}; + +#endif + static ngx_int_t ngx_stream_js_access_handler(ngx_stream_session_t *s) @@ -783,7 +947,6 @@ ngx_stream_js_body_filter(ngx_stream_session_t *s, ngx_chain_t *in, { ngx_int_t rc; ngx_chain_t *out; - ngx_connection_t *c; ngx_stream_js_ctx_t *ctx; ngx_stream_js_srv_conf_t *jscf; @@ -792,10 +955,8 @@ ngx_stream_js_body_filter(ngx_stream_session_t *s, ngx_chain_t *in, return ngx_stream_next_filter(s, in, from_upstream); } - c = s->connection; - - ngx_log_debug1(NGX_LOG_DEBUG_STREAM, c->log, 0, "stream js filter u:%ui", - from_upstream); + ngx_log_debug1(NGX_LOG_DEBUG_STREAM, s->connection->log, 0, + "stream js filter u:%ui", from_upstream); rc = ngx_stream_js_init_vm(s, ngx_stream_js_session_proto_id); @@ -810,7 +971,7 @@ ngx_stream_js_body_filter(ngx_stream_session_t *s, ngx_chain_t *in, ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module); if (!ctx->filter) { - ngx_log_debug1(NGX_LOG_DEBUG_STREAM, c->log, 0, + ngx_log_debug1(NGX_LOG_DEBUG_STREAM, s->connection->log, 0, "stream js filter call \"%V\"" , &jscf->filter); rc = ctx->engine->call((ngx_js_ctx_t *) ctx, &jscf->filter, @@ -990,9 +1151,9 @@ ngx_stream_js_init_vm(ngx_stream_session_t *s, njs_int_t proto_id) return NGX_ERROR; } - ngx_log_debug2(NGX_LOG_DEBUG_STREAM, ctx->log, 0, - "stream js vm clone: %p from: %p", ctx->engine, - jscf->engine); + ngx_log_debug3(NGX_LOG_DEBUG_STREAM, ctx->log, 0, + "stream js vm clone %s: %p from: %p", jscf->engine->name, + ctx->engine, jscf->engine); cln = ngx_pool_cleanup_add(s->connection->pool, 0); if (cln == NULL) { @@ -1806,173 +1967,1077 @@ ngx_engine_njs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, } -static ngx_int_t -ngx_stream_js_init_conf_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf) +#if (NJS_HAVE_QUICKJS) + +static JSValue +ngx_stream_qjs_ext_done(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv, int magic) { - ngx_engine_opts_t options; - ngx_js_main_conf_t *jmcf; + ngx_int_t status; + ngx_stream_js_ctx_t *ctx; + ngx_stream_session_t *s; - memset(&options, 0, sizeof(ngx_engine_opts_t)); + s = ngx_stream_qjs_session(this_val); + if (s == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a session object"); + } - options.engine = conf->type; + status = (ngx_int_t) magic; + status = -status; - if (conf->type == NGX_ENGINE_NJS) { - jmcf = ngx_stream_conf_get_module_main_conf(cf, ngx_stream_js_module); - ngx_stream_js_uptr[NGX_JS_MAIN_CONF_INDEX] = (uintptr_t) jmcf; + if (status == NGX_DONE) { + status = NGX_STREAM_FORBIDDEN; + } - options.u.njs.metas = &ngx_stream_js_metas; - options.u.njs.addons = njs_stream_js_addon_modules; - options.clone = ngx_engine_njs_clone; + if (!JS_IsUndefined(argv[0])) { + if (ngx_qjs_integer(cx, argv[0], &status) != NGX_OK) { + return JS_EXCEPTION; + } + + if (status < NGX_ABORT || status > NGX_STREAM_SERVICE_UNAVAILABLE) { + return JS_ThrowInternalError(cx, "code is out of range"); + } } - return ngx_js_init_conf_vm(cf, conf, &options); + ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module); + + if (ctx->filter) { + return JS_ThrowInternalError(cx, "should not be called while " + "filtering"); + } + + ngx_log_debug1(NGX_LOG_DEBUG_STREAM, s->connection->log, 0, + "stream js set status: %i", status); + + ctx->status = status; + + ngx_stream_js_drop_events(ctx); + + return JS_UNDEFINED; } -static void -ngx_stream_js_periodic_handler(ngx_event_t *ev) +static JSValue +ngx_stream_qjs_ext_log(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv, int level) { - ngx_int_t rc; - ngx_msec_t timer; - ngx_js_periodic_t *periodic; - ngx_connection_t *c; - ngx_stream_js_ctx_t *ctx; - ngx_stream_session_t *s; - ngx_stream_core_main_conf_t *cmcf; + int n; + const char *msg; + ngx_stream_session_t *s; - if (ngx_terminate || ngx_exiting) { - return; + s = ngx_stream_qjs_session(this_val); + if (s == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a session object"); } - periodic = ev->data; + for (n = 0; n < argc; n++) { + msg = JS_ToCString(cx, argv[n]); - timer = periodic->interval; + ngx_js_logger(s->connection, level, (u_char *) msg, ngx_strlen(msg)); - if (periodic->jitter) { - timer += (ngx_msec_t) ngx_random() % periodic->jitter; + JS_FreeCString(cx, msg); } - ngx_add_timer(&periodic->event, timer); + return JS_UNDEFINED; +} - c = periodic->connection; - if (c != NULL) { - ngx_log_error(NGX_LOG_ERR, c->log, 0, - "stream js periodic \"%V\" is already running, killing " - "previous instance", &periodic->method); +static const ngx_stream_qjs_event_t * +ngx_stream_qjs_event(ngx_stream_session_t *s, JSContext *cx, ngx_str_t *event) +{ + ngx_uint_t i, n, type; + ngx_stream_js_ctx_t *ctx; - ngx_stream_js_periodic_finalize(c->data, NGX_ERROR); - } + static const ngx_stream_qjs_event_t events[] = { + { + ngx_string("upload"), + NGX_JS_STRING, + NGX_JS_EVENT_UPLOAD, + }, - ngx_log_debug1(NGX_LOG_DEBUG_STREAM, &periodic->log, 0, - "stream js periodic handler: \"%V\"", &periodic->method); + { + ngx_string("download"), + NGX_JS_STRING, + NGX_JS_EVENT_DOWNLOAD, + }, - c = ngx_get_connection(0, &periodic->log); + { + ngx_string("upstream"), + NGX_JS_BUFFER, + NGX_JS_EVENT_UPLOAD, + }, - if (c == NULL) { - return; - } + { + ngx_string("downstream"), + NGX_JS_BUFFER, + NGX_JS_EVENT_DOWNLOAD, + }, + }; - c->pool = ngx_create_pool(1024, c->log); - if (c->pool == NULL) { - goto free_connection; + ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module); + + i = 0; + n = sizeof(events) / sizeof(events[0]); + + while (i < n) { + if (event->len == events[i].name.len + && ngx_memcmp(event->data, events[i].name.data, event->len) + == 0) + { + break; + } + + i++; } - s = ngx_pcalloc(c->pool, sizeof(ngx_stream_session_t)); - if (s == NULL) { - goto free_pool; + if (i == n) { + (void) JS_ThrowInternalError(cx, "unknown event \"%*s\"", + (int) event->len, event->data); + return NULL; } - s->main_conf = periodic->conf_ctx->main_conf; - s->srv_conf = periodic->conf_ctx->srv_conf; + ctx->events[events[i].id].data_type = events[i].data_type; - s->ctx = ngx_pcalloc(c->pool, sizeof(void *) * ngx_stream_max_module); - if (s->ctx == NULL) { - goto free_pool; + for (n = 0; n < NGX_JS_EVENT_MAX; n++) { + type = ctx->events[n].data_type; + if (type != NGX_JS_UNSET && type != events[i].data_type) { + (void) JS_ThrowInternalError(cx, "mixing string and buffer" + " events is not allowed"); + return NULL; + } } - cmcf = ngx_stream_get_module_main_conf(s, ngx_stream_core_module); + return &events[i]; +} - s->variables = ngx_pcalloc(c->pool, cmcf->variables.nelts - * sizeof(ngx_stream_variable_value_t)); - if (s->variables == NULL) { - goto free_pool; + +static JSValue +ngx_stream_qjs_ext_on(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + ngx_str_t name; + ngx_stream_js_ctx_t *ctx; + ngx_stream_qjs_session_t *ses; + const ngx_stream_qjs_event_t *e; + + ses = JS_GetOpaque(this_val, ngx_stream_qjs_session_class_id); + if (ses == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a session object"); } - c->data = s; - c->destroyed = 0; - c->read->log = &periodic->log; - c->read->handler = ngx_stream_js_periodic_event_handler; + ctx = ngx_stream_get_module_ctx(ses->session, ngx_stream_js_module); - s->received = 1; - s->connection = c; - s->signature = NGX_STREAM_MODULE; + if (ngx_qjs_string(ctx->engine, argv[0], &name) != NGX_OK) { + return JS_EXCEPTION; + } - s->health_check = 1; + e = ngx_stream_qjs_event(ses->session, cx, &name); + if (e == NULL) { + return JS_EXCEPTION; + } - rc = ngx_stream_js_init_vm(s, ngx_stream_js_periodic_session_proto_id); + if (JS_IsFunction(cx, ngx_qjs_arg(ctx->events[e->id].function))) { + return JS_ThrowInternalError(cx, "event handler \"%s\" is already set", + name.data); + } - if (rc != NGX_OK) { - ngx_stream_js_periodic_destroy(s, periodic); - return; + if (!JS_IsFunction(cx, argv[1])) { + return JS_ThrowTypeError(cx, "callback is not a function"); } - periodic->connection = c; + ngx_qjs_arg(ctx->events[e->id].function) = argv[1]; - ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module); + JS_FreeValue(cx, ses->callbacks[e->id]); + ses->callbacks[e->id] = JS_DupValue(cx, argv[1]); - ctx->periodic = periodic; + return JS_UNDEFINED; +} - s->received++; - rc = ctx->engine->call((ngx_js_ctx_t *) ctx, &periodic->method, - &ctx->args[0], 1); +static JSValue +ngx_stream_qjs_ext_off(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + ngx_str_t name; + ngx_stream_js_ctx_t *ctx; + ngx_stream_session_t *s; + const ngx_stream_qjs_event_t *e; - if (rc == NGX_AGAIN) { - rc = NGX_OK; + s = ngx_stream_qjs_session(this_val); + if (s == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a session object"); } - s->received--; + ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module); - ngx_stream_js_periodic_finalize(s, rc); + if (ngx_qjs_string(ctx->engine, argv[0], &name) != NGX_OK) { + return JS_EXCEPTION; + } - return; + e = ngx_stream_qjs_event(s, cx, &name); + if (e == NULL) { + return JS_EXCEPTION; + } -free_pool: + ngx_qjs_arg(ctx->events[e->id].function) = JS_NULL; + ctx->events[e->id].data_type = NGX_JS_UNSET; - ngx_destroy_pool(c->pool); + return JS_UNDEFINED; +} -free_connection: - ngx_close_connection(c); +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_stream_qjs_periodic_class_id); + if (ses == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a periodic object"); + } + + obj = JS_NewObjectProtoClass(cx, JS_NULL, + ngx_stream_qjs_variables_class_id); + + /* + * ngx_stream_session_t is always aligned to 16 bytes, so we can use the + * lower 4 bits of the pointer to store the buffer type. + */ + JS_SetOpaque(obj, (void *) ((uintptr_t) ses->session | (uintptr_t) type)); + + return obj; } -static void -ngx_stream_js_periodic_event_handler(ngx_event_t *ev) +static JSValue +ngx_stream_qjs_ext_remote_address(JSContext *cx, JSValueConst this_val) { ngx_connection_t *c; - ngx_stream_js_ctx_t *ctx; ngx_stream_session_t *s; - c = ev->data; - - ngx_log_debug0(NGX_LOG_DEBUG_STREAM, c->log, 0, - "stream js periodic event handler"); - - if (c->close) { - ngx_stream_js_periodic_finalize(c->data, NGX_ERROR); - return; + s = ngx_stream_qjs_session(this_val); + if (s == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a session object"); } - s = c->data; - - ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module); + c = s->connection; - if (!ngx_js_ctx_pending(ctx)) { - ngx_stream_js_periodic_finalize(s, NGX_OK); - return; - } + return qjs_string_create(cx, c->addr_text.data, c->addr_text.len); +} + + +static JSValue +ngx_stream_qjs_ext_send(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv, int from_upstream) +{ + JSValue val; + unsigned last_buf, flush; + ngx_str_t buffer; + ngx_buf_t *b; + ngx_chain_t *cl; + ngx_connection_t *c; + ngx_stream_js_ctx_t *ctx; + ngx_stream_session_t *s; + + s = ngx_stream_qjs_session(this_val); + if (s == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a session object"); + } + + c = s->connection; + + ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module); + + if (!ctx->filter) { + return JS_ThrowInternalError(cx, "cannot send buffer in this handler"); + } + + if (ngx_qjs_string(ctx->engine, argv[0], &buffer) != NGX_OK) { + return JS_EXCEPTION; + } + + /* + * ctx->buf != NULL when s.send() is called while processing incoming + * data chunks, otherwise s.send() is called asynchronously + */ + + if (ctx->buf != NULL) { + flush = ctx->buf->flush; + last_buf = ctx->buf->last_buf; + + } else { + flush = 0; + last_buf = 0; + } + + if (JS_IsObject(argv[1])) { + val = JS_GetPropertyStr(cx, argv[1], "flush"); + if (JS_IsException(val)) { + return JS_EXCEPTION; + } + + if (!JS_IsUndefined(val)) { + flush = JS_ToBool(cx, val); + JS_FreeValue(cx, val); + } + + val = JS_GetPropertyStr(cx, argv[1], "last"); + if (JS_IsException(val)) { + return JS_EXCEPTION; + } + + if (!JS_IsUndefined(val)) { + last_buf = JS_ToBool(cx, val); + JS_FreeValue(cx, val); + } + + if (from_upstream == NGX_JS_BOOL_UNSET) { + val = JS_GetPropertyStr(cx, argv[1], "from_upstream"); + if (JS_IsException(val)) { + return JS_EXCEPTION; + } + + if (!JS_IsUndefined(val)) { + from_upstream = JS_ToBool(cx, val); + JS_FreeValue(cx, val); + } + + if (from_upstream == NGX_JS_BOOL_UNSET && ctx->buf == NULL) { + return JS_ThrowInternalError(cx, "from_upstream flag is " + "expected when called " + "asynchronously"); + } + } + } + + cl = ngx_chain_get_free_buf(c->pool, &ctx->free); + if (cl == NULL) { + return JS_ThrowInternalError(cx, "memory error"); + } + + b = cl->buf; + + b->flush = flush; + b->last_buf = last_buf; + + b->memory = (buffer.len ? 1 : 0); + b->sync = (buffer.len ? 0 : 1); + b->tag = (ngx_buf_tag_t) &ngx_stream_js_module; + + b->start = buffer.data; + b->end = buffer.data + buffer.len; + + b->pos = b->start; + b->last = b->end; + + if (from_upstream == NGX_JS_BOOL_UNSET) { + *ctx->last_out = cl; + ctx->last_out = &cl->next; + + } else { + + if (ngx_stream_js_next_filter(s, ctx, cl, from_upstream) == NGX_ERROR) { + return JS_ThrowInternalError(cx, "ngx_stream_js_next_filter() " + "failed"); + } + } + + return JS_UNDEFINED; +} + + +static JSValue +ngx_stream_qjs_ext_set_return_value(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + ngx_js_ctx_t *ctx; + ngx_stream_session_t *s; + + s = ngx_stream_qjs_session(this_val); + if (s == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a session object"); + } + + ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module); + + JS_FreeValue(cx, ngx_qjs_arg(ctx->retval)); + ngx_qjs_arg(ctx->retval) = JS_DupValue(cx, argv[0]); + + return JS_UNDEFINED; +} + + +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_ThrowInternalError(cx, "\"this\" is not a session object"); + } + + obj = JS_NewObjectProtoClass(cx, JS_NULL, + ngx_stream_qjs_variables_class_id); + + /* + * ngx_stream_session_t is always aligned to 16 bytes, so we can use the + * lower 4 bits of the pointer to store the buffer type. + */ + JS_SetOpaque(obj, (void *) ((uintptr_t) s | (uintptr_t) type)); + + return obj; +} + + +static JSValue +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); + if (s == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a session object"); + } + + field = (ngx_uint_t *) ((u_char *) s + offset); + + return JS_NewUint32(cx, *field); +} + + +static JSValue +ngx_stream_qjs_ext_flag(JSContext *cx, JSValueConst this_val, int mask) +{ + uintptr_t flags; + + flags = (uintptr_t) JS_GetOpaque(this_val, ngx_stream_qjs_flags_class_id); + + return JS_NewBool(cx, flags & mask); +} + + +static int +ngx_stream_qjs_variables_own_property(JSContext *cx, + JSPropertyDescriptor *pdesc, JSValueConst obj, JSAtom prop) +{ + uint32_t buffer_type; + ngx_str_t name; + ngx_uint_t key; + ngx_stream_session_t *s; + ngx_stream_variable_value_t *vv; + + s = JS_GetOpaque(obj, ngx_stream_qjs_variables_class_id); + if (s == NULL) { + (void) JS_ThrowInternalError(cx, "\"this\" is not a session object"); + return -1; + } + + buffer_type = ngx_js_buffer_type((uintptr_t) s & 15); + s = (ngx_stream_session_t *) ((uintptr_t) s & ~15Ul); + + name.data = (u_char *) JS_AtomToCString(cx, prop); + if (name.data == NULL) { + return -1; + } + + name.len = ngx_strlen(name.data); + + key = ngx_hash_strlow(name.data, name.data, name.len); + + vv = ngx_stream_get_variable(s, &name, key); + JS_FreeCString(cx, (char *) name.data); + if (vv == NULL || vv->not_found) { + return 0; + } + + if (pdesc != NULL) { + pdesc->flags = JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE; + pdesc->getter = JS_UNDEFINED; + pdesc->setter = JS_UNDEFINED; + pdesc->value = ngx_qjs_prop(cx, buffer_type, vv->data, vv->len); + } + + return 1; +} + + +static int +ngx_stream_qjs_variables_set_property(JSContext *cx, JSValueConst obj, + JSAtom prop, JSValueConst value, JSValueConst receiver, int flags) +{ + ngx_str_t name, val; + ngx_uint_t key; + ngx_js_ctx_t *ctx; + ngx_stream_session_t *s; + ngx_stream_variable_t *v; + ngx_stream_variable_value_t *vv; + ngx_stream_core_main_conf_t *cmcf; + + s = JS_GetOpaque(obj, ngx_stream_qjs_variables_class_id); + if (s == NULL) { + (void) JS_ThrowInternalError(cx, "\"this\" is not a session object"); + return -1; + } + + s = (ngx_stream_session_t *) ((uintptr_t) s & ~15Ul); + + name.data = (u_char *) JS_AtomToCString(cx, prop); + if (name.data == NULL) { + return -1; + } + + name.len = ngx_strlen(name.data); + + key = ngx_hash_strlow(name.data, name.data, name.len); + + cmcf = ngx_stream_get_module_main_conf(s, ngx_stream_core_module); + + v = ngx_hash_find(&cmcf->variables_hash, key, name.data, name.len); + JS_FreeCString(cx, (char *) name.data); + + if (v == NULL) { + (void) JS_ThrowInternalError(cx, "variable not found"); + return -1; + } + + ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module); + + if (ngx_qjs_string(ctx->engine, value, &val) != NGX_OK) { + return -1; + } + + if (v->set_handler != NULL) { + vv = ngx_pcalloc(s->connection->pool, + sizeof(ngx_stream_variable_value_t)); + if (vv == NULL) { + (void) JS_ThrowOutOfMemory(cx); + return -1; + } + + vv->valid = 1; + vv->not_found = 0; + vv->data = val.data; + vv->len = val.len; + + v->set_handler(s, vv, v->data); + + return 1; + } + + if (!(v->flags & NGX_STREAM_VAR_INDEXED)) { + (void) JS_ThrowTypeError(cx, "variable is not writable"); + return -1; + } + + vv = &s->variables[v->index]; + + vv->valid = 1; + vv->not_found = 0; + + vv->data = ngx_pnalloc(s->connection->pool, val.len); + if (vv->data == NULL) { + vv->valid = 0; + (void) JS_ThrowOutOfMemory(cx); + return -1; + } + + vv->len = val.len; + ngx_memcpy(vv->data, val.data, vv->len); + + return 1; +} + + +static int +ngx_stream_qjs_variables_define_own_property(JSContext *cx, + JSValueConst obj, JSAtom prop, JSValueConst value, JSValueConst getter, + JSValueConst setter, int flags) +{ + if (!JS_IsUndefined(setter) || !JS_IsUndefined(getter)) { + (void) JS_ThrowTypeError(cx, "cannot define getter or setter"); + return -1; + } + + return ngx_stream_qjs_variables_set_property(cx, obj, prop, value, obj, + flags); +} + + +static ngx_int_t +ngx_stream_qjs_run_event(ngx_stream_session_t *s, ngx_stream_js_ctx_t *ctx, + ngx_stream_js_ev_t *event, ngx_uint_t from_upstream) +{ + size_t len; + u_char *p; + JSContext *cx; + ngx_int_t rc; + ngx_str_t exception; + ngx_buf_t *b; + uintptr_t flags; + ngx_connection_t *c; + JSValue argv[2]; + + cx = ctx->engine->u.qjs.ctx; + + if (!JS_IsFunction(cx, ngx_qjs_arg(event->function))) { + return NGX_OK; + } + + c = s->connection; + b = ctx->filter ? ctx->buf : c->buffer; + + len = b ? b->last - b->pos : 0; + + p = ngx_pnalloc(c->pool, len); + if (p == NULL) { + (void) JS_ThrowOutOfMemory(cx); + goto error; + } + + if (len) { + ngx_memcpy(p, b->pos, len); + } + + argv[0] = ngx_qjs_prop(cx, event->data_type, p, len); + if (JS_IsException(argv[0])) { + goto error; + } + + argv[1] = JS_NewObjectClass(cx, ngx_stream_qjs_flags_class_id); + if (JS_IsException(argv[1])) { + JS_FreeValue(cx, argv[0]); + goto error; + } + + flags = from_upstream << 1 | (uintptr_t) (b && b->last_buf); + + JS_SetOpaque(argv[1], (void *) flags); + + rc = ngx_qjs_call((ngx_js_ctx_t *) ctx, event->function, &argv[0], 2); + JS_FreeValue(cx, argv[0]); + JS_FreeValue(cx, argv[1]); + + if (rc == NGX_ERROR) { +error: + ngx_qjs_exception(ctx->engine, &exception); + + ngx_log_error(NGX_LOG_ERR, c->log, 0, "js exception: %V", + &exception); + + return NGX_ERROR; + } + + return NGX_OK; +} + + +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) +{ + ngx_int_t rc; + JSContext *cx; + ngx_chain_t *cl; + ngx_stream_js_ev_t *event; + + cx = ctx->engine->u.qjs.ctx; + + while (in != NULL) { + ctx->buf = in->buf; + + event = ngx_stream_event(from_upstream); + + if (JS_IsFunction(cx, ngx_qjs_arg(event->function))) { + rc = ngx_stream_qjs_run_event(s, ctx, event, from_upstream); + if (rc != NGX_OK) { + return NGX_ERROR; + } + + ctx->buf->pos = ctx->buf->last; + + } else { + cl = ngx_alloc_chain_link(s->connection->pool); + if (cl == NULL) { + return NGX_ERROR; + } + + cl->buf = ctx->buf; + + *ctx->last_out = cl; + ctx->last_out = &cl->next; + } + + in = in->next; + } + + return NGX_OK; +} + + +static ngx_stream_session_t * +ngx_stream_qjs_session(JSValueConst val) +{ + ngx_stream_qjs_session_t *ses; + + ses = JS_GetOpaque(val, ngx_stream_qjs_session_class_id); + if (ses == NULL) { + return NULL; + } + + return ses->session; +} + + +static JSValue +ngx_stream_qjs_session_make(JSContext *cx, ngx_int_t proto_id, + ngx_stream_session_t *s) +{ + int ret; + JSValue session; + ngx_uint_t i; + ngx_stream_qjs_session_t *ses; + + session = JS_NewObjectClass(cx, proto_id); + if (JS_IsException(session)) { + return JS_EXCEPTION; + } + + ret = qjs_set_to_string_tag(cx, session, "Stream Session"); + if (ret == -1) { + JS_FreeValue(cx, session); + return JS_EXCEPTION; + } + + ses = js_malloc(cx, sizeof(ngx_stream_qjs_session_t)); + if (ses == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + ses->session = s; + + for (i = 0; i < NGX_JS_EVENT_MAX; i++) { + ses->callbacks[i] = JS_UNDEFINED; + } + + JS_SetOpaque(session, ses); + + return session; +} + + +static void +ngx_stream_qjs_session_finalizer(JSRuntime *rt, JSValue val) +{ + ngx_uint_t i; + ngx_stream_qjs_session_t *ses; + + ses = JS_GetOpaque(val, ngx_stream_qjs_session_class_id); + if (ses == NULL) { + return; + } + + for (i = 0; i < NGX_JS_EVENT_MAX; i++) { + JS_FreeValueRT(rt, ses->callbacks[i]); + } + + js_free_rt(rt, ses); +} + + +static ngx_engine_t * +ngx_engine_qjs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, + njs_int_t proto_id, void *external) +{ + JSValue proto; + JSContext *cx; + ngx_engine_t *engine; + ngx_stream_js_ctx_t *sctx; + + engine = ngx_qjs_clone(ctx, cf, external); + if (engine == NULL) { + return NULL; + } + + cx = engine->u.qjs.ctx; + + if (!JS_IsRegisteredClass(JS_GetRuntime(cx), + ngx_stream_qjs_session_class_id)) + { + if (JS_NewClass(JS_GetRuntime(cx), ngx_stream_qjs_session_class_id, + &ngx_stream_qjs_session_class) < 0) + { + return NULL; + } + + proto = JS_NewObject(cx); + JS_SetPropertyFunctionList(cx, proto, ngx_stream_qjs_ext_session, + njs_nitems(ngx_stream_qjs_ext_session)); + + JS_SetClassProto(cx, ngx_stream_qjs_session_class_id, proto); + + if (JS_NewClass(JS_GetRuntime(cx), ngx_stream_qjs_periodic_class_id, + &ngx_stream_qjs_periodic_class) < 0) + { + return NULL; + } + + proto = JS_NewObject(cx); + JS_SetPropertyFunctionList(cx, proto, ngx_stream_qjs_ext_periodic, + njs_nitems(ngx_stream_qjs_ext_periodic)); + + JS_SetClassProto(cx, ngx_stream_qjs_periodic_class_id, proto); + + if (JS_NewClass(JS_GetRuntime(cx), ngx_stream_qjs_flags_class_id, + &ngx_stream_qjs_flags_class) < 0) + { + return NULL; + } + + proto = JS_NewObject(cx); + JS_SetPropertyFunctionList(cx, proto, ngx_stream_qjs_ext_flags, + njs_nitems(ngx_stream_qjs_ext_flags)); + + JS_SetClassProto(cx, ngx_stream_qjs_flags_class_id, proto); + + if (JS_NewClass(JS_GetRuntime(cx), ngx_stream_qjs_variables_class_id, + &ngx_stream_qjs_variables_class) < 0) + { + return NULL; + } + } + + sctx = (ngx_stream_js_ctx_t *) ctx; + sctx->run_event = ngx_stream_qjs_run_event; + sctx->body_filter = ngx_stream_qjs_body_filter; + + if (proto_id == ngx_stream_js_session_proto_id) { + proto_id = ngx_stream_qjs_session_class_id; + + } else if (proto_id == ngx_stream_js_periodic_session_proto_id) { + proto_id = ngx_stream_qjs_periodic_class_id; + } + + ngx_qjs_arg(ctx->args[0]) = ngx_stream_qjs_session_make(cx, proto_id, + external); + if (JS_IsException(ngx_qjs_arg(ctx->args[0]))) { + return NULL; + } + + return engine; +} + + +static void +ngx_stream_qjs_destroy(ngx_engine_t *e, ngx_js_ctx_t *ctx, + ngx_js_loc_conf_t *conf) +{ + ngx_uint_t i; + JSValue cb; + ngx_stream_qjs_session_t *ses; + + if (ctx != NULL) { + /* + * explicitly freeing the callback functions + * to avoid circular references with the session object. + */ + ses = JS_GetOpaque(ngx_qjs_arg(ctx->args[0]), + ngx_stream_qjs_session_class_id); + if (ses != NULL) { + for (i = 0; i < NGX_JS_EVENT_MAX; i++) { + cb = ses->callbacks[i]; + ses->callbacks[i] = JS_UNDEFINED; + JS_FreeValue(e->u.qjs.ctx, cb); + } + } + } + + ngx_engine_qjs_destroy(e, ctx, conf); +} + +#endif + + +static ngx_int_t +ngx_stream_js_init_conf_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf) +{ + ngx_engine_opts_t options; + ngx_js_main_conf_t *jmcf; + + memset(&options, 0, sizeof(ngx_engine_opts_t)); + + options.engine = conf->type; + + jmcf = ngx_stream_conf_get_module_main_conf(cf, ngx_stream_js_module); + ngx_stream_js_uptr[NGX_JS_MAIN_CONF_INDEX] = (uintptr_t) jmcf; + + if (conf->type == NGX_ENGINE_NJS) { + options.u.njs.metas = &ngx_stream_js_metas; + options.u.njs.addons = njs_stream_js_addon_modules; + options.clone = ngx_engine_njs_clone; + } + +#if (NJS_HAVE_QUICKJS) + else if (conf->type == NGX_ENGINE_QJS) { + options.u.qjs.metas = ngx_stream_js_uptr; + options.u.qjs.addons = njs_stream_qjs_addon_modules; + options.clone = ngx_engine_qjs_clone; + options.destroy = ngx_stream_qjs_destroy; + } +#endif + + return ngx_js_init_conf_vm(cf, conf, &options); +} + + +static void +ngx_stream_js_periodic_handler(ngx_event_t *ev) +{ + ngx_int_t rc; + ngx_msec_t timer; + ngx_js_periodic_t *periodic; + ngx_connection_t *c; + ngx_stream_js_ctx_t *ctx; + ngx_stream_session_t *s; + ngx_stream_core_main_conf_t *cmcf; + + if (ngx_terminate || ngx_exiting) { + return; + } + + periodic = ev->data; + + timer = periodic->interval; + + if (periodic->jitter) { + timer += (ngx_msec_t) ngx_random() % periodic->jitter; + } + + ngx_add_timer(&periodic->event, timer); + + c = periodic->connection; + + if (c != NULL) { + ngx_log_error(NGX_LOG_ERR, c->log, 0, + "stream js periodic \"%V\" is already running, killing " + "previous instance", &periodic->method); + + ngx_stream_js_periodic_finalize(c->data, NGX_ERROR); + } + + ngx_log_debug1(NGX_LOG_DEBUG_STREAM, &periodic->log, 0, + "stream js periodic handler: \"%V\"", &periodic->method); + + c = ngx_get_connection(0, &periodic->log); + + if (c == NULL) { + return; + } + + c->pool = ngx_create_pool(1024, c->log); + if (c->pool == NULL) { + goto free_connection; + } + + s = ngx_pcalloc(c->pool, sizeof(ngx_stream_session_t)); + if (s == NULL) { + goto free_pool; + } + + s->main_conf = periodic->conf_ctx->main_conf; + s->srv_conf = periodic->conf_ctx->srv_conf; + + s->ctx = ngx_pcalloc(c->pool, sizeof(void *) * ngx_stream_max_module); + if (s->ctx == NULL) { + goto free_pool; + } + + cmcf = ngx_stream_get_module_main_conf(s, ngx_stream_core_module); + + s->variables = ngx_pcalloc(c->pool, cmcf->variables.nelts + * sizeof(ngx_stream_variable_value_t)); + if (s->variables == NULL) { + goto free_pool; + } + + c->data = s; + c->destroyed = 0; + c->read->log = &periodic->log; + c->read->handler = ngx_stream_js_periodic_event_handler; + + s->received = 1; + s->connection = c; + s->signature = NGX_STREAM_MODULE; + + s->health_check = 1; + + rc = ngx_stream_js_init_vm(s, ngx_stream_js_periodic_session_proto_id); + + if (rc != NGX_OK) { + ngx_stream_js_periodic_destroy(s, periodic); + return; + } + + periodic->connection = c; + + ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module); + + ctx->periodic = periodic; + + s->received++; + + rc = ctx->engine->call((ngx_js_ctx_t *) ctx, &periodic->method, + &ctx->args[0], 1); + + if (rc == NGX_AGAIN) { + rc = NGX_OK; + } + + s->received--; + + ngx_stream_js_periodic_finalize(s, rc); + + return; + +free_pool: + + ngx_destroy_pool(c->pool); + +free_connection: + + ngx_close_connection(c); +} + + +static void +ngx_stream_js_periodic_event_handler(ngx_event_t *ev) +{ + ngx_connection_t *c; + ngx_stream_js_ctx_t *ctx; + ngx_stream_session_t *s; + + c = ev->data; + + ngx_log_debug0(NGX_LOG_DEBUG_STREAM, c->log, 0, + "stream js periodic event handler"); + + if (c->close) { + ngx_stream_js_periodic_finalize(c->data, NGX_ERROR); + return; + } + + s = c->data; + + ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module); + + if (!ngx_js_ctx_pending(ctx)) { + ngx_stream_js_periodic_finalize(s, NGX_OK); + return; + } } @@ -2392,7 +3457,6 @@ ngx_stream_js_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) ngx_stream_js_srv_conf_t *prev = parent; ngx_stream_js_srv_conf_t *conf = child; - ngx_conf_merge_uint_value(conf->type, prev->type, NGX_ENGINE_NJS); ngx_conf_merge_str_value(conf->access, prev->access, ""); ngx_conf_merge_str_value(conf->preread, prev->preread, ""); ngx_conf_merge_str_value(conf->filter, prev->filter, ""); @@ -2418,6 +3482,13 @@ ngx_stream_js_init(ngx_conf_t *cf) ngx_stream_next_filter = ngx_stream_top_filter; ngx_stream_top_filter = ngx_stream_js_body_filter; +#if (NJS_HAVE_QUICKJS) + JS_NewClassID(&ngx_stream_qjs_session_class_id); + JS_NewClassID(&ngx_stream_qjs_periodic_class_id); + JS_NewClassID(&ngx_stream_qjs_flags_class_id); + JS_NewClassID(&ngx_stream_qjs_variables_class_id); +#endif + cmcf = ngx_stream_conf_get_module_main_conf(cf, ngx_stream_core_module); h = ngx_array_push(&cmcf->phases[NGX_STREAM_ACCESS_PHASE].handlers); diff --git a/nginx/t/js_console.t b/nginx/t/js_console.t index fcaac3a65..c94991697 100644 --- a/nginx/t/js_console.t +++ b/nginx/t/js_console.t @@ -41,6 +41,10 @@ http { listen 127.0.0.1:8080; server_name localhost; + location /engine { + js_content test.engine; + } + location /dump { js_content test.dump; } @@ -74,6 +78,10 @@ http { EOF $t->write_file('test.js', <write_file('test.js', <try_run('no njs console')->plan(7); ############################################################################### +my $engine = http_get('/engine'); + http_get('/dump?data=eyJhIjpbMiwzXX0'); http_get('/error?data=IldBS0Ei'); http_get('/info?data=IkJBUiI'); @@ -136,8 +146,16 @@ $t->stop(); like($t->read_file('error.log'), qr/\[error\].*js: WAKA/, 'console.error'); like($t->read_file('error.log'), qr/\[info\].*js: BAR/, 'console.info'); + +SKIP: { + skip "QuickJS has no console.dump() method.", 1 + if $engine =~ /QuickJS$/m; + like($t->read_file('error.log'), qr/\[info\].*js: \{a:\['B','C'\]\}/, 'console.log with object'); + +} + like($t->read_file('error.log'), qr/\[warn\].*js: FOO/, 'console.warn'); like($t->read_file('error.log'), qr/\[info\].*js: foo: \d+\.\d\d\d\d\d\dms/, 'console.time foo'); diff --git a/nginx/t/js_dump.t b/nginx/t/js_dump.t index c00a53a2e..a96c2cd4b 100644 --- a/nginx/t/js_dump.t +++ b/nginx/t/js_dump.t @@ -42,6 +42,10 @@ http { listen 127.0.0.1:8080; server_name localhost; + location /engine { + js_content test.engine; + } + location /dump { js_content test.dump; } @@ -63,6 +67,10 @@ http { EOF $t->write_file('test.js', <write_file('test.js', <try_run('no njs dump')->plan(3); ############################################################################### +SKIP: { + skip "QuickJS has no njs.dump() method.", 1 + if http_get('/engine') =~ /QuickJS$/m; + like(http( 'GET /dump?v=1&t=x HTTP/1.0' . CRLF . 'Foo: bar' . CRLF @@ -95,6 +107,12 @@ like(http( . 'Host: localhost' . CRLF . CRLF ), qr/method:'GET'/, 'njs.dump(r)'); +} + +TODO: { + local $TODO = 'in QuickJS these are non-enumerable getter/setter props' + if http_get('/engine') =~ /^(QuickJS)$/m; + like(http( 'GET /stringify?v=1&t=x HTTP/1.0' . CRLF . 'Foo: bar' . CRLF @@ -107,4 +125,6 @@ like(http( . 'Host: localhost' . CRLF . CRLF ), qr/"status":201/, 'JSON.stringify(reply)'); +} + ############################################################################### diff --git a/nginx/t/js_engine.t b/nginx/t/js_engine.t new file mode 100644 index 000000000..831b5d0e9 --- /dev/null +++ b/nginx/t/js_engine.t @@ -0,0 +1,140 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for http njs module, js_engine directive. + +############################################################################### + +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 proxy/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_import test.js; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /njs { + js_content test.njs; + } + + location /njs/ { + proxy_pass http://127.0.0.1:8081/; + } + + location /qjs/ { + proxy_pass http://127.0.0.1:8082/; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + + js_engine njs; + + location /test { + js_content test.test; + } + + location /override { + js_engine qjs; + js_content test.test; + } + } + + server { + listen 127.0.0.1:8082; + server_name localhost; + + js_engine qjs; + + location /test { + js_content test.test; + } + + location /override { + js_engine njs; + js_content test.test; + } + } +} + +EOF + +$t->write_file('test.js', <try_run('no njs js_engine')->plan(4); + +############################################################################### + +TODO: { +local $TODO = 'not yet' unless has_version('0.8.6'); + +like(http_get('/njs/test'), qr/njs/, 'js_engine njs server'); +like(http_get('/njs/override'), qr/QuickJS/, 'js_engine override'); +like(http_get('/qjs/test'), qr/QuickJS/, 'js_engine qjs server'); +like(http_get('/qjs/override'), qr/njs/, 'js_engine override'); + +} + +$t->stop(); + +############################################################################### + +sub has_version { + my $need = shift; + + http_get('/njs') =~ /^([.0-9]+)$/m; + + my @v = split(/\./, $1); + my ($n, $v); + + for $n (split(/\./, $need)) { + $v = shift @v || 0; + return 0 if $n > $v; + return 1 if $v > $n; + } + + return 1; +} + +############################################################################### diff --git a/nginx/t/js_fetch.t b/nginx/t/js_fetch.t index e0763a6aa..320e06f56 100644 --- a/nginx/t/js_fetch.t +++ b/nginx/t/js_fetch.t @@ -52,6 +52,10 @@ http { js_content test.njs; } + location /engine { + js_content test.engine; + } + location /broken { js_content test.broken; } @@ -134,6 +138,10 @@ $t->write_file('test.js', <write_file('test.js', <try_run('no njs.fetch')->plan(36); +$t->try_run('no njs.fetch'); + +plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; + +$t->plan(36); $t->run_daemon(\&http_daemon, port(8082)); $t->waitforsocket('127.0.0.1:' . port(8082)); diff --git a/nginx/t/js_fetch_https.t b/nginx/t/js_fetch_https.t index 9d4ebb0a8..9a44a3390 100644 --- a/nginx/t/js_fetch_https.t +++ b/nginx/t/js_fetch_https.t @@ -48,6 +48,10 @@ http { js_content test.njs; } + location /engine { + js_content test.engine; + } + location /https { js_content test.https; } @@ -102,6 +106,10 @@ $t->write_file('test.js', <write_file('test.js', < r.return(501, e.message)) } - export default {njs: test_njs, https}; + export default {njs: test_njs, https, engine}; EOF my $d = $t->testdir(); @@ -186,7 +194,11 @@ foreach my $name ('default.example.com', '1.example.com') { . $t->read_file('intermediate.crt')); } -$t->try_run('no njs.fetch')->plan(7); +$t->try_run('no njs.fetch'); + +plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; + +$t->plan(7); $t->run_daemon(\&dns_daemon, port(8981), $t); $t->waitforfile($t->testdir . '/' . port(8981)); diff --git a/nginx/t/js_fetch_objects.t b/nginx/t/js_fetch_objects.t index d0f476302..1bc88a3d8 100644 --- a/nginx/t/js_fetch_objects.t +++ b/nginx/t/js_fetch_objects.t @@ -45,6 +45,10 @@ http { js_content test.njs; } + location /engine { + js_content test.engine; + } + location /headers { js_content test.headers; } @@ -88,6 +92,10 @@ $t->write_file('test.js', <write_file('test.js', <try_run('no njs')->plan(5); +$t->try_run('no njs'); + +plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; + +$t->plan(5); ############################################################################### diff --git a/nginx/t/js_fetch_resolver.t b/nginx/t/js_fetch_resolver.t index 8fb6b66fa..7cea33867 100644 --- a/nginx/t/js_fetch_resolver.t +++ b/nginx/t/js_fetch_resolver.t @@ -50,6 +50,10 @@ http { js_content test.njs; } + location /engine { + js_content test.engine; + } + location /dns { js_content test.dns; @@ -104,6 +108,10 @@ $t->write_file('test.js', <write_file('test.js', <try_run('no njs.fetch')->plan(5); +$t->try_run('no njs.fetch'); + +plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; + +$t->plan(5); $t->run_daemon(\&dns_daemon, port(8981), $t); $t->waitforfile($t->testdir . '/' . port(8981)); diff --git a/nginx/t/js_fetch_timeout.t b/nginx/t/js_fetch_timeout.t index 486656d66..1ac1c7aaa 100644 --- a/nginx/t/js_fetch_timeout.t +++ b/nginx/t/js_fetch_timeout.t @@ -47,6 +47,10 @@ http { js_content test.njs; } + location /engine { + js_content test.engine; + } + location /normal_timeout { js_content test.timeout_test; } @@ -80,6 +84,10 @@ $t->write_file('test.js', <write_file('test.js', < { r.return(200); }, 250, r, 0); } - export default {njs: test_njs, timeout_test, normal_reply, delayed_reply}; + export default {njs: test_njs, engine, timeout_test, normal_reply, + delayed_reply}; EOF -$t->try_run('no js_fetch_timeout')->plan(2); +$t->try_run('no js_fetch_timeout'); + +plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; + +$t->plan(2); ############################################################################### diff --git a/nginx/t/js_fetch_verify.t b/nginx/t/js_fetch_verify.t index d6bb1d9e3..4c97e04d7 100644 --- a/nginx/t/js_fetch_verify.t +++ b/nginx/t/js_fetch_verify.t @@ -48,6 +48,10 @@ http { js_content test.njs; } + location /engine { + js_content test.engine; + } + location /https { js_content test.https; } @@ -76,6 +80,10 @@ $t->write_file('test.js', < reply.text()) @@ -83,7 +91,7 @@ $t->write_file('test.js', < r.return(501, e.message)); } - export default {njs: test_njs, https}; + export default {njs: test_njs, engine, https}; EOF $t->write_file('openssl.conf', <try_run('no js_fetch_verify')->plan(2); +$t->try_run('no js_fetch_verify'); + +plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; + +$t->plan(2); $t->run_daemon(\&dns_daemon, port(8981), $t); $t->waitforfile($t->testdir . '/' . port(8981)); diff --git a/nginx/t/js_object.t b/nginx/t/js_object.t index 97e778a2a..8c1540104 100644 --- a/nginx/t/js_object.t +++ b/nginx/t/js_object.t @@ -42,6 +42,10 @@ http { listen 127.0.0.1:8080; server_name localhost; + location /engine { + js_content test.engine; + } + location /to_string { js_content test.to_string; } @@ -75,6 +79,10 @@ http { EOF $t->write_file('test.js', <write_file('test.js', <write_file('test.js', <write_file('test.js', <try_run('no js_periodic')->plan(9); +$t->try_run('no js_periodic'); + +plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; + +$t->plan(9); ############################################################################### diff --git a/nginx/t/js_preload_object.t b/nginx/t/js_preload_object.t index 407e97fe9..49befd66d 100644 --- a/nginx/t/js_preload_object.t +++ b/nginx/t/js_preload_object.t @@ -45,6 +45,10 @@ http { js_import lib.js; js_preload_object lx from l.json; + location /engine { + js_content lib.engine; + } + location /test { js_content lib.test; } @@ -83,6 +87,10 @@ $t->write_file('lib.js', <write_file('lib.js', <write_file('ga.json', '"ga loaded"'); $t->write_file('l.json', '"l loaded"'); $t->write_file('no_suffix', '"no_suffix loaded"'); -$t->try_run('no js_preload_object available')->plan(12); +$t->try_run('no js_preload_object available'); + +plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; + +$t->plan(12); ############################################################################### diff --git a/nginx/t/js_shared_dict.t b/nginx/t/js_shared_dict.t index ffc286e2c..161282255 100644 --- a/nginx/t/js_shared_dict.t +++ b/nginx/t/js_shared_dict.t @@ -51,6 +51,10 @@ http { js_content test.njs; } + location /engine { + js_content test.engine; + } + location /add { js_content test.add; } @@ -132,6 +136,10 @@ $t->write_file('test.js', <<'EOF'); r.return(200, njs.version); } + function engine(r) { + r.return(200, njs.engine); + } + function convertToValue(dict, v) { if (dict.type == 'number') { return parseInt(v); @@ -257,7 +265,7 @@ $t->write_file('test.js', <<'EOF'); function pop(r) { var dict = ngx.shared[r.args.dict]; - var val = dict.pop(r.args.key); + var val = dict.pop(r.args.key); if (val == '') { val = 'empty'; @@ -302,10 +310,14 @@ $t->write_file('test.js', <<'EOF'); export default { add, capacity, chain, clear, del, free_space, get, has, incr, items, keys, name, njs: test_njs, pop, replace, set, - set_clear, size, zones }; + set_clear, size, zones, engine }; EOF -$t->try_run('no js_shared_dict_zone')->plan(51); +$t->try_run('no js_shared_dict_zone'); + +plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; + +$t->plan(51); ############################################################################### diff --git a/nginx/t/stream_js_console.t b/nginx/t/stream_js_console.t index c3c228000..0c2532894 100644 --- a/nginx/t/stream_js_console.t +++ b/nginx/t/stream_js_console.t @@ -34,6 +34,21 @@ daemon off; events { } +http { + %%TEST_GLOBALS_HTTP%% + + js_import test.js; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /engine { + js_content test.engine; + } + } +} + stream { %%TEST_GLOBALS_STREAM%% @@ -41,7 +56,7 @@ stream { server { - listen 127.0.0.1:8080; + listen 127.0.0.1:8081; js_preread test.log; @@ -49,7 +64,7 @@ stream { } server { - listen 127.0.0.1:8081; + listen 127.0.0.1:8082; js_preread test.timer; @@ -60,6 +75,10 @@ stream { EOF $t->write_file('test.js', < 0) { @@ -85,7 +104,7 @@ $t->write_file('test.js', <run_daemon(\&stream_daemon, port(8090)); @@ -94,14 +113,23 @@ $t->waitforsocket('127.0.0.1:' . port(8090)); ############################################################################### -is(stream('127.0.0.1:' . port(8080))->io('eyJhIjpbIkIiLCJDIl19'), +my $engine = http_get('/engine'); + +is(stream('127.0.0.1:' . port(8081))->io('eyJhIjpbIkIiLCJDIl19'), 'eyJhIjpbIkIiLCJDIl19', 'log test'); -is(stream('127.0.0.1:' . port(8081))->io('timer'), 'timer', 'timer test'); +is(stream('127.0.0.1:' . port(8082))->io('timer'), 'timer', 'timer test'); $t->stop(); +SKIP: { + skip "QuickJS has no console.dump() method.", 1 + if $engine =~ /QuickJS$/m; + like($t->read_file('error.log'), qr/\[info\].*js: \{a:\['B','C'\]\}/, 'console.log with object'); + +} + like($t->read_file('error.log'), qr/\[info\].*js: foo: \d+\.\d\d\d\d\d\dms/, 'console.time foo'); diff --git a/nginx/t/stream_js_exit.t b/nginx/t/stream_js_exit.t index a8bc34aeb..01778f0f7 100644 --- a/nginx/t/stream_js_exit.t +++ b/nginx/t/stream_js_exit.t @@ -45,6 +45,10 @@ http { location /njs { js_content test.njs; } + + location /engine { + js_content test.engine; + } } } @@ -74,6 +78,10 @@ $t->write_file('test.js', < { var v = s.variables; @@ -95,10 +103,14 @@ $t->write_file('test.js', <try_run('no stream njs available')->plan(2); +$t->try_run('no stream njs available'); + +plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; + +$t->plan(2); $t->run_daemon(\&stream_daemon, port(8090)); $t->waitforsocket('127.0.0.1:' . port(8090)); diff --git a/nginx/t/stream_js_fetch.t b/nginx/t/stream_js_fetch.t index 106702dc9..c57128a8f 100644 --- a/nginx/t/stream_js_fetch.t +++ b/nginx/t/stream_js_fetch.t @@ -46,6 +46,10 @@ http { js_content test.njs; } + location /engine { + js_content test.engine; + } + location /validate { js_content test.validate; } @@ -99,6 +103,10 @@ $t->write_file('test.js', <write_file('test.js', <try_run('no stream njs available')->plan(9); +$t->try_run('no stream njs available'); + +plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; + +$t->plan(9); $t->run_daemon(\&stream_daemon, port(8090), port(8091)); $t->waitforsocket('127.0.0.1:' . port(8090)); diff --git a/nginx/t/stream_js_fetch_https.t b/nginx/t/stream_js_fetch_https.t index c49b833bf..5d7c5c20e 100644 --- a/nginx/t/stream_js_fetch_https.t +++ b/nginx/t/stream_js_fetch_https.t @@ -47,6 +47,10 @@ http { location /njs { js_content test.njs; } + + location /engine { + js_content test.engine; + } } server { @@ -159,6 +163,10 @@ $t->write_file('test.js', <write_file('test.js', <testdir(); @@ -263,7 +271,11 @@ foreach my $name ('default.example.com', '1.example.com') { . $t->read_file('intermediate.crt')); } -$t->try_run('no njs.fetch')->plan(6); +$t->try_run('no njs.fetch'); + +plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; + +$t->plan(6); $t->run_daemon(\&dns_daemon, port(8981), $t); $t->waitforfile($t->testdir . '/' . port(8981)); diff --git a/nginx/t/stream_js_fetch_init.t b/nginx/t/stream_js_fetch_init.t index 6de487dab..3f6d72629 100644 --- a/nginx/t/stream_js_fetch_init.t +++ b/nginx/t/stream_js_fetch_init.t @@ -58,6 +58,10 @@ http { js_content test.njs; } + location /engine { + js_content test.engine; + } + location /success { return 200; } @@ -73,16 +77,24 @@ $t->write_file('test.js', <try_run('no stream njs available')->plan(1); +$t->try_run('no stream njs available'); + +plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; + +$t->plan(1); $t->run_daemon(\&stream_daemon, port(8090)); $t->waitforsocket('127.0.0.1:' . port(8090)); diff --git a/nginx/t/stream_js_object.t b/nginx/t/stream_js_object.t index 504b9348d..571d0a878 100644 --- a/nginx/t/stream_js_object.t +++ b/nginx/t/stream_js_object.t @@ -33,35 +33,76 @@ daemon off; events { } +http { + %%TEST_GLOBALS_HTTP%% + + js_import test.js; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /engine { + js_content test.engine; + } + } +} + stream { %%TEST_GLOBALS_STREAM%% - js_set $test test.test; - js_import test.js; + js_set $to_string test.to_string; + js_set $define_prop test.define_prop; + js_set $in_operator test.in_operator; + js_set $redefine_proto test.redefine_proto; + js_set $get_own_prop_descs test.get_own_prop_descs; + server { listen 127.0.0.1:8081; - return $test$status; + return $to_string; + } + + server { + listen 127.0.0.1:8082; + return $define_prop$status; + } + + server { + listen 127.0.0.1:8083; + return $in_operator; + } + + server { + listen 127.0.0.1:8084; + return $redefine_proto; + } + + server { + listen 127.0.0.1:8085; + return $get_own_prop_descs; } } EOF $t->write_file('test.js', <v in s.variables) - .toString() === 'true,false'; + return ['status', 'unknown'].map(v=>v in s.variables).toString(); } function redefine_proto(s) { @@ -76,23 +117,27 @@ $t->write_file('test.js', <v(s)); - } - - export default {test}; + export default { engine, to_string, define_prop, in_operator, + redefine_proto, get_own_prop_descs }; EOF -$t->try_run('no njs stream session object')->plan(1); +$t->try_run('no njs stream session object')->plan(5); ############################################################################### -is(stream('127.0.0.1:' . port(8081))->read(), 'true400', 'var set'); +is(stream('127.0.0.1:' . port(8081))->read(), '[object Stream Session]', + 'to_string'); +is(stream('127.0.0.1:' . port(8082))->read(), '400400', 'define_prop'); +is(stream('127.0.0.1:' . port(8083))->read(), 'true,false', 'in_operator'); +is(stream('127.0.0.1:' . port(8084))->read(), 'true', 'redefine_proto'); + +SKIP: { + skip "In QuickJS methods are in the prototype", 1 + if http_get('/engine') =~ /QuickJS$/m; + +is(stream('127.0.0.1:' . port(8085))->read(), 'true', 'get_own_prop_descs'); + +} ############################################################################### diff --git a/nginx/t/stream_js_preload_object.t b/nginx/t/stream_js_preload_object.t index 3c27098dd..34ffae2ba 100644 --- a/nginx/t/stream_js_preload_object.t +++ b/nginx/t/stream_js_preload_object.t @@ -33,6 +33,21 @@ daemon off; events { } +http { + %%TEST_GLOBALS_HTTP%% + + js_import main.js; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /engine { + js_content main.engine; + } + } +} + stream { %%TEST_GLOBALS_STREAM%% @@ -104,14 +119,22 @@ $t->write_file('lib.js', <write_file('main.js', <write_file('g.json', '{"a":1, "b":[1,2,"element",4,5], "c":{"prop":[{"a":3}]}}'); -$t->try_run('no js_preload_object available')->plan(2); +$t->try_run('no js_preload_object available'); + +plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; + +$t->plan(2); ############################################################################### diff --git a/nginx/t/stream_js_shared_dict.t b/nginx/t/stream_js_shared_dict.t index e8e482f43..0bdfaeb77 100644 --- a/nginx/t/stream_js_shared_dict.t +++ b/nginx/t/stream_js_shared_dict.t @@ -43,6 +43,10 @@ http { location / { return 200; } + + location /engine { + js_content test.engine; + } } } @@ -71,6 +75,10 @@ EOF $t->write_file('test.js', <write_file('test.js', <try_run('no js_shared_dict_zone')->plan(9); +$t->try_run('no js_shared_dict_zone'); + +plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; + +$t->plan(9); $t->run_daemon(\&stream_daemon, port(8090)); $t->waitforsocket('127.0.0.1:' . port(8090));