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 { From noreply at nginx.com Mon May 26 13:57:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Mon, 26 May 2025 13:57:02 +0000 (UTC) Subject: [nginx] SSL: support loading keys via OSSL_STORE. Message-ID: <20250526135702.9DA574900E@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/0fdbfc1ff45adb8e98e71004e5d147987e7d8974 branches: master commit: 0fdbfc1ff45adb8e98e71004e5d147987e7d8974 user: Aleksei Bavshin date: Mon, 16 Dec 2024 17:56:45 -0800 description: SSL: support loading keys via OSSL_STORE. A new "store:..." prefix for the "ssl_certificate_key" directive allows loading keys via the OSSL_STORE API. The change is required to support hardware backed keys in OpenSSL 3.x using the new "provider(7ossl)" modules, such as "pkcs11-provider". While the engine API is present in 3.x, some operating systems (notably, RHEL10) have already disabled it in their builds of OpenSSL. Related: https://trac.nginx.org/nginx/ticket/2449 --- src/event/ngx_event_openssl_cache.c | 86 ++++++++++++++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 5 deletions(-) diff --git a/src/event/ngx_event_openssl_cache.c b/src/event/ngx_event_openssl_cache.c index d62b4c430..cbb05892f 100644 --- a/src/event/ngx_event_openssl_cache.c +++ b/src/event/ngx_event_openssl_cache.c @@ -8,10 +8,16 @@ #include #include +#ifdef ERR_R_OSSL_STORE_LIB +#include +#include +#endif + #define NGX_SSL_CACHE_PATH 0 #define NGX_SSL_CACHE_DATA 1 #define NGX_SSL_CACHE_ENGINE 2 +#define NGX_SSL_CACHE_STORE 3 #define NGX_SSL_CACHE_DISABLED (ngx_array_t *) (uintptr_t) -1 @@ -444,6 +450,11 @@ ngx_ssl_cache_init_key(ngx_pool_t *pool, ngx_uint_t index, ngx_str_t *path, { id->type = NGX_SSL_CACHE_ENGINE; + } else if (index == NGX_SSL_CACHE_PKEY + && ngx_strncmp(path->data, "store:", sizeof("store:") - 1) == 0) + { + id->type = NGX_SSL_CACHE_STORE; + } else { if (ngx_get_full_name(pool, (ngx_str_t *) &ngx_cycle->conf_prefix, path) != NGX_OK) @@ -714,11 +725,6 @@ ngx_ssl_cache_pkey_create(ngx_ssl_cache_key_t *id, char **err, void *data) #endif } - bio = ngx_ssl_cache_create_bio(id, err); - if (bio == NULL) { - return NULL; - } - cb_data.encrypted = 0; if (*passwords) { @@ -734,6 +740,76 @@ ngx_ssl_cache_pkey_create(ngx_ssl_cache_key_t *id, char **err, void *data) cb = NULL; } + if (id->type == NGX_SSL_CACHE_STORE) { + +#ifdef ERR_R_OSSL_STORE_LIB + + u_char *uri; + UI_METHOD *method; + OSSL_STORE_CTX *store; + OSSL_STORE_INFO *info; + + method = (cb != NULL) ? UI_UTIL_wrap_read_pem_callback(cb, 0) : NULL; + uri = id->data + sizeof("store:") - 1; + + store = OSSL_STORE_open((char *) uri, method, pwd, NULL, NULL); + + if (store == NULL) { + *err = "OSSL_STORE_open() failed"; + + if (method != NULL) { + UI_destroy_method(method); + } + + return NULL; + } + + pkey = NULL; + + while (pkey == NULL && !OSSL_STORE_eof(store)) { + info = OSSL_STORE_load(store); + + if (info == NULL) { + continue; + } + + if (OSSL_STORE_INFO_get_type(info) == OSSL_STORE_INFO_PKEY) { + pkey = OSSL_STORE_INFO_get1_PKEY(info); + } + + OSSL_STORE_INFO_free(info); + } + + OSSL_STORE_close(store); + + if (method != NULL) { + UI_destroy_method(method); + } + + if (pkey == NULL) { + *err = "OSSL_STORE_load() failed"; + return NULL; + } + + if (cb_data.encrypted) { + *passwords = NGX_SSL_CACHE_DISABLED; + } + + return pkey; + +#else + + *err = "loading \"store:...\" certificate keys is not supported"; + return NULL; + +#endif + } + + bio = ngx_ssl_cache_create_bio(id, err); + if (bio == NULL) { + return NULL; + } + for ( ;; ) { pkey = PEM_read_bio_PrivateKey(bio, NULL, cb, pwd); From noreply at nginx.com Mon May 26 13:57:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Mon, 26 May 2025 13:57:02 +0000 (UTC) Subject: [nginx] SSL: disabled UI console prompts from worker processes. Message-ID: <20250526135702.A10074900F@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/3d5889a3ee41a282bad54d9c0d3662dba9f52c1b branches: master commit: 3d5889a3ee41a282bad54d9c0d3662dba9f52c1b user: Aleksei Bavshin date: Fri, 17 Jan 2025 12:24:08 -0800 description: SSL: disabled UI console prompts from worker processes. Certain providers may attempt to reload the key on the first use after a fork. Such attempt would require re-prompting the pin, and this time we are not able to pass the password callback. While it is addressable with configuration for a specific provider, it would be prudent to ensure that no such prompts could block worker processes by setting the default UI method. UI_null() first appeared in 1.1.1 along with the OSSL_STORE, so it is safe to assume the same set of guards. --- src/event/ngx_event_openssl_cache.c | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/event/ngx_event_openssl_cache.c b/src/event/ngx_event_openssl_cache.c index cbb05892f..18efc73d0 100644 --- a/src/event/ngx_event_openssl_cache.c +++ b/src/event/ngx_event_openssl_cache.c @@ -122,6 +122,8 @@ static void ngx_ssl_cache_node_insert(ngx_rbtree_node_t *temp, static void ngx_ssl_cache_node_free(ngx_rbtree_t *rbtree, ngx_ssl_cache_node_t *cn); +static ngx_int_t ngx_openssl_cache_init_worker(ngx_cycle_t *cycle); + static ngx_command_t ngx_openssl_cache_commands[] = { @@ -150,7 +152,7 @@ ngx_module_t ngx_openssl_cache_module = { NGX_CORE_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ - NULL, /* init process */ + ngx_openssl_cache_init_worker, /* init process */ NULL, /* init thread */ NULL, /* exit thread */ NULL, /* exit process */ @@ -1233,3 +1235,20 @@ ngx_ssl_cache_node_insert(ngx_rbtree_node_t *temp, node->right = sentinel; ngx_rbt_red(node); } + + +static ngx_int_t +ngx_openssl_cache_init_worker(ngx_cycle_t *cycle) +{ +#ifdef ERR_R_OSSL_STORE_LIB + + if (ngx_process != NGX_PROCESS_WORKER) { + return NGX_OK; + } + + UI_set_default_method(UI_null()); + +#endif + + return NGX_OK; +} From noreply at nginx.com Fri May 23 11:01:03 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 23 May 2025 11:01:03 +0000 (UTC) Subject: [nginx] QUIC: defined SSL API macros in a single place. Message-ID: <20250523110103.1597E48FED@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/e561f7dbcfc27f5f648e5151de0796e691cbc1b0 branches: master commit: e561f7dbcfc27f5f648e5151de0796e691cbc1b0 user: Sergey Kandaurov date: Wed, 21 May 2025 03:54:45 +0400 description: QUIC: defined SSL API macros in a single place. All definitions now set in ngx_event_quic.h, this includes moving NGX_QUIC_OPENSSL_COMPAT from autotests to compile time. Further, to improve code readability, a new NGX_QUIC_QUICTLS_API macro is used for QuicTLS that provides old BoringSSL QUIC API. --- auto/lib/openssl/conf | 3 --- src/event/quic/ngx_event_quic.c | 4 ++-- src/event/quic/ngx_event_quic.h | 12 ++++++++++++ src/event/quic/ngx_event_quic_openssl_compat.h | 8 -------- src/event/quic/ngx_event_quic_ssl.c | 13 +++---------- 5 files changed, 17 insertions(+), 23 deletions(-) diff --git a/auto/lib/openssl/conf b/auto/lib/openssl/conf index fdf430dff..f4b00ebd6 100644 --- a/auto/lib/openssl/conf +++ b/auto/lib/openssl/conf @@ -12,7 +12,6 @@ if [ $OPENSSL != NONE ]; then if [ $USE_OPENSSL_QUIC = YES ]; then have=NGX_QUIC . auto/have - have=NGX_QUIC_OPENSSL_COMPAT . auto/have fi case "$CC" in @@ -154,8 +153,6 @@ else . auto/feature if [ $ngx_found = no ]; then - have=NGX_QUIC_OPENSSL_COMPAT . auto/have - ngx_feature="OpenSSL QUIC compatibility" ngx_feature_test="SSL_CTX_add_custom_ext(NULL, 0, 0, NULL, NULL, NULL, NULL, NULL)" diff --git a/src/event/quic/ngx_event_quic.c b/src/event/quic/ngx_event_quic.c index 9f968d5fb..4f2e50240 100644 --- a/src/event/quic/ngx_event_quic.c +++ b/src/event/quic/ngx_event_quic.c @@ -973,8 +973,8 @@ ngx_quic_handle_payload(ngx_connection_t *c, ngx_quic_header_t *pkt) return NGX_DECLINED; } -#if !defined (OPENSSL_IS_BORINGSSL) - /* OpenSSL provides read keys for an application level before it's ready */ +#if (NGX_QUIC_QUICTLS_API) + /* QuicTLS provides app read keys before completing handshake */ if (pkt->level == ssl_encryption_application && !c->ssl->handshaked) { ngx_log_error(NGX_LOG_INFO, c->log, 0, diff --git a/src/event/quic/ngx_event_quic.h b/src/event/quic/ngx_event_quic.h index 15201671d..50a5c214e 100644 --- a/src/event/quic/ngx_event_quic.h +++ b/src/event/quic/ngx_event_quic.h @@ -12,6 +12,18 @@ #include +#ifdef SSL_R_MISSING_QUIC_TRANSPORT_PARAMETERS_EXTENSION +#define NGX_QUIC_QUICTLS_API 1 + +#elif (defined OPENSSL_IS_BORINGSSL || defined LIBRESSL_VERSION_NUMBER) +#define NGX_QUIC_BORINGSSL_API 1 + +#else +#define NGX_QUIC_BORINGSSL_API 1 +#define NGX_QUIC_OPENSSL_COMPAT 1 +#endif + + #define NGX_QUIC_MAX_UDP_PAYLOAD_SIZE 65527 #define NGX_QUIC_DEFAULT_ACK_DELAY_EXPONENT 3 diff --git a/src/event/quic/ngx_event_quic_openssl_compat.h b/src/event/quic/ngx_event_quic_openssl_compat.h index 77cc3cb0d..89ee41e89 100644 --- a/src/event/quic/ngx_event_quic_openssl_compat.h +++ b/src/event/quic/ngx_event_quic_openssl_compat.h @@ -7,11 +7,6 @@ #ifndef _NGX_EVENT_QUIC_OPENSSL_COMPAT_H_INCLUDED_ #define _NGX_EVENT_QUIC_OPENSSL_COMPAT_H_INCLUDED_ -#if defined SSL_R_MISSING_QUIC_TRANSPORT_PARAMETERS_EXTENSION \ - || defined LIBRESSL_VERSION_NUMBER -#undef NGX_QUIC_OPENSSL_COMPAT -#else - #include #include @@ -53,7 +48,4 @@ int SSL_set_quic_transport_params(SSL *ssl, const uint8_t *params, void SSL_get_peer_quic_transport_params(const SSL *ssl, const uint8_t **out_params, size_t *out_params_len); - -#endif /* TLSEXT_TYPE_quic_transport_parameters */ - #endif /* _NGX_EVENT_QUIC_OPENSSL_COMPAT_H_INCLUDED_ */ diff --git a/src/event/quic/ngx_event_quic_ssl.c b/src/event/quic/ngx_event_quic_ssl.c index e5d481d1c..1bb34831c 100644 --- a/src/event/quic/ngx_event_quic_ssl.c +++ b/src/event/quic/ngx_event_quic_ssl.c @@ -10,13 +10,6 @@ #include -#if defined OPENSSL_IS_BORINGSSL \ - || defined LIBRESSL_VERSION_NUMBER \ - || NGX_QUIC_OPENSSL_COMPAT -#define NGX_QUIC_BORINGSSL_API 1 -#endif - - /* * RFC 9000, 7.5. Cryptographic Message Buffering * @@ -32,7 +25,7 @@ static int ngx_quic_set_read_secret(ngx_ssl_conn_t *ssl_conn, static int ngx_quic_set_write_secret(ngx_ssl_conn_t *ssl_conn, enum ssl_encryption_level_t level, const SSL_CIPHER *cipher, const uint8_t *secret, size_t secret_len); -#else +#else /* NGX_QUIC_QUICTLS_API */ static int ngx_quic_set_encryption_secrets(ngx_ssl_conn_t *ssl_conn, enum ssl_encryption_level_t level, const uint8_t *read_secret, const uint8_t *write_secret, size_t secret_len); @@ -108,7 +101,7 @@ ngx_quic_set_write_secret(ngx_ssl_conn_t *ssl_conn, return 1; } -#else +#else /* NGX_QUIC_QUICTLS_API */ static int ngx_quic_set_encryption_secrets(ngx_ssl_conn_t *ssl_conn, @@ -550,7 +543,7 @@ ngx_quic_init_connection(ngx_connection_t *c) return NGX_ERROR; } -#ifdef OPENSSL_INFO_QUIC +#if (NGX_QUIC_QUICTLS_API) if (SSL_CTX_get_max_early_data(qc->conf->ssl->ctx)) { SSL_set_quic_early_data_enabled(ssl_conn, 1); } From noreply at nginx.com Fri May 23 11:01:03 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 23 May 2025 11:01:03 +0000 (UTC) Subject: [nginx] QUIC: logging missing mandatory TLS extensions only once. Message-ID: <20250523110103.1096248FEC@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/54e6b7cfeeae50f708398468078094fd309828e0 branches: master commit: 54e6b7cfeeae50f708398468078094fd309828e0 user: Sergey Kandaurov date: Tue, 6 May 2025 18:57:01 +0400 description: QUIC: logging missing mandatory TLS extensions only once. Previously, they might be logged on every add_handshake_data callback invocation when using OpenSSL compat layer and processing coalesced handshake messages. Further, the ALPN error message is adjusted to signal the missing extension. Possible reasons were previously narrowed down with ebb6f7d65 changes in the ALPN callback that is invoked earlier in the handshake. --- src/event/quic/ngx_event_quic_ssl.c | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/event/quic/ngx_event_quic_ssl.c b/src/event/quic/ngx_event_quic_ssl.c index 5b897bdb6..e5d481d1c 100644 --- a/src/event/quic/ngx_event_quic_ssl.c +++ b/src/event/quic/ngx_event_quic_ssl.c @@ -195,11 +195,14 @@ ngx_quic_add_handshake_data(ngx_ssl_conn_t *ssl_conn, SSL_get0_alpn_selected(ssl_conn, &alpn_data, &alpn_len); if (alpn_len == 0) { - qc->error = NGX_QUIC_ERR_CRYPTO(SSL_AD_NO_APPLICATION_PROTOCOL); - qc->error_reason = "unsupported protocol in ALPN extension"; + if (qc->error == 0) { + qc->error = NGX_QUIC_ERR_CRYPTO(SSL_AD_NO_APPLICATION_PROTOCOL); + qc->error_reason = "missing ALPN extension"; + + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "quic missing ALPN extension"); + } - ngx_log_error(NGX_LOG_INFO, c->log, 0, - "quic unsupported protocol in ALPN extension"); return 1; } @@ -212,11 +215,15 @@ ngx_quic_add_handshake_data(ngx_ssl_conn_t *ssl_conn, if (client_params_len == 0) { /* RFC 9001, 8.2. QUIC Transport Parameters Extension */ - qc->error = NGX_QUIC_ERR_CRYPTO(SSL_AD_MISSING_EXTENSION); - qc->error_reason = "missing transport parameters"; - ngx_log_error(NGX_LOG_INFO, c->log, 0, - "missing transport parameters"); + if (qc->error == 0) { + qc->error = NGX_QUIC_ERR_CRYPTO(SSL_AD_MISSING_EXTENSION); + qc->error_reason = "missing transport parameters"; + + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "missing transport parameters"); + } + return 1; } From noreply at nginx.com Fri May 23 11:01:03 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 23 May 2025 11:01:03 +0000 (UTC) Subject: [nginx] QUIC: factored out SSL_provide_quic_data() to the helper function. Message-ID: <20250523110103.19BD148FEE@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/9857578f15352ec248813f5b3e58ca55dc82f967 branches: master commit: 9857578f15352ec248813f5b3e58ca55dc82f967 user: Sergey Kandaurov date: Wed, 21 May 2025 20:32:48 +0400 description: QUIC: factored out SSL_provide_quic_data() to the helper function. It is now called from ngx_quic_handle_crypto_frame(), prior to proceeding with the handshake. With this logic removed, the handshake function is renamed to ngx_quic_handshake() to better match ngx_ssl_handshake(). --- src/event/quic/ngx_event_quic_ssl.c | 53 +++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/src/event/quic/ngx_event_quic_ssl.c b/src/event/quic/ngx_event_quic_ssl.c index 1bb34831c..c9ebd70bc 100644 --- a/src/event/quic/ngx_event_quic_ssl.c +++ b/src/event/quic/ngx_event_quic_ssl.c @@ -36,7 +36,8 @@ static int ngx_quic_add_handshake_data(ngx_ssl_conn_t *ssl_conn, static int ngx_quic_flush_flight(ngx_ssl_conn_t *ssl_conn); static int ngx_quic_send_alert(ngx_ssl_conn_t *ssl_conn, enum ssl_encryption_level_t level, uint8_t alert); -static ngx_int_t ngx_quic_crypto_input(ngx_connection_t *c, ngx_chain_t *data, +static ngx_int_t ngx_quic_handshake(ngx_connection_t *c); +static ngx_int_t ngx_quic_crypto_provide(ngx_connection_t *c, ngx_chain_t *out, enum ssl_encryption_level_t level); @@ -357,7 +358,11 @@ ngx_quic_handle_crypto_frame(ngx_connection_t *c, ngx_quic_header_t *pkt, } if (f->offset == ctx->crypto.offset) { - if (ngx_quic_crypto_input(c, frame->data, pkt->level) != NGX_OK) { + if (ngx_quic_crypto_provide(c, frame->data, pkt->level) != NGX_OK) { + return NGX_ERROR; + } + + if (ngx_quic_handshake(c) != NGX_OK) { return NGX_ERROR; } @@ -375,7 +380,11 @@ ngx_quic_handle_crypto_frame(ngx_connection_t *c, ngx_quic_header_t *pkt, cl = ngx_quic_read_buffer(c, &ctx->crypto, (uint64_t) -1); if (cl) { - if (ngx_quic_crypto_input(c, cl, pkt->level) != NGX_OK) { + if (ngx_quic_crypto_provide(c, cl, pkt->level) != NGX_OK) { + return NGX_ERROR; + } + + if (ngx_quic_handshake(c) != NGX_OK) { return NGX_ERROR; } @@ -387,12 +396,9 @@ ngx_quic_handle_crypto_frame(ngx_connection_t *c, ngx_quic_header_t *pkt, static ngx_int_t -ngx_quic_crypto_input(ngx_connection_t *c, ngx_chain_t *data, - enum ssl_encryption_level_t level) +ngx_quic_handshake(ngx_connection_t *c) { int n, sslerr; - ngx_buf_t *b; - ngx_chain_t *cl; ngx_ssl_conn_t *ssl_conn; ngx_quic_frame_t *frame; ngx_quic_connection_t *qc; @@ -401,16 +407,6 @@ ngx_quic_crypto_input(ngx_connection_t *c, ngx_chain_t *data, ssl_conn = c->ssl->connection; - for (cl = data; cl; cl = cl->next) { - b = cl->buf; - - if (!SSL_provide_quic_data(ssl_conn, level, b->pos, b->last - b->pos)) { - ngx_ssl_error(NGX_LOG_ALERT, c->log, 0, - "SSL_provide_quic_data() failed"); - return NGX_ERROR; - } - } - n = SSL_do_handshake(ssl_conn); ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "SSL_do_handshake: %d", n); @@ -503,6 +499,29 @@ ngx_quic_crypto_input(ngx_connection_t *c, ngx_chain_t *data, } +static ngx_int_t +ngx_quic_crypto_provide(ngx_connection_t *c, ngx_chain_t *out, + enum ssl_encryption_level_t level) +{ + ngx_buf_t *b; + ngx_chain_t *cl; + + for (cl = out; cl; cl = cl->next) { + b = cl->buf; + + if (!SSL_provide_quic_data(c->ssl->connection, level, b->pos, + b->last - b->pos)) + { + ngx_ssl_error(NGX_LOG_ALERT, c->log, 0, + "SSL_provide_quic_data() failed"); + return NGX_ERROR; + } + } + + return NGX_OK; +} + + ngx_int_t ngx_quic_init_connection(ngx_connection_t *c) { From noreply at nginx.com Fri May 23 11:01:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 23 May 2025 11:01:02 +0000 (UTC) Subject: [nginx] QUIC: logging level of handshake errors. Message-ID: <20250523110102.F35CC48F7A@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/ef9cd3214ff3d9d1513da8927423f42184dcf8e5 branches: master commit: ef9cd3214ff3d9d1513da8927423f42184dcf8e5 user: Sergey Kandaurov date: Tue, 6 May 2025 15:09:28 +0400 description: QUIC: logging level of handshake errors. Various errors reported by SSL_do_handshake() are now logged at the "info" or "crit" level, akin to handshakes on regular TCP connections. --- src/event/ngx_event_openssl.c | 4 +--- src/event/ngx_event_openssl.h | 2 ++ src/event/quic/ngx_event_quic_ssl.c | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c index 7eb05209d..a7b389444 100644 --- a/src/event/ngx_event_openssl.c +++ b/src/event/ngx_event_openssl.c @@ -45,8 +45,6 @@ static ssize_t ngx_ssl_sendfile(ngx_connection_t *c, ngx_buf_t *file, size_t size); static void ngx_ssl_read_handler(ngx_event_t *rev); static void ngx_ssl_shutdown_handler(ngx_event_t *ev); -static void ngx_ssl_connection_error(ngx_connection_t *c, int sslerr, - ngx_err_t err, char *text); static void ngx_ssl_clear_error(ngx_log_t *log); static ngx_int_t ngx_ssl_session_id_context(ngx_ssl_t *ssl, @@ -3301,7 +3299,7 @@ ngx_ssl_shutdown_handler(ngx_event_t *ev) } -static void +void ngx_ssl_connection_error(ngx_connection_t *c, int sslerr, ngx_err_t err, char *text) { diff --git a/src/event/ngx_event_openssl.h b/src/event/ngx_event_openssl.h index d4a62b82a..9e68deb44 100644 --- a/src/event/ngx_event_openssl.h +++ b/src/event/ngx_event_openssl.h @@ -361,6 +361,8 @@ ngx_chain_t *ngx_ssl_send_chain(ngx_connection_t *c, ngx_chain_t *in, off_t limit); void ngx_ssl_free_buffer(ngx_connection_t *c); ngx_int_t ngx_ssl_shutdown(ngx_connection_t *c); +void ngx_ssl_connection_error(ngx_connection_t *c, int sslerr, ngx_err_t err, + char *text); void ngx_cdecl ngx_ssl_error(ngx_uint_t level, ngx_log_t *log, ngx_err_t err, char *fmt, ...); void ngx_ssl_cleanup_ctx(void *data); diff --git a/src/event/quic/ngx_event_quic_ssl.c b/src/event/quic/ngx_event_quic_ssl.c index f255f77a2..ddc6c7c3b 100644 --- a/src/event/quic/ngx_event_quic_ssl.c +++ b/src/event/quic/ngx_event_quic_ssl.c @@ -427,7 +427,7 @@ ngx_quic_crypto_input(ngx_connection_t *c, ngx_chain_t *data, return NGX_ERROR; } - ngx_ssl_error(NGX_LOG_ERR, c->log, 0, "SSL_do_handshake() failed"); + ngx_ssl_connection_error(c, sslerr, 0, "SSL_do_handshake() failed"); return NGX_ERROR; } } From noreply at nginx.com Fri May 23 11:01:03 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 23 May 2025 11:01:03 +0000 (UTC) Subject: [nginx] QUIC: better approach for premature handshake completion. Message-ID: <20250523110103.2710C48FF0@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/1d4d2f2c962c33aafdd8f79d9fc50b7cacf05e24 branches: master commit: 1d4d2f2c962c33aafdd8f79d9fc50b7cacf05e24 user: Sergey Kandaurov date: Fri, 16 May 2025 01:10:11 +0400 description: QUIC: better approach for premature handshake completion. Using SSL_in_init() to inspect a handshake state was replaced with SSL_is_init_finished(). This represents a more complete fix to the BoringSSL issue addressed in 22671b37e. This provides awareness of the early data handshake state when using OpenSSL 3.5 TLS callbacks in 0-RTT enabled configurations, which, in particular, is used to avoid premature completion of the initial TLS handshake, before required client handshake messages are received. This is a non-functional change when using BoringSSL. It supersedes testing non-positive SSL_do_handshake() results in all supported SSL libraries, hence simplified. In preparation for using OpenSSL 3.5 TLS callbacks. --- src/event/quic/ngx_event_quic_ssl.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/event/quic/ngx_event_quic_ssl.c b/src/event/quic/ngx_event_quic_ssl.c index fc8ebd8cf..6ce926c81 100644 --- a/src/event/quic/ngx_event_quic_ssl.c +++ b/src/event/quic/ngx_event_quic_ssl.c @@ -463,7 +463,7 @@ ngx_quic_handshake(ngx_connection_t *c) } } - if (n <= 0 || SSL_in_init(ssl_conn)) { + if (!SSL_is_init_finished(ssl_conn)) { if (ngx_quic_keys_available(qc->keys, NGX_QUIC_ENCRYPTION_EARLY_DATA, 0) && qc->client_tp_done) { From noreply at nginx.com Fri May 23 11:01:03 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 23 May 2025 11:01:03 +0000 (UTC) Subject: [nginx] QUIC: using QUIC API introduced in OpenSSL 3.5. Message-ID: <20250523110103.2E01848FF4@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/6a134dfd4888fc3850d22294687cfb3940994c69 branches: master commit: 6a134dfd4888fc3850d22294687cfb3940994c69 user: Sergey Kandaurov date: Thu, 13 Feb 2025 17:00:56 +0400 description: QUIC: using QUIC API introduced in OpenSSL 3.5. Similarly to the QUIC API originated in BoringSSL, this API allows to register custom TLS callbacks for an external QUIC implementation. See the SSL_set_quic_tls_cbs manual page for details. Due to a different approach used in OpenSSL 3.5, handling of CRYPTO frames was streamlined to always write an incoming CRYPTO buffer to the crypto context. Using SSL_provide_quic_data(), this results in transient allocation of chain links and buffers for CRYPTO frames received in order. Testing didn't reveal performance degradation of QUIC handshakes, https://github.com/nginx/nginx/pull/646 provides specific results. --- auto/lib/openssl/conf | 10 +- src/event/quic/ngx_event_quic.h | 5 +- src/event/quic/ngx_event_quic_connection.h | 5 + src/event/quic/ngx_event_quic_ssl.c | 411 +++++++++++++++++++++++++---- 4 files changed, 383 insertions(+), 48 deletions(-) diff --git a/auto/lib/openssl/conf b/auto/lib/openssl/conf index f4b00ebd6..3068cae36 100644 --- a/auto/lib/openssl/conf +++ b/auto/lib/openssl/conf @@ -147,11 +147,17 @@ else if [ $USE_OPENSSL_QUIC = YES ]; then - ngx_feature="OpenSSL QUIC support" + ngx_feature="OpenSSL QUIC API" ngx_feature_name="NGX_QUIC" - ngx_feature_test="SSL_set_quic_method(NULL, NULL)" + ngx_feature_test="SSL_set_quic_tls_cbs(NULL, NULL, NULL)" . auto/feature + if [ $ngx_found = no ]; then + ngx_feature="BoringSSL-like QUIC API" + ngx_feature_test="SSL_set_quic_method(NULL, NULL)" + . auto/feature + fi + if [ $ngx_found = no ]; then ngx_feature="OpenSSL QUIC compatibility" ngx_feature_test="SSL_CTX_add_custom_ext(NULL, 0, 0, diff --git a/src/event/quic/ngx_event_quic.h b/src/event/quic/ngx_event_quic.h index 50a5c214e..d95d3d85b 100644 --- a/src/event/quic/ngx_event_quic.h +++ b/src/event/quic/ngx_event_quic.h @@ -12,7 +12,10 @@ #include -#ifdef SSL_R_MISSING_QUIC_TRANSPORT_PARAMETERS_EXTENSION +#ifdef OSSL_RECORD_PROTECTION_LEVEL_NONE +#define NGX_QUIC_OPENSSL_API 1 + +#elif (defined SSL_R_MISSING_QUIC_TRANSPORT_PARAMETERS_EXTENSION) #define NGX_QUIC_QUICTLS_API 1 #elif (defined OPENSSL_IS_BORINGSSL || defined LIBRESSL_VERSION_NUMBER) diff --git a/src/event/quic/ngx_event_quic_connection.h b/src/event/quic/ngx_event_quic_connection.h index 856512118..33922cf80 100644 --- a/src/event/quic/ngx_event_quic_connection.h +++ b/src/event/quic/ngx_event_quic_connection.h @@ -301,6 +301,11 @@ struct ngx_quic_connection_s { unsigned key_phase:1; unsigned validated:1; unsigned client_tp_done:1; + +#if (NGX_QUIC_OPENSSL_API) + unsigned read_level:2; + unsigned write_level:2; +#endif }; diff --git a/src/event/quic/ngx_event_quic_ssl.c b/src/event/quic/ngx_event_quic_ssl.c index 6ce926c81..e961c80cd 100644 --- a/src/event/quic/ngx_event_quic_ssl.c +++ b/src/event/quic/ngx_event_quic_ssl.c @@ -18,6 +18,23 @@ #define NGX_QUIC_MAX_BUFFERED 65535 +#if (NGX_QUIC_OPENSSL_API) + +static int ngx_quic_cbs_send(ngx_ssl_conn_t *ssl_conn, + const unsigned char *data, size_t len, size_t *consumed, void *arg); +static int ngx_quic_cbs_recv_rcd(ngx_ssl_conn_t *ssl_conn, + const unsigned char **data, size_t *bytes_read, void *arg); +static int ngx_quic_cbs_release_rcd(ngx_ssl_conn_t *ssl_conn, + size_t bytes_read, void *arg); +static int ngx_quic_cbs_yield_secret(ngx_ssl_conn_t *ssl_conn, uint32_t level, + int direction, const unsigned char *secret, size_t secret_len, void *arg); +static int ngx_quic_cbs_got_transport_params(ngx_ssl_conn_t *ssl_conn, + const unsigned char *params, size_t params_len, void *arg); +static int ngx_quic_cbs_alert(ngx_ssl_conn_t *ssl_conn, unsigned char alert, + void *arg); + +#else /* NGX_QUIC_BORINGSSL_API || NGX_QUIC_QUICTLS_API */ + static ngx_inline ngx_uint_t ngx_quic_map_encryption_level( enum ssl_encryption_level_t ssl_level); @@ -39,9 +56,270 @@ static int ngx_quic_add_handshake_data(ngx_ssl_conn_t *ssl_conn, static int ngx_quic_flush_flight(ngx_ssl_conn_t *ssl_conn); static int ngx_quic_send_alert(ngx_ssl_conn_t *ssl_conn, enum ssl_encryption_level_t ssl_level, uint8_t alert); + +#endif + static ngx_int_t ngx_quic_handshake(ngx_connection_t *c); -static ngx_int_t ngx_quic_crypto_provide(ngx_connection_t *c, ngx_chain_t *out, - ngx_uint_t level); +static ngx_int_t ngx_quic_crypto_provide(ngx_connection_t *c, ngx_uint_t level); + + +#if (NGX_QUIC_OPENSSL_API) + +static int +ngx_quic_cbs_send(ngx_ssl_conn_t *ssl_conn, + const unsigned char *data, size_t len, size_t *consumed, void *arg) +{ + ngx_connection_t *c = arg; + + ngx_chain_t *out; + unsigned int alpn_len; + ngx_quic_frame_t *frame; + const unsigned char *alpn_data; + ngx_quic_send_ctx_t *ctx; + ngx_quic_connection_t *qc; + + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, + "quic ngx_quic_cbs_send len:%uz", len); + + qc = ngx_quic_get_connection(c); + + *consumed = 0; + + SSL_get0_alpn_selected(ssl_conn, &alpn_data, &alpn_len); + + if (alpn_len == 0) { + qc->error = NGX_QUIC_ERR_CRYPTO(SSL_AD_NO_APPLICATION_PROTOCOL); + qc->error_reason = "missing ALPN extension"; + + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "quic missing ALPN extension"); + return 1; + } + + if (!qc->client_tp_done) { + /* RFC 9001, 8.2. QUIC Transport Parameters Extension */ + qc->error = NGX_QUIC_ERR_CRYPTO(SSL_AD_MISSING_EXTENSION); + qc->error_reason = "missing transport parameters"; + + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "missing transport parameters"); + return 1; + } + + ctx = ngx_quic_get_send_ctx(qc, qc->write_level); + + out = ngx_quic_copy_buffer(c, (u_char *) data, len); + if (out == NGX_CHAIN_ERROR) { + qc->error = NGX_QUIC_ERR_INTERNAL_ERROR; + return 1; + } + + frame = ngx_quic_alloc_frame(c); + if (frame == NULL) { + qc->error = NGX_QUIC_ERR_INTERNAL_ERROR; + return 1; + } + + frame->data = out; + frame->level = qc->write_level; + frame->type = NGX_QUIC_FT_CRYPTO; + frame->u.crypto.offset = ctx->crypto_sent; + frame->u.crypto.length = len; + + ctx->crypto_sent += len; + *consumed = len; + + ngx_quic_queue_frame(qc, frame); + + return 1; +} + + +static int +ngx_quic_cbs_recv_rcd(ngx_ssl_conn_t *ssl_conn, + const unsigned char **data, size_t *bytes_read, void *arg) +{ + ngx_connection_t *c = arg; + + ngx_buf_t *b; + ngx_chain_t *cl; + ngx_quic_send_ctx_t *ctx; + ngx_quic_connection_t *qc; + + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, + "quic ngx_quic_cbs_recv_rcd"); + + qc = ngx_quic_get_connection(c); + ctx = ngx_quic_get_send_ctx(qc, qc->read_level); + + for (cl = ctx->crypto.chain; cl; cl = cl->next) { + b = cl->buf; + + if (b->sync) { + /* hole */ + + *bytes_read = 0; + + break; + } + + *data = b->pos; + *bytes_read = b->last - b->pos; + + break; + } + + return 1; +} + + +static int +ngx_quic_cbs_release_rcd(ngx_ssl_conn_t *ssl_conn, size_t bytes_read, void *arg) +{ + ngx_connection_t *c = arg; + + ngx_chain_t *cl; + ngx_quic_send_ctx_t *ctx; + ngx_quic_connection_t *qc; + + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, + "quic ngx_quic_cbs_release_rcd len:%uz", bytes_read); + + qc = ngx_quic_get_connection(c); + ctx = ngx_quic_get_send_ctx(qc, qc->read_level); + + cl = ngx_quic_read_buffer(c, &ctx->crypto, bytes_read); + if (cl == NGX_CHAIN_ERROR) { + qc->error = NGX_QUIC_ERR_INTERNAL_ERROR; + return 1; + } + + ngx_quic_free_chain(c, cl); + + return 1; +} + + +static int +ngx_quic_cbs_yield_secret(ngx_ssl_conn_t *ssl_conn, uint32_t ssl_level, + int direction, const unsigned char *secret, size_t secret_len, void *arg) +{ + ngx_connection_t *c = arg; + + ngx_uint_t level; + const SSL_CIPHER *cipher; + ngx_quic_connection_t *qc; + + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, + "quic ngx_quic_cbs_yield_secret() level:%uD", ssl_level); +#ifdef NGX_QUIC_DEBUG_CRYPTO + ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0, + "quic %s secret len:%uz %*xs", + direction ? "write" : "read", secret_len, + secret_len, secret); +#endif + + qc = ngx_quic_get_connection(c); + cipher = SSL_get_current_cipher(ssl_conn); + + switch (ssl_level) { + case OSSL_RECORD_PROTECTION_LEVEL_NONE: + level = NGX_QUIC_ENCRYPTION_INITIAL; + break; + case OSSL_RECORD_PROTECTION_LEVEL_EARLY: + level = NGX_QUIC_ENCRYPTION_EARLY_DATA; + break; + case OSSL_RECORD_PROTECTION_LEVEL_HANDSHAKE: + level = NGX_QUIC_ENCRYPTION_HANDSHAKE; + break; + default: /* OSSL_RECORD_PROTECTION_LEVEL_APPLICATION */ + level = NGX_QUIC_ENCRYPTION_APPLICATION; + break; + } + + if (ngx_quic_keys_set_encryption_secret(c->log, direction, qc->keys, level, + cipher, secret, secret_len) + != NGX_OK) + { + qc->error = NGX_QUIC_ERR_INTERNAL_ERROR; + return 1; + } + + if (direction) { + qc->write_level = level; + + } else { + qc->read_level = level; + } + + return 1; +} + + +static int +ngx_quic_cbs_got_transport_params(ngx_ssl_conn_t *ssl_conn, + const unsigned char *params, size_t params_len, void *arg) +{ + ngx_connection_t *c = arg; + + u_char *p, *end; + ngx_quic_tp_t ctp; + ngx_quic_connection_t *qc; + + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, + "quic ngx_quic_cbs_got_transport_params() len:%uz", + params_len); + + qc = ngx_quic_get_connection(c); + + /* defaults for parameters not sent by client */ + ngx_memcpy(&ctp, &qc->ctp, sizeof(ngx_quic_tp_t)); + + p = (u_char *) params; + end = p + params_len; + + if (ngx_quic_parse_transport_params(p, end, &ctp, c->log) != NGX_OK) { + qc->error = NGX_QUIC_ERR_TRANSPORT_PARAMETER_ERROR; + qc->error_reason = "failed to process transport parameters"; + + return 1; + } + + if (ngx_quic_apply_transport_params(c, &ctp) != NGX_OK) { + return 1; + } + + qc->client_tp_done = 1; + + return 1; +} + + +static int +ngx_quic_cbs_alert(ngx_ssl_conn_t *ssl_conn, unsigned char alert, void *arg) +{ + ngx_connection_t *c = arg; + + ngx_quic_connection_t *qc; + + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, + "quic ngx_quic_cbs_alert() alert:%d", (int) alert); + + /* already closed on regular shutdown */ + + qc = ngx_quic_get_connection(c); + if (qc == NULL) { + return 1; + } + + qc->error = NGX_QUIC_ERR_CRYPTO(alert); + qc->error_reason = "handshake failed"; + + return 1; +} + + +#else /* NGX_QUIC_BORINGSSL_API || NGX_QUIC_QUICTLS_API */ static ngx_inline ngx_uint_t @@ -340,13 +618,14 @@ ngx_quic_send_alert(ngx_ssl_conn_t *ssl_conn, return 1; } +#endif + ngx_int_t ngx_quic_handle_crypto_frame(ngx_connection_t *c, ngx_quic_header_t *pkt, ngx_quic_frame_t *frame) { uint64_t last; - ngx_chain_t *cl; ngx_quic_send_ctx_t *ctx; ngx_quic_connection_t *qc; ngx_quic_crypto_frame_t *f; @@ -385,41 +664,18 @@ ngx_quic_handle_crypto_frame(ngx_connection_t *c, ngx_quic_header_t *pkt, return NGX_OK; } - if (f->offset == ctx->crypto.offset) { - if (ngx_quic_crypto_provide(c, frame->data, pkt->level) != NGX_OK) { - return NGX_ERROR; - } - - if (ngx_quic_handshake(c) != NGX_OK) { - return NGX_ERROR; - } - - ngx_quic_skip_buffer(c, &ctx->crypto, last); - - } else { - if (ngx_quic_write_buffer(c, &ctx->crypto, frame->data, f->length, - f->offset) - == NGX_CHAIN_ERROR) - { - return NGX_ERROR; - } + if (ngx_quic_write_buffer(c, &ctx->crypto, frame->data, f->length, + f->offset) + == NGX_CHAIN_ERROR) + { + return NGX_ERROR; } - cl = ngx_quic_read_buffer(c, &ctx->crypto, (uint64_t) -1); - - if (cl) { - if (ngx_quic_crypto_provide(c, cl, pkt->level) != NGX_OK) { - return NGX_ERROR; - } - - if (ngx_quic_handshake(c) != NGX_OK) { - return NGX_ERROR; - } - - ngx_quic_free_chain(c, cl); + if (ngx_quic_crypto_provide(c, pkt->level) != NGX_OK) { + return NGX_ERROR; } - return NGX_OK; + return ngx_quic_handshake(c); } @@ -528,13 +784,24 @@ ngx_quic_handshake(ngx_connection_t *c) static ngx_int_t -ngx_quic_crypto_provide(ngx_connection_t *c, ngx_chain_t *out, - ngx_uint_t level) +ngx_quic_crypto_provide(ngx_connection_t *c, ngx_uint_t level) { +#if (NGX_QUIC_BORINGSSL_API || NGX_QUIC_QUICTLS_API) + ngx_buf_t *b; - ngx_chain_t *cl; + ngx_chain_t *out, *cl; + ngx_quic_send_ctx_t *ctx; + ngx_quic_connection_t *qc; enum ssl_encryption_level_t ssl_level; + qc = ngx_quic_get_connection(c); + ctx = ngx_quic_get_send_ctx(qc, level); + + out = ngx_quic_read_buffer(c, &ctx->crypto, (uint64_t) -1); + if (out == NGX_CHAIN_ERROR) { + return NGX_ERROR; + } + switch (level) { case NGX_QUIC_ENCRYPTION_INITIAL: ssl_level = ssl_encryption_initial; @@ -562,6 +829,10 @@ ngx_quic_crypto_provide(ngx_connection_t *c, ngx_chain_t *out, } } + ngx_quic_free_chain(c, out); + +#endif + return NGX_OK; } @@ -569,14 +840,40 @@ ngx_quic_crypto_provide(ngx_connection_t *c, ngx_chain_t *out, ngx_int_t ngx_quic_init_connection(ngx_connection_t *c) { - u_char *p; - size_t clen; - ssize_t len; - ngx_str_t dcid; - ngx_ssl_conn_t *ssl_conn; - ngx_quic_socket_t *qsock; - ngx_quic_connection_t *qc; - static SSL_QUIC_METHOD quic_method; + u_char *p; + size_t clen; + ssize_t len; + ngx_str_t dcid; + ngx_ssl_conn_t *ssl_conn; + ngx_quic_socket_t *qsock; + ngx_quic_connection_t *qc; + +#if (NGX_QUIC_OPENSSL_API) + static const OSSL_DISPATCH qtdis[] = { + + { OSSL_FUNC_SSL_QUIC_TLS_CRYPTO_SEND, + (void (*)(void)) ngx_quic_cbs_send }, + + { OSSL_FUNC_SSL_QUIC_TLS_CRYPTO_RECV_RCD, + (void (*)(void)) ngx_quic_cbs_recv_rcd }, + + { OSSL_FUNC_SSL_QUIC_TLS_CRYPTO_RELEASE_RCD, + (void (*)(void)) ngx_quic_cbs_release_rcd }, + + { OSSL_FUNC_SSL_QUIC_TLS_YIELD_SECRET, + (void (*)(void)) ngx_quic_cbs_yield_secret }, + + { OSSL_FUNC_SSL_QUIC_TLS_GOT_TRANSPORT_PARAMS, + (void (*)(void)) ngx_quic_cbs_got_transport_params }, + + { OSSL_FUNC_SSL_QUIC_TLS_ALERT, + (void (*)(void)) ngx_quic_cbs_alert }, + + { 0, NULL } + }; +#else /* NGX_QUIC_BORINGSSL_API || NGX_QUIC_QUICTLS_API */ + static SSL_QUIC_METHOD quic_method; +#endif qc = ngx_quic_get_connection(c); @@ -588,6 +885,20 @@ ngx_quic_init_connection(ngx_connection_t *c) ssl_conn = c->ssl->connection; +#if (NGX_QUIC_OPENSSL_API) + + if (SSL_set_quic_tls_cbs(ssl_conn, qtdis, c) == 0) { + ngx_ssl_error(NGX_LOG_ALERT, c->log, 0, + "quic SSL_set_quic_tls_cbs() failed"); + return NGX_ERROR; + } + + if (SSL_CTX_get_max_early_data(qc->conf->ssl->ctx)) { + SSL_set_quic_tls_early_data_enabled(ssl_conn, 1); + } + +#else /* NGX_QUIC_BORINGSSL_API || NGX_QUIC_QUICTLS_API */ + if (!quic_method.send_alert) { #if (NGX_QUIC_BORINGSSL_API) quic_method.set_read_secret = ngx_quic_set_read_secret; @@ -610,6 +921,8 @@ ngx_quic_init_connection(ngx_connection_t *c) if (SSL_CTX_get_max_early_data(qc->conf->ssl->ctx)) { SSL_set_quic_early_data_enabled(ssl_conn, 1); } +#endif + #endif qsock = ngx_quic_get_socket(c); @@ -641,11 +954,19 @@ ngx_quic_init_connection(ngx_connection_t *c) "quic transport parameters len:%uz %*xs", len, len, p); #endif +#if (NGX_QUIC_OPENSSL_API) + if (SSL_set_quic_tls_transport_params(ssl_conn, p, len) == 0) { + ngx_ssl_error(NGX_LOG_ALERT, c->log, 0, + "quic SSL_set_quic_tls_transport_params() failed"); + return NGX_ERROR; + } +#else if (SSL_set_quic_transport_params(ssl_conn, p, len) == 0) { ngx_ssl_error(NGX_LOG_ALERT, c->log, 0, "quic SSL_set_quic_transport_params() failed"); return NGX_ERROR; } +#endif #ifdef OPENSSL_IS_BORINGSSL if (SSL_set_quic_early_data_context(ssl_conn, p, clen) == 0) { From noreply at nginx.com Fri May 23 11:01:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 23 May 2025 11:01:02 +0000 (UTC) Subject: [nginx] QUIC: removed ALPN feature test. Message-ID: <20250523110102.ECFF848F5D@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/aa43385ffa9acda2f9a45265bbe8e9213d9a840e branches: master commit: aa43385ffa9acda2f9a45265bbe8e9213d9a840e user: Sergey Kandaurov date: Tue, 6 May 2025 15:17:44 +0400 description: QUIC: removed ALPN feature test. ALPN support is present in all libraries that have QUIC support, it is safe to compile it unconditionally. --- src/event/quic/ngx_event_quic_ssl.c | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/event/quic/ngx_event_quic_ssl.c b/src/event/quic/ngx_event_quic_ssl.c index 6ad3c0e93..f255f77a2 100644 --- a/src/event/quic/ngx_event_quic_ssl.c +++ b/src/event/quic/ngx_event_quic_ssl.c @@ -169,16 +169,14 @@ ngx_quic_add_handshake_data(ngx_ssl_conn_t *ssl_conn, u_char *p, *end; size_t client_params_len; ngx_chain_t *out; + unsigned int alpn_len; const uint8_t *client_params; ngx_quic_tp_t ctp; ngx_quic_frame_t *frame; ngx_connection_t *c; + const unsigned char *alpn_data; ngx_quic_send_ctx_t *ctx; ngx_quic_connection_t *qc; -#if defined(TLSEXT_TYPE_application_layer_protocol_negotiation) - unsigned int alpn_len; - const unsigned char *alpn_data; -#endif c = ngx_ssl_get_connection(ssl_conn); qc = ngx_quic_get_connection(c); @@ -193,8 +191,6 @@ ngx_quic_add_handshake_data(ngx_ssl_conn_t *ssl_conn, * here; */ -#if defined(TLSEXT_TYPE_application_layer_protocol_negotiation) - SSL_get0_alpn_selected(ssl_conn, &alpn_data, &alpn_len); if (alpn_len == 0) { @@ -206,8 +202,6 @@ ngx_quic_add_handshake_data(ngx_ssl_conn_t *ssl_conn, return 0; } -#endif - SSL_get_peer_quic_transport_params(ssl_conn, &client_params, &client_params_len); From noreply at nginx.com Fri May 23 11:01:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 23 May 2025 11:01:02 +0000 (UTC) Subject: [nginx] QUIC: removed level field from ngx_quic_compat_record_t. Message-ID: <20250523110102.E5A6848F56@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/335993154c85daf4be6bc5b465ff0d4f319082ad branches: master commit: 335993154c85daf4be6bc5b465ff0d4f319082ad user: Sergey Kandaurov date: Tue, 6 May 2025 19:57:44 +0400 description: QUIC: removed level field from ngx_quic_compat_record_t. It was made unused in d15f8f2 after introducing reusable crypto contexts. --- src/event/quic/ngx_event_quic_openssl_compat.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/event/quic/ngx_event_quic_openssl_compat.c b/src/event/quic/ngx_event_quic_openssl_compat.c index 6052bc683..a4a8ea1b6 100644 --- a/src/event/quic/ngx_event_quic_openssl_compat.c +++ b/src/event/quic/ngx_event_quic_openssl_compat.c @@ -35,8 +35,6 @@ typedef struct { ngx_str_t payload; uint64_t number; ngx_quic_compat_keys_t *keys; - - enum ssl_encryption_level_t level; } ngx_quic_compat_record_t; @@ -501,7 +499,6 @@ SSL_provide_quic_data(SSL *ssl, enum ssl_encryption_level_t level, rec.log = c->log; rec.number = com->read_record++; rec.keys = &com->keys; - rec.level = level; if (level == ssl_encryption_initial) { n = ngx_min(len, 65535); From noreply at nginx.com Fri May 23 11:01:03 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 23 May 2025 11:01:03 +0000 (UTC) Subject: [nginx] QUIC: reset qc->error to zero again. Message-ID: <20250523110103.0C7C048FEB@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/5d7fd4a7e3025e8600bb029742a0a28bf4ca9eec branches: master commit: 5d7fd4a7e3025e8600bb029742a0a28bf4ca9eec user: Sergey Kandaurov date: Wed, 14 May 2025 23:33:00 +0400 description: QUIC: reset qc->error to zero again. Following the previous change that removed posting a close event in OpenSSL compat layer, now ngx_quic_close_connection() is always called on error path with either NGX_ERROR or qc->error set. This allows to remove a special value -1 served as a missing error, which simplifies the code. Partially reverts d3fb12d77. Also, this improves handling of the draining connection state, which consists of posting a close event with NGX_OK and no qc->error set, where it was previously converted to NGX_QUIC_ERR_INTERNAL_ERROR. Notably, this is rather a cosmetic fix, because drained connections do not send any packets including CONNECTION_CLOSE, and qc->error is not otherwise used. --- src/event/quic/ngx_event_quic.c | 6 +++--- src/event/quic/ngx_event_quic_ssl.c | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/event/quic/ngx_event_quic.c b/src/event/quic/ngx_event_quic.c index a4ad85d56..9f968d5fb 100644 --- a/src/event/quic/ngx_event_quic.c +++ b/src/event/quic/ngx_event_quic.c @@ -72,7 +72,7 @@ ngx_quic_connstate_dbg(ngx_connection_t *c) if (qc) { - if (qc->error != (ngx_uint_t) -1) { + if (qc->error) { p = ngx_slprintf(p, last, "%s", qc->error_app ? " app" : ""); p = ngx_slprintf(p, last, " error:%ui", qc->error); @@ -520,7 +520,7 @@ ngx_quic_close_connection(ngx_connection_t *c, ngx_int_t rc) * to terminate the connection immediately. */ - if (qc->error == (ngx_uint_t) -1) { + if (qc->error == 0 && rc == NGX_ERROR) { qc->error = NGX_QUIC_ERR_INTERNAL_ERROR; qc->error_app = 0; } @@ -961,7 +961,7 @@ ngx_quic_handle_payload(ngx_connection_t *c, ngx_quic_header_t *pkt) qc = ngx_quic_get_connection(c); - qc->error = (ngx_uint_t) -1; + qc->error = 0; qc->error_reason = 0; c->log->action = "decrypting packet"; diff --git a/src/event/quic/ngx_event_quic_ssl.c b/src/event/quic/ngx_event_quic_ssl.c index dd7ee3702..5b897bdb6 100644 --- a/src/event/quic/ngx_event_quic_ssl.c +++ b/src/event/quic/ngx_event_quic_ssl.c @@ -415,7 +415,7 @@ ngx_quic_crypto_input(ngx_connection_t *c, ngx_chain_t *data, ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "SSL_do_handshake: %d", n); - if (qc->error != (ngx_uint_t) -1) { + if (qc->error) { return NGX_ERROR; } From noreply at nginx.com Fri May 23 11:01:03 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 23 May 2025 11:01:03 +0000 (UTC) Subject: [nginx] QUIC: ssl_encryption_level_t abstraction layer. Message-ID: <20250523110103.231C648FEF@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/bcb9d3fd2cc88eee23a5da854a0e2aa5c5b688d7 branches: master commit: bcb9d3fd2cc88eee23a5da854a0e2aa5c5b688d7 user: Sergey Kandaurov date: Tue, 6 May 2025 15:58:17 +0400 description: QUIC: ssl_encryption_level_t abstraction layer. Encryption level values are decoupled from ssl_encryption_level_t, which is now limited to BoringSSL QUIC callbacks, with mappings provided. Although the values match, this provides a technically safe approach, in particular, to access protection level sized arrays. In preparation for using OpenSSL 3.5 TLS callbacks. --- src/event/quic/ngx_event_quic.c | 36 +++++----- src/event/quic/ngx_event_quic_ack.c | 16 ++--- src/event/quic/ngx_event_quic_connection.h | 24 ++++--- src/event/quic/ngx_event_quic_connid.c | 6 +- src/event/quic/ngx_event_quic_migration.c | 22 +++--- src/event/quic/ngx_event_quic_openssl_compat.c | 11 ++- src/event/quic/ngx_event_quic_output.c | 16 ++--- src/event/quic/ngx_event_quic_protection.c | 19 +++-- src/event/quic/ngx_event_quic_protection.h | 14 ++-- src/event/quic/ngx_event_quic_ssl.c | 98 +++++++++++++++++++------- src/event/quic/ngx_event_quic_streams.c | 16 ++--- src/event/quic/ngx_event_quic_transport.c | 14 ++-- src/event/quic/ngx_event_quic_transport.h | 10 +-- 13 files changed, 174 insertions(+), 128 deletions(-) diff --git a/src/event/quic/ngx_event_quic.c b/src/event/quic/ngx_event_quic.c index 4f2e50240..8df487773 100644 --- a/src/event/quic/ngx_event_quic.c +++ b/src/event/quic/ngx_event_quic.c @@ -260,9 +260,9 @@ ngx_quic_new_connection(ngx_connection_t *c, ngx_quic_conf_t *conf, qc->send_ctx[i].pending_ack = NGX_QUIC_UNSET_PN; } - qc->send_ctx[0].level = ssl_encryption_initial; - qc->send_ctx[1].level = ssl_encryption_handshake; - qc->send_ctx[2].level = ssl_encryption_application; + qc->send_ctx[0].level = NGX_QUIC_ENCRYPTION_INITIAL; + qc->send_ctx[1].level = NGX_QUIC_ENCRYPTION_HANDSHAKE; + qc->send_ctx[2].level = NGX_QUIC_ENCRYPTION_APPLICATION; ngx_queue_init(&qc->free_frames); @@ -800,13 +800,13 @@ ngx_quic_handle_packet(ngx_connection_t *c, ngx_quic_conf_t *conf, pkt->dcid.len, &pkt->dcid); #if (NGX_DEBUG) - if (pkt->level != ssl_encryption_application) { + if (pkt->level != NGX_QUIC_ENCRYPTION_APPLICATION) { ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic packet rx scid len:%uz %xV", pkt->scid.len, &pkt->scid); } - if (pkt->level == ssl_encryption_initial) { + if (pkt->level == NGX_QUIC_ENCRYPTION_INITIAL) { ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic address validation token len:%uz %xV", pkt->token.len, &pkt->token); @@ -823,7 +823,7 @@ ngx_quic_handle_packet(ngx_connection_t *c, ngx_quic_conf_t *conf, return NGX_DECLINED; } - if (pkt->level != ssl_encryption_application) { + if (pkt->level != NGX_QUIC_ENCRYPTION_APPLICATION) { if (pkt->version != qc->version) { ngx_log_error(NGX_LOG_INFO, c->log, 0, @@ -853,7 +853,9 @@ ngx_quic_handle_packet(ngx_connection_t *c, ngx_quic_conf_t *conf, rc = ngx_quic_handle_payload(c, pkt); - if (rc == NGX_DECLINED && pkt->level == ssl_encryption_application) { + if (rc == NGX_DECLINED + && pkt->level == NGX_QUIC_ENCRYPTION_APPLICATION) + { if (ngx_quic_handle_stateless_reset(c, pkt) == NGX_OK) { ngx_log_error(NGX_LOG_INFO, c->log, 0, "quic stateless reset packet detected"); @@ -874,11 +876,11 @@ ngx_quic_handle_packet(ngx_connection_t *c, ngx_quic_conf_t *conf, return ngx_quic_negotiate_version(c, pkt); } - if (pkt->level == ssl_encryption_application) { + if (pkt->level == NGX_QUIC_ENCRYPTION_APPLICATION) { return ngx_quic_send_stateless_reset(c, conf, pkt); } - if (pkt->level != ssl_encryption_initial) { + if (pkt->level != NGX_QUIC_ENCRYPTION_INITIAL) { ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic expected initial, got handshake"); return NGX_ERROR; @@ -976,7 +978,7 @@ ngx_quic_handle_payload(ngx_connection_t *c, ngx_quic_header_t *pkt) #if (NGX_QUIC_QUICTLS_API) /* QuicTLS provides app read keys before completing handshake */ - if (pkt->level == ssl_encryption_application && !c->ssl->handshaked) { + if (pkt->level == NGX_QUIC_ENCRYPTION_APPLICATION && !c->ssl->handshaked) { ngx_log_error(NGX_LOG_INFO, c->log, 0, "quic no %s keys ready, ignoring packet", ngx_quic_level_name(pkt->level)); @@ -1014,14 +1016,14 @@ ngx_quic_handle_payload(ngx_connection_t *c, ngx_quic_header_t *pkt) } } - if (pkt->level == ssl_encryption_handshake) { + if (pkt->level == NGX_QUIC_ENCRYPTION_HANDSHAKE) { /* * RFC 9001, 4.9.1. Discarding Initial Keys * * The successful use of Handshake packets indicates * that no more Initial packets need to be exchanged */ - ngx_quic_discard_ctx(c, ssl_encryption_initial); + ngx_quic_discard_ctx(c, NGX_QUIC_ENCRYPTION_INITIAL); if (!qc->path->validated) { qc->path->validated = 1; @@ -1030,14 +1032,14 @@ ngx_quic_handle_payload(ngx_connection_t *c, ngx_quic_header_t *pkt) } } - if (pkt->level == ssl_encryption_application) { + if (pkt->level == NGX_QUIC_ENCRYPTION_APPLICATION) { /* * RFC 9001, 4.9.3. Discarding 0-RTT Keys * * After receiving a 1-RTT packet, servers MUST discard * 0-RTT keys within a short time */ - ngx_quic_keys_discard(qc->keys, ssl_encryption_early_data); + ngx_quic_keys_discard(qc->keys, NGX_QUIC_ENCRYPTION_EARLY_DATA); } if (qc->closing) { @@ -1064,7 +1066,7 @@ ngx_quic_handle_payload(ngx_connection_t *c, ngx_quic_header_t *pkt) c->log->action = "handling payload"; - if (pkt->level != ssl_encryption_application) { + if (pkt->level != NGX_QUIC_ENCRYPTION_APPLICATION) { return ngx_quic_handle_frames(c, pkt); } @@ -1089,7 +1091,7 @@ ngx_quic_handle_payload(ngx_connection_t *c, ngx_quic_header_t *pkt) void -ngx_quic_discard_ctx(ngx_connection_t *c, enum ssl_encryption_level_t level) +ngx_quic_discard_ctx(ngx_connection_t *c, ngx_uint_t level) { ngx_queue_t *q; ngx_quic_frame_t *f; @@ -1130,7 +1132,7 @@ ngx_quic_discard_ctx(ngx_connection_t *c, enum ssl_encryption_level_t level) ngx_quic_free_frame(c, f); } - if (level == ssl_encryption_initial) { + if (level == NGX_QUIC_ENCRYPTION_INITIAL) { /* close temporary listener with initial dcid */ qsock = ngx_quic_find_socket(c, NGX_QUIC_UNSET_PN); if (qsock) { diff --git a/src/event/quic/ngx_event_quic_ack.c b/src/event/quic/ngx_event_quic_ack.c index c7fd96c2c..abd3f7ade 100644 --- a/src/event/quic/ngx_event_quic_ack.c +++ b/src/event/quic/ngx_event_quic_ack.c @@ -36,7 +36,7 @@ typedef struct { static ngx_inline ngx_msec_t ngx_quic_time_threshold(ngx_quic_connection_t *qc); static uint64_t ngx_quic_packet_threshold(ngx_quic_send_ctx_t *ctx); static void ngx_quic_rtt_sample(ngx_connection_t *c, ngx_quic_ack_frame_t *ack, - enum ssl_encryption_level_t level, ngx_msec_t send_time); + ngx_uint_t level, ngx_msec_t send_time); static ngx_int_t ngx_quic_handle_ack_frame_range(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx, uint64_t min, uint64_t max, ngx_quic_ack_stat_t *st); @@ -108,7 +108,7 @@ ngx_quic_handle_ack_frame(ngx_connection_t *c, ngx_quic_header_t *pkt, ctx = ngx_quic_get_send_ctx(qc, pkt->level); ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, - "quic ngx_quic_handle_ack_frame level:%d", pkt->level); + "quic ngx_quic_handle_ack_frame level:%ui", pkt->level); ack = &f->u.ack; @@ -207,7 +207,7 @@ ngx_quic_handle_ack_frame(ngx_connection_t *c, ngx_quic_header_t *pkt, static void ngx_quic_rtt_sample(ngx_connection_t *c, ngx_quic_ack_frame_t *ack, - enum ssl_encryption_level_t level, ngx_msec_t send_time) + ngx_uint_t level, ngx_msec_t send_time) { ngx_msec_t latest_rtt, ack_delay, adjusted_rtt, rttvar_sample; ngx_quic_connection_t *qc; @@ -260,7 +260,7 @@ ngx_quic_handle_ack_frame_range(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx, qc = ngx_quic_get_connection(c); - if (ctx->level == ssl_encryption_application) { + if (ctx->level == NGX_QUIC_ENCRYPTION_APPLICATION) { if (ngx_quic_handle_path_mtu(c, qc->path, min, max) != NGX_OK) { return NGX_ERROR; } @@ -634,7 +634,7 @@ ngx_quic_detect_lost(ngx_connection_t *c, ngx_quic_ack_stat_t *st) wait = start->send_time + thr - now; ngx_log_debug5(NGX_LOG_DEBUG_EVENT, c->log, 0, - "quic detect_lost pnum:%uL thr:%M pthr:%uL wait:%i level:%d", + "quic detect_lost pnum:%uL thr:%M pthr:%uL wait:%i level:%ui", start->pnum, thr, pkt_thr, (ngx_int_t) wait, start->level); if ((ngx_msec_int_t) wait > 0 @@ -787,7 +787,7 @@ ngx_quic_resend_frames(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx) switch (f->type) { case NGX_QUIC_FT_ACK: case NGX_QUIC_FT_ACK_ECN: - if (ctx->level == ssl_encryption_application) { + if (ctx->level == NGX_QUIC_ENCRYPTION_APPLICATION) { /* force generation of most recent acknowledgment */ ctx->send_ack = NGX_QUIC_MAX_ACK_GAP; } @@ -1073,7 +1073,7 @@ ngx_quic_pto(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx) duration = qc->avg_rtt; duration += ngx_max(4 * qc->rttvar, NGX_QUIC_TIME_GRANULARITY); - if (ctx->level == ssl_encryption_application && c->ssl->handshaked) { + if (ctx->level == NGX_QUIC_ENCRYPTION_APPLICATION && c->ssl->handshaked) { duration += qc->ctp.max_ack_delay; } @@ -1428,7 +1428,7 @@ ngx_quic_generate_ack(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx) return NGX_OK; } - if (ctx->level == ssl_encryption_application) { + if (ctx->level == NGX_QUIC_ENCRYPTION_APPLICATION) { delay = ngx_current_msec - ctx->ack_delay_start; qc = ngx_quic_get_connection(c); diff --git a/src/event/quic/ngx_event_quic_connection.h b/src/event/quic/ngx_event_quic_connection.h index 04cda859e..856512118 100644 --- a/src/event/quic/ngx_event_quic_connection.h +++ b/src/event/quic/ngx_event_quic_connection.h @@ -17,6 +17,15 @@ /* #define NGX_QUIC_DEBUG_ALLOC */ /* log frames and bufs alloc */ /* #define NGX_QUIC_DEBUG_CRYPTO */ +#define NGX_QUIC_ENCRYPTION_INITIAL 0 +#define NGX_QUIC_ENCRYPTION_EARLY_DATA 1 +#define NGX_QUIC_ENCRYPTION_HANDSHAKE 2 +#define NGX_QUIC_ENCRYPTION_APPLICATION 3 +#define NGX_QUIC_ENCRYPTION_LAST 4 + +#define NGX_QUIC_SEND_CTX_LAST (NGX_QUIC_ENCRYPTION_LAST - 1) + + typedef struct ngx_quic_connection_s ngx_quic_connection_t; typedef struct ngx_quic_server_id_s ngx_quic_server_id_t; typedef struct ngx_quic_client_id_s ngx_quic_client_id_t; @@ -46,8 +55,6 @@ typedef struct ngx_quic_keys_s ngx_quic_keys_t; #define NGX_QUIC_UNSET_PN (uint64_t) -1 -#define NGX_QUIC_SEND_CTX_LAST (NGX_QUIC_ENCRYPTION_LAST - 1) - /* 0-RTT and 1-RTT data exist in the same packet number space, * so we have 3 packet number spaces: * @@ -56,9 +63,9 @@ typedef struct ngx_quic_keys_s ngx_quic_keys_t; * 2 - 0-RTT and 1-RTT */ #define ngx_quic_get_send_ctx(qc, level) \ - ((level) == ssl_encryption_initial) ? &((qc)->send_ctx[0]) \ - : (((level) == ssl_encryption_handshake) ? &((qc)->send_ctx[1]) \ - : &((qc)->send_ctx[2])) + ((level) == NGX_QUIC_ENCRYPTION_INITIAL) ? &((qc)->send_ctx[0]) \ + : (((level) == NGX_QUIC_ENCRYPTION_HANDSHAKE) ? &((qc)->send_ctx[1]) \ + : &((qc)->send_ctx[2])) #define ngx_quic_get_connection(c) \ (((c)->udp) ? (((ngx_quic_socket_t *)((c)->udp))->quic) : NULL) @@ -188,7 +195,7 @@ typedef struct { * are also Initial packets. */ struct ngx_quic_send_ctx_s { - enum ssl_encryption_level_t level; + ngx_uint_t level; ngx_quic_buffer_t crypto; uint64_t crypto_sent; @@ -279,7 +286,7 @@ struct ngx_quic_connection_s { off_t received; ngx_uint_t error; - enum ssl_encryption_level_t error_level; + ngx_uint_t error_level; ngx_uint_t error_ftype; const char *error_reason; @@ -299,8 +306,7 @@ struct ngx_quic_connection_s { ngx_int_t ngx_quic_apply_transport_params(ngx_connection_t *c, ngx_quic_tp_t *ctp); -void ngx_quic_discard_ctx(ngx_connection_t *c, - enum ssl_encryption_level_t level); +void ngx_quic_discard_ctx(ngx_connection_t *c, ngx_uint_t level); void ngx_quic_close_connection(ngx_connection_t *c, ngx_int_t rc); void ngx_quic_shutdown_quic(ngx_connection_t *c); diff --git a/src/event/quic/ngx_event_quic_connid.c b/src/event/quic/ngx_event_quic_connid.c index f50868205..4e7b8dc22 100644 --- a/src/event/quic/ngx_event_quic_connid.c +++ b/src/event/quic/ngx_event_quic_connid.c @@ -99,7 +99,7 @@ ngx_quic_handle_new_connection_id_frame(ngx_connection_t *c, return NGX_ERROR; } - frame->level = ssl_encryption_application; + frame->level = NGX_QUIC_ENCRYPTION_APPLICATION; frame->type = NGX_QUIC_FT_RETIRE_CONNECTION_ID; frame->u.retire_cid.sequence_number = f->seqnum; @@ -452,7 +452,7 @@ ngx_quic_send_server_id(ngx_connection_t *c, ngx_quic_server_id_t *sid) return NGX_ERROR; } - frame->level = ssl_encryption_application; + frame->level = NGX_QUIC_ENCRYPTION_APPLICATION; frame->type = NGX_QUIC_FT_NEW_CONNECTION_ID; frame->u.ncid.seqnum = sid->seqnum; frame->u.ncid.retire = 0; @@ -485,7 +485,7 @@ ngx_quic_free_client_id(ngx_connection_t *c, ngx_quic_client_id_t *cid) return NGX_ERROR; } - frame->level = ssl_encryption_application; + frame->level = NGX_QUIC_ENCRYPTION_APPLICATION; frame->type = NGX_QUIC_FT_RETIRE_CONNECTION_ID; frame->u.retire_cid.sequence_number = cid->seqnum; diff --git a/src/event/quic/ngx_event_quic_migration.c b/src/event/quic/ngx_event_quic_migration.c index 6befc3427..42354ca66 100644 --- a/src/event/quic/ngx_event_quic_migration.c +++ b/src/event/quic/ngx_event_quic_migration.c @@ -40,7 +40,7 @@ ngx_quic_handle_path_challenge_frame(ngx_connection_t *c, ngx_quic_frame_t *fp; ngx_quic_connection_t *qc; - if (pkt->level != ssl_encryption_application || pkt->path_challenged) { + if (pkt->level != NGX_QUIC_ENCRYPTION_APPLICATION || pkt->path_challenged) { ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic ignoring PATH_CHALLENGE"); return NGX_OK; @@ -55,7 +55,7 @@ ngx_quic_handle_path_challenge_frame(ngx_connection_t *c, return NGX_ERROR; } - fp->level = ssl_encryption_application; + fp->level = NGX_QUIC_ENCRYPTION_APPLICATION; fp->type = NGX_QUIC_FT_PATH_RESPONSE; fp->u.path_response = *f; @@ -93,7 +93,7 @@ ngx_quic_handle_path_challenge_frame(ngx_connection_t *c, return NGX_ERROR; } - fp->level = ssl_encryption_application; + fp->level = NGX_QUIC_ENCRYPTION_APPLICATION; fp->type = NGX_QUIC_FT_PING; ngx_quic_queue_frame(qc, fp); @@ -177,7 +177,7 @@ valid: if (rst) { /* prevent old path packets contribution to congestion control */ - ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_application); + ctx = ngx_quic_get_send_ctx(qc, NGX_QUIC_ENCRYPTION_APPLICATION); qc->rst_pnum = ctx->pnum; ngx_memzero(&qc->congestion, sizeof(ngx_quic_congestion_t)); @@ -549,7 +549,7 @@ ngx_quic_validate_path(ngx_connection_t *c, ngx_quic_path_t *path) (void) ngx_quic_send_path_challenge(c, path); - ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_application); + ctx = ngx_quic_get_send_ctx(qc, NGX_QUIC_ENCRYPTION_APPLICATION); pto = ngx_max(ngx_quic_pto(c, ctx), 1000); path->expires = ngx_current_msec + pto; @@ -579,7 +579,7 @@ ngx_quic_send_path_challenge(ngx_connection_t *c, ngx_quic_path_t *path) return NGX_ERROR; } - frame->level = ssl_encryption_application; + frame->level = NGX_QUIC_ENCRYPTION_APPLICATION; frame->type = NGX_QUIC_FT_PATH_CHALLENGE; ngx_memcpy(frame->u.path_challenge.data, path->challenge[n], 8); @@ -767,7 +767,7 @@ ngx_quic_expire_path_validation(ngx_connection_t *c, ngx_quic_path_t *path) ngx_quic_connection_t *qc; qc = ngx_quic_get_connection(c); - ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_application); + ctx = ngx_quic_get_send_ctx(qc, NGX_QUIC_ENCRYPTION_APPLICATION); if (++path->tries < NGX_QUIC_PATH_RETRIES) { pto = ngx_max(ngx_quic_pto(c, ctx), 1000) << path->tries; @@ -830,7 +830,7 @@ ngx_quic_expire_path_mtu_delay(ngx_connection_t *c, ngx_quic_path_t *path) ngx_quic_connection_t *qc; qc = ngx_quic_get_connection(c); - ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_application); + ctx = ngx_quic_get_send_ctx(qc, NGX_QUIC_ENCRYPTION_APPLICATION); path->tries = 0; @@ -876,7 +876,7 @@ ngx_quic_expire_path_mtu_discovery(ngx_connection_t *c, ngx_quic_path_t *path) ngx_quic_connection_t *qc; qc = ngx_quic_get_connection(c); - ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_application); + ctx = ngx_quic_get_send_ctx(qc, NGX_QUIC_ENCRYPTION_APPLICATION); if (++path->tries < NGX_QUIC_PATH_RETRIES) { rc = ngx_quic_send_path_mtu_probe(c, path); @@ -922,13 +922,13 @@ ngx_quic_send_path_mtu_probe(ngx_connection_t *c, ngx_quic_path_t *path) return NGX_ERROR; } - frame->level = ssl_encryption_application; + frame->level = NGX_QUIC_ENCRYPTION_APPLICATION; frame->type = NGX_QUIC_FT_PING; frame->ignore_loss = 1; frame->ignore_congestion = 1; qc = ngx_quic_get_connection(c); - ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_application); + ctx = ngx_quic_get_send_ctx(qc, NGX_QUIC_ENCRYPTION_APPLICATION); pnum = ctx->pnum; ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0, diff --git a/src/event/quic/ngx_event_quic_openssl_compat.c b/src/event/quic/ngx_event_quic_openssl_compat.c index c5762f155..58298dcb8 100644 --- a/src/event/quic/ngx_event_quic_openssl_compat.c +++ b/src/event/quic/ngx_event_quic_openssl_compat.c @@ -433,8 +433,7 @@ ngx_quic_compat_message_callback(int write_p, int version, int content_type, case SSL3_RT_HANDSHAKE: ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0, - "quic compat tx %s len:%uz ", - ngx_quic_level_name(level), len); + "quic compat tx level:%d len:%uz", level, len); if (com->method->add_handshake_data(ssl, level, buf, len) != 1) { return; @@ -447,8 +446,8 @@ ngx_quic_compat_message_callback(int write_p, int version, int content_type, alert = ((u_char *) buf)[1]; ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0, - "quic compat %s alert:%ui len:%uz ", - ngx_quic_level_name(level), alert, len); + "quic compat level:%d alert:%ui len:%uz", + level, alert, len); if (com->method->send_alert(ssl, level, alert) != 1) { return; @@ -481,8 +480,8 @@ SSL_provide_quic_data(SSL *ssl, enum ssl_encryption_level_t level, c = ngx_ssl_get_connection(ssl); - ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic compat rx %s len:%uz", - ngx_quic_level_name(level), len); + ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0, + "quic compat rx level:%d len:%uz", level, len); qc = ngx_quic_get_connection(c); com = qc->compat; diff --git a/src/event/quic/ngx_event_quic_output.c b/src/event/quic/ngx_event_quic_output.c index 01f1f9113..8c3350504 100644 --- a/src/event/quic/ngx_event_quic_output.c +++ b/src/event/quic/ngx_event_quic_output.c @@ -294,17 +294,17 @@ ngx_quic_allow_segmentation(ngx_connection_t *c) return 0; } - ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_initial); + ctx = ngx_quic_get_send_ctx(qc, NGX_QUIC_ENCRYPTION_INITIAL); if (!ngx_queue_empty(&ctx->frames)) { return 0; } - ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_handshake); + ctx = ngx_quic_get_send_ctx(qc, NGX_QUIC_ENCRYPTION_HANDSHAKE); if (!ngx_queue_empty(&ctx->frames)) { return 0; } - ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_application); + ctx = ngx_quic_get_send_ctx(qc, NGX_QUIC_ENCRYPTION_APPLICATION); bytes = 0; len = ngx_min(qc->path->mtu, NGX_QUIC_MAX_UDP_SEGMENT_BUF); @@ -349,7 +349,7 @@ ngx_quic_create_segments(ngx_connection_t *c) cg = &qc->congestion; path = qc->path; - ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_application); + ctx = ngx_quic_get_send_ctx(qc, NGX_QUIC_ENCRYPTION_APPLICATION); if (ngx_quic_generate_ack(c, ctx) != NGX_OK) { return NGX_ERROR; @@ -500,7 +500,7 @@ ngx_quic_get_padding_level(ngx_connection_t *c) */ qc = ngx_quic_get_connection(c); - ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_initial); + ctx = ngx_quic_get_send_ctx(qc, NGX_QUIC_ENCRYPTION_INITIAL); for (q = ngx_queue_head(&ctx->frames); q != ngx_queue_sentinel(&ctx->frames); @@ -687,10 +687,10 @@ ngx_quic_init_packet(ngx_connection_t *c, ngx_quic_send_ctx_t *ctx, pkt->flags = NGX_QUIC_PKT_FIXED_BIT; - if (ctx->level == ssl_encryption_initial) { + if (ctx->level == NGX_QUIC_ENCRYPTION_INITIAL) { pkt->flags |= NGX_QUIC_PKT_LONG | NGX_QUIC_PKT_INITIAL; - } else if (ctx->level == ssl_encryption_handshake) { + } else if (ctx->level == NGX_QUIC_ENCRYPTION_HANDSHAKE) { pkt->flags |= NGX_QUIC_PKT_LONG | NGX_QUIC_PKT_HANDSHAKE; } else { @@ -1103,7 +1103,7 @@ ngx_quic_send_new_token(ngx_connection_t *c, ngx_quic_path_t *path) return NGX_ERROR; } - frame->level = ssl_encryption_application; + frame->level = NGX_QUIC_ENCRYPTION_APPLICATION; frame->type = NGX_QUIC_FT_NEW_TOKEN; frame->data = out; frame->u.token.length = token.len; diff --git a/src/event/quic/ngx_event_quic_protection.c b/src/event/quic/ngx_event_quic_protection.c index e5c0df7b4..885843d72 100644 --- a/src/event/quic/ngx_event_quic_protection.c +++ b/src/event/quic/ngx_event_quic_protection.c @@ -130,8 +130,8 @@ ngx_quic_keys_set_initial_secret(ngx_quic_keys_t *keys, ngx_str_t *secret, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a }; - client = &keys->secrets[ssl_encryption_initial].client; - server = &keys->secrets[ssl_encryption_initial].server; + client = &keys->secrets[NGX_QUIC_ENCRYPTION_INITIAL].client; + server = &keys->secrets[NGX_QUIC_ENCRYPTION_INITIAL].server; /* * RFC 9001, section 5. Packet Protection @@ -656,8 +656,8 @@ ngx_quic_crypto_hp_cleanup(ngx_quic_secret_t *s) ngx_int_t ngx_quic_keys_set_encryption_secret(ngx_log_t *log, ngx_uint_t is_write, - ngx_quic_keys_t *keys, enum ssl_encryption_level_t level, - const SSL_CIPHER *cipher, const uint8_t *secret, size_t secret_len) + ngx_quic_keys_t *keys, ngx_uint_t level, const SSL_CIPHER *cipher, + const uint8_t *secret, size_t secret_len) { ngx_int_t key_len; ngx_str_t secret_str; @@ -722,8 +722,8 @@ ngx_quic_keys_set_encryption_secret(ngx_log_t *log, ngx_uint_t is_write, ngx_uint_t -ngx_quic_keys_available(ngx_quic_keys_t *keys, - enum ssl_encryption_level_t level, ngx_uint_t is_write) +ngx_quic_keys_available(ngx_quic_keys_t *keys, ngx_uint_t level, + ngx_uint_t is_write) { if (is_write == 0) { return keys->secrets[level].client.ctx != NULL; @@ -734,8 +734,7 @@ ngx_quic_keys_available(ngx_quic_keys_t *keys, void -ngx_quic_keys_discard(ngx_quic_keys_t *keys, - enum ssl_encryption_level_t level) +ngx_quic_keys_discard(ngx_quic_keys_t *keys, ngx_uint_t level) { ngx_quic_secret_t *client, *server; @@ -765,7 +764,7 @@ ngx_quic_keys_switch(ngx_connection_t *c, ngx_quic_keys_t *keys) { ngx_quic_secrets_t *current, *next, tmp; - current = &keys->secrets[ssl_encryption_application]; + current = &keys->secrets[NGX_QUIC_ENCRYPTION_APPLICATION]; next = &keys->next_key; ngx_quic_crypto_cleanup(¤t->client); @@ -794,7 +793,7 @@ ngx_quic_keys_update(ngx_event_t *ev) qc = ngx_quic_get_connection(c); keys = qc->keys; - current = &keys->secrets[ssl_encryption_application]; + current = &keys->secrets[NGX_QUIC_ENCRYPTION_APPLICATION]; next = &keys->next_key; ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic key update"); diff --git a/src/event/quic/ngx_event_quic_protection.h b/src/event/quic/ngx_event_quic_protection.h index c09456f53..fddc6083a 100644 --- a/src/event/quic/ngx_event_quic_protection.h +++ b/src/event/quic/ngx_event_quic_protection.h @@ -14,8 +14,6 @@ #include -#define NGX_QUIC_ENCRYPTION_LAST ((ssl_encryption_application) + 1) - /* RFC 5116, 5.1/5.3 and RFC 8439, 2.3/2.5 for all supported ciphers */ #define NGX_QUIC_IV_LEN 12 #define NGX_QUIC_TAG_LEN 16 @@ -94,13 +92,11 @@ typedef struct { ngx_int_t ngx_quic_keys_set_initial_secret(ngx_quic_keys_t *keys, ngx_str_t *secret, ngx_log_t *log); ngx_int_t ngx_quic_keys_set_encryption_secret(ngx_log_t *log, - ngx_uint_t is_write, ngx_quic_keys_t *keys, - enum ssl_encryption_level_t level, const SSL_CIPHER *cipher, - const uint8_t *secret, size_t secret_len); -ngx_uint_t ngx_quic_keys_available(ngx_quic_keys_t *keys, - enum ssl_encryption_level_t level, ngx_uint_t is_write); -void ngx_quic_keys_discard(ngx_quic_keys_t *keys, - enum ssl_encryption_level_t level); + ngx_uint_t is_write, ngx_quic_keys_t *keys, ngx_uint_t level, + const SSL_CIPHER *cipher, const uint8_t *secret, size_t secret_len); +ngx_uint_t ngx_quic_keys_available(ngx_quic_keys_t *keys, ngx_uint_t level, + ngx_uint_t is_write); +void ngx_quic_keys_discard(ngx_quic_keys_t *keys, ngx_uint_t level); void ngx_quic_keys_switch(ngx_connection_t *c, ngx_quic_keys_t *keys); void ngx_quic_keys_update(ngx_event_t *ev); void ngx_quic_keys_cleanup(ngx_quic_keys_t *keys); diff --git a/src/event/quic/ngx_event_quic_ssl.c b/src/event/quic/ngx_event_quic_ssl.c index c9ebd70bc..fc8ebd8cf 100644 --- a/src/event/quic/ngx_event_quic_ssl.c +++ b/src/event/quic/ngx_event_quic_ssl.c @@ -18,44 +18,65 @@ #define NGX_QUIC_MAX_BUFFERED 65535 +static ngx_inline ngx_uint_t ngx_quic_map_encryption_level( + enum ssl_encryption_level_t ssl_level); + #if (NGX_QUIC_BORINGSSL_API) static int ngx_quic_set_read_secret(ngx_ssl_conn_t *ssl_conn, - enum ssl_encryption_level_t level, const SSL_CIPHER *cipher, + enum ssl_encryption_level_t ssl_level, const SSL_CIPHER *cipher, const uint8_t *secret, size_t secret_len); static int ngx_quic_set_write_secret(ngx_ssl_conn_t *ssl_conn, - enum ssl_encryption_level_t level, const SSL_CIPHER *cipher, + enum ssl_encryption_level_t ssl_level, const SSL_CIPHER *cipher, const uint8_t *secret, size_t secret_len); #else /* NGX_QUIC_QUICTLS_API */ static int ngx_quic_set_encryption_secrets(ngx_ssl_conn_t *ssl_conn, - enum ssl_encryption_level_t level, const uint8_t *read_secret, + enum ssl_encryption_level_t ssl_level, const uint8_t *read_secret, const uint8_t *write_secret, size_t secret_len); #endif static int ngx_quic_add_handshake_data(ngx_ssl_conn_t *ssl_conn, - enum ssl_encryption_level_t level, const uint8_t *data, size_t len); + enum ssl_encryption_level_t ssl_level, const uint8_t *data, size_t len); static int ngx_quic_flush_flight(ngx_ssl_conn_t *ssl_conn); static int ngx_quic_send_alert(ngx_ssl_conn_t *ssl_conn, - enum ssl_encryption_level_t level, uint8_t alert); + enum ssl_encryption_level_t ssl_level, uint8_t alert); static ngx_int_t ngx_quic_handshake(ngx_connection_t *c); static ngx_int_t ngx_quic_crypto_provide(ngx_connection_t *c, ngx_chain_t *out, - enum ssl_encryption_level_t level); + ngx_uint_t level); + + +static ngx_inline ngx_uint_t +ngx_quic_map_encryption_level(enum ssl_encryption_level_t ssl_level) +{ + switch (ssl_level) { + case ssl_encryption_initial: + return NGX_QUIC_ENCRYPTION_INITIAL; + case ssl_encryption_early_data: + return NGX_QUIC_ENCRYPTION_EARLY_DATA; + case ssl_encryption_handshake: + return NGX_QUIC_ENCRYPTION_HANDSHAKE; + default: /* ssl_encryption_application */ + return NGX_QUIC_ENCRYPTION_APPLICATION; + } +} #if (NGX_QUIC_BORINGSSL_API) static int ngx_quic_set_read_secret(ngx_ssl_conn_t *ssl_conn, - enum ssl_encryption_level_t level, const SSL_CIPHER *cipher, + enum ssl_encryption_level_t ssl_level, const SSL_CIPHER *cipher, const uint8_t *rsecret, size_t secret_len) { + ngx_uint_t level; ngx_connection_t *c; ngx_quic_connection_t *qc; c = ngx_ssl_get_connection(ssl_conn); qc = ngx_quic_get_connection(c); + level = ngx_quic_map_encryption_level(ssl_level); ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, - "quic ngx_quic_set_read_secret() level:%d", level); + "quic ngx_quic_set_read_secret() level:%d", ssl_level); #ifdef NGX_QUIC_DEBUG_CRYPTO ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic read secret len:%uz %*xs", secret_len, @@ -75,17 +96,19 @@ ngx_quic_set_read_secret(ngx_ssl_conn_t *ssl_conn, static int ngx_quic_set_write_secret(ngx_ssl_conn_t *ssl_conn, - enum ssl_encryption_level_t level, const SSL_CIPHER *cipher, + enum ssl_encryption_level_t ssl_level, const SSL_CIPHER *cipher, const uint8_t *wsecret, size_t secret_len) { + ngx_uint_t level; ngx_connection_t *c; ngx_quic_connection_t *qc; c = ngx_ssl_get_connection(ssl_conn); qc = ngx_quic_get_connection(c); + level = ngx_quic_map_encryption_level(ssl_level); ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, - "quic ngx_quic_set_write_secret() level:%d", level); + "quic ngx_quic_set_write_secret() level:%d", ssl_level); #ifdef NGX_QUIC_DEBUG_CRYPTO ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic write secret len:%uz %*xs", secret_len, @@ -106,18 +129,21 @@ ngx_quic_set_write_secret(ngx_ssl_conn_t *ssl_conn, static int ngx_quic_set_encryption_secrets(ngx_ssl_conn_t *ssl_conn, - enum ssl_encryption_level_t level, const uint8_t *rsecret, + enum ssl_encryption_level_t ssl_level, const uint8_t *rsecret, const uint8_t *wsecret, size_t secret_len) { + ngx_uint_t level; ngx_connection_t *c; const SSL_CIPHER *cipher; ngx_quic_connection_t *qc; c = ngx_ssl_get_connection(ssl_conn); qc = ngx_quic_get_connection(c); + level = ngx_quic_map_encryption_level(ssl_level); ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, - "quic ngx_quic_set_encryption_secrets() level:%d", level); + "quic ngx_quic_set_encryption_secrets() level:%d", + ssl_level); #ifdef NGX_QUIC_DEBUG_CRYPTO ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic read secret len:%uz %*xs", secret_len, @@ -134,7 +160,7 @@ ngx_quic_set_encryption_secrets(ngx_ssl_conn_t *ssl_conn, return 1; } - if (level == ssl_encryption_early_data) { + if (level == NGX_QUIC_ENCRYPTION_EARLY_DATA) { return 1; } @@ -159,10 +185,11 @@ ngx_quic_set_encryption_secrets(ngx_ssl_conn_t *ssl_conn, static int ngx_quic_add_handshake_data(ngx_ssl_conn_t *ssl_conn, - enum ssl_encryption_level_t level, const uint8_t *data, size_t len) + enum ssl_encryption_level_t ssl_level, const uint8_t *data, size_t len) { u_char *p, *end; size_t client_params_len; + ngx_uint_t level; ngx_chain_t *out; unsigned int alpn_len; const uint8_t *client_params; @@ -175,6 +202,7 @@ ngx_quic_add_handshake_data(ngx_ssl_conn_t *ssl_conn, c = ngx_ssl_get_connection(ssl_conn); qc = ngx_quic_get_connection(c); + level = ngx_quic_map_encryption_level(ssl_level); ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic ngx_quic_add_handshake_data"); @@ -287,8 +315,8 @@ ngx_quic_flush_flight(ngx_ssl_conn_t *ssl_conn) static int -ngx_quic_send_alert(ngx_ssl_conn_t *ssl_conn, enum ssl_encryption_level_t level, - uint8_t alert) +ngx_quic_send_alert(ngx_ssl_conn_t *ssl_conn, + enum ssl_encryption_level_t ssl_level, uint8_t alert) { ngx_connection_t *c; ngx_quic_connection_t *qc; @@ -296,8 +324,8 @@ ngx_quic_send_alert(ngx_ssl_conn_t *ssl_conn, enum ssl_encryption_level_t level, c = ngx_ssl_get_connection(ssl_conn); ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0, - "quic ngx_quic_send_alert() level:%s alert:%d", - ngx_quic_level_name(level), (int) alert); + "quic ngx_quic_send_alert() level:%d alert:%d", + ssl_level, (int) alert); /* already closed on regular shutdown */ @@ -341,13 +369,13 @@ ngx_quic_handle_crypto_frame(ngx_connection_t *c, ngx_quic_header_t *pkt, } if (last <= ctx->crypto.offset) { - if (pkt->level == ssl_encryption_initial) { + if (pkt->level == NGX_QUIC_ENCRYPTION_INITIAL) { /* speeding up handshake completion */ if (!ngx_queue_empty(&ctx->sent)) { ngx_quic_resend_frames(c, ctx); - ctx = ngx_quic_get_send_ctx(qc, ssl_encryption_handshake); + ctx = ngx_quic_get_send_ctx(qc, NGX_QUIC_ENCRYPTION_HANDSHAKE); while (!ngx_queue_empty(&ctx->sent)) { ngx_quic_resend_frames(c, ctx); } @@ -436,7 +464,7 @@ ngx_quic_handshake(ngx_connection_t *c) } if (n <= 0 || SSL_in_init(ssl_conn)) { - if (ngx_quic_keys_available(qc->keys, ssl_encryption_early_data, 0) + if (ngx_quic_keys_available(qc->keys, NGX_QUIC_ENCRYPTION_EARLY_DATA, 0) && qc->client_tp_done) { if (ngx_quic_init_streams(c) != NGX_OK) { @@ -458,7 +486,7 @@ ngx_quic_handshake(ngx_connection_t *c) return NGX_ERROR; } - frame->level = ssl_encryption_application; + frame->level = NGX_QUIC_ENCRYPTION_APPLICATION; frame->type = NGX_QUIC_FT_HANDSHAKE_DONE; ngx_quic_queue_frame(qc, frame); @@ -482,7 +510,7 @@ ngx_quic_handshake(ngx_connection_t *c) * An endpoint MUST discard its Handshake keys * when the TLS handshake is confirmed. */ - ngx_quic_discard_ctx(c, ssl_encryption_handshake); + ngx_quic_discard_ctx(c, NGX_QUIC_ENCRYPTION_HANDSHAKE); ngx_quic_discover_path_mtu(c, qc->path); @@ -501,15 +529,31 @@ ngx_quic_handshake(ngx_connection_t *c) static ngx_int_t ngx_quic_crypto_provide(ngx_connection_t *c, ngx_chain_t *out, - enum ssl_encryption_level_t level) + ngx_uint_t level) { - ngx_buf_t *b; - ngx_chain_t *cl; + ngx_buf_t *b; + ngx_chain_t *cl; + enum ssl_encryption_level_t ssl_level; + + switch (level) { + case NGX_QUIC_ENCRYPTION_INITIAL: + ssl_level = ssl_encryption_initial; + break; + case NGX_QUIC_ENCRYPTION_EARLY_DATA: + ssl_level = ssl_encryption_early_data; + break; + case NGX_QUIC_ENCRYPTION_HANDSHAKE: + ssl_level = ssl_encryption_handshake; + break; + default: /* NGX_QUIC_ENCRYPTION_APPLICATION */ + ssl_level = ssl_encryption_application; + break; + } for (cl = out; cl; cl = cl->next) { b = cl->buf; - if (!SSL_provide_quic_data(c->ssl->connection, level, b->pos, + if (!SSL_provide_quic_data(c->ssl->connection, ssl_level, b->pos, b->last - b->pos)) { ngx_ssl_error(NGX_LOG_ALERT, c->log, 0, diff --git a/src/event/quic/ngx_event_quic_streams.c b/src/event/quic/ngx_event_quic_streams.c index a9a21f578..18fffeabe 100644 --- a/src/event/quic/ngx_event_quic_streams.c +++ b/src/event/quic/ngx_event_quic_streams.c @@ -280,7 +280,7 @@ ngx_quic_do_reset_stream(ngx_quic_stream_t *qs, ngx_uint_t err) return NGX_ERROR; } - frame->level = ssl_encryption_application; + frame->level = NGX_QUIC_ENCRYPTION_APPLICATION; frame->type = NGX_QUIC_FT_RESET_STREAM; frame->u.reset_stream.id = qs->id; frame->u.reset_stream.error_code = err; @@ -367,7 +367,7 @@ ngx_quic_shutdown_stream_recv(ngx_connection_t *c) ngx_log_debug1(NGX_LOG_DEBUG_EVENT, pc->log, 0, "quic stream id:0x%xL recv shutdown", qs->id); - frame->level = ssl_encryption_application; + frame->level = NGX_QUIC_ENCRYPTION_APPLICATION; frame->type = NGX_QUIC_FT_STOP_SENDING; frame->u.stop_sending.id = qs->id; frame->u.stop_sending.error_code = qc->conf->stream_close_code; @@ -527,7 +527,7 @@ ngx_quic_reject_stream(ngx_connection_t *c, uint64_t id) return NGX_ERROR; } - frame->level = ssl_encryption_application; + frame->level = NGX_QUIC_ENCRYPTION_APPLICATION; frame->type = NGX_QUIC_FT_RESET_STREAM; frame->u.reset_stream.id = id; frame->u.reset_stream.error_code = code; @@ -540,7 +540,7 @@ ngx_quic_reject_stream(ngx_connection_t *c, uint64_t id) return NGX_ERROR; } - frame->level = ssl_encryption_application; + frame->level = NGX_QUIC_ENCRYPTION_APPLICATION; frame->type = NGX_QUIC_FT_STOP_SENDING; frame->u.stop_sending.id = id; frame->u.stop_sending.error_code = code; @@ -1062,7 +1062,7 @@ ngx_quic_stream_flush(ngx_quic_stream_t *qs) return NGX_ERROR; } - frame->level = ssl_encryption_application; + frame->level = NGX_QUIC_ENCRYPTION_APPLICATION; frame->type = NGX_QUIC_FT_STREAM; frame->data = out; @@ -1180,7 +1180,7 @@ ngx_quic_close_stream(ngx_quic_stream_t *qs) return NGX_ERROR; } - frame->level = ssl_encryption_application; + frame->level = NGX_QUIC_ENCRYPTION_APPLICATION; frame->type = NGX_QUIC_FT_MAX_STREAMS; if (qs->id & NGX_QUIC_STREAM_UNIDIRECTIONAL) { @@ -1771,7 +1771,7 @@ ngx_quic_update_max_stream_data(ngx_quic_stream_t *qs) return NGX_ERROR; } - frame->level = ssl_encryption_application; + frame->level = NGX_QUIC_ENCRYPTION_APPLICATION; frame->type = NGX_QUIC_FT_MAX_STREAM_DATA; frame->u.max_stream_data.id = qs->id; frame->u.max_stream_data.limit = qs->recv_max_data; @@ -1807,7 +1807,7 @@ ngx_quic_update_max_data(ngx_connection_t *c) return NGX_ERROR; } - frame->level = ssl_encryption_application; + frame->level = NGX_QUIC_ENCRYPTION_APPLICATION; frame->type = NGX_QUIC_FT_MAX_DATA; frame->u.max_data.max_data = qc->streams.recv_max_data; diff --git a/src/event/quic/ngx_event_quic_transport.c b/src/event/quic/ngx_event_quic_transport.c index 0b3ef4b2e..ba6211c33 100644 --- a/src/event/quic/ngx_event_quic_transport.c +++ b/src/event/quic/ngx_event_quic_transport.c @@ -281,7 +281,7 @@ ngx_int_t ngx_quic_parse_packet(ngx_quic_header_t *pkt) { if (!ngx_quic_long_pkt(pkt->flags)) { - pkt->level = ssl_encryption_application; + pkt->level = NGX_QUIC_ENCRYPTION_APPLICATION; if (ngx_quic_parse_short_header(pkt, NGX_QUIC_SERVER_CID_LEN) != NGX_OK) { @@ -468,13 +468,13 @@ ngx_quic_parse_long_header_v1(ngx_quic_header_t *pkt) return NGX_ERROR; } - pkt->level = ssl_encryption_initial; + pkt->level = NGX_QUIC_ENCRYPTION_INITIAL; } else if (ngx_quic_pkt_zrtt(pkt->flags)) { - pkt->level = ssl_encryption_early_data; + pkt->level = NGX_QUIC_ENCRYPTION_EARLY_DATA; } else if (ngx_quic_pkt_hs(pkt->flags)) { - pkt->level = ssl_encryption_handshake; + pkt->level = NGX_QUIC_ENCRYPTION_HANDSHAKE; } else { ngx_log_error(NGX_LOG_INFO, pkt->log, 0, @@ -593,7 +593,7 @@ ngx_quic_payload_size(ngx_quic_header_t *pkt, size_t pkt_len) /* flags, version, dcid and scid with lengths and zero-length token */ len = 5 + 2 + pkt->dcid.len + pkt->scid.len - + (pkt->level == ssl_encryption_initial ? 1 : 0); + + (pkt->level == NGX_QUIC_ENCRYPTION_INITIAL ? 1 : 0); if (len > pkt_len) { return 0; @@ -632,7 +632,7 @@ ngx_quic_create_long_header(ngx_quic_header_t *pkt, u_char *out, if (out == NULL) { return 5 + 2 + pkt->dcid.len + pkt->scid.len + ngx_quic_varint_len(rem_len) + pkt->num_len - + (pkt->level == ssl_encryption_initial ? 1 : 0); + + (pkt->level == NGX_QUIC_ENCRYPTION_INITIAL ? 1 : 0); } p = start = out; @@ -647,7 +647,7 @@ ngx_quic_create_long_header(ngx_quic_header_t *pkt, u_char *out, *p++ = pkt->scid.len; p = ngx_cpymem(p, pkt->scid.data, pkt->scid.len); - if (pkt->level == ssl_encryption_initial) { + if (pkt->level == NGX_QUIC_ENCRYPTION_INITIAL) { ngx_quic_build_int(&p, 0); } diff --git a/src/event/quic/ngx_event_quic_transport.h b/src/event/quic/ngx_event_quic_transport.h index dcd763df1..656cb09fb 100644 --- a/src/event/quic/ngx_event_quic_transport.h +++ b/src/event/quic/ngx_event_quic_transport.h @@ -47,9 +47,9 @@ (ngx_quic_long_pkt(flags) ? 0x0F : 0x1F) #define ngx_quic_level_name(lvl) \ - (lvl == ssl_encryption_application) ? "app" \ - : (lvl == ssl_encryption_initial) ? "init" \ - : (lvl == ssl_encryption_handshake) ? "hs" : "early" + (lvl == NGX_QUIC_ENCRYPTION_APPLICATION) ? "app" \ + : (lvl == NGX_QUIC_ENCRYPTION_INITIAL) ? "init" \ + : (lvl == NGX_QUIC_ENCRYPTION_HANDSHAKE) ? "hs" : "early" #define NGX_QUIC_MAX_CID_LEN 20 #define NGX_QUIC_SERVER_CID_LEN NGX_QUIC_MAX_CID_LEN @@ -262,7 +262,7 @@ typedef struct ngx_quic_frame_s ngx_quic_frame_t; struct ngx_quic_frame_s { ngx_uint_t type; - enum ssl_encryption_level_t level; + ngx_uint_t level; ngx_queue_t queue; uint64_t pnum; size_t plen; @@ -310,7 +310,7 @@ typedef struct { uint8_t flags; uint32_t version; ngx_str_t token; - enum ssl_encryption_level_t level; + ngx_uint_t level; ngx_uint_t error; /* filled in by parser */ From noreply at nginx.com Fri May 23 11:01:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 23 May 2025 11:01:02 +0000 (UTC) Subject: [nginx] QUIC: removed excessive casts for ngx_ssl_get_connection(). Message-ID: <20250523110102.E8F5548F5C@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/39b1e3fe9de7966c2c66e228cf7b205616de6ee2 branches: master commit: 39b1e3fe9de7966c2c66e228cf7b205616de6ee2 user: Sergey Kandaurov date: Tue, 6 May 2025 15:53:49 +0400 description: QUIC: removed excessive casts for ngx_ssl_get_connection(). They were blindly copied from ngx_ssl_info_callback(), where the ngx_ssl_conn_t pointer is passed with const qualifier. --- src/event/quic/ngx_event_quic_ssl.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/event/quic/ngx_event_quic_ssl.c b/src/event/quic/ngx_event_quic_ssl.c index ba0b5929f..6ad3c0e93 100644 --- a/src/event/quic/ngx_event_quic_ssl.c +++ b/src/event/quic/ngx_event_quic_ssl.c @@ -57,7 +57,7 @@ ngx_quic_set_read_secret(ngx_ssl_conn_t *ssl_conn, ngx_connection_t *c; ngx_quic_connection_t *qc; - c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn); + c = ngx_ssl_get_connection(ssl_conn); qc = ngx_quic_get_connection(c); ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, @@ -87,7 +87,7 @@ ngx_quic_set_write_secret(ngx_ssl_conn_t *ssl_conn, ngx_connection_t *c; ngx_quic_connection_t *qc; - c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn); + c = ngx_ssl_get_connection(ssl_conn); qc = ngx_quic_get_connection(c); ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, @@ -119,7 +119,7 @@ ngx_quic_set_encryption_secrets(ngx_ssl_conn_t *ssl_conn, const SSL_CIPHER *cipher; ngx_quic_connection_t *qc; - c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn); + c = ngx_ssl_get_connection(ssl_conn); qc = ngx_quic_get_connection(c); ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, @@ -180,7 +180,7 @@ ngx_quic_add_handshake_data(ngx_ssl_conn_t *ssl_conn, const unsigned char *alpn_data; #endif - c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn); + c = ngx_ssl_get_connection(ssl_conn); qc = ngx_quic_get_connection(c); ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, @@ -279,7 +279,7 @@ ngx_quic_flush_flight(ngx_ssl_conn_t *ssl_conn) #if (NGX_DEBUG) ngx_connection_t *c; - c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn); + c = ngx_ssl_get_connection(ssl_conn); ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic ngx_quic_flush_flight()"); @@ -295,7 +295,7 @@ ngx_quic_send_alert(ngx_ssl_conn_t *ssl_conn, enum ssl_encryption_level_t level, ngx_connection_t *c; ngx_quic_connection_t *qc; - c = ngx_ssl_get_connection((ngx_ssl_conn_t *) ssl_conn); + c = ngx_ssl_get_connection(ssl_conn); ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic ngx_quic_send_alert() level:%s alert:%d", From noreply at nginx.com Fri May 23 11:01:03 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 23 May 2025 11:01:03 +0000 (UTC) Subject: [nginx] QUIC: adjusted handling of callback errors. Message-ID: <20250523110103.0820448FEA@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/7468a10b62276be4adee0fcd6aaf6244270984ab branches: master commit: 7468a10b62276be4adee0fcd6aaf6244270984ab user: Sergey Kandaurov date: Tue, 13 May 2025 20:12:10 +0400 description: QUIC: adjusted handling of callback errors. Changed handshake callbacks to always return success. This allows to avoid logging SSL_do_handshake() errors with empty or cryptic "internal error" OpenSSL error messages at the inappropriate "crit" log level. Further, connections with failed callbacks are closed now right away when using OpenSSL compat layer. This change supersedes and reverts c37fdcdd1, with the conditions to check callbacks invocation kept to slightly improve code readability of control flow; they are optimized out in the resulting assembly code. --- src/event/quic/ngx_event_quic.c | 3 +++ src/event/quic/ngx_event_quic_openssl_compat.c | 8 ++------ src/event/quic/ngx_event_quic_ssl.c | 27 ++++++++++++++++---------- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/event/quic/ngx_event_quic.c b/src/event/quic/ngx_event_quic.c index 4682ecad9..a4ad85d56 100644 --- a/src/event/quic/ngx_event_quic.c +++ b/src/event/quic/ngx_event_quic.c @@ -135,6 +135,9 @@ ngx_quic_apply_transport_params(ngx_connection_t *c, ngx_quic_tp_t *ctp) if (scid.len != ctp->initial_scid.len || ngx_memcmp(scid.data, ctp->initial_scid.data, scid.len) != 0) { + qc->error = NGX_QUIC_ERR_TRANSPORT_PARAMETER_ERROR; + qc->error_reason = "invalid initial_source_connection_id"; + ngx_log_error(NGX_LOG_INFO, c->log, 0, "quic client initial_source_connection_id mismatch"); return NGX_ERROR; diff --git a/src/event/quic/ngx_event_quic_openssl_compat.c b/src/event/quic/ngx_event_quic_openssl_compat.c index a4a8ea1b6..c5762f155 100644 --- a/src/event/quic/ngx_event_quic_openssl_compat.c +++ b/src/event/quic/ngx_event_quic_openssl_compat.c @@ -437,7 +437,7 @@ ngx_quic_compat_message_callback(int write_p, int version, int content_type, ngx_quic_level_name(level), len); if (com->method->add_handshake_data(ssl, level, buf, len) != 1) { - goto failed; + return; } break; @@ -451,7 +451,7 @@ ngx_quic_compat_message_callback(int write_p, int version, int content_type, ngx_quic_level_name(level), alert, len); if (com->method->send_alert(ssl, level, alert) != 1) { - goto failed; + return; } } @@ -459,10 +459,6 @@ ngx_quic_compat_message_callback(int write_p, int version, int content_type, } return; - -failed: - - ngx_post_event(&qc->close, &ngx_posted_events); } diff --git a/src/event/quic/ngx_event_quic_ssl.c b/src/event/quic/ngx_event_quic_ssl.c index 4f7060ce4..dd7ee3702 100644 --- a/src/event/quic/ngx_event_quic_ssl.c +++ b/src/event/quic/ngx_event_quic_ssl.c @@ -72,7 +72,7 @@ ngx_quic_set_read_secret(ngx_ssl_conn_t *ssl_conn, cipher, rsecret, secret_len) != NGX_OK) { - return 0; + qc->error = NGX_QUIC_ERR_INTERNAL_ERROR; } return 1; @@ -102,7 +102,7 @@ ngx_quic_set_write_secret(ngx_ssl_conn_t *ssl_conn, cipher, wsecret, secret_len) != NGX_OK) { - return 0; + qc->error = NGX_QUIC_ERR_INTERNAL_ERROR; } return 1; @@ -136,7 +136,8 @@ ngx_quic_set_encryption_secrets(ngx_ssl_conn_t *ssl_conn, cipher, rsecret, secret_len) != NGX_OK) { - return 0; + qc->error = NGX_QUIC_ERR_INTERNAL_ERROR; + return 1; } if (level == ssl_encryption_early_data) { @@ -153,7 +154,7 @@ ngx_quic_set_encryption_secrets(ngx_ssl_conn_t *ssl_conn, cipher, wsecret, secret_len) != NGX_OK) { - return 0; + qc->error = NGX_QUIC_ERR_INTERNAL_ERROR; } return 1; @@ -199,7 +200,7 @@ ngx_quic_add_handshake_data(ngx_ssl_conn_t *ssl_conn, ngx_log_error(NGX_LOG_INFO, c->log, 0, "quic unsupported protocol in ALPN extension"); - return 0; + return 1; } SSL_get_peer_quic_transport_params(ssl_conn, &client_params, @@ -216,7 +217,7 @@ ngx_quic_add_handshake_data(ngx_ssl_conn_t *ssl_conn, ngx_log_error(NGX_LOG_INFO, c->log, 0, "missing transport parameters"); - return 0; + return 1; } p = (u_char *) client_params; @@ -231,11 +232,11 @@ ngx_quic_add_handshake_data(ngx_ssl_conn_t *ssl_conn, qc->error = NGX_QUIC_ERR_TRANSPORT_PARAMETER_ERROR; qc->error_reason = "failed to process transport parameters"; - return 0; + return 1; } if (ngx_quic_apply_transport_params(c, &ctp) != NGX_OK) { - return 0; + return 1; } qc->client_tp_done = 1; @@ -245,12 +246,14 @@ ngx_quic_add_handshake_data(ngx_ssl_conn_t *ssl_conn, out = ngx_quic_copy_buffer(c, (u_char *) data, len); if (out == NGX_CHAIN_ERROR) { - return 0; + qc->error = NGX_QUIC_ERR_INTERNAL_ERROR; + return 1; } frame = ngx_quic_alloc_frame(c); if (frame == NULL) { - return 0; + qc->error = NGX_QUIC_ERR_INTERNAL_ERROR; + return 1; } frame->data = out; @@ -412,6 +415,10 @@ ngx_quic_crypto_input(ngx_connection_t *c, ngx_chain_t *data, ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "SSL_do_handshake: %d", n); + if (qc->error != (ngx_uint_t) -1) { + return NGX_ERROR; + } + if (n <= 0) { sslerr = SSL_get_error(ssl_conn, n); From noreply at nginx.com Fri May 23 11:01:03 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 23 May 2025 11:01:03 +0000 (UTC) Subject: [nginx] QUIC: logging of SSL library errors. Message-ID: <20250523110103.0334448FDD@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/47f96993f669543c6cb4979dd3f680ad01314ee5 branches: master commit: 47f96993f669543c6cb4979dd3f680ad01314ee5 user: Sergey Kandaurov date: Wed, 21 May 2025 19:55:31 +0400 description: QUIC: logging of SSL library errors. Logging level for such errors, which should not normally happen, is changed to NGX_LOG_ALERT, and ngx_log_error() is replaced with ngx_ssl_error() for consistency with the rest of the code. --- src/event/quic/ngx_event_quic_ssl.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/event/quic/ngx_event_quic_ssl.c b/src/event/quic/ngx_event_quic_ssl.c index ddc6c7c3b..4f7060ce4 100644 --- a/src/event/quic/ngx_event_quic_ssl.c +++ b/src/event/quic/ngx_event_quic_ssl.c @@ -402,7 +402,7 @@ ngx_quic_crypto_input(ngx_connection_t *c, ngx_chain_t *data, b = cl->buf; if (!SSL_provide_quic_data(ssl_conn, level, b->pos, b->last - b->pos)) { - ngx_ssl_error(NGX_LOG_INFO, c->log, 0, + ngx_ssl_error(NGX_LOG_ALERT, c->log, 0, "SSL_provide_quic_data() failed"); return NGX_ERROR; } @@ -531,7 +531,7 @@ ngx_quic_init_connection(ngx_connection_t *c) } if (SSL_set_quic_method(ssl_conn, &quic_method) == 0) { - ngx_log_error(NGX_LOG_INFO, c->log, 0, + ngx_ssl_error(NGX_LOG_ALERT, c->log, 0, "quic SSL_set_quic_method() failed"); return NGX_ERROR; } @@ -572,14 +572,14 @@ ngx_quic_init_connection(ngx_connection_t *c) #endif if (SSL_set_quic_transport_params(ssl_conn, p, len) == 0) { - ngx_log_error(NGX_LOG_INFO, c->log, 0, + ngx_ssl_error(NGX_LOG_ALERT, c->log, 0, "quic SSL_set_quic_transport_params() failed"); return NGX_ERROR; } #ifdef OPENSSL_IS_BORINGSSL if (SSL_set_quic_early_data_context(ssl_conn, p, clen) == 0) { - ngx_log_error(NGX_LOG_INFO, c->log, 0, + ngx_ssl_error(NGX_LOG_ALERT, c->log, 0, "quic SSL_set_quic_early_data_context() failed"); return NGX_ERROR; } From noreply at nginx.com Mon May 26 22:00:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Mon, 26 May 2025 22:00:02 +0000 (UTC) Subject: [nginx] Core: added support for TCP keepalive parameters on macOS. Message-ID: <20250526220002.A3FA949016@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/5b8a5c08ce28639e788734b2528faad70baa113c branches: master commit: 5b8a5c08ce28639e788734b2528faad70baa113c user: Sergey Kandaurov date: Mon, 26 May 2025 16:11:36 +0400 description: Core: added support for TCP keepalive parameters on macOS. The support first appeared in OS X Mavericks 10.9 and documented since OS X Yosemite 10.10. It has a subtle implementation difference from other operating systems in that the TCP_KEEPALIVE socket option (used in place of TCP_KEEPIDLE) isn't inherited from a listening socket to an accepted socket. An apparent reason for this behaviour is that it might be preserved for the sake of backward compatibility. The TCP_KEEPALIVE socket option is not inherited since appearance in OS X Panther 10.3, which long predates two other TCP_KEEPINTVL and TCP_KEEPCNT socket options. Thanks to Andy Pan for initial work. --- auto/os/darwin | 16 ++++++++++++++++ auto/unix | 26 ++++++++++++++------------ src/core/ngx_connection.c | 4 ++++ src/event/ngx_event_accept.c | 17 +++++++++++++++++ 4 files changed, 51 insertions(+), 12 deletions(-) diff --git a/auto/os/darwin b/auto/os/darwin index 429468f7f..0ede28d0a 100644 --- a/auto/os/darwin +++ b/auto/os/darwin @@ -118,3 +118,19 @@ ngx_feature_libs= ngx_feature_test="int32_t lock = 0; if (!OSAtomicCompareAndSwap32Barrier(0, 1, &lock)) return 1" . auto/feature + + +ngx_feature="TCP_KEEPALIVE" +ngx_feature_name="NGX_HAVE_KEEPALIVE_TUNABLE" +ngx_feature_run=no +ngx_feature_incs="#include + #include + #include " +ngx_feature_path= +ngx_feature_libs= +ngx_feature_test="setsockopt(0, IPPROTO_TCP, TCP_KEEPALIVE, NULL, 0); + setsockopt(0, IPPROTO_TCP, TCP_KEEPINTVL, NULL, 0); + setsockopt(0, IPPROTO_TCP, TCP_KEEPCNT, NULL, 0)" +. auto/feature + +NGX_KEEPALIVE_CHECKED=YES diff --git a/auto/unix b/auto/unix index 8bd1b1370..0dd66cfcd 100644 --- a/auto/unix +++ b/auto/unix @@ -508,18 +508,20 @@ ngx_feature_test="setsockopt(0, IPPROTO_TCP, TCP_DEFER_ACCEPT, NULL, 0)" . auto/feature -ngx_feature="TCP_KEEPIDLE" -ngx_feature_name="NGX_HAVE_KEEPALIVE_TUNABLE" -ngx_feature_run=no -ngx_feature_incs="#include - #include - #include " -ngx_feature_path= -ngx_feature_libs= -ngx_feature_test="setsockopt(0, IPPROTO_TCP, TCP_KEEPIDLE, NULL, 0); - setsockopt(0, IPPROTO_TCP, TCP_KEEPINTVL, NULL, 0); - setsockopt(0, IPPROTO_TCP, TCP_KEEPCNT, NULL, 0)" -. auto/feature +if test -z "$NGX_KEEPALIVE_CHECKED"; then + ngx_feature="TCP_KEEPIDLE" + ngx_feature_name="NGX_HAVE_KEEPALIVE_TUNABLE" + ngx_feature_run=no + ngx_feature_incs="#include + #include + #include " + ngx_feature_path= + ngx_feature_libs= + ngx_feature_test="setsockopt(0, IPPROTO_TCP, TCP_KEEPIDLE, NULL, 0); + setsockopt(0, IPPROTO_TCP, TCP_KEEPINTVL, NULL, 0); + setsockopt(0, IPPROTO_TCP, TCP_KEEPCNT, NULL, 0)" + . auto/feature +fi ngx_feature="TCP_FASTOPEN" diff --git a/src/core/ngx_connection.c b/src/core/ngx_connection.c index 75809d9ad..7cae295eb 100644 --- a/src/core/ngx_connection.c +++ b/src/core/ngx_connection.c @@ -765,6 +765,8 @@ ngx_configure_listening_sockets(ngx_cycle_t *cycle) #if (NGX_HAVE_KEEPALIVE_TUNABLE) +#if !(NGX_DARWIN) + if (ls[i].keepidle) { value = ls[i].keepidle; @@ -782,6 +784,8 @@ ngx_configure_listening_sockets(ngx_cycle_t *cycle) } } +#endif + if (ls[i].keepintvl) { value = ls[i].keepintvl; diff --git a/src/event/ngx_event_accept.c b/src/event/ngx_event_accept.c index 27038799d..033d7e021 100644 --- a/src/event/ngx_event_accept.c +++ b/src/event/ngx_event_accept.c @@ -203,6 +203,23 @@ ngx_event_accept(ngx_event_t *ev) } } +#if (NGX_HAVE_KEEPALIVE_TUNABLE && NGX_DARWIN) + + /* Darwin doesn't inherit TCP_KEEPALIVE from a listening socket */ + + if (ls->keepidle) { + if (setsockopt(s, IPPROTO_TCP, TCP_KEEPALIVE, + (const void *) &ls->keepidle, sizeof(int)) + == -1) + { + ngx_log_error(NGX_LOG_ALERT, ev->log, ngx_socket_errno, + "setsockopt(TCP_KEEPALIVE, %d) failed, ignored", + ls->keepidle); + } + } + +#endif + *log = ls->log; c->recv = ngx_recv; From noreply at nginx.com Thu May 22 23:55:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Thu, 22 May 2025 23:55:02 +0000 (UTC) Subject: [njs] Modules: separating different init worker components. Message-ID: <20250522235502.7D761489E6@pubserv1.nginx> details: https://github.com/nginx/njs/commit/c7128880915d7201db1d2bc05baa5ba5e1287ce6 branches: master commit: c7128880915d7201db1d2bc05baa5ba5e1287ce6 user: Dmitry Volyntsev date: Wed, 14 May 2025 18:00:23 -0700 description: Modules: separating different init worker components. --- nginx/ngx_http_js_module.c | 38 +++++++++++++++++++++++++++----------- nginx/ngx_stream_js_module.c | 42 +++++++++++++++++++++++++++++------------- 2 files changed, 56 insertions(+), 24 deletions(-) diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c index 40bb83a5..3b493bd1 100644 --- a/nginx/ngx_http_js_module.c +++ b/nginx/ngx_http_js_module.c @@ -7684,21 +7684,12 @@ ngx_http_js_init(ngx_conf_t *cf) static ngx_int_t -ngx_http_js_init_worker(ngx_cycle_t *cycle) +ngx_http_js_init_worker_periodics(ngx_js_main_conf_t *jmcf) { ngx_uint_t i; ngx_js_periodic_t *periodics; - ngx_js_main_conf_t *jmcf; - - if ((ngx_process != NGX_PROCESS_WORKER) - && ngx_process != NGX_PROCESS_SINGLE) - { - return NGX_OK; - } - - jmcf = ngx_http_cycle_get_module_main_conf(cycle, ngx_http_js_module); - if (jmcf == NULL || jmcf->periodics == NULL) { + if (jmcf->periodics == NULL) { return NGX_OK; } @@ -7726,6 +7717,31 @@ ngx_http_js_init_worker(ngx_cycle_t *cycle) } +static ngx_int_t +ngx_http_js_init_worker(ngx_cycle_t *cycle) +{ + ngx_js_main_conf_t *jmcf; + + if ((ngx_process != NGX_PROCESS_WORKER) + && ngx_process != NGX_PROCESS_SINGLE) + { + return NGX_OK; + } + + jmcf = ngx_http_cycle_get_module_main_conf(cycle, ngx_http_js_module); + + if (jmcf == NULL) { + return NGX_OK; + } + + if (ngx_http_js_init_worker_periodics(jmcf) != NGX_OK) { + return NGX_ERROR; + } + + return NGX_OK; +} + + static char * ngx_http_js_periodic(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { diff --git a/nginx/ngx_stream_js_module.c b/nginx/ngx_stream_js_module.c index 0e022eb0..bc653a5b 100644 --- a/nginx/ngx_stream_js_module.c +++ b/nginx/ngx_stream_js_module.c @@ -3192,21 +3192,12 @@ ngx_stream_js_periodic_init(ngx_js_periodic_t *periodic) static ngx_int_t -ngx_stream_js_init_worker(ngx_cycle_t *cycle) +ngx_stream_js_init_worker_periodics(ngx_js_main_conf_t *jmcf) { - ngx_uint_t i; - ngx_js_periodic_t *periodics; - ngx_js_main_conf_t *jmcf; - - if ((ngx_process != NGX_PROCESS_WORKER) - && ngx_process != NGX_PROCESS_SINGLE) - { - return NGX_OK; - } + ngx_uint_t i; + ngx_js_periodic_t *periodics; - jmcf = ngx_stream_cycle_get_module_main_conf(cycle, ngx_stream_js_module); - - if (jmcf == NULL || jmcf->periodics == NULL) { + if (jmcf->periodics == NULL) { return NGX_OK; } @@ -3234,6 +3225,31 @@ ngx_stream_js_init_worker(ngx_cycle_t *cycle) } +static ngx_int_t +ngx_stream_js_init_worker(ngx_cycle_t *cycle) +{ + ngx_js_main_conf_t *jmcf; + + if ((ngx_process != NGX_PROCESS_WORKER) + && ngx_process != NGX_PROCESS_SINGLE) + { + return NGX_OK; + } + + jmcf = ngx_stream_cycle_get_module_main_conf(cycle, ngx_stream_js_module); + + if (jmcf == NULL) { + return NGX_OK; + } + + if (ngx_stream_js_init_worker_periodics(jmcf) != NGX_OK) { + return NGX_ERROR; + } + + return NGX_OK; +} + + static char * ngx_stream_js_periodic(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { From noreply at nginx.com Thu May 22 23:55:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Thu, 22 May 2025 23:55:02 +0000 (UTC) Subject: [njs] Tests: removed leftover engine checks. Message-ID: <20250522235502.768B947788@pubserv1.nginx> details: https://github.com/nginx/njs/commit/a340ad8f7d9bd5ee1bfb7e6d76f56e845fe002f3 branches: master commit: a340ad8f7d9bd5ee1bfb7e6d76f56e845fe002f3 user: Dmitry Volyntsev date: Mon, 19 May 2025 18:53:00 -0700 description: Tests: removed leftover engine checks. --- nginx/t/js_fetch.t | 10 +--------- nginx/t/js_fetch_https.t | 10 +--------- nginx/t/js_fetch_objects.t | 10 +--------- nginx/t/js_fetch_resolver.t | 10 +--------- nginx/t/js_fetch_timeout.t | 10 +--------- nginx/t/js_fetch_verify.t | 10 +--------- nginx/t/js_periodic_fetch.t | 10 +--------- nginx/t/js_shared_dict.t | 10 +--------- nginx/t/stream_js_fetch.t | 10 +--------- nginx/t/stream_js_fetch_https.t | 10 +--------- nginx/t/stream_js_fetch_init.t | 10 +--------- nginx/t/stream_js_periodic_fetch.t | 10 +--------- nginx/t/stream_js_shared_dict.t | 10 +--------- 13 files changed, 13 insertions(+), 117 deletions(-) diff --git a/nginx/t/js_fetch.t b/nginx/t/js_fetch.t index 7ee1a602..1c6fde77 100644 --- a/nginx/t/js_fetch.t +++ b/nginx/t/js_fetch.t @@ -52,10 +52,6 @@ http { js_content test.njs; } - location /engine { - js_content test.engine; - } - location /broken { js_content test.broken; } @@ -138,10 +134,6 @@ $t->write_file('test.js', <write_file('test.js', <try_run('no njs.fetch'); diff --git a/nginx/t/js_fetch_https.t b/nginx/t/js_fetch_https.t index 8ede1048..42b5acbb 100644 --- a/nginx/t/js_fetch_https.t +++ b/nginx/t/js_fetch_https.t @@ -48,10 +48,6 @@ http { js_content test.njs; } - location /engine { - js_content test.engine; - } - location /https { js_content test.https; } @@ -106,10 +102,6 @@ $t->write_file('test.js', <write_file('test.js', < r.return(501, e.message)) } - export default {njs: test_njs, https, engine}; + export default {njs: test_njs, https}; EOF my $d = $t->testdir(); diff --git a/nginx/t/js_fetch_objects.t b/nginx/t/js_fetch_objects.t index bc5cc7ed..c9d04c49 100644 --- a/nginx/t/js_fetch_objects.t +++ b/nginx/t/js_fetch_objects.t @@ -45,10 +45,6 @@ http { js_content test.njs; } - location /engine { - js_content test.engine; - } - location /headers { js_content test.headers; } @@ -92,10 +88,6 @@ $t->write_file('test.js', <write_file('test.js', <write_file('test.js', <write_file('test.js', <try_run('no njs.fetch'); diff --git a/nginx/t/js_fetch_timeout.t b/nginx/t/js_fetch_timeout.t index ab1ba24a..2ca1510f 100644 --- a/nginx/t/js_fetch_timeout.t +++ b/nginx/t/js_fetch_timeout.t @@ -47,10 +47,6 @@ http { js_content test.njs; } - location /engine { - js_content test.engine; - } - location /normal_timeout { js_content test.timeout_test; } @@ -84,10 +80,6 @@ $t->write_file('test.js', <write_file('test.js', < { r.return(200); }, 250, r, 0); } - export default {njs: test_njs, engine, timeout_test, normal_reply, + export default {njs: test_njs, timeout_test, normal_reply, delayed_reply}; EOF diff --git a/nginx/t/js_fetch_verify.t b/nginx/t/js_fetch_verify.t index f98b4d8c..8b691a74 100644 --- a/nginx/t/js_fetch_verify.t +++ b/nginx/t/js_fetch_verify.t @@ -48,10 +48,6 @@ http { js_content test.njs; } - location /engine { - js_content test.engine; - } - location /https { js_content test.https; } @@ -80,10 +76,6 @@ $t->write_file('test.js', < reply.text()) @@ -91,7 +83,7 @@ $t->write_file('test.js', < r.return(501, e.message)); } - export default {njs: test_njs, engine, https}; + export default {njs: test_njs, https}; EOF $t->write_file('openssl.conf', <write_file('test.js', <write_file('test.js', <try_run('no js_periodic with fetch'); diff --git a/nginx/t/js_shared_dict.t b/nginx/t/js_shared_dict.t index 8be2831f..b27a33ef 100644 --- a/nginx/t/js_shared_dict.t +++ b/nginx/t/js_shared_dict.t @@ -52,10 +52,6 @@ http { js_content test.njs; } - location /engine { - js_content test.engine; - } - location /add { js_content test.add; } @@ -141,10 +137,6 @@ $t->write_file('test.js', <<'EOF'); r.return(200, njs.version); } - function engine(r) { - r.return(200, njs.engine); - } - function convertToValue(dict, v) { if (dict.type == 'number') { return parseInt(v); @@ -337,7 +329,7 @@ $t->write_file('test.js', <<'EOF'); export default { add, capacity, chain, clear, del, free_space, get, has, incr, items, keys, name, njs: test_njs, pop, replace, set, - set_clear, size, zones, engine, overflow }; + set_clear, size, zones, overflow }; EOF $t->try_run('no js_shared_dict_zone'); diff --git a/nginx/t/stream_js_fetch.t b/nginx/t/stream_js_fetch.t index 9a42ae29..cb87eec7 100644 --- a/nginx/t/stream_js_fetch.t +++ b/nginx/t/stream_js_fetch.t @@ -46,10 +46,6 @@ http { js_content test.njs; } - location /engine { - js_content test.engine; - } - location /validate { js_content test.validate; } @@ -103,10 +99,6 @@ $t->write_file('test.js', <write_file('test.js', <try_run('no stream njs available'); diff --git a/nginx/t/stream_js_fetch_https.t b/nginx/t/stream_js_fetch_https.t index 987a896a..f397ea70 100644 --- a/nginx/t/stream_js_fetch_https.t +++ b/nginx/t/stream_js_fetch_https.t @@ -47,10 +47,6 @@ http { location /njs { js_content test.njs; } - - location /engine { - js_content test.engine; - } } server { @@ -163,10 +159,6 @@ $t->write_file('test.js', <write_file('test.js', <testdir(); diff --git a/nginx/t/stream_js_fetch_init.t b/nginx/t/stream_js_fetch_init.t index f48b9d5e..3980a9ee 100644 --- a/nginx/t/stream_js_fetch_init.t +++ b/nginx/t/stream_js_fetch_init.t @@ -58,10 +58,6 @@ http { js_content test.njs; } - location /engine { - js_content test.engine; - } - location /success { return 200; } @@ -77,17 +73,13 @@ $t->write_file('test.js', <try_run('no stream njs available'); diff --git a/nginx/t/stream_js_periodic_fetch.t b/nginx/t/stream_js_periodic_fetch.t index 60599423..4ebec96e 100644 --- a/nginx/t/stream_js_periodic_fetch.t +++ b/nginx/t/stream_js_periodic_fetch.t @@ -67,10 +67,6 @@ http { listen 127.0.0.1:8080; server_name localhost; - location /engine { - js_content test.engine; - } - location /fetch_ok { return 200 'ok'; } @@ -86,10 +82,6 @@ EOF my $p1 = port(8080); $t->write_file('test.js', <write_file('test.js', <run_daemon(\&stream_daemon, port(8090)); diff --git a/nginx/t/stream_js_shared_dict.t b/nginx/t/stream_js_shared_dict.t index 915cc40b..0435033d 100644 --- a/nginx/t/stream_js_shared_dict.t +++ b/nginx/t/stream_js_shared_dict.t @@ -43,10 +43,6 @@ http { location / { return 200; } - - location /engine { - js_content test.engine; - } } } @@ -75,10 +71,6 @@ EOF $t->write_file('test.js', <write_file('test.js', < details: https://github.com/nginx/njs/commit/36aec3d75898a4f695f2a5759706312bdb82deee branches: master commit: 36aec3d75898a4f695f2a5759706312bdb82deee user: Dmitry Volyntsev date: Wed, 14 May 2025 19:36:33 -0700 description: Modules: introduced NGX_CHB_CTX_INIT(). --- nginx/ngx_js.h | 3 +++ src/njs_chb.c | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/nginx/ngx_js.h b/nginx/ngx_js.h index bb7c1d26..ceb82f74 100644 --- a/nginx/ngx_js.h +++ b/nginx/ngx_js.h @@ -317,6 +317,9 @@ ngx_int_t ngx_js_exception(njs_vm_t *vm, ngx_str_t *s); ngx_engine_t *ngx_njs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external); +#define NGX_CHB_CTX_INIT(chain, pool) \ + njs_chb_init(chain, pool, (njs_chb_alloc_t) ngx_palloc, NULL) + #if (NJS_HAVE_QUICKJS) typedef struct ngx_qjs_event_s ngx_qjs_event_t; diff --git a/src/njs_chb.c b/src/njs_chb.c index 3ee28009..ac88c0bd 100644 --- a/src/njs_chb.c +++ b/src/njs_chb.c @@ -251,7 +251,11 @@ njs_chb_destroy(njs_chb_t *chain) while (n != NULL) { next = n->next; - chain->free(chain->pool, n); + + if (chain->free != NULL) { + chain->free(chain->pool, n); + } + n = next; } } From noreply at nginx.com Thu May 22 23:55:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Thu, 22 May 2025 23:55:02 +0000 (UTC) Subject: [njs] Modules: replaced njs_str_t with ngx_str_t in the shared dictionary. Message-ID: <20250522235502.8752048C14@pubserv1.nginx> details: https://github.com/nginx/njs/commit/6d74d4e039558d4e814633cebbb495bf714b239c branches: master commit: 6d74d4e039558d4e814633cebbb495bf714b239c user: Dmitry Volyntsev date: Thu, 15 May 2025 17:01:25 -0700 description: Modules: replaced njs_str_t with ngx_str_t in the shared dictionary. This is a preparatory patch for upcoming changes to unify common code for njs and QuickJS engines. --- nginx/ngx_js_shared_dict.c | 73 +++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 39 deletions(-) diff --git a/nginx/ngx_js_shared_dict.c b/nginx/ngx_js_shared_dict.c index ccca530d..d4db87d5 100644 --- a/nginx/ngx_js_shared_dict.c +++ b/nginx/ngx_js_shared_dict.c @@ -79,25 +79,25 @@ static njs_int_t njs_js_ext_shared_dict_type(njs_vm_t *vm, njs_object_prop_t *prop, uint32_t unused, njs_value_t *value, njs_value_t *setval, njs_value_t *retval); static ngx_js_dict_node_t *ngx_js_dict_lookup(ngx_js_dict_t *dict, - njs_str_t *key); + ngx_str_t *key); #define NGX_JS_DICT_FLAG_MUST_EXIST 1 #define NGX_JS_DICT_FLAG_MUST_NOT_EXIST 2 static ngx_int_t ngx_js_dict_set(njs_vm_t *vm, ngx_js_dict_t *dict, - njs_str_t *key, njs_value_t *value, ngx_msec_t timeout, unsigned flags); + ngx_str_t *key, njs_value_t *value, ngx_msec_t timeout, unsigned flags); static ngx_int_t ngx_js_dict_add(njs_vm_t *vm, ngx_js_dict_t *dict, - njs_str_t *key, njs_value_t *value, ngx_msec_t timeout, ngx_msec_t now); + ngx_str_t *key, njs_value_t *value, ngx_msec_t timeout, ngx_msec_t now); static ngx_int_t ngx_js_dict_update(njs_vm_t *vm, ngx_js_dict_t *dict, ngx_js_dict_node_t *node, njs_value_t *value, ngx_msec_t timeout, ngx_msec_t now); static ngx_int_t ngx_js_dict_get(njs_vm_t *vm, ngx_js_dict_t *dict, - njs_str_t *key, njs_value_t *retval); + ngx_str_t *key, njs_value_t *retval); static ngx_int_t ngx_js_dict_incr(njs_vm_t *vm, ngx_js_dict_t *dict, - njs_str_t *key, njs_value_t *delta, njs_value_t *init, double *value, + ngx_str_t *key, njs_value_t *delta, njs_value_t *init, double *value, ngx_msec_t timeout); static ngx_int_t ngx_js_dict_delete(njs_vm_t *vm, ngx_js_dict_t *dict, - njs_str_t *key, njs_value_t *retval); + ngx_str_t *key, njs_value_t *retval); static ngx_int_t ngx_js_dict_copy_value_locked(njs_vm_t *vm, ngx_js_dict_t *dict, ngx_js_dict_node_t *node, njs_value_t *retval); @@ -662,7 +662,7 @@ njs_js_ext_shared_dict_delete(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval) { ngx_int_t rc; - njs_str_t key; + ngx_str_t key; ngx_shm_zone_t *shm_zone; shm_zone = njs_vm_external(vm, ngx_js_shared_dict_proto_id, @@ -672,7 +672,7 @@ njs_js_ext_shared_dict_delete(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, return NJS_ERROR; } - if (ngx_js_string(vm, njs_arg(args, nargs, 1), &key) != NGX_OK) { + if (ngx_js_ngx_string(vm, njs_arg(args, nargs, 1), &key) != NGX_OK) { return NJS_ERROR; } @@ -689,7 +689,7 @@ njs_js_ext_shared_dict_get(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval) { ngx_int_t rc; - njs_str_t key; + ngx_str_t key; ngx_shm_zone_t *shm_zone; shm_zone = njs_vm_external(vm, ngx_js_shared_dict_proto_id, @@ -699,7 +699,7 @@ njs_js_ext_shared_dict_get(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, return NJS_ERROR; } - if (ngx_js_string(vm, njs_arg(args, nargs, 1), &key) != NGX_OK) { + if (ngx_js_ngx_string(vm, njs_arg(args, nargs, 1), &key) != NGX_OK) { return NJS_ERROR; } @@ -717,7 +717,7 @@ static njs_int_t njs_js_ext_shared_dict_has(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval) { - njs_str_t key; + ngx_str_t key; ngx_msec_t now; ngx_time_t *tp; ngx_js_dict_t *dict; @@ -731,7 +731,7 @@ njs_js_ext_shared_dict_has(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, return NJS_ERROR; } - if (ngx_js_string(vm, njs_arg(args, nargs, 1), &key) != NGX_OK) { + if (ngx_js_ngx_string(vm, njs_arg(args, nargs, 1), &key) != NGX_OK) { return NJS_ERROR; } @@ -851,7 +851,7 @@ njs_js_ext_shared_dict_incr(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, { double value; ngx_int_t rc; - njs_str_t key; + ngx_str_t key; ngx_msec_t timeout; njs_value_t *delta, *init, *timeo; ngx_js_dict_t *dict; @@ -872,7 +872,7 @@ njs_js_ext_shared_dict_incr(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, return NJS_ERROR; } - if (ngx_js_string(vm, njs_arg(args, nargs, 1), &key) != NGX_OK) { + if (ngx_js_ngx_string(vm, njs_arg(args, nargs, 1), &key) != NGX_OK) { return NJS_ERROR; } @@ -1058,7 +1058,7 @@ njs_js_ext_shared_dict_pop(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval) { ngx_int_t rc; - njs_str_t key; + ngx_str_t key; ngx_shm_zone_t *shm_zone; shm_zone = njs_vm_external(vm, ngx_js_shared_dict_proto_id, @@ -1068,7 +1068,7 @@ njs_js_ext_shared_dict_pop(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, return NJS_ERROR; } - if (ngx_js_string(vm, njs_arg(args, nargs, 1), &key) != NGX_OK) { + if (ngx_js_ngx_string(vm, njs_arg(args, nargs, 1), &key) != NGX_OK) { return NJS_ERROR; } @@ -1086,7 +1086,7 @@ static njs_int_t njs_js_ext_shared_dict_set(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t flags, njs_value_t *retval) { - njs_str_t key; + ngx_str_t key; ngx_int_t rc; ngx_msec_t timeout; njs_value_t *value, *timeo; @@ -1100,7 +1100,7 @@ njs_js_ext_shared_dict_set(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, return NJS_ERROR; } - if (ngx_js_string(vm, njs_arg(args, nargs, 1), &key) != NGX_OK) { + if (ngx_js_ngx_string(vm, njs_arg(args, nargs, 1), &key) != NGX_OK) { return NJS_ERROR; } @@ -1218,7 +1218,7 @@ njs_js_ext_shared_dict_type(njs_vm_t *vm, njs_object_prop_t *prop, uint32_t unused, njs_value_t *value, njs_value_t *setval, njs_value_t *retval) { - njs_str_t type; + ngx_str_t type; ngx_js_dict_t *dict; ngx_shm_zone_t *shm_zone; @@ -1229,36 +1229,31 @@ njs_js_ext_shared_dict_type(njs_vm_t *vm, njs_object_prop_t *prop, } dict = shm_zone->data; - switch (dict->type) { case NGX_JS_DICT_TYPE_STRING: - type = njs_str_value("string"); + ngx_str_set(&type, "string"); break; default: - type = njs_str_value("number"); + ngx_str_set(&type, "number"); break; } - return njs_vm_value_string_create(vm, retval, type.start, type.length); + return njs_vm_value_string_create(vm, retval, type.data, type.len); } static ngx_js_dict_node_t * -ngx_js_dict_lookup(ngx_js_dict_t *dict, njs_str_t *key) +ngx_js_dict_lookup(ngx_js_dict_t *dict, ngx_str_t *key) { uint32_t hash; - ngx_str_t k; ngx_rbtree_t *rbtree; rbtree = &dict->sh->rbtree; - hash = ngx_crc32_long(key->start, key->length); - - k.data = key->start; - k.len = key->length; + hash = ngx_crc32_long(key->data, key->len); - return (ngx_js_dict_node_t *) ngx_str_rbtree_lookup(rbtree, &k, hash); + return (ngx_js_dict_node_t *) ngx_str_rbtree_lookup(rbtree, key, hash); } @@ -1294,7 +1289,7 @@ ngx_js_dict_node_free(ngx_js_dict_t *dict, ngx_js_dict_node_t *node) static ngx_int_t -ngx_js_dict_set(njs_vm_t *vm, ngx_js_dict_t *dict, njs_str_t *key, +ngx_js_dict_set(njs_vm_t *vm, ngx_js_dict_t *dict, ngx_str_t *key, njs_value_t *value, ngx_msec_t timeout, unsigned flags) { ngx_msec_t now; @@ -1346,7 +1341,7 @@ memory_error: static ngx_int_t -ngx_js_dict_add(njs_vm_t *vm, ngx_js_dict_t *dict, njs_str_t *key, +ngx_js_dict_add(njs_vm_t *vm, ngx_js_dict_t *dict, ngx_str_t *key, njs_value_t *value, ngx_msec_t timeout, ngx_msec_t now) { size_t n; @@ -1358,8 +1353,8 @@ ngx_js_dict_add(njs_vm_t *vm, ngx_js_dict_t *dict, njs_str_t *key, ngx_js_dict_expire(dict, now); } - n = sizeof(ngx_js_dict_node_t) + key->length; - hash = ngx_crc32_long(key->start, key->length); + n = sizeof(ngx_js_dict_node_t) + key->len; + hash = ngx_crc32_long(key->data, key->len); node = ngx_js_dict_alloc(dict, n); if (node == NULL) { @@ -1385,8 +1380,8 @@ ngx_js_dict_add(njs_vm_t *vm, ngx_js_dict_t *dict, njs_str_t *key, node->sn.node.key = hash; - ngx_memcpy(node->sn.str.data, key->start, key->length); - node->sn.str.len = key->length; + ngx_memcpy(node->sn.str.data, key->data, key->len); + node->sn.str.len = key->len; ngx_rbtree_insert(&dict->sh->rbtree, &node->sn.node); @@ -1435,7 +1430,7 @@ ngx_js_dict_update(njs_vm_t *vm, ngx_js_dict_t *dict, ngx_js_dict_node_t *node, static ngx_int_t -ngx_js_dict_delete(njs_vm_t *vm, ngx_js_dict_t *dict, njs_str_t *key, +ngx_js_dict_delete(njs_vm_t *vm, ngx_js_dict_t *dict, ngx_str_t *key, njs_value_t *retval) { ngx_int_t rc; @@ -1482,7 +1477,7 @@ ngx_js_dict_delete(njs_vm_t *vm, ngx_js_dict_t *dict, njs_str_t *key, static ngx_int_t -ngx_js_dict_incr(njs_vm_t *vm, ngx_js_dict_t *dict, njs_str_t *key, +ngx_js_dict_incr(njs_vm_t *vm, ngx_js_dict_t *dict, ngx_str_t *key, njs_value_t *delta, njs_value_t *init, double *value, ngx_msec_t timeout) { ngx_msec_t now; @@ -1524,7 +1519,7 @@ ngx_js_dict_incr(njs_vm_t *vm, ngx_js_dict_t *dict, njs_str_t *key, static ngx_int_t -ngx_js_dict_get(njs_vm_t *vm, ngx_js_dict_t *dict, njs_str_t *key, +ngx_js_dict_get(njs_vm_t *vm, ngx_js_dict_t *dict, ngx_str_t *key, njs_value_t *retval) { ngx_int_t rc; From noreply at nginx.com Thu May 22 23:55:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Thu, 22 May 2025 23:55:02 +0000 (UTC) Subject: [njs] Modules: engine independent method to add entries to a shared dict. Message-ID: <20250522235502.8CE5948C19@pubserv1.nginx> details: https://github.com/nginx/njs/commit/bc3b91cda9497717ebd1c150f53e4a37e88b3958 branches: master commit: bc3b91cda9497717ebd1c150f53e4a37e88b3958 user: Dmitry Volyntsev date: Thu, 15 May 2025 17:24:25 -0700 description: Modules: engine independent method to add entries to a shared dict. --- nginx/ngx_js_shared_dict.c | 157 +++++++++++++++++++++------------------------ 1 file changed, 74 insertions(+), 83 deletions(-) diff --git a/nginx/ngx_js_shared_dict.c b/nginx/ngx_js_shared_dict.c index d4db87d5..0a35241f 100644 --- a/nginx/ngx_js_shared_dict.c +++ b/nginx/ngx_js_shared_dict.c @@ -21,16 +21,6 @@ typedef struct { } ngx_js_dict_sh_t; -typedef struct { - ngx_str_node_t sn; - ngx_rbtree_node_t expire; - union { - ngx_str_t value; - double number; - } u; -} ngx_js_dict_node_t; - - struct ngx_js_dict_s { ngx_shm_zone_t *shm_zone; ngx_js_dict_sh_t *sh; @@ -46,6 +36,19 @@ struct ngx_js_dict_s { }; +typedef union { + ngx_str_t str; /* NGX_JS_DICT_TYPE_STRING */ + double number; /* NGX_JS_DICT_TYPE_NUMBER */ +} ngx_js_dict_value_t; + + +typedef struct { + ngx_str_node_t sn; + ngx_rbtree_node_t expire; + ngx_js_dict_value_t value; +} ngx_js_dict_node_t; + + static njs_int_t njs_js_ext_shared_dict_capacity(njs_vm_t *vm, njs_object_prop_t *prop, uint32_t unused, njs_value_t *value, njs_value_t *setval, njs_value_t *retval); @@ -1281,7 +1284,7 @@ ngx_js_dict_node_free(ngx_js_dict_t *dict, ngx_js_dict_node_t *node) shpool = dict->shpool; if (dict->type == NGX_JS_DICT_TYPE_STRING) { - ngx_slab_free_locked(shpool, node->u.value.data); + ngx_slab_free_locked(shpool, node->value.str.data); } ngx_slab_free_locked(shpool, node); @@ -1341,12 +1344,11 @@ memory_error: static ngx_int_t -ngx_js_dict_add(njs_vm_t *vm, ngx_js_dict_t *dict, ngx_str_t *key, - njs_value_t *value, ngx_msec_t timeout, ngx_msec_t now) +ngx_js_dict_add_value(ngx_js_dict_t *dict, ngx_str_t *key, + ngx_js_dict_value_t *value, ngx_msec_t timeout, ngx_msec_t now) { size_t n; uint32_t hash; - njs_str_t string; ngx_js_dict_node_t *node; if (dict->timeout) { @@ -1364,18 +1366,17 @@ ngx_js_dict_add(njs_vm_t *vm, ngx_js_dict_t *dict, ngx_str_t *key, node->sn.str.data = (u_char *) node + sizeof(ngx_js_dict_node_t); if (dict->type == NGX_JS_DICT_TYPE_STRING) { - njs_value_string_get(vm, value, &string); - node->u.value.data = ngx_js_dict_alloc(dict, string.length); - if (node->u.value.data == NULL) { + node->value.str.data = ngx_js_dict_alloc(dict, value->str.len); + if (node->value.str.data == NULL) { ngx_slab_free_locked(dict->shpool, node); return NGX_ERROR; } - ngx_memcpy(node->u.value.data, string.start, string.length); - node->u.value.len = string.length; + ngx_memcpy(node->value.str.data, value->str.data, value->str.len); + node->value.str.len = value->str.len; } else { - node->u.number = njs_value_number(value); + node->value.number = value->number; } node->sn.node.key = hash; @@ -1394,6 +1395,29 @@ ngx_js_dict_add(njs_vm_t *vm, ngx_js_dict_t *dict, ngx_str_t *key, } +static ngx_int_t +ngx_js_dict_add(njs_vm_t *vm, ngx_js_dict_t *dict, ngx_str_t *key, + njs_value_t *value, ngx_msec_t timeout, ngx_msec_t now) +{ + njs_str_t string; + ngx_js_dict_value_t entry; + + if (dict->type == NGX_JS_DICT_TYPE_STRING) { + njs_value_string_get(vm, value, &string); + + entry.str.data = string.start; + entry.str.len = string.length; + + } else { + /* GCC complains about uninitialized entry.str.data. */ + entry.str.data = NULL; + entry.number = njs_value_number(value); + } + + return ngx_js_dict_add_value(dict, key, &entry, timeout, now); +} + + static ngx_int_t ngx_js_dict_update(njs_vm_t *vm, ngx_js_dict_t *dict, ngx_js_dict_node_t *node, njs_value_t *value, ngx_msec_t timeout, ngx_msec_t now) @@ -1409,14 +1433,14 @@ ngx_js_dict_update(njs_vm_t *vm, ngx_js_dict_t *dict, ngx_js_dict_node_t *node, return NGX_ERROR; } - ngx_slab_free_locked(dict->shpool, node->u.value.data); + ngx_slab_free_locked(dict->shpool, node->value.str.data); ngx_memcpy(p, string.start, string.length); - node->u.value.data = p; - node->u.value.len = string.length; + node->value.str.data = p; + node->value.str.len = string.length; } else { - node->u.number = njs_value_number(value); + node->value.number = njs_value_number(value); } if (dict->timeout) { @@ -1502,8 +1526,8 @@ ngx_js_dict_incr(njs_vm_t *vm, ngx_js_dict_t *dict, ngx_str_t *key, *value = njs_value_number(init); } else { - node->u.number += njs_value_number(delta); - *value = node->u.number; + node->value.number += njs_value_number(delta); + *value = node->value.number; if (dict->timeout) { ngx_rbtree_delete(&dict->sh->rbtree_expire, &node->expire); @@ -1568,14 +1592,14 @@ ngx_js_dict_copy_value_locked(njs_vm_t *vm, ngx_js_dict_t *dict, type = dict->type; if (type == NGX_JS_DICT_TYPE_STRING) { - ret = njs_vm_value_string_create(vm, retval, node->u.value.data, - node->u.value.len); + ret = njs_vm_value_string_create(vm, retval, node->value.str.data, + node->value.str.len); if (ret != NJS_OK) { return NGX_ERROR; } } else { - njs_value_number_set(retval, node->u.number); + njs_value_number_set(retval, node->value.number); } return NGX_OK; @@ -2625,13 +2649,13 @@ ngx_qjs_dict_copy_value_locked(JSContext *cx, ngx_js_dict_t *dict, ngx_js_dict_node_t *node) { if (dict->type == NGX_JS_DICT_TYPE_STRING) { - return JS_NewStringLen(cx, (const char *) node->u.value.data, - node->u.value.len); + return JS_NewStringLen(cx, (const char *) node->value.str.data, + node->value.str.len); } /* NGX_JS_DICT_TYPE_NUMBER */ - return JS_NewFloat64(cx, node->u.number); + return JS_NewFloat64(cx, node->value.number); } @@ -2653,64 +2677,31 @@ static ngx_int_t ngx_qjs_dict_add(JSContext *cx, ngx_js_dict_t *dict, ngx_str_t *key, JSValue value, ngx_msec_t timeout, ngx_msec_t now) { - size_t n; - uint32_t hash; - ngx_str_t string; - ngx_js_dict_node_t *node; - - if (dict->timeout) { - ngx_js_dict_expire(dict, now); - } - - n = sizeof(ngx_js_dict_node_t) + key->len; - hash = ngx_crc32_long(key->data, key->len); - - node = ngx_js_dict_alloc(dict, n); - if (node == NULL) { - return NGX_ERROR; - } - - node->sn.str.data = (u_char *) node + sizeof(ngx_js_dict_node_t); + ngx_int_t rc; + ngx_js_dict_value_t entry; if (dict->type == NGX_JS_DICT_TYPE_STRING) { - string.data = (u_char *) JS_ToCStringLen(cx, &string.len, value); - if (string.data == NULL) { - ngx_slab_free_locked(dict->shpool, node); - return NGX_ERROR; - } - - node->u.value.data = ngx_js_dict_alloc(dict, string.len); - if (node->u.value.data == NULL) { - ngx_slab_free_locked(dict->shpool, node); - JS_FreeCString(cx, (char *) string.data); + entry.str.data = (u_char *) JS_ToCStringLen(cx, &entry.str.len, value); + if (entry.str.data == NULL) { return NGX_ERROR; } - ngx_memcpy(node->u.value.data, string.data, string.len); - node->u.value.len = string.len; - - JS_FreeCString(cx, (char *) string.data); - } else { - if (JS_ToFloat64(cx, &node->u.number, value) < 0) { - ngx_slab_free_locked(dict->shpool, node); + /* GCC complains about uninitialized entry.str.data. */ + entry.str.data = NULL; + + if (JS_ToFloat64(cx, &entry.number, value) < 0) { return NGX_ERROR; } } - node->sn.node.key = hash; + rc = ngx_js_dict_add_value(dict, key, &entry, timeout, now); - ngx_memcpy(node->sn.str.data, key->data, key->len); - node->sn.str.len = key->len; - - ngx_rbtree_insert(&dict->sh->rbtree, &node->sn.node); - - if (dict->timeout) { - node->expire.key = now + timeout; - ngx_rbtree_insert(&dict->sh->rbtree_expire, &node->expire); + if (dict->type == NGX_JS_DICT_TYPE_STRING) { + JS_FreeCString(cx, (char *) entry.str.data); } - return NGX_OK; + return rc; } @@ -2824,8 +2815,8 @@ ngx_qjs_dict_incr(JSContext *cx, ngx_js_dict_t *dict, ngx_str_t *key, } } else { - node->u.number += delta; - value = JS_NewFloat64(cx, node->u.number); + node->value.number += delta; + value = JS_NewFloat64(cx, node->value.number); if (dict->timeout) { ngx_rbtree_delete(&dict->sh->rbtree_expire, &node->expire); @@ -2912,16 +2903,16 @@ ngx_qjs_dict_update(JSContext *cx, ngx_js_dict_t *dict, return NGX_ERROR; } - ngx_slab_free_locked(dict->shpool, node->u.value.data); + ngx_slab_free_locked(dict->shpool, node->value.str.data); ngx_memcpy(p, string.data, string.len); - node->u.value.data = p; - node->u.value.len = string.len; + node->value.str.data = p; + node->value.str.len = string.len; JS_FreeCString(cx, (char *) string.data); } else { - if (JS_ToFloat64(cx, &node->u.number, value) < 0) { + if (JS_ToFloat64(cx, &node->value.number, value) < 0) { return NGX_ERROR; } } From noreply at nginx.com Thu May 22 23:55:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Thu, 22 May 2025 23:55:02 +0000 (UTC) Subject: [njs] Modules: added state file for the shared dictionary. Message-ID: <20250522235502.99ABD48C20@pubserv1.nginx> details: https://github.com/nginx/njs/commit/eca03622a5d77d73ef7d89610c906dad5628c37e branches: master commit: eca03622a5d77d73ef7d89610c906dad5628c37e user: Dmitry Volyntsev date: Wed, 14 May 2025 18:16:15 -0700 description: Modules: added state file for the shared dictionary. A new optional state parameter is added for js_shared_dict_zone directive. state parameter specifies a file that keeps the current state of the shared dict in the JSON format and makes it persistent across nginx restarts. This closes #709 feature request on Github. --- nginx/ngx_http_js_module.c | 4 + nginx/ngx_js.h | 5 +- nginx/ngx_js_shared_dict.c | 1146 ++++++++++++++++++++++++++++++++- nginx/ngx_js_shared_dict.h | 1 + nginx/ngx_stream_js_module.c | 4 + nginx/t/js_shared_dict_state.t | 256 ++++++++ nginx/t/stream_js_shared_dict_state.t | 137 ++++ 7 files changed, 1534 insertions(+), 19 deletions(-) diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c index 3b493bd1..28798172 100644 --- a/nginx/ngx_http_js_module.c +++ b/nginx/ngx_http_js_module.c @@ -7738,6 +7738,10 @@ ngx_http_js_init_worker(ngx_cycle_t *cycle) return NGX_ERROR; } + if (ngx_js_dict_init_worker(jmcf) != NGX_OK) { + return NGX_ERROR; + } + return NGX_OK; } diff --git a/nginx/ngx_js.h b/nginx/ngx_js.h index ceb82f74..122881af 100644 --- a/nginx/ngx_js.h +++ b/nginx/ngx_js.h @@ -16,8 +16,6 @@ #include #include #include -#include "ngx_js_fetch.h" -#include "ngx_js_shared_dict.h" #if (NJS_HAVE_QUICKJS) #include @@ -440,4 +438,7 @@ extern njs_module_t njs_xml_module; extern njs_module_t njs_zlib_module; +#include "ngx_js_fetch.h" +#include "ngx_js_shared_dict.h" + #endif /* _NGX_JS_H_INCLUDED_ */ diff --git a/nginx/ngx_js_shared_dict.c b/nginx/ngx_js_shared_dict.c index 0a35241f..5445b4f2 100644 --- a/nginx/ngx_js_shared_dict.c +++ b/nginx/ngx_js_shared_dict.c @@ -18,6 +18,9 @@ typedef struct { ngx_rbtree_t rbtree_expire; ngx_rbtree_node_t sentinel_expire; + + unsigned dirty:1; + unsigned writing:1; } ngx_js_dict_sh_t; @@ -26,12 +29,23 @@ struct ngx_js_dict_s { ngx_js_dict_sh_t *sh; ngx_slab_pool_t *shpool; + /** + * in order for ngx_js_dict_t to be used as a ngx_event_t data, + * fd is used for event debug and should be at the same position + * as in ngx_connection_t. see ngx_event_ident() for details. + */ + ngx_socket_t fd; + ngx_msec_t timeout; ngx_flag_t evict; #define NGX_JS_DICT_TYPE_STRING 0 #define NGX_JS_DICT_TYPE_NUMBER 1 ngx_uint_t type; + ngx_event_t save_event; + ngx_str_t state_file; + ngx_str_t state_temp_file; + ngx_js_dict_t *next; }; @@ -49,6 +63,13 @@ typedef struct { } ngx_js_dict_node_t; +typedef struct { + ngx_str_t key; + ngx_js_dict_value_t value; + ngx_msec_t expire; +} ngx_js_dict_entry_t; + + static njs_int_t njs_js_ext_shared_dict_capacity(njs_vm_t *vm, njs_object_prop_t *prop, uint32_t unused, njs_value_t *value, njs_value_t *setval, njs_value_t *retval); @@ -625,8 +646,14 @@ njs_js_ext_shared_dict_clear(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, done: + dict->sh->dirty = 1; + ngx_rwlock_unlock(&dict->sh->rwlock); + if (dict->state_file.data && !dict->save_event.timer_set) { + ngx_add_timer(&dict->save_event, 1000); + } + njs_value_undefined_set(retval); return NJS_OK; @@ -1246,6 +1273,16 @@ njs_js_ext_shared_dict_type(njs_vm_t *vm, njs_object_prop_t *prop, } +static njs_int_t +ngx_js_dict_shared_error_name(njs_vm_t *vm, njs_object_prop_t *prop, + uint32_t unused, njs_value_t *value, njs_value_t *setval, + njs_value_t *retval) +{ + return njs_vm_value_string_create(vm, retval, + (u_char *) "SharedMemoryError", 17); +} + + static ngx_js_dict_node_t * ngx_js_dict_lookup(ngx_js_dict_t *dict, ngx_str_t *key) { @@ -1329,8 +1366,14 @@ ngx_js_dict_set(njs_vm_t *vm, ngx_js_dict_t *dict, ngx_str_t *key, } } + dict->sh->dirty = 1; + ngx_rwlock_unlock(&dict->sh->rwlock); + if (dict->state_file.data && !dict->save_event.timer_set) { + ngx_add_timer(&dict->save_event, 1000); + } + return NGX_OK; memory_error: @@ -1494,8 +1537,14 @@ ngx_js_dict_delete(njs_vm_t *vm, ngx_js_dict_t *dict, ngx_str_t *key, ngx_js_dict_node_free(dict, node); + dict->sh->dirty = 1; + ngx_rwlock_unlock(&dict->sh->rwlock); + if (dict->state_file.data && !dict->save_event.timer_set) { + ngx_add_timer(&dict->save_event, 1000); + } + return rc; } @@ -1536,8 +1585,14 @@ ngx_js_dict_incr(njs_vm_t *vm, ngx_js_dict_t *dict, ngx_str_t *key, } } + dict->sh->dirty = 1; + ngx_rwlock_unlock(&dict->sh->rwlock); + if (dict->state_file.data && !dict->save_event.timer_set) { + ngx_add_timer(&dict->save_event, 1000); + } + return NGX_OK; } @@ -1676,13 +1731,1013 @@ ngx_js_dict_evict(ngx_js_dict_t *dict, ngx_int_t count) } -static njs_int_t -ngx_js_dict_shared_error_name(njs_vm_t *vm, njs_object_prop_t *prop, - uint32_t unused, njs_value_t *value, njs_value_t *setval, - njs_value_t *retval) +static ngx_int_t +ngx_js_render_string(njs_chb_t *chain, ngx_str_t *str) { - return njs_vm_value_string_create(vm, retval, - (u_char *) "SharedMemoryError", 17); + size_t size; + u_char c, *dst, *dst_end; + const u_char *p, *end; + + static char hex2char[16] = { '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + + p = str->data; + end = p + str->len; + size = str->len + 2; + + dst = njs_chb_reserve(chain, size); + if (dst == NULL) { + return NGX_ERROR; + } + + dst_end = dst + size; + + *dst++ = '\"'; + njs_chb_written(chain, 1); + + while (p < end) { + if (dst_end <= dst + sizeof("\\uXXXX")) { + size = ngx_max(end - p + 1, 6); + dst = njs_chb_reserve(chain, size); + if (dst == NULL) { + return NGX_ERROR; + } + + dst_end = dst + size; + } + + if (*p < ' ' || *p == '\\' || *p == '\"') { + c = (u_char) *p++; + *dst++ = '\\'; + njs_chb_written(chain, 2); + + switch (c) { + case '\\': + *dst++ = '\\'; + break; + case '"': + *dst++ = '\"'; + break; + case '\r': + *dst++ = 'r'; + break; + case '\n': + *dst++ = 'n'; + break; + case '\t': + *dst++ = 't'; + break; + case '\b': + *dst++ = 'b'; + break; + case '\f': + *dst++ = 'f'; + break; + default: + *dst++ = 'u'; + *dst++ = '0'; + *dst++ = '0'; + *dst++ = hex2char[(c & 0xf0) >> 4]; + *dst++ = hex2char[c & 0x0f]; + njs_chb_written(chain, 4); + } + + continue; + } + + dst = njs_utf8_copy(dst, &p, end); + + njs_chb_written(chain, dst - chain->last->pos); + } + + njs_chb_append_literal(chain, "\""); + + return NGX_OK; +} + + +static ngx_int_t +ngx_js_dict_render_json(ngx_js_dict_t *dict, njs_chb_t *chain) +{ + u_char *p, *dst; + size_t len; + ngx_msec_t now; + ngx_time_t *tp; + ngx_rbtree_t *rbtree; + ngx_rbtree_node_t *rn, *next; + ngx_js_dict_node_t *node; + + tp = ngx_timeofday(); + now = tp->sec * 1000 + tp->msec; + + rbtree = &dict->sh->rbtree; + + njs_chb_append_literal(chain,"{"); + + if (rbtree->root == rbtree->sentinel) { + njs_chb_append_literal(chain, "}"); + return NGX_OK; + } + + for (rn = ngx_rbtree_min(rbtree->root, rbtree->sentinel); + rn != NULL; + rn = next) + { + node = (ngx_js_dict_node_t *) rn; + + next = ngx_rbtree_next(rbtree, rn); + + if (dict->timeout && now >= node->expire.key) { + continue; + } + + if (ngx_js_render_string(chain, &node->sn.str) != NGX_OK) { + return NGX_ERROR; + } + + njs_chb_append_literal(chain,":{"); + + if (dict->type == NGX_JS_DICT_TYPE_STRING) { + njs_chb_append_literal(chain,"\"value\":"); + + if (ngx_js_render_string(chain, &node->value.str) != NGX_OK) { + return NGX_ERROR; + } + + } else { + len = sizeof("\"value\":.") + 18 + 6; + dst = njs_chb_reserve(chain, len); + if (dst == NULL) { + return NGX_ERROR; + } + + p = njs_sprintf(dst, dst + len, "\"value\":%.6f", + node->value.number); + njs_chb_written(chain, p - dst); + } + + if (dict->timeout) { + len = sizeof(",\"expire\":1000000000"); + dst = njs_chb_reserve(chain, len); + if (dst == NULL) { + return NGX_ERROR; + } + + p = njs_sprintf(dst, dst + len, ",\"expire\":%ui", + node->expire.key); + njs_chb_written(chain, p - dst); + } + + njs_chb_append_literal(chain, "}"); + + if (next != NULL) { + njs_chb_append_literal(chain, ","); + } + } + + njs_chb_append_literal(chain, "}"); + + return NGX_OK; +} + + +static u_char * +ngx_js_skip_space(u_char *start, u_char *end) +{ + u_char *p; + + for (p = start; p != end; p++) { + + switch (*p) { + case ' ': + case '\t': + case '\r': + case '\n': + continue; + } + + break; + } + + return p; +} + + +static uint32_t +ngx_js_unicode(const u_char *p) +{ + u_char c; + uint32_t utf; + njs_uint_t i; + + utf = 0; + + for (i = 0; i < 4; i++) { + utf <<= 4; + c = p[i] | 0x20; + c -= '0'; + if (c > 9) { + c += '0' - 'a' + 10; + } + + utf |= c; + } + + return utf; +} + + +static u_char * +ngx_js_dict_parse_string(ngx_pool_t *pool, u_char *p, u_char *end, + ngx_str_t *str, const char **err, u_char **at) +{ + u_char ch, *s, *dst, *start, *last; + size_t size, surplus; + uint32_t utf, utf_low; + + enum { + sw_usual = 0, + sw_escape, + sw_encoded1, + sw_encoded2, + sw_encoded3, + sw_encoded4, + } state; + + if (*p != '"') { + *err = "unexpected character, expected '\"'"; + goto error; + } + + start = p + 1; + + dst = NULL; + state = 0; + surplus = 0; + + for (p = start; p < end; p++) { + ch = *p; + + switch (state) { + + case sw_usual: + + if (ch == '"') { + break; + } + + if (ch == '\\') { + state = sw_escape; + continue; + } + + if (ch >= ' ') { + continue; + } + + *err = "Invalid source char"; + goto error; + + case sw_escape: + + switch (ch) { + case '"': + case '\\': + case '/': + case 'n': + case 'r': + case 't': + case 'b': + case 'f': + surplus++; + state = sw_usual; + continue; + + case 'u': + /* + * Basic unicode 6 bytes "\uXXXX" in JSON + * and up to 3 bytes in UTF-8. + * + * Surrogate pair: 12 bytes "\uXXXX\uXXXX" in JSON + * and 3 or 4 bytes in UTF-8. + */ + surplus += 3; + state = sw_encoded1; + continue; + } + + *err = "Invalid escape char"; + goto error; + + case sw_encoded1: + case sw_encoded2: + case sw_encoded3: + case sw_encoded4: + + if ((ch >= '0' && ch <= '9') + || (ch >= 'A' && ch <= 'F') + || (ch >= 'a' && ch <= 'f')) + { + state = (state == sw_encoded4) ? sw_usual : state + 1; + continue; + } + + *err = "Invalid Unicode escape sequence"; + goto error; + } + + break; + } + + if (p == end) { + *err = "unexpected end of input"; + goto error; + } + + /* Points to the ending quote mark. */ + last = p; + + size = last - start - surplus; + + if (surplus != 0) { + p = start; + + dst = ngx_palloc(pool, size); + if (dst == NULL) { + *err = "out of memory"; + goto error; + } + + s = dst; + + do { + ch = *p++; + + if (ch != '\\') { + *s++ = ch; + continue; + } + + ch = *p++; + + switch (ch) { + case '"': + case '\\': + case '/': + *s++ = ch; + continue; + + case 'n': + *s++ = '\n'; + continue; + + case 'r': + *s++ = '\r'; + continue; + + case 't': + *s++ = '\t'; + continue; + + case 'b': + *s++ = '\b'; + continue; + + case 'f': + *s++ = '\f'; + continue; + } + + /* "\uXXXX": Unicode escape sequence. */ + + utf = ngx_js_unicode(p); + p += 4; + + if (njs_surrogate_any(utf)) { + + if (utf > 0xdbff || p[0] != '\\' || p[1] != 'u') { + s = njs_utf8_encode(s, NJS_UNICODE_REPLACEMENT); + continue; + } + + p += 2; + + utf_low = ngx_js_unicode(p); + p += 4; + + if (njs_fast_path(njs_surrogate_trailing(utf_low))) { + utf = njs_surrogate_pair(utf, utf_low); + + } else if (njs_surrogate_leading(utf_low)) { + utf = NJS_UNICODE_REPLACEMENT; + s = njs_utf8_encode(s, NJS_UNICODE_REPLACEMENT); + + } else { + utf = utf_low; + s = njs_utf8_encode(s, NJS_UNICODE_REPLACEMENT); + } + } + + s = njs_utf8_encode(s, utf); + + } while (p != last); + + size = s - dst; + start = dst; + } + + str->data = start; + str->len = size; + + return p + 1; + +error: + + *at = p; + + return NULL; +} + + +static u_char * +ngx_js_dict_parse_entry(ngx_js_dict_t *dict, ngx_pool_t *pool, + ngx_js_dict_entry_t *entry, u_char *buf, u_char *end, const char **err, + u_char **at) +{ + int see_value; + u_char *p, *pp; + double number; + ngx_str_t key, str; + + p = buf; + + if (*p++ != '{') { + *err = "unexpected character, expected '{'"; + goto error; + } + + see_value = 0; + + while (1) { + p = ngx_js_skip_space(p, end); + if (p == end) { + *err = "unexpected end of json"; + goto error; + } + + if (*p == '}') { + break; + } + + p = ngx_js_dict_parse_string(pool, p, end, &key, err, at); + if (p == NULL) { + return NULL; + } + + p = ngx_js_skip_space(p, end); + if (p == end) { + *err = "unexpected end of json"; + goto error; + } + + if (*p++ != ':') { + *err = "unexpected character, expected ':'"; + goto error; + } + + p = ngx_js_skip_space(p, end); + if (p == end) { + *err = "unexpected end of json"; + goto error; + } + + if (*p == '\"') { + p = ngx_js_dict_parse_string(pool, p, end, &str, err, at); + if (p == NULL) { + return NULL; + } + + if (key.len == 5 && ngx_strncmp(key.data, "value", 5) == 0) { + if (dict->type != NGX_JS_DICT_TYPE_STRING) { + *err = "expected string value"; + goto error; + } + + entry->value.str = str; + see_value = 1; + } + + } else { + pp = p; + number = strtod((char *) p, (char **) &p); + if (pp == p) { + *err = "invalid number value"; + goto error; + } + + if (key.len == 5 && ngx_strncmp(key.data, "value", 5) == 0) { + if (dict->type == NGX_JS_DICT_TYPE_STRING) { + *err = "expected number value"; + goto error; + } + + entry->value.number = number; + see_value = 1; + + } else if (key.len == 6 + && ngx_strncmp(key.data, "expire", 6) == 0) + { + entry->expire = number; + } + } + + p = ngx_js_skip_space(p, end); + if (p == end) { + *err = "unexpected end of json"; + goto error; + } + + if (*p == ',') { + p++; + } + } + + if (!see_value) { + *err = "missing value"; + goto error; + } + + return p + 1; + +error: + + *at = p; + + return NULL; +} + + +static ngx_int_t +ngx_js_dict_parse_state(ngx_js_dict_t *dict, ngx_pool_t *pool, + ngx_array_t *entries, u_char *buf, u_char *end) +{ + u_char *p, *at; + const char *err; + ngx_js_dict_entry_t *e; + + /* GCC complains about uninitialized err, at. */ + + err = ""; + at = NULL; + + p = ngx_js_skip_space(buf, end); + if (p == end) { + err = "empty json"; + goto error; + } + + if (*p++ != '{') { + err = "json must start with '{'"; + goto error; + } + + while (1) { + p = ngx_js_skip_space(p, end); + if (p == end) { + err = "unexpected end of json"; + goto error; + } + + if (*p == '}') { + p++; + break; + } + + e = ngx_array_push(entries); + if (e == NULL) { + return NGX_ERROR; + } + + p = ngx_js_dict_parse_string(pool, p, end, &e->key, &err, &at); + if (p == NULL) { + p = at; + goto error; + } + + p = ngx_js_skip_space(p, end); + if (p == end) { + err = "unexpected end of json"; + goto error; + } + + if (*p++ != ':') { + err = "unexpected character, expected ':'"; + goto error; + } + + p = ngx_js_skip_space(p, end); + if (p == end) { + err = "unexpected end of json"; + goto error; + } + + p = ngx_js_dict_parse_entry(dict, pool, e, p, end, &err, &at); + if (p == NULL) { + p = at; + goto error; + } + + p = ngx_js_skip_space(p, end); + if (p == end) { + err = "unexpected end of json"; + goto error; + } + + if (*p == ',') { + p++; + } + } + + p = ngx_js_skip_space(p, end); + + if (p != end) { + err = "unexpected character, expected end of json"; + goto error; + } + + return NGX_OK; + +error: + + ngx_log_error(NGX_LOG_EMERG, dict->shm_zone->shm.log, 0, + "invalid format while loading js_shared_dict_zone \"%V\"" + " from state file \"%s\": %s at offset %z", + &dict->shm_zone->shm.name, dict->state_file.data, err, + p - buf); + + return NGX_ERROR; +} + + +static ngx_int_t +ngx_js_dict_save(ngx_js_dict_t *dict) +{ + + u_char *name; + ngx_int_t rc; + ngx_log_t *log; + njs_chb_t chain; + ngx_file_t file; + ngx_pool_t *pool; + ngx_chain_t *out, *cl, **ll; + njs_chb_node_t *node; + ngx_ext_rename_file_t ext; + + log = dict->shm_zone->shm.log; + + pool = ngx_create_pool(NGX_DEFAULT_POOL_SIZE, log); + if (pool == NULL) { + return NGX_ERROR; + } + + ngx_rwlock_wlock(&dict->sh->rwlock); + + if (!dict->sh->dirty) { + ngx_rwlock_unlock(&dict->sh->rwlock); + ngx_destroy_pool(pool); + return NGX_OK; + } + + if (dict->sh->writing) { + ngx_rwlock_unlock(&dict->sh->rwlock); + ngx_destroy_pool(pool); + return NGX_AGAIN; + } + + ngx_rwlock_downgrade(&dict->sh->rwlock); + + NGX_CHB_CTX_INIT(&chain, pool); + + rc = ngx_js_dict_render_json(dict, &chain); + + if (rc != NGX_OK) { + ngx_rwlock_unlock(&dict->sh->rwlock); + ngx_destroy_pool(pool); + return rc; + } + + dict->sh->writing = 1; + dict->sh->dirty = 0; + + ngx_rwlock_unlock(&dict->sh->rwlock); + + name = dict->state_temp_file.data; + + out = NULL; + ll = &out; + + for (node = chain.nodes; node != NULL; node = node->next) { + cl = ngx_alloc_chain_link(pool); + if (cl == NULL) { + goto error; + } + + cl->buf = ngx_calloc_buf(pool); + if (cl->buf == NULL) { + goto error; + } + + cl->buf->pos = node->start; + cl->buf->last = node->pos; + cl->buf->memory = 1; + cl->buf->last_buf = (node->next == NULL) ? 1 : 0; + + *ll = cl; + ll = &cl->next; + } + + *ll = NULL; + + ngx_memzero(&file, sizeof(ngx_file_t)); + file.name = dict->state_temp_file; + file.log = log; + + file.fd = ngx_open_file(file.name.data, NGX_FILE_WRONLY, NGX_FILE_TRUNCATE, + NGX_FILE_DEFAULT_ACCESS); + + if (file.fd == NGX_INVALID_FILE) { + ngx_log_error(NGX_LOG_ALERT, log, ngx_errno, + ngx_open_file_n " \"%s\" failed", name); + goto error; + } + + rc = ngx_write_chain_to_file(&file, out, 0, pool); + + if (rc == NGX_ERROR) { + ngx_log_error(NGX_LOG_ALERT, log, ngx_errno, + ngx_write_fd_n " \"%s\" failed", file.name.data); + goto error; + } + + if (ngx_close_file(file.fd) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_ALERT, log, ngx_errno, + ngx_close_file_n " \"%s\" failed", file.name.data); + } + + file.fd = NGX_INVALID_FILE; + + ext.access = 0; + ext.time = -1; + ext.create_path = 0; + ext.delete_file = 0; + ext.log = log; + + if (ngx_ext_rename_file(&dict->state_temp_file, &dict->state_file, &ext) + != NGX_OK) + { + goto error; + } + + /* no lock required */ + dict->sh->writing = 0; + ngx_destroy_pool(pool); + + return NGX_OK; + +error: + + if (file.fd != NGX_INVALID_FILE + && ngx_close_file(file.fd) == NGX_FILE_ERROR) + { + ngx_log_error(NGX_LOG_ALERT, log, ngx_errno, + ngx_close_file_n " \"%s\" failed", name); + } + + ngx_destroy_pool(pool); + + /* no lock required */ + dict->sh->writing = 0; + dict->sh->dirty = 1; + + return NGX_ERROR; +} + + +static ngx_int_t +ngx_js_dict_load(ngx_js_dict_t *dict) +{ + off_t size; + u_char *name, *buf; + size_t len; + ssize_t n; + ngx_fd_t fd; + ngx_err_t err; + ngx_int_t rc; + ngx_log_t *log; + ngx_uint_t i; + ngx_msec_t now, expire; + ngx_time_t *tp; + ngx_pool_t *pool; + ngx_array_t data; + ngx_file_info_t fi; + ngx_js_dict_entry_t *entries; + + if (dict->state_file.data == NULL) { + return NGX_OK; + } + + log = dict->shm_zone->shm.log; + + name = dict->state_file.data; + + fd = ngx_open_file(name, NGX_FILE_RDONLY, NGX_FILE_OPEN, 0); + if (fd == NGX_INVALID_FILE) { + err = ngx_errno; + + if (err == NGX_ENOENT || err == NGX_ENOPATH) { + return NGX_OK; + } + + ngx_log_error(NGX_LOG_EMERG, log, err, + ngx_open_file_n " \"%s\" failed", name); + return NGX_ERROR; + } + + if (ngx_fd_info(fd, &fi) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, + ngx_fd_info_n " \"%s\" failed", name); + pool = NULL; + goto failed; + } + + size = ngx_file_size(&fi); + + if (size == 0) { + + if (ngx_close_file(fd) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, + ngx_close_file_n " \"%s\" failed", name); + } + + return NGX_OK; + } + + pool = ngx_create_pool(NGX_DEFAULT_POOL_SIZE, log); + if (pool == NULL) { + goto failed; + } + + len = size; + + buf = ngx_pnalloc(pool, len); + if (buf == NULL) { + goto failed; + } + + n = ngx_read_fd(fd, buf, len); + + if (n == -1) { + ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, + ngx_read_fd_n " \"%s\" failed", name); + goto failed; + } + + if ((size_t) n != len) { + ngx_log_error(NGX_LOG_EMERG, log, 0, + ngx_read_fd_n " has read only %z of %uz from %s", + n, len, name); + goto failed; + } + + if (ngx_close_file(fd) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, + ngx_close_file_n " \"%s\" failed", name); + fd = NGX_INVALID_FILE; + goto failed; + } + + fd = NGX_INVALID_FILE; + + if (ngx_array_init(&data, pool, 4, sizeof(ngx_js_dict_entry_t)) + != NGX_OK) + { + goto failed; + } + + rc = ngx_js_dict_parse_state(dict, pool, &data, buf, buf + len); + + if (rc != NGX_OK) { + goto failed; + } + + entries = data.elts; + + tp = ngx_timeofday(); + now = tp->sec * 1000 + tp->msec; + + for (i = 0; i < data.nelts; i++) { + + if (dict->timeout) { + expire = entries[i].expire; + + if (expire && now >= expire) { + dict->sh->dirty = 1; + continue; + } + + if (expire == 0) { + /* treat state without expire as new */ + expire = now + dict->timeout; + dict->sh->dirty = 1; + } + + } else { + expire = 0; + } + + if (ngx_js_dict_lookup(dict, &entries[i].key) != NULL) { + goto failed; + } + + if (ngx_js_dict_add_value(dict, &entries[i].key, &entries[i].value, + expire, 1) + != NGX_OK) + { + goto failed; + } + } + + ngx_destroy_pool(pool); + + return NGX_OK; + +failed: + + if (fd != NGX_INVALID_FILE && ngx_close_file(fd) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, + ngx_close_file_n " \"%s\" failed", name); + } + + if (pool) { + ngx_destroy_pool(pool); + } + + return NGX_ERROR; +} + + +static void +ngx_js_dict_save_handler(ngx_event_t *ev) +{ + ngx_int_t rc; + ngx_js_dict_t *dict; + + dict = ev->data; + + rc = ngx_js_dict_save(dict); + + if (rc == NGX_OK) { + return; + } + + if (rc == NGX_ERROR && (ngx_terminate || ngx_exiting)) { + ngx_log_error(NGX_LOG_ALERT, ev->log, 0, + "failed to save the state of shared dict zone \"%V\"", + &dict->shm_zone->shm.name); + return; + } + + /* NGX_ERROR, NGX_AGAIN */ + + ngx_add_timer(ev, 1000); +} + + +ngx_int_t +ngx_js_dict_init_worker(ngx_js_main_conf_t *jmcf) +{ + ngx_js_dict_t *dict; + + if ((ngx_process != NGX_PROCESS_WORKER || ngx_worker != 0) + && ngx_process != NGX_PROCESS_SINGLE) + { + return NGX_OK; + } + + if (jmcf->dicts == NULL) { + return NGX_OK; + } + + for (dict = jmcf->dicts; dict != NULL; dict = dict->next) { + + if (!dict->sh->dirty || !dict->state_file.data) { + continue; + } + + ngx_add_timer(&dict->save_event, 1000); + } + + return NGX_OK; } @@ -1752,6 +2807,10 @@ ngx_js_dict_init_zone(ngx_shm_zone_t *shm_zone, void *data) ngx_sprintf(dict->shpool->log_ctx, " in js shared zone \"%V\"%Z", &shm_zone->shm.name); + if (ngx_js_dict_load(dict) != NGX_OK) { + return NGX_ERROR; + } + return NGX_OK; } @@ -1764,7 +2823,7 @@ ngx_js_shared_dict_zone(ngx_conf_t *cf, ngx_command_t *cmd, void *conf, u_char *p; ssize_t size; - ngx_str_t *value, name, s; + ngx_str_t *value, name, file, s; ngx_flag_t evict; ngx_msec_t timeout; ngx_uint_t i, type; @@ -1775,6 +2834,7 @@ ngx_js_shared_dict_zone(ngx_conf_t *cf, ngx_command_t *cmd, void *conf, evict = 0; timeout = 0; name.len = 0; + ngx_str_null(&file); type = NGX_JS_DICT_TYPE_STRING; value = cf->args->elts; @@ -1826,6 +2886,17 @@ ngx_js_shared_dict_zone(ngx_conf_t *cf, ngx_command_t *cmd, void *conf, continue; } + if (ngx_strncmp(value[i].data, "state=", 6) == 0) { + file.data = value[i].data + 6; + file.len = value[i].len - 6; + + if (ngx_conf_full_name(cf->cycle, &file, 0) != NGX_OK) { + return NGX_CONF_ERROR; + } + + continue; + } + if (ngx_strncmp(value[i].data, "timeout=", 8) == 0) { s.data = value[i].data + 8; @@ -1899,6 +2970,23 @@ ngx_js_shared_dict_zone(ngx_conf_t *cf, ngx_command_t *cmd, void *conf, dict->timeout = timeout; dict->type = type; + dict->save_event.handler = ngx_js_dict_save_handler; + dict->save_event.data = dict; + dict->save_event.log = &cf->cycle->new_log; + dict->fd = -1; + + if (file.data) { + dict->state_file = file; + + p = ngx_pnalloc(cf->pool, file.len + sizeof(".tmp")); + if (p == NULL) { + return NGX_CONF_ERROR; + } + + dict->state_temp_file.data = p; + dict->state_temp_file.len = ngx_sprintf(p, "%V.tmp%Z", &file) - p - 1; + } + return NGX_CONF_OK; } @@ -2098,8 +3186,14 @@ ngx_qjs_ext_shared_dict_clear(JSContext *cx, JSValueConst this_val, done: + dict->sh->dirty = 1; + ngx_rwlock_unlock(&dict->sh->rwlock); + if (dict->state_file.data && !dict->save_event.timer_set) { + ngx_add_timer(&dict->save_event, 1000); + } + return JS_UNDEFINED; } @@ -2746,8 +3840,14 @@ ngx_qjs_dict_delete(JSContext *cx, ngx_js_dict_t *dict, ngx_str_t *key, ngx_js_dict_node_free(dict, node); + dict->sh->dirty = 1; + ngx_rwlock_unlock(&dict->sh->rwlock); + if (dict->state_file.data && !dict->save_event.timer_set) { + ngx_add_timer(&dict->save_event, 1000); + } + return ret; } @@ -2825,8 +3925,14 @@ ngx_qjs_dict_incr(JSContext *cx, ngx_js_dict_t *dict, ngx_str_t *key, } } + dict->sh->dirty = 1; + ngx_rwlock_unlock(&dict->sh->rwlock); + if (dict->state_file.data && !dict->save_event.timer_set) { + ngx_add_timer(&dict->save_event, 1000); + } + return value; } @@ -2856,24 +3962,30 @@ ngx_qjs_dict_set(JSContext *cx, ngx_js_dict_t *dict, ngx_str_t *key, goto memory_error; } - ngx_rwlock_unlock(&dict->sh->rwlock); + } else { - return JS_TRUE; - } + if (flags & NGX_JS_DICT_FLAG_MUST_NOT_EXIST) { + if (!dict->timeout || now < node->expire.key) { + ngx_rwlock_unlock(&dict->sh->rwlock); + return JS_FALSE; + } + } - if (flags & NGX_JS_DICT_FLAG_MUST_NOT_EXIST) { - if (!dict->timeout || now < node->expire.key) { - ngx_rwlock_unlock(&dict->sh->rwlock); - return JS_FALSE; + if (ngx_qjs_dict_update(cx, dict, node, value, timeout, now) + != NGX_OK) + { + goto memory_error; } } - if (ngx_qjs_dict_update(cx, dict, node, value, timeout, now) != NGX_OK) { - goto memory_error; - } + dict->sh->dirty = 1; ngx_rwlock_unlock(&dict->sh->rwlock); + if (dict->state_file.data && !dict->save_event.timer_set) { + ngx_add_timer(&dict->save_event, 1000); + } + return JS_TRUE; memory_error: diff --git a/nginx/ngx_js_shared_dict.h b/nginx/ngx_js_shared_dict.h index b9c7f967..b082962c 100644 --- a/nginx/ngx_js_shared_dict.h +++ b/nginx/ngx_js_shared_dict.h @@ -13,6 +13,7 @@ njs_int_t njs_js_ext_global_shared_prop(njs_vm_t *vm, njs_object_prop_t *prop, njs_value_t *retval); njs_int_t njs_js_ext_global_shared_keys(njs_vm_t *vm, njs_value_t *value, njs_value_t *keys); +ngx_int_t ngx_js_dict_init_worker(ngx_js_main_conf_t *jmcf); extern njs_module_t ngx_js_shared_dict_module; diff --git a/nginx/ngx_stream_js_module.c b/nginx/ngx_stream_js_module.c index bc653a5b..fb58cdc6 100644 --- a/nginx/ngx_stream_js_module.c +++ b/nginx/ngx_stream_js_module.c @@ -3246,6 +3246,10 @@ ngx_stream_js_init_worker(ngx_cycle_t *cycle) return NGX_ERROR; } + if (ngx_js_dict_init_worker(jmcf) != NGX_OK) { + return NGX_ERROR; + } + return NGX_OK; } diff --git a/nginx/t/js_shared_dict_state.t b/nginx/t/js_shared_dict_state.t new file mode 100644 index 00000000..32eef948 --- /dev/null +++ b/nginx/t/js_shared_dict_state.t @@ -0,0 +1,256 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for js_shared_dict_zone directive, state= parameter. + +############################################################################### + +use warnings; +use strict; + +use Test::More; +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +eval { require JSON::PP; }; +plan(skip_all => "JSON::PP not installed") if $@; + +my $t = Test::Nginx->new()->has(qw/http/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_import test.js; + + js_shared_dict_zone zone=bar:64k type=string state=bar.json; + js_shared_dict_zone zone=waka:32k timeout=1000s type=number state=waka.json; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /add { + js_content test.add; + } + + location /clear { + js_content test.clear; + } + + location /delete { + js_content test.del; + } + + location /get { + js_content test.get; + } + + location /incr { + js_content test.incr; + } + + location /pop { + js_content test.pop; + } + + location /set { + js_content test.set; + } + } +} + +EOF + +$t->write_file('bar.json', <write_file('test.js', <<'EOF'); + function convertToValue(dict, v) { + if (dict.type == 'number') { + return parseInt(v); + + } else if (v == 'empty') { + v = ''; + } + + return v; + } + + function add(r) { + var dict = ngx.shared[r.args.dict]; + var value = convertToValue(dict, r.args.value); + + if (r.args.timeout) { + var timeout = Number(r.args.timeout); + r.return(200, dict.add(r.args.key, value, timeout)); + + } else { + r.return(200, dict.add(r.args.key, value)); + } + } + + function clear(r) { + var dict = ngx.shared[r.args.dict]; + var result = dict.clear(); + r.return(200, result === undefined ? 'undefined' : result); + } + + function del(r) { + var dict = ngx.shared[r.args.dict]; + r.return(200, dict.delete(r.args.key)); + } + + function get(r) { + var dict = ngx.shared[r.args.dict]; + var val = dict.get(r.args.key); + + if (val == '') { + val = 'empty'; + + } else if (val === undefined) { + val = 'undefined'; + } + + r.return(200, val); + } + + function incr(r) { + var dict = ngx.shared[r.args.dict]; + var def = r.args.def ? parseInt(r.args.def) : 0; + + if (r.args.timeout) { + var timeout = Number(r.args.timeout); + var val = dict.incr(r.args.key, parseInt(r.args.by), def, timeout); + r.return(200, val); + + } else { + var val = dict.incr(r.args.key, parseInt(r.args.by), def); + r.return(200, val); + } + } + + function pop(r) { + var dict = ngx.shared[r.args.dict]; + var val = dict.pop(r.args.key); + if (val == '') { + val = 'empty'; + + } else if (val === undefined) { + val = 'undefined'; + } + + r.return(200, val); + } + + function set(r) { + var dict = ngx.shared[r.args.dict]; + var value = convertToValue(dict, r.args.value); + + if (r.args.timeout) { + var timeout = Number(r.args.timeout); + r.return(200, dict.set(r.args.key, value, timeout) === dict); + + } else { + r.return(200, dict.set(r.args.key, value) === dict); + } + } + + export default { add, clear, del, get, incr, pop, set }; +EOF + +$t->try_run('no js_shared_dict_zone with state=')->plan(11); + +############################################################################### + +like(http_get('/get?dict=bar&key=waka'), qr/foo/, 'get bar.waka'); +like(http_get('/get?dict=bar&key=bar'), qr/abc/, 'get bar.bar'); +like(http_get('/get?dict=bar&key=FOO%20%0A'), qr/BAZ/, 'get bar["FOO \\n"]'); +like(http_get('/get?dict=bar&key=abc'), qr/def/, 'get bar.abc'); + +http_get('/set?dict=bar&key=waka&value=foo2'); +http_get('/delete?dict=bar&key=bar'); + +http_get('/set?dict=waka&key=foo&value=42'); + +select undef, undef, undef, 1.1; + +$t->reload(); + +my $bar_state = read_state($t, 'bar.json'); +my $waka_state = read_state($t, 'waka.json'); + +is($bar_state->{waka}->{value}, 'foo2', 'get bar.waka from state'); +is($bar_state->{bar}, undef, 'no bar.bar in state'); +is($waka_state->{foo}->{value}, '42', 'get waka.foo from state'); +like($waka_state->{foo}->{expire}, qr/^\d+$/, 'waka.foo expire'); + +http_get('/pop?dict=bar&key=FOO%20%0A'); + +http_get('/incr?dict=waka&key=foo&by=1'); + +select undef, undef, undef, 1.1; + +$bar_state = read_state($t, 'bar.json'); +$waka_state = read_state($t, 'waka.json'); + +is($bar_state->{'FOO \\n'}, undef, 'no bar.FOO \\n in state'); +is($waka_state->{foo}->{value}, '43', 'get waka.foo from state'); + +http_get('/clear?dict=bar'); + +select undef, undef, undef, 1.1; + +$bar_state = read_state($t, 'bar.json'); + +is($bar_state->{waka}, undef, 'no bar.waka in state'); + +############################################################################### + +sub decode_json { + my $json; + eval { $json = JSON::PP::decode_json(shift) }; + + if ($@) { + return ""; + } + + return $json; +} + +sub read_state { + my ($self, $file) = @_; + my $json = $self->read_file($file); + + if ($json) { + $json = decode_json($json); + } + + return $json; +} + +############################################################################### diff --git a/nginx/t/stream_js_shared_dict_state.t b/nginx/t/stream_js_shared_dict_state.t new file mode 100644 index 00000000..c2edb63e --- /dev/null +++ b/nginx/t/stream_js_shared_dict_state.t @@ -0,0 +1,137 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for js_shared_dict_zone directive, state= parameter. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/stream/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +stream { + %%TEST_GLOBALS_STREAM%% + + js_import test.js; + + js_shared_dict_zone zone=foo:32k state=foo.json; + + server { + listen 127.0.0.1:8081; + js_preread test.preread_verify; + proxy_pass 127.0.0.1:8090; + } +} + +EOF + +$t->write_file('foo.json', <write_file('test.js', <= 4 && collect.readUInt16BE(0) == 0xabcd) { + let id = collect.slice(2,4); + + ngx.shared.foo.get(id) ? s.done(): s.deny(); + + } else if (collect.length) { + s.deny(); + } + }); + } + + export default { preread_verify }; + +EOF + +$t->try_run('no js_shared_dict_zone with state='); + +$t->plan(2); + +$t->run_daemon(\&stream_daemon, port(8090)); +$t->waitforsocket('127.0.0.1:' . port(8090)); + +############################################################################### + +is(stream('127.0.0.1:' . port(8081))->io("\xAB\xCDQY##"), "", + 'access failed, QY is not in the shared dict'); +is(stream('127.0.0.1:' . port(8081))->io("\xAB\xCDQZ##"), "\xAB\xCDQZ##", + 'access granted'); + +############################################################################### + +sub stream_daemon { + my $server = IO::Socket::INET->new( + Proto => 'tcp', + LocalAddr => '127.0.0.1:' . port(8090), + Listen => 5, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + local $SIG{PIPE} = 'IGNORE'; + + while (my $client = $server->accept()) { + $client->autoflush(1); + + log2c("(new connection $client)"); + + $client->sysread(my $buffer, 65536) or next; + + log2i("$client $buffer"); + + log2o("$client $buffer"); + + $client->syswrite($buffer); + + close $client; + } +} + +sub log2i { Test::Nginx::log_core('|| <<', @_); } +sub log2o { Test::Nginx::log_core('|| >>', @_); } +sub log2c { Test::Nginx::log_core('||', @_); } + +sub get { + my ($url, %extra) = @_; + + my $s = IO::Socket::INET->new( + Proto => 'tcp', + PeerAddr => '127.0.0.1:' . port(8082) + ) or die "Can't connect to nginx: $!\n"; + + return http_get($url, socket => $s); +} + +############################################################################### From noreply at nginx.com Tue May 27 22:21:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Tue, 27 May 2025 22:21:02 +0000 (UTC) Subject: [njs] QuickJS: added memory limit check for reuse queue. Message-ID: <20250527222102.04D20475E2@pubserv1.nginx> details: https://github.com/nginx/njs/commit/0c0d4e50ec4d582b345bc64c12b737b109b7bc03 branches: master commit: 0c0d4e50ec4d582b345bc64c12b737b109b7bc03 user: Dmitry Volyntsev date: Wed, 21 May 2025 17:10:15 -0700 description: QuickJS: added memory limit check for reuse queue. --- nginx/ngx_http_js_module.c | 7 +++++++ nginx/ngx_js.c | 26 ++++++++++++++++++++++++++ nginx/ngx_js.h | 1 + nginx/ngx_stream_js_module.c | 7 +++++++ 4 files changed, 41 insertions(+) diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c index 28798172..1e0a927f 100644 --- a/nginx/ngx_http_js_module.c +++ b/nginx/ngx_http_js_module.c @@ -433,6 +433,13 @@ static ngx_command_t ngx_http_js_commands[] = { offsetof(ngx_http_js_loc_conf_t, reuse), NULL }, + { ngx_string("js_context_reuse_max_size"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_size_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_js_loc_conf_t, reuse_max_size), + NULL }, + { ngx_string("js_import"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE13, ngx_js_import, diff --git a/nginx/ngx_js.c b/nginx/ngx_js.c index e4bae32a..515218b9 100644 --- a/nginx/ngx_js.c +++ b/nginx/ngx_js.c @@ -1133,6 +1133,7 @@ ngx_engine_qjs_destroy(ngx_engine_t *e, ngx_js_ctx_t *ctx, JSRuntime *rt; JSContext *cx; JSClassID class_id; + JSMemoryUsage stats; ngx_qjs_event_t *event; ngx_js_opaque_t *opaque; njs_rbtree_node_t *node; @@ -1198,6 +1199,28 @@ ngx_engine_qjs_destroy(ngx_engine_t *e, ngx_js_ctx_t *ctx, cln->data = conf->reuse_queue; } + /* + * After the request object is freed, the runtime's memory usage should + * be low. It can only remain high if the global scope was + * modified. + * + * To prevent unlimited memory consumption growth, check whether memory + * usage exceeds the configured limit. The check is performed rarely to + * avoid performance impact of JS_ComputeMemoryUsage() which is slow. + */ + + if ((ngx_random() & 0xff) == 1) { + JS_ComputeMemoryUsage(JS_GetRuntime(cx), &stats); + + if ((size_t) stats.malloc_size > conf->reuse_max_size) { + ngx_log_error(NGX_LOG_WARN, ctx->log, 0, + "js remaining memory usage of the context " + "exceeds \"js_context_reuse_max_size\" limit: %L" + ", not reusing it", stats.malloc_size); + goto free_ctx; + } + } + if (ngx_js_queue_push(conf->reuse_queue, cx) != NGX_OK) { goto free_ctx; } @@ -3950,6 +3973,7 @@ ngx_js_create_conf(ngx_conf_t *cf, size_t size) conf->preload_objects = NGX_CONF_UNSET_PTR; conf->reuse = NGX_CONF_UNSET_SIZE; + conf->reuse_max_size = NGX_CONF_UNSET_SIZE; conf->buffer_size = NGX_CONF_UNSET_SIZE; conf->max_response_body_size = NGX_CONF_UNSET_SIZE; conf->timeout = NGX_CONF_UNSET_MSEC; @@ -4059,6 +4083,8 @@ ngx_js_merge_conf(ngx_conf_t *cf, void *parent, void *child, ngx_conf_merge_msec_value(conf->timeout, prev->timeout, 60000); ngx_conf_merge_size_value(conf->reuse, prev->reuse, 128); + ngx_conf_merge_size_value(conf->reuse_max_size, prev->reuse_max_size, + 4 * 1024 * 1024); ngx_conf_merge_size_value(conf->buffer_size, prev->buffer_size, 16384); ngx_conf_merge_size_value(conf->max_response_body_size, prev->max_response_body_size, 1048576); diff --git a/nginx/ngx_js.h b/nginx/ngx_js.h index 122881af..99330f88 100644 --- a/nginx/ngx_js.h +++ b/nginx/ngx_js.h @@ -122,6 +122,7 @@ typedef struct { ngx_uint_t type; \ ngx_engine_t *engine; \ ngx_uint_t reuse; \ + size_t reuse_max_size; \ ngx_js_queue_t *reuse_queue; \ ngx_str_t cwd; \ ngx_array_t *imports; \ diff --git a/nginx/ngx_stream_js_module.c b/nginx/ngx_stream_js_module.c index fb58cdc6..328ce581 100644 --- a/nginx/ngx_stream_js_module.c +++ b/nginx/ngx_stream_js_module.c @@ -264,6 +264,13 @@ static ngx_command_t ngx_stream_js_commands[] = { offsetof(ngx_stream_js_srv_conf_t, reuse), NULL }, + { ngx_string("js_context_reuse_max_size"), + NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1, + ngx_conf_set_size_slot, + NGX_STREAM_SRV_CONF_OFFSET, + offsetof(ngx_stream_js_srv_conf_t, reuse_max_size), + NULL }, + { ngx_string("js_import"), NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE13, ngx_js_import, From noreply at nginx.com Wed May 28 15:59:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Wed, 28 May 2025 15:59:02 +0000 (UTC) Subject: [njs] Branch created: seg_fault_at_err_msg Message-ID: <20250528155902.7EF01489E6@pubserv1.nginx> details: https://github.com/nginx/njs/commit/e562f44e08c36c28c627fd67d1b2f23d2ed4f949 branches: seg_fault_at_err_msg commit: e562f44e08c36c28c627fd67d1b2f23d2ed4f949 user: Vadim Zhestikov date: Wed, 28 May 2025 08:56:22 -0700 description: Fixed segmentation fault at error message. This closes #918 issue on Github. --- src/njs_value.c | 2 +- src/test/njs_unit_test.c | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/njs_value.c b/src/njs_value.c index dcdab5a2..9d2065e5 100644 --- a/src/njs_value.c +++ b/src/njs_value.c @@ -609,7 +609,7 @@ njs_property_query(njs_vm_t *vm, njs_property_query_t *pq, njs_value_t *value, default: ret = njs_atom_to_value(vm, &key, atom_id); - if (njs_fast_path(ret == NJS_OK)) { + if (njs_fast_path(ret == NJS_OK) && !njs_is_symbol(&key)) { njs_string_get(vm, &key, &pq->lhq.key); njs_type_error(vm, "cannot get property \"%V\" of %s", &pq->lhq.key, njs_is_null(value) ? "null" diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c index 2227c0a6..cae8766b 100644 --- a/src/test/njs_unit_test.c +++ b/src/test/njs_unit_test.c @@ -3923,6 +3923,9 @@ static njs_unit_test_t njs_test[] = { njs_str("delete this !== true"), njs_str("false") }, + { njs_str("undefined[Symbol()]"), + njs_str("TypeError: cannot get property \"unknown\" of undefined") }, + /* Object shorthand methods. */ { njs_str("var o = {m(){}}; new o.m();"), From noreply at nginx.com Wed May 28 16:12:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Wed, 28 May 2025 16:12:02 +0000 (UTC) Subject: [njs] Branch deleted: seg_fault_at_err_msg Message-ID: <20250528161202.7DEC5489E8@pubserv1.nginx> details: branches: seg_fault_at_err_msg commit: e562f44e08c36c28c627fd67d1b2f23d2ed4f949 user: date: Wed, 28 May 2025 16:12:02 +0000 From noreply at nginx.com Fri May 30 21:12:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 30 May 2025 21:12:02 +0000 (UTC) Subject: [njs] Fixed segfault at error message in njs_property_query(). Message-ID: <20250530211202.B6EF54788F@pubserv1.nginx> details: https://github.com/nginx/njs/commit/f1aa75247a1fc95ca46ce07d4d05288247c9bda8 branches: master commit: f1aa75247a1fc95ca46ce07d4d05288247c9bda8 user: Vadim Zhestikov date: Wed, 28 May 2025 09:16:38 -0700 description: Fixed segfault at error message in njs_property_query(). The issue was introduced in b28e50b1 (0.9.0). This closes #918 issue on Github. --- src/njs_generator.c | 2 +- src/njs_lexer.h | 10 ---------- src/njs_object.h | 7 +++---- src/njs_parser.c | 4 +--- src/njs_value.c | 17 +++-------------- src/njs_variable.c | 2 +- src/test/njs_unit_test.c | 3 +++ 7 files changed, 12 insertions(+), 33 deletions(-) diff --git a/src/njs_generator.c b/src/njs_generator.c index a6209cce..e8c61c0a 100644 --- a/src/njs_generator.c +++ b/src/njs_generator.c @@ -5491,7 +5491,7 @@ njs_generate_reference_error(njs_vm_t *vm, njs_generator_t *generator, ref_err->type = NJS_OBJ_TYPE_REF_ERROR; - njs_lexer_entry(vm, node->u.reference.atom_id, &entry); + njs_atom_string_get(vm, node->u.reference.atom_id, &entry); return njs_name_copy(vm, &ref_err->u.name, &entry); } diff --git a/src/njs_lexer.h b/src/njs_lexer.h index 42f612a6..3728e253 100644 --- a/src/njs_lexer.h +++ b/src/njs_lexer.h @@ -288,16 +288,6 @@ const njs_lexer_keyword_entry_t *njs_lexer_keyword(const u_char *key, njs_int_t njs_lexer_keywords(njs_arr_t *array); -njs_inline void -njs_lexer_entry(njs_vm_t *vm, uintptr_t atom_id, njs_str_t *entry) -{ - njs_value_t value; - - njs_atom_to_value(vm, &value, atom_id); - njs_string_get(vm, &value, entry); -} - - njs_inline njs_bool_t njs_lexer_token_is_keyword(njs_lexer_token_t *token) { diff --git a/src/njs_object.h b/src/njs_object.h index 6b2438df..85e914d4 100644 --- a/src/njs_object.h +++ b/src/njs_object.h @@ -241,18 +241,17 @@ njs_key_string_get(njs_vm_t *vm, njs_value_t *key, njs_str_t *str) } -njs_inline njs_int_t +njs_inline void njs_atom_string_get(njs_vm_t *vm, uint32_t atom_id, njs_str_t *str) { njs_value_t value; if (njs_atom_to_value(vm, &value, atom_id) != NJS_OK) { - return NJS_ERROR; + str->start = (u_char *) "unknown"; + str->length = njs_length("unknown"); } njs_key_string_get(vm, &value, str); - - return NJS_OK; } diff --git a/src/njs_parser.c b/src/njs_parser.c index 50cee672..3cd56fdf 100644 --- a/src/njs_parser.c +++ b/src/njs_parser.c @@ -6702,7 +6702,6 @@ njs_parser_labelled_statement_after(njs_parser_t *parser, njs_int_t ret; njs_str_t str; uintptr_t atom_id; - njs_value_t entry; njs_parser_node_t *node; node = parser->node; @@ -6719,8 +6718,7 @@ njs_parser_labelled_statement_after(njs_parser_t *parser, atom_id = (uint32_t) (uintptr_t) parser->target; - njs_atom_to_value(parser->vm, &entry, atom_id); - njs_string_get(parser->vm, &entry, &str); + njs_atom_string_get(parser->vm, atom_id, &str); ret = njs_name_copy(parser->vm, &parser->node->name, &str); if (ret != NJS_OK) { diff --git a/src/njs_value.c b/src/njs_value.c index dcdab5a2..a097e575 100644 --- a/src/njs_value.c +++ b/src/njs_value.c @@ -560,7 +560,6 @@ njs_property_query(njs_vm_t *vm, njs_property_query_t *pq, njs_value_t *value, { uint32_t index; njs_int_t ret; - njs_value_t key; njs_object_t *obj; njs_function_t *function; @@ -607,19 +606,9 @@ njs_property_query(njs_vm_t *vm, njs_property_query_t *pq, njs_value_t *value, case NJS_UNDEFINED: case NJS_NULL: default: - ret = njs_atom_to_value(vm, &key, atom_id); - - if (njs_fast_path(ret == NJS_OK)) { - njs_string_get(vm, &key, &pq->lhq.key); - njs_type_error(vm, "cannot get property \"%V\" of %s", - &pq->lhq.key, njs_is_null(value) ? "null" - : "undefined"); - return NJS_ERROR; - } - - njs_type_error(vm, "cannot get property \"unknown\" of %s", - njs_is_null(value) ? "null" : "undefined"); - + njs_atom_string_get(vm, atom_id, &pq->lhq.key); + njs_type_error(vm, "cannot get property \"%V\" of %s", &pq->lhq.key, + njs_type_string(value->type)); return NJS_ERROR; } diff --git a/src/njs_variable.c b/src/njs_variable.c index 78bd2afb..40924ccf 100644 --- a/src/njs_variable.c +++ b/src/njs_variable.c @@ -239,7 +239,7 @@ njs_variable_scope_find(njs_parser_t *parser, njs_parser_scope_t *scope, failed: - njs_lexer_entry(parser->vm, atom_id, &entry); + njs_atom_string_get(parser->vm, atom_id, &entry); njs_parser_syntax_error(parser, "\"%V\" has already been declared", &entry); return NULL; diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c index 2227c0a6..27fcbd82 100644 --- a/src/test/njs_unit_test.c +++ b/src/test/njs_unit_test.c @@ -3923,6 +3923,9 @@ static njs_unit_test_t njs_test[] = { njs_str("delete this !== true"), njs_str("false") }, + { njs_str("undefined[Symbol()]"), + njs_str("TypeError: cannot get property \"Symbol()\" of undefined") }, + /* Object shorthand methods. */ { njs_str("var o = {m(){}}; new o.m();"),