From noreply at nginx.com Thu May 1 02:12:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Thu, 1 May 2025 02:12:02 +0000 (UTC) Subject: [njs] Fixed building with --debug=YES after b28e50b1. Message-ID: <20250501021202.2A5A147CA7@pubserv1.nginx> details: https://github.com/nginx/njs/commit/551ae185972779ee24b67f645212145ddd5522dc branches: master commit: 551ae185972779ee24b67f645212145ddd5522dc user: Dmitry Volyntsev date: Wed, 30 Apr 2025 18:23:20 -0700 description: Fixed building with --debug=YES after b28e50b1. In file included from src/njs_main.h:37, from src/njs_diyfp.c:12: src/njs_atom.h: In function ‘njs_atom_to_value’: src/njs_atom.h:54:31: error: invalid use of incomplete typedef ‘njs_flathsh_descr_t’ {aka ‘struct njs_flathsh_descr_s’} 54 | njs_assert(atom_id < h->elts_count); | ^~ src/njs_assert.h:14:15: note: in definition of macro ‘njs_assert’ --- src/njs_flathsh.c | 8 -------- src/njs_flathsh.h | 9 ++++++++- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/njs_flathsh.c b/src/njs_flathsh.c index f2387c95..a9ef6e69 100644 --- a/src/njs_flathsh.c +++ b/src/njs_flathsh.c @@ -76,14 +76,6 @@ #define NJS_FLATHSH_ELTS_MINIMUM_TO_SHRINK 8 -struct njs_flathsh_descr_s { - uint32_t hash_mask; - uint32_t elts_size; /* allocated properties */ - uint32_t elts_count; /* include deleted properties */ - uint32_t elts_deleted_count; -}; - - static njs_flathsh_descr_t *njs_flathsh_alloc(njs_flathsh_query_t *fhq, size_t hash_size, size_t elts_size); static njs_flathsh_descr_t *njs_expand_elts(njs_flathsh_query_t *fhq, diff --git a/src/njs_flathsh.h b/src/njs_flathsh.h index 651afcff..985bdab1 100644 --- a/src/njs_flathsh.h +++ b/src/njs_flathsh.h @@ -19,7 +19,14 @@ typedef struct { } njs_flathsh_elt_t; -typedef struct njs_flathsh_descr_s njs_flathsh_descr_t; +typedef struct { + uint32_t hash_mask; + uint32_t elts_size; /* allocated properties */ + uint32_t elts_count; /* include deleted properties */ + uint32_t elts_deleted_count; +} njs_flathsh_descr_t; + + typedef struct njs_flathsh_query_s njs_flathsh_query_t; typedef njs_int_t (*njs_flathsh_test_t)(njs_flathsh_query_t *fhq, void *data); From noreply at nginx.com Thu May 1 02:12:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Thu, 1 May 2025 02:12:02 +0000 (UTC) Subject: [njs] Using invalid UTF-8 string as atom number 0. Message-ID: <20250501021202.2D74147E89@pubserv1.nginx> details: https://github.com/nginx/njs/commit/4562abcb2fe5c2d916371c1885b62e96aeb99807 branches: master commit: 4562abcb2fe5c2d916371c1885b62e96aeb99807 user: Dmitry Volyntsev date: Wed, 30 Apr 2025 18:32:39 -0700 description: Using invalid UTF-8 string as atom number 0. This prevents a clash with possible valid UTF-8 strings. --- src/njs_atom_defs.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/njs_atom_defs.h b/src/njs_atom_defs.h index f940648a..6328c34b 100644 --- a/src/njs_atom_defs.h +++ b/src/njs_atom_defs.h @@ -5,7 +5,7 @@ */ -NJS_DEF_STRING(unknown, "unknown", 0, NJS_TOKEN_ILLEGAL) +NJS_DEF_STRING(unknown, "\xFF\xFF", 0, NJS_TOKEN_ILLEGAL) NJS_DEF_SYMBOL(asyncIterator, "Symbol.asyncIterator") NJS_DEF_SYMBOL(hasInstance, "Symbol.hasInstance") From noreply at nginx.com Thu May 1 02:39:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Thu, 1 May 2025 02:39:02 +0000 (UTC) Subject: [njs] Fixed regexp undefined value of captured group. Message-ID: <20250501023902.6979247E8E@pubserv1.nginx> details: https://github.com/nginx/njs/commit/30b53314923d9f33605a015f52ae94c195e0e940 branches: master commit: 30b53314923d9f33605a015f52ae94c195e0e940 user: Vadim Zhestikov date: Wed, 30 Apr 2025 14:20:31 -0700 description: Fixed regexp undefined value of captured group. Found by OSS-Fuzz. --- src/njs_regexp.c | 19 ++----------------- src/test/njs_unit_test.c | 3 +++ 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/src/njs_regexp.c b/src/njs_regexp.c index 4f1bb180..ed560b18 100644 --- a/src/njs_regexp.c +++ b/src/njs_regexp.c @@ -1148,24 +1148,12 @@ done: static void njs_regexp_exec_result_free(njs_vm_t *vm, njs_array_t *result) { - njs_uint_t n; - njs_value_t *start; njs_flathsh_t *hash; njs_object_prop_t *prop; njs_flathsh_elt_t *elt; njs_flathsh_each_t lhe; njs_flathsh_query_t lhq; - if (result->object.fast_array) { - start = result->start; - - for (n = 0; n < result->length; n++) { - if (start[n].type == NJS_STRING) { - njs_mp_free(vm->mem_pool, start[n].string.data); - } - } - } - njs_flathsh_each_init(&lhe, &njs_object_hash_proto); hash = &result->object.hash; @@ -1581,7 +1569,7 @@ njs_regexp_prototype_symbol_split(njs_vm_t *vm, njs_value_t *args, njs_value_t r, z, this, s_lvalue, setval, constructor; njs_object_t *object; const u_char *start, *end; - njs_string_prop_t s, sv; + njs_string_prop_t s; njs_value_t arguments[2]; rx = njs_argument(args, 0); @@ -1771,10 +1759,7 @@ njs_regexp_prototype_symbol_split(njs_vm_t *vm, njs_value_t *args, return NJS_ERROR; } - (void) njs_string_prop(vm, &sv, retval); - - ret = njs_array_string_add(vm, array, sv.start, sv.size, - sv.length); + ret = njs_array_add(vm, array, retval); if (njs_slow_path(ret != NJS_OK)) { return ret; } diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c index cc1cc4c9..2227c0a6 100644 --- a/src/test/njs_unit_test.c +++ b/src/test/njs_unit_test.c @@ -9791,6 +9791,9 @@ static njs_unit_test_t njs_test[] = { njs_str("'myCamelCaseString'.split(/(?=[A-Z])/)"), njs_str("my,Camel,Case,String") }, + { njs_str("var r = ' a'.split(/|()/); r+'|'+typeof r[0]+':'+typeof r[1]+':'+typeof r[2]"), + njs_str(" ,,a|string:undefined:string") }, + { njs_str("'мояВерблюжьяСтрока'.split(/(?=[А-Я])/)"), njs_str("моя,Верблюжья,Строка") }, From noreply at nginx.com Fri May 2 01:33:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 2 May 2025 01:33:02 +0000 (UTC) Subject: [njs] Tests: fixed js_body_filter.t. Message-ID: <20250502013302.2198C47E94@pubserv1.nginx> details: https://github.com/nginx/njs/commit/4fbe8a6511949c6137397f98fad9342e0e0bc76c branches: master commit: 4fbe8a6511949c6137397f98fad9342e0e0bc76c user: Dmitry Volyntsev date: Thu, 1 May 2025 09:06:25 -0700 description: Tests: fixed js_body_filter.t. The 1496ed3f commit made visible a problem with the fragile filter tests which depend on the exact sequence of data chunks. The fix is to use perl http server to ensure the order. --- nginx/t/js_body_filter.t | 132 +++++++++++++++++++++++++++++------------------ 1 file changed, 83 insertions(+), 49 deletions(-) diff --git a/nginx/t/js_body_filter.t b/nginx/t/js_body_filter.t index 5fc8292a..6bd6bddb 100644 --- a/nginx/t/js_body_filter.t +++ b/nginx/t/js_body_filter.t @@ -12,6 +12,8 @@ use strict; use Test::More; +use Socket qw/ CRLF IPPROTO_TCP TCP_NODELAY /; + BEGIN { use FindBin; chdir($FindBin::Bin); } use lib 'lib'; @@ -79,21 +81,6 @@ http { proxy_pass http://127.0.0.1:8081/source; } } - - server { - listen 127.0.0.1:8081; - server_name localhost; - - location /source { - postpone_output 1; - js_content test.source; - } - - location /nonutf8_source { - postpone_output 1; - js_content test.nonutf8_source; - } - } } EOF @@ -124,39 +111,6 @@ $t->write_file('test.js', < a + b.length, 0); - r.sendHeader(); - chain(chunks, 0); - } - - function nonutf8_source(r) { - var chunks = ['aaaa', 'bb', 'cc', 'dddd'].map(v=>Buffer.from(v, 'hex')); - chunks.delay = 5; - chunks.r = r; - chunks.chain = chain; - - r.status = 200; - r.sendHeader(); - chain(chunks, 0); - } - function filter(r, data, flags) { if (flags.last || data.length >= Number(r.args.len)) { r.sendBuffer(`\${data}#`, flags); @@ -178,12 +132,15 @@ $t->write_file('test.js', <try_run('no njs body filter')->plan(8); +$t->run_daemon(\&http_daemon, port(8081)); +$t->waitforsocket('127.0.0.1:' . port(8081)); + ############################################################################### like(http_get('/append'), qr/AAABBCDDDDXXX$/, 'append'); @@ -198,3 +155,80 @@ like(http_get('/filter?len=2&dup=1'), qr/AAA#AAABB#BBDDDD#DDDD#$/, like(http_get('/prepend'), qr/XXXAAABBCDDDD$/, 'prepend'); ############################################################################### + +sub http_daemon { + my $port = shift; + my $delay = shift || 0.05; + + my $server = IO::Socket::INET->new( + Proto => 'tcp', + LocalAddr => '127.0.0.1:' . $port, + Listen => 5, + Reuse => 1 + ) or die "Can't create listening socket: $!\n"; + + local $SIG{PIPE} = 'IGNORE'; + + while (my $client = $server->accept()) { + $client->autoflush(1); + + setsockopt($client, IPPROTO_TCP, TCP_NODELAY, 1) + or die "Can't set TCP_NODELAY: $!\n"; + + my $headers = ''; + my $uri = ''; + + while (<$client>) { + $headers .= $_; + last if (/^\x0d?\x0a?$/); + } + + $uri = $1 if $headers =~ /^\S+\s+([^ ]+)\s+HTTP/i; + $uri =~ s/\?.*//; + + log2c("(new connection $client $uri)"); + + if ($uri eq '/source') { + print $client + "HTTP/1.1 200 OK" . CRLF . + "Content-Length: 10" . CRLF . + "Connection: close" . CRLF . + CRLF; + + print $client "AAA"; + select undef, undef, undef, $delay; + print $client "BB"; + select undef, undef, undef, $delay; + print $client "C"; + select undef, undef, undef, $delay; + print $client "DDDD"; + + } elsif ($uri eq '/nonutf8_source') { + print $client + "HTTP/1.1 200 OK" . CRLF . + "Content-Length: 6" . CRLF . + "Connection: close" . CRLF . + CRLF; + + print $client "\xaa\xaa"; + select undef, undef, undef, $delay; + print $client "\xbb"; + select undef, undef, undef, $delay; + print $client "\xcc"; + select undef, undef, undef, $delay; + print $client "\xdd\xdd"; + + } else { + print $client + "HTTP/1.1 404 Not Found" . CRLF . + "Connection: close" . CRLF . + CRLF; + } + + $client->close(); + } +} + +sub log2c { Test::Nginx::log_core('||', @_); } + +############################################################################### From noreply at nginx.com Fri May 2 01:59:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 2 May 2025 01:59:02 +0000 (UTC) Subject: [njs] Fixed GCC 15 build with -Wunterminated-string-initialization. Message-ID: <20250502015902.55ECF47E95@pubserv1.nginx> details: https://github.com/nginx/njs/commit/e3cfb4f70e203866c1bd06e5fb28fcdc7dd967f8 branches: master commit: e3cfb4f70e203866c1bd06e5fb28fcdc7dd967f8 user: Dmitry Volyntsev date: Thu, 1 May 2025 17:05:56 -0700 description: Fixed GCC 15 build with -Wunterminated-string-initialization. In file included from src/njs_main.h:48, from src/njs_diyfp.c:12: src/njs_string.h: In function ‘njs_string_encode’: src/njs_string.h:229:36: error: initializer-string for array of ‘unsigned char’ truncates NUL terminator but destination lacks ‘nonstring’ attribute ( 17 chars into 16 available) [-Werror=unterminated-string-initialization] 229 | static const u_char hex[16] = "0123456789ABCDEF"; --- external/qjs_query_string_module.c | 2 +- src/njs_sprintf.c | 4 ++-- src/njs_string.c | 2 +- src/njs_string.h | 2 +- src/qjs_buffer.c | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/external/qjs_query_string_module.c b/external/qjs_query_string_module.c index bb787229..8f695677 100644 --- a/external/qjs_query_string_module.c +++ b/external/qjs_query_string_module.c @@ -537,7 +537,7 @@ qjs_string_encode(const uint32_t *escape, size_t size, const u_char *src, u_char *dst) { uint8_t byte; - static const u_char hex[16] = "0123456789ABCDEF"; + static const u_char hex[] = "0123456789ABCDEF"; do { byte = *src++; diff --git a/src/njs_sprintf.c b/src/njs_sprintf.c index 16ae9004..64fae9c2 100644 --- a/src/njs_sprintf.c +++ b/src/njs_sprintf.c @@ -95,8 +95,8 @@ njs_vsprintf(u_char *buf, u_char *end, const char *fmt, va_list args) njs_bool_t sign; njs_sprintf_t spf; - static const u_char hexadecimal[16] = "0123456789abcdef"; - static const u_char HEXADECIMAL[16] = "0123456789ABCDEF"; + static const u_char hexadecimal[] = "0123456789abcdef"; + static const u_char HEXADECIMAL[] = "0123456789ABCDEF"; static const u_char nan[] = "[nan]"; static const u_char infinity[] = "[infinity]"; diff --git a/src/njs_string.c b/src/njs_string.c index c38f455b..6d7c464d 100644 --- a/src/njs_string.c +++ b/src/njs_string.c @@ -252,7 +252,7 @@ njs_encode_hex(njs_str_t *dst, const njs_str_t *src) size_t i, len; const u_char *start; - static const u_char hex[16] = "0123456789abcdef"; + static const u_char hex[] = "0123456789abcdef"; len = src->length; start = src->start; diff --git a/src/njs_string.h b/src/njs_string.h index 1961152f..225721ed 100644 --- a/src/njs_string.h +++ b/src/njs_string.h @@ -226,7 +226,7 @@ njs_string_encode(const uint32_t *escape, size_t size, const u_char *src, u_char *dst) { uint8_t byte; - static const u_char hex[16] = "0123456789ABCDEF"; + static const u_char hex[] = "0123456789ABCDEF"; do { byte = *src++; diff --git a/src/qjs_buffer.c b/src/qjs_buffer.c index a45f57ce..890b2028 100644 --- a/src/qjs_buffer.c +++ b/src/qjs_buffer.c @@ -2354,7 +2354,7 @@ qjs_hex_encode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst) size_t i, len; const u_char *start; - static const u_char hex[16] = "0123456789abcdef"; + static const u_char hex[] = "0123456789abcdef"; len = src->length; start = src->start; From noreply at nginx.com Mon May 5 21:51:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Mon, 5 May 2025 21:51:02 +0000 (UTC) Subject: [njs] HTTP: fixed GCC 15 build with -Wunterminated-string-initialization. Message-ID: <20250505215102.96FF23F404@pubserv1.nginx> details: https://github.com/nginx/njs/commit/ddac1cf8e78c5e6c4ea2ec33d48c506939fd2f57 branches: master commit: ddac1cf8e78c5e6c4ea2ec33d48c506939fd2f57 user: Dmitry Volyntsev date: Mon, 5 May 2025 08:53:19 -0700 description: HTTP: fixed GCC 15 build with -Wunterminated-string-initialization. ngx_http_js_module.c:936:22: error: initializer-string for array of ‘char’ truncates NUL terminator but destination lacks ‘nonstring’ attribute (16 chars into 15 available) [-Werror=unterminated-string-initialization] 936 | .value = "PeriodicSession", | ^~~~~~~~~~~~~~~~~ --- src/njs.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/njs.h b/src/njs.h index 0a196321..9f65cd7c 100644 --- a/src/njs.h +++ b/src/njs.h @@ -187,7 +187,7 @@ struct njs_external_s { union { struct { - const char value[15]; /* NJS_STRING_SHORT + 1. */ + const char *value; njs_prop_handler_t handler; uint16_t magic16; uint32_t magic32; From noreply at nginx.com Tue May 6 00:20:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Tue, 6 May 2025 00:20:02 +0000 (UTC) Subject: [njs] Version 0.9.0. Message-ID: <20250506002002.0751B3F410@pubserv1.nginx> details: https://github.com/nginx/njs/commit/fcb99b68f86a72c96e21b81b3b78251174dbd3bf branches: master commit: fcb99b68f86a72c96e21b81b3b78251174dbd3bf user: Vadim Zhestikov date: Mon, 5 May 2025 12:38:32 -0700 description: Version 0.9.0. --- CHANGES | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGES b/CHANGES index 87392f4c..af7e4b61 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,20 @@ +Changes with njs 0.9.0 06 May 2025 + + Core: + + *) Feature: refactored working with built-in strings, symbols + and small integers. + Performance improvements (arewefastyet/benchmarks/v8-v7 benchmark): + Richards: +57% (631 → 989) + Crypto: +7% (1445 → 1551) + RayTrace: +37% (562 → 772) + NavierStokes: +20% (2062 → 2465) + Overall score: +29% (1014 → 1307) + + *) Bugfix: fixed regexp undefined value of captured group. + + *) Bugfix: fixed GCC 15 build with -Wunterminated-string-initialization. + Changes with njs 0.8.10 08 Apr 2025 nginx modules: From noreply at nginx.com Tue May 6 00:29:01 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Tue, 6 May 2025 00:29:01 +0000 (UTC) Subject: [njs] Lightweight tag created: 0.9.0 Message-ID: <20250506002901.DFF4B3F411@pubserv1.nginx> details: https://github.com/nginx/njs/releases/tag/0.9.0 branches: commit: fcb99b68f86a72c96e21b81b3b78251174dbd3bf user: Vadim Zhestikov date: Mon May 5 12:38:32 2025 -0700 description: Version 0.9.0. From noreply at nginx.com Thu May 8 16:31:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Thu, 8 May 2025 16:31:02 +0000 (UTC) Subject: [njs] Version bump. Message-ID: <20250508163102.5CB473F457@pubserv1.nginx> details: https://github.com/nginx/njs/commit/0c7bbc4091853f75d7236cfbb8ed2e18e1c95164 branches: master commit: 0c7bbc4091853f75d7236cfbb8ed2e18e1c95164 user: Dmitry Volyntsev date: Thu, 8 May 2025 09:25:24 -0700 description: Version bump. --- src/njs.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/njs.h b/src/njs.h index 9f65cd7c..017f908a 100644 --- a/src/njs.h +++ b/src/njs.h @@ -11,8 +11,8 @@ #include -#define NJS_VERSION "0.9.0" -#define NJS_VERSION_NUMBER 0x000900 +#define NJS_VERSION "0.9.1" +#define NJS_VERSION_NUMBER 0x000901 #include From noreply at nginx.com Thu May 8 16:31:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Thu, 8 May 2025 16:31:02 +0000 (UTC) Subject: [njs] WebCrypto: added support for HMAC as derivedKeyAlgorithm. Message-ID: <20250508163102.6328D3F458@pubserv1.nginx> details: https://github.com/nginx/njs/commit/7bbb8c2753ea236a1202f4664532f06da97a0d0e branches: master commit: 7bbb8c2753ea236a1202f4664532f06da97a0d0e user: Dmitry Volyntsev date: Wed, 7 May 2025 19:11:24 -0700 description: WebCrypto: added support for HMAC as derivedKeyAlgorithm. In crypto.subtle.deriveKey(). This closes #905 issue on Github. --- external/njs_webcrypto_module.c | 3 +++ external/qjs_webcrypto_module.c | 3 +++ test/webcrypto/derive.t.mjs | 30 ++++++++++++++++++++++++++---- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/external/njs_webcrypto_module.c b/external/njs_webcrypto_module.c index d2918891..6f4b49e1 100644 --- a/external/njs_webcrypto_module.c +++ b/external/njs_webcrypto_module.c @@ -1532,6 +1532,9 @@ njs_ext_derive(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, break; + case NJS_ALGORITHM_HMAC: + break; + default: njs_vm_internal_error(vm, "not implemented deriveKey: \"%V\"", njs_algorithm_string(dalg)); diff --git a/external/qjs_webcrypto_module.c b/external/qjs_webcrypto_module.c index a28a8581..29aea329 100644 --- a/external/qjs_webcrypto_module.c +++ b/external/qjs_webcrypto_module.c @@ -1756,6 +1756,9 @@ qjs_webcrypto_derive(JSContext *cx, JSValueConst this_val, int argc, break; + case QJS_ALGORITHM_HMAC: + break; + default: JS_ThrowTypeError(cx, "not implemented deriveKey: \"%s\"", qjs_algorithm_string(dalg)); diff --git a/test/webcrypto/derive.t.mjs b/test/webcrypto/derive.t.mjs index 5ac13459..4d865da3 100644 --- a/test/webcrypto/derive.t.mjs +++ b/test/webcrypto/derive.t.mjs @@ -3,6 +3,16 @@ includes: [compatFs.js, compatBuffer.js, compatWebcrypto.js, runTsuite.js, webCr flags: [async] ---*/ +function has_usage(usage, x) { + for (let i = 0; i < usage.length; i++) { + if (x === usage[i]) { + return true; + } + } + + return false; +} + async function test(params) { let r; let encoder = new TextEncoder(); @@ -12,10 +22,17 @@ async function test(params) { if (params.derive === "key") { let key = await crypto.subtle.deriveKey(params.algorithm, keyMaterial, params.derivedAlgorithm, - true, [ "encrypt", "decrypt" ]); + true, params.usage); + + if (has_usage(params.usage, "encrypt")) { + r = await crypto.subtle.encrypt(params.derivedAlgorithm, key, + encoder.encode(params.text)); + + } else if (has_usage(params.usage, "sign")) { + r = await crypto.subtle.sign(params.derivedAlgorithm, key, + encoder.encode(params.text)); + } - r = await crypto.subtle.encrypt(params.derivedAlgorithm, key, - encoder.encode(params.text)); } else { r = await crypto.subtle.deriveBits(params.algorithm, keyMaterial, params.length); @@ -63,7 +80,8 @@ let derive_tsuite = { name: "AES-GCM", length: 256, iv: "55667788556677885566778855667788" - } + }, + usage: [ "encrypt", "decrypt" ] }, tests: [ @@ -92,6 +110,10 @@ let derive_tsuite = { { algorithm: { name: "HKDF" }, optional: true, expected: "18ea069ee3317d2db02e02f4a228f50dc80d9a2396e6" }, + { algorithm: { name: "HKDF" }, + derivedAlgorithm: { name: "HMAC", hash: "SHA-256", length: 256 }, + usage: [ "sign", "verify" ], optional: true, + expected: "0b06bd37de54c08cedde2cbb649d6f26d066acfd51717d83b52091e2ae6829c2" }, { derive: "bits", algorithm: { name: "HKDF" }, optional: true, expected: "e089c7491711306c69e077aa19fae6bfd2d4a6d240b0d37317d50472d7291a3e" }, ]}; From noreply at nginx.com Thu May 8 16:31:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Thu, 8 May 2025 16:31:02 +0000 (UTC) Subject: [njs] WebCrypto: fixed extractable handling for crypto.subtle.deriveKey(). Message-ID: <20250508163102.6A2223F459@pubserv1.nginx> details: https://github.com/nginx/njs/commit/37b4c07719e12363f33de8a591a7a61815122c91 branches: master commit: 37b4c07719e12363f33de8a591a7a61815122c91 user: Dmitry Volyntsev date: Wed, 7 May 2025 20:49:21 -0700 description: WebCrypto: fixed extractable handling for crypto.subtle.deriveKey(). --- external/njs_webcrypto_module.c | 1 + external/qjs_webcrypto_module.c | 1 + test/webcrypto/derive.t.mjs | 8 +++++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/external/njs_webcrypto_module.c b/external/njs_webcrypto_module.c index 6f4b49e1..dcca91ce 100644 --- a/external/njs_webcrypto_module.c +++ b/external/njs_webcrypto_module.c @@ -1722,6 +1722,7 @@ free: } } + dkey->extractable = njs_value_bool(njs_arg(args, nargs, 4)); dkey->u.s.raw.start = k; dkey->u.s.raw.length = length; diff --git a/external/qjs_webcrypto_module.c b/external/qjs_webcrypto_module.c index 29aea329..937f96c3 100644 --- a/external/qjs_webcrypto_module.c +++ b/external/qjs_webcrypto_module.c @@ -1948,6 +1948,7 @@ free: } } + dkey->extractable = JS_ToBool(cx, argv[3]); dkey->u.s.raw.start = k; dkey->u.s.raw.length = length; diff --git a/test/webcrypto/derive.t.mjs b/test/webcrypto/derive.t.mjs index 4d865da3..e9a2aac1 100644 --- a/test/webcrypto/derive.t.mjs +++ b/test/webcrypto/derive.t.mjs @@ -22,7 +22,11 @@ async function test(params) { if (params.derive === "key") { let key = await crypto.subtle.deriveKey(params.algorithm, keyMaterial, params.derivedAlgorithm, - true, params.usage); + params.extractable, params.usage); + + if (key.extractable !== params.extractable) { + throw Error(`${params.algorithm.name} failed extractable ${params.extractable} vs ${key.extractable}`); + } if (has_usage(params.usage, "encrypt")) { r = await crypto.subtle.encrypt(params.derivedAlgorithm, key, @@ -81,11 +85,13 @@ let derive_tsuite = { length: 256, iv: "55667788556677885566778855667788" }, + extractable: true, usage: [ "encrypt", "decrypt" ] }, tests: [ { expected: "e7b55c9f9fda69b87648585f76c58109174aaa400cfa" }, + { extractable: false, expected: "e7b55c9f9fda69b87648585f76c58109174aaa400cfa" }, { pass: "pass2", expected: "e87d1787f2807ea0e1f7e1cb265b23004c575cf2ad7e" }, { algorithm: { iterations: 10000 }, expected: "5add0059931ed1db1ca24c26dbe4de5719c43ed18a54" }, { algorithm: { hash: "SHA-512" }, expected: "544d64e5e246fdd2ba290ea932b2d80ef411c76139f4" }, From noreply at nginx.com Thu May 8 17:15:03 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Thu, 8 May 2025 17:15:03 +0000 (UTC) Subject: [njs] Fetch: unify string type to support both njs and QuickJS. Message-ID: <20250508171503.2027E3F45B@pubserv1.nginx> details: https://github.com/nginx/njs/commit/a1e3f15f0a580f7b9d465a5b2ed2287a1705dc84 branches: master commit: a1e3f15f0a580f7b9d465a5b2ed2287a1705dc84 user: Zhidao HONG date: Tue, 22 Apr 2025 01:09:15 +0800 description: Fetch: unify string type to support both njs and QuickJS. --- nginx/ngx_js.c | 16 ++++++++++++++ nginx/ngx_js.h | 1 + nginx/ngx_js_fetch.c | 62 ++++++++++++++++++++++++++++++---------------------- nginx/ngx_js_http.c | 5 ++--- nginx/ngx_js_http.h | 10 ++++----- 5 files changed, 60 insertions(+), 34 deletions(-) diff --git a/nginx/ngx_js.c b/nginx/ngx_js.c index f91fcd99..34221d28 100644 --- a/nginx/ngx_js.c +++ b/nginx/ngx_js.c @@ -2281,6 +2281,22 @@ ngx_js_string(njs_vm_t *vm, njs_value_t *value, njs_str_t *str) } +ngx_int_t +ngx_js_ngx_string(njs_vm_t *vm, njs_value_t *value, ngx_str_t *str) +{ + njs_str_t s; + + if (ngx_js_string(vm, value, &s) != NGX_OK) { + return NGX_ERROR; + } + + str->data = s.start; + str->len = s.length; + + return NGX_OK; +} + + static njs_int_t njs_function_bind(njs_vm_t *vm, const njs_str_t *name, njs_function_native_t native, njs_bool_t ctor) diff --git a/nginx/ngx_js.h b/nginx/ngx_js.h index 0e811f44..e13efc41 100644 --- a/nginx/ngx_js.h +++ b/nginx/ngx_js.h @@ -415,6 +415,7 @@ njs_int_t ngx_js_ext_flags(njs_vm_t *vm, njs_object_prop_t *prop, uint32_t unuse njs_value_t *value, njs_value_t *setval, njs_value_t *retval); ngx_int_t ngx_js_string(njs_vm_t *vm, njs_value_t *value, njs_str_t *str); +ngx_int_t ngx_js_ngx_string(njs_vm_t *vm, njs_value_t *value, ngx_str_t *str); ngx_int_t ngx_js_integer(njs_vm_t *vm, njs_value_t *value, ngx_int_t *n); const char *ngx_js_errno_string(int errnum); diff --git a/nginx/ngx_js_fetch.c b/nginx/ngx_js_fetch.c index 802353d6..05d220b9 100644 --- a/nginx/ngx_js_fetch.c +++ b/nginx/ngx_js_fetch.c @@ -588,12 +588,16 @@ ngx_js_ext_fetch(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, #endif } - http->header_only = njs_strstr_eq(&request.method, &njs_str_value("HEAD")); + if (request.method.len == 4 + && ngx_strncasecmp(request.method.data, (u_char *) "HEAD", 4) == 0) + { + http->header_only = 1; + } NJS_CHB_MP_INIT(&http->chain, njs_vm_memory_pool(vm)); NJS_CHB_MP_INIT(&http->response.chain, njs_vm_memory_pool(vm)); - njs_chb_append(&http->chain, request.method.start, request.method.length); + njs_chb_append(&http->chain, request.method.data, request.method.len); njs_chb_append_literal(&http->chain, " "); if (u.uri.len == 0 || u.uri.data[0] != '/') { @@ -683,10 +687,10 @@ ngx_js_ext_fetch(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, http->tls_name.len = u.host.len; #endif - if (request.body.length != 0) { + if (request.body.len != 0) { njs_chb_sprintf(&http->chain, 32, "Content-Length: %uz" CRLF CRLF, - request.body.length); - njs_chb_append(&http->chain, request.body.start, request.body.length); + request.body.len); + njs_chb_append(&http->chain, request.body.data, request.body.len); } else { njs_chb_append_literal(&http->chain, CRLF); @@ -861,13 +865,13 @@ ngx_js_ext_response_constructor(njs_vm_t *vm, njs_value_t *args, value = njs_vm_object_prop(vm, init, &status_text, &lvalue); if (value != NULL) { - if (ngx_js_string(vm, value, &response->status_text) != NGX_OK) { + if (ngx_js_ngx_string(vm, value, &response->status_text) != NGX_OK) { njs_vm_error(vm, "invalid Response statusText"); return NJS_ERROR; } - p = response->status_text.start; - end = p + response->status_text.length; + p = response->status_text.data; + end = p + response->status_text.len; while (p < end) { if (*p != '\t' && *p < ' ') { @@ -927,6 +931,7 @@ static njs_int_t ngx_js_method_process(njs_vm_t *vm, ngx_js_request_t *request) { u_char *s, *p; + njs_str_t str; const njs_str_t *m; static const njs_str_t forbidden[] = { @@ -946,15 +951,18 @@ ngx_js_method_process(njs_vm_t *vm, ngx_js_request_t *request) njs_null_str, }; + str.start = request->method.data; + str.length = request->method.len; + for (m = &forbidden[0]; m->length != 0; m++) { - if (njs_strstr_case_eq(&request->method, m)) { + if (njs_strstr_case_eq(&str, m)) { njs_vm_error(vm, "forbidden method: %V", m); return NJS_ERROR; } } for (m = &to_normalize[0]; m->length != 0; m++) { - if (njs_strstr_case_eq(&request->method, m)) { + if (njs_strstr_case_eq(&str, m)) { s = &request->m[0]; p = m->start; @@ -962,8 +970,8 @@ ngx_js_method_process(njs_vm_t *vm, ngx_js_request_t *request) *s++ = njs_upper_case(*p++); } - request->method.start = &request->m[0]; - request->method.length = m->length; + request->method.data = &request->m[0]; + request->method.len = m->length; break; } } @@ -1342,8 +1350,10 @@ ngx_js_request_constructor(njs_vm_t *vm, ngx_js_request_t *request, ngx_memzero(request, sizeof(ngx_js_request_t)); - request->method = njs_str_value("GET"); - request->body = njs_str_value(""); + request->method.data = (u_char *) "GET"; + request->method.len = 3; + request->body.data = (u_char *) ""; + request->body.len = 0; request->headers.guard = GUARD_REQUEST; pool = ngx_external_pool(vm, external); @@ -1356,7 +1366,7 @@ ngx_js_request_constructor(njs_vm_t *vm, ngx_js_request_t *request, } if (njs_value_is_string(input)) { - ret = ngx_js_string(vm, input, &request->url); + ret = ngx_js_ngx_string(vm, input, &request->url); if (ret != NJS_OK) { njs_vm_error(vm, "failed to convert url arg"); return NJS_ERROR; @@ -1383,12 +1393,12 @@ ngx_js_request_constructor(njs_vm_t *vm, ngx_js_request_t *request, } } - ngx_js_http_trim(&request->url.start, &request->url.length, 1); + ngx_js_http_trim(&request->url.data, &request->url.len, 1); ngx_memzero(u, sizeof(ngx_url_t)); - u->url.len = request->url.length; - u->url.data = request->url.start; + u->url.len = request->url.len; + u->url.data = request->url.data; u->default_port = 80; u->uri_part = 1; u->no_resolve = 1; @@ -1423,7 +1433,7 @@ ngx_js_request_constructor(njs_vm_t *vm, ngx_js_request_t *request, if (njs_value_is_object(init)) { value = njs_vm_object_prop(vm, init, &method_key, &lvalue); - if (value != NULL && ngx_js_string(vm, value, &request->method) + if (value != NULL && ngx_js_ngx_string(vm, value, &request->method) != NGX_OK) { njs_vm_error(vm, "invalid Request method"); @@ -1497,7 +1507,7 @@ ngx_js_request_constructor(njs_vm_t *vm, ngx_js_request_t *request, value = njs_vm_object_prop(vm, init, &body_key, &lvalue); if (value != NULL) { - if (ngx_js_string(vm, value, &request->body) != NGX_OK) { + if (ngx_js_ngx_string(vm, value, &request->body) != NGX_OK) { njs_vm_error(vm, "invalid Request body"); return NJS_ERROR; } @@ -2185,8 +2195,8 @@ ngx_request_js_ext_body(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, switch (type) { case NGX_JS_BODY_ARRAY_BUFFER: ret = njs_vm_value_array_buffer_set(vm, njs_value_arg(&result), - request->body.start, - request->body.length); + request->body.data, + request->body.len); if (ret != NJS_OK) { njs_vm_memory_error(vm); return NJS_ERROR; @@ -2198,8 +2208,8 @@ ngx_request_js_ext_body(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, case NGX_JS_BODY_TEXT: default: ret = njs_vm_value_string_create(vm, njs_value_arg(&result), - request->body.start, - request->body.length); + request->body.data, + request->body.len); if (ret != NJS_OK) { njs_vm_memory_error(vm); return NJS_ERROR; @@ -2480,8 +2490,8 @@ ngx_response_js_ext_status_text(njs_vm_t *vm, njs_object_prop_t *prop, return NJS_DECLINED; } - njs_vm_value_string_create(vm, retval, response->status_text.start, - response->status_text.length); + njs_vm_value_string_create(vm, retval, response->status_text.data, + response->status_text.len); return NJS_OK; } diff --git a/nginx/ngx_js_http.c b/nginx/ngx_js_http.c index c958b0c8..4555a7ac 100644 --- a/nginx/ngx_js_http.c +++ b/nginx/ngx_js_http.c @@ -627,9 +627,8 @@ ngx_js_http_process_status_line(ngx_js_http_t *http) hp->code); http->response.code = hp->code; - http->response.status_text.start = hp->status_text; - http->response.status_text.length = hp->status_text_end - - hp->status_text; + http->response.status_text.data = hp->status_text; + http->response.status_text.len = hp->status_text_end - hp->status_text; http->process = ngx_js_http_process_headers; return http->process(http); diff --git a/nginx/ngx_js_http.h b/nginx/ngx_js_http.h index f5b171c2..63d0f035 100644 --- a/nginx/ngx_js_http.h +++ b/nginx/ngx_js_http.h @@ -81,20 +81,20 @@ typedef struct { MODE_NAVIGATE, MODE_WEBSOCKET, } mode; - njs_str_t url; - njs_str_t method; + ngx_str_t url; + ngx_str_t method; u_char m[8]; uint8_t body_used; - njs_str_t body; + ngx_str_t body; ngx_js_headers_t headers; njs_opaque_value_t header_value; } ngx_js_request_t; typedef struct { - njs_str_t url; + ngx_str_t url; ngx_int_t code; - njs_str_t status_text; + ngx_str_t status_text; uint8_t body_used; njs_chb_t chain; ngx_js_headers_t headers; From noreply at nginx.com Thu May 8 17:15:03 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Thu, 8 May 2025 17:15:03 +0000 (UTC) Subject: [njs] Fetch: QuickJS support. Message-ID: <20250508171503.353463F45C@pubserv1.nginx> details: https://github.com/nginx/njs/commit/b7f76b71f4a1511cd9d954e6da3b0da4c9de40fb branches: master commit: b7f76b71f4a1511cd9d954e6da3b0da4c9de40fb user: Zhidao HONG date: Tue, 22 Apr 2025 10:56:27 +0800 description: Fetch: QuickJS support. --- nginx/config | 1 + nginx/ngx_http_js_module.c | 3 + nginx/ngx_js.c | 1 + nginx/ngx_js.h | 7 + nginx/ngx_js_fetch.c | 6 +- nginx/ngx_qjs_fetch.c | 2520 ++++++++++++++++++++++++++++++++++++ nginx/ngx_stream_js_module.c | 1 + nginx/t/js_fetch.t | 4 +- nginx/t/js_fetch_https.t | 2 - nginx/t/js_fetch_objects.t | 23 +- nginx/t/js_fetch_resolver.t | 2 - nginx/t/js_fetch_timeout.t | 2 - nginx/t/js_fetch_verify.t | 2 - nginx/t/js_periodic_fetch.t | 2 - nginx/t/stream_js_fetch.t | 2 - nginx/t/stream_js_fetch_https.t | 2 - nginx/t/stream_js_fetch_init.t | 2 - nginx/t/stream_js_periodic_fetch.t | 1 - src/qjs.c | 25 + src/qjs.h | 2 + 20 files changed, 2585 insertions(+), 25 deletions(-) diff --git a/nginx/config b/nginx/config index b994f97f..1c303d9c 100644 --- a/nginx/config +++ b/nginx/config @@ -156,6 +156,7 @@ NJS_ENGINE_LIB="$ngx_addon_dir/../build/libnjs.a" if [ "$NJS_HAVE_QUICKJS" = "YES" ]; then NJS_ENGINE_DEP="$ngx_addon_dir/../build/libqjs.a" NJS_ENGINE_LIB="$ngx_addon_dir/../build/libnjs.a $ngx_addon_dir/../build/libqjs.a" + QJS_SRCS="$QJS_SRCS $ngx_addon_dir/ngx_qjs_fetch.c" fi if [ $HTTP != NO ]; then diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c index 3ac95478..40bb83a5 100644 --- a/nginx/ngx_http_js_module.c +++ b/nginx/ngx_http_js_module.c @@ -1134,6 +1134,9 @@ static JSClassDef ngx_http_qjs_headers_out_class = { qjs_module_t *njs_http_qjs_addon_modules[] = { &ngx_qjs_ngx_module, &ngx_qjs_ngx_shared_dict_module, +#ifdef NJS_HAVE_QUICKJS + &ngx_qjs_ngx_fetch_module, +#endif /* * Shared addons should be in the same order and the same positions * in all nginx modules. diff --git a/nginx/ngx_js.c b/nginx/ngx_js.c index 34221d28..e4bae32a 100644 --- a/nginx/ngx_js.c +++ b/nginx/ngx_js.c @@ -441,6 +441,7 @@ static const JSCFunctionListEntry ngx_qjs_ext_ngx[] = { JS_CGETSET_MAGIC_DEF("ERR", ngx_qjs_ext_constant_integer, NULL, NGX_LOG_ERR), JS_CGETSET_DEF("error_log_path", ngx_qjs_ext_error_log_path, NULL), + JS_CFUNC_DEF("fetch", 2, ngx_qjs_ext_fetch), JS_CGETSET_MAGIC_DEF("INFO", ngx_qjs_ext_constant_integer, NULL, NGX_LOG_INFO), JS_CFUNC_MAGIC_DEF("log", 1, ngx_qjs_ext_log, 0), diff --git a/nginx/ngx_js.h b/nginx/ngx_js.h index e13efc41..bb7c1d26 100644 --- a/nginx/ngx_js.h +++ b/nginx/ngx_js.h @@ -63,6 +63,9 @@ #define NGX_QJS_CLASS_ID_SHARED (NGX_QJS_CLASS_ID_OFFSET + 11) #define NGX_QJS_CLASS_ID_SHARED_DICT (NGX_QJS_CLASS_ID_OFFSET + 12) #define NGX_QJS_CLASS_ID_SHARED_DICT_ERROR (NGX_QJS_CLASS_ID_OFFSET + 13) +#define NGX_QJS_CLASS_ID_FETCH_HEADERS (NGX_QJS_CLASS_ID_OFFSET + 14) +#define NGX_QJS_CLASS_ID_FETCH_REQUEST (NGX_QJS_CLASS_ID_OFFSET + 15) +#define NGX_QJS_CLASS_ID_FETCH_RESPONSE (NGX_QJS_CLASS_ID_OFFSET + 16) typedef struct ngx_js_loc_conf_s ngx_js_loc_conf_t; @@ -346,6 +349,9 @@ ngx_int_t ngx_qjs_exception(ngx_engine_t *e, ngx_str_t *s); ngx_int_t ngx_qjs_integer(JSContext *cx, JSValueConst val, ngx_int_t *n); ngx_int_t ngx_qjs_string(JSContext *cx, JSValueConst val, ngx_str_t *str); +JSValue ngx_qjs_ext_fetch(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv); + #define ngx_qjs_prop(cx, type, start, len) \ ((type == NGX_JS_STRING) ? qjs_string_create(cx, start, len) \ : qjs_buffer_create(cx, (u_char *) start, len)) @@ -382,6 +388,7 @@ extern qjs_module_t qjs_xml_module; extern qjs_module_t qjs_zlib_module; extern qjs_module_t ngx_qjs_ngx_module; extern qjs_module_t ngx_qjs_ngx_shared_dict_module; +extern qjs_module_t ngx_qjs_ngx_fetch_module; #endif diff --git a/nginx/ngx_js_fetch.c b/nginx/ngx_js_fetch.c index 05d220b9..45f2dc10 100644 --- a/nginx/ngx_js_fetch.c +++ b/nginx/ngx_js_fetch.c @@ -1171,7 +1171,7 @@ ngx_js_fetch_alloc(njs_vm_t *vm, ngx_pool_t *pool, ngx_log_t *log) fetch->vm = vm; fetch->event = event; - ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "js fetch alloc:%p", fetch); + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "js http alloc:%p", fetch); return fetch; @@ -1207,7 +1207,7 @@ ngx_js_fetch_destructor(ngx_js_event_t *event) fetch = event->data; http = &fetch->http; - ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, "js fetch destructor:%p", + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, "js http destructor:%p", fetch); ngx_js_http_resolve_done(http); @@ -1273,7 +1273,7 @@ ngx_js_fetch_done(ngx_js_fetch_t *fetch, njs_opaque_value_t *retval, http = &fetch->http; ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0, - "js fetch done fetch:%p rc:%i", fetch, (ngx_int_t) rc); + "js http done fetch:%p rc:%i", fetch, (ngx_int_t) rc); ngx_js_http_close_peer(http); diff --git a/nginx/ngx_qjs_fetch.c b/nginx/ngx_qjs_fetch.c new file mode 100644 index 00000000..084162ba --- /dev/null +++ b/nginx/ngx_qjs_fetch.c @@ -0,0 +1,2520 @@ + +/* + * Copyright (C) hongzhidao + * Copyright (C) F5, Inc. + */ + + +#include +#include +#include +#include +#include "ngx_js.h" +#include "ngx_js_http.h" + + +typedef struct { + ngx_str_t name; + ngx_int_t value; +} ngx_qjs_entry_t; + + +typedef struct { + ngx_js_http_t http; + + JSContext *cx; + ngx_qjs_event_t *event; + + JSValue response_value; + + JSValue promise; + JSValue promise_callbacks[2]; +} ngx_qjs_fetch_t; + + +static ngx_int_t ngx_qjs_method_process(JSContext *cx, + ngx_js_request_t *request); +static ngx_int_t ngx_qjs_headers_inherit(JSContext *cx, + ngx_js_headers_t *headers, ngx_js_headers_t *orig); +static ngx_int_t ngx_qjs_headers_fill(JSContext *cx, ngx_js_headers_t *headers, + JSValue init); +static ngx_qjs_fetch_t *ngx_qjs_fetch_alloc(JSContext *cx, ngx_pool_t *pool, + ngx_log_t *log); +static void ngx_qjs_fetch_error(ngx_js_http_t *http, const char *err); +static void ngx_qjs_fetch_destructor(ngx_qjs_event_t *event); +static void ngx_qjs_fetch_done(ngx_qjs_fetch_t *fetch, JSValue retval, + ngx_int_t rc); + +static ngx_int_t ngx_qjs_request_ctor(JSContext *cx, ngx_js_request_t *request, + ngx_url_t *u, int argc, JSValueConst *argv); + +static ngx_int_t ngx_qjs_fetch_append_headers(ngx_js_http_t *http, + ngx_js_headers_t *headers, u_char *name, size_t len, u_char *value, + size_t vlen); +static void ngx_qjs_fetch_process_done(ngx_js_http_t *http); +static ngx_int_t ngx_qjs_headers_append(JSContext *cx, + ngx_js_headers_t *headers, u_char *name, size_t len, u_char *value, + size_t vlen); + +static JSValue ngx_qjs_fetch_headers_ctor(JSContext *cx, + JSValueConst new_target, int argc, JSValueConst *argv); +static int ngx_qjs_fetch_headers_own_property(JSContext *cx, + JSPropertyDescriptor *desc, JSValueConst obj, JSAtom prop); +static int ngx_qjs_fetch_headers_own_property_names(JSContext *cx, + JSPropertyEnum **ptab, uint32_t *plen, JSValueConst obj); +static JSValue ngx_qjs_ext_fetch_headers_append(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_qjs_ext_fetch_headers_delete(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_qjs_ext_fetch_headers_foreach(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_qjs_ext_fetch_headers_get(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv, int magic); +static JSValue ngx_qjs_ext_fetch_headers_has(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_qjs_ext_fetch_headers_set(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); + +static JSValue ngx_qjs_fetch_request_ctor(JSContext *cx, + JSValueConst new_target, int argc, JSValueConst *argv); +static JSValue ngx_qjs_ext_fetch_request_body(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv, int magic); +static JSValue ngx_qjs_ext_fetch_request_body_used(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_qjs_ext_fetch_request_cache(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_qjs_ext_fetch_request_credentials(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_qjs_ext_fetch_request_headers(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_qjs_ext_fetch_request_field(JSContext *cx, + JSValueConst this_val, int magic); +static JSValue ngx_qjs_ext_fetch_request_mode(JSContext *cx, + JSValueConst this_val); +static void ngx_qjs_fetch_request_finalizer(JSRuntime *rt, JSValue val); + +static JSValue ngx_qjs_fetch_response_ctor(JSContext *cx, + JSValueConst new_target, int argc, JSValueConst *argv); +static JSValue ngx_qjs_ext_fetch_response_status(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_qjs_ext_fetch_response_status_text(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_qjs_ext_fetch_response_ok(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_qjs_ext_fetch_response_body_used(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_qjs_ext_fetch_response_headers(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_qjs_ext_fetch_response_type(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_qjs_ext_fetch_response_body(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv, int magic); +static JSValue ngx_qjs_ext_fetch_response_redirected(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_qjs_ext_fetch_response_field(JSContext *cx, + JSValueConst this_val, int magic); +static void ngx_qjs_fetch_response_finalizer(JSRuntime *rt, JSValue val); + +static JSValue ngx_qjs_fetch_flag(JSContext *cx, const ngx_qjs_entry_t *entries, + ngx_int_t value); +static ngx_int_t ngx_qjs_fetch_flag_set(JSContext *cx, + const ngx_qjs_entry_t *entries, JSValue object, const char *prop); + +static JSModuleDef *ngx_qjs_fetch_init(JSContext *cx, const char *name); + + +static const JSCFunctionListEntry ngx_qjs_ext_fetch_headers_proto[] = { + JS_CFUNC_DEF("append", 2, ngx_qjs_ext_fetch_headers_append), + JS_CFUNC_DEF("delete", 1, ngx_qjs_ext_fetch_headers_delete), + JS_CFUNC_DEF("forEach", 1, ngx_qjs_ext_fetch_headers_foreach), + JS_CFUNC_MAGIC_DEF("get", 1, ngx_qjs_ext_fetch_headers_get, 0), + JS_CFUNC_MAGIC_DEF("getAll", 1, ngx_qjs_ext_fetch_headers_get, 1), + JS_CFUNC_DEF("has", 1, ngx_qjs_ext_fetch_headers_has), + JS_CFUNC_DEF("set", 2, ngx_qjs_ext_fetch_headers_set), +}; + + +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), + 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), + 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), + JS_CGETSET_MAGIC_DEF("url", ngx_qjs_ext_fetch_request_field, NULL, + offsetof(ngx_js_request_t, url) ), +}; + + +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), + 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), + 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), + 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) ), +}; + + +static const JSClassDef ngx_qjs_fetch_headers_class = { + "Headers", + .finalizer = NULL, + .exotic = & (JSClassExoticMethods) { + .get_own_property = ngx_qjs_fetch_headers_own_property, + .get_own_property_names = ngx_qjs_fetch_headers_own_property_names, + }, +}; + + +static const JSClassDef ngx_qjs_fetch_request_class = { + "Request", + .finalizer = ngx_qjs_fetch_request_finalizer, +}; + + +static const JSClassDef ngx_qjs_fetch_response_class = { + "Response", + .finalizer = ngx_qjs_fetch_response_finalizer, +}; + + +static const ngx_qjs_entry_t ngx_qjs_fetch_cache_modes[] = { + { ngx_string("default"), CACHE_MODE_DEFAULT }, + { ngx_string("no-store"), CACHE_MODE_NO_STORE }, + { ngx_string("reload"), CACHE_MODE_RELOAD }, + { ngx_string("no-cache"), CACHE_MODE_NO_CACHE }, + { ngx_string("force-cache"), CACHE_MODE_FORCE_CACHE }, + { ngx_string("only-if-cached"), CACHE_MODE_ONLY_IF_CACHED }, + { ngx_null_string, 0 }, +}; + + +static const ngx_qjs_entry_t ngx_qjs_fetch_credentials[] = { + { ngx_string("same-origin"), CREDENTIALS_SAME_ORIGIN }, + { ngx_string("omit"), CREDENTIALS_OMIT }, + { ngx_string("include"), CREDENTIALS_INCLUDE }, + { ngx_null_string, 0 }, +}; + + +static const ngx_qjs_entry_t ngx_qjs_fetch_modes[] = { + { ngx_string("no-cors"), MODE_NO_CORS }, + { ngx_string("cors"), MODE_CORS }, + { ngx_string("same-origin"), MODE_SAME_ORIGIN }, + { ngx_string("navigate"), MODE_NAVIGATE }, + { ngx_string("websocket"), MODE_WEBSOCKET }, + { ngx_null_string, 0 }, +}; + + +qjs_module_t ngx_qjs_ngx_fetch_module = { + .name = "fetch", + .init = ngx_qjs_fetch_init, +}; + + +JSValue +ngx_qjs_ext_fetch(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + int has_host; + void *external; + JSValue init, value, promise; + ngx_int_t rc; + ngx_url_t u; + ngx_uint_t i; + ngx_pool_t *pool; + ngx_js_ctx_t *ctx; + ngx_js_http_t *http; + ngx_qjs_fetch_t *fetch; + ngx_list_part_t *part; + ngx_js_tb_elt_t *h; + ngx_connection_t *c; + ngx_js_request_t request; + ngx_resolver_ctx_t *rs; + + external = JS_GetContextOpaque(cx); + c = ngx_qjs_external_connection(cx, external); + pool = ngx_qjs_external_pool(cx, external); + + fetch = ngx_qjs_fetch_alloc(cx, pool, c->log); + if (fetch == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + promise = JS_DupValue(cx, fetch->promise); + + rc = ngx_qjs_request_ctor(cx, &request, &u, argc, argv); + if (rc != NGX_OK) { + goto fail; + } + + http = &fetch->http; + http->response.url = request.url; + http->timeout = ngx_qjs_external_fetch_timeout(cx, external); + http->buffer_size = ngx_qjs_external_buffer_size(cx, external); + http->max_response_body_size = + ngx_qjs_external_max_response_buffer_size(cx, external); + +#if (NGX_SSL) + if (u.default_port == 443) { + http->ssl = ngx_qjs_external_ssl(cx, external); + http->ssl_verify = ngx_qjs_external_ssl_verify(cx, external); + } +#endif + + if (JS_IsObject(argv[1])) { + init = argv[1]; + value = JS_GetPropertyStr(cx, init, "buffer_size"); + if (JS_IsException(value)) { + goto fail; + } + + if (!JS_IsUndefined(value)) { + if (JS_ToInt64(cx, (int64_t *) &http->buffer_size, value) < 0) { + JS_FreeValue(cx, value); + goto fail; + } + } + + value = JS_GetPropertyStr(cx, init, "max_response_body_size"); + if (JS_IsException(value)) { + goto fail; + } + + if (!JS_IsUndefined(value)) { + if (JS_ToInt64(cx, (int64_t *) &http->max_response_body_size, + value) < 0) + { + JS_FreeValue(cx, value); + goto fail; + } + } + +#if (NGX_SSL) + value = JS_GetPropertyStr(cx, init, "verify"); + if (JS_IsException(value)) { + goto fail; + } + + if (!JS_IsUndefined(value)) { + http->ssl_verify = JS_ToBool(cx, value); + } +#endif + } + + if (request.method.len == 4 + && ngx_strncasecmp(request.method.data, (u_char *) "HEAD", 4) == 0) + { + http->header_only = 1; + } + + ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx)); + + NJS_CHB_MP_INIT(&http->chain, ctx->engine->pool); + NJS_CHB_MP_INIT(&http->response.chain, ctx->engine->pool); + + njs_chb_append(&http->chain, request.method.data, request.method.len); + njs_chb_append_literal(&http->chain, " "); + + if (u.uri.len == 0 || u.uri.data[0] != '/') { + njs_chb_append_literal(&http->chain, "/"); + } + + njs_chb_append(&http->chain, u.uri.data, u.uri.len); + njs_chb_append_literal(&http->chain, " HTTP/1.1" CRLF); + + has_host = 0; + part = &request.headers.header_list.part; + h = part->elts; + + for (i = 0; /* void */; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + h = part->elts; + i = 0; + } + + if (h[i].hash == 0) { + continue; + } + + if (h[i].key.len == 4 + && ngx_strncasecmp(h[i].key.data, (u_char *) "Host", 4) == 0) + { + has_host = 1; + njs_chb_append_literal(&http->chain, "Host: "); + njs_chb_append(&http->chain, h[i].value.data, h[i].value.len); + njs_chb_append_literal(&http->chain, CRLF); + break; + } + } + + if (!has_host) { + njs_chb_append_literal(&http->chain, "Host: "); + njs_chb_append(&http->chain, u.host.data, u.host.len); + + if (!u.no_port) { + njs_chb_sprintf(&http->chain, 32, ":%d", u.port); + } + + njs_chb_append_literal(&http->chain, CRLF); + } + + part = &request.headers.header_list.part; + h = part->elts; + + for (i = 0; /* void */; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + h = part->elts; + i = 0; + } + + if (h[i].hash == 0) { + continue; + } + + if (h[i].key.len == 4 + && ngx_strncasecmp(h[i].key.data, (u_char *) "Host", 4) == 0) + { + continue; + } + + njs_chb_append(&http->chain, h[i].key.data, h[i].key.len); + njs_chb_append_literal(&http->chain, ": "); + njs_chb_append(&http->chain, h[i].value.data, h[i].value.len); + njs_chb_append_literal(&http->chain, CRLF); + } + + njs_chb_append_literal(&http->chain, "Connection: close" CRLF); + +#if (NGX_SSL) + http->tls_name.data = u.host.data; + http->tls_name.len = u.host.len; +#endif + + if (request.body.len != 0) { + njs_chb_sprintf(&http->chain, 32, "Content-Length: %uz" CRLF CRLF, + request.body.len); + njs_chb_append(&http->chain, request.body.data, request.body.len); + + } else { + njs_chb_append_literal(&http->chain, CRLF); + } + + if (u.addrs == NULL) { + rs = ngx_js_http_resolve(http, ngx_qjs_external_resolver(cx, external), + &u.host, u.port, + ngx_qjs_external_resolver_timeout(cx, external)); + if (rs == NULL) { + JS_FreeValue(cx, promise); + return JS_ThrowOutOfMemory(cx); + } + + if (rs == NGX_NO_RESOLVER) { + JS_ThrowInternalError(cx, "no resolver defined"); + goto fail; + } + + return promise; + } + + http->naddrs = 1; + ngx_memcpy(&http->addr, &u.addrs[0], sizeof(ngx_addr_t)); + http->addrs = &http->addr; + + ngx_js_http_connect(http); + + return promise; + +fail: + + fetch->response_value = JS_GetException(cx); + + ngx_qjs_fetch_done(fetch, fetch->response_value, NGX_ERROR); + + return promise; +} + + +static JSValue +ngx_qjs_fetch_headers_ctor(JSContext *cx, JSValueConst new_target, int argc, + JSValueConst *argv) +{ + JSValue init, proto, obj; + ngx_int_t rc; + ngx_pool_t *pool; + ngx_js_headers_t *headers; + + pool = ngx_qjs_external_pool(cx, JS_GetContextOpaque(cx)); + + headers = ngx_pcalloc(pool, sizeof(ngx_js_headers_t)); + if (headers == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + headers->guard = GUARD_NONE; + + rc = ngx_list_init(&headers->header_list, pool, 4, + sizeof(ngx_js_tb_elt_t)); + if (rc != NGX_OK) { + return JS_ThrowOutOfMemory(cx); + } + + init = argv[0]; + + if (JS_IsObject(init)) { + rc = ngx_qjs_headers_fill(cx, headers, init); + if (rc != NGX_OK) { + return JS_EXCEPTION; + } + } + + proto = JS_GetPropertyStr(cx, new_target, "prototype"); + if (JS_IsException(proto)) { + return JS_EXCEPTION; + } + + obj = JS_NewObjectProtoClass(cx, proto, NGX_QJS_CLASS_ID_FETCH_HEADERS); + JS_FreeValue(cx, proto); + + if (JS_IsException(obj)) { + return JS_EXCEPTION; + } + + JS_SetOpaque(obj, headers); + + return obj; +} + + +static JSValue +ngx_qjs_fetch_request_ctor(JSContext *cx, JSValueConst new_target, int argc, + JSValueConst *argv) +{ + JSValue proto, obj; + ngx_int_t rc; + ngx_url_t u; + ngx_pool_t *pool; + ngx_js_request_t *request; + + pool = ngx_qjs_external_pool(cx, JS_GetContextOpaque(cx)); + + request = ngx_pcalloc(pool, sizeof(ngx_js_request_t)); + if (request == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + rc = ngx_qjs_request_ctor(cx, request, &u, argc, argv); + if (rc != NGX_OK) { + return JS_EXCEPTION; + } + + proto = JS_GetPropertyStr(cx, new_target, "prototype"); + if (JS_IsException(proto)) { + return JS_EXCEPTION; + } + + obj = JS_NewObjectProtoClass(cx, proto, NGX_QJS_CLASS_ID_FETCH_REQUEST); + JS_FreeValue(cx, proto); + + if (JS_IsException(obj)) { + return JS_EXCEPTION; + } + + JS_SetOpaque(obj, request); + + return obj; +} + + +static ngx_int_t +ngx_qjs_request_ctor(JSContext *cx, ngx_js_request_t *request, + ngx_url_t *u, int argc, JSValueConst *argv) +{ + JSValue input, init, value; + ngx_int_t rc; + ngx_pool_t *pool; + ngx_js_request_t *orig; + + input = argv[0]; + if (JS_IsUndefined(input)) { + JS_ThrowInternalError(cx, "1st argument is required"); + return NGX_ERROR; + } + + /* + * set by ngx_memzero(): + * + * request->url.len = 0; + * request->body.length = 0; + * request->cache_mode = CACHE_MODE_DEFAULT; + * request->credentials = CREDENTIALS_SAME_ORIGIN; + * request->mode = MODE_NO_CORS; + * request->headers.content_type = NULL; + */ + + ngx_memzero(request, sizeof(ngx_js_request_t)); + + request->method.data = (u_char *) "GET"; + request->method.len = 3; + request->body.data = NULL; + request->body.len = 0; + request->headers.guard = GUARD_REQUEST; + ngx_qjs_arg(request->header_value) = JS_UNDEFINED; + + pool = ngx_qjs_external_pool(cx, JS_GetContextOpaque(cx)); + + rc = ngx_list_init(&request->headers.header_list, pool, 4, + sizeof(ngx_js_tb_elt_t)); + if (rc != NGX_OK) { + JS_ThrowOutOfMemory(cx); + return NGX_ERROR; + } + + if (JS_IsString(input)) { + rc = ngx_qjs_string(cx, input, &request->url); + if (rc != NGX_OK) { + JS_ThrowInternalError(cx, "failed to convert url arg"); + return NGX_ERROR; + } + + } else { + orig = JS_GetOpaque2(cx, input, NGX_QJS_CLASS_ID_FETCH_REQUEST); + if (orig == NULL) { + JS_ThrowInternalError(cx, + "input is not string or a Request object"); + return NGX_ERROR; + } + + request->url = orig->url; + request->method = orig->method; + request->body = orig->body; + request->body_used = orig->body_used; + request->cache_mode = orig->cache_mode; + request->credentials = orig->credentials; + request->mode = orig->mode; + + rc = ngx_qjs_headers_inherit(cx, &request->headers, &orig->headers); + if (rc != NGX_OK) { + return NGX_ERROR; + } + } + + ngx_js_http_trim(&request->url.data, &request->url.len, 1); + + ngx_memzero(u, sizeof(ngx_url_t)); + + u->url = request->url; + u->default_port = 80; + u->uri_part = 1; + u->no_resolve = 1; + + if (u->url.len > 7 + && ngx_strncasecmp(u->url.data, (u_char *) "http://", 7) == 0) + { + u->url.len -= 7; + u->url.data += 7; + +#if (NGX_SSL) + } else if (u->url.len > 8 + && ngx_strncasecmp(u->url.data, (u_char *) "https://", 8) == 0) + { + u->url.len -= 8; + u->url.data += 8; + u->default_port = 443; +#endif + + } else { + JS_ThrowInternalError(cx, "unsupported URL schema (only http or https" + " are supported)"); + return NGX_ERROR; + } + + if (ngx_parse_url(pool, u) != NGX_OK) { + JS_ThrowInternalError(cx, "invalid url"); + return NGX_ERROR; + } + + if (JS_IsObject(argv[1])) { + init = argv[1]; + value = JS_GetPropertyStr(cx, init, "method"); + if (JS_IsException(value)) { + JS_ThrowInternalError(cx, "invalid Request method"); + return NGX_ERROR; + } + + if (!JS_IsUndefined(value)) { + rc = ngx_qjs_string(cx, value, &request->method); + JS_FreeValue(cx, value); + + if (rc != NGX_OK) { + JS_ThrowInternalError(cx, "invalid Request method"); + return NGX_ERROR; + } + } + + rc = ngx_qjs_method_process(cx, request); + if (rc != NGX_OK) { + return NGX_ERROR; + } + + rc = ngx_qjs_fetch_flag_set(cx, ngx_qjs_fetch_cache_modes, init, + "cache"); + if (rc == NGX_ERROR) { + return NGX_ERROR; + } + + request->cache_mode = rc; + + rc = ngx_qjs_fetch_flag_set(cx, ngx_qjs_fetch_credentials, init, + "credentials"); + if (rc == NGX_ERROR) { + return NGX_ERROR; + } + + request->credentials = rc; + + rc = ngx_qjs_fetch_flag_set(cx, ngx_qjs_fetch_modes, init, "mode"); + if (rc == NGX_ERROR) { + return NGX_ERROR; + } + + request->mode = rc; + + value = JS_GetPropertyStr(cx, init, "headers"); + if (JS_IsException(value)) { + JS_ThrowInternalError(cx, "invalid Request headers"); + return NGX_ERROR; + } + + if (!JS_IsUndefined(value)) { + if (!JS_IsObject(value)) { + JS_ThrowInternalError(cx, "Headers is not an object"); + return NGX_ERROR; + } + + /* + * There are no API to reset or destroy ngx_list, + * just allocating a new one. + */ + + ngx_memset(&request->headers, 0, sizeof(ngx_js_headers_t)); + request->headers.guard = GUARD_REQUEST; + + rc = ngx_list_init(&request->headers.header_list, pool, 4, + sizeof(ngx_js_tb_elt_t)); + if (rc != NGX_OK) { + JS_FreeValue(cx, value); + JS_ThrowOutOfMemory(cx); + return NGX_ERROR; + } + + rc = ngx_qjs_headers_fill(cx, &request->headers, value); + JS_FreeValue(cx, value); + + if (rc != NGX_OK) { + return NGX_ERROR; + } + } + + value = JS_GetPropertyStr(cx, init, "body"); + if (JS_IsException(value)) { + JS_ThrowInternalError(cx, "invalid Request body"); + return NGX_ERROR; + } + + if (!JS_IsUndefined(value)) { + if (ngx_qjs_string(cx, value, &request->body) != NGX_OK) { + JS_FreeValue(cx, value); + JS_ThrowInternalError(cx, "invalid Request body"); + return NGX_ERROR; + } + + if (request->headers.content_type == NULL && JS_IsString(value)) { + rc = ngx_qjs_headers_append(cx, &request->headers, + (u_char *) "Content-Type", + sizeof("Content-Type") - 1, + (u_char *) "text/plain;charset=UTF-8", + sizeof("text/plain;charset=UTF-8") - 1); + if (rc != NGX_OK) { + JS_FreeValue(cx, value); + return NGX_ERROR; + } + } + + JS_FreeValue(cx, value); + } + } + + return NGX_OK; +} + + +static JSValue +ngx_qjs_fetch_response_ctor(JSContext *cx, JSValueConst new_target, int argc, + JSValueConst *argv) +{ + int ret; + u_char *p, *end; + JSValue init, value, body, proto, obj; + ngx_str_t bd; + ngx_int_t rc; + ngx_pool_t *pool; + ngx_js_ctx_t *ctx; + ngx_js_response_t *response; + + pool = ngx_qjs_external_pool(cx, JS_GetContextOpaque(cx)); + + response = ngx_pcalloc(pool, sizeof(ngx_js_response_t)); + if (response == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + /* + * set by ngx_pcalloc(): + * + * response->url.length = 0; + * response->status_text.length = 0; + */ + + response->code = 200; + response->headers.guard = GUARD_RESPONSE; + ngx_qjs_arg(response->header_value) = JS_UNDEFINED; + + ret = ngx_list_init(&response->headers.header_list, pool, 4, + sizeof(ngx_js_tb_elt_t)); + if (ret != NGX_OK) { + JS_ThrowOutOfMemory(cx); + } + + init = argv[1]; + + if (JS_IsObject(init)) { + value = JS_GetPropertyStr(cx, init, "status"); + if (JS_IsException(value)) { + return JS_ThrowInternalError(cx, "invalid Response status"); + } + + if (!JS_IsUndefined(value)) { + ret = JS_ToInt64(cx, (int64_t *) &response->code, value); + JS_FreeValue(cx, value); + + if (ret < 0) { + return JS_EXCEPTION; + } + + if (response->code < 200 || response->code > 599) { + return JS_ThrowInternalError(cx, "status provided (%d) is " + "outside of [200, 599] range", + (int) response->code); + } + } + + value = JS_GetPropertyStr(cx, init, "statusText"); + if (JS_IsException(value)) { + return JS_ThrowInternalError(cx, "invalid Response statusText"); + } + + if (!JS_IsUndefined(value)) { + ret = ngx_qjs_string(cx, value, &response->status_text); + JS_FreeValue(cx, value); + + if (ret < 0) { + return JS_EXCEPTION; + } + + p = response->status_text.data; + end = p + response->status_text.len; + + while (p < end) { + if (*p != '\t' && *p < ' ') { + return JS_ThrowInternalError(cx, + "invalid Response statusText"); + } + + p++; + } + } + + value = JS_GetPropertyStr(cx, init, "headers"); + if (JS_IsException(value)) { + return JS_ThrowInternalError(cx, "invalid Response headers"); + } + + if (!JS_IsUndefined(value)) { + if (!JS_IsObject(value)) { + JS_FreeValue(cx, value); + return JS_ThrowInternalError(cx, "Headers is not an object"); + } + + rc = ngx_qjs_headers_fill(cx, &response->headers, value); + JS_FreeValue(cx, value); + + if (ret != NGX_OK) { + return JS_EXCEPTION; + } + } + } + + ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx)); + + NJS_CHB_MP_INIT(&response->chain, ctx->engine->pool); + + body = argv[0]; + + if (!JS_IsNullOrUndefined(body)) { + if (ngx_qjs_string(cx, body, &bd) != NGX_OK) { + return JS_ThrowInternalError(cx, "invalid Response body"); + } + + njs_chb_append(&response->chain, bd.data, bd.len); + + if (JS_IsString(body)) { + rc = ngx_qjs_headers_append(cx, &response->headers, + (u_char *) "Content-Type", + sizeof("Content-Type") - 1, + (u_char *) "text/plain;charset=UTF-8", + sizeof("text/plain;charset=UTF-8") - 1); + if (rc != NGX_OK) { + return JS_EXCEPTION; + } + } + } + + proto = JS_GetPropertyStr(cx, new_target, "prototype"); + if (JS_IsException(proto)) { + return JS_EXCEPTION; + } + + obj = JS_NewObjectProtoClass(cx, proto, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + JS_FreeValue(cx, proto); + + if (JS_IsException(obj)) { + return JS_EXCEPTION; + } + + JS_SetOpaque(obj, response); + + return obj; +} + + +static u_char +ngx_js_upper_case(u_char c) +{ + return (u_char) ((c >= 'a' && c <= 'z') ? c & 0xDF : c); +} + + +static ngx_int_t +ngx_qjs_method_process(JSContext *cx, ngx_js_request_t *request) +{ + u_char *s; + const u_char *p; + const ngx_str_t *m; + + static const ngx_str_t forbidden[] = { + ngx_string("CONNECT"), + ngx_string("TRACE"), + ngx_string("TRACK"), + ngx_null_string, + }; + + static const ngx_str_t to_normalize[] = { + ngx_string("DELETE"), + ngx_string("GET"), + ngx_string("HEAD"), + ngx_string("OPTIONS"), + ngx_string("POST"), + ngx_string("PUT"), + ngx_null_string, + }; + + for (m = &forbidden[0]; m->len != 0; m++) { + if (request->method.len == m->len + && ngx_strncasecmp(request->method.data, m->data, m->len) == 0) + { + JS_ThrowInternalError(cx, "forbidden method: %.*s", + (int) m->len, m->data); + return NGX_ERROR; + } + } + + for (m = &to_normalize[0]; m->len != 0; m++) { + if (request->method.len == m->len + && ngx_strncasecmp(request->method.data, m->data, m->len) == 0) + { + s = &request->m[0]; + p = m->data; + + while (*p != '\0') { + *s++ = ngx_js_upper_case(*p++); + } + + request->method.data = &request->m[0]; + request->method.len = m->len; + break; + } + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_qjs_headers_inherit(JSContext *cx, ngx_js_headers_t *headers, + ngx_js_headers_t *orig) +{ + ngx_int_t rc; + ngx_uint_t i; + ngx_list_part_t *part; + ngx_js_tb_elt_t *h; + + part = &orig->header_list.part; + h = part->elts; + + for (i = 0; /* void */; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + h = part->elts; + i = 0; + } + + if (h[i].hash == 0) { + continue; + } + + rc = ngx_qjs_headers_append(cx, headers, h[i].key.data, h[i].key.len, + h[i].value.data, h[i].value.len); + if (rc != NGX_OK) { + return NGX_ERROR; + } + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_qjs_headers_fill_header_free(JSContext *cx, ngx_js_headers_t *headers, + JSValue prop_name, JSValue prop_value) +{ + ngx_int_t rc; + ngx_str_t name, value; + + if (ngx_qjs_string(cx, prop_name, &name) != NGX_OK) { + JS_FreeValue(cx, prop_name); + JS_FreeValue(cx, prop_value); + return NGX_ERROR; + } + + if (ngx_qjs_string(cx, prop_value, &value) != NGX_OK) { + JS_FreeValue(cx, prop_name); + JS_FreeValue(cx, prop_value); + return NGX_ERROR; + } + + rc = ngx_qjs_headers_append(cx, headers, name.data, name.len, + value.data, value.len); + + JS_FreeValue(cx, prop_name); + JS_FreeValue(cx, prop_value); + + return rc; +} + + +static ngx_int_t +ngx_qjs_headers_fill(JSContext *cx, ngx_js_headers_t *headers, JSValue init) +{ + JSValue header, prop_name, prop_value; + uint32_t i, len, length; + ngx_int_t rc; + JSPropertyEnum *tab; + ngx_js_headers_t *hh; + + hh = JS_GetOpaque2(cx, init, NGX_QJS_CLASS_ID_FETCH_HEADERS); + if (hh != NULL) { + return ngx_qjs_headers_inherit(cx, headers, hh); + } + + if (JS_GetOwnPropertyNames(cx, &tab, &len, init, + JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) < 0) { + return NGX_ERROR; + } + + if (qjs_is_array(cx, init)) { + for (i = 0; i < len; i++) { + header = JS_GetPropertyUint32(cx, init, i); + if (JS_IsException(header)) { + goto fail; + } + + if (qjs_array_length(cx, header, &length)) { + JS_FreeValue(cx, header); + goto fail; + } + + if (length != 2) { + JS_FreeValue(cx, header); + JS_ThrowInternalError(cx, + "header does not contain exactly two items"); + goto fail; + } + + prop_name = JS_GetPropertyUint32(cx, header, 0); + prop_value = JS_GetPropertyUint32(cx, header, 1); + + JS_FreeValue(cx, header); + + rc = ngx_qjs_headers_fill_header_free(cx, headers, prop_name, + prop_value); + if (rc != NGX_OK) { + goto fail; + } + } + + } else { + + for (i = 0; i < len; i++) { + prop_name = JS_AtomToString(cx, tab[i].atom); + + prop_value = JS_GetProperty(cx, init, tab[i].atom); + if (JS_IsException(prop_value)) { + JS_FreeValue(cx, prop_name); + goto fail; + } + + rc = ngx_qjs_headers_fill_header_free(cx, headers, prop_name, + prop_value); + if (rc != NGX_OK) { + goto fail; + } + } + } + + qjs_free_prop_enum(cx, tab, len); + + return NGX_OK; + +fail: + + qjs_free_prop_enum(cx, tab, len); + + return NGX_ERROR; +} + + +static ngx_qjs_fetch_t * +ngx_qjs_fetch_alloc(JSContext *cx, ngx_pool_t *pool, ngx_log_t *log) +{ + ngx_js_ctx_t *ctx; + ngx_js_http_t *http; + ngx_qjs_fetch_t *fetch; + ngx_qjs_event_t *event; + + fetch = ngx_pcalloc(pool, sizeof(ngx_qjs_fetch_t)); + if (fetch == NULL) { + return NULL; + } + + http = &fetch->http; + + http->pool = pool; + http->log = log; + + http->timeout = 10000; + + http->http_parse.content_length_n = -1; + + ngx_qjs_arg(http->response.header_value) = JS_UNDEFINED; + + http->append_headers = ngx_qjs_fetch_append_headers; + http->ready_handler = ngx_qjs_fetch_process_done; + http->error_handler = ngx_qjs_fetch_error; + + fetch->promise = JS_NewPromiseCapability(cx, fetch->promise_callbacks); + if (JS_IsException(fetch->promise)) { + return NULL; + } + + event = ngx_palloc(pool, sizeof(ngx_qjs_event_t)); + if (event == NULL) { + goto fail; + } + + ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx)); + + event->ctx = cx; + event->destructor = ngx_qjs_fetch_destructor; + event->fd = ctx->event_id++; + event->data = fetch; + + ngx_js_add_event(ctx, event); + + fetch->cx = cx; + fetch->event = event; + + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "js http alloc:%p", fetch); + + return fetch; + +fail: + + JS_FreeValue(cx, fetch->promise); + JS_FreeValue(cx, fetch->promise_callbacks[0]); + JS_FreeValue(cx, fetch->promise_callbacks[1]); + + JS_ThrowInternalError(cx, "internal error"); + + return NULL; +} + + +static void +ngx_qjs_fetch_error(ngx_js_http_t *http, const char *err) +{ + ngx_qjs_fetch_t *fetch; + + fetch = (ngx_qjs_fetch_t *) http; + + JS_ThrowInternalError(fetch->cx, "%s", err); + + fetch->response_value = JS_GetException(fetch->cx); + + ngx_qjs_fetch_done(fetch, fetch->response_value, NGX_ERROR); +} + + +static void +ngx_qjs_fetch_destructor(ngx_qjs_event_t *event) +{ + JSContext *cx; + ngx_js_http_t *http; + ngx_qjs_fetch_t *fetch; + + cx = event->ctx; + fetch = event->data; + http = &fetch->http; + + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, "js http destructor:%p", + fetch); + + ngx_js_http_resolve_done(http); + ngx_js_http_close_peer(http); + + JS_FreeValue(cx, fetch->promise_callbacks[0]); + JS_FreeValue(cx, fetch->promise_callbacks[1]); + JS_FreeValue(cx, fetch->promise); + JS_FreeValue(cx, fetch->response_value); +} + + +static void +ngx_qjs_fetch_done(ngx_qjs_fetch_t *fetch, JSValue retval, ngx_int_t rc) +{ + void *external; + JSValue action; + JSContext *cx; + ngx_js_ctx_t *ctx; + ngx_js_http_t *http; + ngx_qjs_event_t *event; + + http = &fetch->http; + + ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0, + "js http done fetch:%p rc:%i", fetch, rc); + + ngx_js_http_close_peer(http); + + if (fetch->event != NULL) { + action = fetch->promise_callbacks[(rc != NGX_OK)]; + + cx = fetch->cx; + event = fetch->event; + + rc = ngx_qjs_call(cx, action, &retval, 1); + + external = JS_GetContextOpaque(cx); + ctx = ngx_qjs_external_ctx(cx, external); + ngx_js_del_event(ctx, event); + + ngx_qjs_external_event_finalize(cx)(external, rc); + } +} + + +static ngx_int_t +ngx_qjs_fetch_append_headers(ngx_js_http_t *http, ngx_js_headers_t *headers, + u_char *name, size_t len, u_char *value, size_t vlen) +{ + ngx_qjs_fetch_t *fetch; + + fetch = (ngx_qjs_fetch_t *) http; + + return ngx_qjs_headers_append(fetch->cx, &http->response.headers, + name, len, value, vlen); +} + + +static void +ngx_qjs_fetch_process_done(ngx_js_http_t *http) +{ + ngx_qjs_fetch_t *fetch; + + fetch = (ngx_qjs_fetch_t *) http; + + fetch->response_value = JS_NewObjectClass(fetch->cx, + NGX_QJS_CLASS_ID_FETCH_RESPONSE); + if (JS_IsException(fetch->response_value)) { + ngx_qjs_fetch_error(http, "fetch response creation failed"); + return; + } + + JS_SetOpaque(fetch->response_value, &http->response); + + ngx_qjs_fetch_done(fetch, fetch->response_value, NGX_OK); +} + + +static ngx_int_t +ngx_qjs_headers_append(JSContext *cx, ngx_js_headers_t *headers, + u_char *name, size_t len, u_char *value, size_t vlen) +{ + u_char *p, *end; + ngx_int_t ret; + ngx_uint_t i; + ngx_list_part_t *part; + ngx_js_tb_elt_t *h, **ph; + + ngx_js_http_trim(&value, &vlen, 0); + + ret = ngx_js_check_header_name(name, len); + if (ret != NGX_OK) { + JS_ThrowInternalError(cx, "invalid header name"); + return NGX_ERROR; + } + + p = value; + end = p + vlen; + + while (p < end) { + if (*p == '\0') { + JS_ThrowInternalError(cx, "invalid header value"); + return NGX_ERROR; + } + + p++; + } + + if (headers->guard == GUARD_IMMUTABLE) { + JS_ThrowInternalError(cx, "cannot append to immutable object"); + return NGX_ERROR; + } + + ph = NULL; + part = &headers->header_list.part; + h = part->elts; + + for (i = 0; /* void */; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + h = part->elts; + i = 0; + } + + if (h[i].hash == 0) { + continue; + } + + if (len == h[i].key.len + && (ngx_strncasecmp(name, h[i].key.data, len) == 0)) + { + ph = &h[i].next; + while (*ph) { ph = &(*ph)->next; } + break; + } + } + + h = ngx_list_push(&headers->header_list); + if (h == NULL) { + JS_ThrowOutOfMemory(cx); + return NGX_ERROR; + } + + if (ph != NULL) { + *ph = h; + } + + h->hash = 1; + h->key.data = name; + h->key.len = len; + h->value.data = value; + h->value.len = vlen; + h->next = NULL; + + if (len == (sizeof("Content-Type") - 1) + && ngx_strncasecmp(name, (u_char *) "Content-Type", len) == 0) + { + headers->content_type = h; + } + + return NGX_OK; +} + + +static JSValue +ngx_qjs_headers_ext_keys(JSContext *cx, JSValue value) +{ + int ret, found; + JSValue keys, key, item, func, retval; + uint32_t length; + ngx_str_t hdr; + ngx_uint_t i, k, n; + ngx_list_part_t *part; + ngx_js_tb_elt_t *h; + ngx_js_headers_t *headers; + + headers = JS_GetOpaque2(cx, value, NGX_QJS_CLASS_ID_FETCH_HEADERS); + if (headers == NULL) { + return JS_NULL; + } + + keys = JS_NewArray(cx); + if (JS_IsException(keys)) { + return JS_EXCEPTION; + } + + n = 0; + + part = &headers->header_list.part; + h = part->elts; + + for (i = 0; /* void */; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + h = part->elts; + i = 0; + } + + if (h[i].hash == 0) { + continue; + } + + if (qjs_array_length(cx, keys, &length)) { + goto fail; + } + + for (k = 0; k < length; k++) { + key = JS_GetPropertyUint32(cx, keys, k); + if (JS_IsException(key)) { + goto fail; + } + + hdr.data = (u_char *) JS_ToCStringLen(cx, &hdr.len, key); + JS_FreeValue(cx, key); + + found = h[i].key.len == hdr.len + && ngx_strncasecmp(h[i].key.data, + hdr.data, hdr.len) == 0; + + JS_FreeCString(cx, (const char *) hdr.data); + + if (found) { + break; + } + } + + if (k == n) { + item = JS_NewStringLen(cx, (const char *) h[i].key.data, + h[i].key.len); + if (JS_IsException(value)) { + goto fail; + } + + ret = JS_DefinePropertyValueUint32(cx, keys, n, item, + JS_PROP_C_W_E); + if (ret < 0) { + JS_FreeValue(cx, item); + goto fail; + } + + n++; + } + } + + func = JS_GetPropertyStr(cx, keys, "sort"); + if (JS_IsException(func)) { + JS_ThrowInternalError(cx, "sort function not found"); + goto fail; + } + + retval = JS_Call(cx, func, keys, 0, NULL); + + JS_FreeValue(cx, func); + JS_FreeValue(cx, keys); + + return retval; + +fail: + + JS_FreeValue(cx, keys); + + return JS_EXCEPTION; +} + + +static JSValue +ngx_qjs_headers_get(JSContext *cx, JSValue this_val, ngx_str_t *name, + int as_array) +{ + int ret; + JSValue retval, value; + njs_chb_t chain; + ngx_uint_t i; + ngx_list_part_t *part; + ngx_js_tb_elt_t *h, *ph; + ngx_js_headers_t *headers; + + headers = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_HEADERS); + if (headers == NULL) { + return JS_NULL; + } + + part = &headers->header_list.part; + h = part->elts; + ph = NULL; + + for (i = 0; /* void */; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + h = part->elts; + i = 0; + } + + if (h[i].hash == 0) { + continue; + } + + if (h[i].key.len == name->len + && ngx_strncasecmp(h[i].key.data, name->data, name->len) == 0) + { + ph = &h[i]; + break; + } + } + + if (as_array) { + retval = JS_NewArray(cx); + if (JS_IsException(retval)) { + return JS_EXCEPTION; + } + + i = 0; + while (ph != NULL) { + value = JS_NewStringLen(cx, (const char *) ph->value.data, + ph->value.len); + if (JS_IsException(value)) { + JS_FreeValue(cx, retval); + return JS_EXCEPTION; + } + + ret = JS_DefinePropertyValueUint32(cx, retval, i, value, + JS_PROP_C_W_E); + if (ret < 0) { + JS_FreeValue(cx, retval); + JS_FreeValue(cx, value); + return JS_EXCEPTION; + } + + i++; + ph = ph->next; + } + + return retval; + } + + if (ph == NULL) { + return JS_NULL; + } + + NJS_CHB_CTX_INIT(&chain, cx); + + h = ph; + + for ( ;; ) { + njs_chb_append(&chain, h->value.data, h->value.len); + + if (h->next == NULL) { + break; + } + + njs_chb_append_literal(&chain, ", "); + h = h->next; + } + + retval = qjs_string_create_chb(cx, &chain); + + return retval; +} + + +static int +ngx_qjs_fetch_headers_own_property(JSContext *cx, JSPropertyDescriptor *desc, + JSValueConst obj, JSAtom prop) +{ + JSValue value; + ngx_str_t name; + + name.data = (u_char *) JS_AtomToCString(cx, prop); + if (name.data == NULL) { + return -1; + } + + name.len = ngx_strlen(name.data); + + value = ngx_qjs_headers_get(cx, obj, &name, 0); + JS_FreeCString(cx, (char *) name.data); + + if (JS_IsException(value)) { + return -1; + } + + if (JS_IsNull(value)) { + return 0; + } + + if (desc == NULL) { + JS_FreeValue(cx, value); + + } else { + desc->flags = JS_PROP_ENUMERABLE; + desc->getter = JS_UNDEFINED; + desc->setter = JS_UNDEFINED; + desc->value = value; + } + + return 1; +} + + +static int +ngx_qjs_fetch_headers_own_property_names(JSContext *cx, JSPropertyEnum **ptab, + uint32_t *plen, JSValueConst obj) +{ + int ret; + JSAtom key; + JSValue keys; + ngx_uint_t i; + ngx_list_part_t *part; + ngx_js_tb_elt_t *h; + ngx_js_headers_t *headers; + + headers = JS_GetOpaque2(cx, obj, NGX_QJS_CLASS_ID_FETCH_HEADERS); + if (headers == NULL) { + (void) JS_ThrowInternalError(cx, "\"this\" is not a Headers object"); + return -1; + } + + keys = JS_NewObject(cx); + if (JS_IsException(keys)) { + return -1; + } + + part = &headers->header_list.part; + h = part->elts; + + for (i = 0; /* void */; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + h = part->elts; + i = 0; + } + + if (h[i].hash == 0) { + continue; + } + + key = JS_NewAtomLen(cx, (const char *) h[i].key.data, h[i].key.len); + if (key == JS_ATOM_NULL) { + goto fail; + } + + if (JS_DefinePropertyValue(cx, keys, key, JS_UNDEFINED, + JS_PROP_ENUMERABLE) < 0) + { + JS_FreeAtom(cx, key); + goto fail; + } + + JS_FreeAtom(cx, key); + } + + ret = JS_GetOwnPropertyNames(cx, ptab, plen, keys, + JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY); + + JS_FreeValue(cx, keys); + + return ret; + +fail: + + JS_FreeValue(cx, keys); + + return -1; +} + + +static JSValue +ngx_qjs_ext_fetch_headers_append(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + ngx_int_t rc; + ngx_js_headers_t *headers; + + headers = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_HEADERS); + if (headers == NULL) { + return JS_ThrowInternalError(cx, + "\"this\" is not fetch headers object"); + } + + rc = ngx_qjs_headers_fill_header_free(cx, headers, + JS_DupValue(cx, argv[0]), + JS_DupValue(cx, argv[1])); + if (rc != NGX_OK) { + return JS_EXCEPTION; + } + + return JS_UNDEFINED; +} + + +static JSValue +ngx_qjs_ext_fetch_headers_delete(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + ngx_int_t rc; + ngx_str_t name; + ngx_uint_t i; + ngx_list_part_t *part; + ngx_js_tb_elt_t *h; + ngx_js_headers_t *headers; + + headers = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_HEADERS); + if (headers == NULL) { + return JS_ThrowInternalError(cx, + "\"this\" is not fetch headers object"); + } + + rc = ngx_qjs_string(cx, argv[0], &name); + if (rc != NGX_OK) { + return JS_EXCEPTION; + } + + part = &headers->header_list.part; + h = part->elts; + + for (i = 0; /* void */; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + h = part->elts; + i = 0; + } + + if (h[i].hash == 0) { + continue; + } + + if (name.len == h[i].key.len + && (ngx_strncasecmp(name.data, h[i].key.data, name.len) == 0)) + { + h[i].hash = 0; + } + } + + if (name.len == (sizeof("Content-Type") - 1) + && ngx_strncasecmp(name.data, (u_char *) "Content-Type", name.len) + == 0) + { + headers->content_type = NULL; + } + + return JS_UNDEFINED; +} + + +static JSValue +ngx_qjs_ext_fetch_headers_foreach(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + int ret; + JSValue callback, keys, key; + JSValue header, retval, arguments[2]; + uint32_t length;; + ngx_str_t name; + ngx_uint_t i; + ngx_js_headers_t *headers; + + headers = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_HEADERS); + if (headers == NULL) { + return JS_ThrowInternalError(cx, + "\"this\" is not fetch headers object"); + } + + callback = argv[0]; + + if (!JS_IsFunction(cx, callback)) { + return JS_ThrowInternalError(cx, "\"callback\" is not a function"); + } + + keys = ngx_qjs_headers_ext_keys(cx, this_val); + if (JS_IsException(keys)) { + return JS_EXCEPTION; + } + + if (qjs_array_length(cx, keys, &length)) { + goto fail; + } + + for (i = 0; i < length; i++) { + key = JS_GetPropertyUint32(cx, keys, i); + if (JS_IsException(key)) { + goto fail; + } + + ret = ngx_qjs_string(cx, key, &name); + if (ret != NGX_OK) { + JS_FreeValue(cx, key); + goto fail; + } + + header = ngx_qjs_headers_get(cx, this_val, &name, 0); + if (JS_IsException(header)) { + JS_FreeValue(cx, key); + goto fail; + } + + arguments[0] = key; + arguments[1] = header; + + retval = JS_Call(cx, callback, JS_UNDEFINED, 2, arguments); + + JS_FreeValue(cx, key); + JS_FreeValue(cx, header); + JS_FreeValue(cx, retval); + } + + JS_FreeValue(cx, keys); + + return JS_UNDEFINED; + +fail: + + JS_FreeValue(cx, keys); + + return JS_EXCEPTION; +} + + +static JSValue +ngx_qjs_ext_fetch_headers_get(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv, int magic) +{ + ngx_int_t rc; + ngx_str_t name; + + rc = ngx_qjs_string(cx, argv[0], &name); + if (rc != NGX_OK) { + return JS_EXCEPTION; + } + + return ngx_qjs_headers_get(cx, this_val, &name, magic); +} + + +static JSValue +ngx_qjs_ext_fetch_headers_has(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + JSValue retval; + ngx_int_t rc; + ngx_str_t name; + + rc = ngx_qjs_string(cx, argv[0], &name); + if (rc != NGX_OK) { + return JS_EXCEPTION; + } + + retval = ngx_qjs_headers_get(cx, this_val, &name, 0); + if (JS_IsException(retval)) { + return JS_EXCEPTION; + } + + rc = !JS_IsNull(retval); + JS_FreeValue(cx, retval); + + return JS_NewBool(cx, rc); +} + + +static JSValue +ngx_qjs_ext_fetch_headers_set(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + ngx_int_t rc; + ngx_str_t name, value; + ngx_uint_t i; + ngx_list_part_t *part; + ngx_js_tb_elt_t *h, **ph, **pp; + ngx_js_headers_t *headers; + + headers = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_HEADERS); + if (headers == NULL) { + return JS_ThrowInternalError(cx, + "\"this\" is not fetch headers object"); + } + + rc = ngx_qjs_string(cx, argv[0], &name); + if (rc != NGX_OK) { + return JS_EXCEPTION; + } + + rc = ngx_qjs_string(cx, argv[1], &value); + if (rc != NGX_OK) { + return JS_EXCEPTION; + } + + part = &headers->header_list.part; + h = part->elts; + + for (i = 0; /* void */; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + h = part->elts; + i = 0; + } + + if (h[i].hash == 0) { + continue; + } + + if (name.len == h[i].key.len + && (ngx_strncasecmp(name.data, h[i].key.data, name.len) == 0)) + { + h[i].value.len = value.len; + h[i].value.data = value.data; + + ph = &h[i].next; + + while (*ph) { + pp = ph; + ph = &(*ph)->next; + *pp = NULL; + } + + return JS_UNDEFINED; + } + } + + rc = ngx_qjs_headers_append(cx, headers, name.data, name.len, + value.data, value.len); + if (rc != NGX_OK) { + return JS_EXCEPTION; + } + + return JS_UNDEFINED; +} + + +static JSValue +ngx_qjs_ext_fetch_request_body(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv, int magic) +{ + char * string; + JSValue result; + ngx_js_request_t *request; + + request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + if (request == NULL) { + return JS_UNDEFINED; + } + + if (request->body_used) { + return JS_ThrowInternalError(cx, "body stream already read"); + } + + request->body_used = 1; + + switch (magic) { + case NGX_QJS_BODY_ARRAY_BUFFER: + /* + * no free_func for JS_NewArrayBuffer() + * because request->body is allocated from e->pool + * and will be freed when context is freed. + */ + result = JS_NewArrayBuffer(cx, request->body.data, request->body.len, + NULL, NULL, 0); + if (JS_IsException(result)) { + return JS_ThrowOutOfMemory(cx); + } + + break; + + case NGX_QJS_BODY_JSON: + case NGX_QJS_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) { + string = js_malloc(cx, request->body.len + 1); + + JS_FreeValue(cx, result); + result = JS_UNDEFINED; + + if (string == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + ngx_memcpy(string, request->body.data, request->body.len); + string[request->body.len] = '\0'; + + /* 'string' must be zero terminated. */ + result = JS_ParseJSON(cx, string, request->body.len, ""); + js_free(cx, string); + if (JS_IsException(result)) { + break; + } + } + } + + return qjs_promise_result(cx, result); +} + + +static JSValue +ngx_qjs_ext_fetch_request_body_used(JSContext *cx, JSValueConst this_val) +{ + ngx_js_request_t *request; + + request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + if (request == NULL) { + return JS_UNDEFINED; + } + + return JS_NewBool(cx, request->body_used); +} + + +static JSValue +ngx_qjs_ext_fetch_request_cache(JSContext *cx, JSValueConst this_val) +{ + ngx_js_request_t *request; + + request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + if (request == NULL) { + return JS_UNDEFINED; + } + + return ngx_qjs_fetch_flag(cx, ngx_qjs_fetch_cache_modes, + request->cache_mode); +} + + +static JSValue +ngx_qjs_ext_fetch_request_credentials(JSContext *cx, JSValueConst this_val) +{ + ngx_js_request_t *request; + + request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + if (request == NULL) { + return JS_UNDEFINED; + } + + return ngx_qjs_fetch_flag(cx, ngx_qjs_fetch_credentials, + request->credentials); +} + + +static JSValue +ngx_qjs_ext_fetch_request_headers(JSContext *cx, JSValueConst this_val) +{ + JSValue header; + ngx_js_request_t *request; + + request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + if (request == NULL) { + return JS_UNDEFINED; + } + + header = ngx_qjs_arg(request->header_value); + + if (JS_IsUndefined(header)) { + header = JS_NewObjectClass(cx, NGX_QJS_CLASS_ID_FETCH_HEADERS); + if (JS_IsException(header)) { + return JS_ThrowInternalError(cx, "fetch header creation failed"); + } + + JS_SetOpaque(header, &request->headers); + + ngx_qjs_arg(request->header_value) = header; + } + + return JS_DupValue(cx, header); +} + + +static JSValue +ngx_qjs_ext_fetch_request_field(JSContext *cx, JSValueConst this_val, int magic) +{ + ngx_str_t *field; + ngx_js_request_t *request; + + request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + if (request == NULL) { + return JS_UNDEFINED; + } + + field = (ngx_str_t *) ((u_char *) request + magic); + + return qjs_string_create(cx, field->data, field->len); +} + + +static JSValue +ngx_qjs_ext_fetch_request_mode(JSContext *cx, JSValueConst this_val) +{ + ngx_js_request_t *request; + + request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + if (request == NULL) { + return JS_UNDEFINED; + } + + return ngx_qjs_fetch_flag(cx, ngx_qjs_fetch_modes, request->mode); +} + + +static void +ngx_qjs_fetch_request_finalizer(JSRuntime *rt, JSValue val) +{ + ngx_js_request_t *request; + + request = JS_GetOpaque(val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + + JS_FreeValueRT(rt, ngx_qjs_arg(request->header_value)); +} + + +static JSValue +ngx_qjs_ext_fetch_response_status(JSContext *cx, JSValueConst this_val) +{ + ngx_js_response_t *response; + + response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + if (response == NULL) { + return JS_UNDEFINED; + } + + return JS_NewUint32(cx, response->code); +} + + +static JSValue +ngx_qjs_ext_fetch_response_status_text(JSContext *cx, JSValueConst this_val) +{ + ngx_js_response_t *response; + + response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + if (response == NULL) { + return JS_UNDEFINED; + } + + return qjs_string_create(cx, response->status_text.data, + response->status_text.len); +} + + +static JSValue +ngx_qjs_ext_fetch_response_ok(JSContext *cx, JSValueConst this_val) +{ + ngx_js_response_t *response; + + response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + if (response == NULL) { + return JS_UNDEFINED; + } + + return JS_NewBool(cx, response->code >= 200 && response->code < 300); +} + + +static JSValue +ngx_qjs_ext_fetch_response_body_used(JSContext *cx, JSValueConst this_val) +{ + ngx_js_response_t *response; + + response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + if (response == NULL) { + return JS_UNDEFINED; + } + + return JS_NewBool(cx, response->body_used); +} + + +static JSValue +ngx_qjs_ext_fetch_response_headers(JSContext *cx, JSValueConst this_val) +{ + JSValue header; + ngx_js_response_t *response; + + response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + if (response == NULL) { + return JS_UNDEFINED; + } + + header = ngx_qjs_arg(response->header_value); + + if (JS_IsUndefined(header)) { + header = JS_NewObjectClass(cx, NGX_QJS_CLASS_ID_FETCH_HEADERS); + if (JS_IsException(header)) { + return JS_ThrowInternalError(cx, "fetch header creation failed"); + } + + JS_SetOpaque(header, &response->headers); + + ngx_qjs_arg(response->header_value) = header; + } + + return JS_DupValue(cx, header); +} + + +static JSValue +ngx_qjs_ext_fetch_response_type(JSContext *cx, JSValueConst this_val) +{ + ngx_js_response_t *response; + + response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + if (response == NULL) { + return JS_UNDEFINED; + } + + return JS_NewString(cx, "basic"); +} + + +static JSValue +ngx_qjs_ext_fetch_response_body(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv, int magic) +{ + JSValue result; + njs_int_t ret; + njs_str_t string; + ngx_js_response_t *response; + + response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + if (response == NULL) { + return JS_UNDEFINED; + } + + if (response->body_used) { + return JS_ThrowInternalError(cx, "body stream already read"); + } + + response->body_used = 1; + + switch (magic) { + case NGX_QJS_BODY_ARRAY_BUFFER: + case NGX_QJS_BODY_TEXT: + ret = njs_chb_join(&response->chain, &string); + if (ret != NJS_OK) { + return JS_ThrowOutOfMemory(cx); + } + + if (magic == NGX_QJS_BODY_TEXT) { + result = qjs_string_create(cx, string.start, string.length); + if (JS_IsException(result)) { + return JS_ThrowOutOfMemory(cx); + } + + break; + } + + /* + * no free_func for JS_NewArrayBuffer() + * because string.start is allocated from e->pool + * and will be freed when context is freed. + */ + result = JS_NewArrayBuffer(cx, string.start, string.length, NULL, NULL, + 0); + if (JS_IsException(result)) { + return JS_ThrowOutOfMemory(cx); + } + + break; + + case NGX_QJS_BODY_JSON: + default: + /* 'string.start' must be zero terminated. */ + njs_chb_append_literal(&response->chain, "\0"); + ret = njs_chb_join(&response->chain, &string); + if (ret != NJS_OK) { + return JS_ThrowOutOfMemory(cx); + } + + result = JS_ParseJSON(cx, (char *) string.start, string.length - 1, + ""); + if (JS_IsException(result)) { + break; + } + } + + return qjs_promise_result(cx, result); +} + + +static JSValue +ngx_qjs_ext_fetch_response_redirected(JSContext *cx, JSValueConst this_val) +{ + ngx_js_response_t *response; + + response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + if (response == NULL) { + return JS_UNDEFINED; + } + + return JS_NewBool(cx, 0); +} + + +static JSValue +ngx_qjs_ext_fetch_response_field(JSContext *cx, JSValueConst this_val, int magic) +{ + ngx_str_t *field; + ngx_js_response_t *response; + + response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + if (response == NULL) { + return JS_UNDEFINED; + } + + field = (ngx_str_t *) ((u_char *) response + magic); + + return qjs_string_create(cx, field->data, field->len); +} + + +static void +ngx_qjs_fetch_response_finalizer(JSRuntime *rt, JSValue val) +{ + ngx_js_response_t *response; + + response = JS_GetOpaque(val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + + JS_FreeValueRT(rt, ngx_qjs_arg(response->header_value)); + njs_chb_destroy(&response->chain); +} + + +static JSValue +ngx_qjs_fetch_flag(JSContext *cx, const ngx_qjs_entry_t *entries, + ngx_int_t value) +{ + const ngx_qjs_entry_t *e; + + for (e = entries; e->name.len != 0; e++) { + if (e->value == value) { + return qjs_string_create(cx, e->name.data, e->name.len); + } + } + + return JS_ThrowInternalError(cx, "unknown fetch flag: %i", (int) value); +} + + +static ngx_int_t +ngx_qjs_fetch_flag_set(JSContext *cx, const ngx_qjs_entry_t *entries, + JSValue object, const char *prop) +{ + JSValue value; + ngx_int_t rc; + ngx_str_t flag; + const ngx_qjs_entry_t *e; + + value = JS_GetPropertyStr(cx, object, prop); + if (JS_IsException(value)) { + JS_ThrowInternalError(cx, "failed to get %s property", prop); + return NGX_ERROR; + } + + if (JS_IsUndefined(value)) { + return entries[0].value; + } + + rc = ngx_qjs_string(cx, value, &flag); + JS_FreeValue(cx, value); + if (rc != NGX_OK) { + return NGX_ERROR; + } + + for (e = entries; e->name.len != 0; e++) { + if (flag.len == e->name.len + && ngx_strncasecmp(e->name.data, flag.data, flag.len) == 0) + { + return e->value; + } + } + + JS_ThrowInternalError(cx, "unknown %s type: %.*s", prop, + (int) flag.len, flag.data); + + return NGX_ERROR; +} + + +static JSModuleDef * +ngx_qjs_fetch_init(JSContext *cx, const char *name) +{ + int i, class_id; + JSValue global_obj, proto, class; + + static const JSClassDef *const classes[] = { + &ngx_qjs_fetch_headers_class, + &ngx_qjs_fetch_request_class, + &ngx_qjs_fetch_response_class, + NULL + }; + + static JSCFunction *ctors[] = { + ngx_qjs_fetch_headers_ctor, + ngx_qjs_fetch_request_ctor, + ngx_qjs_fetch_response_ctor, + NULL + }; + + static const JSCFunctionListEntry *const protos[] = { + ngx_qjs_ext_fetch_headers_proto, + ngx_qjs_ext_fetch_request_proto, + ngx_qjs_ext_fetch_response_proto, + NULL + }; + + static const uint8_t proto_sizes[] = { + njs_nitems(ngx_qjs_ext_fetch_headers_proto), + njs_nitems(ngx_qjs_ext_fetch_request_proto), + njs_nitems(ngx_qjs_ext_fetch_response_proto), + 0 + }; + + global_obj = JS_GetGlobalObject(cx); + + for (i = 0; classes[i] != NULL; i++) { + class_id = NGX_QJS_CLASS_ID_FETCH_HEADERS + i; + + if (JS_NewClass(JS_GetRuntime(cx), class_id, classes[i]) < 0) { + return NULL; + } + + proto = JS_NewObject(cx); + if (JS_IsException(proto)) { + JS_FreeValue(cx, global_obj); + return NULL; + } + + JS_SetPropertyFunctionList(cx, proto, protos[i], proto_sizes[i]); + + class = JS_NewCFunction2(cx, ctors[i], classes[i]->class_name, 2, + JS_CFUNC_constructor, 0); + if (JS_IsException(class)) { + JS_FreeValue(cx, proto); + JS_FreeValue(cx, global_obj); + return NULL; + } + + JS_SetConstructor(cx, class, proto); + JS_SetClassProto(cx, class_id, proto); + + if (JS_SetPropertyStr(cx, global_obj, classes[i]->class_name, class) + < 0) + { + JS_FreeValue(cx, class); + JS_FreeValue(cx, proto); + JS_FreeValue(cx, global_obj); + return NULL; + } + } + + JS_FreeValue(cx, global_obj); + + return JS_NewCModule(cx, name, NULL); +} diff --git a/nginx/ngx_stream_js_module.c b/nginx/ngx_stream_js_module.c index 5c837494..0e022eb0 100644 --- a/nginx/ngx_stream_js_module.c +++ b/nginx/ngx_stream_js_module.c @@ -834,6 +834,7 @@ static JSClassDef ngx_stream_qjs_variables_class = { qjs_module_t *njs_stream_qjs_addon_modules[] = { &ngx_qjs_ngx_module, &ngx_qjs_ngx_shared_dict_module, + &ngx_qjs_ngx_fetch_module, /* * Shared addons should be in the same order and the same positions * in all nginx modules. diff --git a/nginx/t/js_fetch.t b/nginx/t/js_fetch.t index ae9d1f61..7ee1a602 100644 --- a/nginx/t/js_fetch.t +++ b/nginx/t/js_fetch.t @@ -342,6 +342,8 @@ $t->write_file('test.js', <try_run('no njs.fetch'); -plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; - $t->plan(37); $t->run_daemon(\&http_daemon, port(8082)); diff --git a/nginx/t/js_fetch_https.t b/nginx/t/js_fetch_https.t index 9a44a339..8ede1048 100644 --- a/nginx/t/js_fetch_https.t +++ b/nginx/t/js_fetch_https.t @@ -196,8 +196,6 @@ foreach my $name ('default.example.com', '1.example.com') { $t->try_run('no njs.fetch'); -plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; - $t->plan(7); $t->run_daemon(\&dns_daemon, port(8981), $t); diff --git a/nginx/t/js_fetch_objects.t b/nginx/t/js_fetch_objects.t index 67cabdfc..bc5cc7ed 100644 --- a/nginx/t/js_fetch_objects.t +++ b/nginx/t/js_fetch_objects.t @@ -271,7 +271,9 @@ $t->write_file('test.js', <write_file('test.js', < { + var r = new Request("http://nginx.org", {body: 'ABC'}); + var body = await r.arrayBuffer(); + body = new Uint8Array(body); + return new TextDecoder().decode(body); + }, 'ABC'], + ['json()', async () => { + var r = new Request("http://nginx.org", {body: '{"a": 42}'}); + var body = await r.json(); + return body.a; + }, 42], ['user content type', async () => { var r = new Request("http://nginx.org", {body: 'ABC', @@ -443,6 +456,12 @@ $t->write_file('test.js', < { + var r = new Response('foo'); + var body = await r.arrayBuffer(); + body = new Uint8Array(body); + return new TextDecoder().decode(body); + }, 'foo'], ['json', async () => { var r = new Response('{"a": {"b": 42}}'); var json = await r.json(); @@ -515,8 +534,6 @@ EOF $t->try_run('no njs'); -plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; - $t->plan(5); ############################################################################### diff --git a/nginx/t/js_fetch_resolver.t b/nginx/t/js_fetch_resolver.t index 7cea3386..031ff43c 100644 --- a/nginx/t/js_fetch_resolver.t +++ b/nginx/t/js_fetch_resolver.t @@ -146,8 +146,6 @@ EOF $t->try_run('no njs.fetch'); -plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; - $t->plan(5); $t->run_daemon(\&dns_daemon, port(8981), $t); diff --git a/nginx/t/js_fetch_timeout.t b/nginx/t/js_fetch_timeout.t index 5b207b90..ab1ba24a 100644 --- a/nginx/t/js_fetch_timeout.t +++ b/nginx/t/js_fetch_timeout.t @@ -116,8 +116,6 @@ EOF $t->try_run('no js_fetch_timeout'); -plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; - $t->plan(2); ############################################################################### diff --git a/nginx/t/js_fetch_verify.t b/nginx/t/js_fetch_verify.t index 4c97e04d..f98b4d8c 100644 --- a/nginx/t/js_fetch_verify.t +++ b/nginx/t/js_fetch_verify.t @@ -114,8 +114,6 @@ foreach my $name ('localhost') { $t->try_run('no js_fetch_verify'); -plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; - $t->plan(2); $t->run_daemon(\&dns_daemon, port(8981), $t); diff --git a/nginx/t/js_periodic_fetch.t b/nginx/t/js_periodic_fetch.t index d7bcfb76..0231b662 100644 --- a/nginx/t/js_periodic_fetch.t +++ b/nginx/t/js_periodic_fetch.t @@ -121,8 +121,6 @@ EOF $t->try_run('no js_periodic with fetch'); -plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; - $t->plan(3); ############################################################################### diff --git a/nginx/t/stream_js_fetch.t b/nginx/t/stream_js_fetch.t index c57128a8..9a42ae29 100644 --- a/nginx/t/stream_js_fetch.t +++ b/nginx/t/stream_js_fetch.t @@ -171,8 +171,6 @@ EOF $t->try_run('no stream njs available'); -plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; - $t->plan(9); $t->run_daemon(\&stream_daemon, port(8090), port(8091)); diff --git a/nginx/t/stream_js_fetch_https.t b/nginx/t/stream_js_fetch_https.t index 5d7c5c20..987a896a 100644 --- a/nginx/t/stream_js_fetch_https.t +++ b/nginx/t/stream_js_fetch_https.t @@ -273,8 +273,6 @@ foreach my $name ('default.example.com', '1.example.com') { $t->try_run('no njs.fetch'); -plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; - $t->plan(6); $t->run_daemon(\&dns_daemon, port(8981), $t); diff --git a/nginx/t/stream_js_fetch_init.t b/nginx/t/stream_js_fetch_init.t index 3f6d7262..f48b9d5e 100644 --- a/nginx/t/stream_js_fetch_init.t +++ b/nginx/t/stream_js_fetch_init.t @@ -92,8 +92,6 @@ EOF $t->try_run('no stream njs available'); -plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; - $t->plan(1); $t->run_daemon(\&stream_daemon, port(8090)); diff --git a/nginx/t/stream_js_periodic_fetch.t b/nginx/t/stream_js_periodic_fetch.t index e88d69d5..60599423 100644 --- a/nginx/t/stream_js_periodic_fetch.t +++ b/nginx/t/stream_js_periodic_fetch.t @@ -147,7 +147,6 @@ EOF $t->run_daemon(\&stream_daemon, port(8090)); $t->try_run('no js_periodic with fetch'); -plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; $t->plan(3); $t->waitforsocket('127.0.0.1:' . port(8090)); diff --git a/src/qjs.c b/src/qjs.c index a941ba71..7993de1b 100644 --- a/src/qjs.c +++ b/src/qjs.c @@ -1147,6 +1147,31 @@ qjs_string_base64url(JSContext *cx, const njs_str_t *src) } +int +qjs_array_length(JSContext *cx, JSValueConst arr, uint32_t *plen) +{ + int ret; + JSValue value; + uint32_t len; + + value = JS_GetPropertyStr(cx, arr, "length"); + if (JS_IsException(value)) { + return -1; + } + + ret = JS_ToUint32(cx, &len, value); + JS_FreeValue(cx, value); + + if (ret) { + return -1; + } + + *plen = len; + + return 0; +} + + static JSValue qjs_promise_fill_trampoline(JSContext *cx, int argc, JSValueConst *argv) { diff --git a/src/qjs.h b/src/qjs.h index d3bbc0e8..e920453e 100644 --- a/src/qjs.h +++ b/src/qjs.h @@ -136,6 +136,8 @@ JSValue qjs_string_create_chb(JSContext *cx, njs_chb_t *chain); void qjs_free_prop_enum(JSContext *ctx, JSPropertyEnum *tab, uint32_t len); +int qjs_array_length(JSContext *cx, JSValueConst arr, uint32_t *plen); + JSValue qjs_promise_result(JSContext *cx, JSValue result); JSValue qjs_string_hex(JSContext *cx, const njs_str_t *src); From noreply at nginx.com Thu May 8 17:15:03 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Thu, 8 May 2025 17:15:03 +0000 (UTC) Subject: [njs] Fetch: separated ngx_js_http_* from ngx_js_fetch_*. Message-ID: <20250508171503.17E613F45A@pubserv1.nginx> details: https://github.com/nginx/njs/commit/401d622aa5fb9c757fde3f6e5cef0731b2b7726a branches: master commit: 401d622aa5fb9c757fde3f6e5cef0731b2b7726a user: Zhidao HONG date: Tue, 22 Apr 2025 01:04:31 +0800 description: Fetch: separated ngx_js_http_* from ngx_js_fetch_*. --- nginx/config | 2 + nginx/ngx_js_fetch.c | 2190 +++++++------------------------------------------- nginx/ngx_js_http.c | 1531 +++++++++++++++++++++++++++++++++++ nginx/ngx_js_http.h | 160 ++++ 4 files changed, 2000 insertions(+), 1883 deletions(-) diff --git a/nginx/config b/nginx/config index 03ec03d0..b994f97f 100644 --- a/nginx/config +++ b/nginx/config @@ -6,9 +6,11 @@ NJS_ZLIB=${NJS_ZLIB:-YES} NJS_QUICKJS=${NJS_QUICKJS:-YES} NJS_DEPS="$ngx_addon_dir/ngx_js.h \ + $ngx_addon_dir/ngx_js_http.h \ $ngx_addon_dir/ngx_js_fetch.h \ $ngx_addon_dir/ngx_js_shared_dict.h" NJS_SRCS="$ngx_addon_dir/ngx_js.c \ + $ngx_addon_dir/ngx_js_http.c \ $ngx_addon_dir/ngx_js_fetch.c \ $ngx_addon_dir/ngx_js_regex.c \ $ngx_addon_dir/ngx_js_shared_dict.c" diff --git a/nginx/ngx_js_fetch.c b/nginx/ngx_js_fetch.c index 4902fe4f..802353d6 100644 --- a/nginx/ngx_js_fetch.c +++ b/nginx/ngx_js_fetch.c @@ -12,9 +12,7 @@ #include #include #include "ngx_js.h" - - -typedef struct ngx_js_http_s ngx_js_http_t; +#include "ngx_js_http.h" typedef struct { @@ -24,136 +22,16 @@ typedef struct { typedef struct { - ngx_uint_t state; - ngx_uint_t code; - u_char *status_text; - u_char *status_text_end; - ngx_uint_t count; - ngx_flag_t chunked; - off_t content_length_n; - - u_char *header_name_start; - u_char *header_name_end; - u_char *header_start; - u_char *header_end; -} ngx_js_http_parse_t; - - -typedef struct { - u_char *pos; - uint64_t chunk_size; - uint8_t state; - uint8_t last; -} ngx_js_http_chunk_parse_t; - - -typedef struct ngx_js_tb_elt_s ngx_js_tb_elt_t; - -struct ngx_js_tb_elt_s { - ngx_uint_t hash; - ngx_str_t key; - ngx_str_t value; - ngx_js_tb_elt_t *next; -}; - - -typedef struct { - enum { - GUARD_NONE = 0, - GUARD_REQUEST, - GUARD_IMMUTABLE, - GUARD_RESPONSE, - } guard; - ngx_list_t header_list; - ngx_js_tb_elt_t *content_type; -} ngx_js_headers_t; - - -typedef struct { - enum { - CACHE_MODE_DEFAULT = 0, - CACHE_MODE_NO_STORE, - CACHE_MODE_RELOAD, - CACHE_MODE_NO_CACHE, - CACHE_MODE_FORCE_CACHE, - CACHE_MODE_ONLY_IF_CACHED, - } cache_mode; - enum { - CREDENTIALS_SAME_ORIGIN = 0, - CREDENTIALS_INCLUDE, - CREDENTIALS_OMIT, - } credentials; - enum { - MODE_NO_CORS = 0, - MODE_SAME_ORIGIN, - MODE_CORS, - MODE_NAVIGATE, - MODE_WEBSOCKET, - } mode; - njs_str_t url; - njs_str_t method; - u_char m[8]; - uint8_t body_used; - njs_str_t body; - ngx_js_headers_t headers; - njs_opaque_value_t header_value; -} ngx_js_request_t; - - -typedef struct { - njs_str_t url; - ngx_int_t code; - njs_str_t status_text; - uint8_t body_used; - njs_chb_t chain; - ngx_js_headers_t headers; - njs_opaque_value_t header_value; -} ngx_js_response_t; - - -struct ngx_js_http_s { - ngx_log_t *log; - ngx_pool_t *pool; + ngx_js_http_t http; njs_vm_t *vm; ngx_js_event_t *event; - ngx_resolver_ctx_t *ctx; - ngx_addr_t addr; - ngx_addr_t *addrs; - ngx_uint_t naddrs; - ngx_uint_t naddr; - in_port_t port; - - ngx_peer_connection_t peer; - ngx_msec_t timeout; - - ngx_int_t buffer_size; - ngx_int_t max_response_body_size; - - unsigned header_only; - -#if (NGX_SSL) - ngx_str_t tls_name; - ngx_ssl_t *ssl; - njs_bool_t ssl_verify; -#endif - - ngx_buf_t *buffer; - ngx_buf_t *chunk; - njs_chb_t chain; - - ngx_js_response_t response; njs_opaque_value_t response_value; njs_opaque_value_t promise; njs_opaque_value_t promise_callbacks[2]; - - uint8_t done; - ngx_js_http_parse_t http_parse; - ngx_js_http_chunk_parse_t http_chunk_parse; - ngx_int_t (*process)(ngx_js_http_t *http); -}; +} ngx_js_fetch_t; static njs_int_t ngx_js_method_process(njs_vm_t *vm, ngx_js_request_t *r); @@ -161,44 +39,29 @@ static njs_int_t ngx_js_headers_inherit(njs_vm_t *vm, ngx_js_headers_t *headers, ngx_js_headers_t *orig); static njs_int_t ngx_js_headers_fill(njs_vm_t *vm, ngx_js_headers_t *headers, njs_value_t *init); -static ngx_js_http_t *ngx_js_http_alloc(njs_vm_t *vm, ngx_pool_t *pool, +static ngx_js_fetch_t *ngx_js_fetch_alloc(njs_vm_t *vm, ngx_pool_t *pool, ngx_log_t *log); -static void ngx_js_http_resolve_done(ngx_js_http_t *http); -static void ngx_js_http_close_peer(ngx_js_http_t *http); -static void ngx_js_http_destructor(ngx_js_event_t *event); -static ngx_resolver_ctx_t *ngx_js_http_resolve(ngx_js_http_t *http, - ngx_resolver_t *r, ngx_str_t *host, in_port_t port, ngx_msec_t timeout); -static void ngx_js_http_resolve_handler(ngx_resolver_ctx_t *ctx); +static void ngx_js_fetch_error(ngx_js_http_t *http, const char *err); +static void ngx_js_fetch_destructor(ngx_js_event_t *event); static njs_int_t ngx_js_fetch_promissified_result(njs_vm_t *vm, njs_value_t *result, njs_int_t rc, njs_value_t *retval); -static void ngx_js_http_fetch_done(ngx_js_http_t *http, - njs_opaque_value_t *retval, njs_int_t rc); +static void ngx_js_fetch_done(ngx_js_fetch_t *fetch, njs_opaque_value_t *retval, + njs_int_t rc); static njs_int_t ngx_js_http_promise_trampoline(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval); -static void ngx_js_http_connect(ngx_js_http_t *http); -static void ngx_js_http_next(ngx_js_http_t *http); -static void ngx_js_http_write_handler(ngx_event_t *wev); -static void ngx_js_http_read_handler(ngx_event_t *rev); static njs_int_t ngx_js_request_constructor(njs_vm_t *vm, ngx_js_request_t *request, ngx_url_t *u, njs_external_ptr_t external, njs_value_t *args, njs_uint_t nargs); +static ngx_int_t ngx_js_fetch_append_headers(ngx_js_http_t *http, + ngx_js_headers_t *headers, u_char *name, size_t len, u_char *value, + size_t vlen); +static void ngx_js_fetch_process_done(ngx_js_http_t *http); static njs_int_t ngx_js_headers_append(njs_vm_t *vm, ngx_js_headers_t *headers, u_char *name, size_t len, u_char *value, size_t vlen); -static ngx_int_t ngx_js_http_process_status_line(ngx_js_http_t *http); -static ngx_int_t ngx_js_http_process_headers(ngx_js_http_t *http); -static ngx_int_t ngx_js_http_process_body(ngx_js_http_t *http); -static ngx_int_t ngx_js_http_parse_status_line(ngx_js_http_parse_t *hp, - ngx_buf_t *b); -static ngx_int_t ngx_js_http_parse_header_line(ngx_js_http_parse_t *hp, - ngx_buf_t *b); -static ngx_int_t ngx_js_http_parse_chunked(ngx_js_http_chunk_parse_t *hcp, - ngx_buf_t *b, njs_chb_t *chain); -static void ngx_js_http_dummy_handler(ngx_event_t *ev); - static njs_int_t ngx_headers_js_ext_append(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_headers_js_ext_delete(njs_vm_t *vm, njs_value_t *args, @@ -254,15 +117,6 @@ static njs_int_t ngx_response_js_ext_type(njs_vm_t *vm, static njs_int_t ngx_response_js_ext_body(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval); -#if (NGX_SSL) -static void ngx_js_http_ssl_init_connection(ngx_js_http_t *http); -static void ngx_js_http_ssl_handshake_handler(ngx_connection_t *c); -static void ngx_js_http_ssl_handshake(ngx_js_http_t *http); -static njs_int_t ngx_js_http_ssl_name(ngx_js_http_t *http); -#endif - -static void ngx_js_http_trim(u_char **value, size_t *len, - njs_bool_t trim_c0_control_or_space); static njs_int_t ngx_fetch_flag(njs_vm_t *vm, const ngx_js_entry_t *entries, njs_int_t value, njs_value_t *retval); static njs_int_t ngx_fetch_flag_set(njs_vm_t *vm, const ngx_js_entry_t *entries, @@ -663,6 +517,7 @@ ngx_js_ext_fetch(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, ngx_pool_t *pool; njs_value_t *init, *value; ngx_js_http_t *http; + ngx_js_fetch_t *fetch; ngx_list_part_t *part; ngx_js_tb_elt_t *h; ngx_js_request_t request; @@ -681,11 +536,13 @@ ngx_js_ext_fetch(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, c = ngx_external_connection(vm, external); pool = ngx_external_pool(vm, external); - http = ngx_js_http_alloc(vm, pool, c->log); - if (http == NULL) { + fetch = ngx_js_fetch_alloc(vm, pool, c->log); + if (fetch == NULL) { return NJS_ERROR; } + http = &fetch->http; + ret = ngx_js_request_constructor(vm, &request, &u, external, args, nargs); if (ret != NJS_OK) { goto fail; @@ -734,6 +591,7 @@ ngx_js_ext_fetch(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, http->header_only = njs_strstr_eq(&request.method, &njs_str_value("HEAD")); NJS_CHB_MP_INIT(&http->chain, njs_vm_memory_pool(vm)); + NJS_CHB_MP_INIT(&http->response.chain, njs_vm_memory_pool(vm)); njs_chb_append(&http->chain, request.method.start, request.method.length); njs_chb_append_literal(&http->chain, " "); @@ -848,7 +706,7 @@ ngx_js_ext_fetch(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, goto fail; } - njs_value_assign(retval, njs_value_arg(&http->promise)); + njs_value_assign(retval, njs_value_arg(&fetch->promise)); return NJS_OK; } @@ -859,18 +717,17 @@ ngx_js_ext_fetch(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, ngx_js_http_connect(http); - njs_value_assign(retval, njs_value_arg(&http->promise)); + njs_value_assign(retval, njs_value_arg(&fetch->promise)); return NJS_OK; fail: - njs_vm_exception_get(vm, njs_value_arg(&lvalue)); - ngx_js_http_fetch_done(http, &lvalue, NJS_ERROR); + ngx_js_fetch_done(fetch, &lvalue, NJS_ERROR); - njs_value_assign(retval, njs_value_arg(&http->promise)); + njs_value_assign(retval, njs_value_arg(&fetch->promise)); return NJS_OK; } @@ -1249,30 +1106,36 @@ ngx_js_headers_fill(njs_vm_t *vm, ngx_js_headers_t *headers, njs_value_t *init) } -static ngx_js_http_t * -ngx_js_http_alloc(njs_vm_t *vm, ngx_pool_t *pool, ngx_log_t *log) +static ngx_js_fetch_t * +ngx_js_fetch_alloc(njs_vm_t *vm, ngx_pool_t *pool, ngx_log_t *log) { njs_int_t ret; ngx_js_ctx_t *ctx; ngx_js_http_t *http; + ngx_js_fetch_t *fetch; ngx_js_event_t *event; njs_function_t *callback; - http = ngx_pcalloc(pool, sizeof(ngx_js_http_t)); - if (http == NULL) { + fetch = ngx_pcalloc(pool, sizeof(ngx_js_fetch_t)); + if (fetch == NULL) { goto failed; } + http = &fetch->http; + http->pool = pool; http->log = log; - http->vm = vm; http->timeout = 10000; http->http_parse.content_length_n = -1; - ret = njs_vm_promise_create(vm, njs_value_arg(&http->promise), - njs_value_arg(&http->promise_callbacks)); + http->append_headers = ngx_js_fetch_append_headers; + http->ready_handler = ngx_js_fetch_process_done; + http->error_handler = ngx_js_fetch_error; + + ret = njs_vm_promise_create(vm, njs_value_arg(&fetch->promise), + njs_value_arg(&fetch->promise_callbacks)); if (ret != NJS_OK) { goto failed; } @@ -1291,17 +1154,18 @@ ngx_js_http_alloc(njs_vm_t *vm, ngx_pool_t *pool, ngx_log_t *log) event->ctx = vm; njs_value_function_set(njs_value_arg(&event->function), callback); - event->destructor = ngx_js_http_destructor; + event->destructor = ngx_js_fetch_destructor; event->fd = ctx->event_id++; - event->data = http; + event->data = fetch; ngx_js_add_event(ctx, event); - http->event = event; + fetch->vm = vm; + fetch->event = event; - ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "js fetch alloc:%p", http); + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, log, 0, "js fetch alloc:%p", fetch); - return http; + return fetch; failed: @@ -1312,194 +1176,31 @@ failed: static void -ngx_js_http_error(ngx_js_http_t *http, const char *fmt, ...) -{ - u_char *p, *end; - va_list args; - u_char err[NGX_MAX_ERROR_STR]; - - end = err + NGX_MAX_ERROR_STR - 1; - - va_start(args, fmt); - p = njs_vsprintf(err, end, fmt, args); - *p = '\0'; - va_end(args); - - njs_vm_error(http->vm, (const char *) err); - njs_vm_exception_get(http->vm, njs_value_arg(&http->response_value)); - ngx_js_http_fetch_done(http, &http->response_value, NJS_ERROR); -} - - -static ngx_resolver_ctx_t * -ngx_js_http_resolve(ngx_js_http_t *http, ngx_resolver_t *r, ngx_str_t *host, - in_port_t port, ngx_msec_t timeout) -{ - ngx_int_t ret; - ngx_resolver_ctx_t *ctx; - - ctx = ngx_resolve_start(r, NULL); - if (ctx == NULL) { - return NULL; - } - - if (ctx == NGX_NO_RESOLVER) { - return ctx; - } - - http->ctx = ctx; - http->port = port; - - ctx->name = *host; - ctx->handler = ngx_js_http_resolve_handler; - ctx->data = http; - ctx->timeout = timeout; - - ret = ngx_resolve_name(ctx); - if (ret != NGX_OK) { - http->ctx = NULL; - return NULL; - } - - return ctx; -} - - -static void -ngx_js_http_resolve_handler(ngx_resolver_ctx_t *ctx) -{ - u_char *p; - size_t len; - socklen_t socklen; - ngx_uint_t i; - ngx_js_http_t *http; - struct sockaddr *sockaddr; - - http = ctx->data; - - if (ctx->state) { - ngx_js_http_error(http, "\"%V\" could not be resolved (%i: %s)", - &ctx->name, ctx->state, - ngx_resolver_strerror(ctx->state)); - return; - } - - ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, - "http fetch resolved: \"%V\"", &ctx->name); - -#if (NGX_DEBUG) - { - u_char text[NGX_SOCKADDR_STRLEN]; - ngx_str_t addr; - ngx_uint_t i; - - addr.data = text; - - for (i = 0; i < ctx->naddrs; i++) { - addr.len = ngx_sock_ntop(ctx->addrs[i].sockaddr, ctx->addrs[i].socklen, - text, NGX_SOCKADDR_STRLEN, 0); - - ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, - "name was resolved to \"%V\"", &addr); - } - } -#endif - - http->naddrs = ctx->naddrs; - http->addrs = ngx_pcalloc(http->pool, http->naddrs * sizeof(ngx_addr_t)); - - if (http->addrs == NULL) { - goto failed; - } - - for (i = 0; i < ctx->naddrs; i++) { - socklen = ctx->addrs[i].socklen; - - sockaddr = ngx_palloc(http->pool, socklen); - if (sockaddr == NULL) { - goto failed; - } - - ngx_memcpy(sockaddr, ctx->addrs[i].sockaddr, socklen); - ngx_inet_set_port(sockaddr, http->port); - - http->addrs[i].sockaddr = sockaddr; - http->addrs[i].socklen = socklen; - - p = ngx_pnalloc(http->pool, NGX_SOCKADDR_STRLEN); - if (p == NULL) { - goto failed; - } - - len = ngx_sock_ntop(sockaddr, socklen, p, NGX_SOCKADDR_STRLEN, 1); - http->addrs[i].name.len = len; - http->addrs[i].name.data = p; - } - - ngx_js_http_resolve_done(http); - - ngx_js_http_connect(http); - - return; - -failed: - - ngx_js_http_error(http, "memory error"); -} - - -static void -ngx_js_http_close_connection(ngx_connection_t *c) +ngx_js_fetch_error(ngx_js_http_t *http, const char *err) { - ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, - "js fetch close connection: %d", c->fd); - -#if (NGX_SSL) - if (c->ssl) { - c->ssl->no_wait_shutdown = 1; + ngx_js_fetch_t *fetch; - if (ngx_ssl_shutdown(c) == NGX_AGAIN) { - c->ssl->handler = ngx_js_http_close_connection; - return; - } - } -#endif - - c->destroyed = 1; + fetch = (ngx_js_fetch_t *) http; - ngx_close_connection(c); -} - - -static void -ngx_js_http_resolve_done(ngx_js_http_t *http) -{ - if (http->ctx != NULL) { - ngx_resolve_name_done(http->ctx); - http->ctx = NULL; - } -} + njs_vm_error(fetch->vm, err); + njs_vm_exception_get(fetch->vm, njs_value_arg(&fetch->response_value)); -static void -ngx_js_http_close_peer(ngx_js_http_t *http) -{ - if (http->peer.connection != NULL) { - ngx_js_http_close_connection(http->peer.connection); - http->peer.connection = NULL; - } + ngx_js_fetch_done(fetch, &fetch->response_value, NJS_ERROR); } static void -ngx_js_http_destructor(ngx_js_event_t *event) +ngx_js_fetch_destructor(ngx_js_event_t *event) { - ngx_js_http_t *http; + ngx_js_http_t *http; + ngx_js_fetch_t *fetch; - http = event->data; + fetch = event->data; + http = &fetch->http; ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, "js fetch destructor:%p", - http); + fetch); ngx_js_http_resolve_done(http); ngx_js_http_close_peer(http); @@ -1552,26 +1253,29 @@ error: static void -ngx_js_http_fetch_done(ngx_js_http_t *http, njs_opaque_value_t *retval, +ngx_js_fetch_done(ngx_js_fetch_t *fetch, njs_opaque_value_t *retval, njs_int_t rc) { njs_vm_t *vm; ngx_js_ctx_t *ctx; + ngx_js_http_t *http; ngx_js_event_t *event; njs_opaque_value_t arguments[2], *action; + http = &fetch->http; + ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0, - "js fetch done http:%p rc:%i", http, (ngx_int_t) rc); + "js fetch done fetch:%p rc:%i", fetch, (ngx_int_t) rc); ngx_js_http_close_peer(http); - if (http->event != NULL) { - action = &http->promise_callbacks[(rc != NJS_OK)]; + if (fetch->event != NULL) { + action = &fetch->promise_callbacks[(rc != NJS_OK)]; njs_value_assign(&arguments[0], action); njs_value_assign(&arguments[1], retval); - vm = http->vm; - event = http->event; + vm = fetch->vm; + event = fetch->event; rc = ngx_js_call(vm, njs_value_function(njs_value_arg(&event->function)), &arguments[0], 2); @@ -1600,1627 +1304,347 @@ ngx_js_http_promise_trampoline(njs_vm_t *vm, njs_value_t *args, } -static void -ngx_js_http_connect(ngx_js_http_t *http) +static njs_int_t +ngx_js_request_constructor(njs_vm_t *vm, ngx_js_request_t *request, + ngx_url_t *u, njs_external_ptr_t external, njs_value_t *args, + njs_uint_t nargs) { - ngx_int_t rc; - ngx_addr_t *addr; - - addr = &http->addrs[http->naddr]; - - ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0, - "js fetch connect %ui/%ui", http->naddr, http->naddrs); - - http->peer.sockaddr = addr->sockaddr; - http->peer.socklen = addr->socklen; - http->peer.name = &addr->name; - http->peer.get = ngx_event_get_peer; - http->peer.log = http->log; - http->peer.log_error = NGX_ERROR_ERR; - - rc = ngx_event_connect_peer(&http->peer); + njs_int_t ret; + ngx_uint_t rc; + ngx_pool_t *pool; + njs_value_t *input, *init, *value, *headers; + ngx_js_request_t *orig; + njs_opaque_value_t lvalue; - if (rc == NGX_ERROR) { - ngx_js_http_error(http, "connect failed"); - return; - } + static const njs_str_t body_key = njs_str("body"); + static const njs_str_t cache_key = njs_str("cache"); + static const njs_str_t cred_key = njs_str("credentials"); + static const njs_str_t headers_key = njs_str("headers"); + static const njs_str_t mode_key = njs_str("mode"); + static const njs_str_t method_key = njs_str("method"); - if (rc == NGX_BUSY || rc == NGX_DECLINED) { - ngx_js_http_next(http); - return; + input = njs_arg(args, nargs, 1); + if (njs_value_is_undefined(input)) { + njs_vm_error(vm, "1st argument is required"); + return NJS_ERROR; } - http->peer.connection->data = http; - http->peer.connection->pool = http->pool; + /* + * set by ngx_memzero(): + * + * request->url.length = 0; + * request->body.length = 0; + * request->cache_mode = CACHE_MODE_DEFAULT; + * request->credentials = CREDENTIALS_SAME_ORIGIN; + * request->mode = MODE_NO_CORS; + * request->headers.content_type = NULL; + */ - http->peer.connection->write->handler = ngx_js_http_write_handler; - http->peer.connection->read->handler = ngx_js_http_read_handler; + ngx_memzero(request, sizeof(ngx_js_request_t)); - http->process = ngx_js_http_process_status_line; + request->method = njs_str_value("GET"); + request->body = njs_str_value(""); + request->headers.guard = GUARD_REQUEST; - ngx_add_timer(http->peer.connection->read, http->timeout); - ngx_add_timer(http->peer.connection->write, http->timeout); + pool = ngx_external_pool(vm, external); -#if (NGX_SSL) - if (http->ssl != NULL && http->peer.connection->ssl == NULL) { - ngx_js_http_ssl_init_connection(http); - return; + rc = ngx_list_init(&request->headers.header_list, pool, 4, + sizeof(ngx_js_tb_elt_t)); + if (rc != NGX_OK) { + njs_vm_memory_error(vm); + return NJS_ERROR; } -#endif - if (rc == NGX_OK) { - ngx_js_http_write_handler(http->peer.connection->write); - } -} + if (njs_value_is_string(input)) { + ret = ngx_js_string(vm, input, &request->url); + if (ret != NJS_OK) { + njs_vm_error(vm, "failed to convert url arg"); + return NJS_ERROR; + } + } else { + orig = njs_vm_external(vm, ngx_http_js_fetch_request_proto_id, input); + if (orig == NULL) { + njs_vm_error(vm, "input is not string or a Request object"); + return NJS_ERROR; + } -#if (NGX_SSL) + request->url = orig->url; + request->method = orig->method; + request->body = orig->body; + request->body_used = orig->body_used; + request->cache_mode = orig->cache_mode; + request->credentials = orig->credentials; + request->mode = orig->mode; -static void -ngx_js_http_ssl_init_connection(ngx_js_http_t *http) -{ - ngx_int_t rc; - ngx_connection_t *c; + ret = ngx_js_headers_inherit(vm, &request->headers, &orig->headers); + if (ret != NJS_OK) { + return NJS_ERROR; + } + } + + ngx_js_http_trim(&request->url.start, &request->url.length, 1); - c = http->peer.connection; + ngx_memzero(u, sizeof(ngx_url_t)); - ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0, - "js fetch secure connect %ui/%ui", http->naddr, - http->naddrs); + u->url.len = request->url.length; + u->url.data = request->url.start; + u->default_port = 80; + u->uri_part = 1; + u->no_resolve = 1; - if (ngx_ssl_create_connection(http->ssl, c, NGX_SSL_BUFFER|NGX_SSL_CLIENT) - != NGX_OK) + if (u->url.len > 7 + && njs_strncasecmp(u->url.data, (u_char *) "http://", 7) == 0) { - ngx_js_http_error(http, "failed to create ssl connection"); - return; - } + u->url.len -= 7; + u->url.data += 7; - c->sendfile = 0; +#if (NGX_SSL) + } else if (u->url.len > 8 + && njs_strncasecmp(u->url.data, (u_char *) "https://", 8) == 0) + { + u->url.len -= 8; + u->url.data += 8; + u->default_port = 443; +#endif - if (ngx_js_http_ssl_name(http) != NGX_OK) { - ngx_js_http_error(http, "failed to create ssl connection"); - return; + } else { + njs_vm_error(vm, "unsupported URL schema (only http or https are" + " supported)"); + return NJS_ERROR; } - c->log->action = "SSL handshaking to fetch target"; - - rc = ngx_ssl_handshake(c); - - if (rc == NGX_AGAIN) { - c->data = http; - c->ssl->handler = ngx_js_http_ssl_handshake_handler; - return; + if (ngx_parse_url(pool, u) != NGX_OK) { + njs_vm_error(vm, "invalid url"); + return NJS_ERROR; } - ngx_js_http_ssl_handshake(http); -} - - -static void -ngx_js_http_ssl_handshake_handler(ngx_connection_t *c) -{ - ngx_js_http_t *http; + init = njs_arg(args, nargs, 2); - http = c->data; + if (njs_value_is_object(init)) { + value = njs_vm_object_prop(vm, init, &method_key, &lvalue); + if (value != NULL && ngx_js_string(vm, value, &request->method) + != NGX_OK) + { + njs_vm_error(vm, "invalid Request method"); + return NJS_ERROR; + } - http->peer.connection->write->handler = ngx_js_http_write_handler; - http->peer.connection->read->handler = ngx_js_http_read_handler; + ret = ngx_js_method_process(vm, request); + if (ret != NJS_OK) { + return NJS_ERROR; + } - ngx_js_http_ssl_handshake(http); -} - - -static void -ngx_js_http_ssl_handshake(ngx_js_http_t *http) -{ - long rc; - ngx_connection_t *c; - - c = http->peer.connection; - - if (c->ssl->handshaked) { - if (http->ssl_verify) { - rc = SSL_get_verify_result(c->ssl->connection); - - if (rc != X509_V_OK) { - ngx_log_error(NGX_LOG_ERR, c->log, 0, - "js fetch SSL certificate verify error: (%l:%s)", - rc, X509_verify_cert_error_string(rc)); - goto failed; + value = njs_vm_object_prop(vm, init, &cache_key, &lvalue); + if (value != NULL) { + ret = ngx_fetch_flag_set(vm, ngx_js_fetch_cache_modes, value, + "cache"); + if (ret == NJS_ERROR) { + return NJS_ERROR; } - if (ngx_ssl_check_host(c, &http->tls_name) != NGX_OK) { - ngx_log_error(NGX_LOG_ERR, c->log, 0, - "js fetch SSL certificate does not match \"%V\"", - &http->tls_name); - goto failed; - } + request->cache_mode = ret; } - c->write->handler = ngx_js_http_write_handler; - c->read->handler = ngx_js_http_read_handler; + value = njs_vm_object_prop(vm, init, &cred_key, &lvalue); + if (value != NULL) { + ret = ngx_fetch_flag_set(vm, ngx_js_fetch_credentials, value, + "credentials"); + if (ret == NJS_ERROR) { + return NJS_ERROR; + } - if (c->read->ready) { - ngx_post_event(c->read, &ngx_posted_events); + request->credentials = ret; } - http->process = ngx_js_http_process_status_line; - ngx_js_http_write_handler(c->write); - - return; - } - -failed: - - ngx_js_http_next(http); - } - - -static njs_int_t -ngx_js_http_ssl_name(ngx_js_http_t *http) -{ -#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME - u_char *p; - - /* as per RFC 6066, literal IPv4 and IPv6 addresses are not permitted */ - ngx_str_t *name = &http->tls_name; + value = njs_vm_object_prop(vm, init, &mode_key, &lvalue); + if (value != NULL) { + ret = ngx_fetch_flag_set(vm, ngx_js_fetch_modes, value, "mode"); + if (ret == NJS_ERROR) { + return NJS_ERROR; + } - if (name->len == 0 || *name->data == '[') { - goto done; - } + request->mode = ret; + } - if (ngx_inet_addr(name->data, name->len) != INADDR_NONE) { - goto done; - } + headers = njs_vm_object_prop(vm, init, &headers_key, &lvalue); + if (headers != NULL) { + if (!njs_value_is_object(headers)) { + njs_vm_error(vm, "Headers is not an object"); + return NJS_ERROR; + } - /* - * SSL_set_tlsext_host_name() needs a null-terminated string, - * hence we explicitly null-terminate name here - */ + /* + * There are no API to reset or destroy ngx_list, + * just allocating a new one. + */ - p = ngx_pnalloc(http->pool, name->len + 1); - if (p == NULL) { - return NGX_ERROR; - } + ngx_memset(&request->headers, 0, sizeof(ngx_js_headers_t)); + request->headers.guard = GUARD_REQUEST; - (void) ngx_cpystrn(p, name->data, name->len + 1); + rc = ngx_list_init(&request->headers.header_list, pool, 4, + sizeof(ngx_js_tb_elt_t)); + if (rc != NGX_OK) { + njs_vm_memory_error(vm); + return NJS_ERROR; + } - name->data = p; + ret = ngx_js_headers_fill(vm, &request->headers, headers); + if (ret != NJS_OK) { + return NJS_ERROR; + } + } - ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, - "js fetch SSL server name: \"%s\"", name->data); + value = njs_vm_object_prop(vm, init, &body_key, &lvalue); + if (value != NULL) { + if (ngx_js_string(vm, value, &request->body) != NGX_OK) { + njs_vm_error(vm, "invalid Request body"); + return NJS_ERROR; + } - if (SSL_set_tlsext_host_name(http->peer.connection->ssl->connection, - (char *) name->data) - == 0) - { - ngx_ssl_error(NGX_LOG_ERR, http->log, 0, - "SSL_set_tlsext_host_name(\"%s\") failed", name->data); - return NGX_ERROR; + if (request->headers.content_type == NULL + && njs_value_is_string(value)) + { + ret = ngx_js_headers_append(vm, &request->headers, + (u_char *) "Content-Type", + njs_length("Content-Type"), + (u_char *) "text/plain;charset=UTF-8", + njs_length("text/plain;charset=UTF-8")); + if (ret != NJS_OK) { + return NJS_ERROR; + } + } + } } -#endif -done: - return NJS_OK; } -#endif - -static void -ngx_js_http_next(ngx_js_http_t *http) +static ngx_int_t +ngx_js_fetch_append_headers(ngx_js_http_t *http, ngx_js_headers_t *headers, + u_char *name, size_t len, u_char *value, size_t vlen) { - ngx_log_debug0(NGX_LOG_DEBUG_EVENT, http->log, 0, "js fetch next addr"); - - if (++http->naddr >= http->naddrs) { - ngx_js_http_error(http, "connect failed"); - return; - } - - if (http->peer.connection != NULL) { - ngx_js_http_close_connection(http->peer.connection); - http->peer.connection = NULL; - } + ngx_js_fetch_t *fetch; - http->buffer = NULL; + fetch = (ngx_js_fetch_t *) http; - ngx_js_http_connect(http); + return ngx_js_headers_append(fetch->vm, headers, name, len, value, vlen); } static void -ngx_js_http_write_handler(ngx_event_t *wev) +ngx_js_fetch_process_done(ngx_js_http_t *http) { - ssize_t n, size; - ngx_buf_t *b; - ngx_js_http_t *http; - ngx_connection_t *c; - - c = wev->data; - http = c->data; - - ngx_log_debug0(NGX_LOG_DEBUG_EVENT, wev->log, 0, "js fetch write handler"); - - if (wev->timedout) { - ngx_js_http_error(http, "write timed out"); - return; - } - -#if (NGX_SSL) - if (http->ssl != NULL && http->peer.connection->ssl == NULL) { - ngx_js_http_ssl_init_connection(http); - return; - } -#endif - - b = http->buffer; - - if (b == NULL) { - size = njs_chb_size(&http->chain); - if (size < 0) { - ngx_js_http_error(http, "memory error"); - return; - } - - b = ngx_create_temp_buf(http->pool, size); - if (b == NULL) { - ngx_js_http_error(http, "memory error"); - return; - } + njs_int_t ret; + ngx_js_fetch_t *fetch; - njs_chb_join_to(&http->chain, b->last); - b->last += size; + fetch = (ngx_js_fetch_t *) http; - http->buffer = b; - } - - size = b->last - b->pos; - - n = c->send(c, b->pos, size); - - if (n == NGX_ERROR) { - ngx_js_http_next(http); + ret = njs_vm_external_create(fetch->vm, + njs_value_arg(&fetch->response_value), + ngx_http_js_fetch_response_proto_id, + &fetch->http.response, 0); + if (ret != NJS_OK) { + ngx_js_fetch_error(http, "fetch response creation failed"); return; } - if (n > 0) { - b->pos += n; - - if (n == size) { - wev->handler = ngx_js_http_dummy_handler; - - http->buffer = NULL; - - if (wev->timer_set) { - ngx_del_timer(wev); - } - - if (ngx_handle_write_event(wev, 0) != NGX_OK) { - ngx_js_http_error(http, "write failed"); - } - - return; - } - } - - if (!wev->timer_set) { - ngx_add_timer(wev, http->timeout); - } + ngx_js_fetch_done(fetch, &fetch->response_value, NJS_OK); } -static void -ngx_js_http_read_handler(ngx_event_t *rev) +static njs_int_t +ngx_js_headers_append(njs_vm_t *vm, ngx_js_headers_t *headers, + u_char *name, size_t len, u_char *value, size_t vlen) { - ssize_t n, size; - ngx_int_t rc; - ngx_buf_t *b; - ngx_js_http_t *http; - ngx_connection_t *c; - - c = rev->data; - http = c->data; + u_char *p, *end; + ngx_int_t ret; + ngx_uint_t i; + ngx_js_tb_elt_t *h, **ph; + ngx_list_part_t *part; - ngx_log_debug0(NGX_LOG_DEBUG_EVENT, rev->log, 0, "js fetch read handler"); + ngx_js_http_trim(&value, &vlen, 0); - if (rev->timedout) { - ngx_js_http_error(http, "read timed out"); - return; + ret = ngx_js_check_header_name(name, len); + if (ret != NGX_OK) { + njs_vm_error(vm, "invalid header name"); + return NJS_ERROR; } - if (http->buffer == NULL) { - b = ngx_create_temp_buf(http->pool, http->buffer_size); - if (b == NULL) { - ngx_js_http_error(http, "memory error"); - return; + p = value; + end = p + vlen; + + while (p < end) { + if (*p == '\0') { + njs_vm_error(vm, "invalid header value"); + return NJS_ERROR; } - http->buffer = b; + p++; } - for ( ;; ) { - b = http->buffer; - size = b->end - b->last; - - n = c->recv(c, b->last, size); + if (headers->guard == GUARD_IMMUTABLE) { + njs_vm_error(vm, "cannot append to immutable object"); + return NJS_ERROR; + } - if (n > 0) { - b->last += n; + ph = NULL; + part = &headers->header_list.part; + h = part->elts; - rc = http->process(http); + for (i = 0; /* void */; i++) { - if (rc == NGX_ERROR) { - return; + if (i >= part->nelts) { + if (part->next == NULL) { + break; } - continue; + part = part->next; + h = part->elts; + i = 0; } - if (n == NGX_AGAIN) { - if (ngx_handle_read_event(rev, 0) != NGX_OK) { - ngx_js_http_error(http, "read failed"); - } - - return; + if (h[i].hash == 0) { + continue; } - if (n == NGX_ERROR) { - ngx_js_http_next(http); - return; + if (len == h[i].key.len + && (njs_strncasecmp(name, h[i].key.data, len) == 0)) + { + ph = &h[i].next; + while (*ph) { ph = &(*ph)->next; } + break; } - - break; - } - - http->done = 1; - - rc = http->process(http); - - if (rc == NGX_DONE) { - /* handler was called */ - return; } - if (rc == NGX_AGAIN) { - ngx_js_http_error(http, "prematurely closed connection"); - } -} - - -static njs_int_t -ngx_js_request_constructor(njs_vm_t *vm, ngx_js_request_t *request, - ngx_url_t *u, njs_external_ptr_t external, njs_value_t *args, - njs_uint_t nargs) -{ - njs_int_t ret; - ngx_uint_t rc; - ngx_pool_t *pool; - njs_value_t *input, *init, *value, *headers; - ngx_js_request_t *orig; - njs_opaque_value_t lvalue; - - static const njs_str_t body_key = njs_str("body"); - static const njs_str_t cache_key = njs_str("cache"); - static const njs_str_t cred_key = njs_str("credentials"); - static const njs_str_t headers_key = njs_str("headers"); - static const njs_str_t mode_key = njs_str("mode"); - static const njs_str_t method_key = njs_str("method"); - - input = njs_arg(args, nargs, 1); - if (njs_value_is_undefined(input)) { - njs_vm_error(vm, "1st argument is required"); + h = ngx_list_push(&headers->header_list); + if (h == NULL) { + njs_vm_memory_error(vm); return NJS_ERROR; } - /* - * set by ngx_memzero(): - * - * request->url.length = 0; - * request->body.length = 0; - * request->cache_mode = CACHE_MODE_DEFAULT; - * request->credentials = CREDENTIALS_SAME_ORIGIN; - * request->mode = MODE_NO_CORS; - * request->headers.content_type = NULL; - */ - - ngx_memzero(request, sizeof(ngx_js_request_t)); - - request->method = njs_str_value("GET"); - request->body = njs_str_value(""); - request->headers.guard = GUARD_REQUEST; + if (ph != NULL) { + *ph = h; + } - pool = ngx_external_pool(vm, external); + h->hash = 1; + h->key.data = name; + h->key.len = len; + h->value.data = value; + h->value.len = vlen; + h->next = NULL; - rc = ngx_list_init(&request->headers.header_list, pool, 4, - sizeof(ngx_js_tb_elt_t)); - if (rc != NGX_OK) { - njs_vm_memory_error(vm); - return NJS_ERROR; - } - - if (njs_value_is_string(input)) { - ret = ngx_js_string(vm, input, &request->url); - if (ret != NJS_OK) { - njs_vm_error(vm, "failed to convert url arg"); - return NJS_ERROR; - } - - } else { - orig = njs_vm_external(vm, ngx_http_js_fetch_request_proto_id, input); - if (orig == NULL) { - njs_vm_error(vm, "input is not string or a Request object"); - return NJS_ERROR; - } - - request->url = orig->url; - request->method = orig->method; - request->body = orig->body; - request->body_used = orig->body_used; - request->cache_mode = orig->cache_mode; - request->credentials = orig->credentials; - request->mode = orig->mode; - - ret = ngx_js_headers_inherit(vm, &request->headers, &orig->headers); - if (ret != NJS_OK) { - return NJS_ERROR; - } - } - - ngx_js_http_trim(&request->url.start, &request->url.length, 1); - - ngx_memzero(u, sizeof(ngx_url_t)); - - u->url.len = request->url.length; - u->url.data = request->url.start; - u->default_port = 80; - u->uri_part = 1; - u->no_resolve = 1; - - if (u->url.len > 7 - && njs_strncasecmp(u->url.data, (u_char *) "http://", 7) == 0) - { - u->url.len -= 7; - u->url.data += 7; - -#if (NGX_SSL) - } else if (u->url.len > 8 - && njs_strncasecmp(u->url.data, (u_char *) "https://", 8) == 0) - { - u->url.len -= 8; - u->url.data += 8; - u->default_port = 443; -#endif - - } else { - njs_vm_error(vm, "unsupported URL schema (only http or https are" - " supported)"); - return NJS_ERROR; - } - - if (ngx_parse_url(pool, u) != NGX_OK) { - njs_vm_error(vm, "invalid url"); - return NJS_ERROR; - } - - init = njs_arg(args, nargs, 2); - - if (njs_value_is_object(init)) { - value = njs_vm_object_prop(vm, init, &method_key, &lvalue); - if (value != NULL && ngx_js_string(vm, value, &request->method) - != NGX_OK) - { - njs_vm_error(vm, "invalid Request method"); - return NJS_ERROR; - } - - ret = ngx_js_method_process(vm, request); - if (ret != NJS_OK) { - return NJS_ERROR; - } - - value = njs_vm_object_prop(vm, init, &cache_key, &lvalue); - if (value != NULL) { - ret = ngx_fetch_flag_set(vm, ngx_js_fetch_cache_modes, value, - "cache"); - if (ret == NJS_ERROR) { - return NJS_ERROR; - } - - request->cache_mode = ret; - } - - value = njs_vm_object_prop(vm, init, &cred_key, &lvalue); - if (value != NULL) { - ret = ngx_fetch_flag_set(vm, ngx_js_fetch_credentials, value, - "credentials"); - if (ret == NJS_ERROR) { - return NJS_ERROR; - } - - request->credentials = ret; - } - - value = njs_vm_object_prop(vm, init, &mode_key, &lvalue); - if (value != NULL) { - ret = ngx_fetch_flag_set(vm, ngx_js_fetch_modes, value, "mode"); - if (ret == NJS_ERROR) { - return NJS_ERROR; - } - - request->mode = ret; - } - - headers = njs_vm_object_prop(vm, init, &headers_key, &lvalue); - if (headers != NULL) { - if (!njs_value_is_object(headers)) { - njs_vm_error(vm, "Headers is not an object"); - return NJS_ERROR; - } - - /* - * There are no API to reset or destroy ngx_list, - * just allocating a new one. - */ - - ngx_memset(&request->headers, 0, sizeof(ngx_js_headers_t)); - request->headers.guard = GUARD_REQUEST; - - rc = ngx_list_init(&request->headers.header_list, pool, 4, - sizeof(ngx_js_tb_elt_t)); - if (rc != NGX_OK) { - njs_vm_memory_error(vm); - return NJS_ERROR; - } - - ret = ngx_js_headers_fill(vm, &request->headers, headers); - if (ret != NJS_OK) { - return NJS_ERROR; - } - } - - value = njs_vm_object_prop(vm, init, &body_key, &lvalue); - if (value != NULL) { - if (ngx_js_string(vm, value, &request->body) != NGX_OK) { - njs_vm_error(vm, "invalid Request body"); - return NJS_ERROR; - } - - if (request->headers.content_type == NULL - && njs_value_is_string(value)) - { - ret = ngx_js_headers_append(vm, &request->headers, - (u_char *) "Content-Type", - njs_length("Content-Type"), - (u_char *) "text/plain;charset=UTF-8", - njs_length("text/plain;charset=UTF-8")); - if (ret != NJS_OK) { - return NJS_ERROR; - } - } - } + if (len == njs_strlen("Content-Type") + && ngx_strncasecmp(name, (u_char *) "Content-Type", len) == 0) + { + headers->content_type = h; } return NJS_OK; } -njs_inline njs_int_t -ngx_js_http_whitespace(u_char c) -{ - switch (c) { - case 0x09: /* */ - case 0x0A: /* */ - case 0x0D: /* */ - case 0x20: /* */ - return 1; - - default: - return 0; - } -} - - -static void -ngx_js_http_trim(u_char **value, size_t *len, - njs_bool_t trim_c0_control_or_space) -{ - u_char *start, *end; - - start = *value; - end = start + *len; - - for ( ;; ) { - if (start == end) { - break; - } - - if (ngx_js_http_whitespace(*start) - || (trim_c0_control_or_space && *start <= ' ')) - { - start++; - continue; - } - - break; - } - - for ( ;; ) { - if (start == end) { - break; - } - - end--; - - if (ngx_js_http_whitespace(*end) - || (trim_c0_control_or_space && *end <= ' ')) - { - continue; - } - - end++; - break; - } - - *value = start; - *len = end - start; -} - - -static const uint32_t token_map[] = { - 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ - - /* ?>=< ;:98 7654 3210 /.-, +*)( '&%$ #"! */ - 0x03ff6cfa, /* 0000 0011 1111 1111 0110 1100 1111 1010 */ - - /* _^]\ [ZYX WVUT SRQP ONML KJIH GFED CBA@ */ - 0xc7fffffe, /* 1100 0111 1111 1111 1111 1111 1111 1110 */ - - /* ~}| {zyx wvut srqp onml kjih gfed cba` */ - 0x57ffffff, /* 0101 0111 1111 1111 1111 1111 1111 1111 */ - - 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ - 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ - 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ - 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ -}; - - -njs_inline njs_bool_t -njs_is_token(uint32_t byte) -{ - return ((token_map[byte >> 5] & ((uint32_t) 1 << (byte & 0x1f))) != 0); -} - - -static njs_int_t -ngx_js_headers_append(njs_vm_t *vm, ngx_js_headers_t *headers, - u_char *name, size_t len, u_char *value, size_t vlen) -{ - u_char *p, *end; - ngx_uint_t i; - ngx_js_tb_elt_t *h, **ph; - ngx_list_part_t *part; - - ngx_js_http_trim(&value, &vlen, 0); - - p = name; - end = p + len; - - while (p < end) { - if (!njs_is_token(*p)) { - njs_vm_error(vm, "invalid header name"); - return NJS_ERROR; - } - - p++; - } - - p = value; - end = p + vlen; - - while (p < end) { - if (*p == '\0') { - njs_vm_error(vm, "invalid header value"); - return NJS_ERROR; - } - - p++; - } - - if (headers->guard == GUARD_IMMUTABLE) { - njs_vm_error(vm, "cannot append to immutable object"); - return NJS_ERROR; - } - - ph = NULL; - part = &headers->header_list.part; - h = part->elts; - - for (i = 0; /* void */; i++) { - - if (i >= part->nelts) { - if (part->next == NULL) { - break; - } - - part = part->next; - h = part->elts; - i = 0; - } - - if (h[i].hash == 0) { - continue; - } - - if (len == h[i].key.len - && (njs_strncasecmp(name, h[i].key.data, len) == 0)) - { - ph = &h[i].next; - while (*ph) { ph = &(*ph)->next; } - break; - } - } - - h = ngx_list_push(&headers->header_list); - if (h == NULL) { - njs_vm_memory_error(vm); - return NJS_ERROR; - } - - if (ph != NULL) { - *ph = h; - } - - h->hash = 1; - h->key.data = name; - h->key.len = len; - h->value.data = value; - h->value.len = vlen; - h->next = NULL; - - if (len == njs_strlen("Content-Type") - && ngx_strncasecmp(name, (u_char *) "Content-Type", len) == 0) - { - headers->content_type = h; - } - - return NJS_OK; -} - - -static ngx_int_t -ngx_js_http_process_status_line(ngx_js_http_t *http) -{ - ngx_int_t rc; - ngx_js_http_parse_t *hp; - - hp = &http->http_parse; - - rc = ngx_js_http_parse_status_line(hp, http->buffer); - - if (rc == NGX_OK) { - ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, "js fetch status %ui", - hp->code); - - http->response.code = hp->code; - http->response.status_text.start = hp->status_text; - http->response.status_text.length = hp->status_text_end - - hp->status_text; - http->process = ngx_js_http_process_headers; - - return http->process(http); - } - - if (rc == NGX_AGAIN) { - return NGX_AGAIN; - } - - /* rc == NGX_ERROR */ - - ngx_js_http_error(http, "invalid fetch status line"); - - return NGX_ERROR; -} - - -static ngx_int_t -ngx_js_http_process_headers(ngx_js_http_t *http) -{ - size_t len, vlen; - ngx_int_t rc; - njs_int_t ret; - ngx_js_http_parse_t *hp; - - ngx_log_debug0(NGX_LOG_DEBUG_EVENT, http->log, 0, - "js fetch process headers"); - - hp = &http->http_parse; - - if (http->response.headers.header_list.size == 0) { - rc = ngx_list_init(&http->response.headers.header_list, http->pool, 4, - sizeof(ngx_js_tb_elt_t)); - if (rc != NGX_OK) { - ngx_js_http_error(http, "alloc failed"); - return NGX_ERROR; - } - } - - for ( ;; ) { - rc = ngx_js_http_parse_header_line(hp, http->buffer); - - if (rc == NGX_OK) { - len = hp->header_name_end - hp->header_name_start; - vlen = hp->header_end - hp->header_start; - - ret = ngx_js_headers_append(http->vm, &http->response.headers, - hp->header_name_start, len, - hp->header_start, vlen); - - if (ret == NJS_ERROR) { - ngx_js_http_error(http, "cannot add respose header"); - return NGX_ERROR; - } - - ngx_log_debug4(NGX_LOG_DEBUG_EVENT, http->log, 0, - "js fetch header \"%*s: %*s\"", - len, hp->header_name_start, vlen, hp->header_start); - - if (len == njs_strlen("Transfer-Encoding") - && vlen == njs_strlen("chunked") - && ngx_strncasecmp(hp->header_name_start, - (u_char *) "Transfer-Encoding", len) == 0 - && ngx_strncasecmp(hp->header_start, (u_char *) "chunked", - vlen) == 0) - { - hp->chunked = 1; - } - - if (len == njs_strlen("Content-Length") - && ngx_strncasecmp(hp->header_name_start, - (u_char *) "Content-Length", len) == 0) - { - hp->content_length_n = ngx_atoof(hp->header_start, vlen); - if (hp->content_length_n == NGX_ERROR) { - ngx_js_http_error(http, "invalid fetch content length"); - return NGX_ERROR; - } - - if (!http->header_only - && hp->content_length_n - > (off_t) http->max_response_body_size) - { - ngx_js_http_error(http, - "fetch content length is too large"); - return NGX_ERROR; - } - } - - continue; - } - - if (rc == NGX_DONE) { - http->response.headers.guard = GUARD_IMMUTABLE; - break; - } - - if (rc == NGX_AGAIN) { - return NGX_AGAIN; - } - - /* rc == NGX_ERROR */ - - ngx_js_http_error(http, "invalid fetch header"); - - return NGX_ERROR; - } - - njs_chb_destroy(&http->chain); - - NJS_CHB_MP_INIT(&http->response.chain, njs_vm_memory_pool(http->vm)); - - http->process = ngx_js_http_process_body; - - return http->process(http); -} - - -static ngx_int_t -ngx_js_http_process_body(ngx_js_http_t *http) -{ - ssize_t size, chsize, need; - ngx_int_t rc; - njs_int_t ret; - ngx_buf_t *b; - - ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, - "js fetch process body done:%ui", (ngx_uint_t) http->done); - - if (http->done) { - size = njs_chb_size(&http->response.chain); - if (size < 0) { - ngx_js_http_error(http, "memory error"); - return NGX_ERROR; - } - - if (!http->header_only - && http->http_parse.chunked - && http->http_parse.content_length_n == -1) - { - ngx_js_http_error(http, "invalid fetch chunked response"); - return NGX_ERROR; - } - - if (http->header_only - || http->http_parse.content_length_n == -1 - || size == http->http_parse.content_length_n) - { - ret = njs_vm_external_create(http->vm, - njs_value_arg(&http->response_value), - ngx_http_js_fetch_response_proto_id, - &http->response, 0); - if (ret != NJS_OK) { - ngx_js_http_error(http, "fetch response creation failed"); - return NGX_ERROR; - } - - ngx_js_http_fetch_done(http, &http->response_value, NJS_OK); - return NGX_DONE; - } - - if (size < http->http_parse.content_length_n) { - return NGX_AGAIN; - } - - ngx_js_http_error(http, "fetch trailing data"); - return NGX_ERROR; - } - - b = http->buffer; - - if (http->http_parse.chunked) { - rc = ngx_js_http_parse_chunked(&http->http_chunk_parse, b, - &http->response.chain); - if (rc == NGX_ERROR) { - ngx_js_http_error(http, "invalid fetch chunked response"); - return NGX_ERROR; - } - - size = njs_chb_size(&http->response.chain); - - if (rc == NGX_OK) { - http->http_parse.content_length_n = size; - } - - if (size > http->max_response_body_size * 10) { - ngx_js_http_error(http, "very large fetch chunked response"); - return NGX_ERROR; - } - - b->pos = http->http_chunk_parse.pos; - - } else { - size = njs_chb_size(&http->response.chain); - - if (http->header_only) { - need = 0; - - } else if (http->http_parse.content_length_n == -1) { - need = http->max_response_body_size - size; - - } else { - need = http->http_parse.content_length_n - size; - } - - chsize = ngx_min(need, b->last - b->pos); - - if (size + chsize > http->max_response_body_size) { - ngx_js_http_error(http, "fetch response body is too large"); - return NGX_ERROR; - } - - if (chsize > 0) { - njs_chb_append(&http->response.chain, b->pos, chsize); - b->pos += chsize; - } - - rc = (need > chsize) ? NGX_AGAIN : NGX_DONE; - } - - if (b->pos == b->end) { - if (http->chunk == NULL) { - b = ngx_create_temp_buf(http->pool, http->buffer_size); - if (b == NULL) { - ngx_js_http_error(http, "memory error"); - return NGX_ERROR; - } - - http->buffer = b; - http->chunk = b; - - } else { - b->last = b->start; - b->pos = b->start; - } - } - - return rc; -} - - -static ngx_int_t -ngx_js_http_parse_status_line(ngx_js_http_parse_t *hp, ngx_buf_t *b) -{ - u_char ch; - u_char *p; - enum { - sw_start = 0, - sw_H, - sw_HT, - sw_HTT, - sw_HTTP, - sw_first_major_digit, - sw_major_digit, - sw_first_minor_digit, - sw_minor_digit, - sw_status, - sw_space_after_status, - sw_status_text, - sw_almost_done - } state; - - state = hp->state; - - for (p = b->pos; p < b->last; p++) { - ch = *p; - - switch (state) { - - /* "HTTP/" */ - case sw_start: - switch (ch) { - case 'H': - state = sw_H; - break; - default: - return NGX_ERROR; - } - break; - - case sw_H: - switch (ch) { - case 'T': - state = sw_HT; - break; - default: - return NGX_ERROR; - } - break; - - case sw_HT: - switch (ch) { - case 'T': - state = sw_HTT; - break; - default: - return NGX_ERROR; - } - break; - - case sw_HTT: - switch (ch) { - case 'P': - state = sw_HTTP; - break; - default: - return NGX_ERROR; - } - break; - - case sw_HTTP: - switch (ch) { - case '/': - state = sw_first_major_digit; - break; - default: - return NGX_ERROR; - } - break; - - /* the first digit of major HTTP version */ - case sw_first_major_digit: - if (ch < '1' || ch > '9') { - return NGX_ERROR; - } - - state = sw_major_digit; - break; - - /* the major HTTP version or dot */ - case sw_major_digit: - if (ch == '.') { - state = sw_first_minor_digit; - break; - } - - if (ch < '0' || ch > '9') { - return NGX_ERROR; - } - - break; - - /* the first digit of minor HTTP version */ - case sw_first_minor_digit: - if (ch < '0' || ch > '9') { - return NGX_ERROR; - } - - state = sw_minor_digit; - break; - - /* the minor HTTP version or the end of the request line */ - case sw_minor_digit: - if (ch == ' ') { - state = sw_status; - break; - } - - if (ch < '0' || ch > '9') { - return NGX_ERROR; - } - - break; - - /* HTTP status code */ - case sw_status: - if (ch == ' ') { - break; - } - - if (ch < '0' || ch > '9') { - return NGX_ERROR; - } - - hp->code = hp->code * 10 + (ch - '0'); - - if (++hp->count == 3) { - state = sw_space_after_status; - } - - break; - - /* space or end of line */ - case sw_space_after_status: - switch (ch) { - case ' ': - state = sw_status_text; - break; - case '.': /* IIS may send 403.1, 403.2, etc */ - state = sw_status_text; - break; - case CR: - break; - case LF: - goto done; - default: - return NGX_ERROR; - } - break; - - /* any text until end of line */ - case sw_status_text: - switch (ch) { - case CR: - hp->status_text_end = p; - state = sw_almost_done; - break; - case LF: - hp->status_text_end = p; - goto done; - } - - if (hp->status_text == NULL) { - hp->status_text = p; - } - - break; - - /* end of status line */ - case sw_almost_done: - switch (ch) { - case LF: - goto done; - default: - return NGX_ERROR; - } - } - } - - b->pos = p; - hp->state = state; - - return NGX_AGAIN; - -done: - - b->pos = p + 1; - hp->state = sw_start; - - return NGX_OK; -} - - -static ngx_int_t -ngx_js_http_parse_header_line(ngx_js_http_parse_t *hp, ngx_buf_t *b) -{ - u_char c, ch, *p; - enum { - sw_start = 0, - sw_name, - sw_space_before_value, - sw_value, - sw_space_after_value, - sw_almost_done, - sw_header_almost_done - } state; - - state = hp->state; - - for (p = b->pos; p < b->last; p++) { - ch = *p; - - switch (state) { - - /* first char */ - case sw_start: - - switch (ch) { - case CR: - hp->header_end = p; - state = sw_header_almost_done; - break; - case LF: - hp->header_end = p; - goto header_done; - default: - state = sw_name; - hp->header_name_start = p; - - c = (u_char) (ch | 0x20); - if (c >= 'a' && c <= 'z') { - break; - } - - if (ch >= '0' && ch <= '9') { - break; - } - - return NGX_ERROR; - } - break; - - /* header name */ - case sw_name: - c = (u_char) (ch | 0x20); - if (c >= 'a' && c <= 'z') { - break; - } - - if (ch == ':') { - hp->header_name_end = p; - state = sw_space_before_value; - break; - } - - if (ch == '-' || ch == '_') { - break; - } - - if (ch >= '0' && ch <= '9') { - break; - } - - if (ch == CR) { - hp->header_name_end = p; - hp->header_start = p; - hp->header_end = p; - state = sw_almost_done; - break; - } - - if (ch == LF) { - hp->header_name_end = p; - hp->header_start = p; - hp->header_end = p; - goto done; - } - - return NGX_ERROR; - - /* space* before header value */ - case sw_space_before_value: - switch (ch) { - case ' ': - break; - case CR: - hp->header_start = p; - hp->header_end = p; - state = sw_almost_done; - break; - case LF: - hp->header_start = p; - hp->header_end = p; - goto done; - default: - hp->header_start = p; - state = sw_value; - break; - } - break; - - /* header value */ - case sw_value: - switch (ch) { - case ' ': - hp->header_end = p; - state = sw_space_after_value; - break; - case CR: - hp->header_end = p; - state = sw_almost_done; - break; - case LF: - hp->header_end = p; - goto done; - } - break; - - /* space* before end of header line */ - case sw_space_after_value: - switch (ch) { - case ' ': - break; - case CR: - state = sw_almost_done; - break; - case LF: - goto done; - default: - state = sw_value; - break; - } - break; - - /* end of header line */ - case sw_almost_done: - switch (ch) { - case LF: - goto done; - default: - return NGX_ERROR; - } - - /* end of header */ - case sw_header_almost_done: - switch (ch) { - case LF: - goto header_done; - default: - return NGX_ERROR; - } - } - } - - b->pos = p; - hp->state = state; - - return NGX_AGAIN; - -done: - - b->pos = p + 1; - hp->state = sw_start; - - return NGX_OK; - -header_done: - - b->pos = p + 1; - hp->state = sw_start; - - return NGX_DONE; -} - - -#define \ -ngx_size_is_sufficient(cs) \ - (cs < ((__typeof__(cs)) 1 << (sizeof(cs) * 8 - 4))) - - -#define NGX_JS_HTTP_CHUNK_MIDDLE 0 -#define NGX_JS_HTTP_CHUNK_ON_BORDER 1 -#define NGX_JS_HTTP_CHUNK_END 2 - - -static ngx_int_t -ngx_js_http_chunk_buffer(ngx_js_http_chunk_parse_t *hcp, ngx_buf_t *b, - njs_chb_t *chain) -{ - size_t size; - - size = b->last - hcp->pos; - - if (hcp->chunk_size < size) { - njs_chb_append(chain, hcp->pos, hcp->chunk_size); - hcp->pos += hcp->chunk_size; - - return NGX_JS_HTTP_CHUNK_END; - } - - njs_chb_append(chain, hcp->pos, size); - hcp->pos += size; - - hcp->chunk_size -= size; - - if (hcp->chunk_size == 0) { - return NGX_JS_HTTP_CHUNK_ON_BORDER; - } - - return NGX_JS_HTTP_CHUNK_MIDDLE; -} - - -static ngx_int_t -ngx_js_http_parse_chunked(ngx_js_http_chunk_parse_t *hcp, - ngx_buf_t *b, njs_chb_t *chain) -{ - u_char c, ch; - ngx_int_t rc; - - enum { - sw_start = 0, - sw_chunk_size, - sw_chunk_size_linefeed, - sw_chunk_end_newline, - sw_chunk_end_linefeed, - sw_chunk, - } state; - - state = hcp->state; - - hcp->pos = b->pos; - - while (hcp->pos < b->last) { - /* - * The sw_chunk state is tested outside the switch - * to preserve hcp->pos and to not touch memory. - */ - if (state == sw_chunk) { - rc = ngx_js_http_chunk_buffer(hcp, b, chain); - if (rc == NGX_ERROR) { - return rc; - } - - if (rc == NGX_JS_HTTP_CHUNK_MIDDLE) { - break; - } - - state = sw_chunk_end_newline; - - if (rc == NGX_JS_HTTP_CHUNK_ON_BORDER) { - break; - } - - /* rc == NGX_JS_HTTP_CHUNK_END */ - } - - ch = *hcp->pos++; - - switch (state) { - - case sw_start: - state = sw_chunk_size; - - c = ch - '0'; - - if (c <= 9) { - hcp->chunk_size = c; - continue; - } - - c = (ch | 0x20) - 'a'; - - if (c <= 5) { - hcp->chunk_size = 0x0A + c; - continue; - } - - return NGX_ERROR; - - case sw_chunk_size: - - c = ch - '0'; - - if (c > 9) { - c = (ch | 0x20) - 'a'; - - if (c <= 5) { - c += 0x0A; - - } else if (ch == '\r') { - state = sw_chunk_size_linefeed; - continue; - - } else { - return NGX_ERROR; - } - } - - if (ngx_size_is_sufficient(hcp->chunk_size)) { - hcp->chunk_size = (hcp->chunk_size << 4) + c; - continue; - } - - return NGX_ERROR; - - case sw_chunk_size_linefeed: - if (ch == '\n') { - - if (hcp->chunk_size != 0) { - state = sw_chunk; - continue; - } - - hcp->last = 1; - state = sw_chunk_end_newline; - continue; - } - - return NGX_ERROR; - - case sw_chunk_end_newline: - if (ch == '\r') { - state = sw_chunk_end_linefeed; - continue; - } - - return NGX_ERROR; - - case sw_chunk_end_linefeed: - if (ch == '\n') { - - if (!hcp->last) { - state = sw_start; - continue; - } - - return NGX_OK; - } - - return NGX_ERROR; - - case sw_chunk: - /* - * This state is processed before the switch. - * It added here just to suppress a warning. - */ - continue; - } - } - - hcp->state = state; - - return NGX_AGAIN; -} - - -static void -ngx_js_http_dummy_handler(ngx_event_t *ev) -{ - ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, 0, "js fetch dummy handler"); -} - - static njs_int_t ngx_headers_js_get(njs_vm_t *vm, njs_value_t *value, njs_str_t *name, njs_value_t *retval, njs_bool_t as_array) diff --git a/nginx/ngx_js_http.c b/nginx/ngx_js_http.c new file mode 100644 index 00000000..c958b0c8 --- /dev/null +++ b/nginx/ngx_js_http.c @@ -0,0 +1,1531 @@ + +/* + * Copyright (C) Dmitry Volyntsev + * Copyright (C) hongzhidao + * Copyright (C) Antoine Bonavita + * Copyright (C) NGINX, Inc. + */ + + +#include +#include +#include +#include +#include "ngx_js.h" +#include "ngx_js_http.h" + + +static void ngx_js_http_resolve_handler(ngx_resolver_ctx_t *ctx); +static void ngx_js_http_next(ngx_js_http_t *http); +static void ngx_js_http_write_handler(ngx_event_t *wev); +static void ngx_js_http_read_handler(ngx_event_t *rev); +static void ngx_js_http_dummy_handler(ngx_event_t *ev); + +static ngx_int_t ngx_js_http_process_status_line(ngx_js_http_t *http); +static ngx_int_t ngx_js_http_process_headers(ngx_js_http_t *http); +static ngx_int_t ngx_js_http_process_body(ngx_js_http_t *http); +static ngx_int_t ngx_js_http_parse_status_line(ngx_js_http_parse_t *hp, + ngx_buf_t *b); +static ngx_int_t ngx_js_http_parse_header_line(ngx_js_http_parse_t *hp, + ngx_buf_t *b); +static ngx_int_t ngx_js_http_parse_chunked(ngx_js_http_chunk_parse_t *hcp, + ngx_buf_t *b, njs_chb_t *chain); + +#if (NGX_SSL) +static void ngx_js_http_ssl_init_connection(ngx_js_http_t *http); +static void ngx_js_http_ssl_handshake_handler(ngx_connection_t *c); +static void ngx_js_http_ssl_handshake(ngx_js_http_t *http); +static ngx_int_t ngx_js_http_ssl_name(ngx_js_http_t *http); +#endif + + +static void +ngx_js_http_error(ngx_js_http_t *http, const char *fmt, ...) +{ + u_char *p, *end; + va_list args; + u_char err[NGX_MAX_ERROR_STR]; + + end = err + NGX_MAX_ERROR_STR - 1; + + va_start(args, fmt); + p = njs_vsprintf(err, end, fmt, args); + *p = '\0'; + va_end(args); + + http->error_handler(http, (const char *) err); +} + + +ngx_resolver_ctx_t * +ngx_js_http_resolve(ngx_js_http_t *http, ngx_resolver_t *r, ngx_str_t *host, + in_port_t port, ngx_msec_t timeout) +{ + ngx_int_t ret; + ngx_resolver_ctx_t *ctx; + + ctx = ngx_resolve_start(r, NULL); + if (ctx == NULL) { + return NULL; + } + + if (ctx == NGX_NO_RESOLVER) { + return ctx; + } + + http->ctx = ctx; + http->port = port; + + ctx->name = *host; + ctx->handler = ngx_js_http_resolve_handler; + ctx->data = http; + ctx->timeout = timeout; + + ret = ngx_resolve_name(ctx); + if (ret != NGX_OK) { + http->ctx = NULL; + return NULL; + } + + return ctx; +} + + +static void +ngx_js_http_resolve_handler(ngx_resolver_ctx_t *ctx) +{ + u_char *p; + size_t len; + socklen_t socklen; + ngx_uint_t i; + ngx_js_http_t *http; + struct sockaddr *sockaddr; + + http = ctx->data; + + if (ctx->state) { + ngx_js_http_error(http, "\"%V\" could not be resolved (%i: %s)", + &ctx->name, ctx->state, + ngx_resolver_strerror(ctx->state)); + return; + } + + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, + "http resolved: \"%V\"", &ctx->name); + +#if (NGX_DEBUG) + { + u_char text[NGX_SOCKADDR_STRLEN]; + ngx_str_t addr; + ngx_uint_t i; + + addr.data = text; + + for (i = 0; i < ctx->naddrs; i++) { + addr.len = ngx_sock_ntop(ctx->addrs[i].sockaddr, ctx->addrs[i].socklen, + text, NGX_SOCKADDR_STRLEN, 0); + + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, + "name was resolved to \"%V\"", &addr); + } + } +#endif + + http->naddrs = ctx->naddrs; + http->addrs = ngx_pcalloc(http->pool, http->naddrs * sizeof(ngx_addr_t)); + + if (http->addrs == NULL) { + goto failed; + } + + for (i = 0; i < ctx->naddrs; i++) { + socklen = ctx->addrs[i].socklen; + + sockaddr = ngx_palloc(http->pool, socklen); + if (sockaddr == NULL) { + goto failed; + } + + ngx_memcpy(sockaddr, ctx->addrs[i].sockaddr, socklen); + ngx_inet_set_port(sockaddr, http->port); + + http->addrs[i].sockaddr = sockaddr; + http->addrs[i].socklen = socklen; + + p = ngx_pnalloc(http->pool, NGX_SOCKADDR_STRLEN); + if (p == NULL) { + goto failed; + } + + len = ngx_sock_ntop(sockaddr, socklen, p, NGX_SOCKADDR_STRLEN, 1); + http->addrs[i].name.len = len; + http->addrs[i].name.data = p; + } + + ngx_js_http_resolve_done(http); + + ngx_js_http_connect(http); + + return; + +failed: + + ngx_js_http_error(http, "memory error"); +} + + +static void +ngx_js_http_close_connection(ngx_connection_t *c) +{ + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, + "js http close connection: %d", c->fd); + +#if (NGX_SSL) + if (c->ssl) { + c->ssl->no_wait_shutdown = 1; + + if (ngx_ssl_shutdown(c) == NGX_AGAIN) { + c->ssl->handler = ngx_js_http_close_connection; + return; + } + } +#endif + + c->destroyed = 1; + + ngx_close_connection(c); +} + + +void +ngx_js_http_resolve_done(ngx_js_http_t *http) +{ + if (http->ctx != NULL) { + ngx_resolve_name_done(http->ctx); + http->ctx = NULL; + } +} + + +void +ngx_js_http_close_peer(ngx_js_http_t *http) +{ + if (http->peer.connection != NULL) { + ngx_js_http_close_connection(http->peer.connection); + http->peer.connection = NULL; + } +} + + +void +ngx_js_http_connect(ngx_js_http_t *http) +{ + ngx_int_t rc; + ngx_addr_t *addr; + + addr = &http->addrs[http->naddr]; + + ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0, + "js http connect %ui/%ui", http->naddr, http->naddrs); + + http->peer.sockaddr = addr->sockaddr; + http->peer.socklen = addr->socklen; + http->peer.name = &addr->name; + http->peer.get = ngx_event_get_peer; + http->peer.log = http->log; + http->peer.log_error = NGX_ERROR_ERR; + + rc = ngx_event_connect_peer(&http->peer); + + if (rc == NGX_ERROR) { + ngx_js_http_error(http, "connect failed"); + return; + } + + if (rc == NGX_BUSY || rc == NGX_DECLINED) { + ngx_js_http_next(http); + return; + } + + http->peer.connection->data = http; + http->peer.connection->pool = http->pool; + + http->peer.connection->write->handler = ngx_js_http_write_handler; + http->peer.connection->read->handler = ngx_js_http_read_handler; + + http->process = ngx_js_http_process_status_line; + + ngx_add_timer(http->peer.connection->read, http->timeout); + ngx_add_timer(http->peer.connection->write, http->timeout); + +#if (NGX_SSL) + if (http->ssl != NULL && http->peer.connection->ssl == NULL) { + ngx_js_http_ssl_init_connection(http); + return; + } +#endif + + if (rc == NGX_OK) { + ngx_js_http_write_handler(http->peer.connection->write); + } +} + + +#if (NGX_SSL) + +static void +ngx_js_http_ssl_init_connection(ngx_js_http_t *http) +{ + ngx_int_t rc; + ngx_connection_t *c; + + c = http->peer.connection; + + ngx_log_debug2(NGX_LOG_DEBUG_EVENT, http->log, 0, + "js http secure connect %ui/%ui", http->naddr, + http->naddrs); + + if (ngx_ssl_create_connection(http->ssl, c, NGX_SSL_BUFFER|NGX_SSL_CLIENT) + != NGX_OK) + { + ngx_js_http_error(http, "failed to create ssl connection"); + return; + } + + c->sendfile = 0; + + if (ngx_js_http_ssl_name(http) != NGX_OK) { + ngx_js_http_error(http, "failed to create ssl connection"); + return; + } + + c->log->action = "SSL handshaking to http target"; + + rc = ngx_ssl_handshake(c); + + if (rc == NGX_AGAIN) { + c->data = http; + c->ssl->handler = ngx_js_http_ssl_handshake_handler; + return; + } + + ngx_js_http_ssl_handshake(http); +} + + +static void +ngx_js_http_ssl_handshake_handler(ngx_connection_t *c) +{ + ngx_js_http_t *http; + + http = c->data; + + http->peer.connection->write->handler = ngx_js_http_write_handler; + http->peer.connection->read->handler = ngx_js_http_read_handler; + + ngx_js_http_ssl_handshake(http); +} + + +static void +ngx_js_http_ssl_handshake(ngx_js_http_t *http) +{ + long rc; + ngx_connection_t *c; + + c = http->peer.connection; + + if (c->ssl->handshaked) { + if (http->ssl_verify) { + rc = SSL_get_verify_result(c->ssl->connection); + + if (rc != X509_V_OK) { + ngx_log_error(NGX_LOG_ERR, c->log, 0, + "js http SSL certificate verify error: (%l:%s)", + rc, X509_verify_cert_error_string(rc)); + goto failed; + } + + if (ngx_ssl_check_host(c, &http->tls_name) != NGX_OK) { + ngx_log_error(NGX_LOG_ERR, c->log, 0, + "js http SSL certificate does not match \"%V\"", + &http->tls_name); + goto failed; + } + } + + c->write->handler = ngx_js_http_write_handler; + c->read->handler = ngx_js_http_read_handler; + + if (c->read->ready) { + ngx_post_event(c->read, &ngx_posted_events); + } + + http->process = ngx_js_http_process_status_line; + ngx_js_http_write_handler(c->write); + + return; + } + +failed: + + ngx_js_http_next(http); +} + + +static ngx_int_t +ngx_js_http_ssl_name(ngx_js_http_t *http) +{ +#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME + u_char *p; + + /* as per RFC 6066, literal IPv4 and IPv6 addresses are not permitted */ + ngx_str_t *name = &http->tls_name; + + if (name->len == 0 || *name->data == '[') { + goto done; + } + + if (ngx_inet_addr(name->data, name->len) != INADDR_NONE) { + goto done; + } + + /* + * SSL_set_tlsext_host_name() needs a null-terminated string, + * hence we explicitly null-terminate name here + */ + + p = ngx_pnalloc(http->pool, name->len + 1); + if (p == NULL) { + return NGX_ERROR; + } + + (void) ngx_cpystrn(p, name->data, name->len + 1); + + name->data = p; + + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, + "js http SSL server name: \"%s\"", name->data); + + if (SSL_set_tlsext_host_name(http->peer.connection->ssl->connection, + (char *) name->data) + == 0) + { + ngx_ssl_error(NGX_LOG_ERR, http->log, 0, + "SSL_set_tlsext_host_name(\"%s\") failed", name->data); + return NGX_ERROR; + } + +#endif +done: + + return NGX_OK; +} + +#endif + + +static void +ngx_js_http_next(ngx_js_http_t *http) +{ + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, http->log, 0, "js http next addr"); + + if (++http->naddr >= http->naddrs) { + ngx_js_http_error(http, "connect failed"); + return; + } + + if (http->peer.connection != NULL) { + ngx_js_http_close_connection(http->peer.connection); + http->peer.connection = NULL; + } + + http->buffer = NULL; + + ngx_js_http_connect(http); +} + + +static void +ngx_js_http_write_handler(ngx_event_t *wev) +{ + ssize_t n, size; + ngx_buf_t *b; + ngx_js_http_t *http; + ngx_connection_t *c; + + c = wev->data; + http = c->data; + + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, wev->log, 0, "js http write handler"); + + if (wev->timedout) { + ngx_js_http_error(http, "write timed out"); + return; + } + +#if (NGX_SSL) + if (http->ssl != NULL && http->peer.connection->ssl == NULL) { + ngx_js_http_ssl_init_connection(http); + return; + } +#endif + + b = http->buffer; + + if (b == NULL) { + size = njs_chb_size(&http->chain); + if (size < 0) { + ngx_js_http_error(http, "memory error"); + return; + } + + b = ngx_create_temp_buf(http->pool, size); + if (b == NULL) { + ngx_js_http_error(http, "memory error"); + return; + } + + njs_chb_join_to(&http->chain, b->last); + b->last += size; + + http->buffer = b; + } + + size = b->last - b->pos; + + n = c->send(c, b->pos, size); + + if (n == NGX_ERROR) { + ngx_js_http_next(http); + return; + } + + if (n > 0) { + b->pos += n; + + if (n == size) { + wev->handler = ngx_js_http_dummy_handler; + + http->buffer = NULL; + + if (wev->timer_set) { + ngx_del_timer(wev); + } + + if (ngx_handle_write_event(wev, 0) != NGX_OK) { + ngx_js_http_error(http, "write failed"); + } + + return; + } + } + + if (!wev->timer_set) { + ngx_add_timer(wev, http->timeout); + } +} + + +static void +ngx_js_http_read_handler(ngx_event_t *rev) +{ + ssize_t n, size; + ngx_int_t rc; + ngx_buf_t *b; + ngx_js_http_t *http; + ngx_connection_t *c; + + c = rev->data; + http = c->data; + + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, rev->log, 0, "js http read handler"); + + if (rev->timedout) { + ngx_js_http_error(http, "read timed out"); + return; + } + + if (http->buffer == NULL) { + b = ngx_create_temp_buf(http->pool, http->buffer_size); + if (b == NULL) { + ngx_js_http_error(http, "memory error"); + return; + } + + http->buffer = b; + } + + for ( ;; ) { + b = http->buffer; + size = b->end - b->last; + + n = c->recv(c, b->last, size); + + if (n > 0) { + b->last += n; + + rc = http->process(http); + + if (rc == NGX_ERROR) { + return; + } + + continue; + } + + if (n == NGX_AGAIN) { + if (ngx_handle_read_event(rev, 0) != NGX_OK) { + ngx_js_http_error(http, "read failed"); + } + + return; + } + + if (n == NGX_ERROR) { + ngx_js_http_next(http); + return; + } + + break; + } + + http->done = 1; + + rc = http->process(http); + + if (rc == NGX_DONE) { + /* handler was called */ + return; + } + + if (rc == NGX_AGAIN) { + ngx_js_http_error(http, "prematurely closed connection"); + } +} + + +static void +ngx_js_http_dummy_handler(ngx_event_t *ev) +{ + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, 0, "js http dummy handler"); +} + + +static ngx_int_t +ngx_js_http_process_status_line(ngx_js_http_t *http) +{ + ngx_int_t rc; + ngx_js_http_parse_t *hp; + + hp = &http->http_parse; + + rc = ngx_js_http_parse_status_line(hp, http->buffer); + + if (rc == NGX_OK) { + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, "js http status %ui", + hp->code); + + http->response.code = hp->code; + http->response.status_text.start = hp->status_text; + http->response.status_text.length = hp->status_text_end + - hp->status_text; + http->process = ngx_js_http_process_headers; + + return http->process(http); + } + + if (rc == NGX_AGAIN) { + return NGX_AGAIN; + } + + /* rc == NGX_ERROR */ + + ngx_js_http_error(http, "invalid http status line"); + + return NGX_ERROR; +} + + +static ngx_int_t +ngx_js_http_process_headers(ngx_js_http_t *http) +{ + size_t len, vlen; + ngx_int_t rc; + ngx_js_http_parse_t *hp; + + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, http->log, 0, + "js http process headers"); + + hp = &http->http_parse; + + if (http->response.headers.header_list.size == 0) { + rc = ngx_list_init(&http->response.headers.header_list, http->pool, 4, + sizeof(ngx_js_tb_elt_t)); + if (rc != NGX_OK) { + ngx_js_http_error(http, "alloc failed"); + return NGX_ERROR; + } + } + + for ( ;; ) { + rc = ngx_js_http_parse_header_line(hp, http->buffer); + + if (rc == NGX_OK) { + len = hp->header_name_end - hp->header_name_start; + vlen = hp->header_end - hp->header_start; + + rc = http->append_headers(http, &http->response.headers, + hp->header_name_start, len, + hp->header_start, vlen); + + if (rc == NGX_ERROR) { + ngx_js_http_error(http, "cannot add respose header"); + return NGX_ERROR; + } + + ngx_log_debug4(NGX_LOG_DEBUG_EVENT, http->log, 0, + "js http header \"%*s: %*s\"", + len, hp->header_name_start, vlen, hp->header_start); + + if (len == (sizeof("Transfer-Encoding") -1) + && vlen == (sizeof("chunked") - 1) + && ngx_strncasecmp(hp->header_name_start, + (u_char *) "Transfer-Encoding", len) == 0 + && ngx_strncasecmp(hp->header_start, (u_char *) "chunked", + vlen) == 0) + { + hp->chunked = 1; + } + + if (len == (sizeof("Content-Length") - 1) + && ngx_strncasecmp(hp->header_name_start, + (u_char *) "Content-Length", len) == 0) + { + hp->content_length_n = ngx_atoof(hp->header_start, vlen); + if (hp->content_length_n == NGX_ERROR) { + ngx_js_http_error(http, "invalid http content length"); + return NGX_ERROR; + } + + if (!http->header_only + && hp->content_length_n + > (off_t) http->max_response_body_size) + { + ngx_js_http_error(http, + "http content length is too large"); + return NGX_ERROR; + } + } + + continue; + } + + if (rc == NGX_DONE) { + http->response.headers.guard = GUARD_IMMUTABLE; + break; + } + + if (rc == NGX_AGAIN) { + return NGX_AGAIN; + } + + /* rc == NGX_ERROR */ + + ngx_js_http_error(http, "invalid http header"); + + return NGX_ERROR; + } + + njs_chb_destroy(&http->chain); + + http->process = ngx_js_http_process_body; + + return http->process(http); +} + + +static ngx_int_t +ngx_js_http_process_body(ngx_js_http_t *http) +{ + ssize_t size, chsize, need; + ngx_int_t rc; + ngx_buf_t *b; + + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, http->log, 0, + "js http process body done:%ui", (ngx_uint_t) http->done); + + if (http->done) { + size = njs_chb_size(&http->response.chain); + if (size < 0) { + ngx_js_http_error(http, "memory error"); + return NGX_ERROR; + } + + if (!http->header_only + && http->http_parse.chunked + && http->http_parse.content_length_n == -1) + { + ngx_js_http_error(http, "invalid http chunked response"); + return NGX_ERROR; + } + + if (http->header_only + || http->http_parse.content_length_n == -1 + || size == http->http_parse.content_length_n) + { + http->ready_handler(http); + return NGX_DONE; + } + + if (size < http->http_parse.content_length_n) { + return NGX_AGAIN; + } + + ngx_js_http_error(http, "http trailing data"); + return NGX_ERROR; + } + + b = http->buffer; + + if (http->http_parse.chunked) { + rc = ngx_js_http_parse_chunked(&http->http_chunk_parse, b, + &http->response.chain); + if (rc == NGX_ERROR) { + ngx_js_http_error(http, "invalid http chunked response"); + return NGX_ERROR; + } + + size = njs_chb_size(&http->response.chain); + + if (rc == NGX_OK) { + http->http_parse.content_length_n = size; + } + + if (size > http->max_response_body_size * 10) { + ngx_js_http_error(http, "very large http chunked response"); + return NGX_ERROR; + } + + b->pos = http->http_chunk_parse.pos; + + } else { + size = njs_chb_size(&http->response.chain); + + if (http->header_only) { + need = 0; + + } else if (http->http_parse.content_length_n == -1) { + need = http->max_response_body_size - size; + + } else { + need = http->http_parse.content_length_n - size; + } + + chsize = ngx_min(need, b->last - b->pos); + + if (size + chsize > http->max_response_body_size) { + ngx_js_http_error(http, "http response body is too large"); + return NGX_ERROR; + } + + if (chsize > 0) { + njs_chb_append(&http->response.chain, b->pos, chsize); + b->pos += chsize; + } + + rc = (need > chsize) ? NGX_AGAIN : NGX_DONE; + } + + if (b->pos == b->end) { + if (http->chunk == NULL) { + b = ngx_create_temp_buf(http->pool, http->buffer_size); + if (b == NULL) { + ngx_js_http_error(http, "memory error"); + return NGX_ERROR; + } + + http->buffer = b; + http->chunk = b; + + } else { + b->last = b->start; + b->pos = b->start; + } + } + + return rc; +} + + +static ngx_int_t +ngx_js_http_parse_status_line(ngx_js_http_parse_t *hp, ngx_buf_t *b) +{ + u_char ch; + u_char *p; + enum { + sw_start = 0, + sw_H, + sw_HT, + sw_HTT, + sw_HTTP, + sw_first_major_digit, + sw_major_digit, + sw_first_minor_digit, + sw_minor_digit, + sw_status, + sw_space_after_status, + sw_status_text, + sw_almost_done + } state; + + state = hp->state; + + for (p = b->pos; p < b->last; p++) { + ch = *p; + + switch (state) { + + /* "HTTP/" */ + case sw_start: + switch (ch) { + case 'H': + state = sw_H; + break; + default: + return NGX_ERROR; + } + break; + + case sw_H: + switch (ch) { + case 'T': + state = sw_HT; + break; + default: + return NGX_ERROR; + } + break; + + case sw_HT: + switch (ch) { + case 'T': + state = sw_HTT; + break; + default: + return NGX_ERROR; + } + break; + + case sw_HTT: + switch (ch) { + case 'P': + state = sw_HTTP; + break; + default: + return NGX_ERROR; + } + break; + + case sw_HTTP: + switch (ch) { + case '/': + state = sw_first_major_digit; + break; + default: + return NGX_ERROR; + } + break; + + /* the first digit of major HTTP version */ + case sw_first_major_digit: + if (ch < '1' || ch > '9') { + return NGX_ERROR; + } + + state = sw_major_digit; + break; + + /* the major HTTP version or dot */ + case sw_major_digit: + if (ch == '.') { + state = sw_first_minor_digit; + break; + } + + if (ch < '0' || ch > '9') { + return NGX_ERROR; + } + + break; + + /* the first digit of minor HTTP version */ + case sw_first_minor_digit: + if (ch < '0' || ch > '9') { + return NGX_ERROR; + } + + state = sw_minor_digit; + break; + + /* the minor HTTP version or the end of the request line */ + case sw_minor_digit: + if (ch == ' ') { + state = sw_status; + break; + } + + if (ch < '0' || ch > '9') { + return NGX_ERROR; + } + + break; + + /* HTTP status code */ + case sw_status: + if (ch == ' ') { + break; + } + + if (ch < '0' || ch > '9') { + return NGX_ERROR; + } + + hp->code = hp->code * 10 + (ch - '0'); + + if (++hp->count == 3) { + state = sw_space_after_status; + } + + break; + + /* space or end of line */ + case sw_space_after_status: + switch (ch) { + case ' ': + state = sw_status_text; + break; + case '.': /* IIS may send 403.1, 403.2, etc */ + state = sw_status_text; + break; + case CR: + break; + case LF: + goto done; + default: + return NGX_ERROR; + } + break; + + /* any text until end of line */ + case sw_status_text: + switch (ch) { + case CR: + hp->status_text_end = p; + state = sw_almost_done; + break; + case LF: + hp->status_text_end = p; + goto done; + } + + if (hp->status_text == NULL) { + hp->status_text = p; + } + + break; + + /* end of status line */ + case sw_almost_done: + switch (ch) { + case LF: + goto done; + default: + return NGX_ERROR; + } + } + } + + b->pos = p; + hp->state = state; + + return NGX_AGAIN; + +done: + + b->pos = p + 1; + hp->state = sw_start; + + return NGX_OK; +} + + +static ngx_int_t +ngx_js_http_parse_header_line(ngx_js_http_parse_t *hp, ngx_buf_t *b) +{ + u_char c, ch, *p; + enum { + sw_start = 0, + sw_name, + sw_space_before_value, + sw_value, + sw_space_after_value, + sw_almost_done, + sw_header_almost_done + } state; + + state = hp->state; + + for (p = b->pos; p < b->last; p++) { + ch = *p; + + switch (state) { + + /* first char */ + case sw_start: + + switch (ch) { + case CR: + hp->header_end = p; + state = sw_header_almost_done; + break; + case LF: + hp->header_end = p; + goto header_done; + default: + state = sw_name; + hp->header_name_start = p; + + c = (u_char) (ch | 0x20); + if (c >= 'a' && c <= 'z') { + break; + } + + if (ch >= '0' && ch <= '9') { + break; + } + + return NGX_ERROR; + } + break; + + /* header name */ + case sw_name: + c = (u_char) (ch | 0x20); + if (c >= 'a' && c <= 'z') { + break; + } + + if (ch == ':') { + hp->header_name_end = p; + state = sw_space_before_value; + break; + } + + if (ch == '-' || ch == '_') { + break; + } + + if (ch >= '0' && ch <= '9') { + break; + } + + if (ch == CR) { + hp->header_name_end = p; + hp->header_start = p; + hp->header_end = p; + state = sw_almost_done; + break; + } + + if (ch == LF) { + hp->header_name_end = p; + hp->header_start = p; + hp->header_end = p; + goto done; + } + + return NGX_ERROR; + + /* space* before header value */ + case sw_space_before_value: + switch (ch) { + case ' ': + break; + case CR: + hp->header_start = p; + hp->header_end = p; + state = sw_almost_done; + break; + case LF: + hp->header_start = p; + hp->header_end = p; + goto done; + default: + hp->header_start = p; + state = sw_value; + break; + } + break; + + /* header value */ + case sw_value: + switch (ch) { + case ' ': + hp->header_end = p; + state = sw_space_after_value; + break; + case CR: + hp->header_end = p; + state = sw_almost_done; + break; + case LF: + hp->header_end = p; + goto done; + } + break; + + /* space* before end of header line */ + case sw_space_after_value: + switch (ch) { + case ' ': + break; + case CR: + state = sw_almost_done; + break; + case LF: + goto done; + default: + state = sw_value; + break; + } + break; + + /* end of header line */ + case sw_almost_done: + switch (ch) { + case LF: + goto done; + default: + return NGX_ERROR; + } + + /* end of header */ + case sw_header_almost_done: + switch (ch) { + case LF: + goto header_done; + default: + return NGX_ERROR; + } + } + } + + b->pos = p; + hp->state = state; + + return NGX_AGAIN; + +done: + + b->pos = p + 1; + hp->state = sw_start; + + return NGX_OK; + +header_done: + + b->pos = p + 1; + hp->state = sw_start; + + return NGX_DONE; +} + + +#define \ +ngx_size_is_sufficient(cs) \ + (cs < ((__typeof__(cs)) 1 << (sizeof(cs) * 8 - 4))) + + +#define NGX_JS_HTTP_CHUNK_MIDDLE 0 +#define NGX_JS_HTTP_CHUNK_ON_BORDER 1 +#define NGX_JS_HTTP_CHUNK_END 2 + + +static ngx_int_t +ngx_js_http_chunk_buffer(ngx_js_http_chunk_parse_t *hcp, ngx_buf_t *b, + njs_chb_t *chain) +{ + size_t size; + + size = b->last - hcp->pos; + + if (hcp->chunk_size < size) { + njs_chb_append(chain, hcp->pos, hcp->chunk_size); + hcp->pos += hcp->chunk_size; + + return NGX_JS_HTTP_CHUNK_END; + } + + njs_chb_append(chain, hcp->pos, size); + hcp->pos += size; + + hcp->chunk_size -= size; + + if (hcp->chunk_size == 0) { + return NGX_JS_HTTP_CHUNK_ON_BORDER; + } + + return NGX_JS_HTTP_CHUNK_MIDDLE; +} + + +static ngx_int_t +ngx_js_http_parse_chunked(ngx_js_http_chunk_parse_t *hcp, + ngx_buf_t *b, njs_chb_t *chain) +{ + u_char c, ch; + ngx_int_t rc; + + enum { + sw_start = 0, + sw_chunk_size, + sw_chunk_size_linefeed, + sw_chunk_end_newline, + sw_chunk_end_linefeed, + sw_chunk, + } state; + + state = hcp->state; + + hcp->pos = b->pos; + + while (hcp->pos < b->last) { + /* + * The sw_chunk state is tested outside the switch + * to preserve hcp->pos and to not touch memory. + */ + if (state == sw_chunk) { + rc = ngx_js_http_chunk_buffer(hcp, b, chain); + if (rc == NGX_ERROR) { + return rc; + } + + if (rc == NGX_JS_HTTP_CHUNK_MIDDLE) { + break; + } + + state = sw_chunk_end_newline; + + if (rc == NGX_JS_HTTP_CHUNK_ON_BORDER) { + break; + } + + /* rc == NGX_JS_HTTP_CHUNK_END */ + } + + ch = *hcp->pos++; + + switch (state) { + + case sw_start: + state = sw_chunk_size; + + c = ch - '0'; + + if (c <= 9) { + hcp->chunk_size = c; + continue; + } + + c = (ch | 0x20) - 'a'; + + if (c <= 5) { + hcp->chunk_size = 0x0A + c; + continue; + } + + return NGX_ERROR; + + case sw_chunk_size: + + c = ch - '0'; + + if (c > 9) { + c = (ch | 0x20) - 'a'; + + if (c <= 5) { + c += 0x0A; + + } else if (ch == '\r') { + state = sw_chunk_size_linefeed; + continue; + + } else { + return NGX_ERROR; + } + } + + if (ngx_size_is_sufficient(hcp->chunk_size)) { + hcp->chunk_size = (hcp->chunk_size << 4) + c; + continue; + } + + return NGX_ERROR; + + case sw_chunk_size_linefeed: + if (ch == '\n') { + + if (hcp->chunk_size != 0) { + state = sw_chunk; + continue; + } + + hcp->last = 1; + state = sw_chunk_end_newline; + continue; + } + + return NGX_ERROR; + + case sw_chunk_end_newline: + if (ch == '\r') { + state = sw_chunk_end_linefeed; + continue; + } + + return NGX_ERROR; + + case sw_chunk_end_linefeed: + if (ch == '\n') { + + if (!hcp->last) { + state = sw_start; + continue; + } + + return NGX_OK; + } + + return NGX_ERROR; + + case sw_chunk: + /* + * This state is processed before the switch. + * It added here just to suppress a warning. + */ + continue; + } + } + + hcp->state = state; + + return NGX_AGAIN; +} + + +static inline int +ngx_js_http_whitespace(u_char c) +{ + switch (c) { + case 0x09: /* */ + case 0x0A: /* */ + case 0x0D: /* */ + case 0x20: /* */ + return 1; + + default: + return 0; + } +} + + +void +ngx_js_http_trim(u_char **value, size_t *len, int trim_c0_control_or_space) +{ + u_char *start, *end; + + start = *value; + end = start + *len; + + for ( ;; ) { + if (start == end) { + break; + } + + if (ngx_js_http_whitespace(*start) + || (trim_c0_control_or_space && *start <= ' ')) + { + start++; + continue; + } + + break; + } + + for ( ;; ) { + if (start == end) { + break; + } + + end--; + + if (ngx_js_http_whitespace(*end) + || (trim_c0_control_or_space && *end <= ' ')) + { + continue; + } + + end++; + break; + } + + *value = start; + *len = end - start; +} + + +static const uint32_t token_map[] = { + 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ + + /* ?>=< ;:98 7654 3210 /.-, +*)( '&%$ #"! */ + 0x03ff6cfa, /* 0000 0011 1111 1111 0110 1100 1111 1010 */ + + /* _^]\ [ZYX WVUT SRQP ONML KJIH GFED CBA@ */ + 0xc7fffffe, /* 1100 0111 1111 1111 1111 1111 1111 1110 */ + + /* ~}| {zyx wvut srqp onml kjih gfed cba` */ + 0x57ffffff, /* 0101 0111 1111 1111 1111 1111 1111 1111 */ + + 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ + 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ + 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ + 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ +}; + + +static inline int +ngx_is_token(uint32_t byte) +{ + return ((token_map[byte >> 5] & ((uint32_t) 1 << (byte & 0x1f))) != 0); +} + + +ngx_int_t +ngx_js_check_header_name(u_char *name, size_t len) +{ + u_char *p, *end; + + p = name; + end = p + len; + + while (p < end) { + if (!ngx_is_token(*p)) { + return NGX_ERROR; + } + + p++; + } + + return NGX_OK; +} diff --git a/nginx/ngx_js_http.h b/nginx/ngx_js_http.h new file mode 100644 index 00000000..f5b171c2 --- /dev/null +++ b/nginx/ngx_js_http.h @@ -0,0 +1,160 @@ + +/* + * Copyright (C) Dmitry Volyntsev + * Copyright (C) hongzhidao + * Copyright (C) Antoine Bonavita + * Copyright (C) NGINX, Inc. + */ + + +#ifndef _NGX_JS_HTTP_H_INCLUDED_ +#define _NGX_JS_HTTP_H_INCLUDED_ + + +typedef struct ngx_js_http_s ngx_js_http_t; + + +typedef struct { + ngx_uint_t state; + ngx_uint_t code; + u_char *status_text; + u_char *status_text_end; + ngx_uint_t count; + ngx_flag_t chunked; + off_t content_length_n; + + u_char *header_name_start; + u_char *header_name_end; + u_char *header_start; + u_char *header_end; +} ngx_js_http_parse_t; + + +typedef struct { + u_char *pos; + uint64_t chunk_size; + uint8_t state; + uint8_t last; +} ngx_js_http_chunk_parse_t; + + +typedef struct ngx_js_tb_elt_s ngx_js_tb_elt_t; + +struct ngx_js_tb_elt_s { + ngx_uint_t hash; + ngx_str_t key; + ngx_str_t value; + ngx_js_tb_elt_t *next; +}; + + +typedef struct { + enum { + GUARD_NONE = 0, + GUARD_REQUEST, + GUARD_IMMUTABLE, + GUARD_RESPONSE, + } guard; + ngx_list_t header_list; + ngx_js_tb_elt_t *content_type; +} ngx_js_headers_t; + + +typedef struct { + enum { + CACHE_MODE_DEFAULT = 0, + CACHE_MODE_NO_STORE, + CACHE_MODE_RELOAD, + CACHE_MODE_NO_CACHE, + CACHE_MODE_FORCE_CACHE, + CACHE_MODE_ONLY_IF_CACHED, + } cache_mode; + enum { + CREDENTIALS_SAME_ORIGIN = 0, + CREDENTIALS_INCLUDE, + CREDENTIALS_OMIT, + } credentials; + enum { + MODE_NO_CORS = 0, + MODE_SAME_ORIGIN, + MODE_CORS, + MODE_NAVIGATE, + MODE_WEBSOCKET, + } mode; + njs_str_t url; + njs_str_t method; + u_char m[8]; + uint8_t body_used; + njs_str_t body; + ngx_js_headers_t headers; + njs_opaque_value_t header_value; +} ngx_js_request_t; + + +typedef struct { + njs_str_t url; + ngx_int_t code; + njs_str_t status_text; + uint8_t body_used; + njs_chb_t chain; + ngx_js_headers_t headers; + njs_opaque_value_t header_value; +} ngx_js_response_t; + + +struct ngx_js_http_s { + ngx_log_t *log; + ngx_pool_t *pool; + + ngx_resolver_ctx_t *ctx; + ngx_addr_t addr; + ngx_addr_t *addrs; + ngx_uint_t naddrs; + ngx_uint_t naddr; + in_port_t port; + + ngx_peer_connection_t peer; + ngx_msec_t timeout; + + ngx_int_t buffer_size; + ngx_int_t max_response_body_size; + + unsigned header_only; + +#if (NGX_SSL) + ngx_str_t tls_name; + ngx_ssl_t *ssl; + njs_bool_t ssl_verify; +#endif + + ngx_buf_t *buffer; + ngx_buf_t *chunk; + njs_chb_t chain; + + ngx_js_response_t response; + + uint8_t done; + ngx_js_http_parse_t http_parse; + ngx_js_http_chunk_parse_t http_chunk_parse; + ngx_int_t (*process)(ngx_js_http_t *http); + ngx_int_t (*append_headers)(ngx_js_http_t *http, + ngx_js_headers_t *headers, + u_char *name, size_t len, + u_char *value, size_t vlen); + void (*ready_handler)(ngx_js_http_t *http); + void (*error_handler)(ngx_js_http_t *http, + const char *err); +}; + + +ngx_resolver_ctx_t *ngx_js_http_resolve(ngx_js_http_t *http, ngx_resolver_t *r, + ngx_str_t *host, in_port_t port, ngx_msec_t timeout); +void ngx_js_http_connect(ngx_js_http_t *http); +void ngx_js_http_resolve_done(ngx_js_http_t *http); +void ngx_js_http_close_peer(ngx_js_http_t *http); +void ngx_js_http_trim(u_char **value, size_t *len, + int trim_c0_control_or_space); +ngx_int_t ngx_js_check_header_name(u_char *name, size_t len); + + +#endif /* _NGX_JS_HTTP_H_INCLUDED_ */ From noreply at nginx.com Thu May 15 18:40:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Thu, 15 May 2025 18:40:02 +0000 (UTC) Subject: [njs] Add missing null checks for promise code. Message-ID: <20250515184002.5A2B43F4F8@pubserv1.nginx> details: https://github.com/nginx/njs/commit/04721dd3ecb53e080f3f578f587b069c5bc21dae branches: master commit: 04721dd3ecb53e080f3f578f587b069c5bc21dae user: Dmitry Sviridkin date: Thu, 15 May 2025 13:55:13 +0100 description: Add missing null checks for promise code. --- src/njs_promise.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/njs_promise.c b/src/njs_promise.c index f8951cca..78043af4 100644 --- a/src/njs_promise.c +++ b/src/njs_promise.c @@ -457,6 +457,10 @@ njs_promise_trigger_reactions(njs_vm_t *vm, njs_value_t *value, function = njs_promise_create_function(vm, sizeof(njs_promise_context_t)); + if (njs_slow_path(function == NULL)) { + return njs_value_arg(&njs_value_null); + } + function->u.native = njs_promise_reaction_job; njs_set_data(&arguments[0], reaction, 0); @@ -784,6 +788,11 @@ njs_promise_prototype_then(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, } function = njs_promise_create_function(vm, sizeof(njs_promise_context_t)); + if (njs_slow_path(function == NULL)) { + /* vm error is already set by njs_promise_create_function */ + return NJS_ERROR; + } + function->u.native = njs_promise_constructor; njs_set_function(&constructor, function); From noreply at nginx.com Fri May 16 19:16:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 16 May 2025 19:16:02 +0000 (UTC) Subject: [njs] WebCrypto: added ECDH support. Message-ID: <20250516191602.C22103F50B@pubserv1.nginx> details: https://github.com/nginx/njs/commit/1b69415d8c29bde08cc4c79dbb4b88827c55d8b9 branches: master commit: 1b69415d8c29bde08cc4c79dbb4b88827c55d8b9 user: Dmitry Volyntsev date: Thu, 8 May 2025 17:13:01 -0700 description: WebCrypto: added ECDH support. --- external/njs_webcrypto_module.c | 239 +++++++++++++++++++++++++++++++++--- external/qjs_webcrypto_module.c | 245 ++++++++++++++++++++++++++++++++++--- test/harness/webCryptoUtils.js | 16 +++ test/webcrypto/derive_ecdh.t.mjs | 188 ++++++++++++++++++++++++++++ test/webcrypto/ec2_secp384r1.pkcs8 | 6 + test/webcrypto/ec2_secp384r1.spki | 5 + test/webcrypto/ec2_secp521r1.pkcs8 | 8 ++ test/webcrypto/ec2_secp521r1.spki | 6 + test/webcrypto/ec_secp384r1.pkcs8 | 6 + test/webcrypto/ec_secp384r1.spki | 5 + test/webcrypto/ec_secp521r1.pkcs8 | 8 ++ test/webcrypto/ec_secp521r1.spki | 6 + test/webcrypto/export.t.mjs | 24 +++- 13 files changed, 728 insertions(+), 34 deletions(-) diff --git a/external/njs_webcrypto_module.c b/external/njs_webcrypto_module.c index 8cc172cc..d9b05d09 100644 --- a/external/njs_webcrypto_module.c +++ b/external/njs_webcrypto_module.c @@ -28,7 +28,6 @@ typedef enum { NJS_KEY_USAGE_SIGN = 1 << 6, NJS_KEY_USAGE_VERIFY = 1 << 7, NJS_KEY_USAGE_WRAP_KEY = 1 << 8, - NJS_KEY_USAGE_UNSUPPORTED = 1 << 9, NJS_KEY_USAGE_UNWRAP_KEY = 1 << 10, } njs_webcrypto_key_usage_t; @@ -281,9 +280,11 @@ static njs_webcrypto_entry_t njs_webcrypto_alg[] = { njs_webcrypto_algorithm(NJS_ALGORITHM_ECDH, NJS_KEY_USAGE_DERIVE_KEY | NJS_KEY_USAGE_DERIVE_BITS | - NJS_KEY_USAGE_GENERATE_KEY | - NJS_KEY_USAGE_UNSUPPORTED, - NJS_KEY_FORMAT_UNKNOWN, + NJS_KEY_USAGE_GENERATE_KEY, + NJS_KEY_FORMAT_PKCS8 | + NJS_KEY_FORMAT_SPKI | + NJS_KEY_FORMAT_RAW | + NJS_KEY_FORMAT_JWK, 0) }, @@ -1441,6 +1442,188 @@ fail: } +static njs_int_t +njs_ext_derive_ecdh(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, + njs_index_t derive_key, njs_webcrypto_key_t *key, njs_value_t *retval) +{ + u_char *k; + size_t olen; + int64_t length; + unsigned usage; + EVP_PKEY *priv_pkey, *pub_pkey; + njs_int_t ret; + njs_value_t *value, *dobject; + EVP_PKEY_CTX *pctx; + njs_opaque_value_t lvalue; + njs_webcrypto_key_t *dkey, *pkey; + njs_webcrypto_algorithm_t *dalg; + + static const njs_str_t string_public = njs_str("public"); + + dobject = njs_arg(args, nargs, 3); + + if (derive_key) { + dalg = njs_key_algorithm(vm, dobject); + if (njs_slow_path(dalg == NULL)) { + goto fail; + } + + value = njs_vm_object_prop(vm, dobject, &string_length, &lvalue); + if (value == NULL) { + njs_vm_type_error(vm, "derivedKeyAlgorithm.length is not provided"); + goto fail; + } + + } else { + dalg = NULL; + value = dobject; + } + + ret = njs_value_to_integer(vm, value, &length); + if (njs_slow_path(ret != NJS_OK)) { + goto fail; + } + + dkey = NULL; + length /= 8; + + if (derive_key) { + ret = njs_key_usage(vm, njs_arg(args, nargs, 5), &usage); + if (njs_slow_path(ret != NJS_OK)) { + goto fail; + } + + if (njs_slow_path(usage & ~dalg->usage)) { + njs_vm_type_error(vm, "unsupported key usage for \"ECDH\" key"); + goto fail; + } + + dkey = njs_mp_zalloc(njs_vm_memory_pool(vm), + sizeof(njs_webcrypto_key_t)); + if (njs_slow_path(dkey == NULL)) { + njs_vm_memory_error(vm); + goto fail; + } + + dkey->alg = dalg; + dkey->usage = usage; + } + + value = njs_vm_object_prop(vm, njs_arg(args, nargs, 1), &string_public, + &lvalue); + if (value == NULL) { + njs_vm_type_error(vm, "ECDH algorithm.public is not provided"); + goto fail; + } + + pkey = njs_vm_external(vm, njs_webcrypto_crypto_key_proto_id, value); + if (njs_slow_path(pkey == NULL)) { + njs_vm_type_error(vm, "algorithm.public is not a CryptoKey object"); + goto fail; + } + + if (njs_slow_path(pkey->alg->type != NJS_ALGORITHM_ECDH)) { + njs_vm_type_error(vm, "algorithm.public is not an ECDH key"); + goto fail; + } + + if (njs_slow_path(key->u.a.curve != pkey->u.a.curve)) { + njs_vm_type_error(vm, "ECDH keys must use the same curve"); + goto fail; + } + + if (!key->u.a.privat) { + njs_vm_type_error(vm, "baseKey must be a private key for ECDH"); + goto fail; + } + + if (pkey->u.a.privat) { + njs_vm_type_error(vm, "algorithm.public must be a public key"); + goto fail; + } + + priv_pkey = key->u.a.pkey; + pub_pkey = pkey->u.a.pkey; + + pctx = EVP_PKEY_CTX_new(priv_pkey, NULL); + if (njs_slow_path(pctx == NULL)) { + njs_webcrypto_error(vm, "EVP_PKEY_CTX_new() failed"); + goto fail; + } + + if (EVP_PKEY_derive_init(pctx) != 1) { + njs_webcrypto_error(vm, "EVP_PKEY_derive_init() failed"); + EVP_PKEY_CTX_free(pctx); + goto fail; + } + + if (EVP_PKEY_derive_set_peer(pctx, pub_pkey) != 1) { + njs_webcrypto_error(vm, "EVP_PKEY_derive_set_peer() failed"); + EVP_PKEY_CTX_free(pctx); + goto fail; + } + + olen = (size_t) length; + if (EVP_PKEY_derive(pctx, NULL, &olen) != 1) { + njs_webcrypto_error(vm, "EVP_PKEY_derive() failed (size query)"); + EVP_PKEY_CTX_free(pctx); + goto fail; + } + + if (njs_slow_path(olen < (size_t) length)) { + njs_vm_type_error(vm, "derived bit length is too small"); + EVP_PKEY_CTX_free(pctx); + goto fail; + } + + k = njs_mp_alloc(njs_vm_memory_pool(vm), olen); + if (njs_slow_path(k == NULL)) { + njs_vm_memory_error(vm); + EVP_PKEY_CTX_free(pctx); + goto fail; + } + + if (EVP_PKEY_derive(pctx, k, &olen) != 1) { + njs_webcrypto_error(vm, "EVP_PKEY_derive() failed"); + EVP_PKEY_CTX_free(pctx); + goto fail; + } + + EVP_PKEY_CTX_free(pctx); + + if (derive_key) { + if (dalg->type == NJS_ALGORITHM_HMAC) { + ret = njs_algorithm_hash(vm, dobject, &dkey->hash); + if (njs_slow_path(ret == NJS_ERROR)) { + goto fail; + } + } + + dkey->extractable = njs_value_bool(njs_arg(args, nargs, 4)); + + dkey->u.s.raw.start = k; + dkey->u.s.raw.length = length; + + ret = njs_vm_external_create(vm, njs_value_arg(&lvalue), + njs_webcrypto_crypto_key_proto_id, + dkey, 0); + } else { + ret = njs_vm_value_array_buffer_set(vm, njs_value_arg(&lvalue), k, + length); + } + + if (njs_slow_path(ret != NJS_OK)) { + goto fail; + } + + return njs_webcrypto_result(vm, &lvalue, NJS_OK, retval); + +fail: + + return njs_webcrypto_result(vm, NULL, NJS_ERROR, retval); +} + + static njs_int_t njs_ext_derive(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t derive_key, njs_value_t *retval) @@ -1454,8 +1637,8 @@ njs_ext_derive(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_value_t *value, *aobject, *dobject; const EVP_MD *md; EVP_PKEY_CTX *pctx; - njs_webcrypto_key_t *key, *dkey; njs_opaque_value_t lvalue; + njs_webcrypto_key_t *key, *dkey; njs_webcrypto_hash_t hash; njs_webcrypto_algorithm_t *alg, *dalg; @@ -1491,6 +1674,10 @@ njs_ext_derive(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, goto fail; } + if (alg->type == NJS_ALGORITHM_ECDH) { + return njs_ext_derive_ecdh(vm, args, nargs, derive_key, key, retval); + } + dobject = njs_arg(args, nargs, 3); if (derive_key) { @@ -1707,7 +1894,6 @@ free: (void) &info; #endif - case NJS_ALGORITHM_ECDH: default: njs_vm_internal_error(vm, "not implemented deriveKey " "algorithm: \"%V\"", njs_algorithm_string(alg)); @@ -2270,6 +2456,7 @@ njs_ext_export_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, case NJS_ALGORITHM_RSA_PSS: case NJS_ALGORITHM_RSA_OAEP: case NJS_ALGORITHM_ECDSA: + case NJS_ALGORITHM_ECDH: ret = njs_export_jwk_asymmetric(vm, key, njs_value_arg(&value)); if (njs_slow_path(ret != NJS_OK)) { goto fail; @@ -2373,7 +2560,9 @@ njs_ext_export_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, case NJS_KEY_FORMAT_RAW: default: - if (key->alg->type == NJS_ALGORITHM_ECDSA) { + if (key->alg->type == NJS_ALGORITHM_ECDSA + || key->alg->type == NJS_ALGORITHM_ECDH) + { ret = njs_export_raw_ec(vm, key, njs_value_arg(&value)); if (njs_slow_path(ret != NJS_OK)) { goto fail; @@ -2540,6 +2729,7 @@ njs_ext_generate_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, break; case NJS_ALGORITHM_ECDSA: + case NJS_ALGORITHM_ECDH: nid = 0; ret = njs_algorithm_curve(vm, aobject, &nid); if (njs_slow_path(ret == NJS_ERROR)) { @@ -2572,7 +2762,14 @@ njs_ext_generate_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, ctx = NULL; key->u.a.privat = 1; - key->usage = NJS_KEY_USAGE_SIGN; + + if (alg->type == NJS_ALGORITHM_ECDSA) { + key->usage = NJS_KEY_USAGE_SIGN; + + } else { + /* ECDH */ + key->usage = NJS_KEY_USAGE_DERIVE_KEY | NJS_KEY_USAGE_DERIVE_BITS; + } keypub = njs_webcrypto_key_alloc(vm, alg, usage, extractable); if (njs_slow_path(keypub == NULL)) { @@ -2586,7 +2783,15 @@ njs_ext_generate_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, keypub->u.a.pkey = key->u.a.pkey; keypub->u.a.curve = key->u.a.curve; - keypub->usage = NJS_KEY_USAGE_VERIFY; + + if (alg->type == NJS_ALGORITHM_ECDSA) { + keypub->usage = NJS_KEY_USAGE_VERIFY; + + } else { + /* ECDH */ + keypub->usage = NJS_KEY_USAGE_DERIVE_KEY + | NJS_KEY_USAGE_DERIVE_BITS; + } ret = njs_vm_external_create(vm, njs_value_arg(&priv), njs_webcrypto_crypto_key_proto_id, key, 0); @@ -3565,7 +3770,17 @@ njs_ext_import_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, goto fail; } - mask = key->u.a.privat ? ~NJS_KEY_USAGE_SIGN : ~NJS_KEY_USAGE_VERIFY; + if (alg->type == NJS_ALGORITHM_ECDSA) { + mask = key->u.a.privat ? ~NJS_KEY_USAGE_SIGN + : ~NJS_KEY_USAGE_VERIFY; + } else { + if (key->u.a.privat) { + mask = ~(NJS_KEY_USAGE_DERIVE_KEY | NJS_KEY_USAGE_DERIVE_BITS); + + } else { + mask = 0; + } + } if (key->usage & mask) { njs_vm_type_error(vm, "key usage mismatch for \"%V\" key", @@ -4598,10 +4813,6 @@ njs_key_algorithm(njs_vm_t *vm, njs_value_t *options) for (e = &njs_webcrypto_alg[0]; e->name.length != 0; e++) { if (njs_strstr_case_eq(&a, &e->name)) { alg = (njs_webcrypto_algorithm_t *) e->value; - if (alg->usage & NJS_KEY_USAGE_UNSUPPORTED) { - njs_vm_type_error(vm, "unsupported algorithm: \"%V\"", &a); - return NULL; - } return alg; } diff --git a/external/qjs_webcrypto_module.c b/external/qjs_webcrypto_module.c index f26b6505..b9c645d9 100644 --- a/external/qjs_webcrypto_module.c +++ b/external/qjs_webcrypto_module.c @@ -115,6 +115,8 @@ static JSValue qjs_cipher_aes_ctr(JSContext *cx, njs_str_t *data, qjs_webcrypto_key_t *key, JSValue options, int encrypt); static JSValue qjs_cipher_aes_cbc(JSContext *cx, njs_str_t *data, qjs_webcrypto_key_t *key, JSValue options, int encrypt); +static JSValue qjs_derive_ecdh(JSContext *cx, JSValueConst *argv, int argc, + int derive_key, qjs_webcrypto_key_t *key); static JSValue qjs_webcrypto_derive(JSContext *cx, JSValueConst this_val, int argc, JSValueConst *argv, int derive_key); static JSValue qjs_webcrypto_digest(JSContext *cx, JSValueConst this_val, @@ -272,9 +274,11 @@ static qjs_webcrypto_entry_t qjs_webcrypto_alg[] = { qjs_webcrypto_algorithm(QJS_ALGORITHM_ECDH, QJS_KEY_USAGE_DERIVE_KEY | QJS_KEY_USAGE_DERIVE_BITS | - QJS_KEY_USAGE_GENERATE_KEY | - QJS_KEY_USAGE_UNSUPPORTED, - QJS_KEY_FORMAT_UNKNOWN, + QJS_KEY_USAGE_GENERATE_KEY, + QJS_KEY_FORMAT_PKCS8 | + QJS_KEY_FORMAT_SPKI | + QJS_KEY_FORMAT_RAW | + QJS_KEY_FORMAT_JWK, 0) }, @@ -430,7 +434,7 @@ static const JSCFunctionListEntry qjs_webcrypto_subtle[] = { JS_CFUNC_DEF("importKey", 5, qjs_webcrypto_import_key), JS_CFUNC_MAGIC_DEF("decrypt", 4, qjs_webcrypto_cipher, 0), JS_CFUNC_MAGIC_DEF("deriveBits", 4, qjs_webcrypto_derive, 0), - JS_CFUNC_MAGIC_DEF("deriveKey", 4, qjs_webcrypto_derive, 1), + JS_CFUNC_MAGIC_DEF("deriveKey", 5, qjs_webcrypto_derive, 1), JS_CFUNC_DEF("digest", 3, qjs_webcrypto_digest), JS_CFUNC_MAGIC_DEF("encrypt", 4, qjs_webcrypto_cipher, 1), JS_CFUNC_DEF("exportKey", 3, qjs_webcrypto_export_key), @@ -1664,6 +1668,190 @@ qjs_export_raw_ec(JSContext *cx, qjs_webcrypto_key_t *key) } +static JSValue +qjs_derive_ecdh(JSContext *cx, JSValueConst *argv, int argc, int derive_key, + qjs_webcrypto_key_t *key) +{ + u_char *k; + size_t olen; + int64_t length; + JSValue ret, result, value, dobject; + unsigned usage; + EVP_PKEY *priv_pkey, *pub_pkey; + EVP_PKEY_CTX *pctx; + qjs_webcrypto_key_t *dkey, *pkey; + qjs_webcrypto_algorithm_t *dalg; + + result = JS_UNDEFINED; + dobject = argv[2]; + + if (derive_key) { + dalg = qjs_key_algorithm(cx, dobject); + if (dalg == NULL) { + goto fail; + } + + value = JS_GetPropertyStr(cx, dobject, "length"); + if (JS_IsException(value)) { + goto fail; + } + + if (JS_IsUndefined(value)) { + JS_ThrowTypeError(cx, "derivedKeyAlgorithm.length is not provided"); + JS_FreeValue(cx, value); + goto fail; + } + + } else { + dalg = NULL; + value = JS_DupValue(cx, dobject); + } + + if (JS_ToInt64(cx, &length, value) < 0) { + JS_FreeValue(cx, value); + goto fail; + } + + JS_FreeValue(cx, value); + + dkey = NULL; + length /= 8; + + if (derive_key) { + ret = qjs_key_usage(cx, argv[4], &usage); + if (JS_IsException(ret)) { + goto fail; + } + + if (usage & ~dalg->usage) { + JS_ThrowTypeError(cx, "unsupported key usage for \"ECDH\" key"); + goto fail; + } + + result = qjs_webcrypto_key_make(cx, dalg, usage, 0); + if (JS_IsException(result)) { + JS_ThrowOutOfMemory(cx); + goto fail; + } + + dkey = JS_GetOpaque(result, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY); + } + + value = JS_GetPropertyStr(cx, argv[0], "public"); + if (JS_IsException(value)) { + goto fail; + } + + if (JS_IsUndefined(value)) { + JS_ThrowTypeError(cx, "ECDH algorithm.public is not provided"); + JS_FreeValue(cx, value); + goto fail; + } + + pkey = JS_GetOpaque(value, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY); + JS_FreeValue(cx, value); + if (pkey == NULL) { + JS_ThrowTypeError(cx, "algorithm.public is not a CryptoKey object"); + goto fail; + } + + if (pkey->alg->type != QJS_ALGORITHM_ECDH) { + JS_ThrowTypeError(cx, "algorithm.public is not an ECDH key"); + goto fail; + } + + if (key->u.a.curve != pkey->u.a.curve) { + JS_ThrowTypeError(cx, "ECDH keys must use the same curve"); + goto fail; + } + + if (!key->u.a.privat) { + JS_ThrowTypeError(cx, "baseKey must be a private key for ECDH"); + goto fail; + } + + if (pkey->u.a.privat) { + JS_ThrowTypeError(cx, "algorithm.public must be a public key"); + goto fail; + } + + priv_pkey = key->u.a.pkey; + pub_pkey = pkey->u.a.pkey; + + pctx = EVP_PKEY_CTX_new(priv_pkey, NULL); + if (pctx == NULL) { + qjs_webcrypto_error(cx, "EVP_PKEY_CTX_new() failed"); + goto fail; + } + + if (EVP_PKEY_derive_init(pctx) != 1) { + qjs_webcrypto_error(cx, "EVP_PKEY_derive_init() failed"); + EVP_PKEY_CTX_free(pctx); + goto fail; + } + + if (EVP_PKEY_derive_set_peer(pctx, pub_pkey) != 1) { + qjs_webcrypto_error(cx, "EVP_PKEY_derive_set_peer() failed"); + EVP_PKEY_CTX_free(pctx); + goto fail; + } + + olen = (size_t) length; + if (EVP_PKEY_derive(pctx, NULL, &olen) != 1) { + qjs_webcrypto_error(cx, "EVP_PKEY_derive() failed (size query)"); + EVP_PKEY_CTX_free(pctx); + goto fail; + } + + if (olen < (size_t) length) { + JS_ThrowTypeError(cx, "derived bit length is too small"); + EVP_PKEY_CTX_free(pctx); + goto fail; + } + + k = js_malloc(cx, olen); + if (k == NULL) { + JS_ThrowOutOfMemory(cx); + EVP_PKEY_CTX_free(pctx); + goto fail; + } + + if (EVP_PKEY_derive(pctx, k, &olen) != 1) { + qjs_webcrypto_error(cx, "EVP_PKEY_derive() failed"); + EVP_PKEY_CTX_free(pctx); + js_free(cx, k); + goto fail; + } + + EVP_PKEY_CTX_free(pctx); + + if (derive_key) { + if (dalg->type == QJS_ALGORITHM_HMAC) { + ret = qjs_algorithm_hash(cx, dobject, &dkey->hash); + if (JS_IsException(ret)) { + js_free(cx, k); + goto fail; + } + } + + dkey->extractable = JS_ToBool(cx, argv[3]); + + dkey->u.s.raw.start = k; + dkey->u.s.raw.length = length; + + } else { + result = qjs_new_array_buffer(cx, k, length); + } + + return qjs_promise_result(cx, result); + +fail: + JS_FreeValue(cx, result); + + return qjs_promise_result(cx, JS_EXCEPTION); +} + + static JSValue qjs_webcrypto_derive(JSContext *cx, JSValueConst this_val, int argc, JSValueConst *argv, int derive_key) @@ -1709,6 +1897,10 @@ qjs_webcrypto_derive(JSContext *cx, JSValueConst this_val, int argc, return JS_EXCEPTION; } + if (alg->type == QJS_ALGORITHM_ECDH) { + return qjs_derive_ecdh(cx, argv, argc, derive_key, key); + } + dobject = argv[2]; if (derive_key) { @@ -1933,7 +2125,6 @@ free: (void) &info; #endif - case QJS_ALGORITHM_ECDH: default: JS_ThrowTypeError(cx, "not implemented deriveKey algorithm: \"%s\"", qjs_algorithm_string(alg)); @@ -2051,6 +2242,7 @@ qjs_webcrypto_export_key(JSContext *cx, JSValueConst this_val, int argc, case QJS_ALGORITHM_RSA_PSS: case QJS_ALGORITHM_RSA_OAEP: case QJS_ALGORITHM_ECDSA: + case QJS_ALGORITHM_ECDH: ret = qjs_export_jwk_asymmetric(cx, key); if (JS_IsException(ret)) { goto fail; @@ -2156,7 +2348,9 @@ qjs_webcrypto_export_key(JSContext *cx, JSValueConst this_val, int argc, case QJS_KEY_FORMAT_RAW: default: - if (key->alg->type == QJS_ALGORITHM_ECDSA) { + if (key->alg->type == QJS_ALGORITHM_ECDSA + || key->alg->type == QJS_ALGORITHM_ECDH) + { ret = qjs_export_raw_ec(cx, key); if (JS_IsException(ret)) { goto fail; @@ -2311,6 +2505,7 @@ qjs_webcrypto_generate_key(JSContext *cx, JSValueConst this_val, break; case QJS_ALGORITHM_ECDSA: + case QJS_ALGORITHM_ECDH: ret = qjs_algorithm_curve(cx, options, &wkey->u.a.curve); if (JS_IsException(ret)) { goto fail; @@ -2342,7 +2537,14 @@ qjs_webcrypto_generate_key(JSContext *cx, JSValueConst this_val, ctx = NULL; wkey->u.a.privat = 1; - wkey->usage = QJS_KEY_USAGE_SIGN; + + if (alg->type == QJS_ALGORITHM_ECDSA) { + wkey->usage = QJS_KEY_USAGE_SIGN; + + } else { + /* ECDH */ + wkey->usage = QJS_KEY_USAGE_DERIVE_KEY | QJS_KEY_USAGE_DERIVE_BITS; + } keypub = qjs_webcrypto_key_make(cx, alg, usage, extractable); if (JS_IsException(keypub)) { @@ -2358,7 +2560,15 @@ qjs_webcrypto_generate_key(JSContext *cx, JSValueConst this_val, wkeypub->u.a.pkey = wkey->u.a.pkey; wkeypub->u.a.curve = wkey->u.a.curve; - wkeypub->usage = QJS_KEY_USAGE_VERIFY; + + if (alg->type == QJS_ALGORITHM_ECDSA) { + wkeypub->usage = QJS_KEY_USAGE_VERIFY; + + } else { + /* ECDH */ + wkeypub->usage = QJS_KEY_USAGE_DERIVE_KEY + | QJS_KEY_USAGE_DERIVE_BITS; + } obj = JS_NewObject(cx); if (JS_IsException(obj)) { @@ -3408,7 +3618,17 @@ qjs_webcrypto_import_key(JSContext *cx, JSValueConst this_val, int argc, goto fail; } - mask = wkey->u.a.privat ? ~QJS_KEY_USAGE_SIGN : ~QJS_KEY_USAGE_VERIFY; + if (alg->type == QJS_ALGORITHM_ECDSA) { + mask = wkey->u.a.privat ? ~QJS_KEY_USAGE_SIGN + : ~QJS_KEY_USAGE_VERIFY; + } else { + if (wkey->u.a.privat) { + mask = ~(QJS_KEY_USAGE_DERIVE_KEY | QJS_KEY_USAGE_DERIVE_BITS); + + } else { + mask = 0; + } + } if (wkey->usage & mask) { JS_ThrowTypeError(cx, "key usage mismatch for \"%s\" key", @@ -4369,13 +4589,6 @@ qjs_key_algorithm(JSContext *cx, JSValue options) for (e = &qjs_webcrypto_alg[0]; e->name.length != 0; e++) { if (njs_strstr_case_eq(&a, &e->name)) { alg = (qjs_webcrypto_algorithm_t *) e->value; - if (alg->usage & QJS_KEY_USAGE_UNSUPPORTED) { - JS_ThrowTypeError(cx, "unsupported algorithm: \"%.*s\"", - (int) a.length, a.start); - JS_FreeCString(cx, (char *) a.start); - return NULL; - } - JS_FreeCString(cx, (char *) a.start); return alg; } diff --git a/test/harness/webCryptoUtils.js b/test/harness/webCryptoUtils.js index d403f39a..c37fb489 100644 --- a/test/harness/webCryptoUtils.js +++ b/test/harness/webCryptoUtils.js @@ -19,3 +19,19 @@ function load_jwk(data) { return data; } + +function compareUsage(a, b) { + a.sort(); + b.sort(); + + if (b.length !== a.length) { + return false; + } + + for (var i = 0; i < a.length; i++) { + if (b[i] !== a[i]) { + return false; + } + } + return true; +} diff --git a/test/webcrypto/derive_ecdh.t.mjs b/test/webcrypto/derive_ecdh.t.mjs new file mode 100644 index 00000000..50c1925e --- /dev/null +++ b/test/webcrypto/derive_ecdh.t.mjs @@ -0,0 +1,188 @@ +/*--- +includes: [compatFs.js, compatBuffer.js, compatWebcrypto.js, webCryptoUtils.js, runTsuite.js] +flags: [async] +---*/ + +async function testDeriveBits(params) { + let aliceKeyPair = await load_key_pair(params.pair[0]); + let bobKeyPair = await load_key_pair(params.pair[1]); + + let ecdhParams = { name: "ECDH", public: bobKeyPair.publicKey }; + + let result = await crypto.subtle.deriveBits(ecdhParams, aliceKeyPair.privateKey, params.length); + result = Buffer.from(result).toString('base64url'); + + if (result !== params.expected) { + throw Error(`ECDH deriveBits failed expected: "${params.expected}" vs "${result}"`); + } + + let ecdhParamsReverse = { name: "ECDH", public: aliceKeyPair.publicKey }; + + let secondResult = await crypto.subtle.deriveBits(ecdhParamsReverse, bobKeyPair.privateKey, params.length); + secondResult = Buffer.from(secondResult).toString('base64url'); + + if (secondResult !== params.expected) { + throw Error(`ECDH reverse deriveBits failed expected: "${params.expected}" vs "${secondResult}"`); + } + + return "SUCCESS"; +} + +function deriveCurveFromName(name) { + if (/secp384r1/.test(name)) { + return "P-384"; + } + + if (/secp521r1/.test(name)) { + return "P-521"; + } + + return "P-256"; +} + +async function load_key_pair(name) { + let pair = {}; + let pem = fs.readFileSync(`test/webcrypto/${name}.pkcs8`); + let key = pem_to_der(pem, "private"); + + pair.privateKey = await crypto.subtle.importKey("pkcs8", key, + { name: "ECDH", namedCurve: deriveCurveFromName(name) }, + true, ["deriveBits", "deriveKey"]); + + pem = fs.readFileSync(`test/webcrypto/${name}.spki`); + key = pem_to_der(pem, "public"); + pair.publicKey = await crypto.subtle.importKey("spki", key, + { name: "ECDH", namedCurve: deriveCurveFromName(name) }, + true, []); + + return pair; +} + +let ecdh_bits_tsuite = { + name: "ECDH-DeriveBits", + skip: () => (!has_buffer() || !has_webcrypto()), + T: testDeriveBits, + opts: { + pair: ['ec', 'ec2'], + length: 256 + }, + tests: [ + { expected: "mMAGhQ_1Wr3u6Y6VyzVuolCA7x8RM-15e73laLJMUok" }, + { pair: ['ec_secp384r1', 'ec2_secp384r1'], + expected: "4OmeRzZZ53eCgn09zI2TumH4n4Zp-nfHsfZTOBEu8Hg" }, + { pair: ['ec_secp384r1', 'ec2_secp384r1'], + length: 384, + expected: "4OmeRzZZ53eCgn09zI2TumH4n4Zp-nfHsfZTOBEu8HjB0GF2YrOw5dCUgavKZaNR" }, + { pair: ['ec_secp521r1', 'ec2_secp521r1'], + length: 528, + expected: "ATBls20ukLQI7AJQ6LRnyD6wLDR_FDmBoAdVX5_DB_bMDe_uYMjN-jQqPTkGNIo6NOqmXMX9KNQ-AqL8aPjySMMm" }, + ] +}; + +async function testDeriveKey(params) { + let aliceKeyPair = await load_key_pair(params.pair[0]); + let bobKeyPair = await load_key_pair(params.pair[1]); + let eveKeyPair = await crypto.subtle.generateKey({ name: "ECDH", namedCurve: deriveCurveFromName(params.pair[0]) }, + true, ["deriveKey", "deriveBits"]); + + let ecdhParamsAlice = { name: "ECDH", public: bobKeyPair.publicKey }; + let ecdhParamsBob = { name: "ECDH", public: aliceKeyPair.publicKey }; + let ecdhParamsEve = { name: "ECDH", public: eveKeyPair.publicKey }; + + let derivedAlgorithm = { name: params.derivedAlgorithm.name }; + + derivedAlgorithm.length = params.derivedAlgorithm.length; + derivedAlgorithm.hash = params.derivedAlgorithm.hash; + + let aliceDerivedKey = await crypto.subtle.deriveKey(ecdhParamsAlice, aliceKeyPair.privateKey, + derivedAlgorithm, params.extractable, params.usage); + + if (aliceDerivedKey.extractable !== params.extractable) { + throw Error(`ECDH extractable test failed: ${params.extractable} vs ${aliceDerivedKey.extractable}`); + } + + if (compareUsage(aliceDerivedKey.usages, params.usage) !== true) { + throw Error(`ECDH usage test failed: ${params.usage} vs ${aliceDerivedKey.usages}`); + } + + let bobDerivedKey = await crypto.subtle.deriveKey(ecdhParamsBob, bobKeyPair.privateKey, + derivedAlgorithm, params.extractable, params.usage); + + let eveDerivedKey = await crypto.subtle.deriveKey(ecdhParamsEve, eveKeyPair.privateKey, + derivedAlgorithm, params.extractable, params.usage); + + if (params.extractable && + (params.derivedAlgorithm.name === "AES-GCM" + || params.derivedAlgorithm.name === "AES-CBC" + || params.derivedAlgorithm.name === "AES-CTR" + || params.derivedAlgorithm.name === "HMAC")) + { + const aliceRawKey = await crypto.subtle.exportKey("raw", aliceDerivedKey); + const bobRawKey = await crypto.subtle.exportKey("raw", bobDerivedKey); + const eveRawKey = await crypto.subtle.exportKey("raw", eveDerivedKey); + + const aliceKeyData = Buffer.from(aliceRawKey).toString("base64url"); + const bobKeyData = Buffer.from(bobRawKey).toString("base64url"); + const eveKeyData = Buffer.from(eveRawKey).toString("base64url"); + + if (aliceKeyData !== bobKeyData) { + throw Error(`ECDH key symmetry test failed: keys are not equal`); + } + + if (aliceKeyData !== params.expected) { + throw Error(`ECDH key symmetry test failed: expected: "${params.expected}" vs "${aliceKeyData}"`); + } + + if (aliceKeyData === eveKeyData) { + throw Error(`ECDH key symmetry test failed: keys are equal`); + } + } + + return "SUCCESS"; +} + +let ecdh_key_tsuite = { + name: "ECDH-DeriveKey", + skip: () => (!has_buffer() || !has_webcrypto()), + T: testDeriveKey, + opts: { + pair: ['ec', 'ec2'], + extractable: true, + derivedAlgorithm: { + name: "AES-GCM", + length: 256 + }, + expected: "mMAGhQ_1Wr3u6Y6VyzVuolCA7x8RM-15e73laLJMUok", + usage: ["encrypt", "decrypt"] + }, + tests: [ + { }, + { extractable: false }, + { derivedAlgorithm: { name: "AES-CBC", length: 256 } }, + { derivedAlgorithm: { name: "AES-CTR", length: 256 } }, + { derivedAlgorithm: { name: "AES-GCM", length: 256 } }, + { derivedAlgorithm: { name: "HMAC", hash: "SHA-256", length: 256 }, + usage: ["sign", "verify"] }, + + { pair: ['ec_secp384r1', 'ec2_secp384r1'], + expected: "4OmeRzZZ53eCgn09zI2TumH4n4Zp-nfHsfZTOBEu8Hg" }, + { pair: ['ec_secp384r1', 'ec2_secp384r1'], extractable: false }, + + { pair: ['ec_secp521r1', 'ec_secp384r1'], + exception: "TypeError: ECDH keys must use the same curve" }, + + { pair: ['ec_secp521r1', 'ec2_secp521r1'], + derivedAlgorithm: { name: "AES-GCM", length: 128 }, + expected: "ATBls20ukLQI7AJQ6LRnyA" }, + { pair: ['ec_secp521r1', 'ec2_secp521r1'], + derivedAlgorithm: { name: "HMAC", hash: "SHA-384", length: 512 }, + expected: "ATBls20ukLQI7AJQ6LRnyD6wLDR_FDmBoAdVX5_DB_bMDe_uYMjN-jQqPTkGNIo6NOqmXMX9KNQ-AqL8aPjySA", + usage: ["sign", "verify"] } + ] +}; + +run([ + ecdh_bits_tsuite, + ecdh_key_tsuite, +]) +.then($DONE, $DONE); diff --git a/test/webcrypto/ec2_secp384r1.pkcs8 b/test/webcrypto/ec2_secp384r1.pkcs8 new file mode 100644 index 00000000..c94fdf9f --- /dev/null +++ b/test/webcrypto/ec2_secp384r1.pkcs8 @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDM3LQ/iPLxtGh4I0IH +tkE14NPOSBWWxa0C5Dt7KFA2T5Goh/C9hulx4waSJtJgpsmhZANiAAQr0yzNebjy +oATeQbsSQGcBgC6Vm31MqarylkteLBxC+tWVgrCjxps/ZN9l+wOBo6kceuGrmoi6 +YJYkRAZk9QOCODFou+VyW741sQRtenfkCb904Iy83tLXw9CCOZ3M5tM= +-----END PRIVATE KEY----- diff --git a/test/webcrypto/ec2_secp384r1.spki b/test/webcrypto/ec2_secp384r1.spki new file mode 100644 index 00000000..52379e77 --- /dev/null +++ b/test/webcrypto/ec2_secp384r1.spki @@ -0,0 +1,5 @@ +-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEK9MszXm48qAE3kG7EkBnAYAulZt9TKmq +8pZLXiwcQvrVlYKwo8abP2TfZfsDgaOpHHrhq5qIumCWJEQGZPUDgjgxaLvlclu+ +NbEEbXp35Am/dOCMvN7S18PQgjmdzObT +-----END PUBLIC KEY----- diff --git a/test/webcrypto/ec2_secp521r1.pkcs8 b/test/webcrypto/ec2_secp521r1.pkcs8 new file mode 100644 index 00000000..8d6000cf --- /dev/null +++ b/test/webcrypto/ec2_secp521r1.pkcs8 @@ -0,0 +1,8 @@ +-----BEGIN PRIVATE KEY----- +MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIBK+Wq6/RhJ0n1s/+r +qcwVBZYo6OFeOpwlmvFfrrsRwxWnptigR6kKXm1/w7AX7eHFuc+kyVI5KXu7hJUP +S9sAwcmhgYkDgYYABAE5InvhsngiOkoFhRcSDgxmFMjWMZG6BAw57Cwz2ar9VoyY +GYIJtw976kc8Yz+NPz6BNJpfo2wv6YnyrV6CEFqbtQAXI5DY7kk1qsaawgZcFoaH +ngIII80o6Eo9OMwsVzTUmkkAmWGySwvqRge3eVMJTkPjY1AxoP5aOJr+qcDRbZLr +0A== +-----END PRIVATE KEY----- diff --git a/test/webcrypto/ec2_secp521r1.spki b/test/webcrypto/ec2_secp521r1.spki new file mode 100644 index 00000000..8834d890 --- /dev/null +++ b/test/webcrypto/ec2_secp521r1.spki @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBOSJ74bJ4IjpKBYUXEg4MZhTI1jGR +ugQMOewsM9mq/VaMmBmCCbcPe+pHPGM/jT8+gTSaX6NsL+mJ8q1eghBam7UAFyOQ +2O5JNarGmsIGXBaGh54CCCPNKOhKPTjMLFc01JpJAJlhsksL6kYHt3lTCU5D42NQ +MaD+Wjia/qnA0W2S69A= +-----END PUBLIC KEY----- diff --git a/test/webcrypto/ec_secp384r1.pkcs8 b/test/webcrypto/ec_secp384r1.pkcs8 new file mode 100644 index 00000000..17f8d319 --- /dev/null +++ b/test/webcrypto/ec_secp384r1.pkcs8 @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDA50z1r3E3NARroawH9 +eAXuoQPu1xVbcRDZ0bTNgdOHDh2E0uW5fybZnAYVjbbEPxuhZANiAATu1zCeNJ+n +F65Wjdoltr1AnHDxn+k+9KdQeXd//JMWaBReirIcmU40qvSzLmQtPiDoMHFpMf11 +UCjSMLA8sVNtEwD0bdUmYfoGBNgwzk/4y5vTiyCNSozso3xx+4/WuGs= +-----END PRIVATE KEY----- diff --git a/test/webcrypto/ec_secp384r1.spki b/test/webcrypto/ec_secp384r1.spki new file mode 100644 index 00000000..820adad8 --- /dev/null +++ b/test/webcrypto/ec_secp384r1.spki @@ -0,0 +1,5 @@ +-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE7tcwnjSfpxeuVo3aJba9QJxw8Z/pPvSn +UHl3f/yTFmgUXoqyHJlONKr0sy5kLT4g6DBxaTH9dVAo0jCwPLFTbRMA9G3VJmH6 +BgTYMM5P+Mub04sgjUqM7KN8cfuP1rhr +-----END PUBLIC KEY----- diff --git a/test/webcrypto/ec_secp521r1.pkcs8 b/test/webcrypto/ec_secp521r1.pkcs8 new file mode 100644 index 00000000..7a6fe728 --- /dev/null +++ b/test/webcrypto/ec_secp521r1.pkcs8 @@ -0,0 +1,8 @@ +-----BEGIN PRIVATE KEY----- +MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAEGh8E2g1TbnN0xzm +7nKGSvDbSVZHaA+XQEuTbhklfRcMJH8X8oqOJRzl4m9nIGmXy6TGPwIMlA6maRwB +PSEGqsChgYkDgYYABADSOIb4Rpa/7WDON8vDH6DPTR9gOFcFkI2lOa68MEdE7pF1 +m57cuJ0X2qTlFS6YuurbpiF6h4cltB1pM3eGQXKNcgD6stL3WjMjpC8Phv9Q391Z +2E0ezlX0nDtFPIXAwmxptIC2U7WxHRQqkQwgJyq6xklp3vkD/eFeOSi/j0qvKrlD +SA== +-----END PRIVATE KEY----- diff --git a/test/webcrypto/ec_secp521r1.spki b/test/webcrypto/ec_secp521r1.spki new file mode 100644 index 00000000..9bd02998 --- /dev/null +++ b/test/webcrypto/ec_secp521r1.spki @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQA0jiG+EaWv+1gzjfLwx+gz00fYDhX +BZCNpTmuvDBHRO6RdZue3LidF9qk5RUumLrq26YheoeHJbQdaTN3hkFyjXIA+rLS +91ozI6QvD4b/UN/dWdhNHs5V9Jw7RTyFwMJsabSAtlO1sR0UKpEMICcqusZJad75 +A/3hXjkov49Kryq5Q0g= +-----END PUBLIC KEY----- diff --git a/test/webcrypto/export.t.mjs b/test/webcrypto/export.t.mjs index 81b549df..58fbf8bb 100644 --- a/test/webcrypto/export.t.mjs +++ b/test/webcrypto/export.t.mjs @@ -375,17 +375,18 @@ let ec_tsuite = { key: { fmt: "spki", key: "ec.spki", alg: { name: "ECDSA", namedCurve: "P-256" }, - extractable: true, - usage: [ "verify" ] }, + extractable: true }, export: { fmt: "jwk" }, expected: { ext: true, kty: "EC" }, }, tests: [ - { expected: { key_ops: [ "verify" ], + { key: { usage: [ "verify" ] }, + expected: { key_ops: [ "verify" ], x: "cUUsZRGuVYReGiTQl9MqR7ir6dZIgw-8pM-F0phJWdw", y: "4Nzn_7uNz0AA3U4fhpfVxSD4U5QciGyEoM4r3jC7bjI", crv: "P-256" } }, + { key: { alg: { name: "ECDH", namedCurve: "P-256" }, usage: [ ] } }, { key: { fmt: "pkcs8", key: "ec.pkcs8", usage: [ "sign" ] }, expected: { key_ops: [ "sign" ], x: "cUUsZRGuVYReGiTQl9MqR7ir6dZIgw-8pM-F0phJWdw", @@ -397,7 +398,22 @@ let ec_tsuite = { expected: "ArrayBuffer:MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgE2sW0_4a3QXaSTJ0JKbSUbieKTD1UFtr7i_2CuetP6ChRANCAARxRSxlEa5VhF4aJNCX0ypHuKvp1kiDD7ykz4XSmElZ3ODc5_-7jc9AAN1OH4aX1cUg-FOUHIhshKDOK94wu24y" }, { export: { fmt: "pkcs8" }, exception: "TypeError: public key of \"ECDSA\" cannot be exported as PKCS8" }, - { export: { fmt: "raw" }, + { key: { alg: { name: "ECDH", namedCurve: "P-256" }, + fmt: "pkcs8", key: "ec.pkcs8", + usage: [ "deriveKey" ] }, + export: { fmt: "pkcs8" }, + expected: "ArrayBuffer:MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgE2sW0_4a3QXaSTJ0JKbSUbieKTD1UFtr7i_2CuetP6ChRANCAARxRSxlEa5VhF4aJNCX0ypHuKvp1kiDD7ykz4XSmElZ3ODc5_-7jc9AAN1OH4aX1cUg-FOUHIhshKDOK94wu24y" }, + { key: { usage: [ "verify" ] }, + export: { fmt: "spki" }, + expected: "ArrayBuffer:MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcUUsZRGuVYReGiTQl9MqR7ir6dZIgw-8pM-F0phJWdzg3Of_u43PQADdTh-Gl9XFIPhTlByIbISgziveMLtuMg"}, + { key: { alg: { name: "ECDH", namedCurve: "P-256" }, usage: [ ] }, + export: { fmt: "spki" }, + expected: "ArrayBuffer:MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcUUsZRGuVYReGiTQl9MqR7ir6dZIgw-8pM-F0phJWdzg3Of_u43PQADdTh-Gl9XFIPhTlByIbISgziveMLtuMg"}, + { key: { usage: [ "verify" ] }, + export: { fmt: "raw" }, + expected: "ArrayBuffer:BHFFLGURrlWEXhok0JfTKke4q-nWSIMPvKTPhdKYSVnc4Nzn_7uNz0AA3U4fhpfVxSD4U5QciGyEoM4r3jC7bjI" }, + { key: { alg: { name: "ECDH", namedCurve: "P-256" }, usage: [ ] }, + export: { fmt: "raw" }, expected: "ArrayBuffer:BHFFLGURrlWEXhok0JfTKke4q-nWSIMPvKTPhdKYSVnc4Nzn_7uNz0AA3U4fhpfVxSD4U5QciGyEoM4r3jC7bjI" }, { key: { fmt: "pkcs8", key: "ec.pkcs8", usage: [ "sign" ] }, export: { fmt: "raw" }, From noreply at nginx.com Fri May 16 19:16:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 16 May 2025 19:16:02 +0000 (UTC) Subject: [njs] WebCrypto: making SHA-256 the default hash algorithm. Message-ID: <20250516191602.BA1793F507@pubserv1.nginx> details: https://github.com/nginx/njs/commit/637fc26eac8622ccf8c73cfa4604e9afe54c3f34 branches: master commit: 637fc26eac8622ccf8c73cfa4604e9afe54c3f34 user: Dmitry Volyntsev date: Thu, 8 May 2025 18:03:21 -0700 description: WebCrypto: making SHA-256 the default hash algorithm. --- external/njs_webcrypto_module.c | 4 ++-- external/qjs_webcrypto_module.c | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/external/njs_webcrypto_module.c b/external/njs_webcrypto_module.c index dcca91ce..8cc172cc 100644 --- a/external/njs_webcrypto_module.c +++ b/external/njs_webcrypto_module.c @@ -4642,8 +4642,8 @@ njs_algorithm_hash(njs_vm_t *vm, njs_value_t *options, if (njs_value_is_object(options)) { val = njs_vm_object_prop(vm, options, &string_hash, &value); - if (njs_slow_path(val == NULL)) { - njs_value_undefined_set(njs_value_arg(&value)); + if (val == NULL) { + return NJS_HASH_SHA256; } } else { diff --git a/external/qjs_webcrypto_module.c b/external/qjs_webcrypto_module.c index 937f96c3..f26b6505 100644 --- a/external/qjs_webcrypto_module.c +++ b/external/qjs_webcrypto_module.c @@ -4437,6 +4437,11 @@ qjs_algorithm_hash(JSContext *cx, JSValue options, qjs_webcrypto_hash_t *hash) if (JS_IsObject(options)) { v = JS_GetPropertyStr(cx, options, "hash"); + if (JS_IsUndefined(v)) { + *hash = QJS_HASH_SHA256; + return JS_UNDEFINED; + } + if (JS_IsException(v)) { return JS_EXCEPTION; } From noreply at nginx.com Fri May 16 19:16:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 16 May 2025 19:16:02 +0000 (UTC) Subject: [njs] WebCrypto: improved working with curve constants. Message-ID: <20250516191602.C7B8E3F50C@pubserv1.nginx> details: https://github.com/nginx/njs/commit/f3bc5338cb9df269ca6573cf46b5d39721803cf2 branches: master commit: f3bc5338cb9df269ca6573cf46b5d39721803cf2 user: Dmitry Volyntsev date: Tue, 13 May 2025 19:08:16 -0700 description: WebCrypto: improved working with curve constants. In crypto.subtle.generateKey(). --- external/njs_webcrypto_module.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/external/njs_webcrypto_module.c b/external/njs_webcrypto_module.c index d9b05d09..8d7f78e7 100644 --- a/external/njs_webcrypto_module.c +++ b/external/njs_webcrypto_module.c @@ -2593,7 +2593,6 @@ static njs_int_t njs_ext_generate_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval) { - int nid; unsigned usage; njs_int_t ret; njs_bool_t extractable; @@ -2730,8 +2729,7 @@ njs_ext_generate_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, case NJS_ALGORITHM_ECDSA: case NJS_ALGORITHM_ECDH: - nid = 0; - ret = njs_algorithm_curve(vm, aobject, &nid); + ret = njs_algorithm_curve(vm, aobject, &key->u.a.curve); if (njs_slow_path(ret == NJS_ERROR)) { goto fail; } @@ -2747,7 +2745,7 @@ njs_ext_generate_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, goto fail; } - if (EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx, nid) <= 0) { + if (EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx, key->u.a.curve) <= 0) { njs_webcrypto_error(vm, "EVP_PKEY_CTX_set_ec_paramgen_curve_nid() " "failed"); goto fail; From noreply at nginx.com Tue May 20 01:55:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Tue, 20 May 2025 01:55:02 +0000 (UTC) Subject: [njs] WebCrypto: fixed issue introduced in 637fc26e. Message-ID: <20250520015502.2878E47771@pubserv1.nginx> details: https://github.com/nginx/njs/commit/95ea07b6553227638c9727f52dae99e94b08962a branches: master commit: 95ea07b6553227638c9727f52dae99e94b08962a user: Dmitry Volyntsev date: Mon, 19 May 2025 14:58:12 -0700 description: WebCrypto: fixed issue introduced in 637fc26e. Found by Clang static analyzer. --- external/njs_webcrypto_module.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/external/njs_webcrypto_module.c b/external/njs_webcrypto_module.c index 8d7f78e7..b9a74353 100644 --- a/external/njs_webcrypto_module.c +++ b/external/njs_webcrypto_module.c @@ -4852,7 +4852,8 @@ njs_algorithm_hash(njs_vm_t *vm, njs_value_t *options, if (njs_value_is_object(options)) { val = njs_vm_object_prop(vm, options, &string_hash, &value); if (val == NULL) { - return NJS_HASH_SHA256; + *hash = NJS_HASH_SHA256; + return NJS_OK; } } else {