From noreply at nginx.com Mon Jan 6 23:53:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Mon, 6 Jan 2025 23:53:02 +0000 (UTC) Subject: [njs] Version bump. Message-ID: <20250106235302.A6C0847810@pubserv1.nginx> details: https://github.com/nginx/njs/commit/4fb1c0ca6c950dc0460eaeec1ba3e96a53070878 branches: master commit: 4fb1c0ca6c950dc0460eaeec1ba3e96a53070878 user: Dmitry Volyntsev date: Mon, 6 Jan 2025 09:09:43 -0800 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 06eb770c..42a6f405 100644 --- a/src/njs.h +++ b/src/njs.h @@ -11,8 +11,8 @@ #include -#define NJS_VERSION "0.8.8" -#define NJS_VERSION_NUMBER 0x000808 +#define NJS_VERSION "0.8.9" +#define NJS_VERSION_NUMBER 0x000809 #include From noreply at nginx.com Mon Jan 6 23:53:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Mon, 6 Jan 2025 23:53:02 +0000 (UTC) Subject: [njs] Modules: removed extra VM creation per server. Message-ID: <20250106235302.AC1FD48EF4@pubserv1.nginx> details: https://github.com/nginx/njs/commit/855aa4c9fac01bd9fbdb1602b523edc00117ff09 branches: master commit: 855aa4c9fac01bd9fbdb1602b523edc00117ff09 user: Dmitry Volyntsev date: Fri, 3 Jan 2025 22:25:15 -0800 description: Modules: removed extra VM creation per server. Previously, when js_import was declared in http or stream blocks, an extra copy of the VM instance was created for each server block. This was not needed and consumed a lot of memory for configurations with many server blocks. This issue was introduced in 9b674412 (0.8.6) and was partially fixed for location blocks only in 685b64f0 (0.8.7). --- nginx/ngx_js.c | 17 ++++++++ nginx/t/js_import2.t | 12 +++++- nginx/t/js_merge_location_blocks.t | 83 ++++++++++++++++++++++++++++++++++++++ nginx/t/js_merge_server_blocks.t | 78 +++++++++++++++++++++++++++++++++++ nginx/t/stream_js.t | 6 ++- 5 files changed, 194 insertions(+), 2 deletions(-) diff --git a/nginx/ngx_js.c b/nginx/ngx_js.c index 12b577a2..5c2a44cb 100644 --- a/nginx/ngx_js.c +++ b/nginx/ngx_js.c @@ -3343,6 +3343,16 @@ ngx_js_merge_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf, ngx_array_t *imports, *preload_objects, *paths; ngx_js_named_path_t *import, *pi, *pij, *preload; + if (prev->imports != NGX_CONF_UNSET_PTR && prev->engine == NULL) { + /* + * special handling to preserve conf->engine + * in the "http" or "stream" section to inherit it to all servers + */ + if (init_vm(cf, (ngx_js_loc_conf_t *) prev) != NGX_OK) { + return NGX_ERROR; + } + } + if (conf->imports == NGX_CONF_UNSET_PTR && conf->type == prev->type && conf->paths == NGX_CONF_UNSET_PTR @@ -3851,6 +3861,9 @@ ngx_js_init_conf_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf, return NGX_ERROR; } + ngx_log_error(NGX_LOG_NOTICE, cf->log, 0, "js vm init %s: %p", + conf->engine->name, conf->engine); + cln = ngx_pool_cleanup_add(cf->pool, 0); if (cln == NULL) { return NGX_ERROR; @@ -4039,6 +4052,10 @@ ngx_js_merge_conf(ngx_conf_t *cf, void *parent, void *child, ngx_js_loc_conf_t *conf = child; ngx_conf_merge_uint_value(conf->type, prev->type, NGX_ENGINE_NJS); + if (prev->type == NGX_CONF_UNSET_UINT) { + prev->type = NGX_ENGINE_NJS; + } + ngx_conf_merge_msec_value(conf->timeout, prev->timeout, 60000); ngx_conf_merge_size_value(conf->reuse, prev->reuse, 128); ngx_conf_merge_size_value(conf->buffer_size, prev->buffer_size, 16384); diff --git a/nginx/t/js_import2.t b/nginx/t/js_import2.t index cd29d2dc..7fdc624d 100644 --- a/nginx/t/js_import2.t +++ b/nginx/t/js_import2.t @@ -41,6 +41,7 @@ http { js_set $test foo.bar.p; + # context 1 js_import foo from main.js; location /njs { @@ -52,11 +53,13 @@ http { } location /test_lib { + # context 2 js_import lib.js; js_content lib.test; } location /test_fun { + # context 3 js_import fun.js; js_content fun; } @@ -75,6 +78,7 @@ http { server_name localhost; location /test_fun { + # context 4 js_import fun.js; js_content fun; } @@ -114,7 +118,7 @@ $t->write_file('main.js', <try_run('no njs available')->plan(5); +$t->try_run('no njs available')->plan(6); ############################################################################### @@ -124,4 +128,10 @@ like(http_get('/test_fun'), qr/FUN-TEST/s, 'fun'); like(http_get('/proxy/test_fun'), qr/FUN-TEST/s, 'proxy fun'); like(http_get('/test_var'), qr/P-TEST/s, 'foo.bar.p'); +$t->stop(); + +my $content = $t->read_file('error.log'); +my $count = () = $content =~ m/js vm init/g; +ok($count == 4, 'uniq js vm contexts'); + ############################################################################### diff --git a/nginx/t/js_merge_location_blocks.t b/nginx/t/js_merge_location_blocks.t new file mode 100644 index 00000000..328d5338 --- /dev/null +++ b/nginx/t/js_merge_location_blocks.t @@ -0,0 +1,83 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (c) Nginx, Inc. + +# Tests for http njs module, check for proper location blocks merging. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_import main.js; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /a { + js_content main.version; + } + + location /b { + js_content main.version; + } + + location /c { + js_content main.version; + } + + location /d { + js_content main.version; + } + } +} + +EOF + +$t->write_file('main.js', <try_run('no njs available')->plan(1); + +############################################################################### + +$t->stop(); + +my $content = $t->read_file('error.log'); +my $count = () = $content =~ m/ js vm init/g; +ok($count == 1, 'http js block imported once'); + +############################################################################### diff --git a/nginx/t/js_merge_server_blocks.t b/nginx/t/js_merge_server_blocks.t new file mode 100644 index 00000000..c3f261af --- /dev/null +++ b/nginx/t/js_merge_server_blocks.t @@ -0,0 +1,78 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (c) Nginx, Inc. + +# Tests for http njs module, check for proper server blocks merging. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_import main.js; + + server { + listen 127.0.0.1:8080; + } + + server { + listen 127.0.0.1:8081; + } + + server { + listen 127.0.0.1:8082; + } + + server { + listen 127.0.0.1:8083; + } +} + +EOF + +$t->write_file('main.js', <try_run('no njs available')->plan(1); + +############################################################################### + +$t->stop(); + +my $content = $t->read_file('error.log'); +my $count = () = $content =~ m/ js vm init/g; +ok($count == 1, 'http js block imported once'); + +############################################################################### diff --git a/nginx/t/stream_js.t b/nginx/t/stream_js.t index 52ab688e..0834b68a 100644 --- a/nginx/t/stream_js.t +++ b/nginx/t/stream_js.t @@ -394,7 +394,7 @@ $t->write_file('test.js', <run_daemon(\&stream_daemon, port(8090)); -$t->try_run('no stream njs available')->plan(24); +$t->try_run('no stream njs available')->plan(25); $t->waitforsocket('127.0.0.1:' . port(8090)); ############################################################################### @@ -450,6 +450,10 @@ like($t->read_file('status.log'), qr/$p[0]:200/, 'status undecided'); like($t->read_file('status.log'), qr/$p[1]:200/, 'status allow'); like($t->read_file('status.log'), qr/$p[2]:403/, 'status deny'); +my $content = $t->read_file('error.log'); +my $count = () = $content =~ m/ js vm init/g; +ok($count == 2, 'http and stream js blocks imported once each'); + ############################################################################### sub has_version { From noreply at nginx.com Thu Jan 9 13:09:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Thu, 9 Jan 2025 13:09:02 +0000 (UTC) Subject: [nginx] Year 2025. Message-ID: <20250109130902.8EF1347816@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/febe6e728ff83cfc5d5bcc0c74b4d8d63dc296b0 branches: master commit: febe6e728ff83cfc5d5bcc0c74b4d8d63dc296b0 user: Roman Arutyunyan date: Thu, 9 Jan 2025 17:00:14 +0400 description: Year 2025. --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index a7ec58a75..4efd90b51 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ Copyright (C) 2002-2021 Igor Sysoev -Copyright (C) 2011-2024 Nginx, Inc. +Copyright (C) 2011-2025 Nginx, Inc. All rights reserved. Redistribution and use in source and binary forms, with or without From noreply at nginx.com Thu Jan 9 13:20:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Thu, 9 Jan 2025 13:20:02 +0000 (UTC) Subject: [nginx] Gzip: compatibility with recent zlib-ng 2.2.x versions. Message-ID: <20250109132002.8F90947818@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/57d54fd922e7ecbebb78598d13adc9df1a4b69c0 branches: master commit: 57d54fd922e7ecbebb78598d13adc9df1a4b69c0 user: Sergey Kandaurov date: Mon, 23 Dec 2024 17:57:45 +0400 description: Gzip: compatibility with recent zlib-ng 2.2.x versions. It now uses 5/4 times more memory for the pending buffer. Further, a single allocation is now used, which takes additional 56 bytes for deflate_allocs in 64-bit mode aligned to 16, to store sub-allocation pointers, and the total allocation size now padded up to 128 bytes, which takes theoretically 200 additional bytes in total. This fits though into "4 * (64 + sizeof(void*))" additional space for ZALLOC used in zlib-ng 2.1.x versions. The comment was updated to reflect this. --- src/http/modules/ngx_http_gzip_filter_module.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/http/modules/ngx_http_gzip_filter_module.c b/src/http/modules/ngx_http_gzip_filter_module.c index b55527845..7113df695 100644 --- a/src/http/modules/ngx_http_gzip_filter_module.c +++ b/src/http/modules/ngx_http_gzip_filter_module.c @@ -516,8 +516,10 @@ ngx_http_gzip_filter_memory(ngx_http_request_t *r, ngx_http_gzip_ctx_t *ctx) /* * Another zlib variant, https://github.com/zlib-ng/zlib-ng. * It used to force window bits to 13 for fast compression level, - * uses (64 + sizeof(void*)) additional space on all allocations - * for alignment, 16-byte padding in one of window-sized buffers, + * used (64 + sizeof(void*)) additional space on all allocations + * for alignment and 16-byte padding in one of window-sized buffers, + * uses a single allocation with up to 200 bytes for alignment and + * internal pointers, 5/4 times more memory for the pending buffer, * and 128K hash. */ @@ -526,7 +528,7 @@ ngx_http_gzip_filter_memory(ngx_http_request_t *r, ngx_http_gzip_ctx_t *ctx) } ctx->allocated = 8192 + 16 + (1 << (wbits + 2)) - + 131072 + (1 << (memlevel + 8)) + + 131072 + (5 << (memlevel + 6)) + 4 * (64 + sizeof(void*)); ctx->zlib_ng = 1; } From noreply at nginx.com Mon Jan 13 23:31:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Mon, 13 Jan 2025 23:31:02 +0000 (UTC) Subject: [njs] 2025 year. Message-ID: <20250113233102.7428D4873D@pubserv1.nginx> details: https://github.com/nginx/njs/commit/17676e5c9a9f5bdd63509632c31a9019686b171e branches: master commit: 17676e5c9a9f5bdd63509632c31a9019686b171e user: Dmitry Volyntsev date: Mon, 13 Jan 2025 14:58:28 -0800 description: 2025 year. --- LICENSE | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index b510378f..187d237a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,9 +1,10 @@ /* - * Copyright (C) 2015-2024 NGINX, Inc. + * Copyright (C) 2015-2019 NGINX, Inc. + * Copyright (C) 2019-2025 F5, Inc. * Copyright (C) 2015-2021 Igor Sysoev - * Copyright (C) 2017-2024 Dmitry Volyntsev + * Copyright (C) 2017-2025 Dmitry Volyntsev * Copyright (C) 2019-2022 Alexander Borisov - * Copyright (C) 2022-2024 Vadim Zhestikov + * Copyright (C) 2022-2025 Vadim Zhestikov * All rights reserved. * * Redistribution and use in source and binary forms, with or without From noreply at nginx.com Mon Jan 13 23:31:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Mon, 13 Jan 2025 23:31:02 +0000 (UTC) Subject: [njs] Version 0.8.9. Message-ID: <20250113233102.77B7548EF2@pubserv1.nginx> details: https://github.com/nginx/njs/commit/b87ad67adb2c557bd96e52a3221748a7ba028858 branches: master commit: b87ad67adb2c557bd96e52a3221748a7ba028858 user: Dmitry Volyntsev date: Mon, 13 Jan 2025 14:56:10 -0800 description: Version 0.8.9. --- CHANGES | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGES b/CHANGES index cc7ebef4..c67eaf8b 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,20 @@ +Changes with njs 0.8.9 14 Jan 2025 + + nginx modules: + + *) Bugfix: removed extra VM creation per server. + Previously, when js_import was declared in http or stream blocks, + an extra copy of the VM instance was created for each server + block. This was not needed and consumed a lot of memory for + configurations with many server blocks. + + This issue was introduced in 9b674412 (0.8.6) and was partially + fixed for location blocks only in 685b64f0 (0.8.7). + + Core: + + *) Feature: added fs module for QuickJS engine. + Changes with njs 0.8.8 10 Dec 2024 nginx modules: From noreply at nginx.com Mon Jan 13 23:33:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Mon, 13 Jan 2025 23:33:02 +0000 (UTC) Subject: [njs] Lightweight tag created: 0.8.9 Message-ID: <20250113233302.34F9C48EF2@pubserv1.nginx> details: https://github.com/nginx/njs/releases/tag/0.8.9 branches: commit: b87ad67adb2c557bd96e52a3221748a7ba028858 user: Dmitry Volyntsev date: Mon Jan 13 14:56:10 2025 -0800 description: Version 0.8.9. From noreply at nginx.com Thu Jan 16 17:10:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Thu, 16 Jan 2025 17:10:02 +0000 (UTC) Subject: [nginx] Slice filter: log the expected range in case of range error. Message-ID: <20250116171002.CA63148F15@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/47f862ffad6a7068100d50887c495f80973ca47b branches: master commit: 47f862ffad6a7068100d50887c495f80973ca47b user: Daniel Vasquez Lopez date: Thu, 21 Nov 2024 14:27:07 -0800 description: Slice filter: log the expected range in case of range error. --- src/http/modules/ngx_http_slice_filter_module.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/http/modules/ngx_http_slice_filter_module.c b/src/http/modules/ngx_http_slice_filter_module.c index 186380a2f..3b0bef629 100644 --- a/src/http/modules/ngx_http_slice_filter_module.c +++ b/src/http/modules/ngx_http_slice_filter_module.c @@ -165,8 +165,8 @@ ngx_http_slice_header_filter(ngx_http_request_t *r) if (cr.start != ctx->start || cr.end != end) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, - "unexpected range in slice response: %O-%O", - cr.start, cr.end); + "unexpected range in slice response: %O-%O, " + "expected: %O-%O", cr.start, cr.end, ctx->start, end); return NGX_ERROR; } From noreply at nginx.com Fri Jan 17 00:38:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 17 Jan 2025 00:38:02 +0000 (UTC) Subject: [nginx] SSL: encrypted certificate keys are exempt from object cache. Message-ID: <20250117003802.4490948F15@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/7677d5646aeb761b8b9da5af3eb10c008aae3f90 branches: master commit: 7677d5646aeb761b8b9da5af3eb10c008aae3f90 user: Sergey Kandaurov date: Wed, 18 Dec 2024 20:09:58 +0400 description: SSL: encrypted certificate keys are exempt from object cache. SSL object cache, as previously introduced in 1.27.2, did not take into account encrypted certificate keys that might be unexpectedly fetched from the cache regardless of the matching passphrase. To avoid this, caching of encrypted certificate keys is now disabled based on the passphrase callback invocation. A notable exception is encrypted certificate keys configured without ssl_password_file. They are loaded once resulting in the passphrase prompt on startup and reused in other contexts as applicable. --- src/event/ngx_event_openssl_cache.c | 53 ++++++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/src/event/ngx_event_openssl_cache.c b/src/event/ngx_event_openssl_cache.c index 9d1962759..c79f77456 100644 --- a/src/event/ngx_event_openssl_cache.c +++ b/src/event/ngx_event_openssl_cache.c @@ -13,6 +13,8 @@ #define NGX_SSL_CACHE_DATA 1 #define NGX_SSL_CACHE_ENGINE 2 +#define NGX_SSL_CACHE_DISABLED (ngx_array_t *) (uintptr_t) -1 + #define ngx_ssl_cache_get_conf(cycle) \ (ngx_ssl_cache_t *) ngx_get_conf(cycle->conf_ctx, ngx_openssl_cache_module) @@ -61,6 +63,12 @@ typedef struct { } ngx_ssl_cache_t; +typedef struct { + ngx_str_t *pwd; + unsigned encrypted:1; +} ngx_ssl_cache_pwd_t; + + static ngx_int_t ngx_ssl_cache_init_key(ngx_pool_t *pool, ngx_uint_t index, ngx_str_t *path, ngx_ssl_cache_key_t *id); static ngx_ssl_cache_node_t *ngx_ssl_cache_lookup(ngx_ssl_cache_t *cache, @@ -228,9 +236,10 @@ ngx_ssl_cache_fetch(ngx_conf_t *cf, ngx_uint_t index, char **err, } if (value == NULL) { - value = type->create(&id, err, data); - if (value == NULL) { - return NULL; + value = type->create(&id, err, &data); + + if (value == NULL || data == NGX_SSL_CACHE_DISABLED) { + return value; } } @@ -269,7 +278,7 @@ ngx_ssl_cache_connection_fetch(ngx_pool_t *pool, ngx_uint_t index, char **err, return NULL; } - return ngx_ssl_cache_types[index].create(&id, err, data); + return ngx_ssl_cache_types[index].create(&id, err, &data); } @@ -472,13 +481,13 @@ ngx_ssl_cache_cert_ref(char **err, void *data) static void * ngx_ssl_cache_pkey_create(ngx_ssl_cache_key_t *id, char **err, void *data) { - ngx_array_t *passwords = data; + ngx_array_t **passwords = data; - BIO *bio; - EVP_PKEY *pkey; - ngx_str_t *pwd; - ngx_uint_t tries; - pem_password_cb *cb; + BIO *bio; + EVP_PKEY *pkey; + ngx_uint_t tries; + pem_password_cb *cb; + ngx_ssl_cache_pwd_t cb_data, *pwd; if (id->type == NGX_SSL_CACHE_ENGINE) { @@ -531,12 +540,16 @@ ngx_ssl_cache_pkey_create(ngx_ssl_cache_key_t *id, char **err, void *data) return NULL; } - if (passwords) { - tries = passwords->nelts; - pwd = passwords->elts; + cb_data.encrypted = 0; + + if (*passwords) { + cb_data.pwd = (*passwords)->elts; + tries = (*passwords)->nelts; + pwd = &cb_data; cb = ngx_ssl_cache_pkey_password_callback; } else { + cb_data.pwd = NULL; tries = 1; pwd = NULL; cb = NULL; @@ -552,7 +565,7 @@ ngx_ssl_cache_pkey_create(ngx_ssl_cache_key_t *id, char **err, void *data) if (tries-- > 1) { ERR_clear_error(); (void) BIO_reset(bio); - pwd++; + cb_data.pwd++; continue; } @@ -561,6 +574,10 @@ ngx_ssl_cache_pkey_create(ngx_ssl_cache_key_t *id, char **err, void *data) return NULL; } + if (cb_data.encrypted) { + *passwords = NGX_SSL_CACHE_DISABLED; + } + BIO_free(bio); return pkey; @@ -571,7 +588,9 @@ static int ngx_ssl_cache_pkey_password_callback(char *buf, int size, int rwflag, void *userdata) { - ngx_str_t *pwd = userdata; + ngx_ssl_cache_pwd_t *data = userdata; + + ngx_str_t *pwd; if (rwflag) { ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, 0, @@ -580,6 +599,10 @@ ngx_ssl_cache_pkey_password_callback(char *buf, int size, int rwflag, return 0; } + data->encrypted = 1; + + pwd = data->pwd; + if (pwd == NULL) { return 0; } From noreply at nginx.com Fri Jan 17 00:38:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 17 Jan 2025 00:38:02 +0000 (UTC) Subject: [nginx] SSL: cache revalidation of file based dynamic certificates. Message-ID: <20250117003802.541A048F17@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/4b96ad14f3607ab39b160715aeba721097ac4da4 branches: master commit: 4b96ad14f3607ab39b160715aeba721097ac4da4 user: Sergey Kandaurov date: Mon, 13 Jan 2025 21:40:04 +0400 description: SSL: cache revalidation of file based dynamic certificates. Revalidation is based on file modification time and uniq file index, and happens after the cache object validity time is expired. --- src/event/ngx_event_openssl_cache.c | 44 +++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/src/event/ngx_event_openssl_cache.c b/src/event/ngx_event_openssl_cache.c index 7589e6c90..eb03e16b2 100644 --- a/src/event/ngx_event_openssl_cache.c +++ b/src/event/ngx_event_openssl_cache.c @@ -289,6 +289,7 @@ ngx_ssl_cache_connection_fetch(ngx_ssl_cache_t *cache, ngx_pool_t *pool, void *value; time_t now; uint32_t hash; + ngx_file_info_t fi; ngx_ssl_cache_key_t id; ngx_ssl_cache_type_t *type; ngx_ssl_cache_node_t *cn; @@ -318,7 +319,33 @@ ngx_ssl_cache_connection_fetch(ngx_ssl_cache_t *cache, ngx_pool_t *pool, goto found; } - if (now - cn->created > cache->valid) { + if (now - cn->created <= cache->valid) { + goto found; + } + + switch (id.type) { + + case NGX_SSL_CACHE_PATH: + + if (ngx_file_info(id.data, &fi) != NGX_FILE_ERROR) { + + if (ngx_file_uniq(&fi) == cn->uniq + && ngx_file_mtime(&fi) == cn->mtime) + { + break; + } + + cn->mtime = ngx_file_mtime(&fi); + cn->uniq = ngx_file_uniq(&fi); + + } else { + cn->mtime = 0; + cn->uniq = 0; + } + + /* fall through */ + + default: ngx_log_debug1(NGX_LOG_DEBUG_CORE, pool->log, 0, "update cached ssl object: %s", cn->id.data); @@ -337,9 +364,10 @@ ngx_ssl_cache_connection_fetch(ngx_ssl_cache_t *cache, ngx_pool_t *pool, } cn->value = value; - cn->created = now; } + cn->created = now; + goto found; } @@ -365,6 +393,18 @@ ngx_ssl_cache_connection_fetch(ngx_ssl_cache_t *cache, ngx_pool_t *pool, ngx_cpystrn(cn->id.data, id.data, id.len + 1); + if (id.type == NGX_SSL_CACHE_PATH) { + + if (ngx_file_info(id.data, &fi) != NGX_FILE_ERROR) { + cn->mtime = ngx_file_mtime(&fi); + cn->uniq = ngx_file_uniq(&fi); + + } else { + cn->mtime = 0; + cn->uniq = 0; + } + } + ngx_ssl_cache_expire(cache, 1, pool->log); if (cache->current >= cache->max) { From noreply at nginx.com Fri Jan 17 00:38:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 17 Jan 2025 00:38:02 +0000 (UTC) Subject: [nginx] SSL: object cache inheritance from the old configuration cycle. Message-ID: <20250117003802.418B547857@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/8311e14ae614529aabe9e72e87051d191b723fb4 branches: master commit: 8311e14ae614529aabe9e72e87051d191b723fb4 user: Sergey Kandaurov date: Wed, 18 Dec 2024 20:03:35 +0400 description: SSL: object cache inheritance from the old configuration cycle. Memory based objects are always inherited, engine based objects are never inherited to adhere the volatile nature of engines, file based objects are inherited subject to modification time and file index. The previous behaviour to bypass cache from the old configuration cycle is preserved with a new directive "ssl_object_cache_inheritable off;". --- src/event/ngx_event_openssl_cache.c | 107 +++++++++++++++++++++++++++++++++--- 1 file changed, 99 insertions(+), 8 deletions(-) diff --git a/src/event/ngx_event_openssl_cache.c b/src/event/ngx_event_openssl_cache.c index 8829e2879..9d1962759 100644 --- a/src/event/ngx_event_openssl_cache.c +++ b/src/event/ngx_event_openssl_cache.c @@ -14,6 +14,14 @@ #define NGX_SSL_CACHE_ENGINE 2 +#define ngx_ssl_cache_get_conf(cycle) \ + (ngx_ssl_cache_t *) ngx_get_conf(cycle->conf_ctx, ngx_openssl_cache_module) + +#define ngx_ssl_cache_get_old_conf(cycle) \ + cycle->old_cycle->conf_ctx ? ngx_ssl_cache_get_conf(cycle->old_cycle) \ + : NULL + + typedef struct { unsigned type:2; unsigned len:30; @@ -39,12 +47,17 @@ typedef struct { ngx_ssl_cache_key_t id; ngx_ssl_cache_type_t *type; void *value; + + time_t mtime; + ngx_file_uniq_t uniq; } ngx_ssl_cache_node_t; typedef struct { ngx_rbtree_t rbtree; ngx_rbtree_node_t sentinel; + + ngx_flag_t inheritable; } ngx_ssl_cache_t; @@ -76,22 +89,36 @@ static void *ngx_ssl_cache_ca_create(ngx_ssl_cache_key_t *id, char **err, static BIO *ngx_ssl_cache_create_bio(ngx_ssl_cache_key_t *id, char **err); static void *ngx_openssl_cache_create_conf(ngx_cycle_t *cycle); +static char *ngx_openssl_cache_init_conf(ngx_cycle_t *cycle, void *conf); static void ngx_ssl_cache_cleanup(void *data); static void ngx_ssl_cache_node_insert(ngx_rbtree_node_t *temp, ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel); +static ngx_command_t ngx_openssl_cache_commands[] = { + + { ngx_string("ssl_object_cache_inheritable"), + NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_FLAG, + ngx_conf_set_flag_slot, + 0, + offsetof(ngx_ssl_cache_t, inheritable), + NULL }, + + ngx_null_command +}; + + static ngx_core_module_t ngx_openssl_cache_module_ctx = { ngx_string("openssl_cache"), ngx_openssl_cache_create_conf, - NULL + ngx_openssl_cache_init_conf }; ngx_module_t ngx_openssl_cache_module = { NGX_MODULE_V1, &ngx_openssl_cache_module_ctx, /* module context */ - NULL, /* module directives */ + ngx_openssl_cache_commands, /* module directives */ NGX_CORE_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ @@ -132,8 +159,13 @@ void * ngx_ssl_cache_fetch(ngx_conf_t *cf, ngx_uint_t index, char **err, ngx_str_t *path, void *data) { + void *value; + time_t mtime; uint32_t hash; - ngx_ssl_cache_t *cache; + ngx_int_t rc; + ngx_file_uniq_t uniq; + ngx_file_info_t fi; + ngx_ssl_cache_t *cache, *old_cache; ngx_ssl_cache_key_t id; ngx_ssl_cache_type_t *type; ngx_ssl_cache_node_t *cn; @@ -151,12 +183,60 @@ ngx_ssl_cache_fetch(ngx_conf_t *cf, ngx_uint_t index, char **err, hash = ngx_murmur_hash2(id.data, id.len); cn = ngx_ssl_cache_lookup(cache, type, &id, hash); + if (cn != NULL) { return type->ref(err, cn->value); } + value = NULL; + + if (id.type == NGX_SSL_CACHE_PATH + && (rc = ngx_file_info(id.data, &fi)) != NGX_FILE_ERROR) + { + mtime = ngx_file_mtime(&fi); + uniq = ngx_file_uniq(&fi); + + } else { + rc = NGX_FILE_ERROR; + mtime = 0; + uniq = 0; + } + + /* try to use a reference from the old cycle */ + + old_cache = ngx_ssl_cache_get_old_conf(cf->cycle); + + if (old_cache && old_cache->inheritable) { + cn = ngx_ssl_cache_lookup(old_cache, type, &id, hash); + + if (cn != NULL) { + switch (id.type) { + + case NGX_SSL_CACHE_DATA: + value = type->ref(err, cn->value); + break; + + default: + if (rc != NGX_FILE_ERROR + && uniq == cn->uniq && mtime == cn->mtime) + { + value = type->ref(err, cn->value); + } + break; + } + } + } + + if (value == NULL) { + value = type->create(&id, err, data); + if (value == NULL) { + return NULL; + } + } + cn = ngx_palloc(cf->pool, sizeof(ngx_ssl_cache_node_t) + id.len + 1); if (cn == NULL) { + type->free(value); return NULL; } @@ -165,14 +245,12 @@ ngx_ssl_cache_fetch(ngx_conf_t *cf, ngx_uint_t index, char **err, cn->id.len = id.len; cn->id.type = id.type; cn->type = type; + cn->value = value; + cn->mtime = mtime; + cn->uniq = uniq; ngx_cpystrn(cn->id.data, id.data, id.len + 1); - cn->value = type->create(&id, err, data); - if (cn->value == NULL) { - return NULL; - } - ngx_rbtree_insert(&cache->rbtree, &cn->node); return type->ref(err, cn->value); @@ -729,6 +807,8 @@ ngx_openssl_cache_create_conf(ngx_cycle_t *cycle) return NULL; } + cache->inheritable = NGX_CONF_UNSET; + cln = ngx_pool_cleanup_add(cycle->pool, 0); if (cln == NULL) { return NULL; @@ -744,6 +824,17 @@ ngx_openssl_cache_create_conf(ngx_cycle_t *cycle) } +static char * +ngx_openssl_cache_init_conf(ngx_cycle_t *cycle, void *conf) +{ + ngx_ssl_cache_t *cache = conf; + + ngx_conf_init_value(cache->inheritable, 1); + + return NGX_CONF_OK; +} + + static void ngx_ssl_cache_cleanup(void *data) { From noreply at nginx.com Fri Jan 17 00:38:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 17 Jan 2025 00:38:02 +0000 (UTC) Subject: [nginx] SSL: caching certificates and certificate keys with variables. Message-ID: <20250117003802.4F8E448F16@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/0e756d67aa1e42e3b1b360936eb4d6c06bced2c1 branches: master commit: 0e756d67aa1e42e3b1b360936eb4d6c06bced2c1 user: Sergey Kandaurov date: Tue, 29 Oct 2024 16:25:11 +0400 description: SSL: caching certificates and certificate keys with variables. A new directive "ssl_certificate_cache max=N [valid=time] [inactive=time]" enables caching of SSL certificate chain and secret key objects specified by "ssl_certificate" and "ssl_certificate_key" directives with variables. Co-authored-by: Aleksei Bavshin --- src/event/ngx_event_openssl.c | 11 +- src/event/ngx_event_openssl.h | 12 +- src/event/ngx_event_openssl_cache.c | 240 +++++++++++++++++++++++++++++---- src/http/modules/ngx_http_ssl_module.c | 106 +++++++++++++++ src/http/modules/ngx_http_ssl_module.h | 2 + src/http/ngx_http_request.c | 1 + src/http/ngx_http_upstream.c | 2 +- src/stream/ngx_stream_proxy_module.c | 2 +- src/stream/ngx_stream_ssl_module.c | 107 +++++++++++++++ src/stream/ngx_stream_ssl_module.h | 66 ++++----- 10 files changed, 481 insertions(+), 68 deletions(-) diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c index 35e9f3c88..8963c8124 100644 --- a/src/event/ngx_event_openssl.c +++ b/src/event/ngx_event_openssl.c @@ -562,15 +562,16 @@ ngx_ssl_certificate(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *cert, ngx_int_t ngx_ssl_connection_certificate(ngx_connection_t *c, ngx_pool_t *pool, - ngx_str_t *cert, ngx_str_t *key, ngx_array_t *passwords) + ngx_str_t *cert, ngx_str_t *key, ngx_ssl_cache_t *cache, + ngx_array_t *passwords) { char *err; X509 *x509; EVP_PKEY *pkey; STACK_OF(X509) *chain; - chain = ngx_ssl_cache_connection_fetch(pool, NGX_SSL_CACHE_CERT, &err, - cert, NULL); + chain = ngx_ssl_cache_connection_fetch(cache, pool, NGX_SSL_CACHE_CERT, + &err, cert, NULL); if (chain == NULL) { if (err != NULL) { ngx_ssl_error(NGX_LOG_ERR, c->log, 0, @@ -610,8 +611,8 @@ ngx_ssl_connection_certificate(ngx_connection_t *c, ngx_pool_t *pool, #endif - pkey = ngx_ssl_cache_connection_fetch(pool, NGX_SSL_CACHE_PKEY, &err, - key, passwords); + pkey = ngx_ssl_cache_connection_fetch(cache, pool, NGX_SSL_CACHE_PKEY, + &err, key, passwords); if (pkey == NULL) { if (err != NULL) { ngx_ssl_error(NGX_LOG_ERR, c->log, 0, diff --git a/src/event/ngx_event_openssl.h b/src/event/ngx_event_openssl.h index 2147205d6..0713c5671 100644 --- a/src/event/ngx_event_openssl.h +++ b/src/event/ngx_event_openssl.h @@ -83,7 +83,8 @@ #endif -typedef struct ngx_ssl_ocsp_s ngx_ssl_ocsp_t; +typedef struct ngx_ssl_cache_s ngx_ssl_cache_t; +typedef struct ngx_ssl_ocsp_s ngx_ssl_ocsp_t; struct ngx_ssl_s { @@ -214,7 +215,8 @@ ngx_int_t ngx_ssl_certificates(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_int_t ngx_ssl_certificate(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *cert, ngx_str_t *key, ngx_array_t *passwords); ngx_int_t ngx_ssl_connection_certificate(ngx_connection_t *c, ngx_pool_t *pool, - ngx_str_t *cert, ngx_str_t *key, ngx_array_t *passwords); + ngx_str_t *cert, ngx_str_t *key, ngx_ssl_cache_t *cache, + ngx_array_t *passwords); ngx_int_t ngx_ssl_ciphers(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *ciphers, ngx_uint_t prefer_server_ciphers); @@ -237,10 +239,12 @@ ngx_int_t ngx_ssl_ocsp_get_status(ngx_connection_t *c, const char **s); void ngx_ssl_ocsp_cleanup(ngx_connection_t *c); ngx_int_t ngx_ssl_ocsp_cache_init(ngx_shm_zone_t *shm_zone, void *data); +ngx_ssl_cache_t *ngx_ssl_cache_init(ngx_pool_t *pool, ngx_uint_t max, + time_t valid, time_t inactive); void *ngx_ssl_cache_fetch(ngx_conf_t *cf, ngx_uint_t index, char **err, ngx_str_t *path, void *data); -void *ngx_ssl_cache_connection_fetch(ngx_pool_t *pool, ngx_uint_t index, - char **err, ngx_str_t *path, void *data); +void *ngx_ssl_cache_connection_fetch(ngx_ssl_cache_t *cache, ngx_pool_t *pool, + ngx_uint_t index, char **err, ngx_str_t *path, void *data); ngx_array_t *ngx_ssl_read_password_file(ngx_conf_t *cf, ngx_str_t *file); ngx_array_t *ngx_ssl_preserve_passwords(ngx_conf_t *cf, diff --git a/src/event/ngx_event_openssl_cache.c b/src/event/ngx_event_openssl_cache.c index c79f77456..7589e6c90 100644 --- a/src/event/ngx_event_openssl_cache.c +++ b/src/event/ngx_event_openssl_cache.c @@ -46,21 +46,31 @@ typedef struct { typedef struct { ngx_rbtree_node_t node; + ngx_queue_t queue; ngx_ssl_cache_key_t id; ngx_ssl_cache_type_t *type; void *value; + time_t created; + time_t accessed; + time_t mtime; ngx_file_uniq_t uniq; } ngx_ssl_cache_node_t; -typedef struct { +struct ngx_ssl_cache_s { ngx_rbtree_t rbtree; ngx_rbtree_node_t sentinel; + ngx_queue_t expire_queue; ngx_flag_t inheritable; -} ngx_ssl_cache_t; + + ngx_uint_t current; + ngx_uint_t max; + time_t valid; + time_t inactive; +}; typedef struct { @@ -73,6 +83,8 @@ static ngx_int_t ngx_ssl_cache_init_key(ngx_pool_t *pool, ngx_uint_t index, ngx_str_t *path, ngx_ssl_cache_key_t *id); static ngx_ssl_cache_node_t *ngx_ssl_cache_lookup(ngx_ssl_cache_t *cache, ngx_ssl_cache_type_t *type, ngx_ssl_cache_key_t *id, uint32_t hash); +static void ngx_ssl_cache_expire(ngx_ssl_cache_t *cache, ngx_uint_t n, + ngx_log_t *log); static void *ngx_ssl_cache_cert_create(ngx_ssl_cache_key_t *id, char **err, void *data); @@ -101,6 +113,8 @@ static char *ngx_openssl_cache_init_conf(ngx_cycle_t *cycle, void *conf); static void ngx_ssl_cache_cleanup(void *data); static void ngx_ssl_cache_node_insert(ngx_rbtree_node_t *temp, ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel); +static void ngx_ssl_cache_node_free(ngx_rbtree_t *rbtree, + ngx_ssl_cache_node_t *cn); static ngx_command_t ngx_openssl_cache_commands[] = { @@ -260,6 +274,8 @@ ngx_ssl_cache_fetch(ngx_conf_t *cf, ngx_uint_t index, char **err, ngx_cpystrn(cn->id.data, id.data, id.len + 1); + ngx_queue_init(&cn->queue); + ngx_rbtree_insert(&cache->rbtree, &cn->node); return type->ref(err, cn->value); @@ -267,10 +283,15 @@ ngx_ssl_cache_fetch(ngx_conf_t *cf, ngx_uint_t index, char **err, void * -ngx_ssl_cache_connection_fetch(ngx_pool_t *pool, ngx_uint_t index, char **err, - ngx_str_t *path, void *data) +ngx_ssl_cache_connection_fetch(ngx_ssl_cache_t *cache, ngx_pool_t *pool, + ngx_uint_t index, char **err, ngx_str_t *path, void *data) { - ngx_ssl_cache_key_t id; + void *value; + time_t now; + uint32_t hash; + ngx_ssl_cache_key_t id; + ngx_ssl_cache_type_t *type; + ngx_ssl_cache_node_t *cn; *err = NULL; @@ -278,7 +299,89 @@ ngx_ssl_cache_connection_fetch(ngx_pool_t *pool, ngx_uint_t index, char **err, return NULL; } - return ngx_ssl_cache_types[index].create(&id, err, &data); + type = &ngx_ssl_cache_types[index]; + + if (cache == NULL) { + return type->create(&id, err, &data); + } + + now = ngx_time(); + + hash = ngx_murmur_hash2(id.data, id.len); + + cn = ngx_ssl_cache_lookup(cache, type, &id, hash); + + if (cn != NULL) { + ngx_queue_remove(&cn->queue); + + if (id.type == NGX_SSL_CACHE_DATA) { + goto found; + } + + if (now - cn->created > cache->valid) { + ngx_log_debug1(NGX_LOG_DEBUG_CORE, pool->log, 0, + "update cached ssl object: %s", cn->id.data); + + type->free(cn->value); + + value = type->create(&id, err, &data); + + if (value == NULL || data == NGX_SSL_CACHE_DISABLED) { + ngx_rbtree_delete(&cache->rbtree, &cn->node); + + cache->current--; + + ngx_free(cn); + + return value; + } + + cn->value = value; + cn->created = now; + } + + goto found; + } + + value = type->create(&id, err, &data); + + if (value == NULL || data == NGX_SSL_CACHE_DISABLED) { + return value; + } + + cn = ngx_alloc(sizeof(ngx_ssl_cache_node_t) + id.len + 1, pool->log); + if (cn == NULL) { + type->free(value); + return NULL; + } + + cn->node.key = hash; + cn->id.data = (u_char *)(cn + 1); + cn->id.len = id.len; + cn->id.type = id.type; + cn->type = type; + cn->value = value; + cn->created = now; + + ngx_cpystrn(cn->id.data, id.data, id.len + 1); + + ngx_ssl_cache_expire(cache, 1, pool->log); + + if (cache->current >= cache->max) { + ngx_ssl_cache_expire(cache, 0, pool->log); + } + + ngx_rbtree_insert(&cache->rbtree, &cn->node); + + cache->current++; + +found: + + cn->accessed = now; + + ngx_queue_insert_head(&cache->expire_queue, &cn->queue); + + return type->ref(err, cn->value); } @@ -365,6 +468,37 @@ ngx_ssl_cache_lookup(ngx_ssl_cache_t *cache, ngx_ssl_cache_type_t *type, } +static void +ngx_ssl_cache_expire(ngx_ssl_cache_t *cache, ngx_uint_t n, + ngx_log_t *log) +{ + time_t now; + ngx_queue_t *q; + ngx_ssl_cache_node_t *cn; + + now = ngx_time(); + + while (n < 3) { + + if (ngx_queue_empty(&cache->expire_queue)) { + return; + } + + q = ngx_queue_last(&cache->expire_queue); + + cn = ngx_queue_data(q, ngx_ssl_cache_node_t, queue); + + if (n++ != 0 && now - cn->accessed <= cache->inactive) { + return; + } + + ngx_ssl_cache_node_free(&cache->rbtree, cn); + + cache->current--; + } +} + + static void * ngx_ssl_cache_cert_create(ngx_ssl_cache_key_t *id, char **err, void *data) { @@ -822,27 +956,15 @@ ngx_ssl_cache_create_bio(ngx_ssl_cache_key_t *id, char **err) static void * ngx_openssl_cache_create_conf(ngx_cycle_t *cycle) { - ngx_ssl_cache_t *cache; - ngx_pool_cleanup_t *cln; + ngx_ssl_cache_t *cache; - cache = ngx_pcalloc(cycle->pool, sizeof(ngx_ssl_cache_t)); + cache = ngx_ssl_cache_init(cycle->pool, 0, 0, 0); if (cache == NULL) { return NULL; } cache->inheritable = NGX_CONF_UNSET; - cln = ngx_pool_cleanup_add(cycle->pool, 0); - if (cln == NULL) { - return NULL; - } - - cln->handler = ngx_ssl_cache_cleanup; - cln->data = cache; - - ngx_rbtree_init(&cache->rbtree, &cache->sentinel, - ngx_ssl_cache_node_insert); - return cache; } @@ -858,6 +980,39 @@ ngx_openssl_cache_init_conf(ngx_cycle_t *cycle, void *conf) } +ngx_ssl_cache_t * +ngx_ssl_cache_init(ngx_pool_t *pool, ngx_uint_t max, time_t valid, + time_t inactive) +{ + ngx_ssl_cache_t *cache; + ngx_pool_cleanup_t *cln; + + cache = ngx_pcalloc(pool, sizeof(ngx_ssl_cache_t)); + if (cache == NULL) { + return NULL; + } + + ngx_rbtree_init(&cache->rbtree, &cache->sentinel, + ngx_ssl_cache_node_insert); + + ngx_queue_init(&cache->expire_queue); + + cache->max = max; + cache->valid = valid; + cache->inactive = inactive; + + cln = ngx_pool_cleanup_add(pool, 0); + if (cln == NULL) { + return NULL; + } + + cln->handler = ngx_ssl_cache_cleanup; + cln->data = cache; + + return cache; +} + + static void ngx_ssl_cache_cleanup(void *data) { @@ -873,12 +1028,47 @@ ngx_ssl_cache_cleanup(void *data) return; } - for (node = ngx_rbtree_min(tree->root, tree->sentinel); - node; - node = ngx_rbtree_next(tree, node)) - { + node = ngx_rbtree_min(tree->root, tree->sentinel); + + while (node != NULL) { cn = ngx_rbtree_data(node, ngx_ssl_cache_node_t, node); - cn->type->free(cn->value); + node = ngx_rbtree_next(tree, node); + + ngx_ssl_cache_node_free(tree, cn); + + if (cache->max) { + cache->current--; + } + } + + if (cache->current) { + ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, 0, + "%ui items still left in ssl cache", + cache->current); + } + + if (!ngx_queue_empty(&cache->expire_queue)) { + ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, 0, + "queue still is not empty in ssl cache"); + + } +} + + +static void +ngx_ssl_cache_node_free(ngx_rbtree_t *rbtree, ngx_ssl_cache_node_t *cn) +{ + cn->type->free(cn->value); + + ngx_rbtree_delete(rbtree, &cn->node); + + if (!ngx_queue_empty(&cn->queue)) { + ngx_queue_remove(&cn->queue); + + ngx_log_debug1(NGX_LOG_DEBUG_CORE, ngx_cycle->log, 0, + "delete cached ssl object: %s", cn->id.data); + + ngx_free(cn); } } diff --git a/src/http/modules/ngx_http_ssl_module.c b/src/http/modules/ngx_http_ssl_module.c index 0e892b04d..dbfe5c08b 100644 --- a/src/http/modules/ngx_http_ssl_module.c +++ b/src/http/modules/ngx_http_ssl_module.c @@ -43,6 +43,8 @@ static char *ngx_http_ssl_merge_srv_conf(ngx_conf_t *cf, static ngx_int_t ngx_http_ssl_compile_certificates(ngx_conf_t *cf, ngx_http_ssl_srv_conf_t *conf); +static char *ngx_http_ssl_certificate_cache(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); static char *ngx_http_ssl_password_file(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); static char *ngx_http_ssl_session_cache(ngx_conf_t *cf, ngx_command_t *cmd, @@ -108,6 +110,13 @@ static ngx_command_t ngx_http_ssl_commands[] = { offsetof(ngx_http_ssl_srv_conf_t, certificate_keys), NULL }, + { ngx_string("ssl_certificate_cache"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE123, + ngx_http_ssl_certificate_cache, + NGX_HTTP_SRV_CONF_OFFSET, + 0, + NULL }, + { ngx_string("ssl_password_file"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1, ngx_http_ssl_password_file, @@ -619,6 +628,7 @@ ngx_http_ssl_create_srv_conf(ngx_conf_t *cf) sscf->verify_depth = NGX_CONF_UNSET_UINT; sscf->certificates = NGX_CONF_UNSET_PTR; sscf->certificate_keys = NGX_CONF_UNSET_PTR; + sscf->certificate_cache = NGX_CONF_UNSET_PTR; sscf->passwords = NGX_CONF_UNSET_PTR; sscf->conf_commands = NGX_CONF_UNSET_PTR; sscf->builtin_session_cache = NGX_CONF_UNSET; @@ -664,6 +674,9 @@ ngx_http_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) ngx_conf_merge_ptr_value(conf->certificate_keys, prev->certificate_keys, NULL); + ngx_conf_merge_ptr_value(conf->certificate_cache, prev->certificate_cache, + NULL); + ngx_conf_merge_ptr_value(conf->passwords, prev->passwords, NULL); ngx_conf_merge_str_value(conf->dhparam, prev->dhparam, ""); @@ -984,6 +997,99 @@ found: } +static char * +ngx_http_ssl_certificate_cache(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_http_ssl_srv_conf_t *sscf = conf; + + time_t inactive, valid; + ngx_str_t *value, s; + ngx_int_t max; + ngx_uint_t i; + + if (sscf->certificate_cache != NGX_CONF_UNSET_PTR) { + return "is duplicate"; + } + + value = cf->args->elts; + + max = 0; + inactive = 10; + valid = 60; + + for (i = 1; i < cf->args->nelts; i++) { + + if (ngx_strncmp(value[i].data, "max=", 4) == 0) { + + max = ngx_atoi(value[i].data + 4, value[i].len - 4); + if (max <= 0) { + goto failed; + } + + continue; + } + + if (ngx_strncmp(value[i].data, "inactive=", 9) == 0) { + + s.len = value[i].len - 9; + s.data = value[i].data + 9; + + inactive = ngx_parse_time(&s, 1); + if (inactive == (time_t) NGX_ERROR) { + goto failed; + } + + continue; + } + + if (ngx_strncmp(value[i].data, "valid=", 6) == 0) { + + s.len = value[i].len - 6; + s.data = value[i].data + 6; + + valid = ngx_parse_time(&s, 1); + if (valid == (time_t) NGX_ERROR) { + goto failed; + } + + continue; + } + + if (ngx_strcmp(value[i].data, "off") == 0) { + + sscf->certificate_cache = NULL; + + continue; + } + + failed: + + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid parameter \"%V\"", &value[i]); + return NGX_CONF_ERROR; + } + + if (sscf->certificate_cache == NULL) { + return NGX_CONF_OK; + } + + if (max == 0) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "\"ssl_certificate_cache\" must have " + "the \"max\" parameter"); + return NGX_CONF_ERROR; + } + + sscf->certificate_cache = ngx_ssl_cache_init(cf->pool, max, valid, + inactive); + if (sscf->certificate_cache == NULL) { + return NGX_CONF_ERROR; + } + + return NGX_CONF_OK; +} + + static char * ngx_http_ssl_password_file(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { diff --git a/src/http/modules/ngx_http_ssl_module.h b/src/http/modules/ngx_http_ssl_module.h index c69c8ffd2..8650fab93 100644 --- a/src/http/modules/ngx_http_ssl_module.h +++ b/src/http/modules/ngx_http_ssl_module.h @@ -38,6 +38,8 @@ typedef struct { ngx_array_t *certificate_values; ngx_array_t *certificate_key_values; + ngx_ssl_cache_t *certificate_cache; + ngx_str_t dhparam; ngx_str_t ecdh_curve; ngx_str_t client_certificate; diff --git a/src/http/ngx_http_request.c b/src/http/ngx_http_request.c index 3cca57cf5..f44c9e79b 100644 --- a/src/http/ngx_http_request.c +++ b/src/http/ngx_http_request.c @@ -1054,6 +1054,7 @@ ngx_http_ssl_certificate(ngx_ssl_conn_t *ssl_conn, void *arg) "ssl key: \"%s\"", key.data); if (ngx_ssl_connection_certificate(c, r->pool, &cert, &key, + sscf->certificate_cache, sscf->passwords) != NGX_OK) { diff --git a/src/http/ngx_http_upstream.c b/src/http/ngx_http_upstream.c index d95662c56..e6382ef79 100644 --- a/src/http/ngx_http_upstream.c +++ b/src/http/ngx_http_upstream.c @@ -2018,7 +2018,7 @@ ngx_http_upstream_ssl_certificate(ngx_http_request_t *r, ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0, "http upstream ssl key: \"%s\"", key.data); - if (ngx_ssl_connection_certificate(c, r->pool, &cert, &key, + if (ngx_ssl_connection_certificate(c, r->pool, &cert, &key, NULL, u->conf->ssl_passwords) != NGX_OK) { diff --git a/src/stream/ngx_stream_proxy_module.c b/src/stream/ngx_stream_proxy_module.c index e978056ef..21b579af3 100644 --- a/src/stream/ngx_stream_proxy_module.c +++ b/src/stream/ngx_stream_proxy_module.c @@ -1324,7 +1324,7 @@ ngx_stream_proxy_ssl_certificate(ngx_stream_session_t *s) ngx_log_debug1(NGX_LOG_DEBUG_STREAM, c->log, 0, "stream upstream ssl key: \"%s\"", key.data); - if (ngx_ssl_connection_certificate(c, c->pool, &cert, &key, + if (ngx_ssl_connection_certificate(c, c->pool, &cert, &key, NULL, pscf->ssl_passwords) != NGX_OK) { diff --git a/src/stream/ngx_stream_ssl_module.c b/src/stream/ngx_stream_ssl_module.c index dfbaa0e2f..b84995d61 100644 --- a/src/stream/ngx_stream_ssl_module.c +++ b/src/stream/ngx_stream_ssl_module.c @@ -47,6 +47,8 @@ static char *ngx_stream_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent, static ngx_int_t ngx_stream_ssl_compile_certificates(ngx_conf_t *cf, ngx_stream_ssl_srv_conf_t *conf); +static char *ngx_stream_ssl_certificate_cache(ngx_conf_t *cf, + ngx_command_t *cmd, void *conf); static char *ngx_stream_ssl_password_file(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); static char *ngx_stream_ssl_session_cache(ngx_conf_t *cf, ngx_command_t *cmd, @@ -117,6 +119,13 @@ static ngx_command_t ngx_stream_ssl_commands[] = { offsetof(ngx_stream_ssl_srv_conf_t, certificate_keys), NULL }, + { ngx_string("ssl_certificate_cache"), + NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE123, + ngx_stream_ssl_certificate_cache, + NGX_STREAM_SRV_CONF_OFFSET, + 0, + NULL }, + { ngx_string("ssl_password_file"), NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1, ngx_stream_ssl_password_file, @@ -718,6 +727,7 @@ ngx_stream_ssl_certificate(ngx_ssl_conn_t *ssl_conn, void *arg) "ssl key: \"%s\"", key.data); if (ngx_ssl_connection_certificate(c, c->pool, &cert, &key, + sscf->certificate_cache, sscf->passwords) != NGX_OK) { @@ -844,6 +854,7 @@ ngx_stream_ssl_create_srv_conf(ngx_conf_t *cf) sscf->handshake_timeout = NGX_CONF_UNSET_MSEC; sscf->certificates = NGX_CONF_UNSET_PTR; sscf->certificate_keys = NGX_CONF_UNSET_PTR; + sscf->certificate_cache = NGX_CONF_UNSET_PTR; sscf->passwords = NGX_CONF_UNSET_PTR; sscf->conf_commands = NGX_CONF_UNSET_PTR; sscf->prefer_server_ciphers = NGX_CONF_UNSET; @@ -892,6 +903,9 @@ ngx_stream_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) ngx_conf_merge_ptr_value(conf->certificate_keys, prev->certificate_keys, NULL); + ngx_conf_merge_ptr_value(conf->certificate_cache, prev->certificate_cache, + NULL); + ngx_conf_merge_ptr_value(conf->passwords, prev->passwords, NULL); ngx_conf_merge_str_value(conf->dhparam, prev->dhparam, ""); @@ -1202,6 +1216,99 @@ found: } +static char * +ngx_stream_ssl_certificate_cache(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_stream_ssl_srv_conf_t *sscf = conf; + + time_t inactive, valid; + ngx_str_t *value, s; + ngx_int_t max; + ngx_uint_t i; + + if (sscf->certificate_cache != NGX_CONF_UNSET_PTR) { + return "is duplicate"; + } + + value = cf->args->elts; + + max = 0; + inactive = 10; + valid = 60; + + for (i = 1; i < cf->args->nelts; i++) { + + if (ngx_strncmp(value[i].data, "max=", 4) == 0) { + + max = ngx_atoi(value[i].data + 4, value[i].len - 4); + if (max <= 0) { + goto failed; + } + + continue; + } + + if (ngx_strncmp(value[i].data, "inactive=", 9) == 0) { + + s.len = value[i].len - 9; + s.data = value[i].data + 9; + + inactive = ngx_parse_time(&s, 1); + if (inactive == (time_t) NGX_ERROR) { + goto failed; + } + + continue; + } + + if (ngx_strncmp(value[i].data, "valid=", 6) == 0) { + + s.len = value[i].len - 6; + s.data = value[i].data + 6; + + valid = ngx_parse_time(&s, 1); + if (valid == (time_t) NGX_ERROR) { + goto failed; + } + + continue; + } + + if (ngx_strcmp(value[i].data, "off") == 0) { + + sscf->certificate_cache = NULL; + + continue; + } + + failed: + + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid parameter \"%V\"", &value[i]); + return NGX_CONF_ERROR; + } + + if (sscf->certificate_cache == NULL) { + return NGX_CONF_OK; + } + + if (max == 0) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "\"ssl_certificate_cache\" must have " + "the \"max\" parameter"); + return NGX_CONF_ERROR; + } + + sscf->certificate_cache = ngx_ssl_cache_init(cf->pool, max, valid, + inactive); + if (sscf->certificate_cache == NULL) { + return NGX_CONF_ERROR; + } + + return NGX_CONF_OK; +} + + static char * ngx_stream_ssl_password_file(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { diff --git a/src/stream/ngx_stream_ssl_module.h b/src/stream/ngx_stream_ssl_module.h index e6769426c..ffa03a6f3 100644 --- a/src/stream/ngx_stream_ssl_module.h +++ b/src/stream/ngx_stream_ssl_module.h @@ -15,53 +15,55 @@ typedef struct { - ngx_msec_t handshake_timeout; + ngx_msec_t handshake_timeout; - ngx_flag_t prefer_server_ciphers; - ngx_flag_t reject_handshake; + ngx_flag_t prefer_server_ciphers; + ngx_flag_t reject_handshake; - ngx_ssl_t ssl; + ngx_ssl_t ssl; - ngx_uint_t protocols; + ngx_uint_t protocols; - ngx_uint_t verify; - ngx_uint_t verify_depth; + ngx_uint_t verify; + ngx_uint_t verify_depth; - ssize_t builtin_session_cache; + ssize_t builtin_session_cache; - time_t session_timeout; + time_t session_timeout; - ngx_array_t *certificates; - ngx_array_t *certificate_keys; + ngx_array_t *certificates; + ngx_array_t *certificate_keys; - ngx_array_t *certificate_values; - ngx_array_t *certificate_key_values; + ngx_array_t *certificate_values; + ngx_array_t *certificate_key_values; - ngx_str_t dhparam; - ngx_str_t ecdh_curve; - ngx_str_t client_certificate; - ngx_str_t trusted_certificate; - ngx_str_t crl; - ngx_str_t alpn; + ngx_ssl_cache_t *certificate_cache; - ngx_str_t ciphers; + ngx_str_t dhparam; + ngx_str_t ecdh_curve; + ngx_str_t client_certificate; + ngx_str_t trusted_certificate; + ngx_str_t crl; + ngx_str_t alpn; - ngx_array_t *passwords; - ngx_array_t *conf_commands; + ngx_str_t ciphers; - ngx_shm_zone_t *shm_zone; + ngx_array_t *passwords; + ngx_array_t *conf_commands; - ngx_flag_t session_tickets; - ngx_array_t *session_ticket_keys; + ngx_shm_zone_t *shm_zone; - ngx_uint_t ocsp; - ngx_str_t ocsp_responder; - ngx_shm_zone_t *ocsp_cache_zone; + ngx_flag_t session_tickets; + ngx_array_t *session_ticket_keys; - ngx_flag_t stapling; - ngx_flag_t stapling_verify; - ngx_str_t stapling_file; - ngx_str_t stapling_responder; + ngx_uint_t ocsp; + ngx_str_t ocsp_responder; + ngx_shm_zone_t *ocsp_cache_zone; + + ngx_flag_t stapling; + ngx_flag_t stapling_verify; + ngx_str_t stapling_file; + ngx_str_t stapling_responder; } ngx_stream_ssl_srv_conf_t; From noreply at nginx.com Fri Jan 17 00:38:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 17 Jan 2025 00:38:02 +0000 (UTC) Subject: [nginx] Upstream: caching certificates and certificate keys with variables. Message-ID: <20250117003802.5FC0E48F1A@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/454ad0ef33a347eba1a62d18c8fc0498f4dcfd64 branches: master commit: 454ad0ef33a347eba1a62d18c8fc0498f4dcfd64 user: Sergey Kandaurov date: Tue, 29 Oct 2024 18:20:53 +0400 description: Upstream: caching certificates and certificate keys with variables. Caching is enabled with proxy_ssl_certificate_cache and friends. Co-authored-by: Aleksei Bavshin --- src/http/modules/ngx_http_grpc_module.c | 106 +++++++++++++++++++++++++++++ src/http/modules/ngx_http_proxy_module.c | 106 +++++++++++++++++++++++++++++ src/http/modules/ngx_http_uwsgi_module.c | 106 +++++++++++++++++++++++++++++ src/http/ngx_http_upstream.c | 3 +- src/http/ngx_http_upstream.h | 1 + src/stream/ngx_stream_proxy_module.c | 111 ++++++++++++++++++++++++++++++- 6 files changed, 431 insertions(+), 2 deletions(-) diff --git a/src/http/modules/ngx_http_grpc_module.c b/src/http/modules/ngx_http_grpc_module.c index 326720447..8e246c3cf 100644 --- a/src/http/modules/ngx_http_grpc_module.c +++ b/src/http/modules/ngx_http_grpc_module.c @@ -205,6 +205,8 @@ static char *ngx_http_grpc_pass(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); #if (NGX_HTTP_SSL) +static char *ngx_http_grpc_ssl_certificate_cache(ngx_conf_t *cf, + ngx_command_t *cmd, void *conf); static char *ngx_http_grpc_ssl_password_file(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); static char *ngx_http_grpc_ssl_conf_command_check(ngx_conf_t *cf, void *post, @@ -437,6 +439,13 @@ static ngx_command_t ngx_http_grpc_commands[] = { offsetof(ngx_http_grpc_loc_conf_t, upstream.ssl_certificate_key), NULL }, + { ngx_string("grpc_ssl_certificate_cache"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE123, + ngx_http_grpc_ssl_certificate_cache, + NGX_HTTP_LOC_CONF_OFFSET, + 0, + NULL }, + { ngx_string("grpc_ssl_password_file"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, ngx_http_grpc_ssl_password_file, @@ -4386,6 +4395,7 @@ ngx_http_grpc_create_loc_conf(ngx_conf_t *cf) conf->ssl_verify_depth = NGX_CONF_UNSET_UINT; conf->upstream.ssl_certificate = NGX_CONF_UNSET_PTR; conf->upstream.ssl_certificate_key = NGX_CONF_UNSET_PTR; + conf->upstream.ssl_certificate_cache = NGX_CONF_UNSET_PTR; conf->upstream.ssl_passwords = NGX_CONF_UNSET_PTR; conf->ssl_conf_commands = NGX_CONF_UNSET_PTR; #endif @@ -4497,6 +4507,8 @@ ngx_http_grpc_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) prev->upstream.ssl_certificate, NULL); ngx_conf_merge_ptr_value(conf->upstream.ssl_certificate_key, prev->upstream.ssl_certificate_key, NULL); + ngx_conf_merge_ptr_value(conf->upstream.ssl_certificate_cache, + prev->upstream.ssl_certificate_cache, NULL); ngx_conf_merge_ptr_value(conf->upstream.ssl_passwords, prev->upstream.ssl_passwords, NULL); @@ -4847,6 +4859,100 @@ ngx_http_grpc_pass(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) #if (NGX_HTTP_SSL) +static char * +ngx_http_grpc_ssl_certificate_cache(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf) +{ + ngx_http_grpc_loc_conf_t *plcf = conf; + + time_t inactive, valid; + ngx_str_t *value, s; + ngx_int_t max; + ngx_uint_t i; + + if (plcf->upstream.ssl_certificate_cache != NGX_CONF_UNSET_PTR) { + return "is duplicate"; + } + + value = cf->args->elts; + + max = 0; + inactive = 10; + valid = 60; + + for (i = 1; i < cf->args->nelts; i++) { + + if (ngx_strncmp(value[i].data, "max=", 4) == 0) { + + max = ngx_atoi(value[i].data + 4, value[i].len - 4); + if (max <= 0) { + goto failed; + } + + continue; + } + + if (ngx_strncmp(value[i].data, "inactive=", 9) == 0) { + + s.len = value[i].len - 9; + s.data = value[i].data + 9; + + inactive = ngx_parse_time(&s, 1); + if (inactive == (time_t) NGX_ERROR) { + goto failed; + } + + continue; + } + + if (ngx_strncmp(value[i].data, "valid=", 6) == 0) { + + s.len = value[i].len - 6; + s.data = value[i].data + 6; + + valid = ngx_parse_time(&s, 1); + if (valid == (time_t) NGX_ERROR) { + goto failed; + } + + continue; + } + + if (ngx_strcmp(value[i].data, "off") == 0) { + + plcf->upstream.ssl_certificate_cache = NULL; + + continue; + } + + failed: + + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid parameter \"%V\"", &value[i]); + return NGX_CONF_ERROR; + } + + if (plcf->upstream.ssl_certificate_cache == NULL) { + return NGX_CONF_OK; + } + + if (max == 0) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "\"grpc_ssl_certificate_cache\" must have " + "the \"max\" parameter"); + return NGX_CONF_ERROR; + } + + plcf->upstream.ssl_certificate_cache = ngx_ssl_cache_init(cf->pool, max, + valid, inactive); + if (plcf->upstream.ssl_certificate_cache == NULL) { + return NGX_CONF_ERROR; + } + + return NGX_CONF_OK; +} + + static char * ngx_http_grpc_ssl_password_file(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { diff --git a/src/http/modules/ngx_http_proxy_module.c b/src/http/modules/ngx_http_proxy_module.c index 855ef523d..27c34fef2 100644 --- a/src/http/modules/ngx_http_proxy_module.c +++ b/src/http/modules/ngx_http_proxy_module.c @@ -226,6 +226,8 @@ static char *ngx_http_proxy_cache_key(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); #endif #if (NGX_HTTP_SSL) +static char *ngx_http_proxy_ssl_certificate_cache(ngx_conf_t *cf, + ngx_command_t *cmd, void *conf); static char *ngx_http_proxy_ssl_password_file(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); #endif @@ -775,6 +777,13 @@ static ngx_command_t ngx_http_proxy_commands[] = { offsetof(ngx_http_proxy_loc_conf_t, upstream.ssl_certificate_key), NULL }, + { ngx_string("proxy_ssl_certificate_cache"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE123, + ngx_http_proxy_ssl_certificate_cache, + NGX_HTTP_LOC_CONF_OFFSET, + 0, + NULL }, + { ngx_string("proxy_ssl_password_file"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, ngx_http_proxy_ssl_password_file, @@ -3613,6 +3622,7 @@ ngx_http_proxy_create_loc_conf(ngx_conf_t *cf) conf->upstream.ssl_verify = NGX_CONF_UNSET; conf->upstream.ssl_certificate = NGX_CONF_UNSET_PTR; conf->upstream.ssl_certificate_key = NGX_CONF_UNSET_PTR; + conf->upstream.ssl_certificate_cache = NGX_CONF_UNSET_PTR; conf->upstream.ssl_passwords = NGX_CONF_UNSET_PTR; conf->ssl_verify_depth = NGX_CONF_UNSET_UINT; conf->ssl_conf_commands = NGX_CONF_UNSET_PTR; @@ -3964,6 +3974,8 @@ ngx_http_proxy_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) prev->upstream.ssl_certificate, NULL); ngx_conf_merge_ptr_value(conf->upstream.ssl_certificate_key, prev->upstream.ssl_certificate_key, NULL); + ngx_conf_merge_ptr_value(conf->upstream.ssl_certificate_cache, + prev->upstream.ssl_certificate_cache, NULL); ngx_conf_merge_ptr_value(conf->upstream.ssl_passwords, prev->upstream.ssl_passwords, NULL); @@ -5074,6 +5086,100 @@ ngx_http_proxy_cache_key(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) #if (NGX_HTTP_SSL) +static char * +ngx_http_proxy_ssl_certificate_cache(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf) +{ + ngx_http_proxy_loc_conf_t *plcf = conf; + + time_t inactive, valid; + ngx_str_t *value, s; + ngx_int_t max; + ngx_uint_t i; + + if (plcf->upstream.ssl_certificate_cache != NGX_CONF_UNSET_PTR) { + return "is duplicate"; + } + + value = cf->args->elts; + + max = 0; + inactive = 10; + valid = 60; + + for (i = 1; i < cf->args->nelts; i++) { + + if (ngx_strncmp(value[i].data, "max=", 4) == 0) { + + max = ngx_atoi(value[i].data + 4, value[i].len - 4); + if (max <= 0) { + goto failed; + } + + continue; + } + + if (ngx_strncmp(value[i].data, "inactive=", 9) == 0) { + + s.len = value[i].len - 9; + s.data = value[i].data + 9; + + inactive = ngx_parse_time(&s, 1); + if (inactive == (time_t) NGX_ERROR) { + goto failed; + } + + continue; + } + + if (ngx_strncmp(value[i].data, "valid=", 6) == 0) { + + s.len = value[i].len - 6; + s.data = value[i].data + 6; + + valid = ngx_parse_time(&s, 1); + if (valid == (time_t) NGX_ERROR) { + goto failed; + } + + continue; + } + + if (ngx_strcmp(value[i].data, "off") == 0) { + + plcf->upstream.ssl_certificate_cache = NULL; + + continue; + } + + failed: + + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid parameter \"%V\"", &value[i]); + return NGX_CONF_ERROR; + } + + if (plcf->upstream.ssl_certificate_cache == NULL) { + return NGX_CONF_OK; + } + + if (max == 0) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "\"proxy_ssl_certificate_cache\" must have " + "the \"max\" parameter"); + return NGX_CONF_ERROR; + } + + plcf->upstream.ssl_certificate_cache = ngx_ssl_cache_init(cf->pool, max, + valid, inactive); + if (plcf->upstream.ssl_certificate_cache == NULL) { + return NGX_CONF_ERROR; + } + + return NGX_CONF_OK; +} + + static char * ngx_http_proxy_ssl_password_file(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { diff --git a/src/http/modules/ngx_http_uwsgi_module.c b/src/http/modules/ngx_http_uwsgi_module.c index 7988cc589..14aae5bf1 100644 --- a/src/http/modules/ngx_http_uwsgi_module.c +++ b/src/http/modules/ngx_http_uwsgi_module.c @@ -92,6 +92,8 @@ static char *ngx_http_uwsgi_cache_key(ngx_conf_t *cf, ngx_command_t *cmd, #endif #if (NGX_HTTP_SSL) +static char *ngx_http_uwsgi_ssl_certificate_cache(ngx_conf_t *cf, + ngx_command_t *cmd, void *conf); static char *ngx_http_uwsgi_ssl_password_file(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); static char *ngx_http_uwsgi_ssl_conf_command_check(ngx_conf_t *cf, void *post, @@ -559,6 +561,13 @@ static ngx_command_t ngx_http_uwsgi_commands[] = { offsetof(ngx_http_uwsgi_loc_conf_t, upstream.ssl_certificate_key), NULL }, + { ngx_string("uwsgi_ssl_certificate_cache"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE123, + ngx_http_uwsgi_ssl_certificate_cache, + NGX_HTTP_LOC_CONF_OFFSET, + 0, + NULL }, + { ngx_string("uwsgi_ssl_password_file"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, ngx_http_uwsgi_ssl_password_file, @@ -1590,6 +1599,7 @@ ngx_http_uwsgi_create_loc_conf(ngx_conf_t *cf) conf->ssl_verify_depth = NGX_CONF_UNSET_UINT; conf->upstream.ssl_certificate = NGX_CONF_UNSET_PTR; conf->upstream.ssl_certificate_key = NGX_CONF_UNSET_PTR; + conf->upstream.ssl_certificate_cache = NGX_CONF_UNSET_PTR; conf->upstream.ssl_passwords = NGX_CONF_UNSET_PTR; conf->ssl_conf_commands = NGX_CONF_UNSET_PTR; #endif @@ -1921,6 +1931,8 @@ ngx_http_uwsgi_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) prev->upstream.ssl_certificate, NULL); ngx_conf_merge_ptr_value(conf->upstream.ssl_certificate_key, prev->upstream.ssl_certificate_key, NULL); + ngx_conf_merge_ptr_value(conf->upstream.ssl_certificate_cache, + prev->upstream.ssl_certificate_cache, NULL); ngx_conf_merge_ptr_value(conf->upstream.ssl_passwords, prev->upstream.ssl_passwords, NULL); @@ -2455,6 +2467,100 @@ ngx_http_uwsgi_cache_key(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) #if (NGX_HTTP_SSL) +static char * +ngx_http_uwsgi_ssl_certificate_cache(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf) +{ + ngx_http_uwsgi_loc_conf_t *plcf = conf; + + time_t inactive, valid; + ngx_str_t *value, s; + ngx_int_t max; + ngx_uint_t i; + + if (plcf->upstream.ssl_certificate_cache != NGX_CONF_UNSET_PTR) { + return "is duplicate"; + } + + value = cf->args->elts; + + max = 0; + inactive = 10; + valid = 60; + + for (i = 1; i < cf->args->nelts; i++) { + + if (ngx_strncmp(value[i].data, "max=", 4) == 0) { + + max = ngx_atoi(value[i].data + 4, value[i].len - 4); + if (max <= 0) { + goto failed; + } + + continue; + } + + if (ngx_strncmp(value[i].data, "inactive=", 9) == 0) { + + s.len = value[i].len - 9; + s.data = value[i].data + 9; + + inactive = ngx_parse_time(&s, 1); + if (inactive == (time_t) NGX_ERROR) { + goto failed; + } + + continue; + } + + if (ngx_strncmp(value[i].data, "valid=", 6) == 0) { + + s.len = value[i].len - 6; + s.data = value[i].data + 6; + + valid = ngx_parse_time(&s, 1); + if (valid == (time_t) NGX_ERROR) { + goto failed; + } + + continue; + } + + if (ngx_strcmp(value[i].data, "off") == 0) { + + plcf->upstream.ssl_certificate_cache = NULL; + + continue; + } + + failed: + + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid parameter \"%V\"", &value[i]); + return NGX_CONF_ERROR; + } + + if (plcf->upstream.ssl_certificate_cache == NULL) { + return NGX_CONF_OK; + } + + if (max == 0) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "\"uwsgi_ssl_certificate_cache\" must have " + "the \"max\" parameter"); + return NGX_CONF_ERROR; + } + + plcf->upstream.ssl_certificate_cache = ngx_ssl_cache_init(cf->pool, max, + valid, inactive); + if (plcf->upstream.ssl_certificate_cache == NULL) { + return NGX_CONF_ERROR; + } + + return NGX_CONF_OK; +} + + static char * ngx_http_uwsgi_ssl_password_file(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { diff --git a/src/http/ngx_http_upstream.c b/src/http/ngx_http_upstream.c index e6382ef79..77dc032f2 100644 --- a/src/http/ngx_http_upstream.c +++ b/src/http/ngx_http_upstream.c @@ -2018,7 +2018,8 @@ ngx_http_upstream_ssl_certificate(ngx_http_request_t *r, ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0, "http upstream ssl key: \"%s\"", key.data); - if (ngx_ssl_connection_certificate(c, r->pool, &cert, &key, NULL, + if (ngx_ssl_connection_certificate(c, r->pool, &cert, &key, + u->conf->ssl_certificate_cache, u->conf->ssl_passwords) != NGX_OK) { diff --git a/src/http/ngx_http_upstream.h b/src/http/ngx_http_upstream.h index 57ee06f8d..069c0f7a4 100644 --- a/src/http/ngx_http_upstream.h +++ b/src/http/ngx_http_upstream.h @@ -245,6 +245,7 @@ typedef struct { ngx_http_complex_value_t *ssl_certificate; ngx_http_complex_value_t *ssl_certificate_key; + ngx_ssl_cache_t *ssl_certificate_cache; ngx_array_t *ssl_passwords; #endif diff --git a/src/stream/ngx_stream_proxy_module.c b/src/stream/ngx_stream_proxy_module.c index 21b579af3..7f8bfc4e0 100644 --- a/src/stream/ngx_stream_proxy_module.c +++ b/src/stream/ngx_stream_proxy_module.c @@ -49,6 +49,7 @@ typedef struct { ngx_str_t ssl_crl; ngx_stream_complex_value_t *ssl_certificate; ngx_stream_complex_value_t *ssl_certificate_key; + ngx_ssl_cache_t *ssl_certificate_cache; ngx_array_t *ssl_passwords; ngx_array_t *ssl_conf_commands; @@ -94,6 +95,8 @@ static char *ngx_stream_proxy_bind(ngx_conf_t *cf, ngx_command_t *cmd, #if (NGX_STREAM_SSL) static ngx_int_t ngx_stream_proxy_send_proxy_protocol(ngx_stream_session_t *s); +static char *ngx_stream_proxy_ssl_certificate_cache(ngx_conf_t *cf, + ngx_command_t *cmd, void *conf); static char *ngx_stream_proxy_ssl_password_file(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); static char *ngx_stream_proxy_ssl_conf_command_check(ngx_conf_t *cf, void *post, @@ -341,6 +344,13 @@ static ngx_command_t ngx_stream_proxy_commands[] = { offsetof(ngx_stream_proxy_srv_conf_t, ssl_certificate_key), NULL }, + { ngx_string("proxy_ssl_certificate_cache"), + NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE123, + ngx_stream_proxy_ssl_certificate_cache, + NGX_STREAM_SRV_CONF_OFFSET, + 0, + NULL }, + { ngx_string("proxy_ssl_password_file"), NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1, ngx_stream_proxy_ssl_password_file, @@ -1029,6 +1039,100 @@ ngx_stream_proxy_send_proxy_protocol(ngx_stream_session_t *s) } +static char * +ngx_stream_proxy_ssl_certificate_cache(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf) +{ + ngx_stream_proxy_srv_conf_t *pscf = conf; + + time_t inactive, valid; + ngx_str_t *value, s; + ngx_int_t max; + ngx_uint_t i; + + if (pscf->ssl_certificate_cache != NGX_CONF_UNSET_PTR) { + return "is duplicate"; + } + + value = cf->args->elts; + + max = 0; + inactive = 10; + valid = 60; + + for (i = 1; i < cf->args->nelts; i++) { + + if (ngx_strncmp(value[i].data, "max=", 4) == 0) { + + max = ngx_atoi(value[i].data + 4, value[i].len - 4); + if (max <= 0) { + goto failed; + } + + continue; + } + + if (ngx_strncmp(value[i].data, "inactive=", 9) == 0) { + + s.len = value[i].len - 9; + s.data = value[i].data + 9; + + inactive = ngx_parse_time(&s, 1); + if (inactive == (time_t) NGX_ERROR) { + goto failed; + } + + continue; + } + + if (ngx_strncmp(value[i].data, "valid=", 6) == 0) { + + s.len = value[i].len - 6; + s.data = value[i].data + 6; + + valid = ngx_parse_time(&s, 1); + if (valid == (time_t) NGX_ERROR) { + goto failed; + } + + continue; + } + + if (ngx_strcmp(value[i].data, "off") == 0) { + + pscf->ssl_certificate_cache = NULL; + + continue; + } + + failed: + + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid parameter \"%V\"", &value[i]); + return NGX_CONF_ERROR; + } + + if (pscf->ssl_certificate_cache == NULL) { + return NGX_CONF_OK; + } + + if (max == 0) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "\"proxy_ssl_certificate_cache\" must have " + "the \"max\" parameter"); + return NGX_CONF_ERROR; + } + + pscf->ssl_certificate_cache = ngx_ssl_cache_init(cf->pool, max, valid, + inactive); + if (pscf->ssl_certificate_cache == NULL) { + return NGX_CONF_ERROR; + } + + return NGX_CONF_OK; +} + + static char * ngx_stream_proxy_ssl_password_file(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) @@ -1324,7 +1428,8 @@ ngx_stream_proxy_ssl_certificate(ngx_stream_session_t *s) ngx_log_debug1(NGX_LOG_DEBUG_STREAM, c->log, 0, "stream upstream ssl key: \"%s\"", key.data); - if (ngx_ssl_connection_certificate(c, c->pool, &cert, &key, NULL, + if (ngx_ssl_connection_certificate(c, c->pool, &cert, &key, + pscf->ssl_certificate_cache, pscf->ssl_passwords) != NGX_OK) { @@ -2120,6 +2225,7 @@ ngx_stream_proxy_create_srv_conf(ngx_conf_t *cf) conf->ssl_verify_depth = NGX_CONF_UNSET_UINT; conf->ssl_certificate = NGX_CONF_UNSET_PTR; conf->ssl_certificate_key = NGX_CONF_UNSET_PTR; + conf->ssl_certificate_cache = NGX_CONF_UNSET_PTR; conf->ssl_passwords = NGX_CONF_UNSET_PTR; conf->ssl_conf_commands = NGX_CONF_UNSET_PTR; #endif @@ -2206,6 +2312,9 @@ ngx_stream_proxy_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) ngx_conf_merge_ptr_value(conf->ssl_certificate_key, prev->ssl_certificate_key, NULL); + ngx_conf_merge_ptr_value(conf->ssl_certificate_cache, + prev->ssl_certificate_cache, NULL); + ngx_conf_merge_ptr_value(conf->ssl_passwords, prev->ssl_passwords, NULL); ngx_conf_merge_ptr_value(conf->ssl_conf_commands, From noreply at nginx.com Fri Jan 17 00:38:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 17 Jan 2025 00:38:02 +0000 (UTC) Subject: [nginx] SSL: avoid using mismatched certificate/key cached pairs. Message-ID: <20250117003802.663EB48F1B@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/5d5d9adccfeaff7d5926737ee5dfa43937fe5899 branches: master commit: 5d5d9adccfeaff7d5926737ee5dfa43937fe5899 user: Sergey Kandaurov date: Wed, 8 Jan 2025 17:50:33 +0400 description: SSL: avoid using mismatched certificate/key cached pairs. This can happen with certificates and certificate keys specified with variables due to partial cache update in various scenarios: - cache expiration with only one element of pair evicted - on-disk update with non-cacheable encrypted keys - non-atomic on-disk update The fix is to retry with fresh data on X509_R_KEY_VALUES_MISMATCH. --- src/event/ngx_event_openssl.c | 28 +++++++++++++++++++++++++--- src/event/ngx_event_openssl.h | 2 ++ src/event/ngx_event_openssl_cache.c | 9 +++++++-- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c index 8963c8124..0681ca3a2 100644 --- a/src/event/ngx_event_openssl.c +++ b/src/event/ngx_event_openssl.c @@ -567,10 +567,17 @@ ngx_ssl_connection_certificate(ngx_connection_t *c, ngx_pool_t *pool, { char *err; X509 *x509; + u_long n; EVP_PKEY *pkey; + ngx_uint_t mask; STACK_OF(X509) *chain; - chain = ngx_ssl_cache_connection_fetch(cache, pool, NGX_SSL_CACHE_CERT, + mask = 0; + +retry: + + chain = ngx_ssl_cache_connection_fetch(cache, pool, + NGX_SSL_CACHE_CERT | mask, &err, cert, NULL); if (chain == NULL) { if (err != NULL) { @@ -611,7 +618,8 @@ ngx_ssl_connection_certificate(ngx_connection_t *c, ngx_pool_t *pool, #endif - pkey = ngx_ssl_cache_connection_fetch(cache, pool, NGX_SSL_CACHE_PKEY, + pkey = ngx_ssl_cache_connection_fetch(cache, pool, + NGX_SSL_CACHE_PKEY | mask, &err, key, passwords); if (pkey == NULL) { if (err != NULL) { @@ -624,9 +632,23 @@ ngx_ssl_connection_certificate(ngx_connection_t *c, ngx_pool_t *pool, } if (SSL_use_PrivateKey(c->ssl->connection, pkey) == 0) { + EVP_PKEY_free(pkey); + + /* there can be mismatched pairs on uneven cache update */ + + n = ERR_peek_last_error(); + + if (ERR_GET_LIB(n) == ERR_LIB_X509 + && ERR_GET_REASON(n) == X509_R_KEY_VALUES_MISMATCH + && mask == 0) + { + ERR_clear_error(); + mask = NGX_SSL_CACHE_INVALIDATE; + goto retry; + } + ngx_ssl_error(NGX_LOG_ERR, c->log, 0, "SSL_use_PrivateKey(\"%s\") failed", key->data); - EVP_PKEY_free(pkey); return NGX_ERROR; } diff --git a/src/event/ngx_event_openssl.h b/src/event/ngx_event_openssl.h index 0713c5671..c9dc50c75 100644 --- a/src/event/ngx_event_openssl.h +++ b/src/event/ngx_event_openssl.h @@ -206,6 +206,8 @@ typedef struct { #define NGX_SSL_CACHE_CRL 2 #define NGX_SSL_CACHE_CA 3 +#define NGX_SSL_CACHE_INVALIDATE 0x80000000 + ngx_int_t ngx_ssl_init(ngx_log_t *log); ngx_int_t ngx_ssl_create(ngx_ssl_t *ssl, ngx_uint_t protocols, void *data); diff --git a/src/event/ngx_event_openssl_cache.c b/src/event/ngx_event_openssl_cache.c index eb03e16b2..d62b4c430 100644 --- a/src/event/ngx_event_openssl_cache.c +++ b/src/event/ngx_event_openssl_cache.c @@ -289,6 +289,7 @@ ngx_ssl_cache_connection_fetch(ngx_ssl_cache_t *cache, ngx_pool_t *pool, void *value; time_t now; uint32_t hash; + ngx_uint_t invalidate; ngx_file_info_t fi; ngx_ssl_cache_key_t id; ngx_ssl_cache_type_t *type; @@ -296,6 +297,9 @@ ngx_ssl_cache_connection_fetch(ngx_ssl_cache_t *cache, ngx_pool_t *pool, *err = NULL; + invalidate = index & NGX_SSL_CACHE_INVALIDATE; + index &= ~NGX_SSL_CACHE_INVALIDATE; + if (ngx_ssl_cache_init_key(pool, index, path, &id) != NGX_OK) { return NULL; } @@ -319,7 +323,7 @@ ngx_ssl_cache_connection_fetch(ngx_ssl_cache_t *cache, ngx_pool_t *pool, goto found; } - if (now - cn->created <= cache->valid) { + if (!invalidate && now - cn->created <= cache->valid) { goto found; } @@ -329,7 +333,8 @@ ngx_ssl_cache_connection_fetch(ngx_ssl_cache_t *cache, ngx_pool_t *pool, if (ngx_file_info(id.data, &fi) != NGX_FILE_ERROR) { - if (ngx_file_uniq(&fi) == cn->uniq + if (!invalidate + && ngx_file_uniq(&fi) == cn->uniq && ngx_file_mtime(&fi) == cn->mtime) { break; From noreply at nginx.com Thu Jan 23 00:06:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Thu, 23 Jan 2025 00:06:02 +0000 (UTC) Subject: [njs] Tests: added request body test when body is in a file. Message-ID: <20250123000602.331BF47699@pubserv1.nginx> details: https://github.com/nginx/njs/commit/1aabab0c26e83727fa23b66aff1f239c905017f3 branches: master commit: 1aabab0c26e83727fa23b66aff1f239c905017f3 user: Dmitry Volyntsev date: Fri, 17 Jan 2025 17:03:08 -0800 description: Tests: added request body test when body is in a file. --- nginx/t/js_request_body.t | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/nginx/t/js_request_body.t b/nginx/t/js_request_body.t index 350e7fb8..7a5005d4 100644 --- a/nginx/t/js_request_body.t +++ b/nginx/t/js_request_body.t @@ -51,12 +51,19 @@ http { client_body_in_file_only on; js_content test.body; } + + location /read_body_from_temp_file { + client_body_in_file_only clean; + js_content test.read_body_from_temp_file; + } } } EOF $t->write_file('test.js', <write_file('test.js', <try_run('no njs request body')->plan(3); +$t->try_run('no njs request body')->plan(4); ############################################################################### @@ -80,6 +92,8 @@ like(http_post('/in_file'), qr/request body is in a file/, 'request body in file'); like(http_post_big('/body'), qr/200.*^(1234567890){1024}$/ms, 'request body big'); +like(http_post_big('/read_body_from_temp_file'), + qr/200.*^(1234567890){1024}$/ms, 'request body big from temp file'); ############################################################################### From noreply at nginx.com Thu Jan 23 00:06:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Thu, 23 Jan 2025 00:06:02 +0000 (UTC) Subject: [njs] QuickJS: fixed Buffer.concat() with a single argument. Message-ID: <20250123000602.4AF4648F0C@pubserv1.nginx> details: https://github.com/nginx/njs/commit/13b37cbc9d6288b7dae79ddd73a3f152cc8ae72d branches: master commit: 13b37cbc9d6288b7dae79ddd73a3f152cc8ae72d user: Dmitry Volyntsev date: Thu, 16 Jan 2025 17:36:44 -0800 description: QuickJS: fixed Buffer.concat() with a single argument. --- src/qjs_buffer.c | 2 +- test/buffer.t.js | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/qjs_buffer.c b/src/qjs_buffer.c index 3652a07a..9840377f 100644 --- a/src/qjs_buffer.c +++ b/src/qjs_buffer.c @@ -158,7 +158,7 @@ static const JSCFunctionListEntry qjs_buffer_props[] = { JS_CFUNC_MAGIC_DEF("allocUnsafe", 3, qjs_bufferobj_alloc, 1), JS_CFUNC_DEF("byteLength", 2, qjs_buffer_byte_length), JS_CFUNC_DEF("compare", 6, qjs_buffer_compare), - JS_CFUNC_DEF("concat", 1, qjs_buffer_concat), + JS_CFUNC_DEF("concat", 2, qjs_buffer_concat), JS_CFUNC_DEF("from", 3, qjs_buffer_from), JS_CFUNC_DEF("isBuffer", 1, qjs_buffer_is_buffer), JS_CFUNC_DEF("isEncoding", 1, qjs_buffer_is_encoding), diff --git a/test/buffer.t.js b/test/buffer.t.js index 9becf487..8e3f4ca9 100644 --- a/test/buffer.t.js +++ b/test/buffer.t.js @@ -79,7 +79,14 @@ let concat_tsuite = { name: "Buffer.concat() tests", skip: () => (!has_buffer()), T: async (params) => { - let r = Buffer.concat(params.buffers, params.length); + let r; + + if (params.length) { + r = Buffer.concat(params.buffers, params.length); + + } else { + r = Buffer.concat(params.buffers); + } if (r.toString() !== params.expected) { throw Error(`unexpected output "${r.toString()}" != "${params.expected}"`); From noreply at nginx.com Thu Jan 23 00:06:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Thu, 23 Jan 2025 00:06:02 +0000 (UTC) Subject: [njs] QuickJS: accept ArrayBuffer as an arument for qjs_typed_array_data(). Message-ID: <20250123000602.3A6B2476A0@pubserv1.nginx> details: https://github.com/nginx/njs/commit/1a482317e65d52cdaa9f0e8bff0a8e624f359b9e branches: master commit: 1a482317e65d52cdaa9f0e8bff0a8e624f359b9e user: Dmitry Volyntsev date: Mon, 13 Jan 2025 22:00:10 -0800 description: QuickJS: accept ArrayBuffer as an arument for qjs_typed_array_data(). --- src/qjs.c | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/qjs.c b/src/qjs.c index f9615668..487a03fc 100644 --- a/src/qjs.c +++ b/src/qjs.c @@ -456,17 +456,25 @@ qjs_bytes_free(JSContext *ctx, qjs_bytes_t *bytes) JSValue qjs_typed_array_data(JSContext *ctx, JSValueConst value, njs_str_t *data) { - size_t byte_offset, byte_length; + size_t byte_offset, byte_length; + JSValue ab; + + /* TODO: DataView. */ - value = JS_GetTypedArrayBuffer(ctx, value, &byte_offset, &byte_length, + ab = JS_GetTypedArrayBuffer(ctx, value, &byte_offset, &byte_length, NULL); - if (JS_IsException(value)) { - return value; + if (JS_IsException(ab)) { + data->start = JS_GetArrayBuffer(ctx, &data->length, value); + if (data->start == NULL) { + return JS_EXCEPTION; + } + + return JS_UNDEFINED; } - data->start = JS_GetArrayBuffer(ctx, &data->length, value); + data->start = JS_GetArrayBuffer(ctx, &data->length, ab); - JS_FreeValue(ctx, value); + JS_FreeValue(ctx, ab); if (data->start == NULL) { return JS_EXCEPTION; From noreply at nginx.com Thu Jan 23 00:06:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Thu, 23 Jan 2025 00:06:02 +0000 (UTC) Subject: [njs] Tests: moving request body tests to js_request_body.t. Message-ID: <20250123000602.362AD4769E@pubserv1.nginx> details: https://github.com/nginx/njs/commit/e92e2af9768ef327a5aacb4aa97746213dcdad52 branches: master commit: e92e2af9768ef327a5aacb4aa97746213dcdad52 user: Dmitry Volyntsev date: Fri, 17 Jan 2025 17:19:00 -0800 description: Tests: moving request body tests to js_request_body.t. --- nginx/t/js.t | 62 +++-------------------------------------------- nginx/t/js_request_body.t | 16 ++++++++++-- 2 files changed, 18 insertions(+), 60 deletions(-) diff --git a/nginx/t/js.t b/nginx/t/js.t index b7dfe3e6..a8bdab96 100644 --- a/nginx/t/js.t +++ b/nginx/t/js.t @@ -83,15 +83,6 @@ http { return 200 $test_global; } - location /body { - js_content test.request_body; - } - - location /in_file { - client_body_in_file_only on; - js_content test.request_body; - } - location /status { js_content test.status; } @@ -100,14 +91,6 @@ http { js_content test.buffer_variable; } - location /request_body { - js_content test.request_body; - } - - location /request_body_cache { - js_content test.request_body_cache; - } - location /send { js_content test.send; } @@ -194,22 +177,6 @@ $t->write_file('test.js', <write_file('test.js', <try_run('no njs available')->plan(29); +$t->try_run('no njs available')->plan(25); ############################################################################### @@ -289,12 +255,6 @@ like(http_get('/uri'), qr/uri=\/uri/, 'r.uri'); like(http_get('/status'), qr/204 No Content/, 'r.status'); -like(http_post('/body'), qr/REQ-BODY/, 'request body'); -like(http_post('/in_file'), qr/request body is in a file/, - 'request body in file'); -like(http_post_big('/body'), qr/200.*^(1234567890){1024}$/ms, - 'request body big'); - like(http_get('/send?foo=12345&n=11&foo-2=bar&ndd=&foo-3=z'), qr/n=foo, v=12 n=foo-2, v=ba n=foo-3, v=z/, 'r.send'); @@ -323,8 +283,6 @@ like(http_post('/type?path=requestText'), qr/200 OK.*type: string$/s, 'requestText type'); like(http_post('/type?path=requestBuffer'), qr/200 OK.*type: buffer$/s, 'requestBuffer type'); -like(http_post('/request_body_cache'), - qr/requestText:string requestBuffer:buffer$/s, 'request body cache'); like(http_get('/var'), qr/variable=127.0.0.1/, 'r.variables'); like(http_get('/global'), qr/global=njs/, 'global code'); @@ -414,16 +372,4 @@ sub http_post { return http($p, %extra); } -sub http_post_big { - my ($url, %extra) = @_; - - my $p = "POST $url HTTP/1.0" . CRLF . - "Host: localhost" . CRLF . - "Content-Length: 10240" . CRLF . - CRLF . - ("1234567890" x 1024); - - return http($p, %extra); -} - ############################################################################### diff --git a/nginx/t/js_request_body.t b/nginx/t/js_request_body.t index 7a5005d4..360e4565 100644 --- a/nginx/t/js_request_body.t +++ b/nginx/t/js_request_body.t @@ -56,6 +56,10 @@ http { client_body_in_file_only clean; js_content test.read_body_from_temp_file; } + + location /request_body_cache { + js_content test.request_body_cache; + } } } @@ -79,11 +83,17 @@ $t->write_file('test.js', <try_run('no njs request body')->plan(4); +$t->try_run('no njs request body')->plan(5); ############################################################################### @@ -94,6 +104,8 @@ like(http_post_big('/body'), qr/200.*^(1234567890){1024}$/ms, 'request body big'); like(http_post_big('/read_body_from_temp_file'), qr/200.*^(1234567890){1024}$/ms, 'request body big from temp file'); +like(http_post('/request_body_cache'), + qr/requestText:string requestBuffer:buffer$/s, 'request body cache'); ############################################################################### From noreply at nginx.com Thu Jan 23 00:06:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Thu, 23 Jan 2025 00:06:02 +0000 (UTC) Subject: [njs] QuickJS: added TextDecoder and TextEncoder. Message-ID: <20250123000602.45DFA47D41@pubserv1.nginx> details: https://github.com/nginx/njs/commit/447d66d41d41504db976e900d94e75a90d388265 branches: master commit: 447d66d41d41504db976e900d94e75a90d388265 user: Dmitry Volyntsev date: Fri, 10 Jan 2025 23:20:36 -0800 description: QuickJS: added TextDecoder and TextEncoder. --- src/qjs.c | 568 +++++++++++++++++++++++++++++++++++++++++++++++ src/qjs.h | 11 +- src/qjs_buffer.c | 4 +- src/test/njs_unit_test.c | 122 ---------- test/text_decoder.t.js | 151 +++++++++++++ test/text_encoder.t.js | 87 ++++++++ 6 files changed, 814 insertions(+), 129 deletions(-) diff --git a/src/qjs.c b/src/qjs.c index 487a03fc..e21e6568 100644 --- a/src/qjs.c +++ b/src/qjs.c @@ -19,6 +19,26 @@ typedef struct { } qjs_signal_entry_t; +typedef enum { + QJS_ENCODING_UTF8, +} qjs_encoding_t; + + +typedef struct { + qjs_encoding_t encoding; + int fatal; + int ignore_bom; + + njs_unicode_decode_t ctx; +} qjs_text_decoder_t; + + +typedef struct { + njs_str_t name; + qjs_encoding_t encoding; +} qjs_encoding_label_t; + + extern char **environ; @@ -32,6 +52,26 @@ static JSValue qjs_process_kill(JSContext *ctx, JSValueConst this_val, static JSValue qjs_process_pid(JSContext *ctx, JSValueConst this_val); static JSValue qjs_process_ppid(JSContext *ctx, JSValueConst this_val); +static int qjs_add_intrinsic_text_decoder(JSContext *cx, JSValueConst global); +static JSValue qjs_text_decoder_to_string_tag(JSContext *ctx, + JSValueConst this_val); +static JSValue qjs_text_decoder_decode(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv); +static JSValue qjs_text_decoder_encoding(JSContext *ctx, JSValueConst this_val); +static JSValue qjs_text_decoder_fatal(JSContext *ctx, JSValueConst this_val); +static JSValue qjs_text_decoder_ignore_bom(JSContext *ctx, + JSValueConst this_val); +static void qjs_text_decoder_finalizer(JSRuntime *rt, JSValue val); + +static int qjs_add_intrinsic_text_encoder(JSContext *cx, JSValueConst global); +static JSValue qjs_text_encoder_to_string_tag(JSContext *ctx, + JSValueConst this_val); +static JSValue qjs_text_encoder_encode(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv); +static JSValue qjs_text_encoder_encode_into(JSContext *ctx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue qjs_text_encoder_encoding(JSContext *ctx, JSValueConst this_val); + /* P1990 signals from `man 7 signal` are supported */ static qjs_signal_entry_t qjs_signals_table[] = { @@ -58,10 +98,35 @@ static qjs_signal_entry_t qjs_signals_table[] = { }; +static qjs_encoding_label_t qjs_encoding_labels[] = +{ + { njs_str("utf-8"), QJS_ENCODING_UTF8 }, + { njs_str("utf8") , QJS_ENCODING_UTF8 }, + { njs_null_str, 0 } +}; + + static const JSCFunctionListEntry qjs_global_proto[] = { JS_CGETSET_DEF("njs", qjs_njs_getter, NULL), }; +static const JSCFunctionListEntry qjs_text_decoder_proto[] = { + JS_CGETSET_DEF("[Symbol.toStringTag]", qjs_text_decoder_to_string_tag, + NULL), + JS_CFUNC_DEF("decode", 1, qjs_text_decoder_decode), + JS_CGETSET_DEF("encoding", qjs_text_decoder_encoding, NULL), + JS_CGETSET_DEF("fatal", qjs_text_decoder_fatal, NULL), + JS_CGETSET_DEF("ignoreBOM", qjs_text_decoder_ignore_bom, NULL), +}; + +static const JSCFunctionListEntry qjs_text_encoder_proto[] = { + JS_CGETSET_DEF("[Symbol.toStringTag]", qjs_text_encoder_to_string_tag, + NULL), + JS_CFUNC_DEF("encode", 1, qjs_text_encoder_encode), + JS_CFUNC_DEF("encodeInto", 1, qjs_text_encoder_encode_into), + JS_CGETSET_DEF("encoding", qjs_text_encoder_encoding, NULL), +}; + static const JSCFunctionListEntry qjs_njs_proto[] = { JS_CGETSET_DEF("[Symbol.toStringTag]", qjs_njs_to_string_tag, NULL), JS_PROP_STRING_DEF("version", NJS_VERSION, JS_PROP_C_W_E), @@ -80,6 +145,12 @@ static const JSCFunctionListEntry qjs_process_proto[] = { }; +static JSClassDef qjs_text_decoder_class = { + "TextDecoder", + .finalizer = qjs_text_decoder_finalizer, +}; + + JSContext * qjs_new_context(JSRuntime *rt, qjs_module_t **addons) { @@ -121,6 +192,14 @@ qjs_new_context(JSRuntime *rt, qjs_module_t **addons) global_obj = JS_GetGlobalObject(ctx); + if (qjs_add_intrinsic_text_decoder(ctx, global_obj) < 0) { + return NULL; + } + + if (qjs_add_intrinsic_text_encoder(ctx, global_obj) < 0) { + return NULL; + } + JS_SetPropertyFunctionList(ctx, global_obj, qjs_global_proto, njs_nitems(qjs_global_proto)); @@ -393,6 +472,495 @@ qjs_process_object(JSContext *ctx, int argc, const char **argv) } +static int +qjs_text_decoder_encoding_arg(JSContext *cx, int argc, JSValueConst *argv, + qjs_text_decoder_t *td) +{ + njs_str_t str; + qjs_encoding_label_t *label; + + if (argc < 1) { + td->encoding = QJS_ENCODING_UTF8; + return 0; + } + + str.start = (u_char *) JS_ToCStringLen(cx, &str.length, argv[0]); + if (str.start == NULL) { + JS_ThrowOutOfMemory(cx); + return -1; + } + + for (label = &qjs_encoding_labels[0]; label->name.length != 0; label++) { + if (njs_strstr_eq(&str, &label->name)) { + td->encoding = label->encoding; + JS_FreeCString(cx, (char *) str.start); + return 0; + } + } + + JS_ThrowTypeError(cx, "The \"%.*s\" encoding is not supported", + (int) str.length, str.start); + JS_FreeCString(cx, (char *) str.start); + + return -1; +} + + +static int +qjs_text_decoder_options(JSContext *cx, int argc, JSValueConst *argv, + qjs_text_decoder_t *td) +{ + JSValue val; + + if (argc < 2) { + td->fatal = 0; + td->ignore_bom = 0; + + return 0; + } + + val = JS_GetPropertyStr(cx, argv[1], "fatal"); + if (JS_IsException(val)) { + return -1; + } + + td->fatal = JS_ToBool(cx, val); + JS_FreeValue(cx, val); + + val = JS_GetPropertyStr(cx, argv[1], "ignoreBOM"); + if (JS_IsException(val)) { + return -1; + } + + td->ignore_bom = JS_ToBool(cx, val); + JS_FreeValue(cx, val); + + return 0; +} + + +static JSValue +qjs_text_decoder_ctor(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + JSValue obj; + qjs_text_decoder_t *td; + + obj = JS_NewObjectClass(cx, QJS_CORE_CLASS_ID_TEXT_DECODER); + if (JS_IsException(obj)) { + return JS_EXCEPTION; + } + + td = js_mallocz(cx, sizeof(qjs_text_decoder_t)); + if (td == NULL) { + JS_ThrowOutOfMemory(cx); + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } + + if (qjs_text_decoder_encoding_arg(cx, argc, argv, td) < 0) { + js_free(cx, td); + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } + + if (qjs_text_decoder_options(cx, argc, argv, td) < 0) { + js_free(cx, td); + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } + + njs_utf8_decode_init(&td->ctx); + + JS_SetOpaque(obj, td); + + return obj; +} + + +static int +qjs_add_intrinsic_text_decoder(JSContext *cx, JSValueConst global) +{ + JSValue ctor, proto; + + if (JS_NewClass(JS_GetRuntime(cx), QJS_CORE_CLASS_ID_TEXT_DECODER, + &qjs_text_decoder_class) < 0) + { + return -1; + } + + proto = JS_NewObject(cx); + if (JS_IsException(proto)) { + return -1; + } + + JS_SetPropertyFunctionList(cx, proto, qjs_text_decoder_proto, + njs_nitems(qjs_text_decoder_proto)); + + JS_SetClassProto(cx, QJS_CORE_CLASS_ID_TEXT_DECODER, proto); + + ctor = JS_NewCFunction2(cx, qjs_text_decoder_ctor, "TextDecoder", 2, + JS_CFUNC_constructor, 0); + if (JS_IsException(ctor)) { + return -1; + } + + JS_SetConstructor(cx, ctor, proto); + + return JS_SetPropertyStr(cx, global, "TextDecoder", ctor); +} + + +static JSValue +qjs_text_decoder_to_string_tag(JSContext *ctx, JSValueConst this_val) +{ + return JS_NewString(ctx, "TextDecoder"); +} + + +static JSValue +qjs_text_decoder_decode(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + int stream; + size_t size; + u_char *dst; + JSValue ret; + ssize_t length; + njs_str_t data; + const u_char *end; + qjs_text_decoder_t *td; + njs_unicode_decode_t ctx; + + td = JS_GetOpaque(this_val, QJS_CORE_CLASS_ID_TEXT_DECODER); + if (td == NULL) { + return JS_ThrowInternalError(cx, "'this' is not a TextDecoder"); + } + + ret = qjs_typed_array_data(cx, argv[0], &data); + if (JS_IsException(ret)) { + return ret; + } + + stream = 0; + + if (argc > 1) { + ret = JS_GetPropertyStr(cx, argv[1], "stream"); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + + stream = JS_ToBool(cx, ret); + JS_FreeValue(cx, ret); + } + + ctx = td->ctx; + end = data.start + data.length; + + if (data.start != NULL && !td->ignore_bom) { + data.start += njs_utf8_bom(data.start, end); + } + + length = njs_utf8_stream_length(&ctx, data.start, end - data.start, !stream, + td->fatal, &size); + + if (length == -1) { + return JS_ThrowTypeError(cx, "The encoded data was not valid"); + } + + dst = js_malloc(cx, size + 1); + if (dst == NULL) { + JS_ThrowOutOfMemory(cx); + return JS_EXCEPTION; + } + + (void) njs_utf8_stream_encode(&td->ctx, data.start, end, dst, !stream, 0); + + ret = JS_NewStringLen(cx, (const char *) dst, size); + js_free(cx, dst); + + if (!stream) { + njs_utf8_decode_init(&td->ctx); + } + + return ret; +} + + +static JSValue +qjs_text_decoder_encoding(JSContext *ctx, JSValueConst this_val) +{ + qjs_text_decoder_t *td; + + td = JS_GetOpaque(this_val, QJS_CORE_CLASS_ID_TEXT_DECODER); + if (td == NULL) { + return JS_ThrowInternalError(ctx, "'this' is not a TextDecoder"); + } + + switch (td->encoding) { + case QJS_ENCODING_UTF8: + return JS_NewString(ctx, "utf-8"); + } + + return JS_UNDEFINED; +} + + +static JSValue +qjs_text_decoder_fatal(JSContext *ctx, JSValueConst this_val) +{ + qjs_text_decoder_t *td; + + td = JS_GetOpaque(this_val, QJS_CORE_CLASS_ID_TEXT_DECODER); + if (td == NULL) { + return JS_ThrowInternalError(ctx, "'this' is not a TextDecoder"); + } + + return JS_NewBool(ctx, td->fatal); +} + + +static JSValue +qjs_text_decoder_ignore_bom(JSContext *ctx, JSValueConst this_val) +{ + qjs_text_decoder_t *td; + + td = JS_GetOpaque(this_val, QJS_CORE_CLASS_ID_TEXT_DECODER); + if (td == NULL) { + return JS_ThrowInternalError(ctx, "'this' is not a TextDecoder"); + } + + return JS_NewBool(ctx, td->ignore_bom); +} + + +static void +qjs_text_decoder_finalizer(JSRuntime *rt, JSValue val) +{ + qjs_text_decoder_t *td; + + td = JS_GetOpaque(val, QJS_CORE_CLASS_ID_TEXT_DECODER); + if (td != NULL) { + js_free_rt(rt, td); + } +} + + +static JSValue +qjs_text_encoder_ctor(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + JSValue obj; + + obj = JS_NewObjectClass(cx, QJS_CORE_CLASS_ID_TEXT_ENCODER); + if (JS_IsException(obj)) { + return JS_EXCEPTION; + } + + JS_SetOpaque(obj, (void *) 1); + + return obj; +} + + +static int +qjs_add_intrinsic_text_encoder(JSContext *cx, JSValueConst global) +{ + JSValue ctor, proto; + + proto = JS_NewObject(cx); + if (JS_IsException(proto)) { + return -1; + } + + JS_SetPropertyFunctionList(cx, proto, qjs_text_encoder_proto, + njs_nitems(qjs_text_encoder_proto)); + + JS_SetClassProto(cx, QJS_CORE_CLASS_ID_TEXT_ENCODER, proto); + + ctor = JS_NewCFunction2(cx, qjs_text_encoder_ctor, "TextEncoder", 0, + JS_CFUNC_constructor, 0); + if (JS_IsException(ctor)) { + return -1; + } + + JS_SetConstructor(cx, ctor, proto); + + return JS_SetPropertyStr(cx, global, "TextEncoder", ctor); +} + + +static JSValue +qjs_text_encoder_to_string_tag(JSContext *ctx, JSValueConst this_val) +{ + return JS_NewString(ctx, "TextEncoder"); +} + + +static JSValue +qjs_text_encoder_encoding(JSContext *ctx, JSValueConst this_val) +{ + return JS_NewString(ctx, "utf-8"); +} + + +static JSValue +qjs_text_encoder_encode(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + void *te; + JSValue len, ta, ret; + njs_str_t utf8, dst; + + te = JS_GetOpaque(this_val, QJS_CORE_CLASS_ID_TEXT_ENCODER); + if (te == NULL) { + return JS_ThrowInternalError(cx, "'this' is not a TextEncoder"); + } + + if (!JS_IsString(argv[0])) { + return JS_ThrowTypeError(cx, "The input argument must be a string"); + } + + utf8.start = (u_char *) JS_ToCStringLen(cx, &utf8.length, argv[0]); + if (utf8.start == NULL) { + return JS_EXCEPTION; + } + + len = JS_NewInt64(cx, utf8.length); + + ta = qjs_new_uint8_array(cx, 1, &len); + if (JS_IsException(ta)) { + JS_FreeCString(cx, (char *) utf8.start); + return ta; + } + + ret = qjs_typed_array_data(cx, ta, &dst); + if (JS_IsException(ret)) { + JS_FreeCString(cx, (char *) utf8.start); + return ret; + } + + memcpy(dst.start, utf8.start, utf8.length); + JS_FreeCString(cx, (char *) utf8.start); + + return ta; +} + + +static int +qjs_is_uint8_array(JSContext *cx, JSValueConst value) +{ + int ret; + JSValue ctor, global; + + global = JS_GetGlobalObject(cx); + + ctor = JS_GetPropertyStr(cx, global, "Uint8Array"); + if (JS_IsException(ctor)) { + JS_FreeValue(cx, global); + return -1; + } + + ret = JS_IsInstanceOf(cx, value, ctor); + JS_FreeValue(cx, ctor); + JS_FreeValue(cx, global); + + return ret; +} + + +static JSValue +qjs_text_encoder_encode_into(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + int read, written; + void *te; + size_t size; + u_char *to, *to_end; + JSValue ret; + uint32_t cp; + njs_str_t utf8, dst; + const u_char *start, *end; + njs_unicode_decode_t ctx; + + te = JS_GetOpaque(this_val, QJS_CORE_CLASS_ID_TEXT_ENCODER); + if (te == NULL) { + return JS_ThrowInternalError(cx, "'this' is not a TextEncoder"); + } + + if (!JS_IsString(argv[0])) { + return JS_ThrowTypeError(cx, "The input argument must be a string"); + } + + ret = qjs_typed_array_data(cx, argv[1], &dst); + if (JS_IsException(ret)) { + return ret; + } + + if (!qjs_is_uint8_array(cx, argv[1])) { + return JS_ThrowTypeError(cx, "The output argument must be a" + " Uint8Array"); + } + + utf8.start = (u_char *) JS_ToCStringLen(cx, &utf8.length, argv[0]); + if (utf8.start == NULL) { + return JS_EXCEPTION; + } + + read = 0; + written = 0; + + start = utf8.start; + end = start + utf8.length; + + to = dst.start; + to_end = to + dst.length; + + njs_utf8_decode_init(&ctx); + + while (start < end) { + cp = njs_utf8_decode(&ctx, &start, end); + + if (cp > NJS_UNICODE_MAX_CODEPOINT) { + cp = NJS_UNICODE_REPLACEMENT; + } + + size = njs_utf8_size(cp); + + if (to + size > to_end) { + break; + } + + read += (cp > 0xFFFF) ? 2 : 1; + written += size; + + to = njs_utf8_encode(to, cp); + } + + JS_FreeCString(cx, (char *) utf8.start); + + ret = JS_NewObject(cx); + if (JS_IsException(ret)) { + return ret; + } + + if (JS_DefinePropertyValueStr(cx, ret, "read", JS_NewInt32(cx, read), + JS_PROP_C_W_E) < 0) + { + JS_FreeValue(cx, ret); + return JS_EXCEPTION; + } + + if (JS_DefinePropertyValueStr(cx, ret, "written", JS_NewInt32(cx, written), + JS_PROP_C_W_E) < 0) + { + JS_FreeValue(cx, ret); + return JS_EXCEPTION; + } + + return ret; +} + int qjs_to_bytes(JSContext *ctx, qjs_bytes_t *bytes, JSValueConst value) { diff --git a/src/qjs.h b/src/qjs.h index dec6419d..76bf5c3d 100644 --- a/src/qjs.h +++ b/src/qjs.h @@ -36,10 +36,12 @@ #define QJS_CORE_CLASS_ID_OFFSET 64 #define QJS_CORE_CLASS_ID_BUFFER (QJS_CORE_CLASS_ID_OFFSET) #define QJS_CORE_CLASS_ID_UINT8_ARRAY_CTOR (QJS_CORE_CLASS_ID_OFFSET + 1) -#define QJS_CORE_CLASS_ID_FS_STATS (QJS_CORE_CLASS_ID_OFFSET + 2) -#define QJS_CORE_CLASS_ID_FS_DIRENT (QJS_CORE_CLASS_ID_OFFSET + 3) -#define QJS_CORE_CLASS_ID_FS_FILEHANDLE (QJS_CORE_CLASS_ID_OFFSET + 4) -#define QJS_CORE_CLASS_ID_LAST (QJS_CORE_CLASS_ID_OFFSET + 5) +#define QJS_CORE_CLASS_ID_TEXT_DECODER (QJS_CORE_CLASS_ID_OFFSET + 2) +#define QJS_CORE_CLASS_ID_TEXT_ENCODER (QJS_CORE_CLASS_ID_OFFSET + 3) +#define QJS_CORE_CLASS_ID_FS_STATS (QJS_CORE_CLASS_ID_OFFSET + 4) +#define QJS_CORE_CLASS_ID_FS_DIRENT (QJS_CORE_CLASS_ID_OFFSET + 5) +#define QJS_CORE_CLASS_ID_FS_FILEHANDLE (QJS_CORE_CLASS_ID_OFFSET + 6) +#define QJS_CORE_CLASS_ID_LAST (QJS_CORE_CLASS_ID_OFFSET + 7) typedef JSModuleDef *(*qjs_addon_init_pt)(JSContext *ctx, const char *name); @@ -53,6 +55,7 @@ typedef struct { JSContext *qjs_new_context(JSRuntime *rt, qjs_module_t **addons); +JSValue qjs_new_uint8_array(JSContext *ctx, int argc, JSValueConst *argv); JSValue qjs_buffer_alloc(JSContext *ctx, size_t size); JSValue qjs_buffer_create(JSContext *ctx, u_char *start, size_t size); JSValue qjs_buffer_chb_alloc(JSContext *ctx, njs_chb_t *chain); diff --git a/src/qjs_buffer.c b/src/qjs_buffer.c index 9f451e26..3652a07a 100644 --- a/src/qjs_buffer.c +++ b/src/qjs_buffer.c @@ -90,8 +90,6 @@ static int qjs_hex_encode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst); static size_t qjs_hex_encode_length(JSContext *ctx, const njs_str_t *src); static int qjs_hex_decode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst); static size_t qjs_hex_decode_length(JSContext *ctx, const njs_str_t *src); -static JSValue qjs_new_uint8_array(JSContext *ctx, int argc, - JSValueConst *argv); static JSModuleDef *qjs_buffer_init(JSContext *ctx, const char *name); @@ -2465,7 +2463,7 @@ qjs_buffer_chb_alloc(JSContext *ctx, njs_chb_t *chain) } -static JSValue +JSValue qjs_new_uint8_array(JSContext *ctx, int argc, JSValueConst *argv) { JSValue ret; diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c index d6ae4ed8..2e9a5379 100644 --- a/src/test/njs_unit_test.c +++ b/src/test/njs_unit_test.c @@ -19339,128 +19339,6 @@ static njs_unit_test_t njs_test[] = { njs_str("var t = \"123\"; t = parseInt(t); t"), njs_str("123") }, - /* TextEncoder. */ - - { njs_str("var en = new TextEncoder(); typeof en.encode()"), - njs_str("object") }, - - { njs_str("var en = new TextEncoder(); en.encode()"), - njs_str("") }, - - { njs_str("var en = new TextEncoder(); var res = en.encode('α'); res"), - njs_str("206,177") }, - - { njs_str("var en = new TextEncoder(); var res = en.encode('α1α'); res[2]"), - njs_str("49") }, - - { njs_str("var en = new TextEncoder(); en.encoding"), - njs_str("utf-8") }, - - { njs_str("TextEncoder.prototype.encode.apply({}, [])"), - njs_str("TypeError: \"this\" is not a TextEncoder") }, - - { njs_str("var en = new TextEncoder();" - "var utf8 = new Uint8Array(5);" - "var res = en.encodeInto('ααααα', utf8); njs.dump(res)"), - njs_str("{read:2,written:4}") }, - - { njs_str("var en = new TextEncoder();" - "var utf8 = new Uint8Array(10);" - "var res = en.encodeInto('ααααα', utf8); njs.dump(res)"), - njs_str("{read:5,written:10}") }, - - { njs_str("var en = new TextEncoder();" - "var utf8 = new Uint8Array(10);" - "en.encodeInto('ααααα', utf8.subarray(2)); utf8[0]"), - njs_str("0") }, - - { njs_str("TextEncoder.prototype.encodeInto.apply({}, [])"), - njs_str("TypeError: \"this\" is not a TextEncoder") }, - - { njs_str("(new TextEncoder()).encodeInto('', 0.12) "), - njs_str("TypeError: The \"destination\" argument must be an instance of Uint8Array") }, - - /* TextDecoder. */ - - { njs_str("var de = new TextDecoder();" - "var u8arr = new Uint8Array([240, 160, 174, 183]);" - "var u16arr = new Uint16Array(u8arr.buffer);" - "var u32arr = new Uint32Array(u8arr.buffer);" - "[u8arr, u16arr, u32arr].map(v=>de.decode(v)).join(',')"), - njs_str("𠮷,𠮷,𠮷") }, - - { njs_str("var de = new TextDecoder();" - "[new Uint8Array([240, 160]), " - " new Uint8Array([174]), " - " new Uint8Array([183])].map(v=>de.decode(v, {stream: 1}))[2]"), - njs_str("𠮷") }, - - { njs_str("var de = new TextDecoder();" - "de.decode(new Uint8Array([240, 160]), {stream: 1});" - "de.decode(new Uint8Array([174]), {stream: 1});" - "de.decode(new Uint8Array([183]))"), - njs_str("𠮷") }, - - { njs_str("var de = new TextDecoder();" - "de.decode(new Uint8Array([240, 160]), {stream: 1});" - "de.decode()"), - njs_str("�") }, - - { njs_str("var de = new TextDecoder('utf-8', {fatal: true});" - "de.decode(new Uint8Array([240, 160]))"), - njs_str("TypeError: The encoded data was not valid") }, - - { njs_str("var de = new TextDecoder('utf-8', {fatal: false});" - "de.decode(new Uint8Array([240, 160]))"), - njs_str("�") }, - - { njs_str("var en = new TextEncoder();" - "var de = new TextDecoder('utf-8', {ignoreBOM: true});" - "en.encode(de.decode(new Uint8Array([239, 187, 191, 50])))"), - njs_str("239,187,191,50") }, - - { njs_str("var en = new TextEncoder();" - "var de = new TextDecoder('utf-8', {ignoreBOM: false});" - "en.encode(de.decode(new Uint8Array([239, 187, 191, 50])))"), - njs_str("50") }, - - { njs_str("var en = new TextEncoder(); var de = new TextDecoder();" - "en.encode(de.decode(new Uint8Array([239, 187, 191, 50])))"), - njs_str("50") }, - - { njs_str("var de = new TextDecoder(); de.decode('')"), - njs_str("TypeError: The \"input\" argument must be an instance of TypedArray") }, - - { njs_str("var de = new TextDecoder({})"), - njs_str("RangeError: The \"[object Object]\" encoding is not supported") }, - - { njs_str("var de = new TextDecoder('foo')"), - njs_str("RangeError: The \"foo\" encoding is not supported") }, - - { njs_str("var de = new TextDecoder(); de.encoding"), - njs_str("utf-8") }, - - { njs_str("var de = new TextDecoder(); de.fatal"), - njs_str("false") }, - - { njs_str("var de = new TextDecoder(); de.ignoreBOM"), - njs_str("false") }, - - { njs_str("TextDecoder.prototype.decode.apply({}, new Uint8Array([1]))"), - njs_str("TypeError: \"this\" is not a TextDecoder") }, - - { njs_str("var de = new TextDecoder();" - "var buf = new Uint32Array([1,2,3]).buffer;" - "var en = new TextEncoder();" - "njs.dump(new Uint32Array(en.encode(de.decode(buf)).buffer))"), - njs_str("Uint32Array [1,2,3]") }, - - { njs_str("var de = new TextDecoder();" - "var buf = new Uint32Array([1,2,3]).subarray(1,2);" - "var en = new TextEncoder();" - "njs.dump(new Uint32Array(en.encode(de.decode(buf)).buffer))"), - njs_str("Uint32Array [2]") }, - /* let */ { njs_str("let x"), diff --git a/test/text_decoder.t.js b/test/text_decoder.t.js new file mode 100644 index 00000000..2eb879c0 --- /dev/null +++ b/test/text_decoder.t.js @@ -0,0 +1,151 @@ +/*--- +includes: [runTsuite.js, compareArray.js] +flags: [async] +---*/ + +function p(args, default_opts) { + let params = merge({}, default_opts); + params = merge(params, args); + + return params; +} + +let stream_tsuite = { + name: "TextDecoder() stream tests", + T: async (params) => { + let td = new TextDecoder('utf-8'); + + if (td.encoding !== 'utf-8') { + throw Error(`unexpected encoding "${td.encoding}" != "utf-8"`); + } + + if (td.fatal !== false) { + throw Error(`unexpected fatal "${td.fatal}" != "false"`); + } + + if (td.ignoreBOM !== false) { + throw Error(`unexpected ignoreBOM "${td.ignoreBOM}" != "false"`); + } + + let chunks = []; + for (var i = 0; i < params.chunks.length; i++) { + let r = td.decode(params.chunks[i], { stream: (i != params.chunks.length - 1) }); + chunks.push(r); + } + + if (!compareArray(chunks, params.expected)) { + throw Error(`unexpected output "${chunks.join('|')}" != "${params.expected.join('|')}"`); + } + + return 'SUCCESS'; + }, + + prepare_args: p, + opts: {}, + + tests: [ + { chunks: [new Uint8Array([0xF0, 0x9F, 0x8C, 0x9F])], + expected: ['🌟'] }, + // BOM is ignored + { chunks: [new Uint8Array([0xEF, 0xBB, 0xBF, 0xF0, 0x9F, 0x8C, 0x9F])], + expected: ['🌟'] }, + { chunks: [(new Uint8Array([0xF0, 0x9F, 0x8C, 0x9F])).buffer], + expected: ['🌟'] }, + { chunks: [new Uint32Array((new Uint8Array([0xF0, 0x9F, 0x8C, 0x9F])).buffer)], + expected: ['🌟'] }, + { chunks: [new Uint8Array((new Uint8Array([0x00, 0xF0, 0x9F, 0x8C, 0x9F, 0x00])).buffer, 1, 4)], + expected: ['🌟'] }, + { chunks: [new Uint8Array([0xF0, 0x9F]), new Uint8Array([0x8C, 0x9F])], + expected: ['', '🌟'] }, + { chunks: [new Uint8Array([0xF0, 0xA0]), new Uint8Array([0xAE]), new Uint8Array([0xB7])], + expected: ['', '', '𠮷'] }, + { chunks: [new Uint8Array([0xF0, 0xA0]), new Uint8Array([])], + expected: ['', '�'] }, + { chunks: [''], + exception: 'TypeError: TypeError: not a TypedArray' }, + ], +}; + +let fatal_tsuite = { + name: "TextDecoder() fatal tests", + T: async (params) => { + let td = new TextDecoder('utf8', {fatal: true, ignoreBOM: true}); + + if (td.encoding !== 'utf-8') { + throw Error(`unexpected encoding "${td.encoding}" != "utf-8"`); + } + + if (td.fatal !== true) { + throw Error(`unexpected fatal "${td.fatal}" != "true"`); + } + + if (td.ignoreBOM !== true) { + throw Error(`unexpected ignoreBOM "${td.ignoreBOM}" != "true"`); + } + + let chunks = []; + for (var i = 0; i < params.chunks.length; i++) { + let r = td.decode(params.chunks[i]); + chunks.push(r); + } + + if (!compareArray(chunks, params.expected)) { + throw Error(`unexpected output "${chunks.join('|')}" != "${params.expected.join('|')}"`); + } + + return 'SUCCESS'; + }, + + prepare_args: p, + opts: {}, + + tests: [ + { chunks: [new Uint8Array([0xF0, 0xA0, 0xAE, 0xB7])], + expected: ['𠮷'] }, + { chunks: [new Uint8Array([0xF0, 0xA0, 0xAE])], + exception: 'Error: The encoded data was not valid' }, + { chunks: [new Uint8Array([0xF0, 0xA0])], + exception: 'Error: The encoded data was not valid' }, + { chunks: [new Uint8Array([0xF0])], + exception: 'Error: The encoded data was not valid' }, + ], +}; + +let ignoreBOM_tsuite = { + name: "TextDecoder() ignoreBOM tests", + T: async (params) => { + let td = new TextDecoder('utf8', params.opts); + let te = new TextEncoder(); + + let res = te.encode(td.decode(params.value)); + + if (!compareArray(res, params.expected)) { + throw Error(`unexpected output "${res}" != "${params.expected}"`); + } + + return 'SUCCESS'; + }, + + prepare_args: p, + opts: {}, + + tests: [ + { value: new Uint8Array([239, 187, 191, 50]), + opts: {ignoreBOM: true}, + expected: [239, 187, 191, 50] }, + { value: new Uint8Array([239, 187, 191, 50]), + opts: {ignoreBOM: false}, + expected: [50] }, + { value: new Uint8Array([239, 187, 191, 50]), + opts: {}, + expected: [50] }, + ], +}; + + +run([ + stream_tsuite, + fatal_tsuite, + ignoreBOM_tsuite, +]) +.then($DONE, $DONE); diff --git a/test/text_encoder.t.js b/test/text_encoder.t.js new file mode 100644 index 00000000..e790ae37 --- /dev/null +++ b/test/text_encoder.t.js @@ -0,0 +1,87 @@ + +/*--- +includes: [runTsuite.js, compareArray.js] +flags: [async] +---*/ + +function p(args, default_opts) { + let params = merge({}, default_opts); + params = merge(params, args); + + return params; +} + +let encode_tsuite = { + name: "TextEncoder() encode tests", + T: async (params) => { + let te = new TextEncoder(); + + if (te.encoding !== 'utf-8') { + throw Error(`unexpected encoding "${td.encoding}" != "utf-8"`); + } + + let res = te.encode(params.value); + + if (!(res instanceof Uint8Array)) { + throw Error(`unexpected result "${res}" is not Uint8Array`); + } + + if (!compareArray(Array.from(res), params.expected)) { + throw Error(`unexpected output "${res}" != "${params.expected}"`); + } + + return 'SUCCESS'; + }, + + prepare_args: p, + opts: {}, + + tests: [ + { value: "", expected: [] }, + { value: "abc", expected: [97, 98, 99] }, + { value: "α1α", expected: [206, 177, 49, 206, 177] }, + { value: 0.12, exception: 'TypeError: TextEncoder.prototype.encode requires a string' }, + ], +}; + +let encodeinto_tsuite = { + name: "TextEncoder() encodeInto tests", + T: async (params) => { + let te = new TextEncoder(); + + let res = te.encodeInto(params.value, params.dest); + + if (res.written !== params.expected.length) { + throw Error(`unexpected written "${res.written}" != "${params.expected.length}"`); + } + + if (res.read !== params.read) { + throw Error(`unexpected read "${res.read}" != "${params.read}"`); + } + + if (!compareArray(Array.from(params.dest).slice(0, res.written), params.expected)) { + throw Error(`unexpected output "${res}" != "${params.expected}"`); + } + + return 'SUCCESS'; + }, + + prepare_args: p, + opts: {}, + + tests: [ + { value: "", dest: new Uint8Array(4), expected: [], read: 0 }, + { value: "aα", dest: new Uint8Array(3), expected: [97, 206, 177], read: 2 }, + { value: "αααα", dest: new Uint8Array(4), expected: [206, 177, 206, 177], read: 2 }, + { value: "αααα", dest: new Uint8Array(5), expected: [206, 177, 206, 177], read: 2 }, + { value: "αααα", dest: new Uint8Array(6), expected: [206, 177, 206, 177, 206, 177], read: 3 }, + { value: "", dest: 0.12, exception: 'TypeError: TextEncoder.prototype.encodeInto requires a string' }, + { value: 0.12, exception: 'TypeError: TextEncoder.prototype.encodeInto requires a string' }, + ], +}; + +run([ + encode_tsuite, + encodeinto_tsuite, +]) +.then($DONE, $DONE); From noreply at nginx.com Thu Jan 23 18:51:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Thu, 23 Jan 2025 18:51:02 +0000 (UTC) Subject: [nginx] Upstream: fixed --with-compat build without SSL, broken by 454ad0e. Message-ID: <20250123185102.8342D47799@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/5ab4f32e9de1d0c8523d3a22fc20a3067e20b68d branches: master commit: 5ab4f32e9de1d0c8523d3a22fc20a3067e20b68d user: Pavel Pautov date: Tue, 21 Jan 2025 18:41:16 -0800 description: Upstream: fixed --with-compat build without SSL, broken by 454ad0e. --- src/core/ngx_core.h | 1 + src/event/ngx_event_openssl.h | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/ngx_core.h b/src/core/ngx_core.h index 88db7dc98..02890b843 100644 --- a/src/core/ngx_core.h +++ b/src/core/ngx_core.h @@ -26,6 +26,7 @@ typedef struct ngx_event_aio_s ngx_event_aio_t; typedef struct ngx_connection_s ngx_connection_t; typedef struct ngx_thread_task_s ngx_thread_task_t; typedef struct ngx_ssl_s ngx_ssl_t; +typedef struct ngx_ssl_cache_s ngx_ssl_cache_t; typedef struct ngx_proxy_protocol_s ngx_proxy_protocol_t; typedef struct ngx_quic_stream_s ngx_quic_stream_t; typedef struct ngx_ssl_connection_s ngx_ssl_connection_t; diff --git a/src/event/ngx_event_openssl.h b/src/event/ngx_event_openssl.h index c9dc50c75..9ad4d177b 100644 --- a/src/event/ngx_event_openssl.h +++ b/src/event/ngx_event_openssl.h @@ -83,7 +83,6 @@ #endif -typedef struct ngx_ssl_cache_s ngx_ssl_cache_t; typedef struct ngx_ssl_ocsp_s ngx_ssl_ocsp_t; From noreply at nginx.com Fri Jan 24 23:13:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 24 Jan 2025 23:13:02 +0000 (UTC) Subject: [njs] QuickJS: reimplemented process.argv. Message-ID: <20250124231302.9A68D48F17@pubserv1.nginx> details: https://github.com/nginx/njs/commit/70f75ed0cb19ab2348207b48b5b1e7ea7b9cac33 branches: master commit: 70f75ed0cb19ab2348207b48b5b1e7ea7b9cac33 user: Dmitry Volyntsev date: Tue, 21 Jan 2025 18:07:53 -0800 description: QuickJS: reimplemented process.argv. Without using JS_SetOpaque(), because in QuickJS-NG opaque pointer cannot be set for internal classes, including ordinary objects. --- src/qjs.c | 76 ++++++++++++++++++++------------------------------------------- 1 file changed, 24 insertions(+), 52 deletions(-) diff --git a/src/qjs.c b/src/qjs.c index e21e6568..8c5b0b6d 100644 --- a/src/qjs.c +++ b/src/qjs.c @@ -45,7 +45,6 @@ extern char **environ; static JSValue qjs_njs_getter(JSContext *ctx, JSValueConst this_val); static JSValue qjs_njs_to_string_tag(JSContext *ctx, JSValueConst this_val); static JSValue qjs_process_to_string_tag(JSContext *ctx, JSValueConst this_val); -static JSValue qjs_process_argv(JSContext *ctx, JSValueConst this_val); static JSValue qjs_process_env(JSContext *ctx, JSValueConst this_val); static JSValue qjs_process_kill(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv); @@ -137,7 +136,6 @@ static const JSCFunctionListEntry qjs_njs_proto[] = { static const JSCFunctionListEntry qjs_process_proto[] = { JS_CGETSET_DEF("[Symbol.toStringTag]", qjs_process_to_string_tag, NULL), - JS_CGETSET_DEF("argv", qjs_process_argv, NULL), JS_CGETSET_DEF("env", qjs_process_env, NULL), JS_CFUNC_DEF("kill", 2, qjs_process_kill), JS_CGETSET_DEF("pid", qjs_process_pid, NULL), @@ -262,51 +260,6 @@ qjs_process_to_string_tag(JSContext *ctx, JSValueConst this_val) } -static JSValue -qjs_process_argv(JSContext *ctx, JSValueConst this_val) -{ - int i, ret, argc; - JSValue val, str; - const char **argv; - - val = JS_GetPropertyStr(ctx, this_val, "argc"); - if (JS_IsException(val)) { - return JS_EXCEPTION; - } - - if (JS_ToInt32(ctx, &argc, val) < 0) { - return JS_EXCEPTION; - } - - argv = JS_GetOpaque(this_val, JS_GetClassID(this_val)); - if (argv == NULL) { - return JS_NewArray(ctx); - } - - val = JS_NewArray(ctx); - if (JS_IsException(val)) { - return JS_EXCEPTION; - } - - for (i = 0; i < argc; i++) { - str = JS_NewStringLen(ctx, argv[i], njs_strlen(argv[i])); - if (JS_IsException(str)) { - JS_FreeValue(ctx, val); - return JS_EXCEPTION; - } - - ret = JS_DefinePropertyValueUint32(ctx, val, i, str, JS_PROP_C_W_E); - if (ret < 0) { - JS_FreeValue(ctx, str); - JS_FreeValue(ctx, val); - return JS_EXCEPTION; - } - } - - return val; -} - - static JSValue qjs_process_env(JSContext *ctx, JSValueConst this_val) { @@ -451,20 +404,39 @@ qjs_process_ppid(JSContext *ctx, JSValueConst this_val) JSValue qjs_process_object(JSContext *ctx, int argc, const char **argv) { - JSValue obj; + int i; + JSValue obj, str, val; + + val = JS_NewArray(ctx); + if (JS_IsException(val)) { + return JS_EXCEPTION; + } + + for (i = 0; i < argc; i++) { + str = JS_NewStringLen(ctx, argv[i], njs_strlen(argv[i])); + if (JS_IsException(str)) { + JS_FreeValue(ctx, val); + return JS_EXCEPTION; + } + + if (JS_DefinePropertyValueUint32(ctx, val, i, str, JS_PROP_C_W_E) < 0) { + JS_FreeValue(ctx, str); + JS_FreeValue(ctx, val); + return JS_EXCEPTION; + } + } obj = JS_NewObject(ctx); if (JS_IsException(obj)) { + JS_FreeValue(ctx, val); return JS_EXCEPTION; } JS_SetPropertyFunctionList(ctx, obj, qjs_process_proto, njs_nitems(qjs_process_proto)); - JS_SetOpaque(obj, argv); - - if (JS_SetPropertyStr(ctx, obj, "argc", JS_NewInt32(ctx, argc)) < 0) { - JS_FreeValue(ctx, obj); + if (JS_SetPropertyStr(ctx, obj, "argv", val) < 0) { + JS_FreeValue(ctx, val); return JS_EXCEPTION; } From noreply at nginx.com Fri Jan 24 23:13:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 24 Jan 2025 23:13:02 +0000 (UTC) Subject: [njs] Version bump. Message-ID: <20250124231302.974304786C@pubserv1.nginx> details: https://github.com/nginx/njs/commit/de248a1c12c983a5dfd426356e62782aa44d6600 branches: master commit: de248a1c12c983a5dfd426356e62782aa44d6600 user: Dmitry Volyntsev date: Thu, 23 Jan 2025 15:45:43 -0800 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 42a6f405..c7e67aca 100644 --- a/src/njs.h +++ b/src/njs.h @@ -11,8 +11,8 @@ #include -#define NJS_VERSION "0.8.9" -#define NJS_VERSION_NUMBER 0x000809 +#define NJS_VERSION "0.8.10" +#define NJS_VERSION_NUMBER 0x00080a #include From noreply at nginx.com Fri Jan 24 23:13:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 24 Jan 2025 23:13:02 +0000 (UTC) Subject: [njs] QuickJS: correctly handling value len for empty query params. Message-ID: <20250124231302.A08B248F19@pubserv1.nginx> details: https://github.com/nginx/njs/commit/9f15b6986a1b5cd20b9c589142f5a0e586f8d798 branches: master commit: 9f15b6986a1b5cd20b9c589142f5a0e586f8d798 user: Dmitry Volyntsev date: Wed, 22 Jan 2025 17:44:11 -0800 description: QuickJS: correctly handling value len for empty query params. --- nginx/ngx_http_js_module.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c index e900b716..9b6d8ea1 100644 --- a/nginx/ngx_http_js_module.c +++ b/nginx/ngx_http_js_module.c @@ -4848,7 +4848,7 @@ ngx_http_qjs_ext_args(JSContext *cx, JSValueConst this_val) return JS_EXCEPTION; } - val = qjs_string_create(cx, v + 1, p - v - 1); + val = qjs_string_create(cx, v + 1, (p == v) ? 0 : p - v - 1); if (JS_IsException(val)) { chain.free(cx, decoded.start); JS_FreeAtom(cx, key); From noreply at nginx.com Fri Jan 24 23:13:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 24 Jan 2025 23:13:02 +0000 (UTC) Subject: [njs] CI: added test with QuickJS-NG in check-pr job. Message-ID: <20250124231302.AA71948F20@pubserv1.nginx> details: https://github.com/nginx/njs/commit/e257fba89a1627c121d9886aeabd3bc822371dda branches: master commit: e257fba89a1627c121d9886aeabd3bc822371dda user: Dmitry Volyntsev date: Wed, 22 Jan 2025 18:03:55 -0800 description: CI: added test with QuickJS-NG in check-pr job. --- .github/workflows/check-pr.yml | 81 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/.github/workflows/check-pr.yml b/.github/workflows/check-pr.yml index 0c7d7f3e..7cff7937 100644 --- a/.github/workflows/check-pr.yml +++ b/.github/workflows/check-pr.yml @@ -48,6 +48,14 @@ jobs: cd quickjs CFLAGS=$CC_OPT LDFLAGS=$LD_OPT $MAKE_UTILITY -j$(nproc) libquickjs.a + - name: Check out and build quickjs-ng + run: | + git clone https://github.com/quickjs-ng/quickjs quickjs-ng + cd quickjs-ng + git checkout v0.8.0 + CFLAGS="$CC_OPT -fPIC" LDFLAGS=$LD_OPT cmake -B build + cmake --build build --target qjs -j $(nproc) + - name: Configure and make njs run: | ./configure \ @@ -88,6 +96,20 @@ jobs: $MAKE_UTILITY test $MAKE_UTILITY clean + - name: Configure and make njs with quickjs-ng + run: | + ./configure \ + --with-quickjs \ + --cc-opt="$CC_OPT -Iquickjs-ng" \ + --ld-opt="$LD_OPT -Lquickjs-ng/build" \ + || cat build/autoconf.err + $MAKE_UTILITY -j$(nproc) + + - name: Test njs with quickjs-ng + run: | + $MAKE_UTILITY test + $MAKE_UTILITY clean + - name: Configure and build nginx and njs modules run: | cd nginx-source @@ -168,3 +190,62 @@ jobs: TEST_NGINX_VERBOSE: 1 ASAN_OPTIONS: "detect_odr_violation=0:report_globals=0:fast_unwind_on_malloc=0" LSAN_OPTIONS: "suppressions=${{ github.workspace }}/lsan_suppressions.txt" + + - name: Configure and build nginx and njs modules with quickjs-ng, asan, static modules + run: | + cd nginx-source + $NGINX_CONFIGURE_CMD --with-cc-opt="$CC_OPT -I${{ github.workspace }}/quickjs-ng -fsanitize=address -DNJS_DEBUG_MEMORY -DNGX_DEBUG_PALLOC -DNGX_DEBUG_MALLOC" --with-ld-opt="$LD_OPT -L${{ github.workspace }}/quickjs-ng/build -fsanitize=address" --add-module=../nginx || cat objs/autoconf.err + $MAKE_UTILITY -j$(nproc) + + - name: Test njs modules, quickjs-ng, static modules + run: | + ulimit -c unlimited + prove -v -j$(nproc) -Inginx-tests/lib --state=save nginx/t . || prove -v -Inginx-tests/lib --state=failed + env: + TEST_NGINX_BINARY: "${{ github.workspace }}/nginx-source/objs/nginx" + TEST_NGINX_VERBOSE: 1 + ASAN_OPTIONS: "detect_odr_violation=0:report_globals=0" + LSAN_OPTIONS: "suppressions=${{ github.workspace }}/lsan_suppressions.txt" + + - name: Test njs modules (js_engine qjs), quickjs-ng, static modules + run: | + ulimit -c unlimited + prove -v -j$(nproc) -Inginx-tests/lib --state=save nginx/t . || prove -v -Inginx-tests/lib --state=failed + env: + TEST_NGINX_BINARY: "${{ github.workspace }}/nginx-source/objs/nginx" + TEST_NGINX_GLOBALS_HTTP: "js_engine qjs;" + TEST_NGINX_GLOBALS_STREAM: "js_engine qjs;" + TEST_NGINX_VERBOSE: 1 + ASAN_OPTIONS: "detect_odr_violation=0:report_globals=0" + LSAN_OPTIONS: "suppressions=${{ github.workspace }}/lsan_suppressions.txt" + + - name: Configure and build nginx and njs modules with quickjs-ng, asan, dynamic modules + run: | + cd nginx-source + $NGINX_CONFIGURE_CMD --with-debug --with-cc-opt="$CC_OPT -I${{ github.workspace }}/quickjs-ng -fsanitize=address -DNJS_DEBUG_MEMORY -DNGX_DEBUG_PALLOC -DNGX_DEBUG_MALLOC" --with-ld-opt="$LD_OPT -L${{ github.workspace }}/quickjs-ng/build -fsanitize=address" --add-dynamic-module=../nginx || cat objs/autoconf.err + $MAKE_UTILITY -j$(nproc) modules + $MAKE_UTILITY -j$(nproc) + + - name: Test njs modules, quickjs-ng, dynamic modules + run: | + ulimit -c unlimited + prove -v -j$(nproc) -Inginx-tests/lib --state=save nginx/t . || prove -v -Inginx-tests/lib --state=failed + env: + TEST_NGINX_BINARY: "${{ github.workspace }}/nginx-source/objs/nginx" + TEST_NGINX_GLOBALS: "load_module ${{ github.workspace }}/nginx-source/objs/ngx_stream_js_module.so; load_module ${{ github.workspace }}/nginx-source/objs/ngx_http_js_module.so;" + TEST_NGINX_VERBOSE: 1 + ASAN_OPTIONS: "detect_odr_violation=0:report_globals=0:fast_unwind_on_malloc=0" + LSAN_OPTIONS: "suppressions=${{ github.workspace }}/lsan_suppressions.txt" + + - name: Test njs modules (js_engine qjs), quickjs-ng, dynamic modules + run: | + ulimit -c unlimited + prove -v -j$(nproc) -Inginx-tests/lib --state=save nginx/t . || prove -v -Inginx-tests/lib --state=failed + env: + TEST_NGINX_BINARY: "${{ github.workspace }}/nginx-source/objs/nginx" + TEST_NGINX_GLOBALS: "load_module ${{ github.workspace }}/nginx-source/objs/ngx_stream_js_module.so; load_module ${{ github.workspace }}/nginx-source/objs/ngx_http_js_module.so;" + TEST_NGINX_GLOBALS_HTTP: "js_engine qjs;" + TEST_NGINX_GLOBALS_STREAM: "js_engine qjs;" + TEST_NGINX_VERBOSE: 1 + ASAN_OPTIONS: "detect_odr_violation=0:report_globals=0:fast_unwind_on_malloc=0" + LSAN_OPTIONS: "suppressions=${{ github.workspace }}/lsan_suppressions.txt" From noreply at nginx.com Fri Jan 24 23:13:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Fri, 24 Jan 2025 23:13:02 +0000 (UTC) Subject: [njs] QuickJS: added support for QuickJS-NG library. Message-ID: <20250124231302.A65B648F1D@pubserv1.nginx> details: https://github.com/nginx/njs/commit/d4f12ad9a96b16bf02ac09d52908299e2bf0f431 branches: master commit: d4f12ad9a96b16bf02ac09d52908299e2bf0f431 user: Dmitry Volyntsev date: Tue, 21 Jan 2025 17:09:06 -0800 description: QuickJS: added support for QuickJS-NG library. --- auto/quickjs | 72 ++++++++++++++++++++++++++++++++++++++++++++---- external/qjs_fs_module.c | 22 +++++++-------- nginx/config | 36 ++++++++++++++++++++---- nginx/config.make | 4 +-- src/qjs.h | 8 +++++- test/shell_test.exp | 12 ++++---- 6 files changed, 124 insertions(+), 30 deletions(-) diff --git a/auto/quickjs b/auto/quickjs index 9aeb485a..97432142 100644 --- a/auto/quickjs +++ b/auto/quickjs @@ -53,9 +53,19 @@ if [ $NJS_TRY_QUICKJS = YES ]; then . auto/feature fi + if [ $njs_found = no ]; then + njs_feature="QuickJS-NG library -lqjs" + njs_feature_incs="" + njs_feature_libs="-lqjs -lm -ldl -lpthread" + + . auto/feature + fi + + if [ $njs_found = yes ]; then njs_feature="QuickJS JS_GetClassID()" + njs_feature_name=NJS_HAVE_QUICKJS_GET_CLASS_ID njs_feature_test="#if defined(__GNUC__) && (__GNUC__ >= 8) #pragma GCC diagnostic push #pragma GCC diagnostic ignored \"-Wcast-function-type\" @@ -64,7 +74,7 @@ if [ $NJS_TRY_QUICKJS = YES ]; then #include int main() { - (void) JS_GetClassID; + (void) JS_GetClassID(JS_UNDEFINED); return 0; }" @@ -78,6 +88,7 @@ if [ $NJS_TRY_QUICKJS = YES ]; then fi njs_feature="QuickJS JS_NewTypedArray()" + njs_feature_name=NJS_HAVE_QUICKJS_NEW_TYPED_ARRAY njs_feature_test="#if defined(__GNUC__) && (__GNUC__ >= 8) #pragma GCC diagnostic push #pragma GCC diagnostic ignored \"-Wcast-function-type\" @@ -86,15 +97,66 @@ if [ $NJS_TRY_QUICKJS = YES ]; then #include int main() { - (void) JS_NewTypedArray; + JSValue ta, argv; + JSRuntime *rt; + JSContext *ctx; + + rt = JS_NewRuntime(); + ctx = JS_NewContext(rt); + argv = JS_NewInt64(ctx, 1); + ta = JS_NewTypedArray(ctx, 1, &argv, + JS_TYPED_ARRAY_UINT8); + JS_FreeValue(ctx, ta); + JS_FreeContext(ctx); + JS_FreeRuntime(rt); return 0; }" . auto/feature - if [ $njs_found = yes ]; then - njs_define=NJS_HAVE_QUICKJS_NEW_TYPED_ARRAY . auto/define - fi + njs_feature="QuickJS JS_IsSameValue()" + njs_feature_name=NJS_HAVE_QUICKJS_IS_SAME_VALUE + njs_feature_test="#if defined(__GNUC__) && (__GNUC__ >= 8) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored \"-Wcast-function-type\" + #endif + + #include + + int main() { + JSRuntime *rt; + JSContext *ctx; + + rt = JS_NewRuntime(); + ctx = JS_NewContext(rt); + (void) JS_IsSameValue(ctx, JS_UNDEFINED, JS_UNDEFINED); + JS_FreeContext(ctx); + JS_FreeRuntime(rt); + return 0; + }" + + . auto/feature + + njs_feature="QuickJS version" + njs_feature_name=NJS_QUICKJS_VERSION + njs_feature_run=value + njs_feature_test="#if defined(__GNUC__) && (__GNUC__ >= 8) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored \"-Wcast-function-type\" + #endif + + #include + + int main() { +#if defined(QJS_VERSION_MAJOR) + printf(\"\\\"%s\\\"\", JS_GetVersion()); +#else + printf(\"\\\"Unknown\\\"\"); +#endif + return 0; + }" + + . auto/feature NJS_HAVE_QUICKJS=YES NJS_QUICKJS_LIB="$njs_feature_libs" diff --git a/external/qjs_fs_module.c b/external/qjs_fs_module.c index fc2222ff..2adeef20 100644 --- a/external/qjs_fs_module.c +++ b/external/qjs_fs_module.c @@ -397,7 +397,7 @@ qjs_fs_access(JSContext *cx, JSValueConst this_val, int argc, return JS_EXCEPTION; } - if (JS_SameValue(cx, mode, callback)) { + if (qjs_is_same_value(cx, mode, callback)) { mode = JS_UNDEFINED; } } @@ -586,7 +586,7 @@ qjs_fs_mkdir(JSContext *cx, JSValueConst this_val, int argc, return JS_EXCEPTION; } - if (JS_SameValue(cx, options, callback)) { + if (qjs_is_same_value(cx, options, callback)) { options = JS_UNDEFINED; } } @@ -894,7 +894,7 @@ qjs_fs_read_file(JSContext *cx, JSValueConst this_val, int argc, return JS_EXCEPTION; } - if (JS_SameValue(cx, options, callback)) { + if (qjs_is_same_value(cx, options, callback)) { options = JS_UNDEFINED; } } @@ -955,7 +955,7 @@ qjs_fs_read_file(JSContext *cx, JSValueConst this_val, int argc, str.length = sb.st_size; v = qjs_fs_fd_read(cx, fd, &str); - if (!JS_SameValue(cx, v, JS_TRUE)) { + if (!qjs_is_same_value(cx, v, JS_TRUE)) { if (JS_IsException(v)) { result = JS_EXCEPTION; @@ -1012,7 +1012,7 @@ qjs_fs_readlink(JSContext *cx, JSValueConst this_val, int argc, return JS_EXCEPTION; } - if (JS_SameValue(cx, options, callback)) { + if (qjs_is_same_value(cx, options, callback)) { options = JS_UNDEFINED; } } @@ -1150,7 +1150,7 @@ qjs_fs_readdir(JSContext *cx, JSValueConst this_val, int argc, return JS_EXCEPTION; } - if (JS_SameValue(cx, options, callback)) { + if (qjs_is_same_value(cx, options, callback)) { options = JS_UNDEFINED; } } @@ -1309,7 +1309,7 @@ qjs_fs_realpath(JSContext *cx, JSValueConst this_val, int argc, return JS_EXCEPTION; } - if (JS_SameValue(cx, options, callback)) { + if (qjs_is_same_value(cx, options, callback)) { options = JS_UNDEFINED; } } @@ -1655,7 +1655,7 @@ qjs_fs_rmdir(JSContext *cx, JSValueConst this_val, int argc, return JS_EXCEPTION; } - if (JS_SameValue(cx, options, callback)) { + if (qjs_is_same_value(cx, options, callback)) { options = JS_UNDEFINED; } } @@ -1817,7 +1817,7 @@ qjs_fs_stat(JSContext *cx, JSValueConst this_val, int argc, JSValueConst *argv, return JS_EXCEPTION; } - if (JS_SameValue(cx, options, callback)) { + if (qjs_is_same_value(cx, options, callback)) { options = JS_UNDEFINED; } } @@ -1916,7 +1916,7 @@ qjs_fs_symlink(JSContext *cx, JSValueConst this_val, int argc, return JS_EXCEPTION; } - if (JS_SameValue(cx, type, callback)) { + if (qjs_is_same_value(cx, type, callback)) { type = JS_UNDEFINED; } } @@ -2168,7 +2168,7 @@ qjs_fs_write_file(JSContext *cx, JSValueConst this_val, int argc, return JS_EXCEPTION; } - if (JS_SameValue(cx, options, callback)) { + if (qjs_is_same_value(cx, options, callback)) { options = JS_UNDEFINED; } } diff --git a/nginx/config b/nginx/config index 436f06cb..8e920477 100644 --- a/nginx/config +++ b/nginx/config @@ -39,7 +39,6 @@ if [ $NJS_QUICKJS != NO ]; then ngx_feature_test="JSRuntime *rt; rt = JS_NewRuntime(); - (void) JS_GetClassID; JS_FreeRuntime(rt); return 0;" . auto/feature @@ -66,17 +65,44 @@ if [ $NJS_QUICKJS != NO ]; then . auto/feature fi + if [ $ngx_found = no ]; then + ngx_feature="QuickJS-NG library -lqjs" + ngx_feature_path="" + ngx_feature_libs="-lqjs -lm -ldl -lpthread" + + . auto/feature + fi + if [ $ngx_found = yes ]; then + ngx_feature="QuickJS JS_GetClassID()" + ngx_feature_name=NJS_HAVE_QUICKJS_GET_CLASS_ID + ngx_feature_run=no + ngx_feature_test="(void) JS_GetClassID(JS_UNDEFINED);" + + . auto/feature + + if [ $ngx_found = no ]; then + echo + echo $0: error: QuickJS library found, but JS_GetClassID\(\) is missing. + echo + exit 1; + fi + ngx_feature="QuickJS JS_NewTypedArray()" - ngx_feature_test="(void) JS_NewTypedArray; + ngx_feature_name=NJS_HAVE_QUICKJS_NEW_TYPED_ARRAY + ngx_feature_test="JSValue argv; + (void) JS_NewTypedArray(NULL, 1, &argv, + JS_TYPED_ARRAY_UINT8); return 0;" . auto/feature - if [ $ngx_found = yes ]; then - have=NJS_HAVE_QUICKJS_NEW_TYPED_ARRAY . auto/have - fi + ngx_feature="QuickJS JS_IsSameValue()" + ngx_feature_name=NJS_HAVE_QUICKJS_IS_SAME_VALUE + ngx_feature_test="(void) JS_IsSameValue(NULL, JS_UNDEFINED, JS_UNDEFINED);" + + . auto/feature NJS_HAVE_QUICKJS=YES NJS_QUICKJS_LIB="$ngx_feature_libs" diff --git a/nginx/config.make b/nginx/config.make index 2fa40063..404a622b 100644 --- a/nginx/config.make +++ b/nginx/config.make @@ -4,14 +4,14 @@ $ngx_addon_dir/../build/libnjs.a: $NGX_MAKEFILE cd $ngx_addon_dir/.. \\ && if [ -f build/Makefile ]; then \$(MAKE) clean; fi \\ && CFLAGS="\$(CFLAGS)" CC="\$(CC)" ./configure --no-openssl \\ - --no-libxml2 --no-zlib --no-pcre --no-quickjs \\ + --no-libxml2 --no-zlib --no-pcre --no-quickjs --ld-opt="$NGX_LD_OPT" \\ && \$(MAKE) libnjs $ngx_addon_dir/../build/libqjs.a: $NGX_MAKEFILE cd $ngx_addon_dir/.. \\ && if [ -f build/Makefile ]; then \$(MAKE) clean; fi \\ && CFLAGS="\$(CFLAGS)" CC="\$(CC)" ./configure --no-openssl \\ - --no-libxml2 --no-zlib --no-pcre \\ + --no-libxml2 --no-zlib --no-pcre --ld-opt="$NGX_LD_OPT" \\ && \$(MAKE) libnjs libqjs END diff --git a/src/qjs.h b/src/qjs.h index 76bf5c3d..71dd297e 100644 --- a/src/qjs.h +++ b/src/qjs.h @@ -29,7 +29,6 @@ #if defined(__GNUC__) && (__GNUC__ >= 8) #pragma GCC diagnostic pop #endif -#define NJS_QUICKJS_VERSION "Unknown version" #include @@ -101,6 +100,13 @@ static inline JS_BOOL JS_IsNullOrUndefined(JSValueConst v) } +#ifdef NJS_HAVE_QUICKJS_IS_SAME_VALUE +#define qjs_is_same_value(cx, a, b) JS_IsSameValue(cx, a, b) +#else +#define qjs_is_same_value(cx, a, b) JS_SameValue(cx, a, b) +#endif + + extern qjs_module_t *qjs_modules[]; #endif /* _QJS_H_INCLUDED_ */ diff --git a/test/shell_test.exp b/test/shell_test.exp index b713ed09..02828270 100644 --- a/test/shell_test.exp +++ b/test/shell_test.exp @@ -302,11 +302,11 @@ njs_test { njs_test { {"function f() { return ({}.a.a); }\r\n" - "undefined"} - {"var e; try {f()} catch (ee) {e = ee}\r\n" - "undefined"} + "undefined\r\n>> "} + {"var e; try {f()} catch (ee) {e = ee}; undefined\r\n" + "undefined\r\n>> "} {"Object.keys(null)\r\n" - "Thrown:\r\nTypeError: cannot convert*to object"} + "Thrown:\r\nTypeError: *annot convert*to object"} {"e\r\n" "TypeError: cannot * property *a* of undefined"} } @@ -382,11 +382,11 @@ njs_test { njs_run {"-c" "setTimeout(() => {console.log('A'.repeat(1024))}, 0); ref"} \ "^Thrown: -ReferenceError: \['\"\]ref\['\"\] is not defined.*" +ReferenceError: .* is not defined.*" njs_run {"-c" "setTimeout(() => {ref}, 0); setTimeout(() => {console.log('A'.repeat(1024))}, 0)"} \ "^Thrown: -ReferenceError: \['\"\]ref\['\"\] is not defined.*" +ReferenceError: .* is not defined.*" njs_test { {"setImmediate(() => { console.log('x'); return Promise.reject('xx'); })\r\n" From noreply at nginx.com Sat Jan 25 02:50:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Sat, 25 Jan 2025 02:50:02 +0000 (UTC) Subject: [njs] QuickJS: added WebCrypto module. Message-ID: <20250125025002.D1A354786C@pubserv1.nginx> details: https://github.com/nginx/njs/commit/75ca26f8347123c3163439a3652c1218bcb3bfa7 branches: master commit: 75ca26f8347123c3163439a3652c1218bcb3bfa7 user: Dmitry Volyntsev date: Thu, 9 Jan 2025 19:25:58 -0800 description: QuickJS: added WebCrypto module. --- auto/qjs_modules | 8 + external/qjs_webcrypto_module.c | 4886 +++++++++++++++++++++++++++++++++++++++ nginx/config | 4 + nginx/ngx_http_js_module.c | 3 + nginx/ngx_js.h | 1 + nginx/ngx_stream_js_module.c | 3 + nginx/t/js_webcrypto.t | 85 + nginx/t/stream_js_webcrypto.t | 83 + src/qjs.c | 14 + src/qjs.h | 24 +- src/qjs_buffer.c | 37 +- src/test/njs_unit_test.c | 40 - test/webcrypto/sign.t.mjs | 4 +- test/webcrypto/verify.t.mjs | 4 +- 14 files changed, 5127 insertions(+), 69 deletions(-) diff --git a/auto/qjs_modules b/auto/qjs_modules index e74679d3..50ce9476 100644 --- a/auto/qjs_modules +++ b/auto/qjs_modules @@ -13,6 +13,14 @@ njs_module_srcs=external/qjs_fs_module.c . auto/qjs_module +if [ $NJS_OPENSSL = YES -a $NJS_HAVE_OPENSSL = YES ]; then + njs_module_name=qjs_webcrypto_module + njs_module_incs= + njs_module_srcs=external/qjs_webcrypto_module.c + + . auto/qjs_module +fi + if [ $NJS_ZLIB = YES -a $NJS_HAVE_ZLIB = YES ]; then njs_module_name=qjs_zlib_module njs_module_incs= diff --git a/external/qjs_webcrypto_module.c b/external/qjs_webcrypto_module.c new file mode 100644 index 00000000..22f5d108 --- /dev/null +++ b/external/qjs_webcrypto_module.c @@ -0,0 +1,4886 @@ + +/* + * Copyright (C) Dmitry Volyntsev + * Copyright (C) F5, Inc. + */ + + +#include +#include +#include "njs_openssl.h" + +typedef enum { + QJS_KEY_FORMAT_RAW = 1 << 1, + QJS_KEY_FORMAT_PKCS8 = 1 << 2, + QJS_KEY_FORMAT_SPKI = 1 << 3, + QJS_KEY_FORMAT_JWK = 1 << 4, + QJS_KEY_FORMAT_UNKNOWN = 1 << 5, +} qjs_webcrypto_key_format_t; + + +typedef enum { + QJS_KEY_JWK_KTY_RSA, + QJS_KEY_JWK_KTY_EC, + QJS_KEY_JWK_KTY_OCT, + QJS_KEY_JWK_KTY_UNKNOWN, +} qjs_webcrypto_jwk_kty_t; + + +typedef enum { + QJS_KEY_USAGE_DECRYPT = 1 << 1, + QJS_KEY_USAGE_DERIVE_BITS = 1 << 2, + QJS_KEY_USAGE_DERIVE_KEY = 1 << 3, + QJS_KEY_USAGE_ENCRYPT = 1 << 4, + QJS_KEY_USAGE_GENERATE_KEY = 1 << 5, + QJS_KEY_USAGE_SIGN = 1 << 6, + QJS_KEY_USAGE_VERIFY = 1 << 7, + QJS_KEY_USAGE_WRAP_KEY = 1 << 8, + QJS_KEY_USAGE_UNSUPPORTED = 1 << 9, + QJS_KEY_USAGE_UNWRAP_KEY = 1 << 10, +} qjs_webcrypto_key_usage_t; + + +typedef enum { + QJS_ALGORITHM_RSASSA_PKCS1_v1_5 = 0, + QJS_ALGORITHM_RSA_PSS, + QJS_ALGORITHM_RSA_OAEP, + QJS_ALGORITHM_HMAC, + QJS_ALGORITHM_AES_GCM, + QJS_ALGORITHM_AES_CTR, + QJS_ALGORITHM_AES_CBC, + QJS_ALGORITHM_ECDSA, + QJS_ALGORITHM_ECDH, + QJS_ALGORITHM_PBKDF2, + QJS_ALGORITHM_HKDF, + QJS_ALGORITHM_MAX, +} qjs_webcrypto_alg_t; + + +typedef enum { + QJS_HASH_UNSET = 0, + QJS_HASH_SHA1, + QJS_HASH_SHA256, + QJS_HASH_SHA384, + QJS_HASH_SHA512, + QJS_HASH_MAX, +} qjs_webcrypto_hash_t; + + +typedef struct { + njs_str_t name; + uintptr_t value; +} qjs_webcrypto_entry_t; + + +typedef struct { + qjs_webcrypto_alg_t type; + unsigned usage; + unsigned fmt; + unsigned raw; +} qjs_webcrypto_algorithm_t; + + +typedef struct { + qjs_webcrypto_algorithm_t *alg; + unsigned usage; + int extractable; + + qjs_webcrypto_hash_t hash; + + union { + struct { + EVP_PKEY *pkey; + int privat; + int curve; + } a; + struct { + njs_str_t raw; + } s; + } u; + +} qjs_webcrypto_key_t; + + +typedef int (*EVP_PKEY_cipher_init_t)(EVP_PKEY_CTX *ctx); +typedef int (*EVP_PKEY_cipher_t)(EVP_PKEY_CTX *ctx, unsigned char *out, + size_t *outlen, const unsigned char *in, size_t inlen); + +static JSValue qjs_webcrypto_cipher(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv, int encrypt); +static JSValue qjs_cipher_pkey(JSContext *cx, njs_str_t *data, + qjs_webcrypto_key_t *key, int encrypt); +static JSValue qjs_cipher_aes_gcm(JSContext *cx, njs_str_t *data, + qjs_webcrypto_key_t *key, JSValue options, int encrypt); +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_webcrypto_derive(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv, int derive_key); +static JSValue qjs_webcrypto_digest(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv); +static JSValue qjs_webcrypto_export_key(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv); +static JSValue qjs_webcrypto_generate_key(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv); +static JSValue qjs_webcrypto_import_key(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv); +static JSValue qjs_webcrypto_sign(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv, int verify); + +static JSValue qjs_webcrypto_key_to_string_tag(JSContext *cx, + JSValueConst this_val); +static JSValue qjs_webcrypto_key_algorithm(JSContext *cx, + JSValueConst this_val); +static JSValue qjs_webcrypto_key_extractable(JSContext *cx, + JSValueConst this_val); +static JSValue qjs_webcrypto_key_type(JSContext *cx, JSValueConst this_val); +static JSValue qjs_webcrypto_key_usages(JSContext *cx, JSValueConst this_val); + +static JSValue qjs_get_random_values(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv); + +static JSValue qjs_webcrypto_key_make(JSContext *cx, + qjs_webcrypto_algorithm_t *alg, unsigned usage, int extractable); +static void qjs_webcrypto_key_finalizer(JSRuntime *rt, JSValue val); + +static qjs_webcrypto_key_format_t qjs_key_format(JSContext *cx, + JSValueConst value); +static qjs_webcrypto_jwk_kty_t qjs_jwk_kty(JSContext *cx, JSValueConst value); +static qjs_webcrypto_algorithm_t *qjs_key_algorithm(JSContext *cx, JSValue val); +static JSValue qjs_algorithm_curve(JSContext *cx, JSValue options, int *curve); +static JSValue qjs_algorithm_hash(JSContext *cx, JSValue options, + qjs_webcrypto_hash_t *hash); +static const EVP_MD *qjs_algorithm_hash_digest(qjs_webcrypto_hash_t hash); +static njs_str_t *qjs_algorithm_curve_name(int curve); +static const char *qjs_format_string(qjs_webcrypto_key_format_t fmt); +static const char *qjs_algorithm_string(qjs_webcrypto_algorithm_t *algorithm); +static const char *qjs_algorithm_hash_name(qjs_webcrypto_hash_t hash); +static JSValue qjs_key_usage(JSContext *cx, JSValue value, unsigned *mask); +static JSValue qjs_key_ops(JSContext *cx, unsigned mask); +static JSValue qjs_webcrypto_result(JSContext *cx, JSValue result, int rc); +static void qjs_webcrypto_error(JSContext *cx, const char *fmt, ...); + +static JSModuleDef *qjs_webcrypto_init(JSContext *cx, const char *name); + + +static qjs_webcrypto_entry_t qjs_webcrypto_alg[] = { + +#define qjs_webcrypto_algorithm(type, usage, fmt, raw) \ + (uintptr_t) & (qjs_webcrypto_algorithm_t) { type, usage, fmt, raw } + + { + njs_str("RSASSA-PKCS1-v1_5"), + qjs_webcrypto_algorithm(QJS_ALGORITHM_RSASSA_PKCS1_v1_5, + QJS_KEY_USAGE_SIGN | + QJS_KEY_USAGE_VERIFY | + QJS_KEY_USAGE_GENERATE_KEY, + QJS_KEY_FORMAT_PKCS8 | + QJS_KEY_FORMAT_SPKI | + QJS_KEY_FORMAT_JWK, + 0) + }, + + { + njs_str("RSA-PSS"), + qjs_webcrypto_algorithm(QJS_ALGORITHM_RSA_PSS, + QJS_KEY_USAGE_SIGN | + QJS_KEY_USAGE_VERIFY | + QJS_KEY_USAGE_GENERATE_KEY, + QJS_KEY_FORMAT_PKCS8 | + QJS_KEY_FORMAT_SPKI | + QJS_KEY_FORMAT_JWK, + 0) + }, + + { + njs_str("RSA-OAEP"), + qjs_webcrypto_algorithm(QJS_ALGORITHM_RSA_OAEP, + QJS_KEY_USAGE_ENCRYPT | + QJS_KEY_USAGE_DECRYPT | + QJS_KEY_USAGE_WRAP_KEY | + QJS_KEY_USAGE_UNWRAP_KEY | + QJS_KEY_USAGE_GENERATE_KEY, + QJS_KEY_FORMAT_PKCS8 | + QJS_KEY_FORMAT_SPKI | + QJS_KEY_FORMAT_JWK, + 0) + }, + + { + njs_str("HMAC"), + qjs_webcrypto_algorithm(QJS_ALGORITHM_HMAC, + QJS_KEY_USAGE_GENERATE_KEY | + QJS_KEY_USAGE_SIGN | + QJS_KEY_USAGE_VERIFY, + QJS_KEY_FORMAT_RAW | + QJS_KEY_FORMAT_JWK, + 1) + }, + + { + njs_str("AES-GCM"), + qjs_webcrypto_algorithm(QJS_ALGORITHM_AES_GCM, + QJS_KEY_USAGE_ENCRYPT | + QJS_KEY_USAGE_DECRYPT | + QJS_KEY_USAGE_WRAP_KEY | + QJS_KEY_USAGE_UNWRAP_KEY | + QJS_KEY_USAGE_GENERATE_KEY, + QJS_KEY_FORMAT_RAW | + QJS_KEY_FORMAT_JWK, + 1) + }, + + { + njs_str("AES-CTR"), + qjs_webcrypto_algorithm(QJS_ALGORITHM_AES_CTR, + QJS_KEY_USAGE_ENCRYPT | + QJS_KEY_USAGE_DECRYPT | + QJS_KEY_USAGE_WRAP_KEY | + QJS_KEY_USAGE_UNWRAP_KEY | + QJS_KEY_USAGE_GENERATE_KEY, + QJS_KEY_FORMAT_RAW | + QJS_KEY_FORMAT_JWK, + 1) + }, + + { + njs_str("AES-CBC"), + qjs_webcrypto_algorithm(QJS_ALGORITHM_AES_CBC, + QJS_KEY_USAGE_ENCRYPT | + QJS_KEY_USAGE_DECRYPT | + QJS_KEY_USAGE_WRAP_KEY | + QJS_KEY_USAGE_UNWRAP_KEY | + QJS_KEY_USAGE_GENERATE_KEY, + QJS_KEY_FORMAT_RAW | + QJS_KEY_FORMAT_JWK, + 1) + }, + + { + njs_str("ECDSA"), + qjs_webcrypto_algorithm(QJS_ALGORITHM_ECDSA, + QJS_KEY_USAGE_SIGN | + QJS_KEY_USAGE_VERIFY | + QJS_KEY_USAGE_GENERATE_KEY, + QJS_KEY_FORMAT_PKCS8 | + QJS_KEY_FORMAT_SPKI | + QJS_KEY_FORMAT_RAW | + QJS_KEY_FORMAT_JWK, + 0) + }, + + { + njs_str("ECDH"), + 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, + 0) + }, + + { + njs_str("PBKDF2"), + qjs_webcrypto_algorithm(QJS_ALGORITHM_PBKDF2, + QJS_KEY_USAGE_DERIVE_KEY | + QJS_KEY_USAGE_DERIVE_BITS, + QJS_KEY_FORMAT_RAW, + 1) + }, + + { + njs_str("HKDF"), + qjs_webcrypto_algorithm(QJS_ALGORITHM_HKDF, + QJS_KEY_USAGE_DERIVE_KEY | + QJS_KEY_USAGE_DERIVE_BITS, + QJS_KEY_FORMAT_RAW, + 1) + }, + + { + njs_null_str, + 0 + } +}; + + +static qjs_webcrypto_entry_t qjs_webcrypto_hash[] = { + { njs_str("SHA-256"), QJS_HASH_SHA256 }, + { njs_str("SHA-384"), QJS_HASH_SHA384 }, + { njs_str("SHA-512"), QJS_HASH_SHA512 }, + { njs_str("SHA-1"), QJS_HASH_SHA1 }, + { njs_null_str, 0 } +}; + + +static qjs_webcrypto_entry_t qjs_webcrypto_curve[] = { + { njs_str("P-256"), NID_X9_62_prime256v1 }, + { njs_str("P-384"), NID_secp384r1 }, + { njs_str("P-521"), NID_secp521r1 }, + { njs_null_str, 0 } +}; + + +static qjs_webcrypto_entry_t qjs_webcrypto_format[] = { + { njs_str("raw"), QJS_KEY_FORMAT_RAW }, + { njs_str("pkcs8"), QJS_KEY_FORMAT_PKCS8 }, + { njs_str("spki"), QJS_KEY_FORMAT_SPKI }, + { njs_str("jwk"), QJS_KEY_FORMAT_JWK }, + { njs_null_str, QJS_KEY_FORMAT_UNKNOWN } +}; + + +static qjs_webcrypto_entry_t qjs_webcrypto_jwk_kty[] = { + { njs_str("RSA"), QJS_KEY_JWK_KTY_RSA }, + { njs_str("EC"), QJS_KEY_JWK_KTY_EC }, + { njs_str("oct"), QJS_KEY_JWK_KTY_OCT }, + { njs_null_str, QJS_KEY_JWK_KTY_UNKNOWN } +}; + + +static qjs_webcrypto_entry_t qjs_webcrypto_usage[] = { + { njs_str("decrypt"), QJS_KEY_USAGE_DECRYPT }, + { njs_str("deriveBits"), QJS_KEY_USAGE_DERIVE_BITS }, + { njs_str("deriveKey"), QJS_KEY_USAGE_DERIVE_KEY }, + { njs_str("encrypt"), QJS_KEY_USAGE_ENCRYPT }, + { njs_str("sign"), QJS_KEY_USAGE_SIGN }, + { njs_str("unwrapKey"), QJS_KEY_USAGE_UNWRAP_KEY }, + { njs_str("verify"), QJS_KEY_USAGE_VERIFY }, + { njs_str("wrapKey"), QJS_KEY_USAGE_WRAP_KEY }, + { njs_null_str, 0 } +}; + + +static qjs_webcrypto_entry_t qjs_webcrypto_alg_hash[] = { + { njs_str("RS1"), QJS_HASH_SHA1 }, + { njs_str("RS256"), QJS_HASH_SHA256 }, + { njs_str("RS384"), QJS_HASH_SHA384 }, + { njs_str("RS512"), QJS_HASH_SHA512 }, + { njs_str("PS1"), QJS_HASH_SHA1 }, + { njs_str("PS256"), QJS_HASH_SHA256 }, + { njs_str("PS384"), QJS_HASH_SHA384 }, + { njs_str("PS512"), QJS_HASH_SHA512 }, + { njs_str("RSA-OAEP"), QJS_HASH_SHA1 }, + { njs_str("RSA-OAEP-256"), QJS_HASH_SHA256 }, + { njs_str("RSA-OAEP-384"), QJS_HASH_SHA384 }, + { njs_str("RSA-OAEP-512"), QJS_HASH_SHA512 }, + { njs_null_str, 0 } +}; + + +static njs_str_t + qjs_webcrypto_alg_name[QJS_ALGORITHM_HMAC + 1][QJS_HASH_MAX] = { + { + njs_null_str, + njs_str("RS1"), + njs_str("RS256"), + njs_str("RS384"), + njs_str("RS512"), + }, + + { + njs_null_str, + njs_str("PS1"), + njs_str("PS256"), + njs_str("PS384"), + njs_str("PS512"), + }, + + { + njs_null_str, + njs_str("RSA-OAEP"), + njs_str("RSA-OAEP-256"), + njs_str("RSA-OAEP-384"), + njs_str("RSA-OAEP-512"), + }, + + { + njs_null_str, + njs_str("HS1"), + njs_str("HS256"), + njs_str("HS384"), + njs_str("HS512"), + }, +}; + +static njs_str_t qjs_webcrypto_alg_aes_name[3][3 + 1] = { + { + njs_str("A128GCM"), + njs_str("A192GCM"), + njs_str("A256GCM"), + njs_null_str, + }, + + { + njs_str("A128CTR"), + njs_str("A192CTR"), + njs_str("A256CTR"), + njs_null_str, + }, + + { + njs_str("A128CBC"), + njs_str("A192CBC"), + njs_str("A256CBC"), + njs_null_str, + }, +}; + + +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_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), + JS_CFUNC_DEF("generateKey", 3, qjs_webcrypto_generate_key), + JS_CFUNC_MAGIC_DEF("sign", 4, qjs_webcrypto_sign, 0), + JS_CFUNC_MAGIC_DEF("verify", 4, qjs_webcrypto_sign, 1), +}; + + +static const JSCFunctionListEntry qjs_webcrypto_key_proto[] = { + JS_CGETSET_DEF("[Symbol.toStringTag]", qjs_webcrypto_key_to_string_tag, + NULL), + JS_CGETSET_DEF("algorithm", qjs_webcrypto_key_algorithm, NULL), + JS_CGETSET_DEF("extractable", qjs_webcrypto_key_extractable, NULL), + JS_CGETSET_DEF("type", qjs_webcrypto_key_type, NULL), + JS_CGETSET_DEF("usages", qjs_webcrypto_key_usages, NULL), +}; + + +static const JSCFunctionListEntry qjs_webcrypto_export[] = { + JS_CFUNC_DEF("getRandomValues", 1, qjs_get_random_values), + JS_OBJECT_DEF("subtle", + qjs_webcrypto_subtle, + njs_nitems(qjs_webcrypto_subtle), + JS_PROP_CONFIGURABLE), +}; + + +static JSClassDef qjs_webcrypto_key_class = { + "WebCryptoKey", + .finalizer = qjs_webcrypto_key_finalizer, +}; + + +qjs_module_t qjs_webcrypto_module = { + .name = "webcrypto", + .init = qjs_webcrypto_init, +}; + + +static JSValue +qjs_webcrypto_cipher(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv, int encrypt) +{ + unsigned mask; + JSValue ret, options; + njs_str_t data; + qjs_webcrypto_key_t *key; + qjs_webcrypto_algorithm_t *alg; + + options = argv[0]; + alg = qjs_key_algorithm(cx, options); + if (alg == NULL) { + goto fail; + } + + key = JS_GetOpaque(argv[1], QJS_CORE_CLASS_ID_WEBCRYPTO_KEY); + if (key == NULL) { + JS_ThrowTypeError(cx, "key is not a CryptoKey object"); + goto fail; + } + + mask = encrypt ? QJS_KEY_USAGE_ENCRYPT : QJS_KEY_USAGE_DECRYPT; + if ((key->usage & mask) != mask) { + JS_ThrowTypeError(cx, "key does not support %s operation", + encrypt ? "encrypt" : "decrypt"); + goto fail; + } + + if (key->alg != alg) { + JS_ThrowTypeError(cx, "cannot %s use \"%s\" with \"%s\" key", + encrypt ? "encrypt" : "decrypt", + qjs_algorithm_string(key->alg), + qjs_algorithm_string(alg)); + goto fail; + } + + ret = qjs_typed_array_data(cx, argv[2], &data); + if (JS_IsException(ret)) { + return ret; + } + + switch (alg->type) { + case QJS_ALGORITHM_RSA_OAEP: + ret = qjs_cipher_pkey(cx, &data, key, encrypt); + if (JS_IsException(ret)) { + goto fail; + } + + break; + + case QJS_ALGORITHM_AES_GCM: + ret = qjs_cipher_aes_gcm(cx, &data, key, options, encrypt); + if (JS_IsException(ret)) { + goto fail; + } + + break; + + case QJS_ALGORITHM_AES_CTR: + ret = qjs_cipher_aes_ctr(cx, &data, key, options, encrypt); + if (JS_IsException(ret)) { + goto fail; + } + + break; + + case QJS_ALGORITHM_AES_CBC: + default: + ret = qjs_cipher_aes_cbc(cx, &data, key, options, encrypt); + if (JS_IsException(ret)) { + goto fail; + } + } + + return qjs_webcrypto_result(cx, ret, 0); + +fail: + + return qjs_webcrypto_result(cx, JS_UNDEFINED, -1); +} + + +static JSValue +qjs_cipher_pkey(JSContext *cx, njs_str_t *data, qjs_webcrypto_key_t *key, + int encrypt) +{ + int rc; + u_char *dst; + size_t outlen; + JSValue ret; + const EVP_MD *md; + EVP_PKEY_CTX *ctx; + EVP_PKEY_cipher_t cipher; + EVP_PKEY_cipher_init_t init; + + ctx = EVP_PKEY_CTX_new(key->u.a.pkey, NULL); + if (ctx == NULL) { + qjs_webcrypto_error(cx, "EVP_PKEY_CTX_new() failed"); + return JS_EXCEPTION; + } + + if (encrypt) { + init = EVP_PKEY_encrypt_init; + cipher = EVP_PKEY_encrypt; + + } else { + init = EVP_PKEY_decrypt_init; + cipher = EVP_PKEY_decrypt; + } + + rc = init(ctx); + if (rc <= 0) { + qjs_webcrypto_error(cx, "EVP_PKEY_%scrypt_init() failed", + encrypt ? "en" : "de"); + ret = JS_EXCEPTION; + goto fail; + } + + md = qjs_algorithm_hash_digest(key->hash); + + EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING); + EVP_PKEY_CTX_set_signature_md(ctx, md); + EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, md); + + rc = cipher(ctx, NULL, &outlen, data->start, data->length); + if (rc <= 0) { + qjs_webcrypto_error(cx, "EVP_PKEY_%scrypt() failed", + encrypt ? "en" : "de"); + ret = JS_EXCEPTION; + goto fail; + } + + dst = js_malloc(cx, outlen); + if (dst == NULL) { + JS_ThrowOutOfMemory(cx); + ret = JS_EXCEPTION; + goto fail; + } + + rc = cipher(ctx, dst, &outlen, data->start, data->length); + if (rc <= 0) { + qjs_webcrypto_error(cx, "EVP_PKEY_%scrypt() failed", + encrypt ? "en" : "de"); + js_free(cx, dst); + ret = JS_EXCEPTION; + goto fail; + } + + ret = qjs_new_array_buffer(cx, dst, outlen); + +fail: + + EVP_PKEY_CTX_free(ctx); + + return ret; +} + + +static JSValue +qjs_cipher_aes_gcm(JSContext *cx, njs_str_t *data, qjs_webcrypto_key_t *key, + JSValue options, int encrypt) +{ + int len, outlen, dstlen; + u_char *dst, *p; + JSValue ret, value; + int64_t taglen; + njs_str_t iv, aad; + EVP_CIPHER_CTX *ctx; + const EVP_CIPHER *cipher; + + switch (key->u.s.raw.length) { + case 16: + cipher = EVP_aes_128_gcm(); + break; + + case 24: + cipher = EVP_aes_192_gcm(); + break; + + case 32: + cipher = EVP_aes_256_gcm(); + break; + + default: + JS_ThrowTypeError(cx, "AES-GCM invalid key length"); + return JS_EXCEPTION; + } + + value = JS_GetPropertyStr(cx, options, "iv"); + if (JS_IsException(value)) { + return JS_EXCEPTION; + } + + if (JS_IsUndefined(value)) { + JS_ThrowTypeError(cx, "AES-GCM algorithm.iv is not provided"); + return JS_EXCEPTION; + } + + ret = qjs_typed_array_data(cx, value, &iv); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + + JS_FreeValue(cx, value); + + taglen = 128; + + value = JS_GetPropertyStr(cx, options, "tagLength"); + if (JS_IsException(value)) { + return JS_EXCEPTION; + } + + if (!JS_IsUndefined(value)) { + if (JS_ToInt64(cx, &taglen, value) < 0) { + return JS_EXCEPTION; + } + } + + if (taglen != 32 && taglen != 64 && taglen != 96 && taglen != 104 + && taglen != 112 && taglen != 120 && taglen != 128) + { + JS_ThrowTypeError(cx, "AES-GCM invalid tagLength"); + return JS_EXCEPTION; + } + + taglen /= 8; + + if (!encrypt && data->length < (size_t) taglen) { + JS_ThrowTypeError(cx, "AES-GCM data is too short"); + return JS_EXCEPTION; + } + + ctx = EVP_CIPHER_CTX_new(); + if (ctx == NULL) { + qjs_webcrypto_error(cx, "EVP_CIPHER_CTX_new() failed"); + return JS_EXCEPTION; + } + + if (EVP_CipherInit_ex(ctx, cipher, NULL, NULL, NULL, encrypt) <= 0) { + qjs_webcrypto_error(cx, "EVP_%sInit_ex() failed", + encrypt ? "Encrypt" : "Decrypt"); + goto fail; + } + + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv.length, NULL) <= 0) { + qjs_webcrypto_error(cx, "EVP_CIPHER_CTX_ctrl() failed"); + goto fail; + } + + if (EVP_CipherInit_ex(ctx, NULL, NULL, key->u.s.raw.start, iv.start, + encrypt) <= 0) + { + qjs_webcrypto_error(cx, "EVP_%sInit_ex() failed", + encrypt ? "Encrypt" : "Decrypt"); + goto fail; + } + + if (!encrypt) { + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, taglen, + &data->start[data->length - taglen]) <= 0) + { + qjs_webcrypto_error(cx, "EVP_CIPHER_CTX_ctrl() failed"); + goto fail; + } + } + + aad.length = 0; + + value = JS_GetPropertyStr(cx, options, "additionalData"); + if (JS_IsException(value)) { + goto fail; + } + + if (!JS_IsUndefined(value)) { + ret = qjs_typed_array_data(cx, value, &aad); + if (JS_IsException(ret)) { + goto fail; + } + } + + JS_FreeValue(cx, value); + + if (aad.length != 0) { + if (EVP_CipherUpdate(ctx, NULL, &outlen, aad.start, aad.length) <= 0) { + qjs_webcrypto_error(cx, "EVP_%sUpdate() failed", + encrypt ? "Encrypt" : "Decrypt"); + goto fail; + } + } + + dstlen = data->length + EVP_CIPHER_CTX_block_size(ctx) + taglen; + dst = js_malloc(cx, dstlen); + if (dst == NULL) { + JS_ThrowOutOfMemory(cx); + goto fail; + } + + if (EVP_CipherUpdate(ctx, dst, &outlen, data->start, + data->length - (encrypt ? 0 : taglen)) <= 0) + { + qjs_webcrypto_error(cx, "EVP_%sUpdate() failed", + encrypt ? "Encrypt" : "Decrypt"); + js_free(cx, dst); + goto fail; + } + + p = &dst[outlen]; + len = EVP_CIPHER_CTX_block_size(ctx); + + if (EVP_CipherFinal_ex(ctx, p, &len) <= 0) { + qjs_webcrypto_error(cx, "EVP_%sFinal_ex() failed", + encrypt ? "Encrypt" : "Decrypt"); + js_free(cx, dst); + goto fail; + } + + outlen += len; + p += len; + + if (encrypt) { + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, taglen, p) <= 0) { + qjs_webcrypto_error(cx, "EVP_CIPHER_CTX_ctrl() failed"); + js_free(cx, dst); + goto fail; + } + + outlen += taglen; + } + + ret = qjs_new_array_buffer(cx, dst, outlen); + + EVP_CIPHER_CTX_free(ctx); + + return ret; + +fail: + + EVP_CIPHER_CTX_free(ctx); + + return JS_EXCEPTION; +} + + +static int +qjs_cipher_aes_ctr128(JSContext *cx, const EVP_CIPHER *cipher, u_char *key, + u_char *data, size_t dlen, u_char *counter, u_char *dst, int *olen, + int encrypt) +{ + int len, outlen; + int ret; + EVP_CIPHER_CTX *ctx; + + ctx = EVP_CIPHER_CTX_new(); + if (ctx == NULL) { + qjs_webcrypto_error(cx, "EVP_CIPHER_CTX_new() failed"); + return -1; + } + + ret = EVP_CipherInit_ex(ctx, cipher, NULL, key, counter, encrypt); + if (ret <= 0) { + qjs_webcrypto_error(cx, "EVP_%sInit_ex() failed", + encrypt ? "Encrypt" : "Decrypt"); + ret = -1; + goto fail; + } + + ret = EVP_CipherUpdate(ctx, dst, &outlen, data, dlen); + if (ret <= 0) { + qjs_webcrypto_error(cx, "EVP_%sUpdate() failed", + encrypt ? "Encrypt" : "Decrypt"); + ret = -1; + goto fail; + } + + ret = EVP_CipherFinal_ex(ctx, &dst[outlen], &len); + if (ret <= 0) { + qjs_webcrypto_error(cx, "EVP_%sFinal_ex() failed", + encrypt ? "Encrypt" : "Decrypt"); + ret = -1; + goto fail; + } + + outlen += len; + *olen = outlen; + + ret = 0; + +fail: + + EVP_CIPHER_CTX_free(ctx); + + return ret; +} + + +static BIGNUM * +qjs_bn_counter128(njs_str_t *ctr, unsigned bits) +{ + unsigned remainder, bytes; + uint8_t buf[16]; + + remainder = bits % 8; + + if (remainder == 0) { + bytes = bits / 8; + + return BN_bin2bn(&ctr->start[ctr->length - bytes], bytes, NULL); + } + + bytes = (bits + 7) / 8; + + memcpy(buf, &ctr->start[ctr->length - bytes], bytes); + + buf[0] &= ~(0xFF << remainder); + + return BN_bin2bn(buf, bytes, NULL); +} + + +njs_inline unsigned +qjs_ceiling_div(unsigned dend, unsigned dsor) +{ + return (dsor == 0) ? 0 : 1 + (dend - 1) / dsor; +} + + +static void +qjs_counter128_reset(u_char *src, u_char *dst, unsigned bits) +{ + size_t index; + unsigned remainder, bytes; + + bytes = bits / 8; + remainder = bits % 8; + + memcpy(dst, src, 16); + + index = 16 - bytes; + + memset(&dst[index], 0, bytes); + + if (remainder) { + dst[index - 1] &= 0xff << remainder; + } +} + + +static JSValue +qjs_cipher_aes_ctr(JSContext *cx, njs_str_t *data, qjs_webcrypto_key_t *key, + JSValue options, int encrypt) +{ + int len, len2; + u_char *dst; + BIGNUM *total, *blocks, *left, *ctr; + size_t size1; + JSValue ret, value; + int64_t length; + njs_str_t iv; + const EVP_CIPHER *cipher; + u_char iv2[16]; + + switch (key->u.s.raw.length) { + case 16: + cipher = EVP_aes_128_ctr(); + break; + + case 24: + cipher = EVP_aes_192_ctr(); + break; + + case 32: + cipher = EVP_aes_256_ctr(); + break; + + default: + JS_ThrowTypeError(cx, "AES-CTR invalid key length"); + return JS_EXCEPTION; + } + + value = JS_GetPropertyStr(cx, options, "counter"); + if (JS_IsException(value)) { + return JS_EXCEPTION; + } + + if (JS_IsUndefined(value)) { + JS_ThrowTypeError(cx, "AES-CTR algorithm.counter is not provided"); + return JS_EXCEPTION; + } + + ret = qjs_typed_array_data(cx, value, &iv); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + + JS_FreeValue(cx, value); + + if (iv.length != 16) { + JS_ThrowTypeError(cx, "AES-CTR algorithm.counter must be 16 bytes " + "long"); + return JS_EXCEPTION; + } + + value = JS_GetPropertyStr(cx, options, "length"); + if (JS_IsException(value)) { + return JS_EXCEPTION; + } + + if (JS_IsUndefined(value)) { + JS_ThrowTypeError(cx, "AES-CTR algorithm.length is not provided"); + return JS_EXCEPTION; + } + + if (JS_ToInt64(cx, &length, value) < 0) { + return JS_EXCEPTION; + } + + if (length == 0 || length > 128) { + JS_ThrowTypeError(cx, "AES-CTR algorithm.length must be between " + "1 and 128"); + return JS_EXCEPTION; + } + + ctr = NULL; + blocks = NULL; + left = NULL; + + total = BN_new(); + if (total == NULL) { + qjs_webcrypto_error(cx, "BN_new() failed"); + return JS_EXCEPTION; + } + + if (BN_lshift(total, BN_value_one(), length) != 1) { + qjs_webcrypto_error(cx, "BN_lshift() failed"); + ret = JS_EXCEPTION; + goto fail; + } + + ctr = qjs_bn_counter128(&iv, length); + if (ctr == NULL) { + qjs_webcrypto_error(cx, "BN_bin2bn() failed"); + ret = JS_EXCEPTION; + goto fail; + } + + blocks = BN_new(); + if (blocks == NULL) { + qjs_webcrypto_error(cx, "BN_new() failed"); + return JS_EXCEPTION; + } + + if (BN_set_word(blocks, qjs_ceiling_div(data->length, AES_BLOCK_SIZE)) + != 1) + { + qjs_webcrypto_error(cx, "BN_set_word() failed"); + ret = JS_EXCEPTION; + goto fail; + } + + if (BN_cmp(blocks, total) > 0) { + JS_ThrowTypeError(cx, "AES-CTR repeated counter"); + ret = JS_EXCEPTION; + goto fail; + } + + left = BN_new(); + if (left == NULL) { + qjs_webcrypto_error(cx, "BN_new() failed"); + return JS_EXCEPTION; + } + + if (BN_sub(left, total, ctr) != 1) { + qjs_webcrypto_error(cx, "BN_sub() failed"); + ret = JS_EXCEPTION; + goto fail; + } + + dst = js_malloc(cx, data->length + EVP_MAX_BLOCK_LENGTH); + if (dst == NULL) { + JS_ThrowOutOfMemory(cx); + ret = JS_EXCEPTION; + goto fail; + } + + if (BN_cmp(left, blocks) >= 0) { + /* + * Doing a single run if a counter is not wrapped-around + * during the ciphering. + */ + if (qjs_cipher_aes_ctr128(cx, cipher, key->u.s.raw.start, + data->start, data->length, iv.start, dst, + &len, encrypt) < 0) + { + js_free(cx, dst); + ret = JS_EXCEPTION; + goto fail; + } + + goto done; + } + + /* + * Otherwise splitting ciphering into two parts: + * Until the wrapping moment + * After the resetting counter to zero. + */ + + size1 = BN_get_word(left) * AES_BLOCK_SIZE; + + if (qjs_cipher_aes_ctr128(cx, cipher, key->u.s.raw.start, data->start, + size1, iv.start, dst, &len, encrypt) < 0) + { + js_free(cx, dst); + ret = JS_EXCEPTION; + goto fail; + } + + qjs_counter128_reset(iv.start, iv2, length); + + if (qjs_cipher_aes_ctr128(cx, cipher, key->u.s.raw.start, + &data->start[size1], data->length - size1, + iv2, &dst[size1], &len2, encrypt) < 0) + { + js_free(cx, dst); + ret = JS_EXCEPTION; + goto fail; + } + + len += len2; + +done: + + ret = qjs_new_array_buffer(cx, dst, len); + +fail: + + BN_free(total); + + if (ctr != NULL) { + BN_free(ctr); + } + + if (blocks != NULL) { + BN_free(blocks); + } + + if (left != NULL) { + BN_free(left); + } + + return ret; +} + + +static JSValue +qjs_cipher_aes_cbc(JSContext *cx, njs_str_t *data, qjs_webcrypto_key_t *key, + JSValue options, int encrypt) +{ + int rc, olen_max, olen, olen2; + u_char *dst; + JSValue ret, value; + unsigned remainder; + njs_str_t iv; + EVP_CIPHER_CTX *ctx; + const EVP_CIPHER *cipher; + + switch (key->u.s.raw.length) { + case 16: + cipher = EVP_aes_128_cbc(); + break; + + case 24: + cipher = EVP_aes_192_cbc(); + break; + + case 32: + cipher = EVP_aes_256_cbc(); + break; + + default: + JS_ThrowTypeError(cx, "AES-CBC invalid key length"); + return JS_EXCEPTION; + } + + value = JS_GetPropertyStr(cx, options, "iv"); + if (JS_IsException(value)) { + return JS_EXCEPTION; + } + + if (JS_IsUndefined(value)) { + JS_ThrowTypeError(cx, "AES-CBC algorithm.iv is not provided"); + return JS_EXCEPTION; + } + + ret = qjs_typed_array_data(cx, value, &iv); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + + JS_FreeValue(cx, value); + + if (iv.length != 16) { + JS_ThrowTypeError(cx, "AES-CBC algorithm.iv must be 16 bytes long"); + return JS_EXCEPTION; + } + + olen_max = data->length + AES_BLOCK_SIZE - 1; + remainder = olen_max % AES_BLOCK_SIZE; + + if (remainder != 0) { + olen_max += AES_BLOCK_SIZE - remainder; + } + + ctx = EVP_CIPHER_CTX_new(); + if (ctx == NULL) { + qjs_webcrypto_error(cx, "EVP_CIPHER_CTX_new() failed"); + return JS_EXCEPTION; + } + + rc = EVP_CipherInit_ex(ctx, cipher, NULL, key->u.s.raw.start, iv.start, + encrypt); + if (rc <= 0) { + qjs_webcrypto_error(cx, "EVP_%sInit_ex() failed", + encrypt ? "Encrypt" : "Decrypt"); + ret = JS_EXCEPTION; + goto fail; + } + + dst = js_malloc(cx, olen_max); + if (dst == NULL) { + JS_ThrowOutOfMemory(cx); + ret = JS_EXCEPTION; + goto fail; + } + + rc = EVP_CipherUpdate(ctx, dst, &olen, data->start, data->length); + if (rc <= 0) { + qjs_webcrypto_error(cx, "EVP_%sUpdate() failed", + encrypt ? "Encrypt" : "Decrypt"); + js_free(cx, dst); + ret = JS_EXCEPTION; + goto fail; + } + + rc = EVP_CipherFinal_ex(ctx, &dst[olen], &olen2); + if (rc <= 0) { + qjs_webcrypto_error(cx, "EVP_%sFinal_ex() failed", + encrypt ? "Encrypt" : "Decrypt"); + js_free(cx, dst); + ret = JS_EXCEPTION; + goto fail; + } + + olen += olen2; + + ret = qjs_new_array_buffer(cx, dst, olen); + +fail: + + EVP_CIPHER_CTX_free(ctx); + + return ret; +} + + +static JSValue +qjs_string_base64url(JSContext *cx, const njs_str_t *src) +{ + size_t padding; + njs_str_t dst; + u_char buf[/* qjs_base64_encoded_length(512) */ 686]; + + if (src->length == 0) { + return JS_NewStringLen(cx, "", 0); + } + + padding = src->length % 3; + padding = (4 >> padding) & 0x03; + + dst.start = buf; + dst.length = qjs_base64_encode_length(cx, src) - padding; + + qjs_base64url_encode(cx, src, &dst); + + return JS_NewStringLen(cx, (const char *) dst.start, dst.length); +} + + +static JSValue +qjs_export_base64url_bignum(JSContext *cx, const BIGNUM *v, size_t size) +{ + njs_str_t src; + u_char buf[512]; + + if (size == 0) { + size = BN_num_bytes(v); + } + + if (njs_bn_bn2binpad(v, &buf[0], size) <= 0) { + JS_ThrowInternalError(cx, "njs_bn_bn2binpad() failed"); + return JS_EXCEPTION; + } + + src.start = buf; + src.length = size; + + return qjs_string_base64url(cx, &src); +} + + +static int +qjs_base64url_bignum_set(JSContext *cx, JSValue jwk, const char *key, + const BIGNUM *v, size_t size) +{ + JSValue value; + + value = qjs_export_base64url_bignum(cx, v, size); + if (JS_IsException(value)) { + return -1; + } + + if (JS_DefinePropertyValueStr(cx, jwk, key, value, JS_PROP_C_W_E) < 0) { + JS_FreeValue(cx, value); + return -1; + } + + return 0; +} + + +static JSValue +qjs_export_jwk_rsa(JSContext *cx, qjs_webcrypto_key_t *key) +{ + JSValue jwk, alg; + njs_str_t *nm; + const RSA *rsa; + const BIGNUM *n_bn, *e_bn, *d_bn, *p_bn, *q_bn, *dp_bn, *dq_bn, *qi_bn; + + rsa = njs_pkey_get_rsa_key(key->u.a.pkey); + + njs_rsa_get0_key(rsa, &n_bn, &e_bn, &d_bn); + + jwk = JS_NewObject(cx); + if (JS_IsException(jwk)) { + return JS_EXCEPTION; + } + + if (qjs_base64url_bignum_set(cx, jwk, "n", n_bn, 0) < 0) { + goto fail; + } + + if (qjs_base64url_bignum_set(cx, jwk, "e", e_bn, 0) < 0) { + goto fail; + } + + if (JS_DefinePropertyValueStr(cx, jwk, "kty", JS_NewString(cx, "RSA"), + JS_PROP_C_W_E) < 0) + { + goto fail; + } + + if (key->u.a.privat) { + njs_rsa_get0_factors(rsa, &p_bn, &q_bn); + njs_rsa_get0_ctr_params(rsa, &dp_bn, &dq_bn, &qi_bn); + + if (qjs_base64url_bignum_set(cx, jwk, "d", d_bn, 0) < 0) { + goto fail; + } + + if (qjs_base64url_bignum_set(cx, jwk, "p", p_bn, 0) < 0) { + goto fail; + } + + if (qjs_base64url_bignum_set(cx, jwk, "q", q_bn, 0) < 0) { + goto fail; + } + + if (qjs_base64url_bignum_set(cx, jwk, "dp", dp_bn, 0) < 0) { + goto fail; + } + + if (qjs_base64url_bignum_set(cx, jwk, "dq", dq_bn, 0) < 0) { + goto fail; + } + + if (qjs_base64url_bignum_set(cx, jwk, "qi", qi_bn, 0) < 0) { + goto fail; + } + } + + nm = &qjs_webcrypto_alg_name[key->alg->type][key->hash]; + + alg = JS_NewStringLen(cx, (char *) nm->start, nm->length); + if (JS_IsException(alg)) { + goto fail; + } + + if (JS_DefinePropertyValueStr(cx, jwk, "alg", alg, JS_PROP_C_W_E) < 0) { + JS_FreeValue(cx, alg); + goto fail; + } + + return jwk; + +fail: + + JS_FreeValue(cx, jwk); + + return JS_EXCEPTION; +} + + +static JSValue +qjs_export_jwk_ec(JSContext *cx, qjs_webcrypto_key_t *key) +{ + int nid, group_bits, group_bytes; + BIGNUM *x_bn, *y_bn; + JSValue jwk, name; + njs_str_t *cname; + const EC_KEY *ec; + const BIGNUM *d_bn; + const EC_POINT *pub; + const EC_GROUP *group; + + x_bn = NULL; + y_bn = NULL; + d_bn = NULL; + jwk = JS_UNDEFINED; + + ec = njs_pkey_get_ec_key(key->u.a.pkey); + + pub = EC_KEY_get0_public_key(ec); + group = EC_KEY_get0_group(ec); + + group_bits = EC_GROUP_get_degree(group); + group_bytes = (group_bits / 8) + (7 + (group_bits % 8)) / 8; + + x_bn = BN_new(); + if (x_bn == NULL) { + goto fail; + } + + y_bn = BN_new(); + if (y_bn == NULL) { + goto fail; + } + + if (!njs_ec_point_get_affine_coordinates(group, pub, x_bn, y_bn)) { + qjs_webcrypto_error(cx, "EC_POINT_get_affine_coordinates() failed"); + goto fail; + } + + jwk = JS_NewObject(cx); + if (JS_IsException(jwk)) { + goto fail; + } + + if (qjs_base64url_bignum_set(cx, jwk, "x", x_bn, group_bytes) < 0) { + goto fail; + } + + BN_free(x_bn); + x_bn = NULL; + + if (qjs_base64url_bignum_set(cx, jwk, "y", y_bn, group_bytes) < 0) { + goto fail; + } + + BN_free(y_bn); + y_bn = NULL; + + nid = EC_GROUP_get_curve_name(group); + + cname = qjs_algorithm_curve_name(nid); + if (cname->length == 0) { + JS_ThrowTypeError(cx, "Unsupported JWK EC curve: %s", OBJ_nid2sn(nid)); + goto fail; + } + + name = JS_NewStringLen(cx, (char *) cname->start, cname->length); + if (JS_IsException(name)) { + goto fail; + } + + if (JS_DefinePropertyValueStr(cx, jwk, "crv", name, JS_PROP_C_W_E) < 0) { + goto fail; + } + + if (JS_DefinePropertyValueStr(cx, jwk, "kty", JS_NewString(cx, "EC"), + JS_PROP_C_W_E) < 0) + { + goto fail; + } + + if (key->u.a.privat) { + d_bn = EC_KEY_get0_private_key(ec); + + if (qjs_base64url_bignum_set(cx, jwk, "d", d_bn, group_bytes) < 0) { + goto fail; + } + } + + return jwk; + +fail: + + JS_FreeValue(cx, jwk); + + if (x_bn != NULL) { + BN_free(x_bn); + } + + if (y_bn != NULL) { + BN_free(y_bn); + } + + return JS_EXCEPTION; +} + + +static JSValue +qjs_export_jwk_asymmetric(JSContext *cx, qjs_webcrypto_key_t *key) +{ + JSValue jwk, ops; + + njs_assert(key->u.a.pkey != NULL); + + switch (EVP_PKEY_id(key->u.a.pkey)) { + case EVP_PKEY_RSA: +#if (OPENSSL_VERSION_NUMBER >= 0x10101001L) + case EVP_PKEY_RSA_PSS: +#endif + jwk = qjs_export_jwk_rsa(cx, key); + if (JS_IsException(jwk)) { + return JS_EXCEPTION; + } + + break; + + case EVP_PKEY_EC: + jwk = qjs_export_jwk_ec(cx, key); + if (JS_IsException(jwk)) { + return JS_EXCEPTION; + } + + break; + + default: + JS_ThrowTypeError(cx, "provided key cannot be exported as JWK"); + return JS_EXCEPTION; + } + + ops = qjs_key_ops(cx, key->usage); + if (JS_IsException(ops)) { + JS_FreeValue(cx, jwk); + return JS_EXCEPTION; + } + + if (JS_DefinePropertyValueStr(cx, jwk, "key_ops", ops, JS_PROP_C_W_E) < 0) { + JS_FreeValue(cx, jwk); + JS_FreeValue(cx, ops); + return JS_EXCEPTION; + } + + if (JS_DefinePropertyValueStr(cx, jwk, "ext", + JS_NewBool(cx, key->extractable), + JS_PROP_C_W_E) < 0) + { + JS_FreeValue(cx, jwk); + return JS_EXCEPTION; + } + + return jwk; +} + + +static JSValue +qjs_export_jwk_oct(JSContext *cx, qjs_webcrypto_key_t *key) +{ + JSValue val, jwk; + njs_str_t *nm; + qjs_webcrypto_alg_t type; + + njs_assert(key->u.s.raw.start != NULL); + + jwk = JS_NewObject(cx); + if (JS_IsException(jwk)) { + return JS_EXCEPTION; + } + + val = qjs_string_base64url(cx, &key->u.s.raw); + if (JS_IsException(val)) { + goto fail; + } + + if (JS_DefinePropertyValueStr(cx, jwk, "k", val, JS_PROP_C_W_E) < 0) { + JS_FreeValue(cx, val); + goto fail; + } + + type = key->alg->type; + + if (key->alg->type == QJS_ALGORITHM_HMAC) { + nm = &qjs_webcrypto_alg_name[type][key->hash]; + val = JS_NewStringLen(cx, (char *) nm->start, nm->length); + if (JS_IsException(val)) { + goto fail; + } + + } else { + switch (key->u.s.raw.length) { + case 16: + case 24: + case 32: + nm = &qjs_webcrypto_alg_aes_name + [type - QJS_ALGORITHM_AES_GCM][(key->u.s.raw.length - 16) / 8]; + val = JS_NewStringLen(cx, (char *) nm->start, nm->length); + if (JS_IsException(val)) { + goto fail; + } + + break; + + default: + val = JS_UNDEFINED; + break; + } + } + + if (JS_DefinePropertyValueStr(cx, jwk, "alg", val, JS_PROP_C_W_E) < 0) { + JS_FreeValue(cx, val); + goto fail; + } + + val = qjs_key_ops(cx, key->usage); + if (JS_IsException(val)) { + goto fail; + } + + if (JS_DefinePropertyValueStr(cx, jwk, "key_ops", val, JS_PROP_C_W_E) < 0) { + JS_FreeValue(cx, val); + goto fail; + } + + if (JS_DefinePropertyValueStr(cx, jwk, "kty", JS_NewString(cx, "oct"), + JS_PROP_C_W_E) < 0) + { + goto fail; + } + + if (JS_DefinePropertyValueStr(cx, jwk, "ext", + JS_NewBool(cx, key->extractable), + JS_PROP_C_W_E) < 0) + { + goto fail; + } + + return jwk; + +fail: + + JS_FreeValue(cx, jwk); + + return JS_EXCEPTION; + +} + + +static JSValue +qjs_export_raw_ec(JSContext *cx, qjs_webcrypto_key_t *key) +{ + size_t size; + u_char *dst; + const EC_KEY *ec; + const EC_GROUP *group; + const EC_POINT *point; + point_conversion_form_t form; + + njs_assert(key->u.a.pkey != NULL); + + if (key->u.a.privat) { + JS_ThrowTypeError(cx, "private key of \"%s\" cannot be exported " + "in \"raw\" format", qjs_algorithm_string(key->alg)); + return JS_EXCEPTION; + } + + ec = njs_pkey_get_ec_key(key->u.a.pkey); + + group = EC_KEY_get0_group(ec); + point = EC_KEY_get0_public_key(ec); + form = POINT_CONVERSION_UNCOMPRESSED; + + size = EC_POINT_point2oct(group, point, form, NULL, 0, NULL); + if (size == 0) { + qjs_webcrypto_error(cx, "EC_POINT_point2oct() failed"); + return JS_EXCEPTION; + } + + dst = js_malloc(cx, size); + if (dst == NULL) { + JS_ThrowOutOfMemory(cx); + return JS_EXCEPTION; + } + + size = EC_POINT_point2oct(group, point, form, dst, size, NULL); + if (size == 0) { + js_free(cx, dst); + qjs_webcrypto_error(cx, "EC_POINT_point2oct() failed"); + return JS_EXCEPTION; + } + + return qjs_new_array_buffer(cx, dst, size); +} + + +static JSValue +qjs_webcrypto_derive(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv, int derive_key) +{ + int rc; + u_char *k; + size_t olen; + int64_t iterations, length; + JSValue ret, val, aobject, dobject, dkey_value; + unsigned usage, mask; + njs_str_t salt, info; + const EVP_MD *md; + EVP_PKEY_CTX *pctx; + qjs_webcrypto_key_t *key, *dkey; + qjs_webcrypto_hash_t hash; + qjs_webcrypto_algorithm_t *alg, *dalg; + + aobject = argv[0]; + + alg = qjs_key_algorithm(cx, aobject); + if (alg == NULL) { + return JS_EXCEPTION; + } + + key = JS_GetOpaque2(cx, argv[1], QJS_CORE_CLASS_ID_WEBCRYPTO_KEY); + if (key == NULL) { + JS_ThrowTypeError(cx, "\"baseKey\" is not a CryptoKey object"); + return JS_EXCEPTION; + } + + mask = derive_key ? QJS_KEY_USAGE_DERIVE_KEY : QJS_KEY_USAGE_DERIVE_BITS; + if (!(key->usage & mask)) { + JS_ThrowTypeError(cx, "provide key does not support \"%s\" operation", + derive_key ? "deriveKey" : "deriveBits"); + return JS_EXCEPTION; + } + + if (key->alg != alg) { + JS_ThrowTypeError(cx, "cannot derive %s using \"%s\" with \"%s\" key", + derive_key ? "key" : "bits", + qjs_algorithm_string(key->alg), + qjs_algorithm_string(alg)); + return JS_EXCEPTION; + } + + dobject = argv[2]; + + if (derive_key) { + dalg = qjs_key_algorithm(cx, dobject); + if (dalg == NULL) { + return JS_EXCEPTION; + } + + ret = JS_GetPropertyStr(cx, dobject, "length"); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + + if (JS_IsUndefined(ret)) { + JS_ThrowTypeError(cx, "derivedKeyAlgorithm.length is not provided"); + return JS_EXCEPTION; + } + + } else { + dalg = NULL; + ret = JS_DupValue(cx, dobject); + } + + if (JS_ToInt64(cx, &length, ret) < 0) { + return JS_EXCEPTION; + } + + JS_FreeValue(cx, ret); + + dkey = NULL; + length /= 8; + dkey_value = JS_UNDEFINED; + + if (derive_key) { + switch (dalg->type) { + case QJS_ALGORITHM_AES_GCM: + case QJS_ALGORITHM_AES_CTR: + case QJS_ALGORITHM_AES_CBC: + + if (length != 16 && length != 32) { + JS_ThrowTypeError(cx, "deriveKey \"%s\" length must be " + "128 or 256", qjs_algorithm_string(dalg)); + return JS_EXCEPTION; + } + + break; + + default: + JS_ThrowTypeError(cx, "not implemented deriveKey: \"%s\"", + qjs_algorithm_string(dalg)); + return JS_EXCEPTION; + } + + ret = qjs_key_usage(cx, argv[4], &usage); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + + if (usage & ~dalg->usage) { + JS_ThrowTypeError(cx, "unsupported key usage for \"%s\" key", + qjs_algorithm_string(alg)); + return JS_EXCEPTION; + } + + dkey_value = qjs_webcrypto_key_make(cx, dalg, usage, 0); + if (JS_IsException(dkey_value)) { + JS_ThrowOutOfMemory(cx); + return JS_EXCEPTION; + } + + dkey = JS_GetOpaque(dkey_value, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY); + } + + k = js_malloc(cx, length); + if (k == NULL) { + JS_ThrowOutOfMemory(cx); + return JS_EXCEPTION; + } + + switch (alg->type) { + case QJS_ALGORITHM_PBKDF2: + ret = qjs_algorithm_hash(cx, aobject, &hash); + if (JS_IsException(ret)) { + goto fail; + } + + val = JS_GetPropertyStr(cx, aobject, "salt"); + if (JS_IsException(val)) { + goto fail; + } + + ret = qjs_typed_array_data(cx, val, &salt); + JS_FreeValue(cx, val); + if (JS_IsException(ret)) { + goto fail; + } + + if (salt.length < 16) { + JS_ThrowTypeError(cx, "PBKDF2 algorithm.salt must be at least " + "16 bytes long"); + goto fail; + } + + ret = JS_GetPropertyStr(cx, aobject, "iterations"); + if (JS_IsException(ret)) { + goto fail; + } + + if (JS_IsUndefined(ret)) { + JS_ThrowTypeError(cx, "PBKDF2 algorithm.iterations is not provided"); + goto fail; + } + + if (JS_ToInt64(cx, &iterations, ret) < 0) { + goto fail; + } + + JS_FreeValue(cx, ret); + + md = qjs_algorithm_hash_digest(hash); + + rc = PKCS5_PBKDF2_HMAC((char *) key->u.s.raw.start, key->u.s.raw.length, + salt.start, salt.length, iterations, md, length, + k); + if (rc <= 0) { + qjs_webcrypto_error(cx, "PKCS5_PBKDF2_HMAC() failed"); + goto fail; + } + + break; + + case QJS_ALGORITHM_HKDF: +#ifdef EVP_PKEY_HKDF + ret = qjs_algorithm_hash(cx, aobject, &hash); + if (JS_IsException(ret)) { + goto fail; + } + + val = JS_GetPropertyStr(cx, aobject, "salt"); + if (JS_IsException(val)) { + goto fail; + } + + ret = qjs_typed_array_data(cx, val, &salt); + JS_FreeValue(cx, val); + if (JS_IsException(ret)) { + goto fail; + } + + val = JS_GetPropertyStr(cx, aobject, "info"); + if (JS_IsException(val)) { + goto fail; + } + + ret = qjs_typed_array_data(cx, val, &info); + JS_FreeValue(cx, val); + if (JS_IsException(ret)) { + goto fail; + } + + pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL); + if (pctx == NULL) { + qjs_webcrypto_error(cx, "EVP_PKEY_CTX_new_id() failed"); + goto fail; + } + + rc = EVP_PKEY_derive_init(pctx); + if (rc <= 0) { + qjs_webcrypto_error(cx, "EVP_PKEY_derive_init() failed"); + goto free; + } + + md = qjs_algorithm_hash_digest(hash); + + rc = EVP_PKEY_CTX_set_hkdf_md(pctx, md); + if (rc <= 0) { + qjs_webcrypto_error(cx, "EVP_PKEY_CTX_set_hkdf_md() failed"); + goto free; + } + + rc = EVP_PKEY_CTX_set1_hkdf_salt(pctx, salt.start, salt.length); + if (rc <= 0) { + qjs_webcrypto_error(cx, "EVP_PKEY_CTX_set1_hkdf_salt() failed"); + goto free; + } + + rc = EVP_PKEY_CTX_set1_hkdf_key(pctx, key->u.s.raw.start, + key->u.s.raw.length); + if (rc <= 0) { + qjs_webcrypto_error(cx, "EVP_PKEY_CTX_set1_hkdf_key() failed"); + goto free; + } + + rc = EVP_PKEY_CTX_add1_hkdf_info(pctx, info.start, info.length); + if (rc <= 0) { + qjs_webcrypto_error(cx, "EVP_PKEY_CTX_add1_hkdf_info() failed"); + goto free; + } + + olen = (size_t) length; + rc = EVP_PKEY_derive(pctx, k, &olen); + if (rc <= 0 || olen != (size_t) length) { + qjs_webcrypto_error(cx, "EVP_PKEY_derive() failed"); + goto free; + } + +free: + EVP_PKEY_CTX_free(pctx); + + if (rc <= 0) { + goto fail; + } + + break; +#else + (void) pctx; + (void) olen; + (void) &string_info; + (void) &info; +#endif + + case QJS_ALGORITHM_ECDH: + default: + JS_ThrowTypeError(cx, "not implemented deriveKey algorithm: \"%s\"", + qjs_algorithm_string(alg)); + goto fail; + } + + if (derive_key) { + if (dalg->type == QJS_ALGORITHM_HMAC) { + ret = qjs_algorithm_hash(cx, dobject, &dkey->hash); + if (JS_IsException(ret)) { + goto fail; + } + } + + dkey->u.s.raw.start = k; + dkey->u.s.raw.length = length; + + ret = dkey_value; + + } else { + + ret = qjs_new_array_buffer(cx, k, length); + } + + return qjs_webcrypto_result(cx, ret, 0); + +fail: + + JS_FreeValue(cx, dkey_value); + + js_free(cx, k); + + return qjs_webcrypto_result(cx, JS_UNDEFINED, -1); +} + + +static JSValue +qjs_webcrypto_digest(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + JSValue ret; + u_char *dst; + unsigned olen; + njs_str_t data; + const EVP_MD *md; + qjs_webcrypto_hash_t hash; + + ret = qjs_algorithm_hash(cx, argv[0], &hash); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + + ret = qjs_typed_array_data(cx, argv[1], &data); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + + md = qjs_algorithm_hash_digest(hash); + olen = EVP_MD_size(md); + + dst = js_malloc(cx, olen); + if (dst == NULL) { + JS_ThrowOutOfMemory(cx); + return JS_EXCEPTION; + } + + if (EVP_Digest(data.start, data.length, dst, &olen, md, NULL) <= 0) { + js_free(cx, dst); + qjs_webcrypto_error(cx, "EVP_Digest() failed"); + return JS_EXCEPTION; + } + + ret = qjs_new_array_buffer(cx, dst, olen); + + return ret; +} + + +static JSValue +qjs_webcrypto_export_key(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + BIO *bio; + BUF_MEM *mem; + JSValue ret; + qjs_webcrypto_key_t *key; + PKCS8_PRIV_KEY_INFO *pkcs8; + qjs_webcrypto_key_format_t fmt; + + fmt = qjs_key_format(cx, argv[0]); + + key = JS_GetOpaque2(cx, argv[1], QJS_CORE_CLASS_ID_WEBCRYPTO_KEY); + if (key == NULL) { + JS_ThrowTypeError(cx, "\"key\" is not a CryptoKey object"); + return JS_EXCEPTION; + } + + if (!(fmt & key->alg->fmt)) { + JS_ThrowTypeError(cx, "unsupported key fmt \"%s\" for \"%s\" key", + qjs_format_string(fmt), + qjs_algorithm_string(key->alg)); + return JS_EXCEPTION; + } + + if (!key->extractable) { + JS_ThrowTypeError(cx, "provided key cannot be extracted"); + return JS_EXCEPTION; + } + + switch (fmt) { + case QJS_KEY_FORMAT_JWK: + switch (key->alg->type) { + case QJS_ALGORITHM_RSASSA_PKCS1_v1_5: + case QJS_ALGORITHM_RSA_PSS: + case QJS_ALGORITHM_RSA_OAEP: + case QJS_ALGORITHM_ECDSA: + ret = qjs_export_jwk_asymmetric(cx, key); + if (JS_IsException(ret)) { + goto fail; + } + + break; + + case QJS_ALGORITHM_AES_GCM: + case QJS_ALGORITHM_AES_CTR: + case QJS_ALGORITHM_AES_CBC: + case QJS_ALGORITHM_HMAC: + ret = qjs_export_jwk_oct(cx, key); + if (JS_IsException(ret)) { + goto fail; + } + + break; + + default: + JS_ThrowTypeError(cx, "provided key of \"%s\" cannot be exported " + "as JWK", qjs_algorithm_string(key->alg)); + goto fail; + } + + break; + + case QJS_KEY_FORMAT_PKCS8: + if (!key->u.a.privat) { + JS_ThrowTypeError(cx, "public key of \"%s\" cannot be exported " + "as PKCS8", qjs_algorithm_string(key->alg)); + goto fail; + } + + bio = BIO_new(BIO_s_mem()); + if (bio == NULL) { + qjs_webcrypto_error(cx, "BIO_new(BIO_s_mem()) failed"); + goto fail; + } + + njs_assert(key->u.a.pkey != NULL); + + pkcs8 = EVP_PKEY2PKCS8(key->u.a.pkey); + if (pkcs8 == NULL) { + BIO_free(bio); + qjs_webcrypto_error(cx, "EVP_PKEY2PKCS8() failed"); + goto fail; + } + + if (!i2d_PKCS8_PRIV_KEY_INFO_bio(bio, pkcs8)) { + BIO_free(bio); + PKCS8_PRIV_KEY_INFO_free(pkcs8); + qjs_webcrypto_error(cx, "i2d_PKCS8_PRIV_KEY_INFO_bio() failed"); + goto fail; + } + + BIO_get_mem_ptr(bio, &mem); + + ret = JS_NewArrayBufferCopy(cx, (const uint8_t *) mem->data, + mem->length); + + BIO_free(bio); + PKCS8_PRIV_KEY_INFO_free(pkcs8); + + if (JS_IsException(ret)) { + goto fail; + } + + break; + + case QJS_KEY_FORMAT_SPKI: + if (key->u.a.privat) { + JS_ThrowTypeError(cx, "private key of \"%s\" cannot be exported " + "as SPKI", qjs_algorithm_string(key->alg)); + goto fail; + } + + bio = BIO_new(BIO_s_mem()); + if (bio == NULL) { + qjs_webcrypto_error(cx, "BIO_new(BIO_s_mem()) failed"); + goto fail; + } + + njs_assert(key->u.a.pkey != NULL); + + if (!i2d_PUBKEY_bio(bio, key->u.a.pkey)) { + BIO_free(bio); + qjs_webcrypto_error(cx, "i2d_PUBKEY_bio() failed"); + goto fail; + } + + BIO_get_mem_ptr(bio, &mem); + + ret = JS_NewArrayBufferCopy(cx, (const uint8_t *) mem->data, + mem->length); + + BIO_free(bio); + + if (JS_IsException(ret)) { + goto fail; + } + + break; + + case QJS_KEY_FORMAT_RAW: + default: + if (key->alg->type == QJS_ALGORITHM_ECDSA) { + ret = qjs_export_raw_ec(cx, key); + if (JS_IsException(ret)) { + goto fail; + } + + break; + } else { + ret = JS_NewArrayBufferCopy(cx, key->u.s.raw.start, + key->u.s.raw.length); + if (JS_IsException(ret)) { + goto fail; + } + } + + } + + return qjs_webcrypto_result(cx, ret, 0); + +fail: + + return qjs_webcrypto_result(cx, JS_UNDEFINED, -1); +} + + +static JSValue +qjs_webcrypto_generate_key(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + int n, extractable; + JSValue ret, key, keypub, options, obj; + unsigned usage; + EVP_PKEY_CTX *ctx; + qjs_webcrypto_key_t *wkey, *wkeypub; + qjs_webcrypto_algorithm_t *alg; + + ctx = NULL; + options = argv[0]; + key = JS_UNDEFINED; + keypub = JS_UNDEFINED; + + alg = qjs_key_algorithm(cx, options); + if (alg == NULL) { + return JS_EXCEPTION; + } + + ret = qjs_key_usage(cx, argv[2], &usage); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + + if (usage & ~alg->usage) { + JS_ThrowTypeError(cx, "unsupported key usage for \"%s\" key", + qjs_algorithm_string(alg)); + return JS_EXCEPTION; + } + + extractable = JS_ToBool(cx, argv[1]); + key = qjs_webcrypto_key_make(cx, alg, usage, extractable); + if (JS_IsException(key)) { + return JS_EXCEPTION; + } + + wkey = JS_GetOpaque(key, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY); + + switch (alg->type) { + case QJS_ALGORITHM_RSASSA_PKCS1_v1_5: + case QJS_ALGORITHM_RSA_PSS: + case QJS_ALGORITHM_RSA_OAEP: + ret = qjs_algorithm_hash(cx, options, &wkey->hash); + if (JS_IsException(ret)) { + goto fail; + } + + ret = JS_GetPropertyStr(cx, options, "modulusLength"); + if (JS_IsException(ret)) { + goto fail; + } + + if (!JS_IsNumber(ret)) { + JS_FreeValue(cx, ret); + JS_ThrowTypeError(cx, "\"modulusLength\" is not a number"); + goto fail; + } + + if (JS_ToInt32(cx, &n, ret) < 0) { + goto fail; + } + + ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL); + if (ctx == NULL) { + qjs_webcrypto_error(cx, "EVP_PKEY_CTX_new_id() failed"); + goto fail; + } + + if (EVP_PKEY_keygen_init(ctx) <= 0) { + qjs_webcrypto_error(cx, "EVP_PKEY_keygen_init() failed"); + goto fail; + } + + if (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, n) <= 0) { + qjs_webcrypto_error(cx, "EVP_PKEY_CTX_set_rsa_keygen_bits() " + "failed"); + goto fail; + } + + if (EVP_PKEY_keygen(ctx, &wkey->u.a.pkey) <= 0) { + qjs_webcrypto_error(cx, "EVP_PKEY_keygen() failed"); + goto fail; + } + + EVP_PKEY_CTX_free(ctx); + ctx = NULL; + + wkey->u.a.privat = 1; + wkey->usage = (alg->type == QJS_ALGORITHM_RSA_OAEP) + ? QJS_KEY_USAGE_DECRYPT + : QJS_KEY_USAGE_SIGN; + + keypub = qjs_webcrypto_key_make(cx, alg, usage, extractable); + if (JS_IsException(keypub)) { + goto fail; + } + + if (njs_pkey_up_ref(wkey->u.a.pkey) <= 0) { + qjs_webcrypto_error(cx, "qjs_pkey_up_ref() failed"); + goto fail; + } + + wkeypub = JS_GetOpaque(keypub, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY); + + wkeypub->u.a.pkey = wkey->u.a.pkey; + wkeypub->hash = wkey->hash; + wkeypub->usage = (alg->type == QJS_ALGORITHM_RSA_OAEP) + ? QJS_KEY_USAGE_ENCRYPT + : QJS_KEY_USAGE_VERIFY; + + obj = JS_NewObject(cx); + if (JS_IsException(obj)) { + goto fail; + } + + if (JS_SetPropertyStr(cx, obj, "privateKey", key) < 0) { + goto fail; + } + + key = JS_UNDEFINED; + + if (JS_SetPropertyStr(cx, obj, "publicKey", keypub) < 0) { + goto fail; + } + + break; + + case QJS_ALGORITHM_ECDSA: + ret = qjs_algorithm_curve(cx, options, &wkey->u.a.curve); + if (JS_IsException(ret)) { + goto fail; + } + + ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL); + if (ctx == NULL) { + qjs_webcrypto_error(cx, "EVP_PKEY_CTX_new_id() failed"); + goto fail; + } + + if (EVP_PKEY_keygen_init(ctx) <= 0) { + qjs_webcrypto_error(cx, "EVP_PKEY_keygen_init() failed"); + goto fail; + } + + if (EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx, wkey->u.a.curve) <= 0) { + qjs_webcrypto_error(cx, "EVP_PKEY_CTX_set_ec_paramgen_curve_nid() " + "failed"); + goto fail; + } + + if (EVP_PKEY_keygen(ctx, &wkey->u.a.pkey) <= 0) { + qjs_webcrypto_error(cx, "EVP_PKEY_keygen() failed"); + goto fail; + } + + EVP_PKEY_CTX_free(ctx); + ctx = NULL; + + wkey->u.a.privat = 1; + wkey->usage = QJS_KEY_USAGE_SIGN; + + keypub = qjs_webcrypto_key_make(cx, alg, usage, extractable); + if (JS_IsException(keypub)) { + goto fail; + } + + if (njs_pkey_up_ref(wkey->u.a.pkey) <= 0) { + qjs_webcrypto_error(cx, "qjs_pkey_up_ref() failed"); + goto fail; + } + + wkeypub = JS_GetOpaque(keypub, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY); + + wkeypub->u.a.pkey = wkey->u.a.pkey; + wkeypub->u.a.curve = wkey->u.a.curve; + wkeypub->usage = QJS_KEY_USAGE_VERIFY; + + obj = JS_NewObject(cx); + if (JS_IsException(obj)) { + goto fail; + } + + if (JS_SetPropertyStr(cx, obj, "privateKey", key) < 0) { + goto fail; + } + + key = JS_UNDEFINED; + + if (JS_SetPropertyStr(cx, obj, "publicKey", keypub) < 0) { + goto fail; + } + + break; + + case QJS_ALGORITHM_AES_GCM: + case QJS_ALGORITHM_AES_CTR: + case QJS_ALGORITHM_AES_CBC: + case QJS_ALGORITHM_HMAC: + if (alg->type == QJS_ALGORITHM_HMAC) { + ret = qjs_algorithm_hash(cx, options, &wkey->hash); + if (JS_IsException(ret)) { + goto fail; + } + + wkey->u.s.raw.length = + EVP_MD_size(qjs_algorithm_hash_digest(wkey->hash)); + + } else { + ret = JS_GetPropertyStr(cx, options, "length"); + if (JS_IsException(ret)) { + goto fail; + } + + if (!JS_IsNumber(ret)) { + JS_FreeValue(cx, ret); + JS_ThrowTypeError(cx, "length is not a number"); + goto fail; + } + + if (JS_ToInt32(cx, &n, ret) < 0) { + goto fail; + } + + wkey->u.s.raw.length = n / 8; + + if (wkey->u.s.raw.length != 16 + && wkey->u.s.raw.length != 24 + && wkey->u.s.raw.length != 32) + { + JS_ThrowTypeError(cx, "length for \"%s\" key should be " + "one of 128, 192, 256", + qjs_algorithm_string(alg)); + goto fail; + } + } + + wkey->u.s.raw.start = js_malloc(cx, wkey->u.s.raw.length); + if (wkey->u.s.raw.start == NULL) { + JS_ThrowOutOfMemory(cx); + goto fail; + } + + if (RAND_bytes(wkey->u.s.raw.start, wkey->u.s.raw.length) <= 0) { + qjs_webcrypto_error(cx, "RAND_bytes() failed"); + goto fail; + } + + obj = key; + + break; + + default: + JS_ThrowTypeError(cx, "not implemented generateKey algorithm: \"%s\"", + qjs_algorithm_string(alg)); + goto fail; + } + + return qjs_webcrypto_result(cx, obj, 0); + +fail: + + if (ctx != NULL) { + EVP_PKEY_CTX_free(ctx); + } + + JS_FreeValue(cx, key); + JS_FreeValue(cx, keypub); + + return qjs_webcrypto_result(cx, JS_UNDEFINED, -1); +} + + +static BIGNUM * +qjs_import_base64url_bignum(JSContext *cx, JSValue value) +{ + BIGNUM *bn; + njs_str_t data, decoded; + u_char buf[512]; + + data.start = (u_char *) JS_ToCStringLen(cx, &data.length, value); + if (data.start == NULL) { + JS_ThrowOutOfMemory(cx); + return NULL; + } + + decoded.length = qjs_base64url_decode_length(cx, &data); + + if (decoded.length > sizeof(buf)) { + JS_ThrowRangeError(cx, "JWK key too long: %zu > 512", decoded.length); + return NULL; + } + + decoded.start = buf; + + qjs_base64url_decode(cx, &data, &decoded); + + bn = BN_bin2bn(decoded.start, decoded.length, NULL); + JS_FreeCString(cx, (char *) data.start); + + return bn; +} + + +static EVP_PKEY * +qjs_import_jwk_rsa(JSContext *cx, JSValue jwk, qjs_webcrypto_key_t *key) +{ + RSA *rsa; + BIGNUM *n_bn, *e_bn, *d_bn, *p_bn, *q_bn, *dp_bn, *dq_bn, + *qi_bn; + JSValue ret, val, n, e, d, p, q, dp, dq, qi; + unsigned usage; + EVP_PKEY *pkey; + njs_str_t alg; + qjs_webcrypto_entry_t *w; + + e = JS_UNDEFINED; + d = JS_UNDEFINED; + + n = JS_GetPropertyStr(cx, jwk, "n"); + if (JS_IsException(n)) { + goto fail0; + } + + e = JS_GetPropertyStr(cx, jwk, "e"); + if (JS_IsException(e)) { + goto fail0; + } + + d = JS_GetPropertyStr(cx, jwk, "d"); + if (JS_IsException(d)) { + goto fail0; + } + + if (!JS_IsString(n) + || !JS_IsString(e) + || (!JS_IsUndefined(d) && !JS_IsString(d))) + { +fail0: + JS_FreeValue(cx, n); + JS_FreeValue(cx, e); + JS_FreeValue(cx, d); + JS_ThrowTypeError(cx, "Invalid JWK RSA key"); + return NULL; + } + + key->u.a.privat = JS_IsString(d); + + val = JS_GetPropertyStr(cx, jwk, "key_ops"); + if (!JS_IsException(val) && !JS_IsUndefined(val)) { + ret = qjs_key_usage(cx, val, &usage); + JS_FreeValue(cx, val); + if (JS_IsException(ret)) { + goto fail0; + } + + if ((key->usage & usage) != key->usage) { + JS_ThrowTypeError(cx, "Key operations and usage mismatch"); + goto fail0; + } + } + + ret = JS_GetPropertyStr(cx, jwk, "alg"); + if (!JS_IsException(ret) && !JS_IsUndefined(ret)) { + alg.start = (u_char *) JS_ToCStringLen(cx, &alg.length, ret); + JS_FreeValue(cx, ret); + if (alg.start == NULL) { + JS_ThrowOutOfMemory(cx); + goto fail0; + } + + for (w = &qjs_webcrypto_alg_hash[0]; w->name.length != 0; w++) { + if (njs_strstr_eq(&alg, &w->name)) { + key->hash = w->value; + break; + } + } + + JS_FreeCString(cx, (char *) alg.start); + } + + if (key->extractable) { + ret = JS_GetPropertyStr(cx, jwk, "ext"); + if (!JS_IsException(ret) + && !JS_IsUndefined(ret) + && !JS_ToBool(cx, ret)) + { + JS_FreeValue(cx, ret); + JS_ThrowTypeError(cx, "JWK RSA is not extractable"); + goto fail0; + } + + JS_FreeValue(cx, ret); + } + + rsa = RSA_new(); + if (rsa == NULL) { + qjs_webcrypto_error(cx, "RSA_new() failed"); + goto fail0; + } + + p = JS_UNDEFINED; + q = JS_UNDEFINED; + dp = JS_UNDEFINED; + dq = JS_UNDEFINED; + qi = JS_UNDEFINED; + + n_bn = qjs_import_base64url_bignum(cx, n); + if (n_bn == NULL) { + goto fail; + } + + e_bn = qjs_import_base64url_bignum(cx, e); + if (e_bn == NULL) { + BN_free(n_bn); + goto fail; + } + + if (!njs_rsa_set0_key(rsa, n_bn, e_bn, NULL)) { + BN_free(n_bn); + BN_free(e_bn); + qjs_webcrypto_error(cx, "RSA_set0_key() failed"); + goto fail; + } + + if (!key->u.a.privat) { + goto done; + } + + p = JS_GetPropertyStr(cx, jwk, "p"); + if (JS_IsException(p)) { + goto fail1; + } + + q = JS_GetPropertyStr(cx, jwk, "q"); + if (JS_IsException(q)) { + goto fail1; + } + + dp = JS_GetPropertyStr(cx, jwk, "dp"); + if (JS_IsException(dp)) { + goto fail1; + } + + dq = JS_GetPropertyStr(cx, jwk, "dq"); + if (JS_IsException(dq)) { + goto fail1; + } + + qi = JS_GetPropertyStr(cx, jwk, "qi"); + if (JS_IsException(qi)) { + goto fail1; + } + + if (!JS_IsString(d) + || !JS_IsString(p) + || !JS_IsString(q) + || !JS_IsString(dp) + || !JS_IsString(dq) + || !JS_IsString(qi)) + { +fail1: + JS_ThrowTypeError(cx, "Invalid JWK RSA key"); + goto fail; + } + + d_bn = qjs_import_base64url_bignum(cx, d); + if (d_bn == NULL) { + goto fail; + } + + if (!njs_rsa_set0_key(rsa, NULL, NULL, d_bn)) { + BN_free(d_bn); + qjs_webcrypto_error(cx, "RSA_set0_key() failed"); + goto fail; + } + + p_bn = qjs_import_base64url_bignum(cx, p); + if (p_bn == NULL) { + goto fail; + } + + q_bn = qjs_import_base64url_bignum(cx, q); + if (q_bn == NULL) { + BN_free(p_bn); + goto fail; + } + + if (!njs_rsa_set0_factors(rsa, p_bn, q_bn)) { + BN_free(p_bn); + BN_free(q_bn); + + qjs_webcrypto_error(cx, "RSA_set0_factors() failed"); + goto fail; + } + + dp_bn = qjs_import_base64url_bignum(cx, dp); + if (dp_bn == NULL) { + goto fail; + } + + dq_bn = qjs_import_base64url_bignum(cx, dq); + if (dq_bn == NULL) { + BN_free(dp_bn); + goto fail; + } + + qi_bn = qjs_import_base64url_bignum(cx, qi); + if (qi_bn == NULL) { + BN_free(dp_bn); + BN_free(dq_bn); + goto fail; + } + + if (!njs_rsa_set0_ctr_params(rsa, dp_bn, dq_bn, qi_bn)) { + BN_free(dp_bn); + BN_free(dq_bn); + BN_free(qi_bn); + qjs_webcrypto_error(cx, "RSA_set0_crt_params() failed"); + goto fail; + } + + JS_FreeValue(cx, p); + JS_FreeValue(cx, q); + JS_FreeValue(cx, dp); + JS_FreeValue(cx, dq); + JS_FreeValue(cx, qi); + +done: + + JS_FreeValue(cx, n); + JS_FreeValue(cx, e); + JS_FreeValue(cx, d); + + pkey = EVP_PKEY_new(); + if (pkey == NULL) { + goto fail; + } + + if (!EVP_PKEY_set1_RSA(pkey, rsa)) { + EVP_PKEY_free(pkey); + goto fail; + } + + RSA_free(rsa); + + return pkey; + +fail: + + JS_FreeValue(cx, n); + JS_FreeValue(cx, e); + JS_FreeValue(cx, d); + JS_FreeValue(cx, p); + JS_FreeValue(cx, q); + JS_FreeValue(cx, dp); + JS_FreeValue(cx, dq); + JS_FreeValue(cx, qi); + + RSA_free(rsa); + + return NULL; +} + + +static EVP_PKEY * +qjs_import_jwk_ec(JSContext *cx, JSValue jwk, qjs_webcrypto_key_t *key) +{ + int curve; + EC_KEY *ec; + BIGNUM *x_bn, *y_bn, *d_bn; + JSValue ret, val, x, y, d; + unsigned usage; + EVP_PKEY *pkey; + njs_str_t name; + qjs_webcrypto_entry_t *e; + + x = JS_UNDEFINED; + y = JS_UNDEFINED; + d = JS_UNDEFINED; + + x = JS_GetPropertyStr(cx, jwk, "x"); + if (JS_IsException(x)) { + goto fail0; + } + + y = JS_GetPropertyStr(cx, jwk, "y"); + if (JS_IsException(y)) { + goto fail0; + } + + d = JS_GetPropertyStr(cx, jwk, "d"); + if (JS_IsException(d)) { + goto fail0; + } + + if (!JS_IsString(x) + || !JS_IsString(y) + || (!JS_IsUndefined(d) && !JS_IsString(d))) + { +fail0: + JS_FreeValue(cx, x); + JS_FreeValue(cx, y); + JS_FreeValue(cx, d); + JS_ThrowTypeError(cx, "Invalid JWK EC key"); + return NULL; + } + + key->u.a.privat = JS_IsString(d); + + val = JS_GetPropertyStr(cx, jwk, "key_ops"); + if (!JS_IsException(val) && !JS_IsUndefined(val)) { + ret = qjs_key_usage(cx, val, &usage); + JS_FreeValue(cx, val); + if (JS_IsException(ret)) { + goto fail0; + } + + if ((key->usage & usage) != key->usage) { + JS_ThrowTypeError(cx, "Key operations and usage mismatch"); + goto fail0; + } + } + + if (key->extractable) { + ret = JS_GetPropertyStr(cx, jwk, "ext"); + if (!JS_IsException(ret) + && !JS_IsUndefined(ret) + && !JS_ToBool(cx, ret)) + { + JS_FreeValue(cx, ret); + JS_ThrowTypeError(cx, "JWK EC is not extractable"); + goto fail0; + } + + JS_FreeValue(cx, ret); + } + + curve = 0; + + ret = JS_GetPropertyStr(cx, jwk, "crv"); + if (!JS_IsException(ret) && !JS_IsUndefined(ret)) { + name.start = (u_char *) JS_ToCStringLen(cx, &name.length, ret); + JS_FreeValue(cx, ret); + if (name.start == NULL) { + JS_ThrowOutOfMemory(cx); + goto fail0; + } + + for (e = &qjs_webcrypto_curve[0]; e->name.length != 0; e++) { + if (njs_strstr_eq(&name, &e->name)) { + curve = e->value; + break; + } + } + + JS_FreeCString(cx, (char *) name.start); + } + + if (curve != key->u.a.curve) { + JS_ThrowTypeError(cx, "JWK EC curve mismatch"); + goto fail0; + } + + ec = EC_KEY_new_by_curve_name(key->u.a.curve); + if (ec == NULL) { + qjs_webcrypto_error(cx, "EC_KEY_new_by_curve_name() failed"); + goto fail0; + } + + y_bn = NULL; + d_bn = NULL; + + x_bn = qjs_import_base64url_bignum(cx, x); + if (x_bn == NULL) { + goto fail; + } + + y_bn = qjs_import_base64url_bignum(cx, y); + if (y_bn == NULL) { + goto fail; + } + + if (!EC_KEY_set_public_key_affine_coordinates(ec, x_bn, y_bn)) { + qjs_webcrypto_error(cx, "EC_KEY_set_public_key_affine_coordinates() " + "failed"); + goto fail; + } + + BN_free(x_bn); + x_bn = NULL; + + BN_free(y_bn); + y_bn = NULL; + + if (key->u.a.privat) { + d_bn = qjs_import_base64url_bignum(cx, d); + if (d_bn == NULL) { + goto fail; + } + + if (!EC_KEY_set_private_key(ec, d_bn)) { + qjs_webcrypto_error(cx, "EC_KEY_set_private_key() failed"); + goto fail; + } + + BN_free(d_bn); + d_bn = NULL; + } + + pkey = EVP_PKEY_new(); + if (pkey == NULL) { + goto fail; + } + + if (!EVP_PKEY_set1_EC_KEY(pkey, ec)) { + qjs_webcrypto_error(cx, "EVP_PKEY_set1_EC_KEY() failed"); + goto fail_pkey; + } + + EC_KEY_free(ec); + + JS_FreeValue(cx, x); + JS_FreeValue(cx, y); + JS_FreeValue(cx, d); + + return pkey; + +fail_pkey: + + EVP_PKEY_free(pkey); + +fail: + + EC_KEY_free(ec); + + if (x_bn != NULL) { + BN_free(x_bn); + } + + if (y_bn != NULL) { + BN_free(y_bn); + } + + if (d_bn != NULL) { + BN_free(d_bn); + } + + JS_FreeValue(cx, x); + JS_FreeValue(cx, y); + JS_FreeValue(cx, d); + + return NULL; +} + + +static EVP_PKEY * +qjs_import_raw_ec(JSContext *cx, njs_str_t *data, qjs_webcrypto_key_t *key) +{ + EC_KEY *ec; + EVP_PKEY *pkey; + EC_POINT *pub; + const EC_GROUP *group; + + ec = EC_KEY_new_by_curve_name(key->u.a.curve); + if (ec == NULL) { + qjs_webcrypto_error(cx, "EC_KEY_new_by_curve_name() failed"); + return NULL; + } + + group = EC_KEY_get0_group(ec); + + pub = EC_POINT_new(group); + if (pub == NULL) { + EC_KEY_free(ec); + qjs_webcrypto_error(cx, "EC_POINT_new() failed"); + return NULL; + } + + if (!EC_POINT_oct2point(group, pub, data->start, data->length, NULL)) { + EC_KEY_free(ec); + EC_POINT_free(pub); + qjs_webcrypto_error(cx, "EC_POINT_oct2point() failed"); + return NULL; + } + + if (!EC_KEY_set_public_key(ec, pub)) { + EC_KEY_free(ec); + EC_POINT_free(pub); + qjs_webcrypto_error(cx, "EC_KEY_set_public_key() failed"); + return NULL; + } + + pkey = EVP_PKEY_new(); + if (pkey == NULL) { + EC_KEY_free(ec); + EC_POINT_free(pub); + qjs_webcrypto_error(cx, "EVP_PKEY_new() failed"); + return NULL; + } + + if (!EVP_PKEY_set1_EC_KEY(pkey, ec)) { + EC_KEY_free(ec); + EC_POINT_free(pub); + EVP_PKEY_free(pkey); + qjs_webcrypto_error(cx, "EVP_PKEY_set1_EC_KEY() failed"); + return NULL; + } + + EC_KEY_free(ec); + EC_POINT_free(pub); + + return pkey; +} + + +static JSValue +qjs_import_jwk_oct(JSContext *cx, JSValue jwk, qjs_webcrypto_key_t *key) +{ + size_t size; + unsigned usage; + JSValue val, ret; + njs_str_t *a, alg, b64; + qjs_webcrypto_alg_t type; + qjs_webcrypto_entry_t *w; + + static qjs_webcrypto_entry_t hashes[] = { + { njs_str("HS1"), QJS_HASH_SHA1 }, + { njs_str("HS256"), QJS_HASH_SHA256 }, + { njs_str("HS384"), QJS_HASH_SHA384 }, + { njs_str("HS512"), QJS_HASH_SHA512 }, + { njs_null_str, 0 } + }; + + val = JS_GetPropertyStr(cx, jwk, "k"); + if (JS_IsException(val) || !JS_IsString(val)) { + JS_ThrowTypeError(cx, "Invalid JWK oct key"); + return JS_EXCEPTION; + } + + b64.start = (u_char *) JS_ToCStringLen(cx, &b64.length, val); + JS_FreeValue(cx, val); + if (b64.start == NULL) { + JS_ThrowOutOfMemory(cx); + return JS_EXCEPTION; + } + + size = qjs_base64url_decode_length(cx, &b64); + + key->u.s.raw.length = size; + key->u.s.raw.start = js_malloc(cx, size); + if (key->u.s.raw.start == NULL) { + JS_FreeCString(cx, (char *) b64.start); + JS_ThrowOutOfMemory(cx); + return JS_EXCEPTION; + } + + qjs_base64url_decode(cx, &b64, &key->u.s.raw); + JS_FreeCString(cx, (char *) b64.start); + + size = 16; + + val = JS_GetPropertyStr(cx, jwk, "alg"); + if (JS_IsException(val)) { + return JS_EXCEPTION; + } + + if (JS_IsString(val)) { + alg.start = (u_char *) JS_ToCStringLen(cx, &alg.length, val); + JS_FreeValue(cx, val); + val = JS_UNDEFINED; + + if (alg.start == NULL) { + JS_ThrowOutOfMemory(cx); + return JS_EXCEPTION; + } + + if (key->alg->type == QJS_ALGORITHM_HMAC) { + for (w = &hashes[0]; w->name.length != 0; w++) { + if (njs_strstr_eq(&alg, &w->name)) { + key->hash = w->value; + goto done; + } + } + + } else { + type = key->alg->type; + a = &qjs_webcrypto_alg_aes_name[type - QJS_ALGORITHM_AES_GCM][0]; + for (; a->length != 0; a++) { + if (njs_strstr_eq(&alg, a)) { + goto done; + } + + size += 8; + } + } + + JS_ThrowTypeError(cx, "unexpected \"alg\" value \"%s\" for JWK key", + alg.start); + JS_FreeCString(cx, (char *) alg.start); + return JS_EXCEPTION; + } + + JS_FreeValue(cx, val); + +done: + + if (key->alg->type != QJS_ALGORITHM_HMAC) { + if (key->u.s.raw.length != size) { + JS_ThrowTypeError(cx, "key size and \"alg\" value \"%s\" mismatch", + alg.start); + JS_FreeCString(cx, (char *) alg.start); + return JS_EXCEPTION; + } + } + + JS_FreeCString(cx, (char *) alg.start); + + val = JS_GetPropertyStr(cx, jwk, "key_ops"); + if (!JS_IsException(val) && !JS_IsUndefined(val)) { + ret = qjs_key_usage(cx, val, &usage); + JS_FreeValue(cx, val); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + + if ((key->usage & usage) != key->usage) { + JS_ThrowTypeError(cx, "Key operations and usage mismatch"); + return JS_EXCEPTION; + } + } + + if (key->extractable) { + val = JS_GetPropertyStr(cx, jwk, "ext"); + if (!JS_IsException(val) + && !JS_IsUndefined(val) + && !JS_ToBool(cx, val)) + { + JS_FreeValue(cx, val); + JS_ThrowTypeError(cx, "JWK oct is not extractable"); + return JS_EXCEPTION; + } + + JS_FreeValue(cx, val); + } + + return JS_UNDEFINED; +} + + +static JSValue +qjs_webcrypto_import_key(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + int nid; + BIO *bio; +#if (OPENSSL_VERSION_NUMBER < 0x30000000L) + RSA *rsa; + EC_KEY *ec; +#else + char gname[80]; +#endif + JSValue options, key, jwk, val, ret; + unsigned mask, usage; + EVP_PKEY *pkey; + njs_str_t key_data; + const u_char *start; +#if (OPENSSL_VERSION_NUMBER < 0x30000000L) + const EC_GROUP *group; +#endif + qjs_webcrypto_key_t *wkey; + PKCS8_PRIV_KEY_INFO *pkcs8; + qjs_webcrypto_hash_t hash; + qjs_webcrypto_jwk_kty_t kty; + qjs_webcrypto_algorithm_t *alg; + qjs_webcrypto_key_format_t fmt; + + pkey = NULL; + key_data.start = NULL; + key_data.length = 0; + + fmt = qjs_key_format(cx, argv[0]); + if (fmt == QJS_KEY_FORMAT_UNKNOWN) { + return JS_EXCEPTION; + } + + options = argv[2]; + + alg = qjs_key_algorithm(cx, options); + if (alg == NULL) { + return JS_EXCEPTION; + } + + if (!(fmt & alg->fmt)) { + JS_ThrowTypeError(cx, "unsupported key fmt \"%s\" for \"%s\" key", + qjs_format_string(fmt), qjs_algorithm_string(alg)); + return JS_EXCEPTION; + } + + ret = qjs_key_usage(cx, argv[4], &usage); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + + if (usage & ~alg->usage) { + JS_ThrowTypeError(cx, "unsupported key usage for \"%s\" key", + qjs_algorithm_string(alg)); + return JS_EXCEPTION; + } + + if (fmt != QJS_KEY_FORMAT_JWK) { + ret = qjs_typed_array_data(cx, argv[1], &key_data); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + } + + key = qjs_webcrypto_key_make(cx, alg, usage, JS_ToBool(cx, argv[3])); + if (JS_IsException(key)) { + return JS_EXCEPTION; + } + + wkey = JS_GetOpaque(key, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY); + + /* + * set by qjs_webcrypto_key_make(): + * + * key->u.a.pkey = NULL; + * key->u.s.raw.length = 0; + * key->u.s.raw.start = NULL; + * key->u.a.curve = 0; + * key->u.a.privat = 0; + * key->hash = QJS_HASH_UNSET; + */ + + switch (fmt) { + case QJS_KEY_FORMAT_PKCS8: + bio = BIO_new_mem_buf(key_data.start, key_data.length); + if (bio == NULL) { + JS_ThrowTypeError(cx, "BIO_new_mem_buf() failed"); + goto fail; + } + + pkcs8 = d2i_PKCS8_PRIV_KEY_INFO_bio(bio, NULL); + if (pkcs8 == NULL) { + BIO_free(bio); + JS_ThrowTypeError(cx, "d2i_PKCS8_PRIV_KEY_INFO_bio() failed"); + goto fail; + } + + pkey = EVP_PKCS82PKEY(pkcs8); + if (pkey == NULL) { + PKCS8_PRIV_KEY_INFO_free(pkcs8); + BIO_free(bio); + JS_ThrowTypeError(cx, "EVP_PKCS82PKEY() failed"); + goto fail; + } + + PKCS8_PRIV_KEY_INFO_free(pkcs8); + BIO_free(bio); + + wkey->u.a.privat = 1; + + break; + + case QJS_KEY_FORMAT_SPKI: + start = key_data.start; + pkey = d2i_PUBKEY(NULL, &start, key_data.length); + if (pkey == NULL) { + JS_ThrowTypeError(cx, "d2i_PUBKEY() failed"); + goto fail; + } + + break; + + case QJS_KEY_FORMAT_JWK: + jwk = argv[1]; + if (!JS_IsObject(jwk)) { + JS_ThrowTypeError(cx, "invalid JWK key data: object value " + "expected"); + goto fail; + } + + val = JS_GetPropertyStr(cx, jwk, "kty"); + if (JS_IsException(val)) { + goto fail; + } + + kty = qjs_jwk_kty(cx, val); + JS_FreeValue(cx, val); + if (kty == QJS_KEY_JWK_KTY_UNKNOWN) { + goto fail; + } + + switch (kty) { + case QJS_KEY_JWK_KTY_RSA: + pkey = qjs_import_jwk_rsa(cx, jwk, wkey); + if (pkey == NULL) { + goto fail; + } + + break; + + case QJS_KEY_JWK_KTY_EC: + ret = qjs_algorithm_curve(cx, options, &wkey->u.a.curve); + if (JS_IsException(ret)) { + goto fail; + } + + pkey = qjs_import_jwk_ec(cx, jwk, wkey); + if (pkey == NULL) { + goto fail; + } + + break; + + case QJS_KEY_JWK_KTY_OCT: + default: + ret = qjs_import_jwk_oct(cx, jwk, wkey); + if (JS_IsException(ret)) { + goto fail; + } + } + + break; + + case QJS_KEY_FORMAT_RAW: + default: + break; + } + + switch (alg->type) { + case QJS_ALGORITHM_RSA_OAEP: + case QJS_ALGORITHM_RSA_PSS: + case QJS_ALGORITHM_RSASSA_PKCS1_v1_5: + +#if (OPENSSL_VERSION_NUMBER < 0x30000000L) + + rsa = EVP_PKEY_get1_RSA(pkey); + if (rsa == NULL) { + qjs_webcrypto_error(cx, "RSA key is not found"); + goto fail; + } + + RSA_free(rsa); + +#else + if (!EVP_PKEY_is_a(pkey, "RSA")) { + qjs_webcrypto_error(cx, "RSA key is not found"); + goto fail; + } +#endif + + ret = qjs_algorithm_hash(cx, options, &hash); + if (JS_IsException(ret)) { + goto fail; + } + + if (wkey->hash != QJS_HASH_UNSET && wkey->hash != hash) { + JS_ThrowTypeError(cx, "RSA JWK hash mismatch"); + goto fail; + } + + if (wkey->u.a.privat) { + mask = (alg->type == QJS_ALGORITHM_RSA_OAEP) + ? ~(QJS_KEY_USAGE_DECRYPT | QJS_KEY_USAGE_UNWRAP_KEY) + : ~(QJS_KEY_USAGE_SIGN); + } else { + mask = (alg->type == QJS_ALGORITHM_RSA_OAEP) + ? ~(QJS_KEY_USAGE_ENCRYPT | QJS_KEY_USAGE_WRAP_KEY) + : ~(QJS_KEY_USAGE_VERIFY); + } + + if (wkey->usage & mask) { + JS_ThrowTypeError(cx, "key usage mismatch for \"%s\" key", + qjs_algorithm_string(alg)); + goto fail; + } + + wkey->hash = hash; + wkey->u.a.pkey = pkey; + + 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; + } + + if (fmt == QJS_KEY_FORMAT_RAW) { + pkey = qjs_import_raw_ec(cx, &key_data, wkey); + if (pkey == NULL) { + goto fail; + } + } + +#if (OPENSSL_VERSION_NUMBER < 0x30000000L) + ec = EVP_PKEY_get1_EC_KEY(pkey); + if (ec == NULL) { + qjs_webcrypto_error(cx, "EC key is not found"); + goto fail; + } + + group = EC_KEY_get0_group(ec); + nid = EC_GROUP_get_curve_name(group); + EC_KEY_free(ec); +#else + if (!EVP_PKEY_is_a(pkey, "EC")) { + qjs_webcrypto_error(cx, "EC key is not found"); + goto fail; + } + + if (EVP_PKEY_get_group_name(pkey, gname, sizeof(gname), NULL) != 1) { + qjs_webcrypto_error(cx, "EVP_PKEY_get_group_name() failed"); + goto fail; + } + + nid = OBJ_txt2nid(gname); +#endif + + if (wkey->u.a.curve != nid) { + qjs_webcrypto_error(cx, "name curve mismatch"); + goto fail; + } + + mask = wkey->u.a.privat ? ~QJS_KEY_USAGE_SIGN : ~QJS_KEY_USAGE_VERIFY; + + if (wkey->usage & mask) { + JS_ThrowTypeError(cx, "key usage mismatch for \"%s\" key", + qjs_algorithm_string(alg)); + goto fail; + } + + wkey->u.a.pkey = pkey; + break; + + case QJS_ALGORITHM_HMAC: + if (fmt == QJS_KEY_FORMAT_RAW) { + ret = qjs_algorithm_hash(cx, options, &wkey->hash); + if (JS_IsException(ret)) { + goto fail; + } + + wkey->u.s.raw.start = js_malloc(cx, key_data.length); + if (wkey->u.s.raw.start == NULL) { + JS_ThrowOutOfMemory(cx); + goto fail; + } + + wkey->u.s.raw.length = key_data.length; + memcpy(wkey->u.s.raw.start, key_data.start, key_data.length); + + } else { + /* QJS_KEY_FORMAT_JWK. */ + + ret = qjs_algorithm_hash(cx, options, &hash); + if (JS_IsException(ret)) { + goto fail; + } + + if (wkey->hash != QJS_HASH_UNSET && wkey->hash != hash) { + JS_ThrowTypeError(cx, "HMAC JWK hash mismatch"); + goto fail; + } + } + + break; + + case QJS_ALGORITHM_AES_GCM: + case QJS_ALGORITHM_AES_CTR: + case QJS_ALGORITHM_AES_CBC: + if (fmt == QJS_KEY_FORMAT_RAW) { + switch (key_data.length) { + case 16: + case 24: + case 32: + break; + + default: + JS_ThrowTypeError(cx, "AES Invalid key length"); + goto fail; + } + + wkey->u.s.raw.start = js_malloc(cx, key_data.length); + if (wkey->u.s.raw.start == NULL) { + JS_ThrowOutOfMemory(cx); + goto fail; + } + + wkey->u.s.raw.length = key_data.length; + memcpy(wkey->u.s.raw.start, key_data.start, key_data.length); + } + + break; + + case QJS_ALGORITHM_PBKDF2: + case QJS_ALGORITHM_HKDF: + default: + wkey->u.s.raw.start = js_malloc(cx, key_data.length); + if (wkey->u.s.raw.start == NULL) { + JS_ThrowOutOfMemory(cx); + goto fail; + } + + wkey->u.s.raw.length = key_data.length; + memcpy(wkey->u.s.raw.start, key_data.start, key_data.length); + break; + } + + return qjs_webcrypto_result(cx, key, 0); + +fail: + + if (pkey != NULL) { + EVP_PKEY_free(pkey); + } + + JS_FreeValue(cx, key); + + return qjs_webcrypto_result(cx, JS_UNDEFINED, -1); +} + + +static int +qjs_set_rsa_padding(JSContext *cx, JSValue options, EVP_PKEY *pkey, + EVP_PKEY_CTX *ctx, qjs_webcrypto_alg_t type) +{ + int padding, rc; + int64_t salt_length; + JSValue value; + + if (type == QJS_ALGORITHM_ECDSA) { + return 0; + } + + padding = (type == QJS_ALGORITHM_RSA_PSS) ? RSA_PKCS1_PSS_PADDING + : RSA_PKCS1_PADDING; + rc = EVP_PKEY_CTX_set_rsa_padding(ctx, padding); + if (rc <= 0) { + qjs_webcrypto_error(cx, "EVP_PKEY_CTX_set_rsa_padding() failed"); + return -1; + } + + if (padding == RSA_PKCS1_PSS_PADDING) { + value = JS_GetPropertyStr(cx, options, "saltLength"); + if (JS_IsException(value)) { + return -1; + } + + if (JS_IsUndefined(value)) { + JS_ThrowTypeError(cx, "RSA-PSS algorithm.saltLength is not " + "provided"); + return -1; + } + + rc = JS_ToInt64(cx, &salt_length, value); + JS_FreeValue(cx, value); + if (rc < 0) { + return -1; + } + + rc = EVP_PKEY_CTX_set_rsa_pss_saltlen(ctx, salt_length); + if (rc <= 0) { + qjs_webcrypto_error(cx, + "EVP_PKEY_CTX_set_rsa_pss_saltlen() failed"); + return -1; + } + } + + return 0; +} + + +static unsigned int +qjs_ec_rs_size(EVP_PKEY *pkey) +{ + int bits; + const EC_KEY *ec_key; + const EC_GROUP *ec_group; + + ec_key = njs_pkey_get_ec_key(pkey); + if (ec_key == NULL) { + return 0; + } + + ec_group = EC_KEY_get0_group(ec_key); + if (ec_group == NULL) { + return 0; + } + + bits = njs_ec_group_order_bits(ec_group); + if (bits == 0) { + return 0; + } + + return (bits + 7) / 8; +} + + +static int +qjs_convert_der_to_p1363(JSContext *cx, EVP_PKEY *pkey, const u_char *der, + size_t der_len, u_char **pout, size_t *out_len) +{ + u_char *data; + unsigned n; + ECDSA_SIG *ec_sig; + const BIGNUM *r, *s; + + ec_sig = NULL; + + n = qjs_ec_rs_size(pkey); + if (n == 0) { + return -1; + } + + data = js_malloc(cx, 2 * n); + if (data == NULL) { + JS_ThrowOutOfMemory(cx); + return -1; + } + + ec_sig = d2i_ECDSA_SIG(NULL, &der, der_len); + if (ec_sig == NULL) { + goto fail; + } + +#if (OPENSSL_VERSION_NUMBER >= 0x10100000L) + ECDSA_SIG_get0(ec_sig, &r, &s); +#else + r = ec_sig->r; + s = ec_sig->s; +#endif + + if (BN_bn2binpad(r, data, n) <= 0) { + goto fail; + } + + if (BN_bn2binpad(s, &data[n], n) <= 0) { + goto fail; + } + + *pout = data; + *out_len = 2 * n; + + ECDSA_SIG_free(ec_sig); + + return 0; + +fail: + + js_free(cx, data); + + if (ec_sig != NULL) { + ECDSA_SIG_free(ec_sig); + } + + return -1; +} + + +static int +qjs_convert_p1363_to_der(JSContext *cx, EVP_PKEY *pkey, u_char *p1363, + size_t p1363_len, u_char **pout, size_t *out_len) +{ + int len; + BIGNUM *r, *s; + u_char *data; + unsigned n; + ECDSA_SIG *ec_sig; + + n = qjs_ec_rs_size(pkey); + + if (n == 0 || p1363_len != 2 * n) { + JS_ThrowTypeError(cx, "invalid ECDSA signature length %zu != %u", + p1363_len, 2 * n); + return -1; + } + + ec_sig = ECDSA_SIG_new(); + if (ec_sig == NULL) { + JS_ThrowOutOfMemory(cx); + return -1; + } + + r = BN_bin2bn(p1363, n, NULL); + if (r == NULL) { + JS_ThrowOutOfMemory(cx); + goto fail; + } + + s = BN_bin2bn(&p1363[n], n, NULL); + if (s == NULL) { + BN_free(r); + JS_ThrowOutOfMemory(cx); + goto fail; + } + + if (ECDSA_SIG_set0(ec_sig, r, s) != 1) { + BN_free(r); + BN_free(s); + JS_ThrowOutOfMemory(cx); + goto fail; + } + + data = js_malloc(cx, 2 * n + 16); + if (data == NULL) { + JS_ThrowOutOfMemory(cx); + goto fail; + } + + *pout = data; + len = i2d_ECDSA_SIG(ec_sig, &data); + + if (len < 0) { + js_free(cx, data); + JS_ThrowTypeError(cx, "i2d_ECDSA_SIG() failed"); + goto fail; + } + + *out_len = len; + + ECDSA_SIG_free(ec_sig); + + return 0; + +fail: + + ECDSA_SIG_free(ec_sig); + + return -1; +} + + +static JSValue +qjs_webcrypto_sign(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv, int verify) +{ + int rc; + u_char *dst, *p, *p1363; + size_t olen, outlen; + JSValue ret, options; + unsigned mask, m_len; + njs_str_t data, sig; + EVP_MD_CTX *mctx; + EVP_PKEY_CTX *pctx; + const EVP_MD *md; + qjs_webcrypto_key_t *key; + qjs_webcrypto_hash_t hash; + qjs_webcrypto_algorithm_t *alg; + unsigned char m[EVP_MAX_MD_SIZE]; + + dst = NULL; + mctx = NULL; + pctx = NULL; + + options = argv[0]; + + alg = qjs_key_algorithm(cx, options); + if (alg == NULL) { + return JS_EXCEPTION; + } + + key = JS_GetOpaque2(cx, argv[1], QJS_CORE_CLASS_ID_WEBCRYPTO_KEY); + if (key == NULL) { + JS_ThrowTypeError(cx, "\"key\" is not a CryptoKey object"); + return JS_EXCEPTION; + } + + mask = verify ? QJS_KEY_USAGE_VERIFY : QJS_KEY_USAGE_SIGN; + if (!(key->usage & mask)) { + JS_ThrowTypeError(cx, "provide key does not support \"%s\" operation", + verify ? "verify" : "sign"); + return JS_EXCEPTION; + } + + if (key->alg != alg) { + JS_ThrowTypeError(cx, "cannot %s using \"%s\" with \"%s\" key", + verify ? "verify" : "sign", + qjs_algorithm_string(key->alg), + qjs_algorithm_string(alg)); + return JS_EXCEPTION; + } + + if (verify) { + ret = qjs_typed_array_data(cx, argv[2], &sig); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + + ret = qjs_typed_array_data(cx, argv[3], &data); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + + } else { + ret = qjs_typed_array_data(cx, argv[2], &data); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + } + + if (alg->type == QJS_ALGORITHM_ECDSA) { + ret = qjs_algorithm_hash(cx, options, &hash); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + + } else { + hash = key->hash; + } + + md = qjs_algorithm_hash_digest(hash); + + /* Clang complains about uninitialized rc. */ + rc = 0; + outlen = 0; + + switch (alg->type) { + case QJS_ALGORITHM_HMAC: + m_len = EVP_MD_size(md); + + if (!verify) { + dst = js_malloc(cx, m_len); + if (dst == NULL) { + JS_ThrowOutOfMemory(cx); + return JS_EXCEPTION; + } + + } else { + dst = (u_char *) &m[0]; + } + + outlen = m_len; + + p = HMAC(md, key->u.s.raw.start, key->u.s.raw.length, data.start, + data.length, dst, &m_len); + + if (p == NULL || m_len != outlen) { + qjs_webcrypto_error(cx, "HMAC() failed"); + goto fail; + } + + if (verify) { + rc = (sig.length == outlen && memcmp(sig.start, dst, outlen) == 0); + } + + break; + + case QJS_ALGORITHM_RSASSA_PKCS1_v1_5: + case QJS_ALGORITHM_RSA_PSS: + case QJS_ALGORITHM_ECDSA: + default: + mctx = njs_evp_md_ctx_new(); + if (mctx == NULL) { + qjs_webcrypto_error(cx, "njs_evp_md_ctx_new() failed"); + goto fail; + } + + rc = EVP_DigestInit_ex(mctx, md, NULL); + if (rc <= 0) { + qjs_webcrypto_error(cx, "EVP_DigestInit_ex() failed"); + goto fail; + } + + rc = EVP_DigestUpdate(mctx, data.start, data.length); + if (rc <= 0) { + qjs_webcrypto_error(cx, "EVP_DigestUpdate() failed"); + goto fail; + } + + rc = EVP_DigestFinal_ex(mctx, m, &m_len); + if (rc <= 0) { + qjs_webcrypto_error(cx, "EVP_DigestFinal_ex() failed"); + goto fail; + } + + olen = EVP_PKEY_size(key->u.a.pkey); + dst = js_malloc(cx, olen); + if (dst == NULL) { + JS_ThrowOutOfMemory(cx); + goto fail; + } + + pctx = EVP_PKEY_CTX_new(key->u.a.pkey, NULL); + if (pctx == NULL) { + qjs_webcrypto_error(cx, "EVP_PKEY_CTX_new() failed"); + goto fail; + } + + if (!verify) { + rc = EVP_PKEY_sign_init(pctx); + if (rc <= 0) { + qjs_webcrypto_error(cx, "EVP_PKEY_sign_init() failed"); + goto fail; + } + + } else { + rc = EVP_PKEY_verify_init(pctx); + if (rc <= 0) { + qjs_webcrypto_error(cx, "EVP_PKEY_verify_init() failed"); + goto fail; + } + } + + rc = qjs_set_rsa_padding(cx, options, key->u.a.pkey, pctx, alg->type); + if (rc < 0) { + goto fail; + } + + rc = EVP_PKEY_CTX_set_signature_md(pctx, md); + if (rc <= 0) { + qjs_webcrypto_error(cx, "EVP_PKEY_CTX_set_signature_md() failed"); + goto fail; + } + + if (!verify) { + outlen = olen; + rc = EVP_PKEY_sign(pctx, dst, &outlen, m, m_len); + if (rc <= 0) { + qjs_webcrypto_error(cx, "EVP_PKEY_sign() failed"); + goto fail; + } + + if (alg->type == QJS_ALGORITHM_ECDSA) { + rc = qjs_convert_der_to_p1363(cx, key->u.a.pkey, dst, outlen, + &p1363, &outlen); + if (rc < 0) { + goto fail; + } + + js_free(cx, dst); + dst = p1363; + } + + } else { + if (alg->type == QJS_ALGORITHM_ECDSA) { + rc = qjs_convert_p1363_to_der(cx, key->u.a.pkey, sig.start, + sig.length, &sig.start, + &sig.length); + if (rc < 0) { + goto fail; + } + } + + rc = EVP_PKEY_verify(pctx, sig.start, sig.length, m, m_len); + + if (alg->type == QJS_ALGORITHM_ECDSA) { + js_free(cx, sig.start); + } + + if (rc < 0) { + qjs_webcrypto_error(cx, "EVP_PKEY_verify() failed"); + goto fail; + } + + js_free(cx, dst); + } + + njs_evp_md_ctx_free(mctx); + mctx = NULL; + + EVP_PKEY_CTX_free(pctx); + pctx = NULL; + } + + if (!verify) { + ret = qjs_new_array_buffer(cx, dst, outlen); + if (JS_IsException(ret)) { + goto fail; + } + + } else { + ret = JS_NewBool(cx, rc != 0); + } + + return qjs_webcrypto_result(cx, ret, 0); + +fail: + + if (mctx != NULL) { + njs_evp_md_ctx_free(mctx); + } + + if (pctx != NULL) { + EVP_PKEY_CTX_free(pctx); + } + + if (dst != NULL) { + js_free(cx, dst); + } + + return qjs_webcrypto_result(cx, JS_UNDEFINED, -1); +} + + +static JSValue +qjs_webcrypto_key_to_string_tag(JSContext *cx, JSValueConst this_val) +{ + return JS_NewString(cx, "CryptoKey"); +} + + +static JSValue +qjs_webcrypto_key_algorithm(JSContext *cx, JSValueConst this_val) +{ + JSValue obj, ret, hash, len, pe; + njs_str_t *name, pe_data; + const BIGNUM *n_bn, *e_bn; + const EC_GROUP *group; + qjs_webcrypto_key_t *key; + + key = JS_GetOpaque2(cx, this_val, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY); + if (key == NULL) { + JS_ThrowTypeError(cx, "\"key\" is not a CryptoKey object"); + return JS_EXCEPTION; + } + + name = &qjs_webcrypto_alg[key->alg->type].name; + + obj = JS_NewObject(cx); + if (JS_IsException(obj)) { + return JS_EXCEPTION; + } + + ret = JS_NewStringLen(cx, (const char *) name->start, name->length); + if (JS_IsException(ret)) { + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } + + if (JS_DefinePropertyValueStr(cx, obj, "name", ret, JS_PROP_C_W_E) + < 0) + { + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } + + switch (key->alg->type) { + case QJS_ALGORITHM_RSASSA_PKCS1_v1_5: + case QJS_ALGORITHM_RSA_PSS: + case QJS_ALGORITHM_RSA_OAEP: + /* RsaHashedKeyGenParams. */ + + njs_assert(key->u.a.pkey != NULL); + njs_assert(EVP_PKEY_id(key->u.a.pkey) == EVP_PKEY_RSA); + + njs_rsa_get0_key(njs_pkey_get_rsa_key(key->u.a.pkey), &n_bn, &e_bn, + NULL); + + if (JS_DefinePropertyValueStr(cx, obj, "modulusLength", + JS_NewInt32(cx, BN_num_bits(n_bn)), + JS_PROP_C_W_E) + < 0) + { + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } + + len = JS_NewInt32(cx, BN_num_bytes(e_bn)); + pe = qjs_new_uint8_array(cx, 1, &len); + if (JS_IsException(pe)) { + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } + + ret = qjs_typed_array_data(cx, pe, &pe_data); + if (JS_IsException(ret)) { + JS_FreeValue(cx, pe); + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } + + BN_bn2bin(e_bn, pe_data.start); + + if (JS_DefinePropertyValueStr(cx, obj, "publicExponent", pe, + JS_PROP_C_W_E) + < 0) + { + JS_FreeValue(cx, pe); + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } + + hash = JS_NewString(cx, qjs_algorithm_hash_name(key->hash)); + if (JS_IsException(hash)) { + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } + + ret = JS_NewObject(cx); + if (JS_IsException(ret)) { + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } + + if (JS_DefinePropertyValueStr(cx, ret, "name", hash, JS_PROP_C_W_E) + < 0) + { + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } + + if (JS_DefinePropertyValueStr(cx, obj, "hash", ret, JS_PROP_C_W_E) + < 0) + { + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } + + break; + + case QJS_ALGORITHM_AES_GCM: + case QJS_ALGORITHM_AES_CTR: + case QJS_ALGORITHM_AES_CBC: + /* AesKeyGenParams. */ + + if (JS_DefinePropertyValueStr(cx, obj, "length", + JS_NewInt32(cx, key->u.s.raw.length * 8), + JS_PROP_C_W_E) + < 0) + { + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } + + break; + + case QJS_ALGORITHM_ECDSA: + case QJS_ALGORITHM_ECDH: + /* EcKeyGenParams. */ + + njs_assert(key->u.a.pkey != NULL); + njs_assert(EVP_PKEY_id(key->u.a.pkey) == EVP_PKEY_EC); + + group = EC_KEY_get0_group(njs_pkey_get_ec_key(key->u.a.pkey)); + name = qjs_algorithm_curve_name(EC_GROUP_get_curve_name(group)); + + ret = JS_NewStringLen(cx, (const char *) name->start, name->length); + if (JS_IsException(ret)) { + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } + + if (JS_DefinePropertyValueStr(cx, obj, "namedCurve", ret, JS_PROP_C_W_E) + < 0) + { + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } + + break; + + case QJS_ALGORITHM_HMAC: + default: + /* HmacKeyGenParams */ + + hash = JS_NewString(cx, qjs_algorithm_hash_name(key->hash)); + if (JS_IsException(hash)) { + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } + + ret = JS_NewObject(cx); + if (JS_IsException(ret)) { + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } + + if (JS_DefinePropertyValueStr(cx, ret, "name", hash, JS_PROP_C_W_E) + < 0) + { + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } + + if (JS_DefinePropertyValueStr(cx, obj, "hash", ret, JS_PROP_C_W_E) + < 0) + { + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } + + break; + } + + return obj; +} + + +static JSValue +qjs_webcrypto_key_extractable(JSContext *cx, JSValueConst this_val) +{ + qjs_webcrypto_key_t *key; + + key = JS_GetOpaque2(cx, this_val, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY); + if (key == NULL) { + JS_ThrowTypeError(cx, "\"key\" is not a CryptoKey object"); + return JS_EXCEPTION; + } + + return JS_NewBool(cx, key->extractable); +} + + +static JSValue +qjs_webcrypto_key_type(JSContext *cx, JSValueConst this_val) +{ + qjs_webcrypto_key_t *key; + + key = JS_GetOpaque2(cx, this_val, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY); + if (key == NULL) { + JS_ThrowTypeError(cx, "\"key\" is not a CryptoKey object"); + return JS_EXCEPTION; + } + + if (key->alg->raw) { + return JS_NewString(cx, "secret"); + } + + return JS_NewString(cx, key->u.a.privat ? "private": "public"); +} + + +static JSValue +qjs_webcrypto_key_usages(JSContext *cx, JSValueConst this_val) +{ + qjs_webcrypto_key_t *key; + + key = JS_GetOpaque2(cx, this_val, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY); + if (key == NULL) { + JS_ThrowTypeError(cx, "\"key\" is not a CryptoKey object"); + return JS_EXCEPTION; + } + + return qjs_key_ops(cx, key->usage); +} + + +static JSValue +qjs_get_random_values(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + JSValue buffer, ret; + njs_str_t fill; + + buffer = JS_DupValue(cx, argv[0]); + + ret = qjs_typed_array_data(cx, buffer, &fill); + if (JS_IsException(ret)) { + JS_FreeValue(cx, buffer); + return JS_EXCEPTION; + } + + if (fill.length > 65536) { + JS_ThrowTypeError(cx, "requested length exceeds 65536 bytes"); + JS_FreeValue(cx, buffer); + return JS_EXCEPTION; + } + + if (RAND_bytes(fill.start, fill.length) != 1) { + JS_FreeValue(cx, buffer); + qjs_webcrypto_error(cx, "RAND_bytes() failed"); + return JS_EXCEPTION; + } + + return buffer; +} + + +static JSValue +qjs_webcrypto_key_make(JSContext *cx, qjs_webcrypto_algorithm_t *alg, + unsigned usage, int extractable) +{ + JSValue key; + qjs_webcrypto_key_t *k; + + key = JS_NewObjectClass(cx, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY); + if (JS_IsException(key)) { + return JS_EXCEPTION; + } + + k = js_mallocz(cx, sizeof(qjs_webcrypto_key_t)); + if (k == NULL) { + return JS_ThrowOutOfMemory(cx); + } + + /* + * k->u.a.pkey = NULL; + * k->u.s.raw.length = 0; + * k->u.s.raw.start = NULL; + * k->u.a.curve = 0; + * k->u.a.privat = 0; + * k->hash = QJS_HASH_UNSET; + */ + + k->alg = alg; + k->usage = usage; + k->extractable = extractable; + + JS_SetOpaque(key, k); + + return key; +} + + +static void +qjs_webcrypto_key_finalizer(JSRuntime *rt, JSValue val) +{ + qjs_webcrypto_key_t *key; + + key = JS_GetOpaque(val, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY); + + if (key != NULL) { + if (!key->alg->raw) { + if (key->u.a.pkey != NULL) { + EVP_PKEY_free(key->u.a.pkey); + } + + } else { + if (key->u.s.raw.start != NULL) { + js_free_rt(rt, key->u.s.raw.start); + } + } + + js_free_rt(rt, key); + } +} + + +static qjs_webcrypto_key_format_t +qjs_key_format(JSContext *cx, JSValueConst value) +{ + njs_str_t format; + qjs_webcrypto_entry_t *e; + + format.start = (u_char *) JS_ToCStringLen(cx, &format.length, value); + if (format.start == NULL) { + return QJS_KEY_FORMAT_UNKNOWN; + } + + for (e = &qjs_webcrypto_format[0]; e->name.length != 0; e++) { + if (njs_strstr_eq(&format, &e->name)) { + JS_FreeCString(cx, (char *) format.start); + return e->value; + } + } + + JS_ThrowTypeError(cx, "unknown key format: \"%s\"", format.start); + JS_FreeCString(cx, (char *) format.start); + + return QJS_KEY_FORMAT_UNKNOWN; +} + + +static qjs_webcrypto_jwk_kty_t +qjs_jwk_kty(JSContext *cx, JSValueConst value) +{ + njs_str_t kty; + qjs_webcrypto_entry_t *e; + + kty.start = (u_char *) JS_ToCStringLen(cx, &kty.length, value); + if (kty.start == NULL) { + return QJS_KEY_JWK_KTY_UNKNOWN; + } + + for (e = &qjs_webcrypto_jwk_kty[0]; e->name.length != 0; e++) { + if (njs_strstr_eq(&kty, &e->name)) { + JS_FreeCString(cx, (char *) kty.start); + return e->value; + } + } + + JS_ThrowTypeError(cx, "invalid JWK key type: %s", kty.start); + JS_FreeCString(cx, (char *) kty.start); + + return QJS_KEY_JWK_KTY_UNKNOWN; +} + + +static qjs_webcrypto_algorithm_t * +qjs_key_algorithm(JSContext *cx, JSValue options) +{ + JSValue v; + njs_str_t a; + qjs_webcrypto_entry_t *e; + qjs_webcrypto_algorithm_t *alg; + + if (JS_IsObject(options)) { + v = JS_GetPropertyStr(cx, options, "name"); + if (JS_IsException(v)) { + return NULL; + } + + } else { + v = JS_DupValue(cx, options); + } + + a.start = (u_char *) JS_ToCStringLen(cx, &a.length, v); + JS_FreeValue(cx, v); + if (a.start == NULL) { + return NULL; + } + + 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; + } + } + + JS_ThrowTypeError(cx, "unknown algorithm name: \"%.*s\"", (int) a.length, + a.start); + JS_FreeCString(cx, (char *) a.start); + + return NULL; +} + + +static JSValue +qjs_algorithm_curve(JSContext *cx, JSValue options, int *curve) +{ + JSValue v; + njs_str_t name; + qjs_webcrypto_entry_t *e; + + if (JS_IsObject(options)) { + v = JS_GetPropertyStr(cx, options, "namedCurve"); + if (JS_IsException(v)) { + return JS_EXCEPTION; + } + + } else { + v = JS_DupValue(cx, options); + } + + name.start = (u_char *) JS_ToCStringLen(cx, &name.length, v); + JS_FreeValue(cx, v); + if (name.start == NULL) { + return JS_EXCEPTION; + } + + for (e = &qjs_webcrypto_curve[0]; e->name.length != 0; e++) { + if (njs_strstr_eq(&name, &e->name)) { + JS_FreeCString(cx, (char *) name.start); + *curve = e->value; + return JS_UNDEFINED; + } + } + + JS_ThrowTypeError(cx, "unknown namedCurve: \"%.*s\"", (int) name.length, + name.start); + JS_FreeCString(cx, (char *) name.start); + + return JS_EXCEPTION; +} + + +static JSValue +qjs_algorithm_hash(JSContext *cx, JSValue options, qjs_webcrypto_hash_t *hash) +{ + JSValue v; + njs_str_t name; + qjs_webcrypto_entry_t *e; + + if (JS_IsObject(options)) { + v = JS_GetPropertyStr(cx, options, "hash"); + if (JS_IsException(v)) { + return JS_EXCEPTION; + } + + } else { + v = JS_DupValue(cx, options); + } + + name.start = (u_char *) JS_ToCStringLen(cx, &name.length, v); + JS_FreeValue(cx, v); + if (name.start == NULL) { + return JS_EXCEPTION; + } + + for (e = &qjs_webcrypto_hash[0]; e->name.length != 0; e++) { + if (njs_strstr_eq(&name, &e->name)) { + JS_FreeCString(cx, (char *) name.start); + *hash = e->value; + return JS_UNDEFINED; + } + } + + JS_ThrowTypeError(cx, "unknown hash name: \"%.*s\"", (int) name.length, + name.start); + JS_FreeCString(cx, (char *) name.start); + + return JS_EXCEPTION; +} + + +static const EVP_MD * +qjs_algorithm_hash_digest(qjs_webcrypto_hash_t hash) +{ + switch (hash) { + case QJS_HASH_SHA256: + return EVP_sha256(); + + case QJS_HASH_SHA384: + return EVP_sha384(); + + case QJS_HASH_SHA512: + return EVP_sha512(); + + case QJS_HASH_SHA1: + default: + break; + } + + return EVP_sha1(); +} + + +static njs_str_t * +qjs_algorithm_curve_name(int curve) +{ + qjs_webcrypto_entry_t *e; + + for (e = &qjs_webcrypto_curve[0]; e->name.length != 0; e++) { + if (e->value == (uintptr_t) curve) { + return &e->name; + } + } + + return &e->name; +} + + +static const char * +qjs_format_string(qjs_webcrypto_key_format_t fmt) +{ + qjs_webcrypto_entry_t *e; + + for (e = &qjs_webcrypto_format[0]; e->name.length != 0; e++) { + if (fmt == e->value) { + break; + } + } + + return (const char *) e->name.start; +} + + +static const char * +qjs_algorithm_string(qjs_webcrypto_algorithm_t *algorithm) +{ + qjs_webcrypto_entry_t *e; + qjs_webcrypto_algorithm_t *alg; + + for (e = &qjs_webcrypto_alg[0]; e->name.length != 0; e++) { + alg = (qjs_webcrypto_algorithm_t *) e->value; + if (alg->type == algorithm->type) { + break; + } + } + + return (const char *) e->name.start; +} + + +static const char * +qjs_algorithm_hash_name(qjs_webcrypto_hash_t hash) +{ + qjs_webcrypto_entry_t *e; + + for (e = &qjs_webcrypto_hash[0]; e->name.length != 0; e++) { + if (e->value == hash) { + break; + } + } + + return (const char *) e->name.start; +} + + +static JSValue +qjs_key_usage(JSContext *cx, JSValue value, unsigned *mask) +{ + int64_t length; + JSValue v; + uint32_t i; + njs_str_t s; + qjs_webcrypto_entry_t *e; + + if (!JS_IsArray(cx, value)) { + JS_ThrowTypeError(cx, "\"keyUsages\" argument must be an Array"); + return JS_EXCEPTION; + } + + v = JS_GetPropertyStr(cx, value, "length"); + if (JS_IsException(v)) { + return JS_EXCEPTION; + } + + if (JS_ToInt64(cx, &length, v) < 0) { + JS_FreeValue(cx, v); + return JS_EXCEPTION; + } + + JS_FreeValue(cx, v); + + *mask = 0; + + for (i = 0; i < length; i++) { + v = JS_GetPropertyUint32(cx, value, i); + if (JS_IsException(v)) { + return JS_EXCEPTION; + } + + s.start = (u_char *) JS_ToCStringLen(cx, &s.length, v); + JS_FreeValue(cx, v); + if (s.start == NULL) { + JS_ThrowOutOfMemory(cx); + return JS_EXCEPTION; + } + + for (e = &qjs_webcrypto_usage[0]; e->name.length != 0; e++) { + if (njs_strstr_eq(&s, &e->name)) { + *mask |= e->value; + break; + } + } + + JS_FreeCString(cx, (char *) s.start); + } + + return JS_UNDEFINED; +} + + +static JSValue +qjs_key_ops(JSContext *cx, unsigned mask) +{ + uint32_t i; + JSValue ops, value; + qjs_webcrypto_entry_t *e; + + ops = JS_NewArray(cx); + if (JS_IsException(ops)) { + return JS_EXCEPTION; + } + + i = 0; + + for (e = &qjs_webcrypto_usage[0]; e->name.length != 0; e++) { + if (mask & e->value) { + value = JS_NewStringLen(cx, (const char *) e->name.start, + e->name.length); + if (JS_IsException(value)) { + JS_FreeValue(cx, ops); + return JS_EXCEPTION; + } + + if (JS_SetPropertyUint32(cx, ops, i++, value) < 0) { + JS_FreeValue(cx, ops); + JS_FreeValue(cx, value); + return JS_EXCEPTION; + } + } + } + + return ops; +} + + +static u_char * +qjs_cpystrn(u_char *dst, u_char *src, size_t n) +{ + if (n == 0) { + return dst; + } + + while (--n) { + *dst = *src; + + if (*dst == '\0') { + return dst; + } + + dst++; + src++; + } + + *dst = '\0'; + + return dst; +} + + +static JSValue +qjs_webcrypto_promise_trampoline(JSContext *cx, int argc, JSValueConst *argv) +{ + return JS_Call(cx, argv[0], JS_UNDEFINED, 1, &argv[1]); +} + + +static JSValue +qjs_webcrypto_result(JSContext *cx, JSValue result, int rc) +{ + JS_BOOL is_error; + JSValue promise, callbacks[2], arguments[2]; + + promise = JS_NewPromiseCapability(cx, callbacks); + if (JS_IsException(promise)) { + JS_FreeValue(cx, result); + return JS_EXCEPTION; + } + + is_error = !!(rc != 0); + + JS_FreeValue(cx, callbacks[!is_error]); + arguments[0] = callbacks[is_error]; + arguments[1] = is_error ? JS_GetException(cx) : result; + + if (JS_EnqueueJob(cx, qjs_webcrypto_promise_trampoline, 2, arguments) < 0) { + JS_FreeValue(cx, promise); + JS_FreeValue(cx, callbacks[is_error]); + JS_FreeValue(cx, arguments[1]); + return JS_EXCEPTION; + } + + JS_FreeValue(cx, arguments[0]); + JS_FreeValue(cx, arguments[1]); + + return promise; +} + + +static void +qjs_webcrypto_error(JSContext *cx, const char *fmt, ...) +{ + int flags; + u_char *p, *last; + va_list args; + const char *data; + unsigned long n; + u_char errstr[NJS_MAX_ERROR_STR]; + + last = &errstr[NJS_MAX_ERROR_STR]; + + va_start(args, fmt); + p = njs_vsprintf(errstr, last - 1, fmt, args); + va_end(args); + + if (ERR_peek_error()) { + p = qjs_cpystrn(p, (u_char *) " (SSL:", last - p); + + for ( ;; ) { + + n = ERR_peek_error_data(&data, &flags); + + if (n == 0) { + break; + } + + /* ERR_error_string_n() requires at least one byte */ + + if (p >= last - 1) { + goto next; + } + + *p++ = ' '; + + ERR_error_string_n(n, (char *) p, last - p); + + while (p < last && *p) { + p++; + } + + if (p < last && *data && (flags & ERR_TXT_STRING)) { + *p++ = ':'; + p = qjs_cpystrn(p, (u_char *) data, last - p); + } + + next: + + (void) ERR_get_error(); + } + + if (p < last) { + *p++ = ')'; + } + } + + JS_ThrowTypeError(cx, "%*s", (int) (p - errstr), errstr); +} + + +static int +qjs_webcrypto_module_init(JSContext *cx, JSModuleDef *m) +{ + int rc; + JSValue proto; + + proto = JS_NewObject(cx); + JS_SetPropertyFunctionList(cx, proto, qjs_webcrypto_export, + njs_nitems(qjs_webcrypto_export)); + + rc = JS_SetModuleExport(cx, m, "default", proto); + if (rc != 0) { + return -1; + } + + return JS_SetModuleExportList(cx, m, qjs_webcrypto_export, + njs_nitems(qjs_webcrypto_export)); +} + + +static JSModuleDef * +qjs_webcrypto_init(JSContext *cx, const char *name) +{ + int rc; + JSValue crypto, proto, global_obj; + JSModuleDef *m; + + (void) qjs_webcrypto_alg_name; + (void) qjs_algorithm_hash_name; + + if (!JS_IsRegisteredClass(JS_GetRuntime(cx), + QJS_CORE_CLASS_ID_WEBCRYPTO_KEY)) + { + if (JS_NewClass(JS_GetRuntime(cx), QJS_CORE_CLASS_ID_WEBCRYPTO_KEY, + &qjs_webcrypto_key_class)) + { + return NULL; + } + + proto = JS_NewObject(cx); + if (JS_IsException(proto)) { + return NULL; + } + + JS_SetPropertyFunctionList(cx, proto, qjs_webcrypto_key_proto, + njs_nitems(qjs_webcrypto_key_proto)); + + JS_SetClassProto(cx, QJS_CORE_CLASS_ID_WEBCRYPTO_KEY, proto); + } + + global_obj = JS_GetGlobalObject(cx); + + crypto = JS_NewObject(cx); + JS_SetPropertyFunctionList(cx, crypto, qjs_webcrypto_export, + njs_nitems(qjs_webcrypto_export)); + + rc = JS_SetPropertyStr(cx, global_obj, "crypto", crypto); + if (rc == -1) { + return NULL; + } + + JS_FreeValue(cx, global_obj); + + m = JS_NewCModule(cx, name, qjs_webcrypto_module_init); + if (m == NULL) { + return NULL; + } + + JS_AddModuleExport(cx, m, "default"); + rc = JS_AddModuleExportList(cx, m, qjs_webcrypto_export, + njs_nitems(qjs_webcrypto_export)); + if (rc != 0) { + return NULL; + } + + return m; +} diff --git a/nginx/config b/nginx/config index 8e920477..2edf7a3d 100644 --- a/nginx/config +++ b/nginx/config @@ -117,6 +117,10 @@ if [ $NJS_OPENSSL != NO ]; then have=NJS_HAVE_OPENSSL . auto/have NJS_SRCS="$NJS_SRCS $ngx_addon_dir/../external/njs_webcrypto_module.c" + if [ "$NJS_HAVE_QUICKJS" = "YES" ]; then + NJS_SRCS="$NJS_SRCS $ngx_addon_dir/../external/qjs_webcrypto_module.c" + fi + echo " enabled webcrypto module" fi diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c index 9b6d8ea1..04d06fb2 100644 --- a/nginx/ngx_http_js_module.c +++ b/nginx/ngx_http_js_module.c @@ -1141,6 +1141,9 @@ qjs_module_t *njs_http_qjs_addon_modules[] = { * Shared addons should be in the same order and the same positions * in all nginx modules. */ +#ifdef NJS_HAVE_OPENSSL + &qjs_webcrypto_module, +#endif #ifdef NJS_HAVE_ZLIB &qjs_zlib_module, #endif diff --git a/nginx/ngx_js.h b/nginx/ngx_js.h index 11dba9ed..833c1490 100644 --- a/nginx/ngx_js.h +++ b/nginx/ngx_js.h @@ -377,6 +377,7 @@ ngx_int_t ngx_qjs_string(ngx_engine_t *e, JSValueConst val, ngx_str_t *str); #define ngx_qjs_external_ctx(cx, e) \ ((ngx_js_external_ctx_pt) ngx_qjs_meta(cx, 11))(e) +extern qjs_module_t qjs_webcrypto_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; diff --git a/nginx/ngx_stream_js_module.c b/nginx/ngx_stream_js_module.c index 98427aae..5f4c6a04 100644 --- a/nginx/ngx_stream_js_module.c +++ b/nginx/ngx_stream_js_module.c @@ -840,6 +840,9 @@ qjs_module_t *njs_stream_qjs_addon_modules[] = { * Shared addons should be in the same order and the same positions * in all nginx modules. */ +#ifdef NJS_HAVE_OPENSSL + &qjs_webcrypto_module, +#endif #ifdef NJS_HAVE_ZLIB &qjs_zlib_module, #endif diff --git a/nginx/t/js_webcrypto.t b/nginx/t/js_webcrypto.t new file mode 100644 index 00000000..f43f9d6d --- /dev/null +++ b/nginx/t/js_webcrypto.t @@ -0,0 +1,85 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for http njs module, WebCrypto module. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_import test.js; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /random_values_test { + js_content test.random_values_test; + } + } +} + +EOF + +$t->write_file('test.js', < a + count1(v), 0); + let nbits = buf.length * 32; + let mean = nbits / 2; + let stdd = Math.sqrt(nbits / 4); + + r.return(200, bits1 > (mean - 10 * stdd) && bits1 < (mean + 10 * stdd)); + } + + export default {random_values_test}; + +EOF + +$t->try_run('no njs')->plan(1); + +############################################################################### + +like(http_get('/random_values_test'), qr/true/, 'random_values_test'); + +############################################################################### diff --git a/nginx/t/stream_js_webcrypto.t b/nginx/t/stream_js_webcrypto.t new file mode 100644 index 00000000..d7b34396 --- /dev/null +++ b/nginx/t/stream_js_webcrypto.t @@ -0,0 +1,83 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for stream njs module, WebCrypto module. + +############################################################################### + +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 stream_return/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +stream { + %%TEST_GLOBALS_STREAM%% + + js_import test.js; + + js_set $test test.random_values_test; + + server { + listen 127.0.0.1:8081; + return $test; + } +} + +EOF + +$t->write_file('test.js', < a + count1(v), 0); + let nbits = buf.length * 32; + let mean = nbits / 2; + let stdd = Math.sqrt(nbits / 4); + + return bits1 > (mean - 10 * stdd) && bits1 < (mean + 10 * stdd); + } + + export default {random_values_test}; +EOF + +$t->try_run('no stream js_var')->plan(1); + +############################################################################### + +is(stream('127.0.0.1:' . port(8081))->io('###'), 'true', 'random values test'); + +############################################################################### diff --git a/src/qjs.c b/src/qjs.c index 8c5b0b6d..56c6a3ba 100644 --- a/src/qjs.c +++ b/src/qjs.c @@ -1027,6 +1027,20 @@ qjs_typed_array_data(JSContext *ctx, JSValueConst value, njs_str_t *data) } +static void +js_array_buffer_free(JSRuntime *rt, void *opaque, void *ptr) +{ + js_free_rt(rt, ptr); +} + + +JSValue +qjs_new_array_buffer(JSContext *cx, uint8_t *src, size_t len) +{ + return JS_NewArrayBuffer(cx, src, len, js_array_buffer_free, NULL, 0); +} + + JSValue qjs_string_create_chb(JSContext *cx, njs_chb_t *chain) { diff --git a/src/qjs.h b/src/qjs.h index 71dd297e..2d3c289c 100644 --- a/src/qjs.h +++ b/src/qjs.h @@ -18,6 +18,7 @@ #include #include #include +#include #if defined(__GNUC__) && (__GNUC__ >= 8) #pragma GCC diagnostic push @@ -32,6 +33,9 @@ #include +#define QJS_CORE_CLASS_ID_OFFSET 64 +#define QJS_CORE_CLASS_ID_BUFFER (QJS_CORE_CLASS_ID_OFFSET) +#define QJS_CORE_CLASS_ID_UINT8_ARRAY_CTOR (QJS_CORE_CLASS_ID_OFFSET + 1) #define QJS_CORE_CLASS_ID_OFFSET 64 #define QJS_CORE_CLASS_ID_BUFFER (QJS_CORE_CLASS_ID_OFFSET) #define QJS_CORE_CLASS_ID_UINT8_ARRAY_CTOR (QJS_CORE_CLASS_ID_OFFSET + 1) @@ -40,7 +44,8 @@ #define QJS_CORE_CLASS_ID_FS_STATS (QJS_CORE_CLASS_ID_OFFSET + 4) #define QJS_CORE_CLASS_ID_FS_DIRENT (QJS_CORE_CLASS_ID_OFFSET + 5) #define QJS_CORE_CLASS_ID_FS_FILEHANDLE (QJS_CORE_CLASS_ID_OFFSET + 6) -#define QJS_CORE_CLASS_ID_LAST (QJS_CORE_CLASS_ID_OFFSET + 7) +#define QJS_CORE_CLASS_ID_WEBCRYPTO_KEY (QJS_CORE_CLASS_ID_OFFSET + 7) +#define QJS_CORE_CLASS_ID_LAST (QJS_CORE_CLASS_ID_OFFSET + 8) typedef JSModuleDef *(*qjs_addon_init_pt)(JSContext *ctx, const char *name); @@ -55,6 +60,7 @@ JSContext *qjs_new_context(JSRuntime *rt, qjs_module_t **addons); JSValue qjs_new_uint8_array(JSContext *ctx, int argc, JSValueConst *argv); +JSValue qjs_new_array_buffer(JSContext *cx, uint8_t *src, size_t len); JSValue qjs_buffer_alloc(JSContext *ctx, size_t size); JSValue qjs_buffer_create(JSContext *ctx, u_char *start, size_t size); JSValue qjs_buffer_chb_alloc(JSContext *ctx, njs_chb_t *chain); @@ -75,6 +81,22 @@ typedef struct { const qjs_buffer_encoding_t *qjs_buffer_encoding(JSContext *ctx, JSValueConst value, JS_BOOL thrw); +int qjs_base64_encode(JSContext *ctx, const njs_str_t *src, + njs_str_t *dst); +size_t qjs_base64_encode_length(JSContext *ctx, const njs_str_t *src); +int qjs_base64_decode(JSContext *ctx, const njs_str_t *src, + njs_str_t *dst); +size_t qjs_base64_decode_length(JSContext *ctx, const njs_str_t *src); +int qjs_base64url_encode(JSContext *ctx, const njs_str_t *src, + njs_str_t *dst); +int qjs_base64url_decode(JSContext *ctx, const njs_str_t *src, + njs_str_t *dst); +size_t qjs_base64url_decode_length(JSContext *ctx, const njs_str_t *src); +int qjs_hex_encode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst); +size_t qjs_hex_encode_length(JSContext *ctx, const njs_str_t *src); +int qjs_hex_decode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst); +size_t qjs_hex_decode_length(JSContext *ctx, const njs_str_t *src); + JSValue qjs_process_object(JSContext *ctx, int argc, const char **argv); typedef struct { diff --git a/src/qjs_buffer.c b/src/qjs_buffer.c index 9840377f..8fc296ad 100644 --- a/src/qjs_buffer.c +++ b/src/qjs_buffer.c @@ -75,21 +75,6 @@ static JSValue qjs_buffer_from_object(JSContext *ctx, JSValueConst obj); static JSValue qjs_buffer_compare_array(JSContext *ctx, JSValue val1, JSValue val2, JSValueConst target_start, JSValueConst target_end, JSValueConst source_start, JSValueConst source_end); -static int qjs_base64_encode(JSContext *ctx, const njs_str_t *src, - njs_str_t *dst); -static size_t qjs_base64_encode_length(JSContext *ctx, const njs_str_t *src); -static int qjs_base64_decode(JSContext *ctx, const njs_str_t *src, - njs_str_t *dst); -static size_t qjs_base64_decode_length(JSContext *ctx, const njs_str_t *src); -static int qjs_base64url_encode(JSContext *ctx, const njs_str_t *src, - njs_str_t *dst); -static int qjs_base64url_decode(JSContext *ctx, const njs_str_t *src, - njs_str_t *dst); -static size_t qjs_base64url_decode_length(JSContext *ctx, const njs_str_t *src); -static int qjs_hex_encode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst); -static size_t qjs_hex_encode_length(JSContext *ctx, const njs_str_t *src); -static int qjs_hex_decode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst); -static size_t qjs_hex_decode_length(JSContext *ctx, const njs_str_t *src); static JSModuleDef *qjs_buffer_init(JSContext *ctx, const char *name); @@ -2175,7 +2160,7 @@ qjs_base64_encode_core(njs_str_t *dst, const njs_str_t *src, } -static int +int qjs_base64_encode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst) { qjs_base64_encode_core(dst, src, qjs_basis64_enc, 1); @@ -2184,7 +2169,7 @@ qjs_base64_encode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst) } -static size_t +size_t qjs_base64_encode_length(JSContext *ctx, const njs_str_t *src) { return qjs_base64_encoded_length(src->length); @@ -2245,7 +2230,7 @@ qjs_base64_decode_length_core(const njs_str_t *src, const u_char *basis) } -static int +int qjs_base64_decode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst) { qjs_base64_decode_core(dst, src, qjs_basis64); @@ -2254,14 +2239,14 @@ qjs_base64_decode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst) } -static size_t +size_t qjs_base64_decode_length(JSContext *ctx, const njs_str_t *src) { return qjs_base64_decode_length_core(src, qjs_basis64); } -static int +int qjs_base64url_encode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst) { qjs_base64_encode_core(dst, src, qjs_basis64url_enc, 0); @@ -2270,7 +2255,7 @@ qjs_base64url_encode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst) } -static int +int qjs_base64url_decode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst) { qjs_base64_decode_core(dst, src, qjs_basis64url); @@ -2279,7 +2264,7 @@ qjs_base64url_decode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst) } -static size_t +size_t qjs_base64url_decode_length(JSContext *ctx, const njs_str_t *src) { return qjs_base64_decode_length_core(src, qjs_basis64url); @@ -2309,7 +2294,7 @@ qjs_char_to_hex(u_char c) } -static int +int qjs_hex_decode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst) { u_char *p; @@ -2344,7 +2329,7 @@ qjs_hex_decode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst) } -static size_t +size_t qjs_hex_decode_length(JSContext *ctx, const njs_str_t *src) { const u_char *p, *end; @@ -2362,7 +2347,7 @@ qjs_hex_decode_length(JSContext *ctx, const njs_str_t *src) } -static int +int qjs_hex_encode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst) { u_char *p, c; @@ -2386,7 +2371,7 @@ qjs_hex_encode(JSContext *ctx, const njs_str_t *src, njs_str_t *dst) } -static size_t +size_t qjs_hex_encode_length(JSContext *ctx, const njs_str_t *src) { return src->length * 2; diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c index 2e9a5379..81fee436 100644 --- a/src/test/njs_unit_test.c +++ b/src/test/njs_unit_test.c @@ -20840,35 +20840,6 @@ static njs_unit_test_t njs_querystring_module_test[] = }; -static njs_unit_test_t njs_webcrypto_test[] = -{ - /* Statistic test - * bits1 is a random variable with Binomial distribution - * Expected value is N / 2 - * Standard deviation is sqrt(N / 4) - */ - { njs_str("function count1(v) {return v.toString(2).match(/1/g).length;}" - "let buf = new Uint32Array(32);" - "crypto.getRandomValues(buf);" - "let bits1 = buf.reduce((a, v)=> a + count1(v), 0);" - "let nbits = buf.length * 32;" - "let mean = nbits / 2;" - "let stddev = Math.sqrt(nbits / 4);" - "let condition = bits1 > (mean - 10 * stddev) && bits1 < (mean + 10 * stddev);" - "condition ? true : [buf, nbits, bits1, mean, stddev]"), - njs_str("true") }, - - { njs_str("let buf = new Uint32Array(4);" - "buf === crypto.getRandomValues(buf)"), - njs_str("true") }, - - { njs_str("crypto.subtle;" - "var d = Object.getOwnPropertyDescriptor(crypto, 'subtle');" - "d.enumerable && !d.configurable && d.writable"), - njs_str("true") }, -}; - - #define NJS_XML_DOC "const xml = require('xml');" \ "let data = `ToveJani`;" \ "let doc = xml.parse(data);" @@ -23918,17 +23889,6 @@ static njs_test_suite_t njs_suites[] = njs_nitems(njs_disabled_denormals_test), njs_disabled_denormals_tests }, - { -#if (NJS_HAVE_OPENSSL && !NJS_HAVE_MEMORY_SANITIZER) - njs_str("webcrypto"), -#else - njs_str(""), -#endif - { .externals = 1, .repeat = 1, .unsafe = 1 }, - njs_webcrypto_test, - njs_nitems(njs_webcrypto_test), - njs_unit_test }, - { #if (NJS_HAVE_LIBXML2 && !NJS_HAVE_MEMORY_SANITIZER) njs_str("xml"), diff --git a/test/webcrypto/sign.t.mjs b/test/webcrypto/sign.t.mjs index 4fa78716..c2d52a6f 100644 --- a/test/webcrypto/sign.t.mjs +++ b/test/webcrypto/sign.t.mjs @@ -51,10 +51,12 @@ async function test(params) { let sig = await crypto.subtle.sign(params.sign_alg, skey, encoder.encode(params.text)) .catch (e => { - if (e.toString().startsWith("Error: EVP_PKEY_CTX_set_signature_md() failed")) { + if (e.message.startsWith("EVP_PKEY_CTX_set_signature_md() failed")) { /* Red Hat Enterprise Linux: SHA-1 is disabled */ return "SKIPPED"; } + + throw e; }); if (sig == "SKIPPED") { diff --git a/test/webcrypto/verify.t.mjs b/test/webcrypto/verify.t.mjs index 70f608f3..5f77ad47 100644 --- a/test/webcrypto/verify.t.mjs +++ b/test/webcrypto/verify.t.mjs @@ -13,10 +13,12 @@ async function test(params) { key, params.signature, params.text) .catch (e => { - if (e.toString().startsWith("Error: EVP_PKEY_CTX_set_signature_md() failed")) { + if (e.message.startsWith("EVP_PKEY_CTX_set_signature_md() failed")) { /* Red Hat Enterprise Linux: SHA-1 is disabled */ return "SKIPPED"; } + + throw e; }); if (r == "SKIPPED") { From noreply at nginx.com Tue Jan 28 16:01:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Tue, 28 Jan 2025 16:01:02 +0000 (UTC) Subject: [nginx] QUIC: added missing casts in iov_base assignments. Message-ID: <20250128160102.54EC24779A@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/64d0795ac41836b6be8fcceba68f1dbb62b4035a branches: master commit: 64d0795ac41836b6be8fcceba68f1dbb62b4035a user: Aleksei Bavshin date: Mon, 27 Jan 2025 10:33:25 -0800 description: QUIC: added missing casts in iov_base assignments. This is consistent with the rest of the code and fixes build on systems with non-standard definition of struct iovec (Solaris, Illumos). --- src/event/quic/ngx_event_quic_output.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/event/quic/ngx_event_quic_output.c b/src/event/quic/ngx_event_quic_output.c index ce6aaab22..f087e2bfa 100644 --- a/src/event/quic/ngx_event_quic_output.c +++ b/src/event/quic/ngx_event_quic_output.c @@ -411,7 +411,7 @@ ngx_quic_send_segments(ngx_connection_t *c, u_char *buf, size_t len, ngx_memzero(msg_control, sizeof(msg_control)); iov.iov_len = len; - iov.iov_base = buf; + iov.iov_base = (void *) buf; msg.msg_iov = &iov; msg.msg_iovlen = 1; @@ -699,7 +699,7 @@ ngx_quic_send(ngx_connection_t *c, u_char *buf, size_t len, ngx_memzero(&msg, sizeof(struct msghdr)); iov.iov_len = len; - iov.iov_base = buf; + iov.iov_base = (void *) buf; msg.msg_iov = &iov; msg.msg_iovlen = 1; From noreply at nginx.com Thu Jan 30 13:17:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Thu, 30 Jan 2025 13:17:02 +0000 (UTC) Subject: [nginx] Configure: fixed --with-libatomic=DIR with recent libatomic_ops. Message-ID: <20250130131702.9C3F247D0C@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/e715202220e2260a8ed125eacf5230d1c1eaeec8 branches: master commit: e715202220e2260a8ed125eacf5230d1c1eaeec8 user: Sergey Kandaurov date: Fri, 17 Jan 2025 17:55:21 +0400 description: Configure: fixed --with-libatomic=DIR with recent libatomic_ops. The build location of the resulting libatomic_ops.a was changed in v7.4.0 after converting libatomic_ops to use libtool. The fix is to use library from the install path, this allows building with both old and new versions. Initially reported here: https://mailman.nginx.org/pipermail/nginx/2018-April/056054.html --- auto/lib/libatomic/conf | 4 ++-- auto/lib/libatomic/make | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/auto/lib/libatomic/conf b/auto/lib/libatomic/conf index 8c8cb438b..dfdc1a622 100644 --- a/auto/lib/libatomic/conf +++ b/auto/lib/libatomic/conf @@ -7,8 +7,8 @@ if [ $NGX_LIBATOMIC != YES ]; then have=NGX_HAVE_LIBATOMIC . auto/have CORE_INCS="$CORE_INCS $NGX_LIBATOMIC/src" - LINK_DEPS="$LINK_DEPS $NGX_LIBATOMIC/src/libatomic_ops.a" - CORE_LIBS="$CORE_LIBS $NGX_LIBATOMIC/src/libatomic_ops.a" + LINK_DEPS="$LINK_DEPS $NGX_LIBATOMIC/build/lib/libatomic_ops.a" + CORE_LIBS="$CORE_LIBS $NGX_LIBATOMIC/build/lib/libatomic_ops.a" else diff --git a/auto/lib/libatomic/make b/auto/lib/libatomic/make index c90318ea1..530c746a6 100644 --- a/auto/lib/libatomic/make +++ b/auto/lib/libatomic/make @@ -3,14 +3,19 @@ # Copyright (C) Nginx, Inc. + case $NGX_LIBATOMIC in + /*) ngx_prefix="$NGX_LIBATOMIC/build" ;; + *) ngx_prefix="$PWD/$NGX_LIBATOMIC/build" ;; + esac + cat << END >> $NGX_MAKEFILE -$NGX_LIBATOMIC/src/libatomic_ops.a: $NGX_LIBATOMIC/Makefile - cd $NGX_LIBATOMIC && \$(MAKE) +$NGX_LIBATOMIC/build/lib/libatomic_ops.a: $NGX_LIBATOMIC/Makefile + cd $NGX_LIBATOMIC && \$(MAKE) && \$(MAKE) install $NGX_LIBATOMIC/Makefile: $NGX_MAKEFILE cd $NGX_LIBATOMIC \\ && if [ -f Makefile ]; then \$(MAKE) distclean; fi \\ - && ./configure + && ./configure --prefix=$ngx_prefix END From noreply at nginx.com Thu Jan 30 14:22:02 2025 From: noreply at nginx.com (noreply at nginx.com) Date: Thu, 30 Jan 2025 14:22:02 +0000 (UTC) Subject: [nginx] Misc: moved documentation in generated ZIP archive. Message-ID: <20250130142202.306BC47D0D@pubserv1.nginx> details: https://github.com/nginx/nginx/commit/04914cfbcbab347e13927b5da4b87e3846038563 branches: master commit: 04914cfbcbab347e13927b5da4b87e3846038563 user: Sergey Kandaurov date: Mon, 9 Dec 2024 13:21:03 +0400 description: Misc: moved documentation in generated ZIP archive. The recently added GitHub files now reside in the docs directory. --- misc/GNUmakefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/misc/GNUmakefile b/misc/GNUmakefile index e18a86a63..b7e76b942 100644 --- a/misc/GNUmakefile +++ b/misc/GNUmakefile @@ -93,6 +93,9 @@ zip: export mv $(TEMP)/$(NGINX)/LICENSE $(TEMP)/$(NGINX)/docs.new mv $(TEMP)/$(NGINX)/README.md $(TEMP)/$(NGINX)/docs.new + mv $(TEMP)/$(NGINX)/CODE_OF_CONDUCT.md $(TEMP)/$(NGINX)/docs.new + mv $(TEMP)/$(NGINX)/CONTRIBUTING.md $(TEMP)/$(NGINX)/docs.new + mv $(TEMP)/$(NGINX)/SECURITY.md $(TEMP)/$(NGINX)/docs.new mv $(TEMP)/$(NGINX)/docs/html $(TEMP)/$(NGINX) rm -r $(TEMP)/$(NGINX)/docs