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 \
#include <ngx_http.h>
#include "ngx_js.h"
#include "ngx_js_modules.h"
+#include "ngx_js_form.h"
typedef struct {
#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
- unsigned body_read_state: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.
/* Pending promise/event for deferred body reads. */
void *body_read_event;
+
+ ngx_js_form_t *request_form;
};
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_entry_value(njs_vm_t *vm,
+ ngx_js_form_entry_t *entry, 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_read_request_body(njs_vm_t *vm,
njs_value_t *args, njs_uint_t nargs, njs_index_t magic,
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 JSValue ngx_http_qjs_request_form_entry_value(JSContext *cx,
+ ngx_js_form_entry_t *entry);
+static ngx_int_t ngx_http_qjs_request_form_max_keys(JSContext *cx,
+ JSValueConst options, ngx_uint_t *max_keys);
+static JSValue ngx_http_qjs_request_form_make(JSContext *cx,
+ ngx_js_form_t *form);
+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 void ngx_http_js_read_body_event_destructor(ngx_qjs_event_t *event);
#endif
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[] = {
}
},
+ {
+ .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"),
};
+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,
+ }
+ },
+};
+
+
static njs_external_t ngx_http_js_ext_periodic_session[] = {
{
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),
};
+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),
+};
+
+
static JSClassDef ngx_http_qjs_request_class = {
"Request",
.finalizer = ngx_http_qjs_request_finalizer,
};
+static JSClassDef ngx_http_qjs_request_form_class = {
+ "RequestForm",
+ .finalizer = NULL,
+};
+
+
static JSClassDef ngx_http_qjs_variables_class = {
"Variables",
.finalizer = NULL,
/* JS called readRequest*(). */
- if (ctx->body_read_state == NGX_HTTP_JS_BODY_READ_DEFERRED) {
+ 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);
goto done;
}
- ctx->body_read_state = NGX_HTTP_JS_BODY_READ_IN_PROGRESS;
+ 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;
}
+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;
+ }
+
+ return NJS_OK;
+}
+
+
+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);
+ }
+
+ 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 (ctx->body_read_state) {
+ 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;
ev = event;
vm = ctx->engine->u.njs.vm;
- rc = ngx_http_js_body_to_value(vm, ctx, (uintptr_t) ev->data,
- njs_value_arg(&result));
+ 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));
* 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 (ctx->body_read_state == NGX_HTTP_JS_BODY_READ_DEFERRED) {
+ if (ngx_http_js_body_read_is_deferred(ctx->body_read_state)) {
r->main->count--;
}
ngx_js_add_event(ctx, event);
- ctx->body_read_event = event;
- ctx->body_read_state = NGX_HTTP_JS_BODY_READ_DEFERRED;
+ 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 || 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].name.len != name.length
+ || ngx_memcmp(entry[i].name.data, name.start, name.length) != 0)
+ {
+ continue;
+ }
+
+ if (!as_array) {
+ return ngx_http_js_request_form_entry_value(vm, &entry[i], retval);
+ }
+
+ value = njs_vm_array_push(vm, retval);
+ if (value == NULL) {
+ return NJS_ERROR;
+ }
+
+ rc = ngx_http_js_request_form_entry_value(vm, &entry[i], value);
+ 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].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++) {
+ njs_value_assign(&arguments[0], this_arg);
+
+ rc = ngx_http_js_request_form_entry_value(vm, &entry[i],
+ njs_value_arg(&arguments[1]));
+ 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_request_form_entry_value(njs_vm_t *vm, ngx_js_form_entry_t *entry,
+ njs_value_t *retval)
+{
+ njs_int_t rc;
+ njs_opaque_value_t value;
+
+ static const njs_str_t name_key = njs_str("name");
+
+ if (!entry->is_file) {
+ return njs_vm_value_string_create(vm, retval, entry->value.data,
+ entry->value.len);
+ }
+
+ rc = njs_vm_object_alloc(vm, retval, NULL);
+ if (rc != NJS_OK) {
+ return NJS_ERROR;
+ }
+
+ rc = njs_vm_value_string_create(vm, njs_value_arg(&value),
+ entry->filename.data, entry->filename.len);
+ if (rc != NJS_OK) {
+ return NJS_ERROR;
+ }
+
+ return njs_vm_object_prop_set(vm, retval, &name_key, &value);
+}
- return NJS_OK;
-resolve:
+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;
- if (ngx_http_js_collect_body(r, ctx) != NGX_OK) {
- njs_vm_memory_error(vm);
+ 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;
}
- return ngx_http_js_body_to_value(vm, ctx, (ngx_uint_t) magic, retval);
+ njs_value_boolean_set(retval, form->has_files);
+
+ return NJS_OK;
}
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));
ev = event;
cx = ctx->engine->u.qjs.ctx;
- result = ngx_http_qjs_body_to_value(cx, ctx, (uintptr_t) ev->data);
+ 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);
}
+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)
+{
+ JSValue obj;
+
+ obj = JS_NewObjectClass(cx, NGX_QJS_CLASS_ID_HTTP_FORM);
+ if (JS_IsException(obj)) {
+ return JS_EXCEPTION;
+ }
+
+ JS_SetOpaque(obj, form);
+
+ return obj;
+}
+
+
+static JSValue
+ngx_http_qjs_form_to_value(JSContext *cx, ngx_http_request_t *r,
+ ngx_http_js_ctx_t *ctx, ngx_uint_t max_keys)
+{
+ ngx_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_qjs_request_form_make(cx, form);
+ }
+
+ if (rc == NGX_JS_FORM_TYPE_ERROR) {
+ return JS_ThrowTypeError(cx, "%.*s", (int) error.len, error.data);
+ }
+
+ 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_read_request_form(JSContext *cx, JSValueConst this_val,
+ int argc, JSValueConst *argv)
+{
+ 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;
+
+ req = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_REQUEST);
+ if (req == NULL) {
+ return JS_ThrowInternalError(cx, "\"this\" is not a request object");
+ }
+
+ 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);
+
+ 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) max_keys;
+
+ 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
+ | 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_form_get(JSContext *cx, JSValueConst this_val,
+ int argc, JSValueConst *argv, int as_array)
+{
+ 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;
+
+ 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");
+ }
+
+ if (as_array) {
+ array = JS_NewArray(cx);
+ if (JS_IsException(array)) {
+ JS_FreeCString(cx, name);
+ return JS_EXCEPTION;
+ }
+
+ } else {
+ array = JS_UNDEFINED;
+ }
+
+ entry = form->entries.elts;
+ n = 0;
+
+ for (i = 0; i < form->entries.nelts; i++) {
+ if (entry[i].name.len != name_len
+ || ngx_memcmp(entry[i].name.data, name, name_len) != 0)
+ {
+ continue;
+ }
+
+ value = ngx_http_qjs_request_form_entry_value(cx, &entry[i]);
+ 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;
+ }
+ }
+
+ JS_FreeCString(cx, name);
+
+ if (as_array) {
+ return array;
+ }
+
+ 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].name.len == name_len
+ && ngx_memcmp(entry[i].name.data, name, name_len) == 0)
+ {
+ JS_FreeCString(cx, name);
+ return JS_NewBool(cx, 1);
+ }
+ }
+
+ JS_FreeCString(cx, name);
+
+ 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++) {
+ args[0] = ngx_http_qjs_request_form_entry_value(cx, &entry[i]);
+ 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;
+ }
+
+ 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);
+ }
+
+ return JS_UNDEFINED;
+}
+
+
+static JSValue
+ngx_http_qjs_request_form_entry_value(JSContext *cx,
+ ngx_js_form_entry_t *entry)
+{
+ JSValue object, value;
+
+ if (!entry->is_file) {
+ return qjs_string_create(cx, entry->value.data, entry->value.len);
+ }
+
+ object = JS_NewObject(cx);
+ if (JS_IsException(object)) {
+ return JS_EXCEPTION;
+ }
+
+ value = qjs_string_create(cx, entry->filename.data, entry->filename.len);
+ if (JS_IsException(value)) {
+ JS_FreeValue(cx, object);
+ return JS_EXCEPTION;
+ }
+
+ if (JS_SetPropertyStr(cx, object, "name", value) < 0) {
+ JS_FreeValue(cx, value);
+ JS_FreeValue(cx, object);
+ return JS_EXCEPTION;
+ }
+
+ return object;
+}
+
+
+static JSValue
+ngx_http_qjs_ext_request_form_has_files(JSContext *cx, JSValueConst this_val,
+ int argc, JSValueConst *argv)
+{
+ ngx_js_form_t *form;
+
+ form = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_FORM);
+ if (form == NULL) {
+ return JS_ThrowInternalError(cx, "\"this\" is not a RequestForm");
+ }
+
+ return JS_NewBool(cx, form->has_files);
+}
+
+
static void
ngx_http_js_read_body_event_destructor(ngx_qjs_event_t *event)
{
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)
{
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,
--- /dev/null
+/*
+ * Copyright (C) Dmitry Volyntsev
+ * Copyright (C) F5, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#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_str_t *filename, 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 *filename, 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 *filename, 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_int_t rc;
+ 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);
+
+ rc = ngx_js_form_parse_param(pool, &p, end, ¶m, &value, "ed,
+ error);
+ if (rc != NGX_JS_FORM_OK) {
+ return rc;
+ }
+
+ 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_int_t rc;
+ 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++;
+ }
+
+ rc = ngx_js_form_decode_urlencoded(pool, p, eq, &name, error);
+ if (rc != NGX_JS_FORM_OK) {
+ return rc;
+ }
+
+ if (eq < amp) {
+ eq++;
+ }
+
+ rc = ngx_js_form_decode_urlencoded(pool, eq, amp, &value, error);
+ if (rc != NGX_JS_FORM_OK) {
+ return rc;
+ }
+
+ rc = ngx_js_form_add_entry(form, pool, &name, &value, &count, max_keys,
+ NULL, 0, error);
+ if (rc != NGX_JS_FORM_OK) {
+ return rc;
+ }
+
+ 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_int_t rc;
+ ngx_str_t name, value, filename;
+ 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] = '-';
+
+ /*
+ * Validate the body opening: a dash-boundary "--BOUNDARY" must
+ * appear, and the close-delimiter "--BOUNDARY--" must not appear
+ * before it. The dash-boundary is a prefix of the close-delimiter,
+ * so both searches use the same buffer with different lengths
+ * (dlen = "--BOUNDARY", cdlen = "--BOUNDARY--").
+ */
+ 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;
+ }
+
+ rc = ngx_js_form_parse_part_headers(pool, p, headers_end, &name,
+ &is_file, &filename, error);
+ if (rc != NGX_JS_FORM_OK) {
+ return rc;
+ }
+
+ 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;
+ }
+ }
+
+ rc = ngx_js_form_add_entry(form, pool, &name, &value, &count, max_keys,
+ &filename, is_file, error);
+ if (rc != NGX_JS_FORM_OK) {
+ return rc;
+ }
+
+ 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 *filename,
+ ngx_str_t *error)
+{
+ u_char *p, *line, *colon, *line_end;
+ ngx_int_t rc;
+ 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;
+ filename->len = 0;
+ filename->data = (u_char *) "";
+
+ *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;
+ }
+
+ rc = ngx_js_form_parse_disposition(pool, &value, name, is_file,
+ filename, error);
+ if (rc != NGX_JS_FORM_OK) {
+ return rc;
+ }
+
+ 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 *filename,
+ ngx_str_t *error)
+{
+ u_char *p, *end;
+ ngx_int_t rc;
+ 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);
+
+ rc = ngx_js_form_parse_param(pool, &p, end, ¶m, ¶m_value,
+ "ed, error);
+ if (rc != NGX_JS_FORM_OK) {
+ return rc;
+ }
+
+ 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;
+ *filename = param_value;
+ 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_str_t *filename, 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;
+
+ if (filename != NULL) {
+ entry->filename = *filename;
+
+ } else {
+ ngx_str_null(&entry->filename);
+ }
+
+ 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;
+}
--- /dev/null
+/*
+ * Copyright (C) Dmitry Volyntsev
+ * Copyright (C) F5, Inc.
+ */
+
+
+#ifndef _NGX_JS_FORM_H_INCLUDED_
+#define _NGX_JS_FORM_H_INCLUDED_
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+
+#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;
+ ngx_str_t filename;
+ 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_ */
--- /dev/null
+#!/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_text_then_form {
+ js_content test.content_text_then_form;
+ }
+
+ location /content_form_error {
+ js_content test.content_form_error;
+ }
+
+ location /content_form_limit {
+ js_content test.content_form_limit;
+ }
+
+ location /content_form_no_options {
+ js_content test.content_form_no_options;
+ }
+ }
+}
+
+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 files = [];
+ let pairs = [];
+ let upload = form.get('upload');
+ let uploadAll = form.getAll('upload')
+ .map(v => typeof v == 'string' ? v : v.name);
+ let uploadFirst = '';
+
+ if (first === null) {
+ first = 'null';
+
+ } else if (typeof first != 'string') {
+ first = `[file:${first.name}]`;
+ }
+
+ if (upload !== null && typeof upload != 'string') {
+ uploadFirst = upload.name;
+ }
+
+ form.forEach((value, key) => {
+ if (typeof value == 'string') {
+ pairs.push(`${key}=${value}`);
+ return;
+ }
+
+ files.push(`${key}:${value.name}`);
+ pairs.push(`${key}=[file:${value.name}]`);
+ });
+
+ return [
+ first,
+ form.getAll('a')
+ .map(v => typeof v == 'string' ? v : `[file:${v.name}]`)
+ .join(','),
+ form.has('a'),
+ form.has('upload'),
+ form.hasFiles(),
+ files.length == 0 ? ''
+ : `get:${uploadFirst};all:${uploadAll};`
+ + `each:${files.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_text_then_form(r) {
+ let text = await r.readRequestText();
+ let form = await r.readRequestForm({maxKeys: 8});
+
+ r.return(200, `${text.length}|${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);
+ }
+ }
+
+ async function content_form_no_options(r) {
+ try {
+ r.return(200, render(await r.readRequestForm({})));
+
+ } catch (e) {
+ r.return(500, `${e.constructor.name}:${e.message}`);
+ }
+ }
+
+ export default { access_form, content, content_form, content_form_hex,
+ content_form_cache, content_text_then_form,
+ content_form_error, content_form_limit,
+ content_form_no_options };
+EOF
+
+$t->try_run('no readRequestForm')->plan(60);
+
+###############################################################################
+
+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_text_then_form',
+ urlencoded_form('a=1&a=2&z=3')),
+ qr/200.*11\|1\|1,2\|true\|false\|false\|\|a=1&a=2&z=3$/s,
+ 'readRequestText() then readRequestForm() reuses cached body');
+
+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\|true\|true\|
+ get:a.txt;all:a.txt,b.txt;each:upload:a.txt,upload:b.txt\|
+ a=1&upload=\[file:a.txt\]&a=2&upload=\[file:b.txt\]&z=3
+ }sx,
+ 'multipart text fields and file metadata');
+
+like(http_post_form('/content_form',
+ multipart_form(
+ { name => 'upload', filename => 'only.txt', value => 'AAA' },
+ )),
+ qr{
+ 200.*null\|\|false\|true\|true\|
+ get:only.txt;all:only.txt;each:upload:only.txt\|
+ upload=\[file:only.txt\]$
+ }sx,
+ 'file parts expose filename metadata');
+
+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');
+
+like(http_post_form('/content_form_no_options', urlencoded_form('a=1&b=2')),
+ qr/200.*1\|1\|true\|false\|false\|\|a=1&b=2/s,
+ 'readRequestForm({}) accepts an empty options object');
+
+my $utf8_filename = "\xe6\x97\xa5\xe6\x9c\xac.txt";
+like(http_post_form('/content_form',
+ multipart_form(
+ { name => 'upload', filename => $utf8_filename, value => 'AAA' },
+ )),
+ qr{200.*null\|\|false\|true\|true\|
+ get:\Q$utf8_filename\E;all:\Q$utf8_filename\E;
+ each:upload:\Q$utf8_filename\E\|
+ upload=\[file:\Q$utf8_filename\E\]$}sx,
+ 'multipart filename preserves raw UTF-8 bytes');
+
+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];
+}
+
+###############################################################################
flush?: boolean
}
+/**
+ * @since 0.9.9
+ */
+interface NginxHTTPRequestFormFile {
+ readonly name: string;
+}
+
+type NginxHTTPRequestFormValue = string | NginxHTTPRequestFormFile;
+
+interface NginxHTTPRequestForm {
+ get(name: NjsStringOrBuffer): NginxHTTPRequestFormValue | null;
+ getAll(name: NjsStringOrBuffer): NginxHTTPRequestFormValue[];
+ has(name: NjsStringOrBuffer): boolean;
+ forEach(callback: (value: NginxHTTPRequestFormValue, key: string,
+ form: NginxHTTPRequestForm) => void, thisArg?: any): void;
+ hasFiles(): boolean;
+}
+
interface NginxHTTPRequest {
/**
* Request arguments object.
* Available in js_access and js_content directives. The request body
* size is limited by client_max_body_size.
*
- * The body is read once and cached on the request: subsequent
- * `readRequestText`, `readRequestArrayBuffer`, and `readRequestJSON`
- * calls resolve synchronously from the cache and do not re-read the
- * wire. This deliberately differs from the WHATWG Fetch Body mixin
- * (which makes the body unusable after the first call) and matches
- * the server-side caching pattern used by Express, Flask, and similar
- * frameworks.
+ * The body is read once and cached on the request: subsequent reads
+ * across any combination of `readRequestText`, `readRequestArrayBuffer`,
+ * `readRequestJSON`, and `readRequestForm` resolve synchronously from
+ * the cache and do not re-read the wire. This deliberately differs
+ * from the WHATWG Fetch Body mixin (which makes the body unusable
+ * after the first call) and matches the server-side caching pattern
+ * used by Express, Flask, and similar frameworks.
*
* A second call issued while a previous `readRequest*` promise has
* not yet resolved throws `"request body is already being read"`.
* @since 0.9.9
*/
readRequestJSON(): Promise<any>;
+ /**
+ * Reads the client request body and parses it as a supported HTML form.
+ *
+ * Supports `application/x-www-form-urlencoded` and
+ * `multipart/form-data`.
+ *
+ * For text fields, the value is the decoded string. For file parts,
+ * the value is a File-like object exposing only the client-supplied
+ * filename via `name`. File contents are not exposed in this release.
+ *
+ * Filename is client-supplied and not sanitized - validate it before
+ * using it for filesystem paths, log lines, or redirects.
+ *
+ * See {@link readRequestText} for body caching, concurrency, and
+ * availability semantics. In addition, the parsed form is itself
+ * cached: a second call returns the same parsed result and ignores
+ * any new options argument.
+ *
+ * @since 0.9.9
+ */
+ readRequestForm(options?: { maxKeys?: number }): Promise<NginxHTTPRequestForm>;
/**
* Subrequest response body. The size of response body is limited by
* the subrequest_output_buffer_size directive.