diff --git a/nginx/config b/nginx/config index 5bc93a3ab..7b34163d1 100644 --- a/nginx/config +++ b/nginx/config @@ -6,11 +6,13 @@ NJS_ZLIB=${NJS_ZLIB:-YES} NJS_QUICKJS=${NJS_QUICKJS:-YES} NJS_DEPS="$ngx_addon_dir/ngx_js.h \ + $ngx_addon_dir/ngx_js_form.h \ $ngx_addon_dir/ngx_js_http.h \ $ngx_addon_dir/ngx_js_fetch.h \ $ngx_addon_dir/ngx_js_modules.h \ $ngx_addon_dir/ngx_js_shared_dict.h" NJS_SRCS="$ngx_addon_dir/ngx_js.c \ + $ngx_addon_dir/ngx_js_form.c \ $ngx_addon_dir/ngx_js_http.c \ $ngx_addon_dir/ngx_js_fetch.c \ $ngx_addon_dir/ngx_js_regex.c \ diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c index d1908ff49..6a8ae4221 100644 --- a/nginx/ngx_http_js_module.c +++ b/nginx/ngx_http_js_module.c @@ -11,6 +11,7 @@ #include #include "ngx_js.h" #include "ngx_js_modules.h" +#include "ngx_js_form.h" typedef struct { @@ -18,6 +19,7 @@ typedef struct { ngx_http_complex_value_t fetch_proxy_cv; + ngx_str_t access; ngx_str_t content; ngx_str_t header_filter; ngx_str_t body_filter; @@ -74,6 +76,65 @@ struct ngx_http_js_ctx_s { ngx_chain_t *in); ngx_js_periodic_t *periodic; + + unsigned in_progress:1; + + /* + * Body-read ownership state for the js_access phase handler. + * + * readRequest*() cannot call ngx_http_read_client_request_body() + * from the JS native directly. The phase handler must choose between + * NGX_AGAIN ("I still own the request") and NGX_DONE ("I finalized + * it myself"), but which one is correct depends on whether the body + * read completes synchronously or goes async -- and that is only + * known after the call returns. + * + * In the async case the body reader takes over r->read_event_handler + * and may call ngx_http_finalize_request() on I/O errors without + * invoking our post_handler. If the phase handler had returned + * NGX_AGAIN, both the phase engine and the body reader would own + * the request, causing a hang on errors such as chunked 413. + * + * IDLE no body read requested (initial and terminal state). + * DEFERRED JS called readRequest*(); the access handler + * will start the read after engine->call() returns. + * IN_PROGRESS ngx_http_read_client_request_body() returned + * NGX_AGAIN; request ownership transferred to the + * body reader via NGX_DONE. access_body_done + * callback resumes phases on completion. + * + * IDLE -> DEFERRED -> IDLE (sync completion or error) + * IDLE -> DEFERRED -> IN_PROGRESS -> IDLE (async completion) + */ +#define NGX_HTTP_JS_BODY_READ_IDLE 0 +#define NGX_HTTP_JS_BODY_READ_DEFERRED 1 +#define NGX_HTTP_JS_BODY_READ_IN_PROGRESS 2 +#define NGX_HTTP_JS_BODY_READ_FORM 4 +#define ngx_http_js_body_read_phase(state) ((state) & 3) +#define ngx_http_js_body_read_is_form(state) \ + (((state) & NGX_HTTP_JS_BODY_READ_FORM) != 0) +#define ngx_http_js_body_read_is_deferred(state) \ + (ngx_http_js_body_read_phase(state) == NGX_HTTP_JS_BODY_READ_DEFERRED) +#define ngx_http_js_body_read_is_in_progress(state) \ + (ngx_http_js_body_read_phase(state) == NGX_HTTP_JS_BODY_READ_IN_PROGRESS) +#define ngx_http_js_body_read_to_in_progress(state) \ + (((state) & NGX_HTTP_JS_BODY_READ_FORM) \ + | NGX_HTTP_JS_BODY_READ_IN_PROGRESS) + unsigned body_read_state:3; + + /* + * Collected request body as a contiguous buffer. + * Shared by both synchronous property getters (requestText, + * requestBuffer) and async readRequest*() methods. + */ + unsigned body_read_nul:1; + u_char *body_read_data; + size_t body_read_len; + + /* Pending promise/event for deferred body reads. */ + void *body_read_event; + + ngx_js_form_t *request_form; }; @@ -112,6 +173,8 @@ typedef struct { } ngx_http_js_entry_t; +static ngx_int_t ngx_http_js_access_handler(ngx_http_request_t *r); +static void ngx_http_js_access_write_event_handler(ngx_http_request_t *r); static ngx_int_t ngx_http_js_content_handler(ngx_http_request_t *r); static void ngx_http_js_content_event_handler(ngx_http_request_t *r); static void ngx_http_js_content_write_event_handler(ngx_http_request_t *r); @@ -124,6 +187,70 @@ static ngx_int_t ngx_http_js_variable_var(ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data); static ngx_int_t ngx_http_js_init_vm(ngx_http_request_t *r, njs_int_t proto_id); static void ngx_http_js_cleanup_ctx(void *data); +static void ngx_http_js_body_read_abort(ngx_http_js_ctx_t *ctx); +static ngx_int_t ngx_http_js_collect_body(ngx_http_request_t *r, + ngx_http_js_ctx_t *ctx); +static void ngx_http_js_access_body_finalize(ngx_http_request_t *r, + ngx_http_js_ctx_t *ctx, ngx_int_t rc); +static void ngx_http_js_access_body_done(ngx_http_request_t *r); +static ngx_int_t ngx_http_js_body_resolve(ngx_http_js_ctx_t *ctx, + void *event); +static ngx_int_t ngx_http_js_request_form(ngx_http_request_t *r, + ngx_http_js_ctx_t *ctx, ngx_uint_t max_keys, ngx_js_form_t **form, + ngx_str_t *error); +static njs_int_t ngx_http_js_form_to_value(njs_vm_t *vm, ngx_http_request_t *r, + ngx_http_js_ctx_t *ctx, ngx_uint_t max_keys, njs_value_t *retval); +static njs_int_t ngx_http_js_request_form_max_keys(njs_vm_t *vm, + njs_value_t *options, ngx_uint_t *max_keys); +static njs_int_t ngx_http_js_request_form_make(njs_vm_t *vm, + ngx_js_form_t *form, njs_value_t *retval); +static njs_int_t ngx_http_js_ext_read_request_form(njs_vm_t *vm, + njs_value_t *args, njs_uint_t nargs, njs_index_t unused, + njs_value_t *retval); +static njs_int_t ngx_http_js_ext_request_form_get(njs_vm_t *vm, + njs_value_t *args, njs_uint_t nargs, njs_index_t as_array, + njs_value_t *retval); +static njs_int_t ngx_http_js_ext_request_form_has(njs_vm_t *vm, + njs_value_t *args, njs_uint_t nargs, njs_index_t unused, + njs_value_t *retval); +static njs_int_t ngx_http_js_ext_request_form_for_each(njs_vm_t *vm, + njs_value_t *args, njs_uint_t nargs, njs_index_t unused, + njs_value_t *retval); +static njs_int_t ngx_http_js_ext_request_form_has_files(njs_vm_t *vm, + njs_value_t *args, njs_uint_t nargs, njs_index_t unused, + njs_value_t *retval); +static njs_int_t ngx_http_js_ext_request_form_file_field_names(njs_vm_t *vm, + njs_value_t *args, njs_uint_t nargs, njs_index_t unused, + njs_value_t *retval); + +static njs_int_t ngx_http_js_ext_read_request_body(njs_vm_t *vm, + njs_value_t *args, njs_uint_t nargs, njs_index_t magic, + njs_value_t *retval); +#if (NJS_HAVE_QUICKJS) +static JSValue ngx_http_qjs_ext_read_request_body(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv, int magic); +static ngx_int_t ngx_http_qjs_body_resolve(ngx_http_js_ctx_t *ctx, + void *event); +static JSValue ngx_http_qjs_form_to_value(JSContext *cx, ngx_http_request_t *r, + ngx_http_js_ctx_t *ctx, ngx_uint_t max_keys); +static ngx_int_t ngx_http_qjs_request_form_max_keys(JSContext *cx, + JSValueConst options, ngx_uint_t *max_keys); +static JSValue ngx_http_qjs_request_form_make(JSContext *cx, + ngx_js_form_t *form); +static JSValue ngx_http_qjs_ext_read_request_form(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_http_qjs_ext_request_form_get(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv, int as_array); +static JSValue ngx_http_qjs_ext_request_form_has(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_http_qjs_ext_request_form_for_each(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_http_qjs_ext_request_form_has_files(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_http_qjs_ext_request_form_file_field_names(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static void ngx_http_js_read_body_event_destructor(ngx_qjs_event_t *event); +#endif static njs_int_t ngx_http_js_ext_keys_header(njs_vm_t *vm, njs_value_t *value, njs_value_t *keys, ngx_list_t *headers); @@ -192,6 +319,8 @@ static njs_int_t ngx_http_js_ext_finish(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval); static njs_int_t ngx_http_js_ext_return(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval); +static njs_int_t ngx_http_js_ext_decline(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t unused, njs_value_t *retval); static njs_int_t ngx_http_js_ext_internal_redirect(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval); @@ -307,6 +436,8 @@ static JSValue ngx_http_qjs_ext_response_body(JSContext *cx, JSValueConst this_val, int type); static JSValue ngx_http_qjs_ext_return(JSContext *cx, JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_http_qjs_ext_decline(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv); static JSValue ngx_http_qjs_ext_send(JSContext *cx, JSValueConst this_val, int argc, JSValueConst *argv); static JSValue ngx_http_qjs_ext_send_buffer(JSContext *cx, @@ -379,6 +510,8 @@ static char *ngx_http_js_periodic(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); static char *ngx_http_js_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); static char *ngx_http_js_var(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); +static char *ngx_http_js_access(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); static char *ngx_http_js_content(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); static char *ngx_http_js_shared_dict_zone(ngx_conf_t *cf, ngx_command_t *cmd, @@ -482,6 +615,13 @@ static ngx_command_t ngx_http_js_commands[] = { 0, NULL }, + { ngx_string("js_access"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_http_js_access, + NGX_HTTP_LOC_CONF_OFFSET, + 0, + NULL }, + { ngx_string("js_content"), NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF|NGX_HTTP_LMT_CONF|NGX_CONF_TAKE1, ngx_http_js_content, @@ -682,6 +822,7 @@ static ngx_http_output_body_filter_pt ngx_http_next_body_filter; 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_int_t ngx_http_js_request_form_proto_id = 3; static njs_external_t ngx_http_js_ext_request[] = { @@ -916,6 +1057,17 @@ static njs_external_t ngx_http_js_ext_request[] = { } }, + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("decline"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = ngx_http_js_ext_decline, + } + }, + { .flags = NJS_EXTERN_METHOD, .name.string = njs_str("send"), @@ -970,6 +1122,53 @@ static njs_external_t ngx_http_js_ext_request[] = { } }, + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("readRequestArrayBuffer"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = ngx_http_js_ext_read_request_body, + .magic8 = NGX_JS_BODY_ARRAY_BUFFER, + } + }, + + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("readRequestJSON"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = ngx_http_js_ext_read_request_body, + .magic8 = NGX_JS_BODY_JSON, + } + }, + + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("readRequestForm"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = ngx_http_js_ext_read_request_form, + } + }, + + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("readRequestText"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = ngx_http_js_ext_read_request_body, + .magic8 = NGX_JS_BODY_TEXT, + } + }, + { .flags = NJS_EXTERN_METHOD, .name.string = njs_str("subrequest"), @@ -1015,6 +1214,85 @@ static njs_external_t ngx_http_js_ext_request[] = { }; +static njs_external_t ngx_http_js_ext_request_form[] = { + + { + .flags = NJS_EXTERN_PROPERTY | NJS_EXTERN_SYMBOL, + .name.symbol = NJS_SYMBOL_TO_STRING_TAG, + .u.property = { + .value = "RequestForm", + } + }, + + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("get"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = ngx_http_js_ext_request_form_get, + } + }, + + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("getAll"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = ngx_http_js_ext_request_form_get, + .magic8 = 1, + } + }, + + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("has"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = ngx_http_js_ext_request_form_has, + } + }, + + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("forEach"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = ngx_http_js_ext_request_form_for_each, + } + }, + + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("hasFiles"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = ngx_http_js_ext_request_form_has_files, + } + }, + + { + .flags = NJS_EXTERN_METHOD, + .name.string = njs_str("fileFieldNames"), + .writable = 1, + .configurable = 1, + .enumerable = 1, + .u.method = { + .native = ngx_http_js_ext_request_form_file_field_names, + } + }, +}; + + static njs_external_t ngx_http_js_ext_periodic_session[] = { { @@ -1132,12 +1410,21 @@ static const JSCFunctionListEntry ngx_http_qjs_ext_request[] = { JS_CGETSET_MAGIC_DEF("responseText", ngx_http_qjs_ext_response_body, NULL, NGX_JS_STRING), JS_CFUNC_DEF("return", 2, ngx_http_qjs_ext_return), + JS_CFUNC_DEF("decline", 0, ngx_http_qjs_ext_decline), JS_CFUNC_DEF("send", 1, ngx_http_qjs_ext_send), JS_CFUNC_DEF("sendBuffer", 2, ngx_http_qjs_ext_send_buffer), JS_CFUNC_DEF("sendHeader", 0, ngx_http_qjs_ext_send_header), 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_MAGIC_DEF("readRequestArrayBuffer", 0, + ngx_http_qjs_ext_read_request_body, + NGX_JS_BODY_ARRAY_BUFFER), + JS_CFUNC_MAGIC_DEF("readRequestJSON", 0, + ngx_http_qjs_ext_read_request_body, NGX_JS_BODY_JSON), + JS_CFUNC_DEF("readRequestForm", 1, ngx_http_qjs_ext_read_request_form), + JS_CFUNC_MAGIC_DEF("readRequestText", 0, + ngx_http_qjs_ext_read_request_body, NGX_JS_BODY_TEXT), 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)), @@ -1157,6 +1444,19 @@ static const JSCFunctionListEntry ngx_http_qjs_ext_periodic[] = { }; +static const JSCFunctionListEntry ngx_http_qjs_ext_request_form[] = { + JS_PROP_STRING_DEF("[Symbol.toStringTag]", "RequestForm", + JS_PROP_CONFIGURABLE), + JS_CFUNC_MAGIC_DEF("get", 1, ngx_http_qjs_ext_request_form_get, 0), + JS_CFUNC_MAGIC_DEF("getAll", 1, ngx_http_qjs_ext_request_form_get, 1), + JS_CFUNC_DEF("has", 1, ngx_http_qjs_ext_request_form_has), + JS_CFUNC_DEF("forEach", 2, ngx_http_qjs_ext_request_form_for_each), + JS_CFUNC_DEF("hasFiles", 0, ngx_http_qjs_ext_request_form_has_files), + JS_CFUNC_DEF("fileFieldNames", 0, + ngx_http_qjs_ext_request_form_file_field_names), +}; + + static JSClassDef ngx_http_qjs_request_class = { "Request", .finalizer = ngx_http_qjs_request_finalizer, @@ -1169,6 +1469,12 @@ static JSClassDef ngx_http_qjs_periodic_class = { }; +static JSClassDef ngx_http_qjs_request_form_class = { + "RequestForm", + .finalizer = NULL, +}; + + static JSClassDef ngx_http_qjs_variables_class = { "Variables", .finalizer = NULL, @@ -1211,6 +1517,118 @@ qjs_module_t *njs_http_qjs_addon_modules[] = { #endif +static ngx_int_t +ngx_http_js_access_handler(ngx_http_request_t *r) +{ + ngx_int_t rc; + ngx_http_js_ctx_t *ctx; + ngx_http_js_loc_conf_t *jlcf; + + jlcf = ngx_http_get_module_loc_conf(r, ngx_http_js_module); + + if (jlcf->access.len == 0) { + return NGX_DECLINED; + } + + if (r != r->main) { + return NGX_DECLINED; + } + + rc = ngx_http_js_init_vm(r, ngx_http_js_request_proto_id); + if (rc != NGX_OK) { + return rc; + } + + ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + + if (ctx->in_progress) { + if (ngx_js_ctx_pending(ctx)) { + return NGX_AGAIN; + } + + ctx->in_progress = 0; + + if (ctx->rejected_promises != NULL + && ctx->rejected_promises->items > 0) + { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + return ctx->status; + } + + ctx->status = NGX_OK; + + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http js access handler \"%V\"", &jlcf->access); + + rc = ctx->engine->call((ngx_js_ctx_t *) ctx, &jlcf->access, &ctx->args[0], + 1); + + if (rc == NGX_ERROR) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + /* JS called readRequest*(). */ + + if (ngx_http_js_body_read_is_deferred(ctx->body_read_state)) { + + rc = ngx_http_read_client_request_body(r, ngx_http_js_access_body_done); + + if (rc >= NGX_HTTP_SPECIAL_RESPONSE) { + ngx_http_js_body_read_abort(ctx); + return rc; + } + + r->preserve_body = 1; + + if (rc == NGX_OK) { + /* + * Sync: access_body_done callback already fired, resolved + * or rejected the promise. access_body_finalize() returned + * without running posted requests. Fall through to let + * the pending/status check handle the result. + */ + ctx->body_read_state = NGX_HTTP_JS_BODY_READ_IDLE; + goto done; + } + + ctx->body_read_state = ngx_http_js_body_read_to_in_progress( + ctx->body_read_state); + ctx->in_progress = 1; + ngx_http_finalize_request(r, NGX_DONE); + return NGX_DONE; + } + +done: + + if (ngx_js_ctx_pending(ctx)) { + ctx->in_progress = 1; + r->write_event_handler = ngx_http_js_access_write_event_handler; + return NGX_AGAIN; + } + + return ctx->status; +} + + +static void +ngx_http_js_access_write_event_handler(ngx_http_request_t *r) +{ + ngx_http_js_ctx_t *ctx; + + ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http js access write event handler"); + + if (!ngx_js_ctx_pending(ctx)) { + ngx_http_core_run_phases(r); + return; + } +} + + static ngx_int_t ngx_http_js_content_handler(ngx_http_request_t *r) { @@ -2822,6 +3240,30 @@ ngx_http_js_ext_return(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, } +static njs_int_t +ngx_http_js_ext_decline(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, + njs_index_t unused, njs_value_t *retval) +{ + ngx_http_js_ctx_t *ctx; + ngx_http_request_t *r; + + r = njs_vm_external(vm, ngx_http_js_request_proto_id, + njs_argument(args, 0)); + if (r == NULL) { + njs_vm_error(vm, "\"this\" is not an external"); + return NJS_ERROR; + } + + ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + + ctx->status = NGX_DECLINED; + + njs_value_undefined_set(retval); + + return NJS_OK; +} + + static njs_int_t ngx_http_js_ext_internal_redirect(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval) @@ -2996,14 +3438,9 @@ ngx_http_js_ext_get_request_body(njs_vm_t *vm, njs_object_prop_t *prop, uint32_t unused, njs_value_t *value, njs_value_t *setval, njs_value_t *retval) { - u_char *p, *body; - size_t len; - ssize_t n; uint32_t buffer_type; - ngx_buf_t *buf; njs_int_t ret; njs_value_t *request_body; - ngx_chain_t *cl; ngx_http_js_ctx_t *ctx; ngx_http_request_t *r; @@ -3031,73 +3468,777 @@ ngx_http_js_ext_get_request_body(njs_vm_t *vm, njs_object_prop_t *prop, return NJS_DECLINED; } - cl = r->request_body->bufs; - buf = cl->buf; + if (ngx_http_js_collect_body(r, ctx) != NGX_OK) { + njs_vm_internal_error(vm, "failed to read request body"); + return NJS_ERROR; + } + + ret = ngx_js_prop(vm, buffer_type, request_body, ctx->body_read_data, + ctx->body_read_len); + if (ret != NJS_OK) { + return NJS_ERROR; + } + + njs_value_assign(retval, request_body); + + return NJS_OK; +} + + +static ngx_int_t +ngx_http_js_collect_body(ngx_http_request_t *r, ngx_http_js_ctx_t *ctx) +{ + u_char *p, *body; + size_t len; + ssize_t n; + ngx_buf_t *buf; + ngx_chain_t *cl; + + if (ctx->body_read_data != NULL) { + return NGX_OK; + } + + if (r->request_body == NULL || r->request_body->bufs == NULL) { + ctx->body_read_data = (u_char *) ""; + ctx->body_read_len = 0; + ctx->body_read_nul = 1; + return NGX_OK; + } + + cl = r->request_body->bufs; + buf = cl->buf; if (r->request_body->temp_file) { ngx_log_error(NGX_LOG_WARN, r->connection->log, 0, "http js reading request body from a temporary file"); if (buf == NULL || !buf->in_file) { - njs_vm_internal_error(vm, "cannot find request body"); - return NJS_ERROR; + return NGX_ERROR; } len = buf->file_last - buf->file_pos; - body = ngx_pnalloc(r->pool, len); + body = ngx_pnalloc(r->pool, len + 1); if (body == NULL) { - njs_vm_memory_error(vm); - return NJS_ERROR; + return NGX_ERROR; } n = ngx_read_file(buf->file, body, len, buf->file_pos); if (n != (ssize_t) len) { - njs_vm_internal_error(vm, "failed to read request body"); - return NJS_ERROR; + return NGX_ERROR; } - goto done; - } + body[len] = '\0'; + ctx->body_read_nul = 1; - if (cl->next == NULL) { + } else if (cl->next == NULL) { len = buf->last - buf->pos; body = buf->pos; - goto done; + } else { + 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 + 1); + if (p == NULL) { + return NGX_ERROR; + } + + body = 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); + } + + *p = '\0'; + ctx->body_read_nul = 1; + } + + ctx->body_read_data = body; + ctx->body_read_len = len; + + return NGX_OK; +} + + +static void +ngx_http_js_body_read_abort(ngx_http_js_ctx_t *ctx) +{ + if (ctx->body_read_event != NULL) { +#if (NJS_HAVE_QUICKJS) + if (ctx->engine->type == NGX_ENGINE_QJS) { + ngx_js_del_event(ctx, (ngx_qjs_event_t *) ctx->body_read_event); + } else +#endif + { + ngx_js_del_event(ctx, (ngx_js_event_t *) ctx->body_read_event); + } + + ctx->body_read_event = NULL; } - len = buf->last - buf->pos; - cl = cl->next; + ctx->body_read_state = NGX_HTTP_JS_BODY_READ_IDLE; +} + - for ( /* void */ ; cl; cl = cl->next) { - buf = cl->buf; - len += buf->last - buf->pos; +static njs_int_t +ngx_http_js_body_to_value(njs_vm_t *vm, ngx_http_js_ctx_t *ctx, + ngx_uint_t type, njs_value_t *retval) +{ + njs_int_t ret; + njs_opaque_value_t arg; + + switch (type) { + case NGX_JS_BODY_ARRAY_BUFFER: + return njs_vm_value_array_buffer_set(vm, retval, + ctx->body_read_data, + ctx->body_read_len); + + case NGX_JS_BODY_JSON: + ret = njs_vm_value_string_create(vm, njs_value_arg(&arg), + ctx->body_read_data, + ctx->body_read_len); + if (ret != NJS_OK) { + return NJS_ERROR; + } + + return njs_vm_json_parse(vm, njs_value_arg(&arg), 1, retval); + + case NGX_JS_BODY_TEXT: + default: + return njs_vm_value_string_create(vm, retval, + ctx->body_read_data, + ctx->body_read_len); } +} - p = ngx_pnalloc(r->pool, len); - if (p == NULL) { + +static ngx_int_t +ngx_http_js_request_form(ngx_http_request_t *r, ngx_http_js_ctx_t *ctx, + ngx_uint_t max_keys, ngx_js_form_t **form, ngx_str_t *error) +{ + ngx_int_t rc; + ngx_str_t content_type; + + if (ctx->request_form != NULL) { + *form = ctx->request_form; + return NGX_OK; + } + + if (r->headers_in.content_type != NULL) { + content_type = r->headers_in.content_type->value; + + } else { + content_type.len = 0; + content_type.data = NULL; + } + + rc = ngx_js_parse_form(r->pool, &content_type, ctx->body_read_data, + ctx->body_read_len, max_keys, form, error); + if (rc != NGX_OK) { + return rc; + } + + ctx->request_form = *form; + + return NGX_OK; +} + + +static njs_int_t +ngx_http_js_request_form_make(njs_vm_t *vm, ngx_js_form_t *form, + njs_value_t *retval) +{ + njs_int_t rc; + + rc = njs_vm_external_create(vm, retval, ngx_http_js_request_form_proto_id, + form, 0); + if (rc != NJS_OK) { njs_vm_memory_error(vm); return NJS_ERROR; } - body = p; - cl = r->request_body->bufs; + return NJS_OK; +} - for ( /* void */ ; cl; cl = cl->next) { - buf = cl->buf; - p = ngx_cpymem(p, buf->pos, buf->last - buf->pos); + +static njs_int_t +ngx_http_js_form_to_value(njs_vm_t *vm, ngx_http_request_t *r, + ngx_http_js_ctx_t *ctx, ngx_uint_t max_keys, njs_value_t *retval) +{ + ngx_int_t rc; + ngx_str_t error; + ngx_js_form_t *form; + + rc = ngx_http_js_request_form(r, ctx, max_keys, &form, &error); + if (rc == NGX_OK) { + return ngx_http_js_request_form_make(vm, form, retval); } -done: + if (rc == NGX_JS_FORM_TYPE_ERROR) { + njs_vm_type_error(vm, "%V", &error); + return NJS_ERROR; + } + + if (rc == NGX_JS_FORM_PARSE_ERROR) { + njs_vm_error(vm, "%V", &error); + return NJS_ERROR; + } + + njs_vm_memory_error(vm); + + return NJS_ERROR; +} + + +static void +ngx_http_js_access_body_finalize(ngx_http_request_t *r, ngx_http_js_ctx_t *ctx, + ngx_int_t rc) +{ + switch (ngx_http_js_body_read_phase(ctx->body_read_state)) { + case NGX_HTTP_JS_BODY_READ_IN_PROGRESS: + ctx->body_read_state = NGX_HTTP_JS_BODY_READ_IDLE; + + if (ngx_js_ctx_pending(ctx)) { + r->write_event_handler = ngx_http_js_access_write_event_handler; + return; + } + + r->write_event_handler = ngx_http_core_run_phases; + ngx_http_core_run_phases(r); + break; + + case NGX_HTTP_JS_BODY_READ_DEFERRED: + /* + * Sync body read completion from the access handler. + * The promise is resolved/rejected but the access handler + * is still on the call stack -- do not run posted requests + * or resume phases here; the access handler will do it. + */ + break; + + case NGX_HTTP_JS_BODY_READ_IDLE: + default: + ngx_http_js_event_finalize(r, rc); + } +} + + +static ngx_int_t +ngx_http_js_body_resolve(ngx_http_js_ctx_t *ctx, void *event) +{ + njs_vm_t *vm; + njs_int_t rc; + ngx_js_event_t *ev; + njs_opaque_value_t result; + + ev = event; + vm = ctx->engine->u.njs.vm; + + if (ngx_http_js_body_read_is_form(ctx->body_read_state)) { + rc = ngx_http_js_form_to_value(vm, njs_vm_external(vm, + ngx_http_js_request_proto_id, + njs_value_arg(&ctx->args[0])), ctx, + (uintptr_t) ev->data, + njs_value_arg(&result)); + + } else { + rc = ngx_http_js_body_to_value(vm, ctx, (uintptr_t) ev->data, + njs_value_arg(&result)); + } + + if (rc != NJS_OK) { + njs_vm_exception_get(vm, njs_value_arg(&result)); + + rc = ngx_js_call(vm, njs_value_function(njs_value_arg(&ev->args[1])), + &result, 1); + + ngx_js_del_event(ctx, ev); + return rc; + } + + rc = ngx_js_call(vm, njs_value_function(njs_value_arg(&ev->function)), + &result, 1); + + ngx_js_del_event(ctx, ev); + + return rc; +} + + +static void +ngx_http_js_access_body_done(ngx_http_request_t *r) +{ + ngx_int_t rc; + ngx_http_js_ctx_t *ctx; + + ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http js body read done"); + + /* + * ngx_http_read_client_request_body() incremented count. + * For the IN_PROGRESS (async) path, ngx_http_finalize_request(NGX_DONE) + * already consumed it; for the DEFERRED (sync) path, we consume it here. + */ + if (ngx_http_js_body_read_is_deferred(ctx->body_read_state)) { + r->main->count--; + } + + if (ctx->body_read_event == NULL) { + return; + } + + if (ngx_http_js_collect_body(r, ctx) != NGX_OK) { + ngx_http_js_body_read_abort(ctx); + ngx_http_js_access_body_finalize(r, ctx, NGX_ERROR); + return; + } + +#if (NJS_HAVE_QUICKJS) + if (ctx->engine->type == NGX_ENGINE_QJS) { + rc = ngx_http_qjs_body_resolve(ctx, ctx->body_read_event); + } else +#endif + { + rc = ngx_http_js_body_resolve(ctx, ctx->body_read_event); + } + + ctx->body_read_event = NULL; + + ngx_http_js_access_body_finalize(r, ctx, rc); +} + + +static njs_int_t +ngx_http_js_ext_read_request_body(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t magic, njs_value_t *retval) +{ + ngx_int_t rc; + ngx_js_event_t *event; + ngx_http_js_ctx_t *ctx; + ngx_http_request_t *r; + + r = njs_vm_external(vm, ngx_http_js_request_proto_id, + njs_argument(args, 0)); + if (r == NULL) { + njs_vm_error(vm, "\"this\" is not an external"); + return NJS_ERROR; + } + + ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + + event = njs_mp_zalloc(njs_vm_memory_pool(vm), + sizeof(ngx_js_event_t) + + sizeof(njs_opaque_value_t) * 2); + if (njs_slow_path(event == NULL)) { + njs_vm_memory_error(vm); + return NJS_ERROR; + } + + /* + * r->request_body is set by ngx_http_read_client_request_body(). + * JS only runs after body reading completes, so non-NULL means + * the body is available. + */ + if (r->request_body) { + goto resolve; + } + + if (ctx->body_read_event) { + njs_vm_error(vm, "request body is already being read"); + return NJS_ERROR; + } + + event->fd = ctx->event_id++; + event->args = (njs_opaque_value_t *) &event[1]; + event->data = (void *) (uintptr_t) magic; + + rc = njs_vm_promise_create(vm, retval, njs_value_arg(event->args)); + if (rc != NJS_OK) { + return NJS_ERROR; + } + + njs_value_assign(&event->function, njs_value_arg(event->args)); + + ngx_js_add_event(ctx, event); + + ctx->body_read_event = event; + ctx->body_read_state = NGX_HTTP_JS_BODY_READ_DEFERRED; + + return NJS_OK; + +resolve: + + if (ngx_http_js_collect_body(r, ctx) != NGX_OK) { + njs_vm_memory_error(vm); + return NJS_ERROR; + } + + return ngx_http_js_body_to_value(vm, ctx, (ngx_uint_t) magic, retval); +} + + +static njs_int_t +ngx_http_js_request_form_max_keys(njs_vm_t *vm, njs_value_t *options, + ngx_uint_t *max_keys) +{ + ngx_int_t n; + njs_value_t *value; + njs_opaque_value_t lvalue; + + static const njs_str_t max_keys_name = njs_str("maxKeys"); + + *max_keys = NGX_JS_FORM_DEFAULT_MAX_KEYS; + + if (njs_value_is_undefined(options)) { + return NJS_OK; + } + + if (!njs_value_is_object(options)) { + njs_vm_type_error(vm, "\"options\" must be an object"); + return NJS_ERROR; + } + + value = njs_vm_object_prop(vm, options, &max_keys_name, &lvalue); + if (value == NULL) { + return NJS_ERROR; + } + + if (njs_value_is_undefined(value)) { + return NJS_OK; + } + + if (ngx_js_integer(vm, value, &n) != NGX_OK) { + return NJS_ERROR; + } + + if (n < 1) { + njs_vm_type_error(vm, "\"maxKeys\" must be a positive integer"); + return NJS_ERROR; + } + + *max_keys = n; + + return NJS_OK; +} + + +static njs_int_t +ngx_http_js_ext_read_request_form(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t unused, njs_value_t *retval) +{ + ngx_int_t rc; + ngx_uint_t max_keys; + ngx_js_event_t *event; + ngx_http_js_ctx_t *ctx; + ngx_http_request_t *r; + + r = njs_vm_external(vm, ngx_http_js_request_proto_id, + njs_argument(args, 0)); + if (r == NULL) { + njs_vm_error(vm, "\"this\" is not an external"); + return NJS_ERROR; + } + + if (ngx_http_js_request_form_max_keys(vm, njs_arg(args, nargs, 1), + &max_keys) + != NJS_OK) + { + return NJS_ERROR; + } + + ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + + if (r->request_body) { + goto resolve; + } + + if (ctx->body_read_event) { + njs_vm_error(vm, "request body is already being read"); + return NJS_ERROR; + } + + event = njs_mp_zalloc(njs_vm_memory_pool(vm), + sizeof(ngx_js_event_t) + + sizeof(njs_opaque_value_t) * 2); + if (event == NULL) { + njs_vm_memory_error(vm); + return NJS_ERROR; + } + + event->fd = ctx->event_id++; + event->args = (njs_opaque_value_t *) &event[1]; + event->data = (void *) (uintptr_t) max_keys; + + rc = njs_vm_promise_create(vm, retval, njs_value_arg(event->args)); + if (rc != NJS_OK) { + return NJS_ERROR; + } + + njs_value_assign(&event->function, njs_value_arg(event->args)); + + ngx_js_add_event(ctx, event); + + ctx->body_read_event = event; + ctx->body_read_state = NGX_HTTP_JS_BODY_READ_DEFERRED + | NGX_HTTP_JS_BODY_READ_FORM; + + return NJS_OK; + +resolve: + + if (ngx_http_js_collect_body(r, ctx) != NGX_OK) { + njs_vm_memory_error(vm); + return NJS_ERROR; + } + + return ngx_http_js_form_to_value(vm, r, ctx, max_keys, retval); +} + + +static njs_int_t +ngx_http_js_ext_request_form_get(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t as_array, njs_value_t *retval) +{ + njs_int_t rc; + njs_str_t name; + njs_value_t *value; + ngx_uint_t i; + ngx_js_form_t *form; + ngx_js_form_entry_t *entry; + + form = njs_vm_external(vm, ngx_http_js_request_form_proto_id, + njs_argument(args, 0)); + if (form == NULL) { + njs_vm_error(vm, "\"this\" is not a RequestForm"); + return NJS_ERROR; + } + + rc = ngx_js_string(vm, njs_arg(args, nargs, 1), &name); + if (rc != NJS_OK) { + njs_vm_type_error(vm, "\"name\" must be a string"); + return NJS_ERROR; + } + + if (as_array) { + rc = njs_vm_array_alloc(vm, retval, 4); + if (rc != NJS_OK) { + return NJS_ERROR; + } + } + + entry = form->entries.elts; + + for (i = 0; i < form->entries.nelts; i++) { + if (entry[i].is_file || entry[i].name.len != name.length + || ngx_memcmp(entry[i].name.data, name.start, name.length) != 0) + { + continue; + } + + if (!as_array) { + return njs_vm_value_string_create(vm, retval, entry[i].value.data, + entry[i].value.len); + } + + value = njs_vm_array_push(vm, retval); + if (value == NULL) { + return NJS_ERROR; + } + + rc = njs_vm_value_string_create(vm, value, entry[i].value.data, + entry[i].value.len); + if (rc != NJS_OK) { + return NJS_ERROR; + } + } + + if (!as_array) { + njs_value_null_set(retval); + } + + return NJS_OK; +} + + +static njs_int_t +ngx_http_js_ext_request_form_has(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t unused, njs_value_t *retval) +{ + njs_int_t rc; + njs_str_t name; + ngx_uint_t i; + ngx_js_form_t *form; + ngx_js_form_entry_t *entry; + + form = njs_vm_external(vm, ngx_http_js_request_form_proto_id, + njs_argument(args, 0)); + if (form == NULL) { + njs_vm_error(vm, "\"this\" is not a RequestForm"); + return NJS_ERROR; + } + + rc = ngx_js_string(vm, njs_arg(args, nargs, 1), &name); + if (rc != NJS_OK) { + njs_vm_type_error(vm, "\"name\" must be a string"); + return NJS_ERROR; + } + + entry = form->entries.elts; + + for (i = 0; i < form->entries.nelts; i++) { + if (!entry[i].is_file && entry[i].name.len == name.length + && ngx_memcmp(entry[i].name.data, name.start, name.length) == 0) + { + njs_value_boolean_set(retval, 1); + return NJS_OK; + } + } + + njs_value_boolean_set(retval, 0); + + return NJS_OK; +} + + +static njs_int_t +ngx_http_js_ext_request_form_for_each(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t unused, njs_value_t *retval) +{ + njs_int_t rc; + njs_value_t *callback, *this_arg, *this; + ngx_uint_t i; + ngx_js_form_t *form; + ngx_js_form_entry_t *entry; + njs_opaque_value_t arguments[4], result; + + this = njs_argument(args, 0); + + form = njs_vm_external(vm, ngx_http_js_request_form_proto_id, this); + if (form == NULL) { + njs_vm_error(vm, "\"this\" is not a RequestForm"); + return NJS_ERROR; + } + + callback = njs_arg(args, nargs, 1); + if (!njs_value_is_function(callback)) { + njs_vm_error(vm, "\"callback\" is not a function"); + return NJS_ERROR; + } + + this_arg = njs_arg(args, nargs, 2); + if (this_arg == NULL) { + this_arg = njs_value_arg(&njs_value_undefined); + } + + entry = form->entries.elts; + + for (i = 0; i < form->entries.nelts; i++) { + if (entry[i].is_file) { + continue; + } + + njs_value_assign(&arguments[0], this_arg); + + rc = njs_vm_value_string_create(vm, njs_value_arg(&arguments[1]), + entry[i].value.data, + entry[i].value.len); + if (rc != NJS_OK) { + return NJS_ERROR; + } + + rc = njs_vm_value_string_create(vm, njs_value_arg(&arguments[2]), + entry[i].name.data, + entry[i].name.len); + if (rc != NJS_OK) { + return NJS_ERROR; + } + + njs_value_assign(&arguments[3], this); + + rc = njs_vm_invoke(vm, njs_value_function(callback), + njs_value_arg(&arguments[1]), 3, + njs_value_arg(&result)); + if (rc != NJS_OK) { + return NJS_ERROR; + } + } + + njs_value_undefined_set(retval); + + return NJS_OK; +} + + +static njs_int_t +ngx_http_js_ext_request_form_has_files(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t unused, njs_value_t *retval) +{ + ngx_js_form_t *form; + + form = njs_vm_external(vm, ngx_http_js_request_form_proto_id, + njs_argument(args, 0)); + if (form == NULL) { + njs_vm_error(vm, "\"this\" is not a RequestForm"); + return NJS_ERROR; + } + + njs_value_boolean_set(retval, form->has_files); + + return NJS_OK; +} + + +static njs_int_t +ngx_http_js_ext_request_form_file_field_names(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t unused, njs_value_t *retval) +{ + njs_int_t rc; + ngx_uint_t i; + njs_value_t *value; + ngx_js_form_t *form; + ngx_js_form_entry_t *entry; + + form = njs_vm_external(vm, ngx_http_js_request_form_proto_id, + njs_argument(args, 0)); + if (form == NULL) { + njs_vm_error(vm, "\"this\" is not a RequestForm"); + return NJS_ERROR; + } + + rc = njs_vm_array_alloc(vm, retval, 4); + if (rc != NJS_OK) { + return NJS_ERROR; + } + + entry = form->entries.elts; + + for (i = 0; i < form->entries.nelts; i++) { + if (!entry[i].is_file) { + continue; + } + + value = njs_vm_array_push(vm, retval); + if (value == NULL) { + return NJS_ERROR; + } - ret = ngx_js_prop(vm, buffer_type, request_body, body, len); - if (ret != NJS_OK) { - return NJS_ERROR; + rc = njs_vm_value_string_create(vm, value, entry[i].name.data, + entry[i].name.len); + if (rc != NJS_OK) { + return NJS_ERROR; + } } - njs_value_assign(retval, request_body); - return NJS_OK; } @@ -4763,6 +5904,13 @@ ngx_js_http_init(njs_vm_t *vm) return NJS_ERROR; } + ngx_http_js_request_form_proto_id = njs_vm_external_prototype(vm, + ngx_http_js_ext_request_form, + njs_nitems(ngx_http_js_ext_request_form)); + if (ngx_http_js_request_form_proto_id < 0) { + return NJS_ERROR; + } + ngx_http_js_periodic_session_proto_id = njs_vm_external_prototype(vm, ngx_http_js_ext_periodic_session, njs_nitems(ngx_http_js_ext_periodic_session)); @@ -5258,52 +6406,385 @@ ngx_http_qjs_ext_periodic_variables(JSContext *cx, static JSValue -ngx_http_qjs_ext_parent(JSContext *cx, JSValueConst this_val) +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_QJS_CLASS_ID_HTTP_REQUEST); + 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) +{ + JSValue body; + uint32_t buffer_type; + ngx_http_request_t *r; + ngx_http_js_ctx_t *ctx; + ngx_http_qjs_request_t *req; + + req = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_REQUEST); + 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); + } + + JS_FreeValue(cx, req->request_body); + } + + r = req->request; + + if (r->request_body == NULL || r->request_body->bufs == NULL) { + return JS_UNDEFINED; + } + + ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + + if (ngx_http_js_collect_body(r, ctx) != NGX_OK) { + return JS_ThrowInternalError(cx, "failed to read request body"); + } + + body = ngx_qjs_prop(cx, buffer_type, ctx->body_read_data, + ctx->body_read_len); + if (JS_IsException(body)) { + return JS_EXCEPTION; + } + + req->request_body = body; + + return JS_DupValue(cx, req->request_body); +} + + +static JSValue +ngx_http_qjs_body_to_value(JSContext *cx, ngx_http_js_ctx_t *ctx, + ngx_uint_t type) +{ + JSValue str; + const char *cstr; + + switch (type) { + case NGX_JS_BODY_ARRAY_BUFFER: + return JS_NewArrayBuffer(cx, ctx->body_read_data, + ctx->body_read_len, NULL, NULL, 0); + + case NGX_JS_BODY_JSON: + if (ctx->body_read_nul) { + return JS_ParseJSON(cx, (const char *) ctx->body_read_data, + ctx->body_read_len, ""); + } + + str = qjs_string_create(cx, ctx->body_read_data, + ctx->body_read_len); + if (JS_IsException(str)) { + return str; + } + + cstr = JS_ToCString(cx, str); + JS_FreeValue(cx, str); + + if (cstr == NULL) { + return JS_EXCEPTION; + } + + str = JS_ParseJSON(cx, cstr, ctx->body_read_len, ""); + JS_FreeCString(cx, cstr); + + return str; + + case NGX_JS_BODY_TEXT: + default: + return qjs_string_create(cx, ctx->body_read_data, + ctx->body_read_len); + } +} + + +static ngx_int_t +ngx_http_qjs_body_resolve(ngx_http_js_ctx_t *ctx, void *event) +{ + JSValue result; + JSContext *cx; + ngx_int_t rc; + ngx_qjs_event_t *ev; + + ev = event; + cx = ctx->engine->u.qjs.ctx; + + if (ngx_http_js_body_read_is_form(ctx->body_read_state)) { + result = ngx_http_qjs_form_to_value(cx, + ngx_http_qjs_request(ngx_qjs_arg(ctx->args[0])), + ctx, (uintptr_t) ev->data); + + } else { + result = ngx_http_qjs_body_to_value(cx, ctx, (uintptr_t) ev->data); + } + + if (JS_IsException(result)) { + result = JS_GetException(cx); + + rc = ngx_qjs_call(cx, ev->args[1], &result, 1); + + JS_FreeValue(cx, result); + ngx_js_del_event(ctx, ev); + return rc; + } + + rc = ngx_qjs_call(cx, ev->function, &result, 1); + + JS_FreeValue(cx, result); + ngx_js_del_event(ctx, ev); + + return rc; +} + + +static JSValue +ngx_http_qjs_ext_read_request_body(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv, int magic) +{ + JSValue retval; + ngx_qjs_event_t *event; + ngx_http_js_ctx_t *ctx; + ngx_http_request_t *r; + ngx_http_qjs_request_t *req; + + req = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_REQUEST); + if (req == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + r = req->request; + ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + + if (r->request_body) { + goto resolve; + } + + if (ctx->body_read_event) { + return JS_ThrowInternalError(cx, "request body is already being read"); + } + + event = ngx_pcalloc(r->pool, sizeof(ngx_qjs_event_t) + sizeof(JSValue) * 2); + if (event == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + event->ctx = cx; + event->fd = ctx->event_id++; + event->args = (JSValue *) &event[1]; + event->data = (void *) (uintptr_t) magic; + + retval = JS_NewPromiseCapability(cx, &event->args[0]); + if (JS_IsException(retval)) { + return JS_EXCEPTION; + } + + event->function = JS_DupValue(cx, event->args[0]); + event->destructor = ngx_http_js_read_body_event_destructor; + + ngx_js_add_event(ctx, event); + + ctx->body_read_event = event; + ctx->body_read_state = NGX_HTTP_JS_BODY_READ_DEFERRED; + + return retval; + +resolve: + + if (ngx_http_js_collect_body(r, ctx) != NGX_OK) { + return JS_ThrowOutOfMemory(cx); + } + + return ngx_http_qjs_body_to_value(cx, ctx, (ngx_uint_t) magic); +} + + +static ngx_int_t +ngx_http_qjs_request_form_max_keys(JSContext *cx, JSValueConst options, + ngx_uint_t *max_keys) +{ + JSValue value; + ngx_int_t n; + + *max_keys = NGX_JS_FORM_DEFAULT_MAX_KEYS; + + if (JS_IsUndefined(options)) { + return NGX_OK; + } + + if (!JS_IsObject(options)) { + JS_ThrowTypeError(cx, "\"options\" must be an object"); + return NGX_ERROR; + } + + value = JS_GetPropertyStr(cx, options, "maxKeys"); + if (JS_IsException(value)) { + return NGX_ERROR; + } + + if (JS_IsUndefined(value)) { + JS_FreeValue(cx, value); + return NGX_OK; + } + + if (ngx_qjs_integer(cx, value, &n) != NGX_OK) { + JS_FreeValue(cx, value); + return NGX_ERROR; + } + + JS_FreeValue(cx, value); + + if (n < 1) { + JS_ThrowTypeError(cx, "\"maxKeys\" must be a positive integer"); + return NGX_ERROR; + } + + *max_keys = n; + + return NGX_OK; +} + + +static JSValue +ngx_http_qjs_request_form_make(JSContext *cx, ngx_js_form_t *form) { - ngx_http_js_ctx_t *ctx; - ngx_http_request_t *r; + JSValue obj; - r = ngx_http_qjs_request(this_val); - if (r == NULL) { - return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + obj = JS_NewObjectClass(cx, NGX_QJS_CLASS_ID_HTTP_FORM); + if (JS_IsException(obj)) { + return JS_EXCEPTION; } - ctx = r->parent ? ngx_http_get_module_ctx(r->parent, ngx_http_js_module) - : NULL; - - if (ctx == NULL) { - return JS_UNDEFINED; - } + JS_SetOpaque(obj, form); - return JS_DupValue(cx, ngx_qjs_arg(ctx->args[0])); + return obj; } static JSValue -ngx_http_qjs_ext_remote_address(JSContext *cx, JSValueConst this_val) +ngx_http_qjs_form_to_value(JSContext *cx, ngx_http_request_t *r, + ngx_http_js_ctx_t *ctx, ngx_uint_t max_keys) { - ngx_connection_t *c; - ngx_http_request_t *r; + ngx_int_t rc; + ngx_str_t error; + ngx_js_form_t *form; - r = ngx_http_qjs_request(this_val); - if (r == NULL) { - return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + rc = ngx_http_js_request_form(r, ctx, max_keys, &form, &error); + if (rc == NGX_OK) { + return ngx_http_qjs_request_form_make(cx, form); } - c = r->connection; + if (rc == NGX_JS_FORM_TYPE_ERROR) { + return JS_ThrowTypeError(cx, "%.*s", (int) error.len, error.data); + } - return qjs_string_create(cx, c->addr_text.data, c->addr_text.len); + if (rc == NGX_JS_FORM_PARSE_ERROR) { + return JS_ThrowInternalError(cx, "%.*s", (int) error.len, error.data); + } + + return JS_ThrowOutOfMemory(cx); } static JSValue -ngx_http_qjs_ext_response_body(JSContext *cx, JSValueConst this_val, int type) +ngx_http_qjs_ext_read_request_form(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) { - u_char *p; - size_t len; - uint32_t buffer_type; - ngx_buf_t *b; - JSValue body; + JSValue retval; + ngx_uint_t max_keys; + ngx_qjs_event_t *event; + ngx_http_js_ctx_t *ctx; ngx_http_request_t *r; ngx_http_qjs_request_t *req; @@ -5312,142 +6793,295 @@ ngx_http_qjs_ext_response_body(JSContext *cx, JSValueConst this_val, int type) 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); - } + if (ngx_http_qjs_request_form_max_keys(cx, argv[0], &max_keys) != NGX_OK) { + return JS_EXCEPTION; } r = req->request; + ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); - b = r->out ? r->out->buf : NULL; - - if (b == NULL) { - return JS_UNDEFINED; + if (r->request_body) { + goto resolve; } - len = b->last - b->pos; + if (ctx->body_read_event) { + return JS_ThrowInternalError(cx, "request body is already being read"); + } - p = ngx_pnalloc(r->pool, len); - if (p == NULL) { + event = ngx_pcalloc(r->pool, sizeof(ngx_qjs_event_t) + sizeof(JSValue) * 2); + if (event == NULL) { return JS_ThrowOutOfMemory(cx); } - if (len) { - ngx_memcpy(p, b->pos, len); - } + event->ctx = cx; + event->fd = ctx->event_id++; + event->args = (JSValue *) &event[1]; + event->data = (void *) (uintptr_t) max_keys; - body = ngx_qjs_prop(cx, buffer_type, p, len); - if (JS_IsException(body)) { + retval = JS_NewPromiseCapability(cx, &event->args[0]); + if (JS_IsException(retval)) { return JS_EXCEPTION; } - req->response_body = body; + event->function = JS_DupValue(cx, event->args[0]); + event->destructor = ngx_http_js_read_body_event_destructor; - return JS_DupValue(cx, req->response_body); + ngx_js_add_event(ctx, event); + + ctx->body_read_event = event; + ctx->body_read_state = NGX_HTTP_JS_BODY_READ_DEFERRED + | NGX_HTTP_JS_BODY_READ_FORM; + + return retval; + +resolve: + + if (ngx_http_js_collect_body(r, ctx) != NGX_OK) { + return JS_ThrowOutOfMemory(cx); + } + + return ngx_http_qjs_form_to_value(cx, r, ctx, max_keys); } static JSValue -ngx_http_qjs_ext_request_body(JSContext *cx, JSValueConst this_val, int type) +ngx_http_qjs_ext_request_form_get(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv, int as_array) { - u_char *p, *data; - size_t len; - ssize_t n; - 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; + JSValue array, value; + size_t name_len; + const char *name; + ngx_uint_t i, n; + ngx_js_form_t *form; + ngx_js_form_entry_t *entry; - req = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_REQUEST); - if (req == NULL) { - return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + form = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_FORM); + if (form == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a RequestForm"); } - buffer_type = ngx_js_buffer_type(type); + name = JS_ToCStringLen(cx, &name_len, argv[0]); + if (name == NULL) { + return JS_ThrowTypeError(cx, "\"name\" must be a string"); + } - if (!JS_IsUndefined(req->request_body)) { - if ((buffer_type == NGX_JS_STRING) == JS_IsString(req->request_body)) { - return JS_DupValue(cx, req->request_body); + if (as_array) { + array = JS_NewArray(cx); + if (JS_IsException(array)) { + JS_FreeCString(cx, name); + return JS_EXCEPTION; } - JS_FreeValue(cx, req->request_body); + } else { + array = JS_UNDEFINED; } - r = req->request; + entry = form->entries.elts; + n = 0; - if (r->request_body == NULL || r->request_body->bufs == NULL) { - return JS_UNDEFINED; + for (i = 0; i < form->entries.nelts; i++) { + if (entry[i].is_file || entry[i].name.len != name_len + || ngx_memcmp(entry[i].name.data, name, name_len) != 0) + { + continue; + } + + value = qjs_string_create(cx, entry[i].value.data, entry[i].value.len); + if (JS_IsException(value)) { + JS_FreeCString(cx, name); + JS_FreeValue(cx, array); + return JS_EXCEPTION; + } + + if (!as_array) { + JS_FreeCString(cx, name); + return value; + } + + if (JS_DefinePropertyValueUint32(cx, array, n++, value, JS_PROP_C_W_E) + < 0) + { + JS_FreeValue(cx, value); + JS_FreeCString(cx, name); + JS_FreeValue(cx, array); + return JS_EXCEPTION; + } } - cl = r->request_body->bufs; - buf = cl->buf; + JS_FreeCString(cx, name); - if (r->request_body->temp_file) { - ngx_log_error(NGX_LOG_WARN, r->connection->log, 0, - "http js reading request body from a temporary file"); + if (as_array) { + return array; + } - if (buf == NULL || !buf->in_file) { - return JS_ThrowInternalError(cx, "cannot find body file"); + return JS_NULL; +} + + +static JSValue +ngx_http_qjs_ext_request_form_has(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + size_t name_len; + const char *name; + ngx_uint_t i; + ngx_js_form_t *form; + ngx_js_form_entry_t *entry; + + form = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_FORM); + if (form == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a RequestForm"); + } + + name = JS_ToCStringLen(cx, &name_len, argv[0]); + if (name == NULL) { + return JS_ThrowTypeError(cx, "\"name\" must be a string"); + } + + entry = form->entries.elts; + + for (i = 0; i < form->entries.nelts; i++) { + if (!entry[i].is_file && entry[i].name.len == name_len + && ngx_memcmp(entry[i].name.data, name, name_len) == 0) + { + JS_FreeCString(cx, name); + return JS_NewBool(cx, 1); } + } - len = buf->file_last - buf->file_pos; + JS_FreeCString(cx, name); - data = ngx_pnalloc(r->pool, len); - if (data == NULL) { - return JS_ThrowOutOfMemory(cx); + return JS_NewBool(cx, 0); +} + + +static JSValue +ngx_http_qjs_ext_request_form_for_each(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + JSValue args[3], ret; + ngx_uint_t i; + ngx_js_form_t *form; + ngx_js_form_entry_t *entry; + + form = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_FORM); + if (form == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a RequestForm"); + } + + if (!JS_IsFunction(cx, argv[0])) { + return JS_ThrowTypeError(cx, "\"callback\" is not a function"); + } + + entry = form->entries.elts; + + for (i = 0; i < form->entries.nelts; i++) { + if (entry[i].is_file) { + continue; } - n = ngx_read_file(buf->file, data, len, buf->file_pos); - if (n != (ssize_t) len) { - return JS_ThrowInternalError(cx, "failed to read request body"); + args[0] = qjs_string_create(cx, entry[i].value.data, + entry[i].value.len); + if (JS_IsException(args[0])) { + return JS_EXCEPTION; + } + + args[1] = qjs_string_create(cx, entry[i].name.data, entry[i].name.len); + if (JS_IsException(args[1])) { + JS_FreeValue(cx, args[0]); + return JS_EXCEPTION; } - goto done; + args[2] = JS_DupValue(cx, this_val); + + ret = JS_Call(cx, argv[0], argc > 1 ? argv[1] : JS_UNDEFINED, 3, args); + + JS_FreeValue(cx, args[0]); + JS_FreeValue(cx, args[1]); + JS_FreeValue(cx, args[2]); + + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + + JS_FreeValue(cx, ret); } - if (cl->next == NULL) { - len = buf->last - buf->pos; - data = buf->pos; + return JS_UNDEFINED; +} + + +static JSValue +ngx_http_qjs_ext_request_form_has_files(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + ngx_js_form_t *form; - goto done; + form = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_FORM); + if (form == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a RequestForm"); } - len = buf->last - buf->pos; - cl = cl->next; + return JS_NewBool(cx, form->has_files); +} + - for ( /* void */ ; cl; cl = cl->next) { - buf = cl->buf; - len += buf->last - buf->pos; +static JSValue +ngx_http_qjs_ext_request_form_file_field_names(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv) +{ + JSValue array, value; + ngx_uint_t i, n; + ngx_js_form_t *form; + ngx_js_form_entry_t *entry; + + form = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_FORM); + if (form == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a RequestForm"); } - p = ngx_pnalloc(r->pool, len); - if (p == NULL) { - return JS_ThrowOutOfMemory(cx); + array = JS_NewArray(cx); + if (JS_IsException(array)) { + return JS_EXCEPTION; } - data = p; - cl = r->request_body->bufs; + entry = form->entries.elts; + n = 0; - for ( /* void */ ; cl; cl = cl->next) { - buf = cl->buf; - p = ngx_cpymem(p, buf->pos, buf->last - buf->pos); - } + for (i = 0; i < form->entries.nelts; i++) { + if (!entry[i].is_file) { + continue; + } -done: + value = qjs_string_create(cx, entry[i].name.data, entry[i].name.len); + if (JS_IsException(value)) { + JS_FreeValue(cx, array); + return JS_EXCEPTION; + } - body = ngx_qjs_prop(cx, buffer_type, data, len); - if (JS_IsException(body)) { - return JS_EXCEPTION; + if (JS_DefinePropertyValueUint32(cx, array, n++, value, JS_PROP_C_W_E) + < 0) + { + JS_FreeValue(cx, value); + JS_FreeValue(cx, array); + return JS_EXCEPTION; + } } - req->request_body = body; + return array; +} - return JS_DupValue(cx, req->request_body); + +static void +ngx_http_js_read_body_event_destructor(ngx_qjs_event_t *event) +{ + JSContext *cx; + + cx = event->ctx; + + JS_FreeValue(cx, event->function); + JS_FreeValue(cx, event->args[0]); + JS_FreeValue(cx, event->args[1]); } @@ -5502,6 +7136,26 @@ ngx_http_qjs_ext_return(JSContext *cx, JSValueConst this_val, } +static JSValue +ngx_http_qjs_ext_decline(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + ngx_http_js_ctx_t *ctx; + ngx_http_request_t *r; + + r = ngx_http_qjs_request(this_val); + if (r == NULL) { + return JS_ThrowInternalError(cx, "\"this\" is not a request object"); + } + + ctx = ngx_http_get_module_ctx(r, ngx_http_js_module); + + ctx->status = NGX_DECLINED; + + return JS_UNDEFINED; +} + + static JSValue ngx_http_qjs_ext_status_get(JSContext *cx, JSValueConst this_val) { @@ -7679,6 +9333,22 @@ ngx_engine_qjs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, JS_SetClassProto(cx, NGX_QJS_CLASS_ID_HTTP_REQUEST, proto); + if (JS_NewClass(JS_GetRuntime(cx), NGX_QJS_CLASS_ID_HTTP_FORM, + &ngx_http_qjs_request_form_class) < 0) + { + return NULL; + } + + proto = JS_NewObject(cx); + if (JS_IsException(proto)) { + return NULL; + } + + JS_SetPropertyFunctionList(cx, proto, ngx_http_qjs_ext_request_form, + njs_nitems(ngx_http_qjs_ext_request_form)); + + JS_SetClassProto(cx, NGX_QJS_CLASS_ID_HTTP_FORM, proto); + if (JS_NewClass(JS_GetRuntime(cx), NGX_QJS_CLASS_ID_HTTP_PERIODIC, &ngx_http_qjs_periodic_class) < 0) { @@ -7773,12 +9443,24 @@ ngx_http_js_init_conf_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf) static ngx_int_t ngx_http_js_init(ngx_conf_t *cf) { + ngx_http_handler_pt *h; + ngx_http_core_main_conf_t *cmcf; + ngx_http_next_header_filter = ngx_http_top_header_filter; ngx_http_top_header_filter = ngx_http_js_header_filter; ngx_http_next_body_filter = ngx_http_top_body_filter; ngx_http_top_body_filter = ngx_http_js_body_filter; + cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module); + + h = ngx_array_push(&cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers); + if (h == NULL) { + return NGX_ERROR; + } + + *h = ngx_http_js_access_handler; + return NGX_OK; } @@ -8156,6 +9838,24 @@ ngx_http_js_var(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) } +static char * +ngx_http_js_access(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_http_js_loc_conf_t *jlcf = conf; + + ngx_str_t *value; + + if (jlcf->access.data) { + return "is duplicate"; + } + + value = cf->args->elts; + jlcf->access = value[1]; + + return NGX_CONF_OK; +} + + static char * ngx_http_js_content(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { @@ -8275,6 +9975,7 @@ ngx_http_js_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) ngx_http_js_loc_conf_t *prev = parent; ngx_http_js_loc_conf_t *conf = child; + ngx_conf_merge_str_value(conf->access, prev->access, ""); ngx_conf_merge_str_value(conf->content, prev->content, ""); ngx_conf_merge_str_value(conf->header_filter, prev->header_filter, ""); ngx_conf_merge_str_value(conf->body_filter, prev->body_filter, ""); @@ -8287,6 +9988,15 @@ ngx_http_js_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) return NGX_CONF_ERROR; } + if (conf->access.len != 0) { + if (conf->imports == NGX_CONF_UNSET_PTR) { + ngx_log_error(NGX_LOG_EMERG, cf->log, 0, + "no imports defined for \"js_access\" \"%V\", " + "use \"js_import\" directive", &conf->access); + return NGX_CONF_ERROR; + } + } + if (conf->content.len != 0) { if (conf->imports == NGX_CONF_UNSET_PTR) { ngx_log_error(NGX_LOG_EMERG, cf->log, 0, diff --git a/nginx/ngx_js.h b/nginx/ngx_js.h index 6012e7fd9..c6cc8a3e7 100644 --- a/nginx/ngx_js.h +++ b/nginx/ngx_js.h @@ -32,6 +32,10 @@ #define NGX_JS_BOOLEAN 8 #define NGX_JS_NUMBER 16 +#define NGX_JS_BODY_ARRAY_BUFFER 0 +#define NGX_JS_BODY_JSON 1 +#define NGX_JS_BODY_TEXT 2 + #define NGX_JS_BOOL_FALSE 0 #define NGX_JS_BOOL_TRUE 1 #define NGX_JS_BOOL_UNSET 2 @@ -52,6 +56,7 @@ enum { NGX_QJS_CLASS_ID_CONSOLE = QJS_CORE_CLASS_ID_LAST, NGX_QJS_CLASS_ID_HTTP_REQUEST, + NGX_QJS_CLASS_ID_HTTP_FORM, NGX_QJS_CLASS_ID_HTTP_PERIODIC, NGX_QJS_CLASS_ID_HTTP_VARS, NGX_QJS_CLASS_ID_HTTP_HEADERS_IN, diff --git a/nginx/ngx_js_fetch.c b/nginx/ngx_js_fetch.c index fd40a5bad..608af25ba 100644 --- a/nginx/ngx_js_fetch.c +++ b/nginx/ngx_js_fetch.c @@ -268,9 +268,6 @@ static njs_external_t ngx_js_ext_http_request[] = { .enumerable = 1, .u.method = { .native = ngx_request_js_ext_body, -#define NGX_JS_BODY_ARRAY_BUFFER 0 -#define NGX_JS_BODY_JSON 1 -#define NGX_JS_BODY_TEXT 2 .magic8 = NGX_JS_BODY_ARRAY_BUFFER } }, diff --git a/nginx/ngx_js_form.c b/nginx/ngx_js_form.c new file mode 100644 index 000000000..fd685961d --- /dev/null +++ b/nginx/ngx_js_form.c @@ -0,0 +1,842 @@ +/* + * Copyright (C) Dmitry Volyntsev + * Copyright (C) F5, Inc. + */ + + +#include +#include +#include "ngx_js_form.h" + + +#define NGX_JS_FORM_URLENCODED 1 +#define NGX_JS_FORM_MULTIPART 2 + +/* + * RFC 2046, section 5.1.1 limits boundary to 70 characters; we allow up + * to 200 to tolerate non-conforming clients while bounding allocation. + */ +#define NGX_JS_FORM_MAX_BOUNDARY_LEN 200 +#define NGX_JS_FORM_MAX_PART_HEADERS 32 +#define NGX_JS_FORM_MAX_PART_HEADER_LINE 4096 +#define NGX_JS_FORM_MAX_PART_HEADER_SIZE 16384 + + +typedef struct { + ngx_str_t boundary; + ngx_uint_t type; +} ngx_js_form_content_type_t; + + +static ngx_int_t ngx_js_form_parse_content_type(ngx_pool_t *pool, + ngx_str_t *content_type, ngx_js_form_content_type_t *ct, + ngx_str_t *error); +static ngx_int_t ngx_js_form_parse_urlencoded(ngx_pool_t *pool, u_char *body, + size_t len, ngx_uint_t max_keys, ngx_js_form_t *form, ngx_str_t *error); +static ngx_int_t ngx_js_form_parse_multipart(ngx_pool_t *pool, u_char *body, + size_t len, ngx_str_t *boundary, ngx_uint_t max_keys, ngx_js_form_t *form, + ngx_str_t *error); +static ngx_int_t ngx_js_form_add_entry(ngx_js_form_t *form, + ngx_pool_t *pool, ngx_str_t *name, ngx_str_t *value, ngx_uint_t *count, + ngx_uint_t max_keys, ngx_flag_t is_file, ngx_str_t *error); +static ngx_int_t ngx_js_form_decode_urlencoded(ngx_pool_t *pool, u_char *start, + u_char *end, ngx_str_t *dst, ngx_str_t *error); +static ngx_int_t ngx_js_form_copy(ngx_pool_t *pool, u_char *start, + u_char *end, ngx_str_t *dst); +static ngx_int_t ngx_js_form_copy_quoted(ngx_pool_t *pool, u_char *start, + u_char *end, ngx_str_t *dst); +static void ngx_js_form_error(ngx_str_t *error, const char *text); +static u_char *ngx_js_form_skip_ows(u_char *p, u_char *end); +static u_char *ngx_js_form_find(u_char *start, u_char *end, u_char *pattern, + size_t len); +static ngx_uint_t ngx_js_form_is_ows(u_char ch); +static ngx_int_t ngx_js_form_parse_part_headers(ngx_pool_t *pool, + u_char *start, u_char *end, ngx_str_t *name, ngx_flag_t *is_file, + ngx_str_t *error); +static ngx_int_t ngx_js_form_parse_disposition(ngx_pool_t *pool, + ngx_str_t *value, ngx_str_t *name, ngx_flag_t *is_file, ngx_str_t *error); +static ngx_int_t ngx_js_form_parse_param(ngx_pool_t *pool, u_char **pp, + u_char *end, ngx_str_t *param, ngx_str_t *value, ngx_flag_t *quoted, + ngx_str_t *error); + + +ngx_int_t +ngx_js_parse_form(ngx_pool_t *pool, ngx_str_t *content_type, u_char *body, + size_t len, ngx_uint_t max_keys, ngx_js_form_t **form, + ngx_str_t *error) +{ + ngx_int_t rc; + ngx_js_form_t *f; + ngx_js_form_content_type_t ct; + + rc = ngx_js_form_parse_content_type(pool, content_type, &ct, error); + if (rc != NGX_JS_FORM_OK) { + return rc; + } + + f = ngx_pcalloc(pool, sizeof(ngx_js_form_t)); + if (f == NULL) { + return NGX_ERROR; + } + + if (ngx_array_init(&f->entries, pool, 4, sizeof(ngx_js_form_entry_t)) + != NGX_OK) + { + return NGX_ERROR; + } + + switch (ct.type) { + case NGX_JS_FORM_URLENCODED: + rc = ngx_js_form_parse_urlencoded(pool, body, len, max_keys, f, error); + break; + + case NGX_JS_FORM_MULTIPART: + rc = ngx_js_form_parse_multipart(pool, body, len, &ct.boundary, + max_keys, f, error); + break; + + default: + ngx_js_form_error(error, "unsupported content type"); + return NGX_JS_FORM_TYPE_ERROR; + } + + if (rc != NGX_JS_FORM_OK) { + return rc; + } + + *form = f; + + return NGX_JS_FORM_OK; +} + + +static ngx_int_t +ngx_js_form_parse_content_type(ngx_pool_t *pool, ngx_str_t *content_type, + ngx_js_form_content_type_t *ct, ngx_str_t *error) +{ + u_char *p, *end, *last, *value_start; + ngx_str_t param, value; + ngx_flag_t quoted; + + if (content_type == NULL || content_type->len == 0) { + ngx_js_form_error(error, "request content type is required"); + return NGX_JS_FORM_TYPE_ERROR; + } + + ct->type = 0; + ct->boundary.len = 0; + ct->boundary.data = NULL; + + p = content_type->data; + end = p + content_type->len; + + last = p; + + while (last < end && *last != ';') { + last++; + } + + value_start = ngx_js_form_skip_ows(p, last); + p = last; + + while (last > value_start && ngx_js_form_is_ows(last[-1])) { + last--; + } + + if ((size_t) (last - value_start) + == sizeof("application/x-www-form-urlencoded") - 1 + && ngx_strncasecmp(value_start, + (u_char *) "application/x-www-form-urlencoded", + last - value_start) + == 0) + { + ct->type = NGX_JS_FORM_URLENCODED; + } + + if ((size_t) (last - value_start) == sizeof("multipart/form-data") - 1 + && ngx_strncasecmp(value_start, (u_char *) "multipart/form-data", + last - value_start) + == 0) + { + ct->type = NGX_JS_FORM_MULTIPART; + } + + if (ct->type == 0) { + ngx_js_form_error(error, "unsupported content type"); + return NGX_JS_FORM_TYPE_ERROR; + } + + while (p < end) { + if (*p++ != ';') { + ngx_js_form_error(error, "malformed content type"); + return NGX_JS_FORM_PARSE_ERROR; + } + + p = ngx_js_form_skip_ows(p, end); + + if (ngx_js_form_parse_param(pool, &p, end, ¶m, &value, "ed, + error) + != NGX_JS_FORM_OK) + { + return NGX_JS_FORM_PARSE_ERROR; + } + + if (param.len == sizeof("boundary") - 1 + && ngx_strncasecmp(param.data, (u_char *) "boundary", param.len) + == 0) + { + if (ct->boundary.data != NULL) { + ngx_js_form_error(error, "duplicate boundary parameter"); + return NGX_JS_FORM_PARSE_ERROR; + } + + if (value.len == 0 || value.len > NGX_JS_FORM_MAX_BOUNDARY_LEN) { + ngx_js_form_error(error, "invalid multipart boundary"); + return NGX_JS_FORM_PARSE_ERROR; + } + + ct->boundary = value; + } + + p = ngx_js_form_skip_ows(p, end); + + if (p == end) { + break; + } + } + + if (ct->type == NGX_JS_FORM_MULTIPART && ct->boundary.data == NULL) { + ngx_js_form_error(error, "multipart boundary is required"); + return NGX_JS_FORM_TYPE_ERROR; + } + + return NGX_JS_FORM_OK; +} + + +static ngx_int_t +ngx_js_form_parse_urlencoded(ngx_pool_t *pool, u_char *body, size_t len, + ngx_uint_t max_keys, ngx_js_form_t *form, ngx_str_t *error) +{ + u_char *p, *end, *amp, *eq; + ngx_str_t name, value; + ngx_uint_t count; + + count = 0; + p = body; + end = body + len; + + if (len == 0) { + return NGX_JS_FORM_OK; + } + + while (p < end) { + if (*p == '&') { + p++; + continue; + } + + amp = p; + + while (amp < end && *amp != '&') { + amp++; + } + + eq = p; + + while (eq < amp && *eq != '=') { + eq++; + } + + if (ngx_js_form_decode_urlencoded(pool, p, eq, &name, error) + != NGX_JS_FORM_OK) + { + return NGX_JS_FORM_PARSE_ERROR; + } + + if (eq < amp) { + eq++; + } + + if (ngx_js_form_decode_urlencoded(pool, eq, amp, &value, error) + != NGX_JS_FORM_OK) + { + return NGX_JS_FORM_PARSE_ERROR; + } + + if (ngx_js_form_add_entry(form, pool, &name, &value, &count, max_keys, + 0, error) + != NGX_JS_FORM_OK) + { + return NGX_JS_FORM_PARSE_ERROR; + } + + if (amp == end) { + break; + } + + p = amp + 1; + } + + return NGX_JS_FORM_OK; +} + + +static ngx_int_t +ngx_js_form_parse_multipart(ngx_pool_t *pool, u_char *body, size_t len, + ngx_str_t *boundary, ngx_uint_t max_keys, ngx_js_form_t *form, + ngx_str_t *error) +{ + size_t dlen, cdlen; + u_char *p, *end, *marker, *next, *headers_end, *part_end, *scan; + u_char *delimiter; + ngx_str_t name, value; + ngx_uint_t count; + ngx_flag_t is_file; + + count = 0; + end = body + len; + dlen = boundary->len + 2; + cdlen = boundary->len + 4; + + delimiter = ngx_pnalloc(pool, cdlen); + if (delimiter == NULL) { + return NGX_ERROR; + } + + delimiter[0] = '-'; + delimiter[1] = '-'; + ngx_memcpy(delimiter + 2, boundary->data, boundary->len); + delimiter[dlen] = '-'; + delimiter[dlen + 1] = '-'; + + p = ngx_js_form_find(body, end, delimiter, cdlen); + marker = ngx_js_form_find(body, end, delimiter, dlen); + + if (marker == NULL || (p != NULL && p < marker)) { + ngx_js_form_error(error, "malformed multipart body"); + return NGX_JS_FORM_PARSE_ERROR; + } + + p = marker + dlen; + + if (p + 2 <= end && p[0] == '-' && p[1] == '-') { + return NGX_JS_FORM_OK; + } + + if (p + 2 > end || p[0] != '\r' || p[1] != '\n') { + ngx_js_form_error(error, "malformed multipart boundary"); + return NGX_JS_FORM_PARSE_ERROR; + } + + p += 2; + + for ( ;; ) { + headers_end = ngx_js_form_find(p, end, (u_char *) "\r\n\r\n", 4); + if (headers_end == NULL) { + ngx_js_form_error(error, "missing multipart header separator"); + return NGX_JS_FORM_PARSE_ERROR; + } + + if ((size_t) (headers_end - p) > NGX_JS_FORM_MAX_PART_HEADER_SIZE) { + ngx_js_form_error(error, "multipart headers are too large"); + return NGX_JS_FORM_PARSE_ERROR; + } + + if (ngx_js_form_parse_part_headers(pool, p, headers_end, &name, + &is_file, error) + != NGX_JS_FORM_OK) + { + return NGX_JS_FORM_PARSE_ERROR; + } + + p = headers_end + sizeof("\r\n\r\n") - 1; + scan = p; + + for ( ;; ) { + next = ngx_js_form_find(scan, end, (u_char *) "\r\n--", 4); + if (next == NULL) { + ngx_js_form_error(error, "truncated multipart body"); + return NGX_JS_FORM_PARSE_ERROR; + } + + if (next + (sizeof("\r\n--") - 1) + boundary->len <= end + && ngx_memcmp(next + (sizeof("\r\n--") - 1), boundary->data, + boundary->len) == 0) + { + break; + } + + scan = next + sizeof("\r\n--") - 1; + } + + part_end = next; + + if (is_file) { + value.len = 0; + value.data = (u_char *) ""; + form->has_files = 1; + + } else { + if (ngx_js_form_copy(pool, p, part_end, &value) != NGX_OK) { + return NGX_ERROR; + } + } + + if (ngx_js_form_add_entry(form, pool, &name, &value, &count, max_keys, + is_file, error) + != NGX_JS_FORM_OK) + { + return NGX_JS_FORM_PARSE_ERROR; + } + + p = next + (sizeof("\r\n--") - 1) + boundary->len; + + if (p + 2 <= end && p[0] == '-' && p[1] == '-') { + return NGX_JS_FORM_OK; + } + + if (p + 2 > end || p[0] != '\r' || p[1] != '\n') { + ngx_js_form_error(error, "malformed multipart boundary"); + return NGX_JS_FORM_PARSE_ERROR; + } + + p += 2; + } +} + + +static ngx_int_t +ngx_js_form_parse_part_headers(ngx_pool_t *pool, u_char *start, + u_char *end, ngx_str_t *name, ngx_flag_t *is_file, ngx_str_t *error) +{ + u_char *p, *line, *colon, *line_end; + ngx_str_t key, value; + ngx_uint_t headers; + ngx_flag_t seen_disposition; + + headers = 0; + seen_disposition = 0; + + name->len = 0; + name->data = NULL; + + *is_file = 0; + + for (p = start; p < end; p = line_end + 2) { + line = p; + line_end = ngx_js_form_find(p, end, (u_char *) "\r\n", 2); + if (line_end == NULL) { + line_end = end; + } + + if ((size_t) (line_end - line) > NGX_JS_FORM_MAX_PART_HEADER_LINE) { + ngx_js_form_error(error, "multipart header line is too long"); + return NGX_JS_FORM_PARSE_ERROR; + } + + if (++headers > NGX_JS_FORM_MAX_PART_HEADERS) { + ngx_js_form_error(error, "too many multipart headers"); + return NGX_JS_FORM_PARSE_ERROR; + } + + colon = line; + + while (colon < line_end && *colon != ':') { + colon++; + } + + if (colon == line_end) { + ngx_js_form_error(error, "malformed multipart header"); + return NGX_JS_FORM_PARSE_ERROR; + } + + key.data = line; + key.len = colon - line; + + colon++; + colon = ngx_js_form_skip_ows(colon, line_end); + + value.data = colon; + value.len = line_end - colon; + + while (value.len > 0 + && ngx_js_form_is_ows(value.data[value.len - 1])) + { + value.len--; + } + + if (key.len == sizeof("Content-Disposition") - 1 + && ngx_strncasecmp(key.data, (u_char *) "Content-Disposition", + key.len) + == 0) + { + if (seen_disposition) { + ngx_js_form_error(error, + "duplicate Content-Disposition header"); + return NGX_JS_FORM_PARSE_ERROR; + } + + if (ngx_js_form_parse_disposition(pool, &value, name, is_file, + error) + != NGX_JS_FORM_OK) + { + return NGX_JS_FORM_PARSE_ERROR; + } + + seen_disposition = 1; + } + + if (line_end == end) { + break; + } + } + + if (!seen_disposition) { + ngx_js_form_error(error, "missing Content-Disposition header"); + return NGX_JS_FORM_PARSE_ERROR; + } + + return NGX_JS_FORM_OK; +} + + +static ngx_int_t +ngx_js_form_parse_disposition(ngx_pool_t *pool, ngx_str_t *value, + ngx_str_t *name, ngx_flag_t *is_file, ngx_str_t *error) +{ + u_char *p, *end; + ngx_str_t param, param_value; + ngx_flag_t quoted, seen_name, seen_file; + + p = value->data; + end = p + value->len; + + while (p < end && *p != ';') { + p++; + } + + if ((size_t) (p - value->data) != sizeof("form-data") - 1 + || ngx_strncasecmp(value->data, (u_char *) "form-data", + p - value->data) + != 0) + { + ngx_js_form_error(error, "unsupported disposition type"); + return NGX_JS_FORM_PARSE_ERROR; + } + + seen_name = 0; + seen_file = 0; + + while (p < end) { + if (*p++ != ';') { + ngx_js_form_error(error, "malformed Content-Disposition"); + return NGX_JS_FORM_PARSE_ERROR; + } + + p = ngx_js_form_skip_ows(p, end); + + if (ngx_js_form_parse_param(pool, &p, end, ¶m, ¶m_value, + "ed, error) + != NGX_JS_FORM_OK) + { + return NGX_JS_FORM_PARSE_ERROR; + } + + if (param.len == sizeof("name") - 1 + && ngx_strncasecmp(param.data, (u_char *) "name", param.len) == 0) + { + if (seen_name) { + ngx_js_form_error(error, "duplicate name parameter"); + return NGX_JS_FORM_PARSE_ERROR; + } + + *name = param_value; + seen_name = 1; + + } else if (param.len == sizeof("filename") - 1 + && ngx_strncasecmp(param.data, (u_char *) "filename", + param.len) + == 0) + { + if (seen_file) { + ngx_js_form_error(error, "duplicate filename parameter"); + return NGX_JS_FORM_PARSE_ERROR; + } + + *is_file = 1; + seen_file = 1; + } + + p = ngx_js_form_skip_ows(p, end); + + if (p == end) { + break; + } + } + + if (!seen_name) { + ngx_js_form_error(error, "multipart field name is required"); + return NGX_JS_FORM_PARSE_ERROR; + } + + return NGX_JS_FORM_OK; +} + + +static ngx_int_t +ngx_js_form_parse_param(ngx_pool_t *pool, u_char **pp, u_char *end, + ngx_str_t *param, ngx_str_t *value, ngx_flag_t *quoted, ngx_str_t *error) +{ + u_char *p, *start; + + p = ngx_js_form_skip_ows(*pp, end); + start = p; + + while (p < end && *p != '=' && *p != ';' && !ngx_js_form_is_ows(*p)) { + p++; + } + + if (p == start) { + ngx_js_form_error(error, "malformed parameter"); + return NGX_JS_FORM_PARSE_ERROR; + } + + if (ngx_js_form_copy(pool, start, p, param) != NGX_OK) { + return NGX_ERROR; + } + + p = ngx_js_form_skip_ows(p, end); + + if (p == end || *p != '=') { + ngx_js_form_error(error, "malformed parameter"); + return NGX_JS_FORM_PARSE_ERROR; + } + + p++; + p = ngx_js_form_skip_ows(p, end); + + *quoted = 0; + + if (p < end && *p == '"') { + start = ++p; + + while (p < end && *p != '"') { + if (*p == '\\' && p + 1 < end) { + p += 2; + continue; + } + + p++; + } + + if (p == end) { + ngx_js_form_error(error, "unterminated quoted parameter"); + return NGX_JS_FORM_PARSE_ERROR; + } + + if (ngx_js_form_copy_quoted(pool, start, p, value) != NGX_OK) { + return NGX_ERROR; + } + + *quoted = 1; + p++; + + } else { + start = p; + + while (p < end && *p != ';' && !ngx_js_form_is_ows(*p)) { + p++; + } + + if (start == p) { + ngx_js_form_error(error, "empty parameter value"); + return NGX_JS_FORM_PARSE_ERROR; + } + + if (ngx_js_form_copy(pool, start, p, value) != NGX_OK) { + return NGX_ERROR; + } + } + + *pp = p; + + return NGX_JS_FORM_OK; +} + + +static ngx_int_t +ngx_js_form_add_entry(ngx_js_form_t *form, ngx_pool_t *pool, ngx_str_t *name, + ngx_str_t *value, ngx_uint_t *count, ngx_uint_t max_keys, + ngx_flag_t is_file, ngx_str_t *error) +{ + ngx_js_form_entry_t *entry; + + if (++(*count) > max_keys) { + ngx_js_form_error(error, "maxKeys limit exceeded"); + return NGX_JS_FORM_PARSE_ERROR; + } + + entry = ngx_array_push(&form->entries); + if (entry == NULL) { + return NGX_ERROR; + } + + entry->name = *name; + entry->value = *value; + entry->is_file = is_file; + + return NGX_JS_FORM_OK; +} + + +static ngx_int_t +ngx_js_form_decode_urlencoded(ngx_pool_t *pool, u_char *start, u_char *end, + ngx_str_t *dst, ngx_str_t *error) +{ + u_char *p, *d, *out; + ngx_int_t n; + + out = ngx_pnalloc(pool, (end - start) + 1); + if (out == NULL) { + return NGX_ERROR; + } + + d = out; + + for (p = start; p < end; p++) { + if (*p == '+') { + *d++ = ' '; + continue; + } + + if (*p == '%') { + if (p + 2 >= end) { + ngx_js_form_error(error, "malformed percent escape"); + return NGX_JS_FORM_PARSE_ERROR; + } + + n = ngx_hextoi(p + 1, 2); + if (n == NGX_ERROR) { + ngx_js_form_error(error, "malformed percent escape"); + return NGX_JS_FORM_PARSE_ERROR; + } + + *d++ = (u_char) n; + p += 2; + continue; + } + + *d++ = *p; + } + + *d = '\0'; + dst->data = out; + dst->len = d - out; + + return NGX_JS_FORM_OK; +} + + +static ngx_int_t +ngx_js_form_copy(ngx_pool_t *pool, u_char *start, u_char *end, ngx_str_t *dst) +{ + dst->len = end - start; + + if (dst->len == 0) { + dst->data = (u_char *) ""; + return NGX_OK; + } + + dst->data = ngx_pnalloc(pool, dst->len + 1); + if (dst->data == NULL) { + return NGX_ERROR; + } + + ngx_memcpy(dst->data, start, dst->len); + dst->data[dst->len] = '\0'; + + return NGX_OK; +} + + +static ngx_int_t +ngx_js_form_copy_quoted(ngx_pool_t *pool, u_char *start, u_char *end, + ngx_str_t *dst) +{ + u_char *p, *d; + + dst->len = end - start; + + if (dst->len == 0) { + dst->data = (u_char *) ""; + return NGX_OK; + } + + dst->data = ngx_pnalloc(pool, dst->len + 1); + if (dst->data == NULL) { + return NGX_ERROR; + } + + d = dst->data; + + for (p = start; p < end; p++) { + if (*p == '\\' && p + 1 < end) { + p++; + } + + *d++ = *p; + } + + *d = '\0'; + dst->len = d - dst->data; + + return NGX_OK; +} + + +static void +ngx_js_form_error(ngx_str_t *error, const char *text) +{ + error->data = (u_char *) text; + error->len = ngx_strlen(text); +} + + +static u_char * +ngx_js_form_skip_ows(u_char *p, u_char *end) +{ + while (p < end && ngx_js_form_is_ows(*p)) { + p++; + } + + return p; +} + + +static ngx_uint_t +ngx_js_form_is_ows(u_char ch) +{ + return ch == ' ' || ch == '\t'; +} + + +static u_char * +ngx_js_form_find(u_char *start, u_char *end, u_char *pattern, size_t len) +{ + u_char *p, *last; + + if ((size_t) (end - start) < len) { + return NULL; + } + + last = end - len + 1; + + for (p = start; p < last; p++) { + if (*p == pattern[0] && ngx_memcmp(p, pattern, len) == 0) { + return p; + } + } + + return NULL; +} diff --git a/nginx/ngx_js_form.h b/nginx/ngx_js_form.h new file mode 100644 index 000000000..3f8c6ae79 --- /dev/null +++ b/nginx/ngx_js_form.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) Dmitry Volyntsev + * Copyright (C) F5, Inc. + */ + + +#ifndef _NGX_JS_FORM_H_INCLUDED_ +#define _NGX_JS_FORM_H_INCLUDED_ + + +#include +#include + +#define NGX_JS_FORM_DEFAULT_MAX_KEYS 128 + +#define NGX_JS_FORM_OK NGX_OK +#define NGX_JS_FORM_TYPE_ERROR NGX_DECLINED +#define NGX_JS_FORM_PARSE_ERROR NGX_DONE + + +typedef struct { + ngx_str_t name; + ngx_str_t value; + unsigned is_file:1; +} ngx_js_form_entry_t; + + +typedef struct { + ngx_array_t entries; + unsigned has_files:1; +} ngx_js_form_t; + + +ngx_int_t ngx_js_parse_form(ngx_pool_t *pool, ngx_str_t *content_type, + u_char *body, size_t len, ngx_uint_t max_keys, ngx_js_form_t **form, + ngx_str_t *error); + + +#endif /* _NGX_JS_FORM_H_INCLUDED_ */ diff --git a/nginx/ngx_qjs_fetch.c b/nginx/ngx_qjs_fetch.c index 8e010bec8..7c0a7a34d 100644 --- a/nginx/ngx_qjs_fetch.c +++ b/nginx/ngx_qjs_fetch.c @@ -131,22 +131,19 @@ static const JSCFunctionListEntry ngx_qjs_ext_fetch_headers_proto[] = { static const JSCFunctionListEntry ngx_qjs_ext_fetch_request_proto[] = { -#define NGX_QJS_BODY_ARRAY_BUFFER 0 -#define NGX_QJS_BODY_JSON 1 -#define NGX_QJS_BODY_TEXT 2 JS_CFUNC_MAGIC_DEF("arrayBuffer", 0, ngx_qjs_ext_fetch_request_body, - NGX_QJS_BODY_ARRAY_BUFFER), + NGX_JS_BODY_ARRAY_BUFFER), JS_CGETSET_DEF("bodyUsed", ngx_qjs_ext_fetch_request_body_used, NULL), JS_CGETSET_DEF("cache", ngx_qjs_ext_fetch_request_cache, NULL), JS_CGETSET_DEF("credentials", ngx_qjs_ext_fetch_request_credentials, NULL), JS_CFUNC_MAGIC_DEF("json", 0, ngx_qjs_ext_fetch_request_body, - NGX_QJS_BODY_JSON), + NGX_JS_BODY_JSON), JS_CGETSET_DEF("headers", ngx_qjs_ext_fetch_request_headers, NULL ), JS_CGETSET_MAGIC_DEF("method", ngx_qjs_ext_fetch_request_field, NULL, offsetof(ngx_js_request_t, method) ), JS_CGETSET_DEF("mode", ngx_qjs_ext_fetch_request_mode, NULL), JS_CFUNC_MAGIC_DEF("text", 0, ngx_qjs_ext_fetch_request_body, - NGX_QJS_BODY_TEXT), + NGX_JS_BODY_TEXT), JS_CGETSET_MAGIC_DEF("url", ngx_qjs_ext_fetch_request_field, NULL, offsetof(ngx_js_request_t, url) ), }; @@ -154,17 +151,17 @@ static const JSCFunctionListEntry ngx_qjs_ext_fetch_request_proto[] = { static const JSCFunctionListEntry ngx_qjs_ext_fetch_response_proto[] = { JS_CFUNC_MAGIC_DEF("arrayBuffer", 0, ngx_qjs_ext_fetch_response_body, - NGX_QJS_BODY_ARRAY_BUFFER), + NGX_JS_BODY_ARRAY_BUFFER), JS_CGETSET_DEF("bodyUsed", ngx_qjs_ext_fetch_response_body_used, NULL), JS_CGETSET_DEF("headers", ngx_qjs_ext_fetch_response_headers, NULL ), JS_CFUNC_MAGIC_DEF("json", 0, ngx_qjs_ext_fetch_response_body, - NGX_QJS_BODY_JSON), + NGX_JS_BODY_JSON), JS_CGETSET_DEF("ok", ngx_qjs_ext_fetch_response_ok, NULL), JS_CGETSET_DEF("redirected", ngx_qjs_ext_fetch_response_redirected, NULL), JS_CGETSET_DEF("status", ngx_qjs_ext_fetch_response_status, NULL), JS_CGETSET_DEF("statusText", ngx_qjs_ext_fetch_response_status_text, NULL), JS_CFUNC_MAGIC_DEF("text", 0, ngx_qjs_ext_fetch_response_body, - NGX_QJS_BODY_TEXT), + NGX_JS_BODY_TEXT), JS_CGETSET_DEF("type", ngx_qjs_ext_fetch_response_type, NULL), JS_CGETSET_MAGIC_DEF("url", ngx_qjs_ext_fetch_response_field, NULL, offsetof(ngx_js_response_t, url) ), @@ -2027,7 +2024,7 @@ ngx_qjs_ext_fetch_request_body(JSContext *cx, JSValueConst this_val, request->body_used = 1; switch (magic) { - case NGX_QJS_BODY_ARRAY_BUFFER: + case NGX_JS_BODY_ARRAY_BUFFER: /* * no free_func for JS_NewArrayBuffer() * because request->body is allocated from e->pool @@ -2041,15 +2038,15 @@ ngx_qjs_ext_fetch_request_body(JSContext *cx, JSValueConst this_val, break; - case NGX_QJS_BODY_JSON: - case NGX_QJS_BODY_TEXT: + case NGX_JS_BODY_JSON: + case NGX_JS_BODY_TEXT: default: result = qjs_string_create(cx, request->body.data, request->body.len); if (JS_IsException(result)) { return JS_ThrowOutOfMemory(cx); } - if (magic == NGX_QJS_BODY_JSON) { + if (magic == NGX_JS_BODY_JSON) { string = js_malloc(cx, request->body.len + 1); JS_FreeValue(cx, result); @@ -2309,14 +2306,14 @@ ngx_qjs_ext_fetch_response_body(JSContext *cx, JSValueConst this_val, response->body_used = 1; switch (magic) { - case NGX_QJS_BODY_ARRAY_BUFFER: - case NGX_QJS_BODY_TEXT: + case NGX_JS_BODY_ARRAY_BUFFER: + case NGX_JS_BODY_TEXT: ret = njs_chb_join(&response->chain, &string); if (ret != NJS_OK) { return JS_ThrowOutOfMemory(cx); } - if (magic == NGX_QJS_BODY_TEXT) { + if (magic == NGX_JS_BODY_TEXT) { result = qjs_string_create(cx, string.start, string.length); if (JS_IsException(result)) { return JS_ThrowOutOfMemory(cx); @@ -2338,7 +2335,7 @@ ngx_qjs_ext_fetch_response_body(JSContext *cx, JSValueConst this_val, break; - case NGX_QJS_BODY_JSON: + case NGX_JS_BODY_JSON: default: /* 'string.start' must be zero terminated. */ njs_chb_append_literal(&response->chain, "\0"); diff --git a/nginx/t/js_access.t b/nginx/t/js_access.t new file mode 100644 index 000000000..25c9b91a0 --- /dev/null +++ b/nginx/t/js_access.t @@ -0,0 +1,408 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) F5, Inc. + +# Tests for http njs module, js_access directive. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http rewrite proxy/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_import test.js; + + js_var $foo; + js_var $upstream; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + js_access test.set_var; + + location /var { + js_content test.content; + } + + location /deny { + js_access test.deny; + js_content test.content; + } + + location /exception { + js_access test.exception; + js_content test.content; + } + + location /noop { + js_access test.noop; + js_content test.content; + } + + location /decline { + js_access test.decline; + js_content test.content; + } + + location /override { + js_access test.override; + js_content test.content; + } + + location /content_only { + js_content test.content_only; + } + + location /async_timeout { + js_access test.async_timeout; + js_content test.content; + } + + location /async_deny { + js_access test.async_deny; + js_content test.content; + } + + location /async_exception { + js_access test.async_exception; + js_content test.content; + } + + location /sr_skip { + js_content test.sr_skip; + } + + location /sub { + js_access test.deny; + js_content test.content; + } + + location /sr { + js_access test.sr; + js_content test.content; + } + + location /fetch { + js_access test.fetch; + js_content test.content; + } + + location /route { + js_access test.route; + proxy_pass http://$upstream; + } + + location /auth_check { + js_content test.auth_check; + } + + location /redirect { + js_access test.redirect; + js_content test.content; + } + + location /redirect_async { + js_access test.redirect_async; + js_content test.content; + } + + location /callback { + js_content test.content; + } + } + + server { + listen 127.0.0.1:8080; + server_name noaccess; + + location /no_access { + js_content test.content_only; + } + } + + server { + listen 127.0.0.1:8081; + + location / { + return 200 "backend1"; + } + } + + server { + listen 127.0.0.1:8082; + + location / { + return 200 "backend2"; + } + } +} + +EOF + +my $p0 = port(8080); +my $p1 = port(8081); +my $p2 = port(8082); + +$t->write_file('test.js', < setTimeout(resolve, 5)); + r.variables.foo = 'timeout_ok'; + } + + async function async_deny(r) { + await new Promise(resolve => setTimeout(resolve, 5)); + r.return(403); + } + + async function async_exception(r) { + await new Promise(resolve => setTimeout(resolve, 5)); + throw new Error("async_access_error"); + } + + async function sr_skip(r) { + let reply = await r.subrequest('/sub'); + r.return(reply.status, reply.responseText); + } + + async function fetch(r) { + let resp = await ngx.fetch( + \`http://127.0.0.1:$p0/auth_check?token=\${r.variables.arg_token}\`); + + if (resp.status != 200) { + r.return(resp.status); + return; + } + + r.variables.foo = await resp.text(); + } + + async function sr(r) { + let reply = await r.subrequest('/auth_check?token=' + + r.variables.arg_token); + if (reply.status != 200) { + r.return(reply.status); + return; + } + + r.variables.foo = reply.responseText; + } + + function route(r) { + let dest = r.variables.arg_dest; + r.variables.upstream = (dest === 'one') + ? '127.0.0.1:$p1' : '127.0.0.1:$p2'; + } + + function auth_check(r) { + let token = r.variables.arg_token; + + if (token === 'valid') { + r.return(200, 'authenticated'); + } else { + r.return(403); + } + } + + function redirect(r) { + r.return(302, 'http://127.0.0.1:$p0/callback'); + } + + async function redirect_async(r) { + await new Promise(resolve => setTimeout(resolve, 5)); + r.return(302, 'http://127.0.0.1:$p0/callback'); + } + + export default { set_var, content, deny, exception, noop, override, + decline, content_only, async_timeout, async_deny, + async_exception, sr_skip, sr, fetch, route, + auth_check, redirect, redirect_async }; + +EOF + +$t->write_file_expand('duplicate.conf', bad_conf( + http => 'js_import test.js;', + location => 'js_access test.noop; js_access test.noop;')); + +$t->write_file_expand('no_import.conf', bad_conf( + location => 'js_access test.noop;')); + +$t->try_run('no js_access')->plan(27); + +############################################################################### + +like(http_get('/var'), qr/var:access_ok/, + 'js_access sets variable from server level'); +like(http_get('/deny'), qr/403 Forbidden/, + 'js_access sync r.return(403) rejects'); +like(http_post('/deny'), qr/403 Forbidden/, + 'js_access deny with request body'); +like(http_get('/exception'), qr/500 Internal Server Error/, + 'js_access sync exception returns 500'); +like(http_get('/noop'), qr/var:/, + 'js_access noop continues to content'); +like(http_get('/decline'), qr/var:/, + 'js_access decline continues to content'); +like(http_get('/override'), qr/var:overridden/, + 'js_access override in location'); +like(http_get('/content_only'), qr/content_only/, + 'js_content without js_access'); +like(http("GET /no_access HTTP/1.0" . CRLF . + "Host: noaccess" . CRLF . CRLF), + qr/content_only/, + 'js_access not inherited in sibling server'); +like(http_get('/async_timeout'), qr/var:timeout_ok/, + 'async js_access with setTimeout'); +like(http_get('/async_deny'), qr/403 Forbidden/, + 'async js_access r.return(403) rejects'); +like(http_get('/async_exception'), qr/500 Internal Server Error/, + 'async js_access exception returns 500'); +like(http_get('/sr_skip'), qr/var:/, + 'js_access skipped for subrequests'); +like(http_get('/sr?token=valid'), qr/var:authenticated/, + 'subrequest access allow'); +like(http_get('/sr?token=invalid'), qr/403 Forbidden/, + 'subrequest access deny'); +like(http_get('/fetch?token=valid'), qr/var:authenticated/, + 'fetch access allow'); +like(http_get('/fetch?token=invalid'), qr/403 Forbidden/, + 'fetch access deny'); +like(http_get('/route?dest=one'), qr/backend1/, + 'variable routing to backend1'); +like(http_get('/route?dest=two'), qr/backend2/, + 'variable routing to backend2'); +like(http_get('/redirect'), qr/302 Moved/, + 'js_access sync redirect'); +like(http_get('/redirect'), qr!Location: http://127.0.0.1:$p0/callback!, + 'js_access sync redirect Location header'); +like(http_get('/redirect_async'), qr/302 Moved/, + 'js_access async redirect'); +like(http_get('/redirect_async'), qr!Location: http://127.0.0.1:$p0/callback!, + 'js_access async redirect Location header'); + +my ($rc, $out) = nginx_test_conf($t, 'duplicate.conf'); + +isnt($rc, 0, 'duplicate js_access fails'); +like($out, qr/"js_access" directive is duplicate/, + 'duplicate js_access error'); + +($rc, $out) = nginx_test_conf($t, 'no_import.conf'); + +isnt($rc, 0, 'js_access without js_import fails'); +like($out, qr/no imports defined for "js_access" "test\.noop"/, + 'js_access without js_import error'); + +############################################################################### + +sub http_post { + my ($url, %extra) = @_; + + my $p = "POST $url HTTP/1.0" . CRLF . + "Host: localhost" . CRLF . + "Content-Length: 8" . CRLF . + CRLF . + "REQ-BODY"; + + return http($p, %extra); +} + +sub nginx_test_conf { + my ($t, $conf) = @_; + my $testdir = $t->testdir(); + my $cmd = "$Test::Nginx::NGINX -p $testdir/ -c $conf -t " + . "-e error.log 2>&1"; + + my $out = `$cmd`; + + return ($? >> 8, $out); +} + +sub bad_conf { + my %args = @_; + my $http = $args{http} // ''; + my $loc = $args{location} // ''; + + return <<"EOF"; + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + $http + + server { + listen 127.0.0.1:8080; + + location / { + $loc + } + } +} + +EOF +} diff --git a/nginx/t/js_access_body.t b/nginx/t/js_access_body.t new file mode 100644 index 000000000..abe53b398 --- /dev/null +++ b/nginx/t/js_access_body.t @@ -0,0 +1,459 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) F5, Inc. + +# Tests for http njs module, js_access body reading methods. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx qw/ :DEFAULT http_end /; + +############################################################################### + +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; + + js_var $foo; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /text { + js_access test.read_text; + js_content test.content; + } + + location /buffer { + js_access test.read_buffer; + js_content test.content; + } + + location /text_twice { + js_access test.read_text_twice; + js_content test.content; + } + + location /buffer_twice { + js_access test.read_buffer_twice; + js_content test.content; + } + + location /concurrent_text_buffer { + js_access test.read_concurrent_text_buffer; + js_content test.content; + } + + location /text_then_buffer { + js_access test.read_text_then_buffer; + js_content test.content; + } + + location /json { + js_access test.read_json; + js_content test.content; + } + + location /json_invalid { + js_access test.read_json_invalid; + js_content test.content; + } + + location /empty { + js_access test.read_text; + js_content test.content; + } + + location /big { + client_body_buffer_size 64k; + js_access test.read_text_length; + js_content test.content; + } + + location /big_4k { + client_body_buffer_size 4k; + js_access test.read_text_length; + js_content test.content; + } + + location /slow { + js_access test.read_text; + js_content test.content; + } + + location /chunked { + js_access test.read_text; + js_content test.content; + } + + location /text_timeout { + js_access test.read_text_timeout; + js_content test.content; + } + + location /access_content_async { + js_access test.read_text_timeout; + js_content test.content_async; + } + + location /content_text { + js_content test.content_text; + } + + location /proxy { + js_access test.read_text; + proxy_pass http://127.0.0.1:%%PORT_8081%%; + } + + location /in_file { + client_body_in_file_only on; + js_access test.read_text; + js_content test.content; + } + + location /too_large { + client_max_body_size 4; + js_access test.read_text; + js_content test.content; + } + + location /too_large_chunked { + client_max_body_size 4; + client_body_timeout 2s; + js_access test.read_text; + js_content test.content; + } + + + } + + server { + listen 127.0.0.1:8081; + + location / { + js_content test.echo_body; + } + } +} + +EOF + +$t->write_file('test.js', < setTimeout(resolve, 5)); + r.return(200, `var:\${r.variables.foo}:content-async`); + } + + async function content_text(r) { + let body = await r.readRequestText(); + r.return(200, `content:\${body}`); + } + + async function read_text(r) { + let body = await r.readRequestText(); + r.variables.foo = body; + } + + async function read_text_timeout(r) { + let body = await r.readRequestText(); + await new Promise(resolve => setTimeout(resolve, 5)); + r.variables.foo = body + ':after-timeout'; + } + + async function read_buffer(r) { + let buf = await r.readRequestArrayBuffer(); + r.variables.foo = String.fromCharCode.apply(null, new Uint8Array(buf)); + } + + async function read_text_twice(r) { + let first = await r.readRequestText(); + let second = await r.readRequestText(); + r.variables.foo = (first === second) ? 'same' : 'different'; + } + + async function read_buffer_twice(r) { + let a = new Uint8Array(await r.readRequestArrayBuffer()); + let b = new Uint8Array(await r.readRequestArrayBuffer()); + let eq = a.length === b.length + && a.every((v, i) => v === b[i]); + r.variables.foo = eq ? 'same' : 'different'; + } + + async function read_concurrent_text_buffer(r) { + try { + await Promise.all([ + r.readRequestText(), + r.readRequestArrayBuffer() + ]); + + r.variables.foo = 'no_error'; + + } catch (e) { + r.variables.foo = e.message; + } + } + + async function read_text_then_buffer(r) { + let text = await r.readRequestText(); + let buf = await r.readRequestArrayBuffer(); + let text2 = String.fromCharCode.apply(null, new Uint8Array(buf)); + r.variables.foo = (text === text2) ? 'same' : 'different'; + } + + async function read_json(r) { + let obj = await r.readRequestJSON(); + r.variables.foo = obj.method + ':' + obj.name; + } + + async function read_json_invalid(r) { + try { + await r.readRequestJSON(); + r.variables.foo = 'no_error'; + } catch (e) { + r.variables.foo = e.constructor.name; + } + } + + async function read_text_length(r) { + let body = await r.readRequestText(); + r.variables.foo = body.length; + } + + function echo_body(r) { + r.return(200, 'echo:' + r.requestText); + } + + export default { content, content_async, content_text, read_text, + read_text_timeout, read_buffer, read_text_twice, + read_buffer_twice, read_concurrent_text_buffer, + read_text_then_buffer, read_json, read_json_invalid, + read_text_length, echo_body }; + +EOF + +$t->try_run('no js_access')->plan(23); + +############################################################################### + +like(http_post('/text'), qr/var:REQ-BODY/, 'readRequestText'); +like(http_post('/buffer'), qr/var:REQ-BODY/, 'readRequestArrayBuffer'); +like(http_post_json('/json', '{"method":"GET","name":"test"}'), + qr/var:GET:test/, 'readRequestJSON'); +like(http_post_json('/json_invalid', 'not-json'), qr/var:SyntaxError/, + 'readRequestJSON invalid rejects with SyntaxError'); +like(http_get('/empty'), qr/var:/, 'readRequestText empty body'); + +like(http_post('/text_twice'), qr/var:same/, + 'readRequestText twice returns same value'); +like(http_post('/buffer_twice'), qr/var:same/, + 'readRequestArrayBuffer twice returns same value'); +like(http_post('/text_then_buffer'), qr/var:same/, + 'readRequestText then readRequestArrayBuffer same content'); +like(http_post('/concurrent_text_buffer'), + qr/var:request body is already being read/, + 'concurrent body read throws error'); + +like(http_post_big('/big'), qr/var:10240/, + 'readRequestText large body'); +like(http_post_big('/big_4k'), qr/var:10240/, + 'readRequestText large body with small buffer'); + +like(http_post('/proxy'), qr/echo:REQ-BODY/, + 'body preserved for proxy_pass'); +like(http_post('/in_file'), qr/var:REQ-BODY/, + 'readRequestText from temp file'); + +like(http_post_slow('/slow'), qr/var:SLOW-BODY/, + 'readRequestText with slow client'); +like(http_post_chunked('/chunked'), qr/var:CHUNKED-BODY/, + 'readRequestText chunked transfer encoding'); +like(http_post('/text_timeout'), qr/var:REQ-BODY:after-timeout/, + 'readRequestText before async action in js_access'); +like(http_post('/access_content_async'), + qr/var:REQ-BODY:after-timeout:content-async/, + 'async js_content after async js_access body read'); +like(http_post('/content_text'), qr/content:REQ-BODY/, + 'readRequestText in js_content'); + +http_post_disconnect('/text'); +like(http_post('/text'), qr/var:REQ-BODY/, + 'readRequestText after client disconnect'); + +like(http_post('/too_large'), qr/413 Request Entity Too Large/, + 'readRequestText client_max_body_size exceeded'); + +like(http_post_slow_chunked('/too_large_chunked'), + qr/413 Request Entity Too Large/, + 'readRequestText chunked body exceeds client_max_body_size'); +like(http_post_chunked_too_large('/too_large_chunked'), + qr/413 Request Entity Too Large/, + 'readRequestText chunked body rejected in preread'); +like(http_post('/text'), qr/var:REQ-BODY/, + 'readRequestText works after chunked 413'); + +############################################################################### + +sub http_post { + my ($url, %extra) = @_; + + my $p = "POST $url HTTP/1.0" . CRLF . + "Host: localhost" . CRLF . + "Content-Length: 8" . CRLF . + CRLF . + "REQ-BODY"; + + return http($p, %extra); +} + +sub http_post_json { + my ($url, $body, %extra) = @_; + + my $p = "POST $url HTTP/1.0" . CRLF . + "Host: localhost" . CRLF . + "Content-Type: application/json" . CRLF . + "Content-Length: " . length($body) . CRLF . + CRLF . + $body; + + return http($p, %extra); +} + +sub http_post_big { + my ($url, %extra) = @_; + + my $p = "POST $url HTTP/1.0" . CRLF . + "Host: localhost" . CRLF . + "Content-Length: 10240" . CRLF . + CRLF . + ("1234567890" x 1024); + + return http($p, %extra); +} + +sub http_post_slow { + my ($url, %extra) = @_; + + my $header = "POST $url HTTP/1.1" . CRLF . + "Host: localhost" . CRLF . + "Content-Length: 9" . CRLF . + "Connection: close" . CRLF . + CRLF; + + my $s = http($header, start => 1); + + select undef, undef, undef, 0.1; + print $s "SLOW"; + + select undef, undef, undef, 0.1; + print $s "-BODY"; + + return http_end($s); +} + +sub http_post_disconnect { + my ($url) = @_; + + my $header = "POST $url HTTP/1.1" . CRLF . + "Host: localhost" . CRLF . + "Content-Length: 1024" . CRLF . + "Connection: close" . CRLF . + CRLF; + + my $s = http($header, start => 1); + + select undef, undef, undef, 0.1; + print $s "PARTIAL"; + + select undef, undef, undef, 0.1; + close($s); + + select undef, undef, undef, 0.3; +} + +sub http_post_slow_chunked { + my ($url, %extra) = @_; + + my $header = "POST $url HTTP/1.1" . CRLF . + "Host: localhost" . CRLF . + "Transfer-Encoding: chunked" . CRLF . + "Connection: close" . CRLF . + CRLF; + + my $s = http($header, start => 1); + + select undef, undef, undef, 0.1; + print $s "8" . CRLF . "TOOLARGE" . CRLF; + + my $resp = http_end($s); + + # wait for nginx to finish lingering close and cleanup + select undef, undef, undef, 0.5; + + return $resp; +} + +sub http_post_chunked { + my ($url, %extra) = @_; + + my $p = "POST $url HTTP/1.1" . CRLF . + "Host: localhost" . CRLF . + "Transfer-Encoding: chunked" . CRLF . + "Connection: close" . CRLF . + CRLF . + "8" . CRLF . + "CHUNKED-" . CRLF . + "4" . CRLF . + "BODY" . CRLF . + "0" . CRLF . + CRLF; + + return http($p, %extra); +} + +sub http_post_chunked_too_large { + my ($url, %extra) = @_; + + my $p = "POST $url HTTP/1.1" . CRLF . + "Host: localhost" . CRLF . + "Transfer-Encoding: chunked" . CRLF . + "Connection: close" . CRLF . + CRLF . + "8" . CRLF . + "TOOLARGE" . CRLF . + "0" . CRLF . + CRLF; + + return http($p, %extra); +} + +############################################################################### diff --git a/nginx/t/js_access_satisfy.t b/nginx/t/js_access_satisfy.t new file mode 100644 index 000000000..926a496cd --- /dev/null +++ b/nginx/t/js_access_satisfy.t @@ -0,0 +1,175 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) F5, Inc. + +# Tests for http njs module, js_access directive with satisfy. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http rewrite access/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_import test.js; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /all_decline_allow { + satisfy all; + allow all; + js_access test.decline; + js_content test.content; + } + + location /all_decline_deny { + satisfy all; + deny all; + js_access test.decline; + js_content test.content; + } + + location /any_allow_deny { + satisfy any; + deny all; + js_access test.allow; + js_content test.content; + } + + location /any_deny_allow { + satisfy any; + allow all; + js_access test.deny; + js_content test.content; + } + + location /any_both_deny { + satisfy any; + deny all; + js_access test.deny; + js_content test.content; + } + + location /any_decline_deny { + satisfy any; + deny all; + js_access test.decline; + js_content test.content; + } + + location /any_decline_allow { + satisfy any; + allow all; + js_access test.decline; + js_content test.content; + } + + location /any_async_allow_deny { + satisfy any; + deny all; + js_access test.async_allow; + js_content test.content; + } + + location /any_async_decline_deny { + satisfy any; + deny all; + js_access test.async_decline; + js_content test.content; + } + } +} + +EOF + +$t->write_file('test.js', <<'EOF'); + function allow(r) { + /* default: normal return yields NGX_OK */ + } + + function deny(r) { + r.return(403); + } + + function decline(r) { + r.decline(); + } + + async function async_allow(r) { + await new Promise(resolve => setTimeout(resolve, 5)); + } + + async function async_decline(r) { + await new Promise(resolve => setTimeout(resolve, 5)); + r.decline(); + } + + function content(r) { + r.return(200, 'PASSED'); + } + + export default { allow, deny, decline, async_allow, async_decline, + content }; +EOF + +$t->try_run('no js_access')->plan(9); + +############################################################################### + +# satisfy all + decline: ip decides +like(http_get('/all_decline_allow'), qr/PASSED/, + 'satisfy all: js declines + ip allows'); +like(http_get('/all_decline_deny'), qr/403 Forbidden/, + 'satisfy all: js declines + ip denies'); + +# satisfy any: js allows overrides ip deny +like(http_get('/any_allow_deny'), qr/PASSED/, + 'satisfy any: js allows + ip denies'); + +# satisfy any: ip allows overrides js deny +like(http_get('/any_deny_allow'), qr/PASSED/, + 'satisfy any: js denies + ip allows'); + +# satisfy any: both deny +like(http_get('/any_both_deny'), qr/403 Forbidden/, + 'satisfy any: both deny'); + +# satisfy any + decline: js has no opinion, ip decides +like(http_get('/any_decline_deny'), qr/403 Forbidden/, + 'satisfy any: js declines + ip denies'); +like(http_get('/any_decline_allow'), qr/PASSED/, + 'satisfy any: js declines + ip allows'); + +# async variants +like(http_get('/any_async_allow_deny'), qr/PASSED/, + 'satisfy any: async js allows + ip denies'); +like(http_get('/any_async_decline_deny'), qr/403 Forbidden/, + 'satisfy any: async js declines + ip denies'); + +############################################################################### diff --git a/nginx/t/js_request_form.t b/nginx/t/js_request_form.t new file mode 100644 index 000000000..a2931db60 --- /dev/null +++ b/nginx/t/js_request_form.t @@ -0,0 +1,600 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) F5, Inc. + +# Tests for http njs module, r.readRequestForm() method. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx qw/ :DEFAULT /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_import test.js; + + js_var $foo; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /access_form { + js_access test.access_form; + js_content test.content; + } + + location /content_form { + js_content test.content_form; + } + + location /content_form_hex { + js_content test.content_form_hex; + } + + location /content_form_cache { + js_content test.content_form_cache; + } + + location /content_form_error { + js_content test.content_form_error; + } + + location /content_form_limit { + js_content test.content_form_limit; + } + } +} + +EOF + +$t->write_file('test.js', <<'EOF'); + function hex(s) { + let out = ''; + + for (let i = 0; i < s.length; i++) { + let c = s.charCodeAt(i); + out += (c < 0x10 ? '0' : '') + c.toString(16); + } + + return out; + } + + function render(form) { + let first = form.get('a'); + let pairs = []; + + if (first === null) { + first = 'null'; + } + + form.forEach((value, key) => pairs.push(`${key}=${value}`)); + + return [ + first, + form.getAll('a').join(','), + form.has('a'), + form.has('upload'), + form.hasFiles(), + form.fileFieldNames().join(','), + pairs.join('&') + ].join('|'); + } + + function content(r) { + r.return(200, `var:${r.variables.foo}`); + } + + async function access_form(r) { + try { + r.variables.foo = render(await r.readRequestForm({maxKeys: 8})); + + } catch (e) { + r.variables.foo = `${e.constructor.name}:${e.message}`; + } + } + + async function content_form(r) { + try { + r.return(200, render(await r.readRequestForm({maxKeys: 8}))); + + } catch (e) { + r.return(500, `${e.constructor.name}:${e.message}`); + } + } + + async function content_form_hex(r) { + try { + let form = await r.readRequestForm({maxKeys: 8}); + let value = form.get('a'); + + if (value === null) { + value = 'NULL'; + } + + r.return(200, hex(value)); + + } catch (e) { + r.return(500, `${e.constructor.name}:${e.message}`); + } + } + + async function content_form_cache(r) { + let form = await r.readRequestForm({maxKeys: 8}); + + await r.readRequestForm({maxKeys: 1}); + + r.return(200, render(form)); + } + + async function content_form_error(r) { + try { + await r.readRequestForm({maxKeys: 8}); + r.return(200, 'no_error'); + + } catch (e) { + r.return(500, `${e.constructor.name}:${e.message}`); + } + } + + async function content_form_limit(r) { + try { + await r.readRequestForm({maxKeys: 1}); + r.return(200, 'no_error'); + + } catch (e) { + r.return(500, e.message); + } + } + + export default { access_form, content, content_form, content_form_hex, + content_form_cache, content_form_error, + content_form_limit }; +EOF + +$t->try_run('no readRequestForm')->plan(57); + +############################################################################### + +like(http_post_form('/access_form', + urlencoded_form('a=1&a=2&empty=&=blank&space=one+two')), + qr/200.*var:1\|1,2\|true\|false\|false\|\|a=1&a=2&empty=&=blank&space=one two/s, + 'readRequestForm() in js_access with urlencoded body'); + +like(http_post_form('/content_form', + urlencoded_form('a=1&a=2&empty=&=blank&space=one+two')), + qr/200.*1\|1,2\|true\|false\|false\|\|a=1&a=2&empty=&=blank&space=one two/s, + 'readRequestForm() in js_content with urlencoded body'); + +like(http_post_form('/content_form_cache', urlencoded_form('a=1&a=2')), + qr/200.*1\|1,2\|true\|false\|false\|\|a=1&a=2/s, + 'successful form parse is cached'); + +like(http_post_form('/content_form', urlencoded_form('')), + qr/200.*null\|\|false\|false\|false\|\|$/s, + 'empty urlencoded body returns an empty form'); + +like(http_post_form('/content_form', + urlencoded_form('&baz=fuz&&muz=tax&')), + qr/200.*null\|\|false\|false\|false\|\|baz=fuz&muz=tax/s, + 'urlencoded empty fields are skipped'); + +like(http_post_form('/content_form', + urlencoded_form('freespace&name&value=12')), + qr/200.*null\|\|false\|false\|false\|\|freespace=&name=&value=12/s, + 'urlencoded fields without equals have an empty value'); + +like(http_post_form('/content_form', + urlencoded_form('==fu=z&baz=bar')), + qr/200.*null\|\|false\|false\|false\|\|==fu=z&baz=bar/s, + 'urlencoded first equals separates name and value'); + +like(http_post_form('/content_form', + urlencoded_form('ba+z=f+uz')), + qr/200.*null\|\|false\|false\|false\|\|ba z=f uz/s, + 'urlencoded plus is decoded in names and values'); + +like(http_post_form('/content_form_hex', urlencoded_form('a=%41%42%43')), + qr/200.*414243$/s, + 'urlencoded percent-decoding of %41%42%43 returns ABC'); + +like(http_post_form('/content_form_hex', urlencoded_form('a=%2a%5F%7e')), + qr/200.*2a5f7e$/s, + 'urlencoded percent-decoding accepts mixed-case hex digits'); + +like(http_post_form('/content_form_hex', urlencoded_form('a=%00')), + qr/200.*00$/s, + 'urlencoded percent-decoding accepts NUL byte'); + +like(http_post_form('/content_form_hex', urlencoded_form('a=x%20+y')), + qr/200.*78202079$/s, + 'urlencoded percent-decoding handles %20 and + in one value'); + +like(http_post_form('/content_form', + ['application/x-www-form-urlencoded ; charset=utf-8', 'a=1']), + qr/200.*1\|1\|true\|false\|false\|\|a=1/s, + 'content type OWS before parameters is skipped'); + +like(http_post_form('/content_form', + multipart_form( + { name => 'a', value => '1' }, + { name => 'upload', filename => 'a.txt', value => 'AAA' }, + { name => 'a', value => '2' }, + { name => 'upload', filename => 'b.txt', value => 'BBB' }, + { name => 'z', value => '3' }, + )), + qr/200.*1\|1,2\|true\|false\|true\|upload,upload\|a=1&a=2&z=3/s, + 'multipart text fields and file markers'); + +like(http_post_form('/content_form', + multipart_form( + { name => 'upload', filename => 'only.txt', value => 'AAA' }, + )), + qr/200.*null\|\|false\|false\|true\|upload\|$/s, + 'file parts are hidden from text accessors'); + +like(http_post_form('/content_form', + multipart_form( + { name => 'a\\"b', value => '1' }, + )), + qr/200.*a"b=1/s, 'quoted multipart parameter escapes are unescaped'); + +like(http_post_form('/content_form', + multipart_form({ name => 'empty', value => '' })), + qr/200.*null\|\|false\|false\|false\|\|empty=$/s, + 'empty multipart text field is preserved'); + +like(http_post_form('/content_form', + ['multipart/form-data; boundary=X', '--X--']), + qr/200.*null\|\|false\|false\|false\|\|$/s, + 'empty multipart body returns an empty form'); + +like(http_post_form('/content_form', + ['multipart/form-data; boundary=X', + '--X' . CRLF + . 'Content-Disposition: form-data; name="ows" ' . CRLF . CRLF + . '1' . CRLF + . '--X--']), + qr/200.*null\|\|false\|false\|false\|\|ows=1/s, + 'multipart header value trailing OWS is skipped'); + +my $fake_boundary = multipart_form( + { name => 'x', + value => "payload\r\n--FAKE\r\n\r\nContent-Disposition: form-data; " + . 'name="injected"' . "\r\n\r\nevil" }, +); + +like(http_post_form('/content_form', $fake_boundary), + qr/200.*x=payload/s, + 'fake multipart boundary in body is treated as payload'); +unlike(http_post_form('/content_form', $fake_boundary), + qr/injected=evil/s, + 'fake multipart boundary does not restart header parsing'); + +like(http_post_form('/content_form_error', urlencoded_form('a=%')), + qr/500.*malformed percent escape/s, + 'urlencoded bare % at end is rejected'); + +like(http_post_form('/content_form_error', urlencoded_form('a=%4')), + qr/500.*malformed percent escape/s, + 'urlencoded %X with missing second digit is rejected'); + +like(http_post_form('/content_form_error', urlencoded_form('a=%gg')), + qr/500.*malformed percent escape/s, + 'urlencoded non-hex percent escape is rejected'); + +like(http_post_form('/content_form_error', urlencoded_form('%Z=1')), + qr/500.*malformed percent escape/s, + 'urlencoded malformed percent escape in name is rejected'); + +like(http_post_form('/content_form_error', ['text/plain', 'a=1']), + qr/500.*TypeError:unsupported content type/s, + 'unsupported content type is rejected'); + +like(http_post_form('/content_form_error', [';boundary=X', '']), + qr/500.*TypeError:unsupported content type/s, + 'empty content type is rejected'); + +like(http_post_raw('/content_form_error', 'a=1'), + qr/500.*TypeError:request content type is required/s, + 'missing content type is rejected'); + +like(http_post_form('/content_form_error', + ['application/x-www-form-urlencoded; =x', 'a=1']), + qr/500.*malformed parameter/s, + 'malformed content type parameter is rejected'); + +like(http_post_form('/content_form_error', ['multipart/form-data', 'a=1']), + qr/500.*TypeError:multipart boundary is required/s, + 'multipart boundary is required'); + +like(http_post_form('/content_form_error', + ['multipart/form-data; boundary=""', '']), + qr/500.*(invalid multipart boundary|empty parameter value)/s, + 'empty quoted multipart boundary is rejected'); + +like(http_post_form('/content_form_error', + ["multipart/form-data; boundary=" . 'x' x 201, '']), + qr/500.*invalid multipart boundary/s, + 'multipart boundary over 200 bytes is rejected'); + +like(http_post_form('/content_form_error', + ['multipart/form-data; boundary=X; boundary=Y', '--X--']), + qr/500.*duplicate boundary parameter/s, + 'duplicate multipart boundary parameter is rejected'); + +like(http_post_form('/content_form_error', + ['multipart/form-data; boundary=X junk', '--X--']), + qr/500.*(malformed content type|malformed parameter)/s, + 'malformed trailing content type parameter data is rejected'); + +like(http_post_form('/content_form_error', + ['multipart/form-data; boundary=XXX', '--XXXjunk']), + qr/500.*malformed multipart boundary/s, + 'multipart opening delimiter without CRLF is rejected'); + +like(http_post_form('/content_form_error', + ['multipart/form-data; boundary=XXX', '-']), + qr/500.*malformed multipart body/s, + 'short multipart body without boundary marker is rejected'); + +like(http_post_form('/content_form_error', + ['multipart/form-data; boundary=X', + '--X' . CRLF + . 'Content-Disposition: form-data; name="a"']), + qr/500.*missing multipart header separator/s, + 'multipart part without header separator is rejected'); + +like(http_post_form('/content_form_error', + ['multipart/form-data; boundary=X', + '--X' . CRLF + . 'X-Large: ' . ('a' x 17000) . CRLF . CRLF + . 'data' . CRLF + . '--X--']), + qr/500.*multipart headers are too large/s, + 'multipart header block size limit is enforced'); + +like(http_post_form('/content_form_error', + ['multipart/form-data; boundary=X', + '--X' . CRLF + . 'X-Long: ' . ('a' x 4100) . CRLF + . 'Content-Disposition: form-data; name="a"' . CRLF . CRLF + . 'data' . CRLF + . '--X--']), + qr/500.*multipart header line is too long/s, + 'multipart header line size limit is enforced'); + +my $many_headers = join('', map { "X-$_: v" . CRLF } 1 .. 33); + +like(http_post_form('/content_form_error', + ['multipart/form-data; boundary=X', + '--X' . CRLF + . $many_headers + . 'Content-Disposition: form-data; name="a"' . CRLF . CRLF + . 'data' . CRLF + . '--X--']), + qr/500.*too many multipart headers/s, + 'multipart header count limit is enforced'); + +like(http_post_form('/content_form_error', + ['multipart/form-data; boundary=X', + '--X' . CRLF + . 'X-Other: foo' . CRLF . CRLF + . 'data' . CRLF + . '--X--']), + qr/500.*missing Content-Disposition header/s, + 'multipart part without Content-Disposition is rejected'); + +like(http_post_form('/content_form_error', + ['multipart/form-data; boundary=X', + '--X' . CRLF + . 'Content-Disposition: form-data; name="a"' . CRLF + . 'Content-Disposition: form-data; name="b"' . CRLF . CRLF + . 'data' . CRLF + . '--X--']), + qr/500.*duplicate Content-Disposition header/s, + 'duplicate multipart Content-Disposition header is rejected'); + +like(http_post_form('/content_form_error', + ['multipart/form-data; boundary=X', + '--X' . CRLF + . 'Content-Disposition: attachment; name="a"' . CRLF . CRLF + . 'data' . CRLF + . '--X--']), + qr/500.*unsupported disposition type/s, + 'unsupported multipart disposition type is rejected'); + +like(http_post_form('/content_form_error', + ['multipart/form-data; boundary=X', + '--X' . CRLF + . 'Content-Disposition: form-data' . CRLF . CRLF + . 'data' . CRLF + . '--X--']), + qr/500.*multipart field name is required/s, + 'multipart Content-Disposition without name is rejected'); + +like(http_post_form('/content_form_error', + ['multipart/form-data; boundary=X', + '--X' . CRLF + . 'Content-Disposition: form-data; name="a" junk' + . CRLF . CRLF + . 'data' . CRLF + . '--X--']), + qr/500.*malformed Content-Disposition/s, + 'multipart Content-Disposition trailing data is rejected'); + +like(http_post_form('/content_form_error', + ['multipart/form-data; boundary=X', + '--X' . CRLF + . 'Content-Disposition: form-data; name="a"; name="b"' + . CRLF . CRLF + . 'data' . CRLF + . '--X--']), + qr/500.*duplicate name parameter/s, + 'duplicate multipart name parameter is rejected'); + +like(http_post_form('/content_form_error', + ['multipart/form-data; boundary=X', + '--X' . CRLF + . 'Content-Disposition: form-data; name="a"; filename="x"; ' + . 'filename="y"' . CRLF . CRLF + . 'data' . CRLF + . '--X--']), + qr/500.*duplicate filename parameter/s, + 'duplicate multipart filename parameter is rejected'); + +like(http_post_form('/content_form_error', + ['multipart/form-data; boundary=X', + '--X' . CRLF + . 'Content-Disposition: form-data; name' . CRLF . CRLF + . 'data' . CRLF + . '--X--']), + qr/500.*malformed parameter/s, + 'multipart parameter without equals is rejected'); + +like(http_post_form('/content_form_error', + ['multipart/form-data; boundary=X', + '--X' . CRLF + . 'Content-Disposition: form-data; name=' . CRLF . CRLF + . 'data' . CRLF + . '--X--']), + qr/500.*empty parameter value/s, + 'multipart parameter with empty unquoted value is rejected'); + +like(http_post_form('/content_form_error', + ['multipart/form-data; boundary=X', + '--X' . CRLF + . 'Content-Disposition: form-data; name="a"; filename="x\\"' + . CRLF . CRLF + . 'data' . CRLF + . '--X--']), + qr/500.*unterminated quoted parameter/s, + 'multipart trailing backslash in quoted parameter is rejected'); + +like(http_post_form('/content_form_error', + ['multipart/form-data; boundary=X', + '--X' . CRLF + . 'NoColonHere' . CRLF + . 'Content-Disposition: form-data; name="a"' . CRLF . CRLF + . 'data' . CRLF + . '--X--']), + qr/500.*malformed multipart header/s, + 'multipart header line without colon is rejected'); + +like(http_post_form('/content_form_error', + ['multipart/form-data; boundary=X', + '--X' . CRLF + . 'Content-Disposition: form-data; name="a"' . CRLF . CRLF + . 'data' . CRLF + . '--Xjunk']), + qr/500.*malformed multipart boundary/s, + 'malformed multipart boundary after part is rejected'); + +like(http_post_form('/content_form_error', + ['multipart/form-data; boundary=XXX', 'no boundary here at all']), + qr/500.*malformed multipart body/s, + 'multipart body without boundary marker is rejected'); + +like(http_post_form('/content_form_error', + ['multipart/form-data; boundary=X', + '--X' . CRLF + . 'Content-Disposition: form-data; name="a"' . CRLF . CRLF + . 'value with no terminating boundary']), + qr/500.*truncated multipart body/s, + 'multipart body without closing boundary is rejected'); + +like(http_post_form('/content_form_limit', urlencoded_form('a=1&b=2')), + qr/500.*maxKeys limit exceeded/s, 'maxKeys limit breach rejects'); + +like(http_post_form('/content_form_limit', urlencoded_form('&a=1&&')), + qr/200.*no_error/s, 'urlencoded empty fields do not count for maxKeys'); + +like(http_post_form('/content_form_limit', + multipart_form({ name => 'a', value => '1' }, + { name => 'b', value => '2' })), + qr/500.*maxKeys limit exceeded/s, + 'multipart maxKeys limit breach rejects'); + +############################################################################### + +sub http_post_form { + my ($url, $form, %extra) = @_; + my ($content_type, $body) = @{$form}; + + my $r = "POST $url HTTP/1.0" . CRLF + . "Host: localhost" . CRLF + . "Content-Type: $content_type" . CRLF + . "Content-Length: " . length($body) . CRLF + . CRLF + . $body; + + return http($r, %extra); +} + +sub http_post_raw { + my ($url, $body, %extra) = @_; + + my $r = "POST $url HTTP/1.0" . CRLF + . "Host: localhost" . CRLF + . "Content-Length: " . length($body) . CRLF + . CRLF + . $body; + + return http($r, %extra); +} + +sub urlencoded_form { + my ($body) = @_; + + return ['application/x-www-form-urlencoded', $body]; +} + +sub multipart_form { + my (@parts) = @_; + my $boundary = '----test-boundary'; + my $body = ''; + + for my $part (@parts) { + $body .= '--' . $boundary . CRLF; + $body .= 'Content-Disposition: form-data; name="' . $part->{name} . '"'; + + if (defined $part->{filename}) { + $body .= '; filename="' . $part->{filename} . '"'; + } + + $body .= CRLF . CRLF; + $body .= $part->{value} . CRLF; + } + + $body .= '--' . $boundary . '--'; + + return ["multipart/form-data; boundary=$boundary", $body]; +} + +############################################################################### diff --git a/src/njs.h b/src/njs.h index 07e452fe6..fc212f8d4 100644 --- a/src/njs.h +++ b/src/njs.h @@ -11,8 +11,8 @@ #include -#define NJS_VERSION "0.9.8" -#define NJS_VERSION_NUMBER 0x000908 +#define NJS_VERSION "0.9.9" +#define NJS_VERSION_NUMBER 0x000909 #include diff --git a/test/ts/test.ts b/test/ts/test.ts index a30e02fe8..f810bedc6 100644 --- a/test/ts/test.ts +++ b/test/ts/test.ts @@ -69,6 +69,15 @@ async function http_module(r: NginxHTTPRequest) { // r.requestBuffer r.requestBuffer?.equals(Buffer.from([1])); + // r.readRequestText + let text: string = await r.readRequestText(); + + // r.readRequestArrayBuffer + let buf: ArrayBuffer = await r.readRequestArrayBuffer(); + + // r.readRequestJSON + let json: any = await r.readRequestJSON(); + // r.responseText r.responseText == 'a'; r.responseText?.startsWith('a'); diff --git a/ts/ngx_http_js_module.d.ts b/ts/ngx_http_js_module.d.ts index d7dd1c9ed..f0b30d5c4 100644 --- a/ts/ngx_http_js_module.d.ts +++ b/ts/ngx_http_js_module.d.ts @@ -273,6 +273,19 @@ interface NginxHTTPSendBufferOptions { flush?: boolean } +/** + * @since 0.9.8 + */ +interface NginxHTTPRequestForm { + get(name: NjsStringOrBuffer): string | null; + getAll(name: NjsStringOrBuffer): string[]; + has(name: NjsStringOrBuffer): boolean; + forEach(callback: (value: string, key: string, + form: NginxHTTPRequestForm) => void, thisArg?: any): void; + hasFiles(): boolean; + fileFieldNames(): string[]; +} + interface NginxHTTPRequest { /** * Request arguments object. @@ -381,6 +394,51 @@ interface NginxHTTPRequest { * @deprecated Use `requestText` or `requestBuffer` instead. */ readonly requestBody?: string; + /** + * Reads the client request body and returns a Promise resolving + * with the body as a string. + * + * Available in js_access and js_content directives. The request body + * size is limited by client_max_body_size. + * + * @returns A Promise that resolves with the request body as a string. + * @since 0.9.8 + */ + readRequestText(): Promise; + /** + * Reads the client request body and returns a Promise resolving + * with the body as an ArrayBuffer. + * + * Available in js_access and js_content directives. The request body + * size is limited by client_max_body_size. + * + * @returns A Promise that resolves with the request body + * as an ArrayBuffer. + * @since 0.9.8 + */ + readRequestArrayBuffer(): Promise; + /** + * Reads the client request body and returns a Promise resolving + * with the body parsed as JSON. + * + * Available in js_access and js_content directives. The request body + * size is limited by client_max_body_size. + * + * @returns A Promise that resolves with the parsed JSON value. + * @since 0.9.8 + */ + readRequestJSON(): Promise; + /** + * Reads the client request body and parses it as a supported HTML form. + * + * Supports `application/x-www-form-urlencoded` and + * `multipart/form-data`. + * + * File parts are detected but their contents are not exposed. + * + * @since 0.9.8 + */ + readRequestForm(options?: { maxKeys?: number }): Promise; /** * Subrequest response body. The size of response body is limited by * the subrequest_output_buffer_size directive. @@ -414,6 +472,13 @@ interface NginxHTTPRequest { * @param body Respose body. */ return(status: number, body?: NjsStringOrBuffer): void; + /** + * Signals that the handler has no opinion about whether access + * should be allowed or denied. Useful with the ``satisfy any`` + * directive: without this call the handler implicitly allows + * access (returns NGX_OK to the access phase checker). + */ + decline(): void; /** * Sends a part of the response body to the client. */