From cce726f4c36f203dc7b9f6efffc1018f0c0eb70a Mon Sep 17 00:00:00 2001 From: dmail Date: Wed, 14 Aug 2024 10:10:55 +0200 Subject: [PATCH 1/7] move https-local --- packages/independent/https-local/CHANGELOG.md | 40 ++ packages/independent/https-local/LICENSE | 21 + packages/independent/https-local/README.md | 481 ++++++++++++++++++ .../demo/install_certificate_authority.mjs | 3 + ...stall_certificate_authority_auto_trust.mjs | 5 + .../https-local/docs/development.md | 55 ++ .../independent/https-local/docs/notes.md | 9 + packages/independent/https-local/package.json | 51 ++ .../scripts/certificate/install_ca.mjs | 7 + .../log_root_certificate_trust.mjs | 16 + .../scripts/certificate/start_node_server.mjs | 24 + .../certificate/trust_root_certificate.mjs | 16 + .../uninstall_certificate_authority.mjs | 5 + .../certificate/untrust_root_certificate.mjs | 15 + .../scripts/hosts/add_localhost_mappings.mjs | 16 + .../hosts/ensure_localhost_mappings.mjs | 9 + .../hosts/remove_localhost_mappings.mjs | 16 + .../hosts/verify_localhost_mappings.mjs | 7 + .../performance/measure_package_import.mjs | 19 + .../performance/measure_package_tarball.mjs | 21 + .../scripts/performance/performance.mjs | 38 ++ .../https-local/scripts/test/test.mjs | 18 + .../https-local/src/certificate_authority.js | 388 ++++++++++++++ .../https-local/src/certificate_request.js | 112 ++++ .../https-local/src/hosts_file_verif.js | 91 ++++ .../src/internal/authority_file_infos.js | 42 ++ .../src/internal/browser_detection.js | 7 + .../certificate_authority_file_urls.js | 96 ++++ .../internal/certificate_data_converter.js | 91 ++++ .../src/internal/certificate_generator.js | 181 +++++++ .../https-local/src/internal/command.js | 9 + .../https-local/src/internal/exec.js | 33 ++ .../https-local/src/internal/forge.js | 5 + .../https-local/src/internal/hosts.js | 9 + .../src/internal/hosts/hosts_utils.js | 5 + .../src/internal/hosts/parse_hosts.js | 141 +++++ .../src/internal/hosts/read_hosts.js | 7 + .../src/internal/hosts/write_hosts.js | 85 ++++ .../src/internal/hosts/write_line_hosts.js | 81 +++ .../src/internal/linux/chrome_linux.js | 74 +++ .../src/internal/linux/firefox_linux.js | 83 +++ .../https-local/src/internal/linux/linux.js | 48 ++ .../src/internal/linux/linux_trust_store.js | 157 ++++++ .../src/internal/linux/nss_linux.js | 39 ++ .../src/internal/mac/chrome_mac.js | 33 ++ .../src/internal/mac/firefox_mac.js | 77 +++ .../https-local/src/internal/mac/mac.js | 58 +++ .../src/internal/mac/mac_keychain.js | 145 ++++++ .../https-local/src/internal/mac/nss_mac.js | 49 ++ .../https-local/src/internal/mac/safari.js | 6 + .../https-local/src/internal/memoize.js | 24 + .../https-local/src/internal/nssdb_browser.js | 358 +++++++++++++ .../https-local/src/internal/platform.js | 13 + .../search_certificate_in_command_output.js | 8 + .../https-local/src/internal/trust_query.js | 4 + .../unsupported_platform.js | 15 + .../src/internal/validity_formatting.js | 79 +++ .../src/internal/windows/chrome_windows.js | 61 +++ .../https-local/src/internal/windows/edge.js | 6 + .../src/internal/windows/firefox_windows.js | 79 +++ .../src/internal/windows/windows.js | 49 ++ .../src/internal/windows/windows_certutil.js | 133 +++++ .../https-local/src/jsenvParameters.js | 14 + packages/independent/https-local/src/main.js | 20 + .../https-local/src/validity_duration.js | 38 ++ .../certificate_generation.test.mjs | 108 ++++ .../hosts_parser/hosts_files/hosts | 13 + .../hosts_files/hosts_after_adding_example | 14 + .../hosts_files/hosts_after_removing_loopback | 13 + .../hosts_parser/hosts_parser.test.mjs | 55 ++ .../install_authority_first_call.test.mjs | 127 +++++ .../install_authority_reuse.test.mjs | 137 +++++ .../root_cert_about_to_expire.test.mjs | 63 +++ .../root_cert_expired.test.mjs | 62 +++ .../try_to_trust.test_manual.mjs | 40 ++ .../hosts_file/try_to_update_hosts.test.mjs | 116 +++++ .../not_trusted_browsers.test.mjs | 88 ++++ .../not_trusted_browsers.test_manual.mjs | 23 + .../request_server_certificate.test.mjs | 57 +++ .../https-local/tests/test_helpers.mjs | 142 ++++++ 80 files changed, 5003 insertions(+) create mode 100644 packages/independent/https-local/CHANGELOG.md create mode 100644 packages/independent/https-local/LICENSE create mode 100644 packages/independent/https-local/README.md create mode 100644 packages/independent/https-local/docs/demo/install_certificate_authority.mjs create mode 100644 packages/independent/https-local/docs/demo/install_certificate_authority_auto_trust.mjs create mode 100644 packages/independent/https-local/docs/development.md create mode 100644 packages/independent/https-local/docs/notes.md create mode 100644 packages/independent/https-local/package.json create mode 100644 packages/independent/https-local/scripts/certificate/install_ca.mjs create mode 100644 packages/independent/https-local/scripts/certificate/log_root_certificate_trust.mjs create mode 100644 packages/independent/https-local/scripts/certificate/start_node_server.mjs create mode 100644 packages/independent/https-local/scripts/certificate/trust_root_certificate.mjs create mode 100644 packages/independent/https-local/scripts/certificate/uninstall_certificate_authority.mjs create mode 100644 packages/independent/https-local/scripts/certificate/untrust_root_certificate.mjs create mode 100644 packages/independent/https-local/scripts/hosts/add_localhost_mappings.mjs create mode 100644 packages/independent/https-local/scripts/hosts/ensure_localhost_mappings.mjs create mode 100644 packages/independent/https-local/scripts/hosts/remove_localhost_mappings.mjs create mode 100644 packages/independent/https-local/scripts/hosts/verify_localhost_mappings.mjs create mode 100644 packages/independent/https-local/scripts/performance/measure_package_import.mjs create mode 100644 packages/independent/https-local/scripts/performance/measure_package_tarball.mjs create mode 100644 packages/independent/https-local/scripts/performance/performance.mjs create mode 100644 packages/independent/https-local/scripts/test/test.mjs create mode 100644 packages/independent/https-local/src/certificate_authority.js create mode 100644 packages/independent/https-local/src/certificate_request.js create mode 100644 packages/independent/https-local/src/hosts_file_verif.js create mode 100644 packages/independent/https-local/src/internal/authority_file_infos.js create mode 100644 packages/independent/https-local/src/internal/browser_detection.js create mode 100644 packages/independent/https-local/src/internal/certificate_authority_file_urls.js create mode 100644 packages/independent/https-local/src/internal/certificate_data_converter.js create mode 100644 packages/independent/https-local/src/internal/certificate_generator.js create mode 100644 packages/independent/https-local/src/internal/command.js create mode 100644 packages/independent/https-local/src/internal/exec.js create mode 100644 packages/independent/https-local/src/internal/forge.js create mode 100644 packages/independent/https-local/src/internal/hosts.js create mode 100644 packages/independent/https-local/src/internal/hosts/hosts_utils.js create mode 100644 packages/independent/https-local/src/internal/hosts/parse_hosts.js create mode 100644 packages/independent/https-local/src/internal/hosts/read_hosts.js create mode 100644 packages/independent/https-local/src/internal/hosts/write_hosts.js create mode 100644 packages/independent/https-local/src/internal/hosts/write_line_hosts.js create mode 100644 packages/independent/https-local/src/internal/linux/chrome_linux.js create mode 100644 packages/independent/https-local/src/internal/linux/firefox_linux.js create mode 100644 packages/independent/https-local/src/internal/linux/linux.js create mode 100644 packages/independent/https-local/src/internal/linux/linux_trust_store.js create mode 100644 packages/independent/https-local/src/internal/linux/nss_linux.js create mode 100644 packages/independent/https-local/src/internal/mac/chrome_mac.js create mode 100644 packages/independent/https-local/src/internal/mac/firefox_mac.js create mode 100644 packages/independent/https-local/src/internal/mac/mac.js create mode 100644 packages/independent/https-local/src/internal/mac/mac_keychain.js create mode 100644 packages/independent/https-local/src/internal/mac/nss_mac.js create mode 100644 packages/independent/https-local/src/internal/mac/safari.js create mode 100644 packages/independent/https-local/src/internal/memoize.js create mode 100644 packages/independent/https-local/src/internal/nssdb_browser.js create mode 100644 packages/independent/https-local/src/internal/platform.js create mode 100644 packages/independent/https-local/src/internal/search_certificate_in_command_output.js create mode 100644 packages/independent/https-local/src/internal/trust_query.js create mode 100644 packages/independent/https-local/src/internal/unsupported_platform/unsupported_platform.js create mode 100644 packages/independent/https-local/src/internal/validity_formatting.js create mode 100644 packages/independent/https-local/src/internal/windows/chrome_windows.js create mode 100644 packages/independent/https-local/src/internal/windows/edge.js create mode 100644 packages/independent/https-local/src/internal/windows/firefox_windows.js create mode 100644 packages/independent/https-local/src/internal/windows/windows.js create mode 100644 packages/independent/https-local/src/internal/windows/windows_certutil.js create mode 100644 packages/independent/https-local/src/jsenvParameters.js create mode 100644 packages/independent/https-local/src/main.js create mode 100644 packages/independent/https-local/src/validity_duration.js create mode 100644 packages/independent/https-local/tests/__internal__/certificate_generation/certificate_generation.test.mjs create mode 100644 packages/independent/https-local/tests/__internal__/hosts_parser/hosts_files/hosts create mode 100644 packages/independent/https-local/tests/__internal__/hosts_parser/hosts_files/hosts_after_adding_example create mode 100644 packages/independent/https-local/tests/__internal__/hosts_parser/hosts_files/hosts_after_removing_loopback create mode 100644 packages/independent/https-local/tests/__internal__/hosts_parser/hosts_parser.test.mjs create mode 100644 packages/independent/https-local/tests/authority_certificate/install_authority_first_call.test.mjs create mode 100644 packages/independent/https-local/tests/authority_certificate/install_authority_reuse.test.mjs create mode 100644 packages/independent/https-local/tests/authority_certificate/root_cert_about_to_expire.test.mjs create mode 100644 packages/independent/https-local/tests/authority_certificate/root_cert_expired.test.mjs create mode 100644 packages/independent/https-local/tests/authority_certificate/try_to_trust.test_manual.mjs create mode 100644 packages/independent/https-local/tests/hosts_file/try_to_update_hosts.test.mjs create mode 100644 packages/independent/https-local/tests/server_certificate/not_trusted_browsers.test.mjs create mode 100644 packages/independent/https-local/tests/server_certificate/not_trusted_browsers.test_manual.mjs create mode 100644 packages/independent/https-local/tests/server_certificate/request_server_certificate.test.mjs create mode 100644 packages/independent/https-local/tests/test_helpers.mjs diff --git a/packages/independent/https-local/CHANGELOG.md b/packages/independent/https-local/CHANGELOG.md new file mode 100644 index 0000000000..27da45729a --- /dev/null +++ b/packages/independent/https-local/CHANGELOG.md @@ -0,0 +1,40 @@ +# 3.0.6 + +- Fix certutil command on windows + +# 3.0.4 + +- proper fix for firefox nss database + +# 3.0.3 + +- update deps to fix firefox nssb not found on mac + +# 3.0.2 + +- add nssdb store paths on linux +- improve firefox and chrome detection on linux +- update dependencies and devDependencies +- update mac os to 2022 in github workflow + +# 3.0.1 + +- fix firefox is running detection (could return true because of playwright) + +# 3.0.0 + +- requestCertificateForLocalhost renamed requestCertificate +- do not force "localhost" in altNames anymore + +# 2.1.0 + +- installCertificateAuthority properly retrust root cert on macOS + +# 2.0.0 + +- requestCertificateForLocalhost changes + - becomes sync + - serverCertificateAltNames renamed altNames + - serverCertificateValidityDurationInMs renamed validityDurationInMs + - serverCertificateCommonName renamed commonName + - returns { certificate, privateKey } instead of { serverCertificate, serverPrivateKey } diff --git a/packages/independent/https-local/LICENSE b/packages/independent/https-local/LICENSE new file mode 100644 index 0000000000..311d2eaf9d --- /dev/null +++ b/packages/independent/https-local/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 jsenv + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/independent/https-local/README.md b/packages/independent/https-local/README.md new file mode 100644 index 0000000000..3c3d2a7b8a --- /dev/null +++ b/packages/independent/https-local/README.md @@ -0,0 +1,481 @@ +# https local [![npm package](https://img.shields.io/npm/v/@jsenv/https-local.svg?logo=npm&label=package)](https://www.npmjs.com/package/@jsenv/https-local) + +A programmatic way to generate locally trusted certificates. + +Generate certificate(s) trusted by your operating system and browsers. +This certificate can be used to start your development server in HTTPS. +Works on mac, linux and windows. + +# How to use + +1 - Install _@jsenv/https-local_ + +```console +npm install --save-dev @jsenv/https-local +``` + +2 - Create _install_certificate_authority.mjs_ + +```js +/* + * This file needs to be executed once. + * After that the root certificate is valid for 20 years. + * Re-executing this file will log the current root certificate validity and trust status. + * Re-executing this file 20 years later would reinstall a root certificate and re-trust it. + * + * Read more in https://github.com/jsenv/https-local#installCertificateAuthority + */ + +import { + installCertificateAuthority, + verifyHostsFile, +} from "@jsenv/https-local" + +await installCertificateAuthority({ + tryToTrust: true, + NSSDynamicInstall: true, +}) +await verifyHostsFile({ + ipMappings: { + "127.0.0.1": ["localhost"], + }, + tryToUpdatesHostsFile: true, +}) +``` + +3 - Run with node + +```console +node ./install_certificate_authority.mjs +``` + +4 - Create _start_dev_server.mjs_ + +```js +/* + * This file uses "@jsenv/https-local" to obtain a certificate used to start a server in https. + * The certificate is valid for 1 year (396 days) and is issued by a certificate authority trusted on this machine. + * If the certificate authority was not installed before executing this file, an error is thrown + * explaining that certificate authority must be installed first. + * + * To install the certificate authority, you can use the following command + * + * > node ./install_certificate_authority.mjs + * + * Read more in https://github.com/jsenv/https-local#requestCertificate + */ + +import { createServer } from "node:https" +import { requestCertificate } from "@jsenv/https-local" + +const { certificate, privateKey } = requestCertificate() + +const server = createServer( + { + cert: certificate, + key: privateKey, + }, + (request, response) => { + const body = "Hello world" + response.writeHead(200, { + "content-type": "text/plain", + "content-length": Buffer.byteLength(body), + }) + response.write(body) + response.end() + }, +) +server.listen(8080) +console.log(`Server listening at https://local.example:8080`) +``` + +5 - Start server with node + +```console +node ./start_dev_server.mjs +``` + +At this stage you have a server running in https. +The rest of this documentation goes into more details. + +# Certificate expiration + +| Certificate | Expires after | How to renew? | +| ----------- | ------------- | ------------------------------------ | +| server | 1 year | Re-run _requestCertificate_ | +| authority | 20 year | Re-run _installCertificateAuthority_ | + +The **server certificate** expires after one year which is the maximum duration allowed by web browsers. +In the unlikely scenario where a local server is running for more than a year without interruption, restart it to re-run requestCertificate. + +The **authority root certificate** expires after 20 years which is close to the maximum allowed duration. +In the very unlikely scenario where you are using the same machine for more than 20 years, re-execute [installCertificateAuthority](#installCertificateAuthority) to update certificate authority then restart your server. + +# installCertificateAuthority + +_installCertificateAuthority_ function generates a certificate authority valid for 20 years. +This certificate authority is needed to generate local certificates that will be trusted by the operating system and web browsers. + +```js +import { installCertificateAuthority } from "@jsenv/https-local" + +await installCertificateAuthority() +``` + +By default, trusting authority root certificate is a manual process. This manual process is documented in [BenMorel/dev-certificates#Import the CA in your browser](https://github.com/BenMorel/dev-certificates/tree/c10cd68945da772f31815b7a36721ddf848ff3a3#import-the-ca-in-your-browser). This process can be done programmatically as explained in [Auto trust](#Auto-trust). + +Find below logs written in terminal when this function is executed. + +
+ mac + +```console +> node ./install_certificate_authority.mjs + +ℹ authority root certificate not found in filesystem +Generating authority root certificate with a validity of 20 years... +✔ authority root certificate written at /Users/dmail/https_local/http_local_root_certificate.crt +ℹ You should add root certificate to mac keychain +ℹ You should add root certificate to firefox +``` + +_second execution logs_ + +```console +> node ./install_certificate_authority.mjs + +✔ authority root certificate found in filesystem +Checking certificate validity... +✔ certificate still valid for 19 years +Detect if certificate attributes have changed... +✔ certificate attributes are the same +Check if certificate is in mac keychain... +ℹ certificate not found in mac keychain +Check if certificate is in firefox... +ℹ certificate not found in firefox +``` + +
+ +
+ linux + +```console +> node ./install_certificate_authority.mjs + +ℹ authority root certificate not found in filesystem +Generating authority root certificate with a validity of 20 years... +✔ authority root certificate written at /home/dmail/.config/https_local/https_local_root_certificate.crt +ℹ You should add certificate to linux +ℹ You should add certificate to chrome +ℹ You should add certificate to firefox +``` + +_second execution logs_ + +```console +> node ./install_certificate_authority.mjs + +✔ authority root certificate found in filesystem +Checking certificate validity... +✔ certificate still valid for 19 years +Detect if certificate attributes have changed... +✔ certificate attributes are the same +Check if certificate is in linux... +ℹ certificate in linux is outdated +Check if certificate is in chrome... +ℹ certificate not found in chrome +Check if certificate is in firefox... +ℹ certificate not found in firefox +``` + +
+ +
+ windows + +```console +> node ./install_certificate_authority.mjs + +ℹ authority root certificate not found in filesystem +Generating authority root certificate with a validity of 20 years... +✔ authority root certificate written at C:\Users\Dmail\AppData\Local\https_local\https_local_root_certificate.crt +ℹ You should add certificate to windows +ℹ You should add certificate to firefox +``` + +_second execution logs_ + +```console +> node ./install_certificate_authority.mjs + +✔ authority root certificate found in filesystem +Checking certificate validity... +✔ certificate still valid for 19 years +Detect if certificate attributes have changed... +✔ certificate attributes are the same +Check if certificate is trusted by windows... +ℹ certificate is not trusted by windows +Check if certificate is trusted by firefox... +ℹ unable to detect if certificate is trusted by firefox (not implemented on windows) +``` + +
+ +## Auto trust + +It's possible to trust root certificate programmatically using _tryToTrust_ + +```js +import { installCertificateAuthority } from "@jsenv/https-local" + +await installCertificateAuthority({ + tryToTrust: true, +}) +``` + +
+ mac + +```console +> node ./install_certificate_authority.mjs + +ℹ authority root certificate not found in filesystem +Generating authority root certificate with a validity of 20 years... +✔ authority root certificate written at /Users/dmail/https_local/https_local_root_certificate.crt +Adding certificate to mac keychain... +❯ sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "/Users/dmail/https_local/https_local_root_certificate.crt" +Password: +✔ certificate added to mac keychain +Adding certificate to firefox... +✔ certificate added to Firefox +``` + +_second execution logs_ + +```console +> node ./install_certificate_authority.mjs + +✔ authority root certificate found in filesystem +Checking certificate validity... +✔ certificate still valid for 19 years +Detect if certificate attributes have changed... +✔ certificate attributes are the same +Check if certificate is in mac keychain... +✔ certificate found in mac keychain +Check if certificate is in Firefox... +✔ certificate found in Firefox +``` + +
+ +
+ linux + +```console +> node ./install_certificate_authority.mjs + +✔ authority root certificate found in filesystem +Checking certificate validity... +✔ certificate still valid for 19 years +Detect if certificate attributes have changed... +✔ certificate attributes are the same +Check if certificate is in linux... +ℹ certificate not in linux +Adding certificate to linux... +❯ sudo /bin/cp -f "/home/dmail/.config/https_local/https_local_root_certificate.crt" /usr/local/share/ca-certificates/https_local_root_certificate.crt +[sudo] Password for dmail : +❯ sudo update-ca-certificates +✔ certificate added to linux +Check if certificate is in chrome... +ℹ certificate not found in chrome +Adding certificate to chrome... +✔ certificate added to chrome +Check if certificate is in firefox... +ℹ certificate not found in firefox +Adding certificate to firefox... +✔ certificate added to firefox +``` + +_second execution logs_ + +```console +> node ./install_certificate_authority.mjs + +✔ authority root certificate found in filesystem +Checking certificate validity... +✔ certificate still valid for 19 years +Detect if certificate attributes have changed... +✔ certificate attributes are the same +Check if certificate is in linux... +✔ certificate found in linux +Check if certificate is in chrome... +✔ certificate found in chrome +Check if certificate is in firefox... +✔ certificate found in firefox +``` + +
+ +
+ windows + +```console +> node ./install_certificate_authority.mjs + +✔ authority root certificate found in filesystem +Checking certificate validity... +✔ certificate still valid for 19 years +Detect if certificate attributes have changed... +✔ certificate attributes are the same +Check if certificate is trusted by windows... +ℹ certificate not trusted by windows +Adding certificate to windows... +❯ certutil -addstore -user root C:\Users\Dmail\AppData\Local\https_local\https_local_root_certificate.crt +✔ certificate added to windows +Check if certificate is trusted by firefox... +ℹ unable to detect if certificate is trusted by firefox (not implemented on windows) +``` + +_second execution logs_ + +```console +> node ./install_certificate_authority.mjs + +✔ authority root certificate found in filesystem +Checking certificate validity... +✔ certificate still valid for 19 years +Detect if certificate attributes have changed... +✔ certificate attributes are the same +Check if certificate is trusted by windows... +✔ certificate trusted by windows +Check if certificate is trusted by firefox... +ℹ unable to detect if certificate is trusted by firefox (not implemented on windows) +``` + +
+ +# requestCertificate + +_requestCertificate_ function returns a certificate and private key that can be used to start a server in HTTPS. + +```js +import { createServer } from "node:https" +import { requestCertificate } from "@jsenv/https-local" + +const { certificate, privateKey } = requestCertificate({ + altNames: ["localhost", "local.example"], +}) +``` + +[installCertificateAuthority](#installCertificateAuthority) must be called before this function. + +# verifyHostsFile + +This function is not mandatory to obtain the https certificates. +But it is useful to programmatically verify ip mappings that are important for your local server are present in hosts file. + +```js +import { verifyHostsFile } from "@jsenv/https-local" + +await verifyHostsFile({ + ipMappings: { + "127.0.0.1": ["localhost", "local.example"], + }, +}) +``` + +Find below logs written in terminal when this function is executed. + +
+ mac and linux + +```console +> node ./verify_hosts.mjs + +Check hosts file content... +⚠ 1 mapping is missing in hosts file +--- hosts file path --- +/etc/hosts +--- line(s) to add --- +127.0.0.1 localhost local.example +``` + +
+ +
+ windows + +```console +> node ./verify_hosts.mjs + +Check hosts file content... +⚠ 1 mapping is missing in hosts file +--- hosts file path --- +C:\\Windows\\System32\\Drivers\\etc\\hosts +--- line(s) to add --- +127.0.0.1 localhost local.example +``` + +
+ +## Auto update hosts + +It's possible to update hosts file programmatically using _tryToUpdateHostsFile_. + +```js +import { verifyHostsFile } from "@jsenv/https-local" + +await verifyHostsFile({ + ipMappings: { + "127.0.0.1": ["localhost", "local.example"], + }, + tryToUpdateHostsFile: true, +}) +``` + +
+ mac and linux + +```console +Check hosts file content... +ℹ 1 mapping is missing in hosts file +Adding 1 mapping(s) in hosts file... +❯ echo "127.0.0.1 local.example" | sudo tee -a /etc/hosts +Password: +✔ mappings added to hosts file +``` + +_Second execution logs_ + +```console +> node ./verify_hosts.mjs + +Check hosts file content... +✔ all ip mappings found in hosts file +``` + +
+ +
+ windows + +```console +Check hosts file content... +ℹ 1 mapping is missing in hosts file +Adding 1 mapping(s) in hosts file... +❯ (echo 127.0.0.1 local.example) >> C:\\Windows\\System32\\Drivers\\etc\\hosts +Password: +✔ mappings added to hosts file +``` + +_Second execution logs_ + +```console +> node ./verify_hosts.mjs + +Check hosts file content... +✔ all ip mappings found in hosts file +``` + +
diff --git a/packages/independent/https-local/docs/demo/install_certificate_authority.mjs b/packages/independent/https-local/docs/demo/install_certificate_authority.mjs new file mode 100644 index 0000000000..55029b9e8d --- /dev/null +++ b/packages/independent/https-local/docs/demo/install_certificate_authority.mjs @@ -0,0 +1,3 @@ +import { installCertificateAuthority } from "@jsenv/https-local" + +await installCertificateAuthority() diff --git a/packages/independent/https-local/docs/demo/install_certificate_authority_auto_trust.mjs b/packages/independent/https-local/docs/demo/install_certificate_authority_auto_trust.mjs new file mode 100644 index 0000000000..a10fbfb81a --- /dev/null +++ b/packages/independent/https-local/docs/demo/install_certificate_authority_auto_trust.mjs @@ -0,0 +1,5 @@ +import { installCertificateAuthority } from "@jsenv/https-local" + +await installCertificateAuthority({ + tryToTrust: true, +}) diff --git a/packages/independent/https-local/docs/development.md b/packages/independent/https-local/docs/development.md new file mode 100644 index 0000000000..3a752fa56a --- /dev/null +++ b/packages/independent/https-local/docs/development.md @@ -0,0 +1,55 @@ + + +# Development + +This document describes the process for running this application on your local computer. + +# Setup + +**Operating System**: Mac, Linux or Windows. + +**Command line tools**: + +- [git](https://git-scm.com/) version 2.26.0 or above +- [node](https://nodejs.org/en/) version 14.17.0 or above + +Then, run the following commands: + +```console +git clone git@github.com:jsenv/jsenv-template-node-package.git +``` + +```console +cd ./jsenv-template-node-package +``` + +```console +npm install +``` + +# Contribution lifecycle + +Expected steps from the moment you start coding to the moment it gets merged on the main branch. + +It's not strictly necessary to run ESLint, prettier, tests locally while developing: You can always open a pull request and rely on the GitHub workflow to run them for you, but it's recommended to run them locally before pushing your changes. + +Create a branch + +```console +git checkout -b branch-name +``` + +Open your code editor (VSCode for example) + +```console +code . +``` + +Do your changes, create your commits and push them whenever you want + +```console +git commit -m "commit message" +git push +``` + +Create a pull request as documented in [Creating a pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request). Feel free to create [draft pull request](https://github.blog/2019-02-14-introducing-draft-pull-requests/) and put it as ready for review when you are done. diff --git a/packages/independent/https-local/docs/notes.md b/packages/independent/https-local/docs/notes.md new file mode 100644 index 0000000000..300d1cd244 --- /dev/null +++ b/packages/independent/https-local/docs/notes.md @@ -0,0 +1,9 @@ +| Tool | How to trust root cert | +| ------- | ------------------------------------------------------------------------------------------- | +| Firefox | https://wiki.mozilla.org/PSM:Changing_Trust_Settings | +| Mac | https://support.apple.com/guide/keychain-access/add-certificates-to-a-keychain-kyca2431/mac | + +See also + +- https://github.com/BenMorel/dev-certificates +- https://manuals.gfi.com/en/kerio/connect/content/server-configuration/ssl-certificates/adding-trusted-root-certificates-to-the-server-1605.html diff --git a/packages/independent/https-local/package.json b/packages/independent/https-local/package.json new file mode 100644 index 0000000000..45afe8a043 --- /dev/null +++ b/packages/independent/https-local/package.json @@ -0,0 +1,51 @@ +{ + "name": "@jsenv/https-local", + "version": "3.1.0", + "description": "A programmatic way to generate locally trusted certificates", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/jsenv/core", + "directory": "packages/independent/https-local" + }, + "publishConfig": { + "access": "public" + }, + "engines": { + "node": ">=20.0.0" + }, + "type": "module", + "exports": { + ".": { + "import": "./src/main.js" + }, + "./*": "./*" + }, + "main": "./src/main.js", + "files": [ + "/src/" + ], + "scripts": { + "test": "node ./scripts/test/test.mjs", + "performance": "node --expose-gc ./scripts/performance/performance.mjs --local --log", + "test:start-node-server": "node ./scripts/certificate/start_node_server.mjs", + "ca:install": "node ./scripts/certificate/install_ca.mjs", + "ca:log-trust": "node ./scripts/certificate/log_root_certificate_trust.mjs", + "ca:trust": "node ./scripts/certificate/trust_root_certificate.mjs", + "ca:untrust": "node ./scripts/certificate/untrust_root_certificate.mjs", + "ca:uninstall": "node ./scripts/certificate/uninstall_certificate_authority.mjs", + "hosts:add-localhost-mappings": "node ./scripts/hosts/add_localhost_mappings.mjs", + "hosts:remove-localhost-mappings": "node ./scripts/hosts/remove_localhost_mappings.mjs", + "hosts:verify-localhost-mappings": "node ./scripts/hosts/verify_localhost_mappings.mjs", + "hosts:ensure-localhost-mappings": "node ./scripts/hosts/ensure_localhost_mappings.mjs" + }, + "dependencies": { + "@jsenv/filesystem": "4.1.9", + "@jsenv/log": "3.3.2", + "@jsenv/urls": "1.2.8", + "command-exists": "1.2.9", + "node-forge": "1.3.1", + "sudo-prompt": "9.2.1", + "which": "3.0.0" + } +} diff --git a/packages/independent/https-local/scripts/certificate/install_ca.mjs b/packages/independent/https-local/scripts/certificate/install_ca.mjs new file mode 100644 index 0000000000..dcfe8d7ce2 --- /dev/null +++ b/packages/independent/https-local/scripts/certificate/install_ca.mjs @@ -0,0 +1,7 @@ +import { installCertificateAuthority } from "@jsenv/https-local" + +await installCertificateAuthority({ + logLevel: "debug", + tryToTrust: false, + NSSDynamicInstall: true, +}) diff --git a/packages/independent/https-local/scripts/certificate/log_root_certificate_trust.mjs b/packages/independent/https-local/scripts/certificate/log_root_certificate_trust.mjs new file mode 100644 index 0000000000..2b3c1bf8ba --- /dev/null +++ b/packages/independent/https-local/scripts/certificate/log_root_certificate_trust.mjs @@ -0,0 +1,16 @@ +import { readFile } from "@jsenv/filesystem"; +import { getCertificateAuthorityFileUrls } from "@jsenv/https-local/src/internal/certificate_authority_file_urls.js"; +import { importPlatformMethods } from "@jsenv/https-local/src/internal/platform.js"; +import { jsenvParameters } from "@jsenv/https-local/src/jsenvParameters.js"; +import { createLogger } from "@jsenv/humanize"; + +const { rootCertificateFileUrl } = getCertificateAuthorityFileUrls(); +const { executeTrustQuery } = await importPlatformMethods(); +const trustInfo = await executeTrustQuery({ + logger: createLogger({ logLevel: "debug" }), + certificateCommonName: jsenvParameters.certificateCommonName, + certificateFileUrl: rootCertificateFileUrl, + certificate: await readFile(rootCertificateFileUrl, { as: "string" }), + verb: "CHECK_TRUST", +}); +console.log(trustInfo); diff --git a/packages/independent/https-local/scripts/certificate/start_node_server.mjs b/packages/independent/https-local/scripts/certificate/start_node_server.mjs new file mode 100644 index 0000000000..7a901f836b --- /dev/null +++ b/packages/independent/https-local/scripts/certificate/start_node_server.mjs @@ -0,0 +1,24 @@ +import { createServer } from "node:https" +import { requestCertificate } from "@jsenv/https-local" + +const { certificate, privateKey } = requestCertificate({ + altNames: ["localhost", "local.example"], +}) + +const server = createServer( + { + cert: certificate, + key: privateKey, + }, + (request, response) => { + const body = "Hello world" + response.writeHead(200, { + "content-type": "text/plain", + "content-length": Buffer.byteLength(body), + }) + response.write(body) + response.end() + }, +) +server.listen(8080) +console.log(`Server listening at https://local.example:8080`) diff --git a/packages/independent/https-local/scripts/certificate/trust_root_certificate.mjs b/packages/independent/https-local/scripts/certificate/trust_root_certificate.mjs new file mode 100644 index 0000000000..4ed1840201 --- /dev/null +++ b/packages/independent/https-local/scripts/certificate/trust_root_certificate.mjs @@ -0,0 +1,16 @@ +import { readFile } from "@jsenv/filesystem"; +import { getCertificateAuthorityFileUrls } from "@jsenv/https-local/src/internal/certificate_authority_file_urls.js"; +import { importPlatformMethods } from "@jsenv/https-local/src/internal/platform.js"; +import { jsenvParameters } from "@jsenv/https-local/src/jsenvParameters.js"; +import { createLogger } from "@jsenv/humanize"; + +const { rootCertificateFileUrl } = getCertificateAuthorityFileUrls(); +const { executeTrustQuery } = await importPlatformMethods(); +await executeTrustQuery({ + logger: createLogger({ logLevel: "debug" }), + certificateCommonName: jsenvParameters.certificateCommonName, + certificateFileUrl: rootCertificateFileUrl, + certificate: await readFile(rootCertificateFileUrl, { as: "string" }), + verb: "ADD_TRUST", + NSSDynamicInstall: true, +}); diff --git a/packages/independent/https-local/scripts/certificate/uninstall_certificate_authority.mjs b/packages/independent/https-local/scripts/certificate/uninstall_certificate_authority.mjs new file mode 100644 index 0000000000..9c493f1868 --- /dev/null +++ b/packages/independent/https-local/scripts/certificate/uninstall_certificate_authority.mjs @@ -0,0 +1,5 @@ +import { uninstallCertificateAuthority } from "@jsenv/https-local" + +await uninstallCertificateAuthority({ + logLevel: "debug", +}) diff --git a/packages/independent/https-local/scripts/certificate/untrust_root_certificate.mjs b/packages/independent/https-local/scripts/certificate/untrust_root_certificate.mjs new file mode 100644 index 0000000000..18ff35efae --- /dev/null +++ b/packages/independent/https-local/scripts/certificate/untrust_root_certificate.mjs @@ -0,0 +1,15 @@ +import { readFile } from "@jsenv/filesystem"; +import { getCertificateAuthorityFileUrls } from "@jsenv/https-local/src/internal/certificate_authority_file_urls.js"; +import { importPlatformMethods } from "@jsenv/https-local/src/internal/platform.js"; +import { jsenvParameters } from "@jsenv/https-local/src/jsenvParameters.js"; +import { createLogger } from "@jsenv/humanize"; + +const { rootCertificateFileUrl } = getCertificateAuthorityFileUrls(); +const { executeTrustQuery } = await importPlatformMethods(); +await executeTrustQuery({ + logger: createLogger({ logLevel: "debug" }), + certificateCommonName: jsenvParameters.certificateCommonName, + certificateFileUrl: rootCertificateFileUrl, + certificate: await readFile(rootCertificateFileUrl, { as: "string" }), + verb: "REMOVE_TRUST", +}); diff --git a/packages/independent/https-local/scripts/hosts/add_localhost_mappings.mjs b/packages/independent/https-local/scripts/hosts/add_localhost_mappings.mjs new file mode 100644 index 0000000000..274dac81bf --- /dev/null +++ b/packages/independent/https-local/scripts/hosts/add_localhost_mappings.mjs @@ -0,0 +1,16 @@ +import { + readHostsFile, + parseHosts, + writeHostsFile, +} from "@jsenv/https-local/src/internal/hosts.js" + +const hostsFileContent = await readHostsFile() +const hostnames = parseHosts(hostsFileContent) +const localIpHostnames = hostnames.getIpHostnames("127.0.0.1") +if (!localIpHostnames.includes("localhost")) { + hostnames.addIpHostname("127.0.0.1", "localhost") +} +if (!localIpHostnames.includes("local.example")) { + hostnames.addIpHostname("127.0.0.1", "local.example") +} +await writeHostsFile(hostnames.asFileContent()) diff --git a/packages/independent/https-local/scripts/hosts/ensure_localhost_mappings.mjs b/packages/independent/https-local/scripts/hosts/ensure_localhost_mappings.mjs new file mode 100644 index 0000000000..bc20b7cc7f --- /dev/null +++ b/packages/independent/https-local/scripts/hosts/ensure_localhost_mappings.mjs @@ -0,0 +1,9 @@ +import { verifyHostsFile } from "@jsenv/https-local" + +await verifyHostsFile({ + logLevel: "debug", + ipMappings: { + "127.0.0.1": ["localhost", "local.example"], + }, + tryToUpdateHostsFile: true, +}) diff --git a/packages/independent/https-local/scripts/hosts/remove_localhost_mappings.mjs b/packages/independent/https-local/scripts/hosts/remove_localhost_mappings.mjs new file mode 100644 index 0000000000..bcc8638a0b --- /dev/null +++ b/packages/independent/https-local/scripts/hosts/remove_localhost_mappings.mjs @@ -0,0 +1,16 @@ +import { + readHostsFile, + parseHosts, + writeHostsFile, +} from "@jsenv/https-local/src/internal/hosts.js" + +const hostsFileContent = await readHostsFile() +const hostnames = parseHosts(hostsFileContent) +const localIpHostnames = hostnames.getIpHostnames("127.0.0.1") +if (localIpHostnames.includes("localhost")) { + hostnames.removeIpHostname("127.0.0.1", "localhost") +} +if (localIpHostnames.includes("local.example.com")) { + hostnames.removeIpHostname("127.0.0.1", "local.example") +} +await writeHostsFile(hostnames.asFileContent()) diff --git a/packages/independent/https-local/scripts/hosts/verify_localhost_mappings.mjs b/packages/independent/https-local/scripts/hosts/verify_localhost_mappings.mjs new file mode 100644 index 0000000000..582a33f4e9 --- /dev/null +++ b/packages/independent/https-local/scripts/hosts/verify_localhost_mappings.mjs @@ -0,0 +1,7 @@ +import { verifyHostsFile } from "@jsenv/https-local" + +await verifyHostsFile({ + ipMappings: { + "127.0.0.1": ["localhost", "local.example"], + }, +}) diff --git a/packages/independent/https-local/scripts/performance/measure_package_import.mjs b/packages/independent/https-local/scripts/performance/measure_package_import.mjs new file mode 100644 index 0000000000..6d03f6d9e7 --- /dev/null +++ b/packages/independent/https-local/scripts/performance/measure_package_import.mjs @@ -0,0 +1,19 @@ +import { startMeasures } from "@jsenv/performance-impact" + +const measures = startMeasures({ + gc: true, + memoryHeap: true, +}) + +// eslint-disable-next-line no-unused-vars +let namespace = await import("@jsenv/https-local") + +const { duration, memoryHeapUsed } = measures.stop() + +export const packageImportMetrics = { + "import duration": { value: duration, unit: "ms" }, + "import memory heap used": { + value: Math.max(memoryHeapUsed, 0), + unit: "byte", + }, +} diff --git a/packages/independent/https-local/scripts/performance/measure_package_tarball.mjs b/packages/independent/https-local/scripts/performance/measure_package_tarball.mjs new file mode 100644 index 0000000000..741538bdeb --- /dev/null +++ b/packages/independent/https-local/scripts/performance/measure_package_tarball.mjs @@ -0,0 +1,21 @@ +import { exec } from "node:child_process" + +const npmPackInfo = await new Promise((resolve, reject) => { + exec(`npm pack --dry-run --json`, (error, stdout) => { + if (error) { + reject(error) + } else { + resolve(JSON.parse(stdout)) + } + }) +}) +const npmTarballInfo = npmPackInfo[0] + +export const packageTarballmetrics = { + "npm tarball size": { value: npmTarballInfo.size, unit: "byte" }, + "npm tarball unpacked size": { + value: npmTarballInfo.unpackedSize, + unit: "byte", + }, + "npm tarball file count": { value: npmTarballInfo.entryCount }, +} diff --git a/packages/independent/https-local/scripts/performance/performance.mjs b/packages/independent/https-local/scripts/performance/performance.mjs new file mode 100644 index 0000000000..091d83bc23 --- /dev/null +++ b/packages/independent/https-local/scripts/performance/performance.mjs @@ -0,0 +1,38 @@ +/* + * This file is designed to be executed locally or by an automated process. + * + * To run it locally, use one of + * - node --expose-gc ./scripts/performance/generate_performance_report.mjs --log + * - npm run measure-performances + * + * The automated process is a GitHub workflow: ".github/workflows/performance_impact.yml" + * It will dynamically import this file and get the "performanceReport" export + * + * See https://github.com/jsenv/performance-impact + */ + +import { importMetricFromFiles } from "@jsenv/performance-impact" + +const { packageImportMetrics, packageTarballMetrics } = + await importMetricFromFiles({ + directoryUrl: new URL("./", import.meta.url), + metricsDescriptions: { + packageImportMetrics: { + file: "./measure_package_import.mjs#packageImportMetrics", + iterations: process.argv.includes("--local") ? 1 : 7, + msToWaitBetweenEachIteration: 500, + }, + packageTarballMetrics: { + file: "./measure_package_tarball.mjs#packageTarballmetrics", + iterations: 1, + }, + }, + logLevel: process.argv.includes("--log") ? "info" : "warn", + }) + +export const performanceReport = { + "package metrics": { + ...packageImportMetrics, + ...packageTarballMetrics, + }, +} diff --git a/packages/independent/https-local/scripts/test/test.mjs b/packages/independent/https-local/scripts/test/test.mjs new file mode 100644 index 0000000000..6b75d3fac1 --- /dev/null +++ b/packages/independent/https-local/scripts/test/test.mjs @@ -0,0 +1,18 @@ +/* + * This file uses "@jsenv/core" to execute all test files. + * See https://github.com/jsenv/jsenv-core/blob/master/docs/testing/readme.md#jsenv-test-runner + */ + +import { executeTestPlan, nodeChildProcess } from "@jsenv/test"; + +await executeTestPlan({ + rootDirectoryUrl: new URL("../../", import.meta.url), + testPlan: { + "tests/**/*.test.mjs": { + node: { + runtime: nodeChildProcess, + }, + }, + }, + coverage: process.argv.includes("--coverage"), +}); diff --git a/packages/independent/https-local/src/certificate_authority.js b/packages/independent/https-local/src/certificate_authority.js new file mode 100644 index 0000000000..858d0a5b6a --- /dev/null +++ b/packages/independent/https-local/src/certificate_authority.js @@ -0,0 +1,388 @@ +import { readFile, removeEntry, writeFile } from "@jsenv/filesystem"; +import { UNICODE, createDetailedMessage, createLogger } from "@jsenv/humanize"; +import { getAuthorityFileInfos } from "./internal/authority_file_infos.js"; +import { attributeDescriptionFromAttributeArray } from "./internal/certificate_data_converter.js"; +import { createAuthorityRootCertificate } from "./internal/certificate_generator.js"; +import { forge } from "./internal/forge.js"; +import { importPlatformMethods } from "./internal/platform.js"; +import { + formatDuration, + formatTimeDelta, +} from "./internal/validity_formatting.js"; +import { jsenvParameters } from "./jsenvParameters.js"; +import { verifyRootCertificateValidityDuration } from "./validity_duration.js"; + +export const installCertificateAuthority = async ({ + logLevel, + logger = createLogger({ logLevel }), + + certificateCommonName = jsenvParameters.certificateCommonName, + certificateValidityDurationInMs = jsenvParameters.certificateValidityDurationInMs, + + tryToTrust = false, + NSSDynamicInstall = false, + + // for unit tests + aboutToExpireRatio = 0.05, +} = {}) => { + if (typeof certificateCommonName !== "string") { + throw new TypeError( + `certificateCommonName must be a string but received ${certificateCommonName}`, + ); + } + if (typeof certificateValidityDurationInMs !== "number") { + throw new TypeError( + `certificateValidityDurationInMs must be a number but received ${certificateValidityDurationInMs}`, + ); + } + if (certificateValidityDurationInMs < 1) { + throw new TypeError( + `certificateValidityDurationInMs must be > 0 but received ${certificateValidityDurationInMs}`, + ); + } + + const validityDurationInfo = verifyRootCertificateValidityDuration( + certificateValidityDurationInMs, + ); + if (!validityDurationInfo.ok) { + certificateValidityDurationInMs = validityDurationInfo.maxAllowedValue; + logger.warn( + createDetailedMessage(validityDurationInfo.message, { + details: validityDurationInfo.details, + }), + ); + } + + const { + authorityJsonFileInfo, + rootCertificateFileInfo, + rootCertificatePrivateKeyFileInfo, + } = getAuthorityFileInfos(); + const authorityJsonFileUrl = authorityJsonFileInfo.url; + const rootCertificateFileUrl = rootCertificateFileInfo.url; + const rootPrivateKeyFileUrl = rootCertificatePrivateKeyFileInfo.url; + const platformMethods = await importPlatformMethods(); + + const generateRootCertificate = async () => { + logger.info( + `Generating authority root certificate with a validity of ${formatDuration( + certificateValidityDurationInMs, + )}...`, + ); + const { rootCertificateForgeObject, rootCertificatePrivateKeyForgeObject } = + await createAuthorityRootCertificate({ + logger, + commonName: certificateCommonName, + validityDurationInMs: certificateValidityDurationInMs, + serialNumber: 0, + }); + + const { pki } = forge; + const rootCertificate = pemAsFileContent( + pki.certificateToPem(rootCertificateForgeObject), + ); + const rootCertificatePrivateKey = pemAsFileContent( + pki.privateKeyToPem(rootCertificatePrivateKeyForgeObject), + ); + + await writeFile(rootCertificateFileUrl, rootCertificate); + await writeFile(rootPrivateKeyFileUrl, rootCertificatePrivateKey); + await writeFile( + authorityJsonFileUrl, + JSON.stringify({ serialNumber: 0 }, null, " "), + ); + + logger.info( + `${UNICODE.OK} authority root certificate written at ${rootCertificateFileInfo.path}`, + ); + return { + rootCertificateForgeObject, + rootCertificatePrivateKeyForgeObject, + rootCertificate, + rootCertificatePrivateKey, + }; + }; + + const generate = async () => { + const { + rootCertificateForgeObject, + rootCertificatePrivateKeyForgeObject, + rootCertificate, + rootCertificatePrivateKey, + } = await generateRootCertificate(); + + const trustInfo = await platformMethods.executeTrustQuery({ + logger, + certificateCommonName, + certificateFileUrl: rootCertificateFileUrl, + certificateIsNew: true, + certificate: rootCertificate, + verb: tryToTrust ? "ADD_TRUST" : "CHECK_TRUST", + NSSDynamicInstall, + }); + + return { + rootCertificateForgeObject, + rootCertificatePrivateKeyForgeObject, + rootCertificate, + rootCertificatePrivateKey, + rootCertificateFilePath: rootCertificateFileInfo.path, + trustInfo, + }; + }; + + const regenerate = async () => { + if (tryToTrust) { + await platformMethods.executeTrustQuery({ + logger, + certificateCommonName, + certificateFileUrl: rootCertificateFileUrl, + certificate: rootCertificate, + verb: "REMOVE_TRUST", + }); + } + return generate(); + }; + + logger.debug(`Search existing certificate authority on filesystem...`); + if (!rootCertificateFileInfo.exists) { + logger.debug( + `Authority root certificate is not on filesystem at ${rootCertificateFileInfo.path}`, + ); + logger.info( + `${UNICODE.INFO} authority root certificate not found in filesystem`, + ); + return generate(); + } + if (!rootCertificatePrivateKeyFileInfo.exists) { + logger.debug( + `Authority root certificate private key is not on filesystem at ${rootCertificatePrivateKeyFileInfo.path}`, + ); + logger.info( + `${UNICODE.INFO} authority root certificate not found in filesystem`, + ); + return generate(); + } + logger.debug( + `found authority root certificate files at ${rootCertificateFileInfo.path} and ${rootCertificatePrivateKeyFileInfo.path}`, + ); + logger.info(`${UNICODE.OK} authority root certificate found in filesystem`); + + const rootCertificate = await readFile(rootCertificateFileInfo.path, { + as: "string", + }); + const { pki } = forge; + const rootCertificateForgeObject = pki.certificateFromPem(rootCertificate); + + logger.info(`Checking certificate validity...`); + const rootCertificateValidityDurationInMs = + getCertificateValidityDurationInMs(rootCertificateForgeObject); + const rootCertificateValidityRemainingMs = getCertificateRemainingMs( + rootCertificateForgeObject, + ); + if (rootCertificateValidityRemainingMs < 0) { + logger.info( + `${UNICODE.INFO} certificate expired ${formatTimeDelta( + rootCertificateValidityRemainingMs, + )}`, + ); + return regenerate(); + } + const rootCertificateValidityRemainingRatio = + rootCertificateValidityRemainingMs / rootCertificateValidityDurationInMs; + if (rootCertificateValidityRemainingRatio < aboutToExpireRatio) { + logger.info( + `${UNICODE.INFO} certificate will expire ${formatTimeDelta( + rootCertificateValidityRemainingMs, + )}`, + ); + return regenerate(); + } + logger.info( + `${UNICODE.OK} certificate still valid for ${formatDuration( + rootCertificateValidityRemainingMs, + )}`, + ); + + logger.info(`Detect if certificate attributes have changed...`); + const rootCertificateDifferences = compareRootCertificateAttributes( + rootCertificateForgeObject, + { + certificateCommonName, + certificateValidityDurationInMs, + }, + ); + if (rootCertificateDifferences.length) { + const paramNames = Object.keys(rootCertificateDifferences); + logger.info( + `${UNICODE.INFO} certificate attributes are outdated: ${paramNames}`, + ); + return regenerate(); + } + logger.info(`${UNICODE.OK} certificate attributes are the same`); + + const rootCertificatePrivateKey = await readFile( + rootCertificatePrivateKeyFileInfo.path, + { + as: "string", + }, + ); + const rootCertificatePrivateKeyForgeObject = pki.privateKeyFromPem( + rootCertificatePrivateKey, + ); + const trustInfo = await platformMethods.executeTrustQuery({ + logger, + certificateCommonName, + certificateFileUrl: rootCertificateFileInfo.url, + certificate: rootCertificate, + verb: tryToTrust ? "ENSURE_TRUST" : "CHECK_TRUST", + NSSDynamicInstall, + }); + + return { + rootCertificateForgeObject, + rootCertificatePrivateKeyForgeObject, + rootCertificate, + rootCertificatePrivateKey, + rootCertificateFilePath: rootCertificateFileInfo.path, + trustInfo, + }; +}; + +// const getCertificateValidSinceInMs = (forgeCertificate) => { +// const { notBefore } = forgeCertificate.validity +// const nowDate = Date.now() +// const msEllapsedSinceValid = nowDate - notBefore +// return msEllapsedSinceValid +// } + +const getCertificateRemainingMs = (certificateForgeObject) => { + const { notAfter } = certificateForgeObject.validity; + const nowDate = Date.now(); + const remainingMs = notAfter - nowDate; + return remainingMs; +}; + +const getCertificateValidityDurationInMs = (certificateForgeObject) => { + const { notBefore, notAfter } = certificateForgeObject.validity; + const validityDurationInMs = notAfter - notBefore; + return validityDurationInMs; +}; + +const compareRootCertificateAttributes = ( + rootCertificateForgeObject, + { certificateCommonName, certificateValidityDurationInMs }, +) => { + const attributeDescription = attributeDescriptionFromAttributeArray( + rootCertificateForgeObject.subject.attributes, + ); + const differences = {}; + + const { commonName } = attributeDescription; + if (commonName !== certificateCommonName) { + differences.certificateCommonName = { + valueFromCertificate: commonName, + valueFromParam: certificateCommonName, + }; + } + + const { notBefore, notAfter } = rootCertificateForgeObject.validity; + const rootCertificateValidityDurationInMs = notAfter - notBefore; + if (rootCertificateValidityDurationInMs !== certificateValidityDurationInMs) { + differences.rootCertificateValidityDurationInMs = { + valueFromCertificate: rootCertificateValidityDurationInMs, + valueFromParam: certificateValidityDurationInMs, + }; + } + + return differences; +}; + +export const uninstallCertificateAuthority = async ({ + logLevel, + logger = createLogger({ logLevel }), + tryToUntrust = false, +} = {}) => { + const { + authorityJsonFileInfo, + rootCertificateFileInfo, + rootCertificatePrivateKeyFileInfo, + } = getAuthorityFileInfos(); + + const filesToRemove = []; + + if (authorityJsonFileInfo.exists) { + filesToRemove.push(authorityJsonFileInfo.url); + } + if (rootCertificateFileInfo.exists) { + // first untrust the root cert file + if (tryToUntrust) { + const rootCertificate = await readFile(rootCertificateFileInfo.url, { + as: "string", + }); + const { pki } = forge; + const rootCertificateForgeObject = + pki.certificateFromPem(rootCertificate); + const rootCertificateCommonName = attributeDescriptionFromAttributeArray( + rootCertificateForgeObject.subject.attributes, + ).commonName; + const { removeCertificateFromTrustStores } = + await importPlatformMethods(); + await removeCertificateFromTrustStores({ + logger, + certificate: rootCertificate, + certificateFileUrl: rootCertificateFileInfo.url, + certificateCommonName: rootCertificateCommonName, + }); + } + filesToRemove.push(rootCertificateFileInfo.url); + } + if (rootCertificatePrivateKeyFileInfo.exists) { + filesToRemove.push(rootCertificatePrivateKeyFileInfo.url); + } + + if (filesToRemove.length) { + logger.info(`Removing certificate authority files...`); + await Promise.all( + filesToRemove.map(async (file) => { + await removeEntry(file); + }), + ); + logger.info( + `${UNICODE.OK} certificate authority files removed from filesystem`, + ); + } +}; + +const pemAsFileContent = (pem) => { + if (process.platform === "win32") { + return pem; + } + // prefer \n when writing pem into files + return pem.replace(/\r\n/g, "\n"); +}; + +/* + * The root certificate files can be "hard" to find because + * located in a dedicated application directory specific to the OS + * To make them easier to find, we write symbolic links near the server + * certificate file pointing to the root certificate files + */ +// if (!isWindows) { // not on windows because symlink requires admin rights +// logger.debug(`Writing root certificate symbol link files`) +// await writeSymbolicLink({ +// from: rootCertificateSymlinkUrl, +// to: rootCertificateFileUrl, +// type: "file", +// allowUseless: true, +// allowOverwrite: true, +// }) +// await writeSymbolicLink({ +// from: rootPrivateKeySymlinkUrl, +// to: rootPrivateKeyFileUrl, +// type: "file", +// allowUseless: true, +// allowOverwrite: true, +// }) +// logger.debug(`Root certificate symbolic links written`) +// } diff --git a/packages/independent/https-local/src/certificate_request.js b/packages/independent/https-local/src/certificate_request.js new file mode 100644 index 0000000000..309542aab7 --- /dev/null +++ b/packages/independent/https-local/src/certificate_request.js @@ -0,0 +1,112 @@ +import { writeFileSync } from "@jsenv/filesystem"; +import { UNICODE, createDetailedMessage, createLogger } from "@jsenv/humanize"; +import { readFileSync } from "node:fs"; +import { getAuthorityFileInfos } from "./internal/authority_file_infos.js"; +import { requestCertificateFromAuthority } from "./internal/certificate_generator.js"; +import { forge } from "./internal/forge.js"; +import { formatDuration } from "./internal/validity_formatting.js"; +import { + createValidityDurationOfXDays, + verifyServerCertificateValidityDuration, +} from "./validity_duration.js"; + +export const requestCertificate = ({ + logLevel, + logger = createLogger({ logLevel }), // to be able to catch logs during unit tests + + altNames = ["localhost"], + commonName = "https local server certificate", + validityDurationInMs = createValidityDurationOfXDays(396), +} = {}) => { + if (typeof validityDurationInMs !== "number") { + throw new TypeError( + `validityDurationInMs must be a number but received ${validityDurationInMs}`, + ); + } + if (validityDurationInMs < 1) { + throw new TypeError( + `validityDurationInMs must be > 0 but received ${validityDurationInMs}`, + ); + } + const validityDurationInfo = + verifyServerCertificateValidityDuration(validityDurationInMs); + if (!validityDurationInfo.ok) { + validityDurationInMs = validityDurationInfo.maxAllowedValue; + logger.warn( + createDetailedMessage(validityDurationInfo.message, { + details: validityDurationInfo.details, + }), + ); + } + + const { + authorityJsonFileInfo, + rootCertificateFileInfo, + rootCertificatePrivateKeyFileInfo, + } = getAuthorityFileInfos(); + if (!rootCertificateFileInfo.exists) { + throw new Error( + `Certificate authority not found, "installCertificateAuthority" must be called before "requestServerCertificate"`, + ); + } + if (!rootCertificatePrivateKeyFileInfo.exists) { + throw new Error(`Cannot find authority root certificate private key`); + } + if (!authorityJsonFileInfo.exists) { + throw new Error(`Cannot find authority json file`); + } + + logger.debug(`Restoring certificate authority from filesystem...`); + const { pki } = forge; + const rootCertificate = String( + readFileSync(new URL(rootCertificateFileInfo.url)), + ); + const rootCertificatePrivateKey = String( + readFileSync(new URL(rootCertificatePrivateKeyFileInfo.url)), + ); + const certificateAuthorityData = JSON.parse( + String(readFileSync(new URL(authorityJsonFileInfo.url))), + ); + const rootCertificateForgeObject = pki.certificateFromPem(rootCertificate); + const rootCertificatePrivateKeyForgeObject = pki.privateKeyFromPem( + rootCertificatePrivateKey, + ); + logger.debug(`${UNICODE.OK} certificate authority restored from filesystem`); + + const serverCertificateSerialNumber = + certificateAuthorityData.serialNumber + 1; + writeFileSync( + authorityJsonFileInfo.url, + JSON.stringify({ serialNumber: serverCertificateSerialNumber }, null, " "), + ); + + logger.debug(`Generating server certificate...`); + const { certificateForgeObject, certificatePrivateKeyForgeObject } = + requestCertificateFromAuthority({ + logger, + authorityCertificateForgeObject: rootCertificateForgeObject, + auhtorityCertificatePrivateKeyForgeObject: + rootCertificatePrivateKeyForgeObject, + serialNumber: serverCertificateSerialNumber, + altNames, + commonName, + validityDurationInMs, + }); + const serverCertificate = pki.certificateToPem(certificateForgeObject); + const serverCertificatePrivateKey = pki.privateKeyToPem( + certificatePrivateKeyForgeObject, + ); + logger.debug( + `${ + UNICODE.OK + } server certificate generated, it will be valid for ${formatDuration( + validityDurationInMs, + )}`, + ); + + return { + certificate: serverCertificate, + privateKey: serverCertificatePrivateKey, + rootCertificateFilePath: rootCertificateFileInfo.path, + }; +}; diff --git a/packages/independent/https-local/src/hosts_file_verif.js b/packages/independent/https-local/src/hosts_file_verif.js new file mode 100644 index 0000000000..9b1fa1dd89 --- /dev/null +++ b/packages/independent/https-local/src/hosts_file_verif.js @@ -0,0 +1,91 @@ +import { createDetailedMessage, createLogger, UNICODE } from "@jsenv/humanize"; +import { + HOSTS_FILE_PATH, + parseHosts, + readHostsFile, + writeLineInHostsFile, +} from "./internal/hosts.js"; + +export const verifyHostsFile = async ({ + ipMappings, + logLevel, + logger = createLogger({ logLevel }), + tryToUpdateHostsFile = false, + // for unit test + hostsFilePath = HOSTS_FILE_PATH, +}) => { + logger.info(`Check hosts file content...`); + const hostsFileContent = await readHostsFile(hostsFilePath); + const hostnames = parseHosts(hostsFileContent); + + const missingMappings = []; + Object.keys(ipMappings).forEach((ip) => { + const ipHostnames = ipMappings[ip]; + if (!Array.isArray(ipHostnames)) { + throw new TypeError( + `ipMappings values must be an array, found ${ipHostnames} for ${ip}`, + ); + } + const existingMappings = hostnames.getIpHostnames(ip); + const missingHostnames = normalizeHostnames(ipHostnames).filter( + (hostname) => !existingMappings.includes(hostname), + ); + if (missingHostnames.length) { + missingMappings.push({ ip, missingHostnames }); + } + }); + const missingMappingCount = missingMappings.length; + if (missingMappingCount === 0) { + logger.info(`${UNICODE.OK} all ip mappings found in hosts file`); + return; + } + + const EOL = process.platform === "win32" ? "\r\n" : "\n"; + + if (!tryToUpdateHostsFile) { + const linesToAdd = missingMappings + .map(({ ip, missingHostnames }) => `${ip} ${missingHostnames.join(" ")}`) + .join(EOL); + logger.warn( + createDetailedMessage( + `${UNICODE.WARNING} ${formatXMappingMissingMessage( + missingMappingCount, + )}`, + { + "hosts file path": hostsFilePath, + "line(s) to add": linesToAdd, + }, + ), + ); + return; + } + + logger.info( + `${UNICODE.INFO} ${formatXMappingMissingMessage(missingMappingCount)}`, + ); + await missingMappings.reduce(async (previous, { ip, missingHostnames }) => { + await previous; + const mapping = `${ip} ${missingHostnames.join(" ")}`; + logger.info(`Append "${mapping}" in host file...`); + + await writeLineInHostsFile(mapping, { + hostsFilePath, + onBeforeExecCommand: (command) => { + logger.info(`${UNICODE.COMMAND} ${command}`); + }, + }); + logger.info(`${UNICODE.OK} mapping added`); + }, Promise.resolve()); +}; + +const normalizeHostnames = (hostnames) => { + return hostnames.map((hostname) => hostname.trim().replace(/[\s;]/g, "")); +}; + +const formatXMappingMissingMessage = (missingMappingCount) => { + if (missingMappingCount) { + return `1 mapping is missing in hosts file`; + } + + return `${missingMappingCount} mappings are missing in hosts file`; +}; diff --git a/packages/independent/https-local/src/internal/authority_file_infos.js b/packages/independent/https-local/src/internal/authority_file_infos.js new file mode 100644 index 0000000000..91cce88e89 --- /dev/null +++ b/packages/independent/https-local/src/internal/authority_file_infos.js @@ -0,0 +1,42 @@ +import { existsSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { getCertificateAuthorityFileUrls } from "./certificate_authority_file_urls.js"; + +export const getAuthorityFileInfos = () => { + const { + certificateAuthorityJsonFileUrl, + rootCertificateFileUrl, + rootCertificatePrivateKeyFileUrl, + } = getCertificateAuthorityFileUrls(); + + const authorityJsonFilePath = fileURLToPath(certificateAuthorityJsonFileUrl); + const authorityJsonFileDetected = existsSync(authorityJsonFilePath); + + const rootCertificateFilePath = fileURLToPath(rootCertificateFileUrl); + const rootCertificateFileDetected = existsSync(rootCertificateFilePath); + + const rootCertificatePrivateKeyFilePath = fileURLToPath( + rootCertificatePrivateKeyFileUrl, + ); + const rootCertificatePrivateKeyFileDetected = existsSync( + rootCertificatePrivateKeyFilePath, + ); + + return { + authorityJsonFileInfo: { + url: certificateAuthorityJsonFileUrl, + path: authorityJsonFilePath, + exists: authorityJsonFileDetected, + }, + rootCertificateFileInfo: { + url: rootCertificateFileUrl, + path: rootCertificateFilePath, + exists: rootCertificateFileDetected, + }, + rootCertificatePrivateKeyFileInfo: { + url: rootCertificatePrivateKeyFileUrl, + path: rootCertificatePrivateKeyFilePath, + exists: rootCertificatePrivateKeyFileDetected, + }, + }; +}; diff --git a/packages/independent/https-local/src/internal/browser_detection.js b/packages/independent/https-local/src/internal/browser_detection.js new file mode 100644 index 0000000000..e98dda112e --- /dev/null +++ b/packages/independent/https-local/src/internal/browser_detection.js @@ -0,0 +1,7 @@ +import { existsSync } from "node:fs" + +export const detectBrowser = (pathCandidates) => { + return pathCandidates.some((pathCandidate) => { + return existsSync(pathCandidate) + }) +} diff --git a/packages/independent/https-local/src/internal/certificate_authority_file_urls.js b/packages/independent/https-local/src/internal/certificate_authority_file_urls.js new file mode 100644 index 0000000000..8a83ba0a58 --- /dev/null +++ b/packages/independent/https-local/src/internal/certificate_authority_file_urls.js @@ -0,0 +1,96 @@ +import { urlToFilename } from "@jsenv/urls" +import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem" + +export const getCertificateAuthorityFileUrls = () => { + // we need a directory common to every instance of @jsenv/https-local + // so that even if it's used multiple times, the certificate autority files + // are reused + const applicationDirectoryUrl = getJsenvApplicationDirectoryUrl() + + const certificateAuthorityJsonFileUrl = new URL( + "./https_local_certificate_authority.json", + applicationDirectoryUrl, + ) + + const rootCertificateFileUrl = new URL( + "./https_local_root_certificate.crt", + applicationDirectoryUrl, + ) + + const rootCertificatePrivateKeyFileUrl = new URL( + "./https_local_root_certificate.key", + applicationDirectoryUrl, + ).href + + return { + certificateAuthorityJsonFileUrl, + rootCertificateFileUrl, + rootCertificatePrivateKeyFileUrl, + } +} + +export const getRootCertificateSymlinkUrls = ({ + rootCertificateFileUrl, + rootPrivateKeyFileUrl, + serverCertificateFileUrl, +}) => { + const serverCertificateDirectory = new URL("./", serverCertificateFileUrl) + .href + + const rootCertificateFilename = urlToFilename(rootCertificateFileUrl) + const rootCertificateSymlinkUrl = new URL( + rootCertificateFilename, + serverCertificateDirectory, + ).href + const rootPrivateKeyFilename = urlToFilename(rootPrivateKeyFileUrl) + const rootPrivateKeySymlinkUrl = new URL( + rootPrivateKeyFilename, + serverCertificateDirectory, + ).href + + return { + rootCertificateSymlinkUrl, + rootPrivateKeySymlinkUrl, + } +} + +// https://github.com/LinusU/node-application-config-path/blob/master/index.js +const getJsenvApplicationDirectoryUrl = () => { + const { platform } = process + + if (platform === "darwin") { + return new URL( + `./Library/Application Support/https_local/`, + assertAndNormalizeDirectoryUrl(process.env.HOME), + ).href + } + + if (platform === "linux") { + if (process.env.XDG_CONFIG_HOME) { + return new URL( + `./https_local/`, + assertAndNormalizeDirectoryUrl(process.env.XDG_CONFIG_HOME), + ).href + } + return new URL( + `./.config/https_local/`, + assertAndNormalizeDirectoryUrl(process.env.HOME), + ).href + } + + if (platform === "win32") { + if (process.env.LOCALAPPDATA) { + return new URL( + `./https_local/`, + assertAndNormalizeDirectoryUrl(process.env.LOCALAPPDATA), + ).href + } + + return new URL( + `./Local Settings/Application Data/https_local/`, + assertAndNormalizeDirectoryUrl(process.env.USERPROFILE), + ).href + } + + throw new Error(`platform not supported`) +} diff --git a/packages/independent/https-local/src/internal/certificate_data_converter.js b/packages/independent/https-local/src/internal/certificate_data_converter.js new file mode 100644 index 0000000000..72e6f5bf71 --- /dev/null +++ b/packages/independent/https-local/src/internal/certificate_data_converter.js @@ -0,0 +1,91 @@ +import { isIP } from "node:net" + +export const subjectAltNamesFromAltNames = (altNames) => { + const altNamesArray = altNames.map((altName) => { + if (isIP(altName)) { + return { + type: 7, + ip: altName, + } + } + if (isUrl(altName)) { + return { + type: 6, + value: altName, + } + } + // 2 is DNS (Domain Name Server) + return { + type: 2, + value: altName, + } + }) + + return altNamesArray +} + +const isUrl = (value) => { + try { + // eslint-disable-next-line no-new + new URL(value) + return true + } catch (e) { + return false + } +} + +export const extensionArrayFromExtensionDescription = ( + extensionDescription, +) => { + const extensionArray = [] + Object.keys(extensionDescription).forEach((key) => { + const value = extensionDescription[key] + if (value) { + extensionArray.push({ + name: key, + ...value, + }) + } + }) + return extensionArray +} + +export const extensionDescriptionFromExtensionArray = (extensionArray) => { + const extensionDescription = {} + extensionArray.forEach((extension) => { + const { name, ...rest } = extension + extensionDescription[name] = rest + }) + return extensionDescription +} + +export const attributeDescriptionFromAttributeArray = (attributeArray) => { + const attributeObject = {} + attributeArray.forEach((attribute) => { + attributeObject[attribute.name] = attribute.value + }) + return attributeObject +} + +export const attributeArrayFromAttributeDescription = ( + attributeDescription, +) => { + const attributeArray = [] + Object.keys(attributeDescription).forEach((key) => { + const value = attributeDescription[key] + if (typeof value === "undefined") { + return + } + attributeArray.push({ + name: key, + value, + }) + }) + return attributeArray +} + +export const normalizeForgeAltNames = (forgeAltNames) => { + return forgeAltNames.map((forgeAltName) => { + return forgeAltName.ip || forgeAltName.value + }) +} diff --git a/packages/independent/https-local/src/internal/certificate_generator.js b/packages/independent/https-local/src/internal/certificate_generator.js new file mode 100644 index 0000000000..0b2fbe48c4 --- /dev/null +++ b/packages/independent/https-local/src/internal/certificate_generator.js @@ -0,0 +1,181 @@ +// https://github.com/digitalbazaar/forge/blob/master/examples/create-cert.js +// https://github.com/digitalbazaar/forge/issues/660#issuecomment-467145103 + +import { forge } from "./forge.js" +import { + attributeArrayFromAttributeDescription, + attributeDescriptionFromAttributeArray, + subjectAltNamesFromAltNames, + extensionArrayFromExtensionDescription, +} from "./certificate_data_converter.js" + +export const createAuthorityRootCertificate = async ({ + commonName, + countryName, + stateOrProvinceName, + localityName, + organizationName, + organizationalUnitName, + validityDurationInMs, + serialNumber, +} = {}) => { + if (typeof serialNumber !== "number") { + throw new TypeError(`serial must be a number but received ${serialNumber}`) + } + + const { pki } = forge + const rootCertificateForgeObject = pki.createCertificate() + const keyPair = pki.rsa.generateKeyPair(2048) // TODO: use async version https://github.com/digitalbazaar/forge#rsa + const rootCertificatePublicKeyForgeObject = keyPair.publicKey + const rootCertificatePrivateKeyForgeObject = keyPair.privateKey + + rootCertificateForgeObject.publicKey = rootCertificatePublicKeyForgeObject + rootCertificateForgeObject.serialNumber = serialNumber.toString(16) + rootCertificateForgeObject.validity.notBefore = new Date() + rootCertificateForgeObject.validity.notAfter = new Date( + Date.now() + validityDurationInMs, + ) + rootCertificateForgeObject.setSubject( + attributeArrayFromAttributeDescription({ + commonName, + countryName, + stateOrProvinceName, + localityName, + organizationName, + organizationalUnitName, + }), + ) + rootCertificateForgeObject.setIssuer( + attributeArrayFromAttributeDescription({ + commonName, + countryName, + stateOrProvinceName, + localityName, + organizationName, + organizationalUnitName, + }), + ) + rootCertificateForgeObject.setExtensions( + extensionArrayFromExtensionDescription({ + basicConstraints: { + critical: true, + cA: true, + }, + keyUsage: { + critical: true, + digitalSignature: true, + keyCertSign: true, + cRLSign: true, + }, + // extKeyUsage: { + // serverAuth: true, + // clientAuth: true, + // }, + // subjectKeyIdentifier: {}, + }), + ) + + // self-sign certificate + rootCertificateForgeObject.sign(rootCertificatePrivateKeyForgeObject) // , forge.sha256.create()) + + return { + rootCertificateForgeObject, + rootCertificatePublicKeyForgeObject, + rootCertificatePrivateKeyForgeObject, + } +} + +export const requestCertificateFromAuthority = ({ + authorityCertificateForgeObject, // could be intermediate or root certificate authority + auhtorityCertificatePrivateKeyForgeObject, + serialNumber, + altNames = [], + commonName, + validityDurationInMs, +}) => { + if ( + typeof authorityCertificateForgeObject !== "object" || + authorityCertificateForgeObject === null + ) { + throw new TypeError( + `authorityCertificateForgeObject must be an object but received ${authorityCertificateForgeObject}`, + ) + } + if ( + typeof auhtorityCertificatePrivateKeyForgeObject !== "object" || + auhtorityCertificatePrivateKeyForgeObject === null + ) { + throw new TypeError( + `auhtorityCertificatePrivateKeyForgeObject must be an object but received ${auhtorityCertificatePrivateKeyForgeObject}`, + ) + } + if (typeof serialNumber !== "number") { + throw new TypeError( + `serialNumber must be a number but received ${serialNumber}`, + ) + } + + const { pki } = forge + const certificateForgeObject = pki.createCertificate() + const keyPair = pki.rsa.generateKeyPair(2048) // TODO: use async version https://github.com/digitalbazaar/forge#rsa + const certificatePublicKeyForgeObject = keyPair.publicKey + const certificatePrivateKeyForgeObject = keyPair.privateKey + + certificateForgeObject.publicKey = certificatePublicKeyForgeObject + certificateForgeObject.serialNumber = serialNumber.toString(16) + certificateForgeObject.validity.notBefore = new Date() + certificateForgeObject.validity.notAfter = new Date( + Date.now() + validityDurationInMs, + ) + + const attributeDescription = { + ...attributeDescriptionFromAttributeArray( + authorityCertificateForgeObject.subject.attributes, + ), + commonName, + // organizationName: serverCertificateOrganizationName + } + const attributeArray = + attributeArrayFromAttributeDescription(attributeDescription) + certificateForgeObject.setSubject(attributeArray) + certificateForgeObject.setIssuer( + authorityCertificateForgeObject.subject.attributes, + ) + certificateForgeObject.setExtensions( + extensionArrayFromExtensionDescription({ + basicConstraints: { + critical: true, + cA: false, + }, + keyUsage: { + critical: true, + digitalSignature: true, + keyEncipherment: true, + }, + extKeyUsage: { + critical: false, + serverAuth: true, + }, + authorityKeyIdentifier: { + critical: false, + // keyIdentifier: authorityCertificateForgeObject.generateSubjectKeyIdentifier().getBytes(), + authorityCertIssuer: true, + serialNumber: Number(0).toString(16), + }, + subjectAltName: { + critical: false, + altNames: subjectAltNamesFromAltNames(altNames), + }, + }), + ) + certificateForgeObject.sign( + auhtorityCertificatePrivateKeyForgeObject, + forge.sha256.create(), + ) + + return { + certificateForgeObject, + certificatePublicKeyForgeObject, + certificatePrivateKeyForgeObject, + } +} diff --git a/packages/independent/https-local/src/internal/command.js b/packages/independent/https-local/src/internal/command.js new file mode 100644 index 0000000000..44c4037625 --- /dev/null +++ b/packages/independent/https-local/src/internal/command.js @@ -0,0 +1,9 @@ +import { createRequire } from "node:module" + +const require = createRequire(import.meta.url) + +export const commandExists = async (command) => { + const { sync } = require("command-exists") + const exists = sync(command) + return exists +} diff --git a/packages/independent/https-local/src/internal/exec.js b/packages/independent/https-local/src/internal/exec.js new file mode 100644 index 0000000000..61cf9da392 --- /dev/null +++ b/packages/independent/https-local/src/internal/exec.js @@ -0,0 +1,33 @@ +import { exec as nodeExec } from "node:child_process" + +export const exec = ( + command, + { cwd, input, onLog = () => {}, onErrorLog = () => {} } = {}, +) => { + return new Promise((resolve, reject) => { + const commandProcess = nodeExec( + command, + { + cwd, + input, + stdio: "silent", + }, + (error, stdout) => { + if (error) { + reject(error) + } else { + resolve(stdout) + } + }, + ) + + commandProcess.stdout.on("data", (data) => { + onLog(data) + }) + commandProcess.stderr.on("data", (data) => { + // debug because this output is part of + // the error message generated by a failing npm publish + onErrorLog(data) + }) + }) +} diff --git a/packages/independent/https-local/src/internal/forge.js b/packages/independent/https-local/src/internal/forge.js new file mode 100644 index 0000000000..aaa8a529ab --- /dev/null +++ b/packages/independent/https-local/src/internal/forge.js @@ -0,0 +1,5 @@ +import { createRequire } from "node:module" + +const require = createRequire(import.meta.url) + +export const forge = require("node-forge") diff --git a/packages/independent/https-local/src/internal/hosts.js b/packages/independent/https-local/src/internal/hosts.js new file mode 100644 index 0000000000..4d32a4c468 --- /dev/null +++ b/packages/independent/https-local/src/internal/hosts.js @@ -0,0 +1,9 @@ +export { HOSTS_FILE_PATH } from "./hosts/hosts_utils.js" + +export { readHostsFile } from "./hosts/read_hosts.js" + +export { parseHosts } from "./hosts/parse_hosts.js" + +export { writeHostsFile } from "./hosts/write_hosts.js" + +export { writeLineInHostsFile } from "./hosts/write_line_hosts.js" diff --git a/packages/independent/https-local/src/internal/hosts/hosts_utils.js b/packages/independent/https-local/src/internal/hosts/hosts_utils.js new file mode 100644 index 0000000000..2da6db921b --- /dev/null +++ b/packages/independent/https-local/src/internal/hosts/hosts_utils.js @@ -0,0 +1,5 @@ +const IS_WINDOWS = process.platform === "win32" + +export const HOSTS_FILE_PATH = IS_WINDOWS + ? "C:\\Windows\\System32\\Drivers\\etc\\hosts" + : "/etc/hosts" diff --git a/packages/independent/https-local/src/internal/hosts/parse_hosts.js b/packages/independent/https-local/src/internal/hosts/parse_hosts.js new file mode 100644 index 0000000000..07d4e15222 --- /dev/null +++ b/packages/independent/https-local/src/internal/hosts/parse_hosts.js @@ -0,0 +1,141 @@ +const IS_WINDOWS = process.platform === "win32"; + +// https://github.com/feross/hostile/blob/master/index.js +export const parseHosts = ( + hosts, + { EOL = IS_WINDOWS ? "\r\n" : "\n" } = {}, +) => { + const lines = []; + hosts.split(/\r?\n/).forEach((line) => { + const lineWithoutComments = line.replace(/#.*/, ""); + // eslint-disable-next-line regexp/no-super-linear-backtracking + const matches = /^\s*?(.+?)\s+(.+?)\s*$/.exec(lineWithoutComments); + if (matches && matches.length === 3) { + const [, ip, host] = matches; + const hostnames = host.split(" "); + lines.push({ type: "rule", ip, hostnames }); + } else { + // Found a comment, blank line, or something else + lines.push({ type: "other", value: line }); + } + }); + + const getAllIpHostnames = () => { + const ipHostnames = {}; + lines.forEach((line) => { + if (line.type === "rule") { + const { ip, hostnames } = line; + const existingHostnames = ipHostnames[ip]; + ipHostnames[ip] = existingHostnames + ? [...existingHostnames, ...hostnames] + : hostnames; + } + }); + return ipHostnames; + }; + + const getIpHostnames = (ip) => { + const hosts = []; + lines.forEach((line) => { + if (line.type === "rule" && line.ip === ip) { + hosts.push(...line.hostnames); + } + }); + return hosts; + }; + + const addIpHostname = (ip, host) => { + const alreadyThere = lines.some( + (line) => + line.type === "rule" && line.ip === ip && line.hostnames.includes(host), + ); + if (alreadyThere) { + return false; + } + + const rule = { type: "rule", ip, hostnames: [host] }; + const lastLineIndex = lines.length - 1; + const lastLine = lines[lastLineIndex]; + // last line is just empty characters, put the rule above it + if (lastLine.type === "other" && /\s*/.test(lastLine.value)) { + lines.splice(lastLineIndex, 0, rule); + } else { + lines.push(rule); + } + return true; + }; + + const removeIpHostname = (ip, host) => { + let lineIndexFound; + let hostnamesFound; + let hostIndexFound; + const found = lines.find((line, lineIndex) => { + if (line.type !== "rule") { + return false; + } + if (line.ip !== ip) { + return false; + } + const { hostnames } = line; + const hostIndex = hostnames.indexOf(host); + if (hostIndex === -1) { + return false; + } + + lineIndexFound = lineIndex; + hostnamesFound = hostnames; + hostIndexFound = hostIndex; + return true; + }); + + if (!found) { + return false; + } + + if (hostnamesFound.length === 1) { + lines.splice(lineIndexFound, 1); + return true; + } + + hostnamesFound.splice(hostIndexFound, 1); + return true; + }; + + const asFileContent = () => { + let hostsFileContent = ""; + const ips = lines + .filter((line) => line.type === "rule") + .map((line) => line.ip); + const longestIp = ips.reduce((previous, ip) => { + const length = ip.length; + return length > previous ? length : previous; + }, 0); + + lines.forEach((line, index) => { + if (line.type === "rule") { + const { ip, hostnames } = line; + const ipLength = ip.length; + const lengthDelta = longestIp - ipLength; + hostsFileContent += `${ip}${" ".repeat(lengthDelta)} ${hostnames.join( + " ", + )}`; + } else { + hostsFileContent += line.value; + } + + const nextLine = lines[index + 1]; + if (nextLine) { + hostsFileContent += EOL; + } + }); + return hostsFileContent; + }; + + return { + getAllIpHostnames, + getIpHostnames, + addIpHostname, + removeIpHostname, + asFileContent, + }; +}; diff --git a/packages/independent/https-local/src/internal/hosts/read_hosts.js b/packages/independent/https-local/src/internal/hosts/read_hosts.js new file mode 100644 index 0000000000..0195fe00d5 --- /dev/null +++ b/packages/independent/https-local/src/internal/hosts/read_hosts.js @@ -0,0 +1,7 @@ +import { readFile } from "@jsenv/filesystem"; +import { HOSTS_FILE_PATH } from "./hosts_utils.js"; + +export const readHostsFile = async (hostsFilePath = HOSTS_FILE_PATH) => { + const hostsFileContent = await readFile(hostsFilePath, { as: "string" }); + return hostsFileContent; +}; diff --git a/packages/independent/https-local/src/internal/hosts/write_hosts.js b/packages/independent/https-local/src/internal/hosts/write_hosts.js new file mode 100644 index 0000000000..fcc29a6b91 --- /dev/null +++ b/packages/independent/https-local/src/internal/hosts/write_hosts.js @@ -0,0 +1,85 @@ +import { createRequire } from "node:module"; +import { exec } from "../exec.js"; +import { HOSTS_FILE_PATH } from "./hosts_utils.js"; + +export const writeHostsFile = async ( + hostsFileContent, + { hostsFilePath = HOSTS_FILE_PATH, onBeforeExecCommand = () => {} } = {}, +) => { + if (process.platform === "win32") { + return writeHostsFileOnWindows({ + hostsFileContent, + hostsFilePath, + onBeforeExecCommand, + }); + } + return writeHostsFileOnLinuxOrMac({ + hostsFileContent, + hostsFilePath, + onBeforeExecCommand, + }); +}; + +const writeHostsFileOnLinuxOrMac = async ({ + hostsFilePath, + hostsFileContent, + onBeforeExecCommand, +}) => { + const needsSudo = hostsFilePath === HOSTS_FILE_PATH; + // https://en.wikipedia.org/wiki/Tee_(command) + const updateHostsFileCommand = needsSudo + ? `echo "${hostsFileContent}" | sudo tee ${hostsFilePath}` + : `echo "${hostsFileContent}" | tee ${hostsFilePath}`; + onBeforeExecCommand(updateHostsFileCommand); + await exec(updateHostsFileCommand); +}; + +const writeHostsFileOnWindows = async ({ + hostsFilePath, + hostsFileContent, + onBeforeExecCommand, +}) => { + const needsSudo = hostsFilePath === HOSTS_FILE_PATH; + const echoCommand = echoWithLinesToSingleCommand(hostsFileContent); + const updateHostsFileCommand = `${echoCommand} > ${hostsFilePath}`; + + if (needsSudo) { + const require = createRequire(import.meta.url); + const sudoPrompt = require("sudo-prompt"); + onBeforeExecCommand(updateHostsFileCommand); + await new Promise((resolve, reject) => { + sudoPrompt.exec( + updateHostsFileCommand, + { name: "write hosts" }, + (error, stdout, stderr) => { + if (error) { + reject(error); + } else if (typeof stderr === "string" && stderr.trim().length > 0) { + reject(stderr); + } else { + resolve(stdout); + } + }, + ); + }); + return; + } + + onBeforeExecCommand(updateHostsFileCommand); + await exec(updateHostsFileCommand); +}; + +const echoWithLinesToSingleCommand = (value) => { + const command = value + .split(/\r\n/g) + .map((value) => `echo ${value}`) + .join(`& `); + return `(${command})`; +}; + +// https://github.com/xxorax/node-shell-escape +// https://github.com/nodejs/node/issues/34840#issuecomment-677402567 +// const escapeCommandArgument = (value) => { +// return `'${value.replace(/'/g, `'"'`)}'` +// // return String(value).replace(/([A-z]:)?([#!"$&'()*,:;<=>?@\[\\\]^`{|}])/g, "$1\\$2") +// } diff --git a/packages/independent/https-local/src/internal/hosts/write_line_hosts.js b/packages/independent/https-local/src/internal/hosts/write_line_hosts.js new file mode 100644 index 0000000000..4f0f92268d --- /dev/null +++ b/packages/independent/https-local/src/internal/hosts/write_line_hosts.js @@ -0,0 +1,81 @@ +import { readFile } from "@jsenv/filesystem"; +import { createRequire } from "node:module"; +import { exec } from "../exec.js"; +import { HOSTS_FILE_PATH } from "./hosts_utils.js"; + +export const writeLineInHostsFile = async ( + lineToAppend, + { hostsFilePath = HOSTS_FILE_PATH, onBeforeExecCommand = () => {} } = {}, +) => { + if (process.platform === "win32") { + return appendToHostsFileOnWindows({ + lineToAppend, + hostsFilePath, + onBeforeExecCommand, + }); + } + return appendToHostsFileOnLinuxOrMac({ + lineToAppend, + hostsFilePath, + onBeforeExecCommand, + }); +}; + +// https://renenyffenegger.ch/notes/Windows/dirs/Windows/System32/cmd_exe/commands/echo/index +const appendToHostsFileOnWindows = async ({ + lineToAppend, + hostsFilePath, + onBeforeExecCommand, +}) => { + const hostsFileContent = await readFile(hostsFilePath, { as: "string" }); + const echoCommand = + hostsFileContent.length > 0 && !hostsFileContent.endsWith("\r\n") + ? `(echo.& echo ${lineToAppend})` + : `(echo ${lineToAppend})`; + const needsSudo = hostsFilePath === HOSTS_FILE_PATH; + const updateHostsFileCommand = `${echoCommand} >> ${hostsFilePath}`; + + if (needsSudo) { + const require = createRequire(import.meta.url); + const sudoPrompt = require("sudo-prompt"); + onBeforeExecCommand(updateHostsFileCommand); + await new Promise((resolve, reject) => { + sudoPrompt.exec( + updateHostsFileCommand, + { name: "append hosts" }, + (error, stdout, stderr) => { + if (error) { + reject(error); + } else if (typeof stderr === "string" && stderr.trim().length > 0) { + reject(stderr); + } else { + resolve(stdout); + } + }, + ); + }); + return; + } + + onBeforeExecCommand(updateHostsFileCommand); + await exec(updateHostsFileCommand); +}; + +const appendToHostsFileOnLinuxOrMac = async ({ + lineToAppend, + hostsFilePath, + onBeforeExecCommand, +}) => { + const hostsFileContent = await readFile(hostsFilePath, { as: "string" }); + const echoCommand = + hostsFileContent.length > 0 && !hostsFileContent.endsWith("\n") + ? `echo "\n${lineToAppend}"` + : `echo "${lineToAppend}"`; + const needsSudo = hostsFilePath === HOSTS_FILE_PATH; + // https://en.wikipedia.org/wiki/Tee_(command) + const updateHostsFileCommand = needsSudo + ? `${echoCommand} | sudo tee -a ${hostsFilePath}` + : `${echoCommand} | tee -a ${hostsFilePath}`; + onBeforeExecCommand(updateHostsFileCommand); + await exec(updateHostsFileCommand); +}; diff --git a/packages/independent/https-local/src/internal/linux/chrome_linux.js b/packages/independent/https-local/src/internal/linux/chrome_linux.js new file mode 100644 index 0000000000..b9e70bb73e --- /dev/null +++ b/packages/independent/https-local/src/internal/linux/chrome_linux.js @@ -0,0 +1,74 @@ +import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem"; +import { UNICODE } from "@jsenv/humanize"; +import { execSync } from "node:child_process"; +import { executeTrustQueryOnBrowserNSSDB } from "../nssdb_browser.js"; +import { + detectIfNSSIsInstalled, + getCertutilBinPath, + getNSSDynamicInstallInfo, + nssCommandName, +} from "./nss_linux.js"; + +export const executeTrustQueryOnChrome = ({ + logger, + certificateCommonName, + certificateFileUrl, + certificateIsNew, + certificate, + verb, + NSSDynamicInstall, +}) => { + return executeTrustQueryOnBrowserNSSDB({ + logger, + certificateCommonName, + certificateFileUrl, + certificateIsNew, + certificate, + + verb, + NSSDynamicInstall, + nssCommandName, + detectIfNSSIsInstalled, + getNSSDynamicInstallInfo, + getCertutilBinPath, + + browserName: "chrome", + browserPaths: ["/usr/bin/google-chrome"], + // chromium seems to use its own store and not ".pki/nssdb" anymore + // as explained in https://chromium.googlesource.com/chromium/src/+/main/net/data/ssl/chrome_root_store/faq.md + browserNSSDBDirectoryUrls: [ + new URL(".pki/nssdb", assertAndNormalizeDirectoryUrl(process.env.HOME)), + new URL( + "snap/chromium/current/.pki/nssdb", + assertAndNormalizeDirectoryUrl(process.env.HOME), + ), // Snapcraft + "file:///etc/pki/nssdb", // CentOS 7 + ], + getBrowserClosedPromise: async () => { + if (!isChromeOpen()) { + return; + } + + logger.warn( + `${UNICODE.WARNING} waiting for you to close Chrome before resuming...`, + ); + const next = async () => { + await new Promise((resolve) => setTimeout(resolve, 50)); + if (isChromeOpen()) { + await next(); + } else { + logger.info(`${UNICODE.OK} Chrome closed, resuming`); + // wait 50ms more to ensure chrome has time to cleanup + // othrwise sometimes there is an SEC_ERROR_REUSED_ISSUER_AND_SERIAL error + // because we updated nss database file while chrome is not fully closed + await new Promise((resolve) => setTimeout(resolve, 50)); + } + }; + await next(); + }, + }); +}; + +const isChromeOpen = () => { + return execSync("ps aux").includes("google chrome"); +}; diff --git a/packages/independent/https-local/src/internal/linux/firefox_linux.js b/packages/independent/https-local/src/internal/linux/firefox_linux.js new file mode 100644 index 0000000000..ba66d39ebd --- /dev/null +++ b/packages/independent/https-local/src/internal/linux/firefox_linux.js @@ -0,0 +1,83 @@ +import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem"; +import { UNICODE } from "@jsenv/humanize"; +import { execSync } from "node:child_process"; +import { executeTrustQueryOnBrowserNSSDB } from "../nssdb_browser.js"; +import { + detectIfNSSIsInstalled, + getCertutilBinPath, + getNSSDynamicInstallInfo, + nssCommandName, +} from "./nss_linux.js"; + +export const executeTrustQueryOnFirefox = ({ + logger, + certificateCommonName, + certificateFileUrl, + certificateIsNew, + certificate, + verb, + NSSDynamicInstall, +}) => { + return executeTrustQueryOnBrowserNSSDB({ + logger, + certificateCommonName, + certificateFileUrl, + certificateIsNew, + certificate, + + verb, + NSSDynamicInstall, + nssCommandName, + detectIfNSSIsInstalled, + getNSSDynamicInstallInfo, + getCertutilBinPath, + + browserName: "firefox", + browserPaths: [ + "/usr/bin/firefox", + "/usr/bin/firefox-nightly", + "/usr/bin/firefox-developer-edition", + "/snap/firefox", + ], + browserNSSDBDirectoryUrls: [ + new URL( + ".mozilla/firefox/", + assertAndNormalizeDirectoryUrl(process.env.HOME), + ), + new URL( + "/.mozilla/firefox-trunk/", + assertAndNormalizeDirectoryUrl(process.env.HOME), + ), + new URL( + "/snap/firefox/common/.mozilla/firefox/", + assertAndNormalizeDirectoryUrl(process.env.HOME), + ), + ], + getBrowserClosedPromise: async () => { + if (!isFirefoxOpen()) { + return; + } + + logger.warn( + `${UNICODE.WARNING} waiting for you to close Firefox before resuming...`, + ); + const next = async () => { + await new Promise((resolve) => setTimeout(resolve, 50)); + if (isFirefoxOpen()) { + await next(); + } else { + logger.info(`${UNICODE.OK} Firefox closed, resuming`); + // wait 50ms more to ensure firefox has time to cleanup + // othrwise sometimes there is an SEC_ERROR_REUSED_ISSUER_AND_SERIAL error + // because we updated nss database file while firefox is not fully closed + await new Promise((resolve) => setTimeout(resolve, 50)); + } + }; + await next(); + }, + }); +}; + +const isFirefoxOpen = () => { + return execSync("ps aux").includes("firefox"); +}; diff --git a/packages/independent/https-local/src/internal/linux/linux.js b/packages/independent/https-local/src/internal/linux/linux.js new file mode 100644 index 0000000000..a009362d90 --- /dev/null +++ b/packages/independent/https-local/src/internal/linux/linux.js @@ -0,0 +1,48 @@ +import { executeTrustQueryOnLinux } from "./linux_trust_store.js" +import { executeTrustQueryOnChrome } from "./chrome_linux.js" +import { executeTrustQueryOnFirefox } from "./firefox_linux.js" + +export const executeTrustQuery = async ({ + logger, + certificateCommonName, + certificateFileUrl, + certificateIsNew, + certificate, + verb, + NSSDynamicInstall, +}) => { + const linuxTrustInfo = await executeTrustQueryOnLinux({ + logger, + certificateCommonName, + certificateFileUrl, + certificateIsNew, + certificate, + verb, + }) + + const chromeTrustInfo = await executeTrustQueryOnChrome({ + logger, + certificateCommonName, + certificateFileUrl, + certificateIsNew, + certificate, + verb, + NSSDynamicInstall, + }) + + const firefoxTrustInfo = await executeTrustQueryOnFirefox({ + logger, + certificateCommonName, + certificateFileUrl, + certificateIsNew, + certificate, + verb, + NSSDynamicInstall, + }) + + return { + linux: linuxTrustInfo, + chrome: chromeTrustInfo, + firefox: firefoxTrustInfo, + } +} diff --git a/packages/independent/https-local/src/internal/linux/linux_trust_store.js b/packages/independent/https-local/src/internal/linux/linux_trust_store.js new file mode 100644 index 0000000000..550c81c4f1 --- /dev/null +++ b/packages/independent/https-local/src/internal/linux/linux_trust_store.js @@ -0,0 +1,157 @@ +/* + * see https://github.com/davewasmer/devcert/blob/master/src/platforms/linux.ts + */ + +import { readFile } from "@jsenv/filesystem"; +import { createDetailedMessage, UNICODE } from "@jsenv/humanize"; +import { existsSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { exec } from "../exec.js"; +import { + VERB_ADD_TRUST, + VERB_CHECK_TRUST, + VERB_ENSURE_TRUST, + VERB_REMOVE_TRUST, +} from "../trust_query.js"; + +const REASON_NEW_AND_TRY_TO_TRUST_DISABLED = + "certificate is new and tryToTrust is disabled"; +const REASON_NOT_FOUND_IN_LINUX = `not found in linux store`; +const REASON_OUTDATED_IN_LINUX = "certificate in linux store is outdated"; +const REASON_FOUND_IN_LINUX = "found in linux store"; +const REASON_ADD_COMMAND_FAILED = "command to add certificate to linux failed"; +const REASON_ADD_COMMAND_COMPLETED = + "command to add certificate to linux completed"; +const REASON_REMOVE_COMMAND_FAILED = + "command to remove certificate from linux failed"; +const REASON_REMOVE_COMMAND_COMPLETED = + "command to remove certificate from linux completed"; + +const LINUX_CERTIFICATE_AUTHORITIES_DIRECTORY_PATH = `/usr/local/share/ca-certificates/`; +const JSENV_AUTHORITY_ROOT_CERTIFICATE_PATH = `${LINUX_CERTIFICATE_AUTHORITIES_DIRECTORY_PATH}https_local_root_certificate.crt`; + +export const executeTrustQueryOnLinux = async ({ + logger, + // certificateCommonName, + certificateFileUrl, + certificateIsNew, + certificate, + verb, +}) => { + if (verb === VERB_CHECK_TRUST && certificateIsNew) { + logger.info(`${UNICODE.INFO} You should add certificate to linux`); + return { + status: "not_trusted", + reason: REASON_NEW_AND_TRY_TO_TRUST_DISABLED, + }; + } + + logger.info(`Check if certificate is in linux...`); + logger.debug( + `Searching certificate file at ${JSENV_AUTHORITY_ROOT_CERTIFICATE_PATH}...`, + ); + const certificateFilePath = fileURLToPath(certificateFileUrl); + const certificateStatus = await getCertificateStatus({ certificate }); + + if (certificateStatus === "missing" || certificateStatus === "outdated") { + if (certificateStatus === "missing") { + logger.info(`${UNICODE.INFO} certificate not in linux`); + } else { + logger.info(`${UNICODE.INFO} certificate in linux is outdated`); + } + if (verb === VERB_CHECK_TRUST || verb === VERB_REMOVE_TRUST) { + return { + status: "not_trusted", + reason: + certificateStatus === "missing" + ? REASON_NOT_FOUND_IN_LINUX + : REASON_OUTDATED_IN_LINUX, + }; + } + + const copyCertificateCommand = `sudo /bin/cp -f "${certificateFilePath}" ${JSENV_AUTHORITY_ROOT_CERTIFICATE_PATH}`; + const updateCertificateCommand = `sudo update-ca-certificates`; + logger.info(`Adding certificate to linux...`); + try { + logger.info(`${UNICODE.COMMAND} ${copyCertificateCommand}`); + await exec(copyCertificateCommand); + logger.info(`${UNICODE.COMMAND} ${updateCertificateCommand}`); + await exec(updateCertificateCommand); + logger.info(`${UNICODE.OK} certificate added to linux`); + return { + status: "trusted", + reason: REASON_ADD_COMMAND_COMPLETED, + }; + } catch (e) { + console.error(e); + logger.error( + createDetailedMessage( + `${UNICODE.FAILURE} failed to add certificate to linux`, + { + "certificate file": certificateFilePath, + }, + ), + ); + return { + status: "not_trusted", + reason: REASON_ADD_COMMAND_FAILED, + }; + } + } + + logger.info(`${UNICODE.OK} certificate found in linux`); + if ( + verb === VERB_CHECK_TRUST || + verb === VERB_ADD_TRUST || + verb === VERB_ENSURE_TRUST + ) { + return { + status: "trusted", + reason: REASON_FOUND_IN_LINUX, + }; + } + + logger.info(`Removing certificate from linux...`); + const removeCertificateCommand = `sudo rm ${JSENV_AUTHORITY_ROOT_CERTIFICATE_PATH}`; + const updateCertificateCommand = `sudo update-ca-certificates`; + try { + logger.info(`${UNICODE.COMMAND} ${removeCertificateCommand}`); + await exec(removeCertificateCommand); + logger.info(`${UNICODE.COMMAND} ${updateCertificateCommand}`); + await exec(updateCertificateCommand); + logger.info(`${UNICODE.OK} certificate removed from linux`); + return { + status: "not_trusted", + reason: REASON_REMOVE_COMMAND_COMPLETED, + }; + } catch (e) { + logger.error( + createDetailedMessage( + `${UNICODE.FAILURE} failed to remove certificate from linux`, + { + "error stack": e.stack, + "certificate file": JSENV_AUTHORITY_ROOT_CERTIFICATE_PATH, + }, + ), + ); + return { + status: "unknown", + reason: REASON_REMOVE_COMMAND_FAILED, + }; + } +}; + +const getCertificateStatus = async ({ certificate }) => { + const certificateInStore = existsSync(JSENV_AUTHORITY_ROOT_CERTIFICATE_PATH); + if (!certificateInStore) { + return "missing"; + } + const certificateInLinuxStore = await readFile( + JSENV_AUTHORITY_ROOT_CERTIFICATE_PATH, + { as: "string" }, + ); + if (certificateInLinuxStore !== certificate) { + return "outdated"; + } + return "found"; +}; diff --git a/packages/independent/https-local/src/internal/linux/nss_linux.js b/packages/independent/https-local/src/internal/linux/nss_linux.js new file mode 100644 index 0000000000..c5f7e02094 --- /dev/null +++ b/packages/independent/https-local/src/internal/linux/nss_linux.js @@ -0,0 +1,39 @@ +// https://github.com/FiloSottile/mkcert/issues/447 + +import { UNICODE } from "@jsenv/humanize"; +import { exec } from "../exec.js"; +import { memoize } from "../memoize.js"; + +export const nssCommandName = "libnss3-tools"; + +export const detectIfNSSIsInstalled = memoize(async ({ logger }) => { + logger.debug(`Detect if nss installed....`); + + const aptCommand = `apt list libnss3-tools --installed`; + logger.debug(`${UNICODE.COMMAND} ${aptCommand}`); + const aptCommandOutput = await exec(aptCommand); + + if (aptCommandOutput.includes("libnss3-tools")) { + logger.debug(`${UNICODE.OK} libnss3-tools is installed`); + return true; + } + + logger.debug(`${UNICODE.INFO} libnss3-tools not installed`); + return false; +}); + +export const getCertutilBinPath = () => "certutil"; + +export const getNSSDynamicInstallInfo = ({ logger }) => { + return { + isInstallable: true, + install: async () => { + const aptInstallCommand = `sudo apt install libnss3-tools`; + logger.info( + `"libnss3-tools" is not installed, trying to install "libnss3-tools"`, + ); + logger.info(`${UNICODE.COMMAND} ${aptInstallCommand}`); + await exec(aptInstallCommand); + }, + }; +}; diff --git a/packages/independent/https-local/src/internal/mac/chrome_mac.js b/packages/independent/https-local/src/internal/mac/chrome_mac.js new file mode 100644 index 0000000000..3d900fc881 --- /dev/null +++ b/packages/independent/https-local/src/internal/mac/chrome_mac.js @@ -0,0 +1,33 @@ +import { UNICODE } from "@jsenv/humanize"; +import { existsSync } from "node:fs"; +import { memoize } from "../memoize.js"; + +const REASON_CHROME_NOT_DETECTED = `Chrome not detected`; + +export const executeTrustQueryOnChrome = ({ logger, macTrustInfo }) => { + const chromeDetected = detectChrome({ logger }); + if (!chromeDetected) { + return { + status: "other", + reason: REASON_CHROME_NOT_DETECTED, + }; + } + + return { + status: macTrustInfo.status, + reason: macTrustInfo.reason, + }; +}; + +const detectChrome = memoize(({ logger }) => { + logger.debug(`Detecting Chrome...`); + const chromeDetected = existsSync("/Applications/Google Chrome.app"); + + if (chromeDetected) { + logger.debug(`${UNICODE.OK} Chrome detected`); + return true; + } + + logger.debug(`${UNICODE.INFO} Chrome not detected`); + return false; +}); diff --git a/packages/independent/https-local/src/internal/mac/firefox_mac.js b/packages/independent/https-local/src/internal/mac/firefox_mac.js new file mode 100644 index 0000000000..f2849a77ba --- /dev/null +++ b/packages/independent/https-local/src/internal/mac/firefox_mac.js @@ -0,0 +1,77 @@ +import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem"; +import { UNICODE, createTaskLog } from "@jsenv/humanize"; +import { execSync } from "node:child_process"; +import { executeTrustQueryOnBrowserNSSDB } from "../nssdb_browser.js"; +import { + detectIfNSSIsInstalled, + getCertutilBinPath, + getNSSDynamicInstallInfo, + nssCommandName, +} from "./nss_mac.js"; + +export const executeTrustQueryOnFirefox = ({ + logger, + certificateCommonName, + certificateFileUrl, + certificateIsNew, + certificate, + verb, + NSSDynamicInstall, +}) => { + return executeTrustQueryOnBrowserNSSDB({ + logger, + certificateCommonName, + certificateFileUrl, + certificateIsNew, + certificate, + + verb, + NSSDynamicInstall, + nssCommandName, + detectIfNSSIsInstalled, + getNSSDynamicInstallInfo, + getCertutilBinPath, + + browserName: "firefox", + browserPaths: [ + "/Applications/Firefox.app", + "/Applications/FirefoxDeveloperEdition.app", + "/Applications/Firefox Developer Edition.app", + "/Applications/Firefox Nightly.app", + ], + browserNSSDBDirectoryUrls: [ + new URL( + `./Library/Application Support/Firefox/Profiles/`, + assertAndNormalizeDirectoryUrl(process.env.HOME), + ), + ], + getBrowserClosedPromise: async () => { + if (!isFirefoxOpen()) { + return; + } + + logger.warn( + `${UNICODE.WARNING} firefox is running, it must be stopped before resuming...`, + ); + const closeFirefoxTask = createTaskLog("waiting for firefox to close"); + const next = async () => { + await new Promise((resolve) => setTimeout(resolve, 50)); + if (isFirefoxOpen()) { + await next(); + } else { + closeFirefoxTask.done(); + // wait 50ms more to ensure firefox has time to cleanup + // othrwise sometimes there is an SEC_ERROR_REUSED_ISSUER_AND_SERIAL error + // because we updated nss database file while firefox is not fully closed + await new Promise((resolve) => setTimeout(resolve, 50)); + } + }; + await next(); + }, + }); +}; + +const isFirefoxOpen = () => { + const psAux = execSync("ps aux"); + return psAux.includes("Firefox.app"); +}; diff --git a/packages/independent/https-local/src/internal/mac/mac.js b/packages/independent/https-local/src/internal/mac/mac.js new file mode 100644 index 0000000000..cee5a6ad2b --- /dev/null +++ b/packages/independent/https-local/src/internal/mac/mac.js @@ -0,0 +1,58 @@ +/* + * see + * - https://github.com/davewasmer/devcert/blob/master/src/platforms/darwin.ts + * - https://www.unix.com/man-page/mojave/1/security/ + */ + +import { executeTrustQueryOnMacKeychain } from "./mac_keychain.js" +import { executeTrustQueryOnChrome } from "./chrome_mac.js" +import { executeTrustQueryOnFirefox } from "./firefox_mac.js" +import { executeTrustQueryOnSafari } from "./safari.js" + +export const executeTrustQuery = async ({ + logger, + certificateCommonName, + certificateFileUrl, + certificateIsNew, + certificate, + verb, + NSSDynamicInstall, +}) => { + const macTrustInfo = await executeTrustQueryOnMacKeychain({ + logger, + certificateCommonName, + certificateFileUrl, + certificateIsNew, + certificate, + verb, + }) + + const chromeTrustInfo = await executeTrustQueryOnChrome({ + logger, + // chrome needs macTrustInfo because it uses OS trust store + macTrustInfo, + }) + + const firefoxTrustInfo = await executeTrustQueryOnFirefox({ + logger, + certificateCommonName, + certificateFileUrl, + certificateIsNew, + certificate, + verb, + NSSDynamicInstall, + }) + + const safariTrustInfo = await executeTrustQueryOnSafari({ + logger, + // safari needs macTrustInfo because it uses OS trust store + macTrustInfo, + }) + + return { + mac: macTrustInfo, + chrome: chromeTrustInfo, + firefox: firefoxTrustInfo, + safari: safariTrustInfo, + } +} diff --git a/packages/independent/https-local/src/internal/mac/mac_keychain.js b/packages/independent/https-local/src/internal/mac/mac_keychain.js new file mode 100644 index 0000000000..743b14a1df --- /dev/null +++ b/packages/independent/https-local/src/internal/mac/mac_keychain.js @@ -0,0 +1,145 @@ +// https://ss64.com/osx/security.html + +import { createDetailedMessage, UNICODE } from "@jsenv/humanize"; +import { fileURLToPath } from "node:url"; +import { exec } from "../exec.js"; +import { searchCertificateInCommandOutput } from "../search_certificate_in_command_output.js"; +import { + VERB_ADD_TRUST, + VERB_CHECK_TRUST, + VERB_ENSURE_TRUST, + VERB_REMOVE_TRUST, +} from "../trust_query.js"; + +const REASON_NEW_AND_TRY_TO_TRUST_DISABLED = + "certificate is new and tryToTrust is disabled"; +const REASON_NOT_IN_KEYCHAIN = "certificate not found in mac keychain"; +const REASON_IN_KEYCHAIN = "certificate found in mac keychain"; +const REASON_ADD_TO_KEYCHAIN_COMMAND_FAILED = + "command to add certificate in mac keychain failed"; +const REASON_ADD_TO_KEYCHAIN_COMMAND_COMPLETED = + "command to add certificate in mac keychain completed"; +const REASON_REMOVE_FROM_KEYCHAIN_COMMAND_FAILED = + "command to remove certificate from mac keychain failed"; +const REASON_REMOVE_FROM_KEYCHAIN_COMMAND_COMPLETED = + "command to remove certificate from mac keychain completed"; + +const systemKeychainPath = "/Library/Keychains/System.keychain"; + +export const executeTrustQueryOnMacKeychain = async ({ + logger, + certificateCommonName, + certificateFileUrl, + certificateIsNew, + certificate, + verb, +}) => { + if (verb === VERB_CHECK_TRUST && certificateIsNew) { + logger.info(`${UNICODE.INFO} You should add certificate to mac keychain`); + return { + status: "not_trusted", + reason: REASON_NEW_AND_TRY_TO_TRUST_DISABLED, + }; + } + + logger.info(`Check if certificate is in mac keychain...`); + // https://ss64.com/osx/security-find-cert.html + const findCertificateCommand = `security find-certificate -a -p ${systemKeychainPath}`; + logger.debug(`${UNICODE.COMMAND} ${findCertificateCommand}`); + const findCertificateCommandOutput = await exec(findCertificateCommand); + const certificateFoundInCommandOutput = searchCertificateInCommandOutput( + findCertificateCommandOutput, + certificate, + ); + + const removeCert = async () => { + // https://ss64.com/osx/security-delete-cert.html + const removeTrustedCertCommand = `sudo security delete-certificate -c "${certificateCommonName}"`; + logger.info(`Removing certificate from mac keychain...`); + logger.info(`${UNICODE.COMMAND} ${removeTrustedCertCommand}`); + try { + await exec(removeTrustedCertCommand); + logger.info(`${UNICODE.OK} certificate removed from mac keychain`); + return { + status: "not_trusted", + reason: REASON_REMOVE_FROM_KEYCHAIN_COMMAND_COMPLETED, + }; + } catch (e) { + logger.error( + createDetailedMessage( + `${UNICODE.FAILURE} failed to remove certificate from mac keychain`, + { + "error stack": e.stack, + "certificate file url": certificateFileUrl, + }, + ), + ); + return { + status: "not_trusted", + reason: REASON_REMOVE_FROM_KEYCHAIN_COMMAND_FAILED, + }; + } + }; + + if (!certificateFoundInCommandOutput) { + logger.info(`${UNICODE.INFO} certificate not found in mac keychain`); + if (verb === VERB_CHECK_TRUST || verb === VERB_REMOVE_TRUST) { + return { + status: "not_trusted", + reason: REASON_NOT_IN_KEYCHAIN, + }; + } + if (verb === VERB_ENSURE_TRUST) { + // It seems possible for certificate PEM representation to be different + // in mackeychain and in the one we have written on the filesystem + // When it happens the certificate is not found but actually exists on mackeychain + // and must be deleted first + await removeCert(); + } + const certificateFilePath = fileURLToPath(certificateFileUrl); + // https://ss64.com/osx/security-cert.html + const addTrustedCertCommand = `sudo security add-trusted-cert -d -r trustRoot -k ${systemKeychainPath} "${certificateFilePath}"`; + logger.info(`Adding certificate to mac keychain...`); + logger.info(`${UNICODE.COMMAND} ${addTrustedCertCommand}`); + try { + await exec(addTrustedCertCommand); + logger.info(`${UNICODE.OK} certificate added to mac keychain`); + return { + status: "trusted", + reason: REASON_ADD_TO_KEYCHAIN_COMMAND_COMPLETED, + }; + } catch (e) { + logger.error( + createDetailedMessage( + `${UNICODE.FAILURE} failed to add certificate to mac keychain`, + { + "error stack": e.stack, + "certificate file": certificateFilePath, + }, + ), + ); + return { + status: "not_trusted", + reason: REASON_ADD_TO_KEYCHAIN_COMMAND_FAILED, + }; + } + } + + // being in the keychain do not guarantee certificate is trusted + // people can still manually untrust the root cert + // but they shouldn't and I couldn't find an API to know if the cert is trusted or not + // just if it's in the keychain + logger.info(`${UNICODE.OK} certificate found in mac keychain`); + if ( + verb === VERB_CHECK_TRUST || + verb === VERB_ADD_TRUST || + verb === VERB_ENSURE_TRUST + ) { + return { + status: "trusted", + reason: REASON_IN_KEYCHAIN, + }; + } + + return removeCert(); +}; diff --git a/packages/independent/https-local/src/internal/mac/nss_mac.js b/packages/independent/https-local/src/internal/mac/nss_mac.js new file mode 100644 index 0000000000..2c8d179f20 --- /dev/null +++ b/packages/independent/https-local/src/internal/mac/nss_mac.js @@ -0,0 +1,49 @@ +import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem"; +import { UNICODE } from "@jsenv/humanize"; +import { fileURLToPath } from "node:url"; +import { commandExists } from "../command.js"; +import { exec } from "../exec.js"; +import { memoize } from "../memoize.js"; + +export const nssCommandName = "nss"; + +export const detectIfNSSIsInstalled = async ({ logger }) => { + logger.debug(`Detecting if nss is installed....`); + const brewListCommand = `brew list --versions nss`; + + try { + await exec(brewListCommand); + logger.debug(`${UNICODE.OK} nss is installed`); + return true; + } catch { + logger.debug(`${UNICODE.INFO} nss not installed`); + return false; + } +}; + +export const getCertutilBinPath = memoize(async () => { + const brewCommand = `brew --prefix nss`; + const brewCommandOutput = await exec(brewCommand); + const nssCommandDirectoryUrl = assertAndNormalizeDirectoryUrl( + brewCommandOutput.trim(), + ); + const certutilBinUrl = new URL(`./bin/certutil`, nssCommandDirectoryUrl).href; + const certutilBinPath = fileURLToPath(certutilBinUrl); + return certutilBinPath; +}); + +export const getNSSDynamicInstallInfo = () => { + return { + isInstallable: commandExists("brew"), + notInstallableReason: `"brew" is not available`, + suggestion: `install "brew" on this mac`, + install: async ({ logger }) => { + const brewInstallCommand = `brew install nss`; + logger.info( + `"nss" is not installed, trying to install "nss" via Homebrew`, + ); + logger.info(`${UNICODE.COMMAND} ${brewInstallCommand}`); + await exec(brewInstallCommand); + }, + }; +}; diff --git a/packages/independent/https-local/src/internal/mac/safari.js b/packages/independent/https-local/src/internal/mac/safari.js new file mode 100644 index 0000000000..2356ce962a --- /dev/null +++ b/packages/independent/https-local/src/internal/mac/safari.js @@ -0,0 +1,6 @@ +export const executeTrustQueryOnSafari = ({ macTrustInfo }) => { + return { + status: macTrustInfo.status, + reason: macTrustInfo.reason, + } +} diff --git a/packages/independent/https-local/src/internal/memoize.js b/packages/independent/https-local/src/internal/memoize.js new file mode 100644 index 0000000000..c49b8708d3 --- /dev/null +++ b/packages/independent/https-local/src/internal/memoize.js @@ -0,0 +1,24 @@ +export const memoize = (compute) => { + let memoized = false + let memoizedValue + + const fnWithMemoization = (...args) => { + if (memoized) { + return memoizedValue + } + // if compute is recursive wait for it to be fully done before storing the lockValue + // so set locked later + memoizedValue = compute(...args) + memoized = true + return memoizedValue + } + + fnWithMemoization.forget = () => { + const value = memoizedValue + memoized = false + memoizedValue = undefined + return value + } + + return fnWithMemoization +} diff --git a/packages/independent/https-local/src/internal/nssdb_browser.js b/packages/independent/https-local/src/internal/nssdb_browser.js new file mode 100644 index 0000000000..e2b831af5f --- /dev/null +++ b/packages/independent/https-local/src/internal/nssdb_browser.js @@ -0,0 +1,358 @@ +/* + * NSS DB stands for Network Security Service DataBase + * Certutil command documentation: https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/tools/NSS_Tools_certutil + */ + +import { + assertAndNormalizeDirectoryUrl, + collectFiles, +} from "@jsenv/filesystem"; +import { createDetailedMessage, UNICODE } from "@jsenv/humanize"; +import { urlToFilename } from "@jsenv/urls"; +import { existsSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { detectBrowser } from "./browser_detection.js"; +import { exec } from "./exec.js"; +import { searchCertificateInCommandOutput } from "./search_certificate_in_command_output.js"; +import { + VERB_ADD_TRUST, + VERB_CHECK_TRUST, + VERB_ENSURE_TRUST, +} from "./trust_query.js"; + +export const executeTrustQueryOnBrowserNSSDB = async ({ + logger, + certificateCommonName, + certificateFileUrl, + certificateIsNew, + certificate, + + verb, + NSSDynamicInstall, + nssCommandName, + detectIfNSSIsInstalled, + getNSSDynamicInstallInfo, + getCertutilBinPath, + + browserName, + browserPaths, + browserNSSDBDirectoryUrls, + getBrowserClosedPromise, +}) => { + logger.debug(`Detecting ${browserName}...`); + + const browserDetected = detectBrowser(browserPaths); + if (!browserDetected) { + logger.debug(`${UNICODE.INFO} ${browserName} not detected`); + return { + status: "other", + reason: `${browserName} not detected`, + }; + } + logger.debug(`${UNICODE.OK} ${browserName} detected`); + + if (verb === VERB_CHECK_TRUST && certificateIsNew) { + logger.info(`${UNICODE.INFO} You should add certificate to ${browserName}`); + return { + status: "not_trusted", + reason: "certificate is new and tryToTrust is disabled", + }; + } + + logger.info(`Check if certificate is in ${browserName}...`); + const nssIsInstalled = await detectIfNSSIsInstalled({ logger }); + const cannotCheckMessage = `${UNICODE.FAILURE} cannot check if certificate is in ${browserName}`; + if (!nssIsInstalled) { + if (verb === VERB_ADD_TRUST || verb === VERB_ENSURE_TRUST) { + const nssDynamicInstallInfo = await getNSSDynamicInstallInfo({ logger }); + if (!nssDynamicInstallInfo.isInstallable) { + const reason = `"${nssCommandName}" is not installed and not cannot be installed`; + logger.warn( + createDetailedMessage(cannotCheckMessage, { + reason, + "reason it cannot be installed": + nssDynamicInstallInfo.notInstallableReason, + "suggested solution": nssDynamicInstallInfo.suggestion, + }), + ); + return { + status: "unknown", + reason, + }; + } + + if (!NSSDynamicInstall) { + const reason = `"${nssCommandName}" is not installed and NSSDynamicInstall is false`; + logger.warn( + createDetailedMessage(cannotCheckMessage, { + reason, + "suggested solution": `Allow "${nssCommandName}" dynamic install with NSSDynamicInstall: true`, + }), + ); + return { + status: "unknown", + reason, + }; + } + + try { + await nssDynamicInstallInfo.install({ logger }); + } catch (e) { + logger.error( + createDetailedMessage(cannotCheckMessage, { + "reason": `error while trying to install "${nssCommandName}"`, + "error stack": e.stack, + }), + ); + return { + status: "unknown", + reason: `"${nssCommandName}" installation failed`, + }; + } + } else { + const reason = `"${nssCommandName}" is not installed`; + logger.info( + createDetailedMessage(cannotCheckMessage, { + reason, + }), + ); + return { + status: "unknown", + reason, + }; + } + } + + let NSSDBFiles; + for (const browserNSSDBDirectoryUrl of browserNSSDBDirectoryUrls) { + NSSDBFiles = await findNSSDBFiles({ + logger, + NSSDBDirectoryUrl: browserNSSDBDirectoryUrl, + }); + if (NSSDBFiles.length > 0) { + break; + } + } + + const fileCount = NSSDBFiles.length; + if (fileCount === 0) { + const reason = `could not find nss database file`; + logger.warn(createDetailedMessage(cannotCheckMessage), { reason }); + return { + status: "unknown", + reason, + }; + } + + const certificateFilePath = fileURLToPath(certificateFileUrl); + const certutilBinPath = await getCertutilBinPath(); + + const checkNSSDB = async ({ NSSDBFileUrl }) => { + const directoryArg = getDirectoryArgFromNSSDBFileUrl(NSSDBFileUrl); + const certutilListCommand = `${certutilBinPath} -L -a -d ${directoryArg} -n "${certificateCommonName}"`; + logger.debug(`Checking if certificate is in nss database...`); + logger.debug(`${UNICODE.COMMAND} ${certutilListCommand}`); + try { + const output = await execCertutilCommmand(certutilListCommand); + const isInDatabase = searchCertificateInCommandOutput( + output, + certificate, + ); + if (isInDatabase) { + return "found"; + } + return "outdated"; + } catch (e) { + if (isCertificateNotFoundError(e)) { + return "missing"; + } + throw e; + } + }; + + const addToNSSDB = async ({ NSSDBFileUrl }) => { + const directoryArg = getDirectoryArgFromNSSDBFileUrl(NSSDBFileUrl); + const certutilAddCommand = `${certutilBinPath} -A -d ${directoryArg} -t C,, -i "${certificateFilePath}" -n "${certificateCommonName}"`; + logger.debug(`Adding certificate to nss database...`); + logger.debug(`${UNICODE.COMMAND} ${certutilAddCommand}`); + await execCertutilCommmand(certutilAddCommand); + logger.debug(`${UNICODE.OK} certificate added to nss database`); + }; + + const removeFromNSSDB = async ({ NSSDBFileUrl }) => { + const directoryArg = getDirectoryArgFromNSSDBFileUrl(NSSDBFileUrl); + const certutilRemoveCommand = `${certutilBinPath} -D -d ${directoryArg} -t C,, -i "${certificateFilePath}" -n "${certificateCommonName}"`; + logger.debug(`Removing certificate from nss database...`); + logger.debug(`${UNICODE.COMMAND} ${certutilRemoveCommand}`); + await execCertutilCommmand(certutilRemoveCommand); + logger.debug(`${UNICODE.OK} certificate removed from nss database`); + }; + + const missings = []; + const outdateds = []; + const founds = []; + await Promise.all( + NSSDBFiles.map(async (NSSDBFileUrl) => { + const certificateStatus = await checkNSSDB({ NSSDBFileUrl }); + + if (certificateStatus === "missing") { + logger.debug(`${UNICODE.INFO} certificate not found in nss database`); + missings.push(NSSDBFileUrl); + return; + } + + if (certificateStatus === "outdated") { + outdateds.push(NSSDBFileUrl); + return; + } + + logger.debug(`${UNICODE.OK} certificate found in nss database`); + founds.push(NSSDBFileUrl); + }), + ); + + const missingCount = missings.length; + const outdatedCount = outdateds.length; + const foundCount = founds.length; + + if (verb === VERB_CHECK_TRUST) { + if (missingCount > 0 || outdatedCount > 0) { + logger.info(`${UNICODE.INFO} certificate not found in ${browserName}`); + return { + status: "not_trusted", + reason: `missing or outdated in ${browserName} nss database file`, + }; + } + logger.info(`${UNICODE.OK} certificate found in ${browserName}`); + return { + status: "trusted", + reason: `found in ${browserName} nss database file`, + }; + } + + if (verb === VERB_ADD_TRUST || verb === VERB_ENSURE_TRUST) { + if (missingCount === 0 && outdatedCount === 0) { + logger.info(`${UNICODE.OK} certificate found in ${browserName}`); + return { + status: "trusted", + reason: `found in all ${browserName} nss database file`, + }; + } + logger.info(`${UNICODE.INFO} certificate not found in ${browserName}`); + logger.info(`Adding certificate to ${browserName}...`); + await getBrowserClosedPromise(); + await Promise.all( + missings.map(async (missing) => { + await addToNSSDB({ NSSDBFileUrl: missing }); + }), + ); + await Promise.all( + outdateds.map(async (outdated) => { + await removeFromNSSDB({ NSSDBFileUrl: outdated }); + await addToNSSDB({ NSSDBFileUrl: outdated }); + }), + ); + logger.info(`${UNICODE.OK} certificate added to ${browserName}`); + return { + status: "trusted", + reason: `added to ${browserName} nss database file`, + }; + } + + if (outdatedCount === 0 && foundCount === 0) { + logger.info(`${UNICODE.INFO} certificate not found in ${browserName}`); + return { + status: "not_trusted", + reason: `not found in ${browserName} nss database file`, + }; + } + logger.info(`${UNICODE.INFO} found certificate in ${browserName}`); + logger.info(`Removing certificate from ${browserName}...`); + await getBrowserClosedPromise(); + await Promise.all( + outdateds.map(async (outdated) => { + await removeFromNSSDB({ NSSDBFileUrl: outdated }); + }), + ); + await Promise.all( + founds.map(async (found) => { + await removeFromNSSDB({ NSSDBFileUrl: found }); + }), + ); + logger.info(`${UNICODE.OK} certificate removed from ${browserName}`); + return { + status: "not_trusted", + reason: `removed from ${browserName} nss database file`, + }; +}; + +const isCertificateNotFoundError = (error) => { + if (error.message.includes("could not find certificate named")) { + return true; + } + if (error.message.includes("PR_FILE_NOT_FOUND_ERROR")) { + return true; + } + return false; +}; + +const NSSDirectoryCache = {}; +const findNSSDBFiles = async ({ logger, NSSDBDirectoryUrl }) => { + NSSDBDirectoryUrl = String(NSSDBDirectoryUrl); + const resultFromCache = NSSDirectoryCache[NSSDBDirectoryUrl]; + if (resultFromCache) { + return resultFromCache; + } + + logger.debug(`Searching nss database files in directory...`); + const NSSDBDirectoryPath = fileURLToPath(NSSDBDirectoryUrl); + const NSSDBDirectoryExists = existsSync(NSSDBDirectoryPath); + if (!NSSDBDirectoryExists) { + logger.info( + `${UNICODE.INFO} nss database directory not found on filesystem at ${NSSDBDirectoryPath}`, + ); + NSSDirectoryCache[NSSDBDirectoryUrl] = []; + return []; + } + NSSDBDirectoryUrl = assertAndNormalizeDirectoryUrl(NSSDBDirectoryUrl); + const NSSDBFiles = await collectFiles({ + directoryUrl: NSSDBDirectoryUrl, + associations: { + isLegacyNSSDB: { "./**/cert8.db": true }, + isModernNSSDB: { "./**/cert9.db": true }, + }, + predicate: ({ isLegacyNSSDB, isModernNSSDB }) => + isLegacyNSSDB || isModernNSSDB, + }); + const fileCount = NSSDBFiles.length; + if (fileCount === 0) { + logger.warn( + `${UNICODE.WARNING} could not find nss database file in ${NSSDBDirectoryUrl}`, + ); + NSSDirectoryCache[NSSDBDirectoryUrl] = []; + return []; + } + + logger.debug( + `${UNICODE.OK} found ${fileCount} nss database file in ${NSSDBDirectoryUrl}`, + ); + const files = NSSDBFiles.map((file) => { + return new URL(file.relativeUrl, NSSDBDirectoryUrl).href; + }); + NSSDirectoryCache[NSSDBDirectoryUrl] = files; + return files; +}; + +const getDirectoryArgFromNSSDBFileUrl = (NSSDBFileUrl) => { + const nssDBFilename = urlToFilename(NSSDBFileUrl); + const nssDBDirectoryUrl = new URL("./", NSSDBFileUrl).href; + const nssDBDirectoryPath = fileURLToPath(nssDBDirectoryUrl); + return nssDBFilename === "cert8.db" + ? `"${nssDBDirectoryPath}"` + : `sql:"${nssDBDirectoryPath}"`; +}; + +const execCertutilCommmand = async (command) => { + const output = await exec(command); + return output; +}; diff --git a/packages/independent/https-local/src/internal/platform.js b/packages/independent/https-local/src/internal/platform.js new file mode 100644 index 0000000000..d2de6d1c35 --- /dev/null +++ b/packages/independent/https-local/src/internal/platform.js @@ -0,0 +1,13 @@ +export const importPlatformMethods = async () => { + const { platform } = process + if (platform === "darwin") { + return await import("./mac/mac.js") + } + if (platform === "linux") { + return await import("./linux/linux.js") + } + if (platform === "win32") { + return await import("./windows/windows.js") + } + return await import("./unsupported_platform/unsupported_platform.js") +} diff --git a/packages/independent/https-local/src/internal/search_certificate_in_command_output.js b/packages/independent/https-local/src/internal/search_certificate_in_command_output.js new file mode 100644 index 0000000000..2df1af3f00 --- /dev/null +++ b/packages/independent/https-local/src/internal/search_certificate_in_command_output.js @@ -0,0 +1,8 @@ +export const searchCertificateInCommandOutput = ( + commandOutput, + certificateAsPEM, +) => { + commandOutput = commandOutput.replace(/\r\n/g, "\n").trim() + certificateAsPEM = certificateAsPEM.replace(/\r\n/g, "\n").trim() + return commandOutput.includes(certificateAsPEM) +} diff --git a/packages/independent/https-local/src/internal/trust_query.js b/packages/independent/https-local/src/internal/trust_query.js new file mode 100644 index 0000000000..bb48077b03 --- /dev/null +++ b/packages/independent/https-local/src/internal/trust_query.js @@ -0,0 +1,4 @@ +export const VERB_CHECK_TRUST = "CHECK_TRUST" +export const VERB_ADD_TRUST = "ADD_TRUST" +export const VERB_ENSURE_TRUST = "ENSURE_TRUST" +export const VERB_REMOVE_TRUST = "REMOVE_TRUST" diff --git a/packages/independent/https-local/src/internal/unsupported_platform/unsupported_platform.js b/packages/independent/https-local/src/internal/unsupported_platform/unsupported_platform.js new file mode 100644 index 0000000000..0ddd4a2d00 --- /dev/null +++ b/packages/independent/https-local/src/internal/unsupported_platform/unsupported_platform.js @@ -0,0 +1,15 @@ +import { UNICODE } from "@jsenv/humanize"; + +const platformTrustInfo = { + status: "unknown", + reason: "unsupported platform", +}; + +export const executeTrustQuery = ({ logger }) => { + logger.warn( + `${UNICODE.WARNING} platform not supported, cannot execute trust query`, + ); + return { + platform: platformTrustInfo, + }; +}; diff --git a/packages/independent/https-local/src/internal/validity_formatting.js b/packages/independent/https-local/src/internal/validity_formatting.js new file mode 100644 index 0000000000..4bd68a2762 --- /dev/null +++ b/packages/independent/https-local/src/internal/validity_formatting.js @@ -0,0 +1,79 @@ +export const formatStillValid = ({ certificateName, validityRemainingMs }) => { + return `${certificateName} still valid for ${formatDuration( + validityRemainingMs, + )}` +} + +export const formatAboutToExpire = ({ + certificateName, + validityRemainingMs, + msEllapsedSinceValid, +}) => { + return `${certificateName} will expire ${formatTimeDelta( + validityRemainingMs, + )}, it was valid during ${formatDuration(msEllapsedSinceValid)}` +} + +export const formatExpired = ({ + certificateName, + msEllapsedSinceExpiration, + validityDurationInMs, +}) => { + return `${certificateName} has expired ${formatTimeDelta( + -msEllapsedSinceExpiration, + )}, it was valid during ${formatDuration(validityDurationInMs)}` +} + +export const formatTimeDelta = (deltaInMs) => { + const unit = pickUnit(Math.abs(deltaInMs)) + const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" }) + const msRounded = unit.min ? Math.floor(deltaInMs / unit.min) : deltaInMs + return rtf.format(msRounded, unit.name) +} + +export const formatDuration = (ms) => { + const unit = pickUnit(ms) + const rtf = new Intl.RelativeTimeFormat("en", { numeric: "always" }) + const msRounded = unit.min ? Math.floor(ms / unit.min) : ms + const parts = rtf.formatToParts(msRounded, unit.name) + if (parts.length > 1 && parts[0].type === "literal") { + return parts + .slice(1) + .map((part) => part.value) + .join("") + } + return parts.map((part) => part.value).join("") +} + +const pickUnit = (ms) => { + const msPerSecond = 1000 + const msPerMinute = msPerSecond * 60 + const msPerHour = msPerMinute * 60 + const msPerDay = msPerHour * 24 + const msPerMonth = msPerDay * 30 + const msPerYear = msPerDay * 365 + + if (ms < msPerSecond) { + return { + name: "second", + // min 0 to allow display of 0.01 second for example + min: 0, + } + } + if (ms < msPerMinute) { + return { name: "second", min: msPerSecond } + } + if (ms < msPerHour) { + return { name: "minute", min: msPerMinute } + } + if (ms < msPerDay) { + return { name: "hour", min: msPerHour } + } + if (ms < msPerMonth) { + return { name: "day", min: msPerDay } + } + if (ms < msPerYear) { + return { name: "month", min: msPerMonth } + } + return { name: "year", min: msPerYear } +} diff --git a/packages/independent/https-local/src/internal/windows/chrome_windows.js b/packages/independent/https-local/src/internal/windows/chrome_windows.js new file mode 100644 index 0000000000..49a1f13749 --- /dev/null +++ b/packages/independent/https-local/src/internal/windows/chrome_windows.js @@ -0,0 +1,61 @@ +import { UNICODE } from "@jsenv/humanize"; +import { existsSync } from "node:fs"; +import { createRequire } from "node:module"; +import { memoize } from "../memoize.js"; + +const require = createRequire(import.meta.url); + +const which = require("which"); + +const REASON_CHROME_NOT_DETECTED = `Chrome not detected`; + +export const executeTrustQueryOnChrome = ({ logger, windowsTrustInfo }) => { + const chromeDetected = detectChrome({ logger }); + if (!chromeDetected) { + return { + status: "other", + reason: REASON_CHROME_NOT_DETECTED, + }; + } + + return { + status: windowsTrustInfo.status, + reason: windowsTrustInfo.reason, + }; +}; + +// https://github.com/litixsoft/karma-detect-browsers/blob/332b4bdb2ab3db7c6a1a6d3ec5a1c6ccf2332c4d/browsers/Chrome.js#L1 +const detectChrome = memoize(({ logger }) => { + logger.debug(`Detecting Chrome...`); + + if (process.env.CHROME_BIN && which.sync(process.env.CHROME_BIN)) { + logger.debug(`${UNICODE.OK} Chrome detected`); + return true; + } + + const executableCandidates = [ + `${process.env.LOCALAPPDATA}\\Google\\Chrome\\Application\\chrome.exe`, + `${process.env.ProgramW6432}\\Google\\Chrome\\Application\\chrome.exe`, + `${process.env.ProgramFiles}\\Google\\Chrome\\Application\\chrome.exe`, + `${process.env["ProgramFiles(x86)"]}\\Google\\Chrome\\Application\\chrome.exe`, + ]; + const someExecutableFound = executableCandidates.some( + (chromeExecutablePathCandidate) => { + if (existsSync(chromeExecutablePathCandidate)) { + return true; + } + try { + which.sync(chromeExecutablePathCandidate); + return true; + } catch {} + return false; + }, + ); + if (someExecutableFound) { + logger.debug(`${UNICODE.OK} Chrome detected`); + return true; + } + + logger.debug(`${UNICODE.OK} Chrome detected`); + return false; +}); diff --git a/packages/independent/https-local/src/internal/windows/edge.js b/packages/independent/https-local/src/internal/windows/edge.js new file mode 100644 index 0000000000..888ffd24ad --- /dev/null +++ b/packages/independent/https-local/src/internal/windows/edge.js @@ -0,0 +1,6 @@ +export const executeTrustQueryOnEdge = ({ windowsTrustInfo }) => { + return { + status: windowsTrustInfo.status, + reason: windowsTrustInfo.reason, + } +} diff --git a/packages/independent/https-local/src/internal/windows/firefox_windows.js b/packages/independent/https-local/src/internal/windows/firefox_windows.js new file mode 100644 index 0000000000..e7c4e691b0 --- /dev/null +++ b/packages/independent/https-local/src/internal/windows/firefox_windows.js @@ -0,0 +1,79 @@ +/* + * Missing things that would be nice to have: + * - A way to install and use NSS command on windows to update firefox NSS dabatase file + */ + +import { UNICODE } from "@jsenv/humanize"; +import { existsSync } from "node:fs"; +import { createRequire } from "node:module"; +import { memoize } from "../memoize.js"; + +const require = createRequire(import.meta.url); + +const which = require("which"); + +const REASON_FIREFOX_NOT_DETECTED = "Firefox not detected"; +const REASON_NOT_IMPLEMENTED_ON_WINDOWS = "not implemented on windows"; + +export const executeTrustQueryOnFirefox = ({ logger, certificateIsNew }) => { + const firefoxDetected = detectFirefox({ logger }); + if (!firefoxDetected) { + return { + status: "other", + reason: REASON_FIREFOX_NOT_DETECTED, + }; + } + + if (certificateIsNew) { + logger.info(`${UNICODE.INFO} You should add certificate to firefox`); + return { + status: "not_trusted", + reason: "certificate is new and tryToTrust is disabled", + }; + } + + logger.info(`Check if certificate is in firefox...`); + logger.info( + `${UNICODE.INFO} cannot check if certificate is in firefox (${REASON_NOT_IMPLEMENTED_ON_WINDOWS})`, + ); + return { + status: "unknown", + reason: REASON_NOT_IMPLEMENTED_ON_WINDOWS, + }; +}; + +// https://github.com/litixsoft/karma-detect-browsers +const detectFirefox = memoize(({ logger }) => { + logger.debug(`Detecting Firefox...`); + + if (process.env.FIREFOX_BIN && which.sync(process.env.FIREFOX_BIN)) { + logger.debug(`${UNICODE.OK} Firefox detected`); + return true; + } + + const executableCandidates = [ + `${process.env.LOCALAPPDATA}\\Mozilla Firefox\\firefox.exe`, + `${process.env.ProgramW6432}\\Mozilla Firefox\\firefox.exe`, + `${process.env.ProgramFiles}\\Mozilla Firefox\\firefox.exe`, + `${process.env["ProgramFiles(x86)"]}\\Mozilla Firefox\\firefox.exe`, + ]; + const someExecutableFound = executableCandidates.some( + (firefoxExecutablePathCandidate) => { + if (existsSync(firefoxExecutablePathCandidate)) { + return true; + } + try { + which.sync(firefoxExecutablePathCandidate); + return true; + } catch {} + return false; + }, + ); + if (someExecutableFound) { + logger.debug(`${UNICODE.OK} Firefox detected`); + return true; + } + + logger.debug(`${UNICODE.INFO} Firefox detected`); + return false; +}); diff --git a/packages/independent/https-local/src/internal/windows/windows.js b/packages/independent/https-local/src/internal/windows/windows.js new file mode 100644 index 0000000000..b4c04931d4 --- /dev/null +++ b/packages/independent/https-local/src/internal/windows/windows.js @@ -0,0 +1,49 @@ +/* + * see + * - https://github.com/davewasmer/devcert/blob/master/src/platforms/darwin.ts + * - https://www.unix.com/man-page/mojave/1/security/ + */ + +import { executeTrustQueryOnWindows } from "./windows_certutil.js" +import { executeTrustQueryOnChrome } from "./chrome_windows.js" +import { executeTrustQueryOnEdge } from "./edge.js" +import { executeTrustQueryOnFirefox } from "./firefox_windows.js" + +export const executeTrustQuery = async ({ + logger, + certificateCommonName, + certificateFileUrl, + certificateIsNew, + certificate, + verb, +}) => { + const windowsTrustInfo = await executeTrustQueryOnWindows({ + logger, + certificateCommonName, + certificateFileUrl, + certificateIsNew, + certificate, + verb, + }) + + const chromeTrustInfo = await executeTrustQueryOnChrome({ + logger, + windowsTrustInfo, + }) + + const edgeTrustInfo = await executeTrustQueryOnEdge({ + windowsTrustInfo, + }) + + const firefoxTrustInfo = await executeTrustQueryOnFirefox({ + logger, + certificateIsNew, + }) + + return { + windows: windowsTrustInfo, + chrome: chromeTrustInfo, + edge: edgeTrustInfo, + firefox: firefoxTrustInfo, + } +} diff --git a/packages/independent/https-local/src/internal/windows/windows_certutil.js b/packages/independent/https-local/src/internal/windows/windows_certutil.js new file mode 100644 index 0000000000..2d8ea8b634 --- /dev/null +++ b/packages/independent/https-local/src/internal/windows/windows_certutil.js @@ -0,0 +1,133 @@ +/* + * see https://github.com/davewasmer/devcert/blob/master/src/platforms/win32.ts + * https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/certutil + */ + +import { createDetailedMessage, UNICODE } from "@jsenv/humanize"; +import { fileURLToPath } from "node:url"; +import { exec } from "../exec.js"; +import { + VERB_ADD_TRUST, + VERB_CHECK_TRUST, + VERB_ENSURE_TRUST, + VERB_REMOVE_TRUST, +} from "../trust_query.js"; + +const REASON_NEW_AND_TRY_TO_TRUST_DISABLED = + "certificate is new and tryToTrust is disabled"; +const REASON_NOT_FOUND_IN_WINDOWS = "not found in windows store"; +const REASON_FOUND_IN_WINDOWS = "found in windows store"; +const REASON_ADD_COMMAND_FAILED = + "command to add certificate to windows store failed"; +const REASON_ADD_COMMAND_COMPLETED = + "command to add certificate to windows store completed"; +const REASON_DELETE_COMMAND_FAILED = + "command to remove certificate from windows store failed"; +const REASON_DELETE_COMMAND_COMPLETED = + "command to remove certificate from windows store completed"; + +export const executeTrustQueryOnWindows = async ({ + logger, + certificateCommonName, + certificateFileUrl, + certificateIsNew, + // certificate, + verb, +}) => { + if (verb === VERB_CHECK_TRUST && certificateIsNew) { + logger.info(`${UNICODE.INFO} You should add certificate to windows`); + return { + status: "not_trusted", + reason: REASON_NEW_AND_TRY_TO_TRUST_DISABLED, + }; + } + + logger.info(`Check if certificate is in windows...`); + // https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/certutil#-viewstore + // TODO: check if -viewstore works better than -store + const certutilListCommand = `certutil -store -user root`; + logger.debug(`${UNICODE.COMMAND} ${certutilListCommand}`); + const certutilListCommandOutput = await exec(certutilListCommand); + const certificateFilePath = fileURLToPath(certificateFileUrl); + + // it's not super accurate and do not take into account if the cert is different + // but it's the best I could do with certutil command on windows + const certificateInStore = certutilListCommandOutput.includes( + certificateCommonName, + ); + if (!certificateInStore) { + logger.info(`${UNICODE.INFO} certificate not found in windows`); + if (verb === VERB_CHECK_TRUST || verb === VERB_REMOVE_TRUST) { + return { + status: "not_trusted", + reason: REASON_NOT_FOUND_IN_WINDOWS, + }; + } + + // https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/certutil#-addstore + const certutilAddCommand = `certutil -addstore -user root "${certificateFilePath}"`; + logger.info(`Adding certificate to windows...`); + logger.info(`${UNICODE.COMMAND} ${certutilAddCommand}`); + try { + await exec(certutilAddCommand); + logger.info(`${UNICODE.OK} certificate added to windows`); + return { + status: "trusted", + reason: REASON_ADD_COMMAND_COMPLETED, + }; + } catch (e) { + logger.error( + createDetailedMessage( + `${UNICODE.FAILURE} Failed to add certificate to windows`, + { + "error stack": e.stack, + "certificate file": certificateFilePath, + }, + ), + ); + return { + status: "not_trusted", + reason: REASON_ADD_COMMAND_FAILED, + }; + } + } + + logger.info(`${UNICODE.OK} certificate found in windows`); + if ( + verb === VERB_CHECK_TRUST || + verb === VERB_ADD_TRUST || + verb === VERB_ENSURE_TRUST + ) { + return { + status: "trusted", + reason: REASON_FOUND_IN_WINDOWS, + }; + } + + // https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/certutil#-delstore + const certutilRemoveCommand = `certutil -delstore -user root "${certificateCommonName}"`; + logger.info(`Removing certificate from windows...`); + logger.info(`${UNICODE.COMMAND} ${certutilRemoveCommand}`); + try { + await exec(certutilRemoveCommand); + logger.info(`${UNICODE.OK} certificate removed from windows`); + return { + status: "not_trusted", + reason: REASON_DELETE_COMMAND_COMPLETED, + }; + } catch (e) { + logger.error( + createDetailedMessage( + `${UNICODE.FAILURE} failed to remove certificate from windows`, + { + "error stack": e.stack, + "certificate file": certificateFilePath, + }, + ), + ); + return { + status: "unknown", // maybe it was not trusted? + reason: REASON_DELETE_COMMAND_FAILED, + }; + } +}; diff --git a/packages/independent/https-local/src/jsenvParameters.js b/packages/independent/https-local/src/jsenvParameters.js new file mode 100644 index 0000000000..aec3ab8236 --- /dev/null +++ b/packages/independent/https-local/src/jsenvParameters.js @@ -0,0 +1,14 @@ +import { createValidityDurationOfXYears } from "./validity_duration.js" + +export const jsenvParameters = { + certificateCommonName: "https local root certificate", + certificateValidityDurationInMs: createValidityDurationOfXYears(20), +} + +// const jsenvCertificateParams = { +// rootCertificateOrganizationName: "jsenv", +// rootCertificateOrganizationalUnitName: "local-https-certificates", +// rootCertificateCountryName: "FR", +// rootCertificateStateOrProvinceName: "Alpes Maritimes", +// rootCertificateLocalityName: "Valbonne", +// } diff --git a/packages/independent/https-local/src/main.js b/packages/independent/https-local/src/main.js new file mode 100644 index 0000000000..20f793e1cf --- /dev/null +++ b/packages/independent/https-local/src/main.js @@ -0,0 +1,20 @@ +/* + * This file is the first file executed by code using the package + * Its responsability is to export what is documented + * Ideally this file should be kept simple to help discovering codebase progressively. + * + * see also + * - https://github.com/davewasmer/devcert + * - https://github.com/FiloSottile/mkcert + */ + +export { + installCertificateAuthority, + uninstallCertificateAuthority, +} from "./certificate_authority.js"; +export { requestCertificate } from "./certificate_request.js"; +export { verifyHostsFile } from "./hosts_file_verif.js"; +export { + createValidityDurationOfXDays, + createValidityDurationOfXYears, +} from "./validity_duration.js"; diff --git a/packages/independent/https-local/src/validity_duration.js b/packages/independent/https-local/src/validity_duration.js new file mode 100644 index 0000000000..4a4b26c364 --- /dev/null +++ b/packages/independent/https-local/src/validity_duration.js @@ -0,0 +1,38 @@ +const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000 +const MILLISECONDS_PER_YEAR = MILLISECONDS_PER_DAY * 365 + +export const verifyRootCertificateValidityDuration = (validityDurationInMs) => { + const durationInYears = validityDurationInMs / MILLISECONDS_PER_YEAR + if (durationInYears > 25) { + return { + ok: false, + maxAllowedValue: MILLISECONDS_PER_YEAR * 25, + message: `root certificate validity duration of ${durationInYears} years is too much, using the max recommended duration: 25 years`, + details: + "https://serverfault.com/questions/847190/in-theory-could-a-ca-make-a-certificate-that-is-valid-for-arbitrarily-long", + } + } + return { ok: true } +} + +export const verifyServerCertificateValidityDuration = ( + validityDurationInMs, +) => { + const validityDurationInDays = validityDurationInMs / MILLISECONDS_PER_DAY + if (validityDurationInDays > 397) { + return { + ok: false, + maxAllowedValue: MILLISECONDS_PER_DAY * 397, + message: `certificate validity duration of ${validityDurationInMs} days is too much, using the max recommended duration: 397 days`, + details: + "https://www.globalsign.com/en/blog/maximum-ssltls-certificate-validity-now-one-year", + } + } + return { ok: true } +} + +export const createValidityDurationOfXYears = (years) => + MILLISECONDS_PER_YEAR * years + 5000 + +export const createValidityDurationOfXDays = (days) => + MILLISECONDS_PER_DAY * days + 5000 diff --git a/packages/independent/https-local/tests/__internal__/certificate_generation/certificate_generation.test.mjs b/packages/independent/https-local/tests/__internal__/certificate_generation/certificate_generation.test.mjs new file mode 100644 index 0000000000..bda40b1348 --- /dev/null +++ b/packages/independent/https-local/tests/__internal__/certificate_generation/certificate_generation.test.mjs @@ -0,0 +1,108 @@ +import { assert } from "@jsenv/assert" + +import { forge } from "@jsenv/https-local/src/internal/forge.js" +import { + createAuthorityRootCertificate, + requestCertificateFromAuthority, +} from "@jsenv/https-local/src/internal/certificate_generator.js" +import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs" + +const { + rootCertificateForgeObject, + rootCertificatePublicKeyForgeObject, + rootCertificatePrivateKeyForgeObject, +} = await createAuthorityRootCertificate({ + logger: createLoggerForTest(), + commonName: "https://github.com/jsenv/server", + countryName: "FR", + stateOrProvinceName: "Alpes Maritimes", + localityName: "Valbonne", + organizationName: "jsenv", + organizationalUnitName: "jsenv server", + validityDurationInMs: 100000, + serialNumber: 0, +}) + +{ + const actual = { + rootCertificateForgeObject, + rootCertificatePublicKeyForgeObject, + rootCertificatePrivateKeyForgeObject, + } + const expected = { + rootCertificateForgeObject: assert.any(Object), + rootCertificatePublicKeyForgeObject: assert.any(Object), + rootCertificatePrivateKeyForgeObject: assert.any(Object), + } + assert({ actual, expected }) +} + +{ + const { pki } = forge + // const rootCertificate = pki.certificateToPem(rootCertificateForgeObject) + // const authorityCertificateForgeObject = pki.certificateFromPem(rootCertificate) + const rootCertificatePrivateKey = pki.privateKeyToPem( + rootCertificatePrivateKeyForgeObject, + ) + await new Promise((resolve) => { + setTimeout(resolve, 1000) + }) + const auhtorityCertificatePrivateKeyForgeObject = pki.privateKeyFromPem( + rootCertificatePrivateKey, + ) + const actual = auhtorityCertificatePrivateKeyForgeObject + const expected = auhtorityCertificatePrivateKeyForgeObject + assert({ actual, expected }) +} + +{ + const { + certificateForgeObject, + certificatePublicKeyForgeObject, + certificatePrivateKeyForgeObject, + } = requestCertificateFromAuthority({ + authorityCertificateForgeObject: rootCertificateForgeObject, + auhtorityCertificatePrivateKeyForgeObject: + rootCertificatePrivateKeyForgeObject, + serialNumber: 1, + altNames: ["localhost"], + validityDurationInMs: 10000, + }) + const actual = { + certificateForgeObject, + certificatePublicKeyForgeObject, + certificatePrivateKeyForgeObject, + } + const expected = { + certificateForgeObject: assert.any(Object), + certificatePublicKeyForgeObject: assert.any(Object), + certificatePrivateKeyForgeObject: assert.any(Object), + } + assert({ actual, expected }) + + // ici ça serais bien de tester des truc de forge, + // genre que le certificat issuer est bien l'authorité + // { + // const { pki } = forge + // const caStore = pki.createCaStore() + // caStore.addCertificate(rootCertificateForgeObject) + // caStore.addCertificate(certificateForgeObject) + // const actual = await new Promise((resolve) => { + // pki.verifyCertificateChain( + // caStore, + // [rootCertificateForgeObject, certificateForgeObject], + // ( + // vfd, + // // depth, + // // chain + // ) => { + // if (vfd === true) { + // resolve() // certificateForgeObject.verifySubjectKeyIdentifier()) + // } + // }, + // ) + // }) + // const expected = false + // assert({ actual, expected }) + // } +} diff --git a/packages/independent/https-local/tests/__internal__/hosts_parser/hosts_files/hosts b/packages/independent/https-local/tests/__internal__/hosts_parser/hosts_files/hosts new file mode 100644 index 0000000000..13a600c08c --- /dev/null +++ b/packages/independent/https-local/tests/__internal__/hosts_parser/hosts_files/hosts @@ -0,0 +1,13 @@ +## +# Header comment +# +# Header comment description +# on two lines +## +127.0.0.1 localhost loopback +255.255.255.255 broadcasthost +::1 localhost +# 127.0.0.1 old-tool.example.com +127.0.0.1 tool.example.com +127.0.0.1 jsenv +# Footer comment diff --git a/packages/independent/https-local/tests/__internal__/hosts_parser/hosts_files/hosts_after_adding_example b/packages/independent/https-local/tests/__internal__/hosts_parser/hosts_files/hosts_after_adding_example new file mode 100644 index 0000000000..ab70dcfd5d --- /dev/null +++ b/packages/independent/https-local/tests/__internal__/hosts_parser/hosts_files/hosts_after_adding_example @@ -0,0 +1,14 @@ +## +# Header comment +# +# Header comment description +# on two lines +## +127.0.0.1 localhost +255.255.255.255 broadcasthost +::1 localhost +# 127.0.0.1 old-tool.example.com +127.0.0.1 tool.example.com +127.0.0.1 jsenv +# Footer comment +127.0.0.1 example diff --git a/packages/independent/https-local/tests/__internal__/hosts_parser/hosts_files/hosts_after_removing_loopback b/packages/independent/https-local/tests/__internal__/hosts_parser/hosts_files/hosts_after_removing_loopback new file mode 100644 index 0000000000..3e32cca93a --- /dev/null +++ b/packages/independent/https-local/tests/__internal__/hosts_parser/hosts_files/hosts_after_removing_loopback @@ -0,0 +1,13 @@ +## +# Header comment +# +# Header comment description +# on two lines +## +127.0.0.1 localhost +255.255.255.255 broadcasthost +::1 localhost +# 127.0.0.1 old-tool.example.com +127.0.0.1 tool.example.com +127.0.0.1 jsenv +# Footer comment diff --git a/packages/independent/https-local/tests/__internal__/hosts_parser/hosts_parser.test.mjs b/packages/independent/https-local/tests/__internal__/hosts_parser/hosts_parser.test.mjs new file mode 100644 index 0000000000..df837a5b77 --- /dev/null +++ b/packages/independent/https-local/tests/__internal__/hosts_parser/hosts_parser.test.mjs @@ -0,0 +1,55 @@ +import { assert } from "@jsenv/assert" +import { readFile } from "@jsenv/filesystem" + +import { parseHosts } from "@jsenv/https-local/src/internal/hosts.js" + +const hostsAContent = await readFile( + new URL("./hosts_files/hosts", import.meta.url), + { as: "string" }, +) +const hostsA = parseHosts(hostsAContent) + +{ + const actual = hostsA.getAllIpHostnames() + const expected = { + "127.0.0.1": ["localhost", "loopback", "tool.example.com", "jsenv"], + "255.255.255.255": ["broadcasthost"], + "::1": ["localhost"], + } + assert({ actual, expected }) +} + +{ + const actual = hostsA.getIpHostnames("127.0.0.1") + const expected = ["localhost", "loopback", "tool.example.com", "jsenv"] + assert({ actual, expected }) +} + +// without touching anything output is the same +{ + const actual = hostsA.asFileContent() + const expected = hostsAContent + assert({ actual, expected }) +} + +// after removing loopback +{ + hostsA.removeIpHostname("127.0.0.1", "loopback") + const actual = hostsA.asFileContent() + const expected = await readFile( + new URL("./hosts_files/hosts_after_removing_loopback", import.meta.url), + { as: "string" }, + ) + assert({ actual, expected }) +} + +// after adding example +{ + hostsA.addIpHostname("127.0.0.1", "example") + const actual = hostsA.asFileContent() + const expected = await readFile( + new URL("./hosts_files/hosts_after_adding_example", import.meta.url), + { as: "string" }, + ) + assert({ actual, expected }) +} diff --git a/packages/independent/https-local/tests/authority_certificate/install_authority_first_call.test.mjs b/packages/independent/https-local/tests/authority_certificate/install_authority_first_call.test.mjs new file mode 100644 index 0000000000..8926b03ceb --- /dev/null +++ b/packages/independent/https-local/tests/authority_certificate/install_authority_first_call.test.mjs @@ -0,0 +1,127 @@ +import { assert } from "@jsenv/assert" +import { UNICODE } from "@jsenv/log" + +import { + installCertificateAuthority, + uninstallCertificateAuthority, +} from "@jsenv/https-local" +import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs" + +await uninstallCertificateAuthority({ + logLevel: "warn", +}) +const loggerForTest = createLoggerForTest({ + // logLevel: "info", + // forwardToConsole: true, +}) +const { + rootCertificateForgeObject, + rootCertificatePrivateKeyForgeObject, + rootCertificate, + rootCertificatePrivateKey, + rootCertificateFilePath, + trustInfo, +} = await installCertificateAuthority({ + logger: loggerForTest, +}) +const { infos, warns, errors } = loggerForTest.getLogs({ + info: true, + warn: true, + error: true, +}) + +const actual = { + // assert what is logged + infos, + warns, + errors, + // assert value returned + rootCertificateForgeObject, + rootCertificatePrivateKeyForgeObject, + rootCertificate, + rootCertificatePrivateKey, + rootCertificateFilePath, + trustInfo, +} +const expected = { + infos: [ + `${UNICODE.INFO} authority root certificate not found in filesystem`, + `Generating authority root certificate with a validity of 20 years...`, + `${UNICODE.OK} authority root certificate written at ${actual.rootCertificateFilePath}`, + ...{ + darwin: [ + `${UNICODE.INFO} You should add certificate to mac keychain`, + `${UNICODE.INFO} You should add certificate to firefox`, + ], + win32: [ + `${UNICODE.INFO} You should add certificate to windows`, + `${UNICODE.INFO} You should add certificate to firefox`, + ], + linux: [ + `${UNICODE.INFO} You should add certificate to linux`, + `${UNICODE.INFO} You should add certificate to chrome`, + `${UNICODE.INFO} You should add certificate to firefox`, + ], + }[process.platform], + ], + warns: [], + errors: [], + rootCertificateForgeObject: assert.any(Object), + rootCertificatePrivateKeyForgeObject: assert.any(Object), + rootCertificate: assert.any(String), + rootCertificatePrivateKey: assert.any(String), + rootCertificateFilePath: assert.any(String), + trustInfo: { + darwin: { + mac: { + status: "not_trusted", + reason: "certificate is new and tryToTrust is disabled", + }, + chrome: { + status: "not_trusted", + reason: "certificate is new and tryToTrust is disabled", + }, + firefox: { + status: "not_trusted", + reason: "certificate is new and tryToTrust is disabled", + }, + safari: { + status: "not_trusted", + reason: "certificate is new and tryToTrust is disabled", + }, + }, + win32: { + windows: { + status: "not_trusted", + reason: "certificate is new and tryToTrust is disabled", + }, + chrome: { + status: "not_trusted", + reason: "certificate is new and tryToTrust is disabled", + }, + edge: { + status: "not_trusted", + reason: "certificate is new and tryToTrust is disabled", + }, + firefox: { + status: "not_trusted", + reason: "certificate is new and tryToTrust is disabled", + }, + }, + linux: { + linux: { + status: "not_trusted", + reason: "certificate is new and tryToTrust is disabled", + }, + chrome: { + status: "not_trusted", + reason: "certificate is new and tryToTrust is disabled", + }, + firefox: { + status: "not_trusted", + reason: "certificate is new and tryToTrust is disabled", + }, + }, + }[process.platform], +} +assert({ actual, expected }) diff --git a/packages/independent/https-local/tests/authority_certificate/install_authority_reuse.test.mjs b/packages/independent/https-local/tests/authority_certificate/install_authority_reuse.test.mjs new file mode 100644 index 0000000000..45e64f8494 --- /dev/null +++ b/packages/independent/https-local/tests/authority_certificate/install_authority_reuse.test.mjs @@ -0,0 +1,137 @@ +import { assert } from "@jsenv/assert" +import { UNICODE } from "@jsenv/log" + +import { + installCertificateAuthority, + uninstallCertificateAuthority, +} from "@jsenv/https-local" +import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs" + +await uninstallCertificateAuthority({ + logLevel: "warn", +}) +const firstCallReturnValue = await installCertificateAuthority({ + logLevel: "warn", +}) +const loggerForTest = createLoggerForTest({ + // logLevel: "info", + // forwardToConsole: true, +}) +const secondCallReturnValue = await installCertificateAuthority({ + logger: loggerForTest, +}) +const secondCallLogs = loggerForTest.getLogs({ + info: true, + warn: true, + error: true, +}) +const sameCertificate = + firstCallReturnValue.rootCertificate === secondCallReturnValue.rootCertificate + +const actual = { + sameCertificate, + secondCallReturnValue, + secondCallLogs, +} +const expected = { + sameCertificate: true, + secondCallReturnValue: { + rootCertificateForgeObject: assert.any(Object), + rootCertificatePrivateKeyForgeObject: assert.any(Object), + rootCertificate: assert.any(String), + rootCertificatePrivateKey: assert.any(String), + rootCertificateFilePath: assert.any(String), + trustInfo: { + darwin: { + mac: { + status: "not_trusted", + reason: "certificate not found in mac keychain", + }, + chrome: { + status: "not_trusted", + reason: "certificate not found in mac keychain", + }, + firefox: { + status: "unknown", + reason: `"nss" is not installed`, + }, + safari: { + status: "not_trusted", + reason: "certificate not found in mac keychain", + }, + }, + win32: { + windows: { + status: "not_trusted", + reason: "not found in windows store", + }, + chrome: { + status: "not_trusted", + reason: "not found in windows store", + }, + edge: { + status: "not_trusted", + reason: "not found in windows store", + }, + firefox: { + status: "unknown", + reason: "not implemented on windows", + }, + }, + linux: { + linux: { + status: "not_trusted", + reason: "not found in linux store", + }, + chrome: { + status: "unknown", + reason: `"libnss3-tools" is not installed`, + }, + firefox: { + status: "unknown", + reason: `"libnss3-tools" is not installed`, + }, + }, + }[process.platform], + }, + secondCallLogs: { + infos: [ + `${UNICODE.OK} authority root certificate found in filesystem`, + `Checking certificate validity...`, + `${UNICODE.OK} certificate still valid for 20 years`, + `Detect if certificate attributes have changed...`, + `${UNICODE.OK} certificate attributes are the same`, + ...{ + darwin: [ + "Check if certificate is in mac keychain...", + `${UNICODE.INFO} certificate not found in mac keychain`, + "Check if certificate is in firefox...", + `${UNICODE.FAILURE} cannot check if certificate is in firefox +--- reason --- +"nss" is not installed`, + ], + win32: [ + "Check if certificate is in windows...", + `${UNICODE.INFO} certificate not found in windows`, + "Check if certificate is in firefox...", + `${UNICODE.INFO} cannot check if certificate is in firefox (not implemented on windows)`, + ], + linux: [ + "Check if certificate is in linux...", + `${UNICODE.INFO} certificate not in linux`, + "Check if certificate is in chrome...", + `${UNICODE.FAILURE} cannot check if certificate is in chrome +--- reason --- +"libnss3-tools" is not installed`, + "Check if certificate is in firefox...", + `${UNICODE.FAILURE} cannot check if certificate is in firefox +--- reason --- +"libnss3-tools" is not installed`, + ], + }[process.platform], + ], + warns: [], + errors: [], + }, +} +assert({ actual, expected }) diff --git a/packages/independent/https-local/tests/authority_certificate/root_cert_about_to_expire.test.mjs b/packages/independent/https-local/tests/authority_certificate/root_cert_about_to_expire.test.mjs new file mode 100644 index 0000000000..5300c21051 --- /dev/null +++ b/packages/independent/https-local/tests/authority_certificate/root_cert_about_to_expire.test.mjs @@ -0,0 +1,63 @@ +import { assert } from "@jsenv/assert" +import { UNICODE } from "@jsenv/log" + +import { + installCertificateAuthority, + uninstallCertificateAuthority, +} from "@jsenv/https-local" +import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs" + +await uninstallCertificateAuthority({ + logLevel: "warn", +}) +await installCertificateAuthority({ + logLevel: "warn", + certificateValidityDurationInMs: 6000, +}) +await new Promise((resolve) => { + setTimeout(resolve, 1500) +}) +const loggerForSecondCall = createLoggerForTest({ + // forwardToConsole: true, +}) +const { rootCertificateFilePath } = await installCertificateAuthority({ + logger: loggerForSecondCall, + certificateValidityDurationInMs: 6000, + aboutToExpireRatio: 0.95, +}) + +{ + const { infos, warns, errors } = loggerForSecondCall.getLogs({ + info: true, + warn: true, + error: true, + }) + const actual = { infos, warns, errors } + const expected = { + infos: [ + `${UNICODE.OK} authority root certificate found in filesystem`, + `Checking certificate validity...`, + assert.matchesRegExp(/certificate will expire in \d seconds/), + `Generating authority root certificate with a validity of 6 seconds...`, + `${UNICODE.OK} authority root certificate written at ${rootCertificateFilePath}`, + ...{ + darwin: [ + `${UNICODE.INFO} You should add certificate to mac keychain`, + `${UNICODE.INFO} You should add certificate to firefox`, + ], + win32: [ + `${UNICODE.INFO} You should add certificate to windows`, + `${UNICODE.INFO} You should add certificate to firefox`, + ], + linux: [ + `${UNICODE.INFO} You should add certificate to linux`, + `${UNICODE.INFO} You should add certificate to chrome`, + `${UNICODE.INFO} You should add certificate to firefox`, + ], + }[process.platform], + ], + warns: [], + errors: [], + } + assert({ actual, expected }) +} diff --git a/packages/independent/https-local/tests/authority_certificate/root_cert_expired.test.mjs b/packages/independent/https-local/tests/authority_certificate/root_cert_expired.test.mjs new file mode 100644 index 0000000000..3df946265b --- /dev/null +++ b/packages/independent/https-local/tests/authority_certificate/root_cert_expired.test.mjs @@ -0,0 +1,62 @@ +import { assert } from "@jsenv/assert" +import { UNICODE } from "@jsenv/log" + +import { + installCertificateAuthority, + uninstallCertificateAuthority, +} from "@jsenv/https-local" +import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs" + +await uninstallCertificateAuthority({ + logLevel: "warn", +}) +await installCertificateAuthority({ + logLevel: "warn", + certificateValidityDurationInMs: 1000, +}) +await new Promise((resolve) => { + setTimeout(resolve, 2500) +}) +const loggerForSecondCall = createLoggerForTest({ + // forwardToConsole: true, +}) +const { rootCertificateFilePath } = await installCertificateAuthority({ + logger: loggerForSecondCall, + certificateValidityDurationInMs: 1000, +}) + +{ + const { infos, warns, errors } = loggerForSecondCall.getLogs({ + info: true, + warn: true, + error: true, + }) + const actual = { infos, warns, errors } + const expected = { + infos: [ + `${UNICODE.OK} authority root certificate found in filesystem`, + `Checking certificate validity...`, + assert.matchesRegExp(/certificate expired \d seconds ago/), + `Generating authority root certificate with a validity of 1 second...`, + `${UNICODE.OK} authority root certificate written at ${rootCertificateFilePath}`, + ...{ + darwin: [ + `${UNICODE.INFO} You should add certificate to mac keychain`, + `${UNICODE.INFO} You should add certificate to firefox`, + ], + win32: [ + `${UNICODE.INFO} You should add certificate to windows`, + `${UNICODE.INFO} You should add certificate to firefox`, + ], + linux: [ + `${UNICODE.INFO} You should add certificate to linux`, + `${UNICODE.INFO} You should add certificate to chrome`, + `${UNICODE.INFO} You should add certificate to firefox`, + ], + }[process.platform], + ], + warns: [], + errors: [], + } + assert({ actual, expected }) +} diff --git a/packages/independent/https-local/tests/authority_certificate/try_to_trust.test_manual.mjs b/packages/independent/https-local/tests/authority_certificate/try_to_trust.test_manual.mjs new file mode 100644 index 0000000000..b2de1e6b98 --- /dev/null +++ b/packages/independent/https-local/tests/authority_certificate/try_to_trust.test_manual.mjs @@ -0,0 +1,40 @@ +// https://github.com/nccgroup/wssip/blob/56d0d2c15a7c0fd4c99be445ec8d6c16571e81a0/lib/mitmengine.js#L450 + +import { writeSymbolicLink } from "@jsenv/filesystem" +import { + installCertificateAuthority, + uninstallCertificateAuthority, + requestCertificate, +} from "@jsenv/https-local" +import { startServerForTest } from "@jsenv/https-local/tests/test_helpers.mjs" + +await uninstallCertificateAuthority({ + tryToUntrust: true, +}) +await installCertificateAuthority({ + tryToTrust: true, +}) +const { certificate, privateKey, rootCertificateFilePath } = requestCertificate( + { + altNames: ["localhost", "*.localhost"], + }, +) + +if (process.platform !== "win32") { + // not on windows because symlink requires admin rights + await writeSymbolicLink({ + from: new URL("./jsenv_root_certificate.pem", import.meta.url), + to: rootCertificateFilePath, + type: "file", + allowUseless: true, + allowOverwrite: true, + }) +} + +const serverOrigin = await startServerForTest({ + port: 4456, + certificate, + privateKey, + keepAlive: true, +}) +console.log(`Open ${serverOrigin} in a browser`) diff --git a/packages/independent/https-local/tests/hosts_file/try_to_update_hosts.test.mjs b/packages/independent/https-local/tests/hosts_file/try_to_update_hosts.test.mjs new file mode 100644 index 0000000000..15b4a1bc35 --- /dev/null +++ b/packages/independent/https-local/tests/hosts_file/try_to_update_hosts.test.mjs @@ -0,0 +1,116 @@ +import { fileURLToPath } from "node:url" +import { assert } from "@jsenv/assert" +import { readFile, writeFile, removeEntry } from "@jsenv/filesystem" +import { UNICODE } from "@jsenv/log" + +import { verifyHostsFile } from "@jsenv/https-local" +import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs" + +const hostFileUrl = new URL("./hosts", import.meta.url) +const hostsFilePath = fileURLToPath(hostFileUrl) + +// 1 ip mapping missing +{ + await writeFile(hostFileUrl, `127.0.0.1 localhost`) + const loggerForTest = createLoggerForTest({ + // forwardToConsole: true, + }) + await verifyHostsFile({ + logger: loggerForTest, + ipMappings: { + "127.0.0.1": ["localhost", "jsenv"], + }, + tryToUpdateHostsFile: true, + hostsFilePath, + }) + + const { infos, warns, errors } = loggerForTest.getLogs({ + info: true, + warn: true, + error: true, + }) + const hostsFileContent = await readFile(hostsFilePath, { as: "string" }) + const actual = { + hostsFileContent, + infos, + warns, + errors, + } + const expected = { + hostsFileContent: + process.platform === "win32" + ? `127.0.0.1 localhost\r\n127.0.0.1 jsenv\r\n` + : `127.0.0.1 localhost\n127.0.0.1 jsenv\n`, + infos: [ + `Check hosts file content...`, + `${UNICODE.INFO} 1 mapping is missing in hosts file`, + `Append "127.0.0.1 jsenv" in host file...`, + process.platform === "win32" + ? `${UNICODE.COMMAND} (echo.& echo 127.0.0.1 jsenv) >> ${hostsFilePath}` + : `${UNICODE.COMMAND} echo "\n127.0.0.1 jsenv" | tee -a ${hostsFilePath}`, + `${UNICODE.OK} mapping added`, + ], + warns: [], + errors: [], + } + assert({ actual, expected }) +} + +// 2 ip mapping missing +{ + await writeFile(hostFileUrl, ``) + await verifyHostsFile({ + logLevel: "warn", + ipMappings: { + "127.0.0.1": ["localhost", "jsenv"], + "192.168.1.1": ["toto"], + }, + tryToUpdateHostsFile: true, + hostsFilePath, + }) + const hostsFileContent = await readFile(hostsFilePath, { as: "string" }) + const actual = hostsFileContent + const expected = + process.platform === "win32" + ? `127.0.0.1 localhost jsenv\r\n192.168.1.1 toto\r\n` + : `127.0.0.1 localhost jsenv\n192.168.1.1 toto\n` + assert({ actual, expected }) +} + +// all hostname there +{ + const loggerForTest = createLoggerForTest({ + // forwardToConsole: true, + }) + await writeFile(hostFileUrl, `127.0.0.1 jsenv`) + await verifyHostsFile({ + logger: loggerForTest, + ipMappings: { + "127.0.0.1": ["jsenv"], + }, + tryToUpdateHostsFile: true, + hostsFilePath, + }) + + const { infos, warns, errors } = loggerForTest.getLogs({ + info: true, + warn: true, + error: true, + }) + const actual = { + infos, + warns, + errors, + } + const expected = { + infos: [ + `Check hosts file content...`, + `${UNICODE.OK} all ip mappings found in hosts file`, + ], + warns: [], + errors: [], + } + assert({ actual, expected }) +} + +await removeEntry(hostFileUrl) diff --git a/packages/independent/https-local/tests/server_certificate/not_trusted_browsers.test.mjs b/packages/independent/https-local/tests/server_certificate/not_trusted_browsers.test.mjs new file mode 100644 index 0000000000..779d947627 --- /dev/null +++ b/packages/independent/https-local/tests/server_certificate/not_trusted_browsers.test.mjs @@ -0,0 +1,88 @@ +import { assert } from "@jsenv/assert" + +import { + installCertificateAuthority, + uninstallCertificateAuthority, + requestCertificate, +} from "@jsenv/https-local" +import { + startServerForTest, + launchChromium, + launchFirefox, + launchWebkit, + requestServerUsingBrowser, +} from "@jsenv/https-local/tests/test_helpers.mjs" + +await uninstallCertificateAuthority({ + logLevel: "warn", +}) +await installCertificateAuthority({ + logLevel: "warn", +}) +const { certificate, privateKey } = requestCertificate({ + logLevel: "warn", +}) + +const serverOrigin = await startServerForTest({ + certificate, + privateKey, +}) + +{ + const browser = await launchChromium() + try { + await requestServerUsingBrowser({ + serverOrigin, + browser, + }) + throw new Error("should throw") + } catch (e) { + const actual = e.errorText + const expected = "net::ERR_CERT_AUTHORITY_INVALID" + assert({ actual, expected }) + } finally { + browser.close() + } +} + +// disabled on windows for now +// there is a little something to change in the expected error to make it pass +if (process.platform !== "win32") { + const browser = await launchFirefox() + try { + await requestServerUsingBrowser({ + serverOrigin, + browser, + }) + throw new Error("should throw") + } catch (e) { + const actual = e.errorText + const expected = "SEC_ERROR_UNKNOWN_ISSUER" + assert({ actual, expected, context: { browser: "firefox", error: e } }) + } finally { + browser.close() + } +} + +// if (process.platform === "darwin") { +{ + const browser = await launchWebkit() + try { + await requestServerUsingBrowser({ + serverOrigin, + browser, + }) + throw new Error("should throw") + } catch (e) { + const actual = e.errorText + const expected = { + win32: "SSL peer certificate or SSH remote key was not OK", + linux: "Unacceptable TLS certificate", + darwin: + "The certificate for this server is invalid. You might be connecting to a server that is pretending to be “localhost” which could put your confidential information at risk.", + }[process.platform] + assert({ actual, expected }) + } finally { + browser.close() + } +} diff --git a/packages/independent/https-local/tests/server_certificate/not_trusted_browsers.test_manual.mjs b/packages/independent/https-local/tests/server_certificate/not_trusted_browsers.test_manual.mjs new file mode 100644 index 0000000000..941724307c --- /dev/null +++ b/packages/independent/https-local/tests/server_certificate/not_trusted_browsers.test_manual.mjs @@ -0,0 +1,23 @@ +import { requestCertificate } from "@jsenv/https-local" +import { + // resetAllCertificateFiles, + startServerForTest, +} from "@jsenv/https-local/tests/test_helpers.mjs" + +// await resetAllCertificateFiles() +const { certificate, privateKey } = requestCertificate({ + logLevel: "debug", + serverCertificateFileUrl: new URL( + "./certificate/server.crt", + import.meta.url, + ), + rootCertificateOrganizationName: "jsenv", + rootCertificateOrganizationalUnitName: "https localhost", +}) +const serverOrigin = await startServerForTest({ + certificate, + privateKey, + keepAlive: true, + port: 5000, +}) +console.log(`Open ${serverOrigin} in a browser`) diff --git a/packages/independent/https-local/tests/server_certificate/request_server_certificate.test.mjs b/packages/independent/https-local/tests/server_certificate/request_server_certificate.test.mjs new file mode 100644 index 0000000000..95b9e8c97a --- /dev/null +++ b/packages/independent/https-local/tests/server_certificate/request_server_certificate.test.mjs @@ -0,0 +1,57 @@ +import { assert } from "@jsenv/assert" +import { UNICODE } from "@jsenv/log" + +import { + installCertificateAuthority, + uninstallCertificateAuthority, + requestCertificate, +} from "@jsenv/https-local" +import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs" + +const loggerDuringTest = createLoggerForTest({ + // forwardToConsole: true, +}) + +await uninstallCertificateAuthority({ + logLevel: "warn", +}) +await installCertificateAuthority({ + logLevel: "warn", +}) +const returnValue = await requestCertificate({ + // logLevel: "warn", + logger: loggerDuringTest, +}) + +{ + const { debugs, infos, warns, errors } = loggerDuringTest.getLogs({ + debug: true, + info: true, + warn: true, + error: true, + }) + const actual = { + debugs, + infos, + warns, + errors, + returnValue, + } + const expected = { + debugs: [ + `Restoring certificate authority from filesystem...`, + `${UNICODE.OK} certificate authority restored from filesystem`, + "Generating server certificate...", + `${UNICODE.OK} server certificate generated, it will be valid for 1 year`, + ], + infos: [], + warns: [], + errors: [], + returnValue: { + certificate: assert.any(String), + privateKey: assert.any(String), + rootCertificateFilePath: assert.any(String), + }, + } + assert({ actual, expected }) +} diff --git a/packages/independent/https-local/tests/test_helpers.mjs b/packages/independent/https-local/tests/test_helpers.mjs new file mode 100644 index 0000000000..cbb156ec26 --- /dev/null +++ b/packages/independent/https-local/tests/test_helpers.mjs @@ -0,0 +1,142 @@ +import { createServer } from "node:https" +import { createRequire } from "node:module" + +const require = createRequire(import.meta.url) + +/* + * Logs are an important part of this package + * For this reason tests msut be capable to ensure which logs are displayed + * and their content. This file provide a logger capable to do that. + */ +export const createLoggerForTest = ({ forwardToConsole = false } = {}) => { + const debugs = [] + const infos = [] + const warns = [] + const errors = [] + + return { + debug: (...args) => { + debugs.push(args.join("")) + if (forwardToConsole) { + console.debug(...args) + } + }, + info: (...args) => { + infos.push(args.join("")) + if (forwardToConsole) { + console.info(...args) + } + }, + warn: (...args) => { + warns.push(args.join("")) + if (forwardToConsole) { + console.warn(...args) + } + }, + error: (...args) => { + errors.push(args.join("")) + if (forwardToConsole) { + console.error(...args) + } + }, + + getLogs: ( + { debug, info, warn, error } = { + debug: true, + info: true, + warn: true, + error: true, + }, + ) => { + return { + ...(debug ? { debugs } : {}), + ...(info ? { infos } : {}), + ...(warn ? { warns } : {}), + ...(error ? { errors } : {}), + } + }, + } +} + +export const startServerForTest = async ({ + certificate, + privateKey, + keepAlive = false, + port = 0, +}) => { + const server = createServer( + { + cert: certificate, + key: privateKey, + }, + (request, response) => { + const body = "Hello world" + response.writeHead(200, { + "content-type": "text/plain", + "content-length": Buffer.byteLength(body), + }) + response.write(body) + response.end() + }, + ) + if (!keepAlive) { + server.unref() + } + const serverPort = await new Promise((resolve) => { + server.on("listening", () => { + // in case port is 0 (randomly assign an available port) + // https://nodejs.org/api/net.html#net_server_listen_port_host_backlog_callback + resolve(server.address().port) + }) + server.listen(port) + }) + return `https://localhost:${serverPort}` +} + +export const launchChromium = () => { + const { chromium } = require("playwright") + return chromium.launch() +} + +export const launchFirefox = () => { + const { firefox } = require("playwright") + return firefox.launch() +} + +export const launchWebkit = () => { + const { webkit } = require("playwright") + return webkit.launch() +} + +export const requestServerUsingBrowser = async ({ serverOrigin, browser }) => { + const page = await browser.newPage() + + return new Promise(async (resolve, reject) => { + page.on("requestfailed", (request) => { + reject(request.failure()) + }) + + page.on("load", () => { + setTimeout(resolve, 400) // this time is required for firefox to trigger "requestfailed" + }) + + page.goto(serverOrigin).catch((e) => { + // chrome + if ( + e.message.includes("ERR_CERT_INVALID") || + e.message.includes("ERR_CERT_AUTHORITY_INVALID") + ) { + return + } + // firefox + if (e.message.includes("SEC_ERROR_UNKNOWN_ISSUER")) { + return + } + // webkit + if (e.message.includes("The certificate for this server is invalid.")) { + return + } + throw e + }) + }) +} From 956400df672c0d41a6a8c615e9dc715e7f1a6f8c Mon Sep 17 00:00:00 2001 From: dmail Date: Wed, 14 Aug 2024 10:11:47 +0200 Subject: [PATCH 2/7] prettier --- .../src/import_one_export_from_file.js | 2 +- packages/independent/https-local/README.md | 48 ++++----- .../demo/install_certificate_authority.mjs | 4 +- ...stall_certificate_authority_auto_trust.mjs | 4 +- .../scripts/certificate/install_ca.mjs | 4 +- .../scripts/certificate/start_node_server.mjs | 20 ++-- .../uninstall_certificate_authority.mjs | 4 +- .../scripts/hosts/add_localhost_mappings.mjs | 16 +-- .../hosts/ensure_localhost_mappings.mjs | 4 +- .../hosts/remove_localhost_mappings.mjs | 16 +-- .../hosts/verify_localhost_mappings.mjs | 4 +- .../performance/measure_package_import.mjs | 10 +- .../performance/measure_package_tarball.mjs | 14 +-- .../scripts/performance/performance.mjs | 6 +- .../src/internal/browser_detection.js | 8 +- .../certificate_authority_file_urls.js | 46 ++++---- .../internal/certificate_data_converter.js | 76 ++++++------- .../src/internal/certificate_generator.js | 78 +++++++------- .../https-local/src/internal/command.js | 12 +-- .../https-local/src/internal/exec.js | 20 ++-- .../https-local/src/internal/forge.js | 6 +- .../https-local/src/internal/hosts.js | 10 +- .../src/internal/hosts/hosts_utils.js | 4 +- .../https-local/src/internal/linux/linux.js | 16 +-- .../https-local/src/internal/mac/mac.js | 20 ++-- .../https-local/src/internal/mac/safari.js | 4 +- .../https-local/src/internal/memoize.js | 28 ++--- .../https-local/src/internal/platform.js | 12 +-- .../search_certificate_in_command_output.js | 8 +- .../https-local/src/internal/trust_query.js | 8 +- .../src/internal/validity_formatting.js | 64 +++++------ .../https-local/src/internal/windows/edge.js | 4 +- .../src/internal/windows/windows.js | 20 ++-- .../https-local/src/jsenvParameters.js | 4 +- .../https-local/src/validity_duration.js | 24 ++--- .../certificate_generation.test.mjs | 40 +++---- .../hosts_parser/hosts_parser.test.mjs | 44 ++++---- .../install_authority_first_call.test.mjs | 22 ++-- .../install_authority_reuse.test.mjs | 27 ++--- .../root_cert_about_to_expire.test.mjs | 28 ++--- .../root_cert_expired.test.mjs | 28 ++--- .../try_to_trust.test_manual.mjs | 20 ++-- .../hosts_file/try_to_update_hosts.test.mjs | 60 +++++------ .../not_trusted_browsers.test.mjs | 60 +++++------ .../not_trusted_browsers.test_manual.mjs | 10 +- .../request_server_certificate.test.mjs | 26 ++--- .../https-local/tests/test_helpers.mjs | 102 +++++++++--------- .../file-size-impact/bin/filesize.mjs | 2 +- .../src/gzip_transformation.js | 2 +- .../src/internal/apply_tracking_config.js | 2 +- .../tests/comment/comment.test.mjs | 2 +- .../tests/comment/comment.test.mjs | 2 +- .../internal/collect_workspace_packages.js | 4 +- .../independent/workflow/monorepo/src/main.js | 4 +- .../workflow/monorepo/src/publish_packages.js | 2 +- .../src/internal/read_project_package.js | 2 +- .../src/internal/format_metric_value.js | 2 +- .../comment_snapshot.test.mjs | 2 +- 58 files changed, 561 insertions(+), 560 deletions(-) diff --git a/packages/independent/dynamic-import-worker/src/import_one_export_from_file.js b/packages/independent/dynamic-import-worker/src/import_one_export_from_file.js index 6cca1d3ffc..fcf2eb47a4 100644 --- a/packages/independent/dynamic-import-worker/src/import_one_export_from_file.js +++ b/packages/independent/dynamic-import-worker/src/import_one_export_from_file.js @@ -1,6 +1,6 @@ -import { Worker } from "node:worker_threads"; import { existsSync } from "node:fs"; import { fileURLToPath } from "node:url"; +import { Worker } from "node:worker_threads"; // we use a worker to bypass node cache on dynamic import const WORKER_COLLECTING_ONE_EXPORT_FILE_URL = new URL( diff --git a/packages/independent/https-local/README.md b/packages/independent/https-local/README.md index 3c3d2a7b8a..abb5b0e428 100644 --- a/packages/independent/https-local/README.md +++ b/packages/independent/https-local/README.md @@ -29,18 +29,18 @@ npm install --save-dev @jsenv/https-local import { installCertificateAuthority, verifyHostsFile, -} from "@jsenv/https-local" +} from "@jsenv/https-local"; await installCertificateAuthority({ tryToTrust: true, NSSDynamicInstall: true, -}) +}); await verifyHostsFile({ ipMappings: { "127.0.0.1": ["localhost"], }, tryToUpdatesHostsFile: true, -}) +}); ``` 3 - Run with node @@ -65,10 +65,10 @@ node ./install_certificate_authority.mjs * Read more in https://github.com/jsenv/https-local#requestCertificate */ -import { createServer } from "node:https" -import { requestCertificate } from "@jsenv/https-local" +import { createServer } from "node:https"; +import { requestCertificate } from "@jsenv/https-local"; -const { certificate, privateKey } = requestCertificate() +const { certificate, privateKey } = requestCertificate(); const server = createServer( { @@ -76,17 +76,17 @@ const server = createServer( key: privateKey, }, (request, response) => { - const body = "Hello world" + const body = "Hello world"; response.writeHead(200, { "content-type": "text/plain", "content-length": Buffer.byteLength(body), - }) - response.write(body) - response.end() + }); + response.write(body); + response.end(); }, -) -server.listen(8080) -console.log(`Server listening at https://local.example:8080`) +); +server.listen(8080); +console.log(`Server listening at https://local.example:8080`); ``` 5 - Start server with node @@ -117,9 +117,9 @@ _installCertificateAuthority_ function generates a certificate authority valid f This certificate authority is needed to generate local certificates that will be trusted by the operating system and web browsers. ```js -import { installCertificateAuthority } from "@jsenv/https-local" +import { installCertificateAuthority } from "@jsenv/https-local"; -await installCertificateAuthority() +await installCertificateAuthority(); ``` By default, trusting authority root certificate is a manual process. This manual process is documented in [BenMorel/dev-certificates#Import the CA in your browser](https://github.com/BenMorel/dev-certificates/tree/c10cd68945da772f31815b7a36721ddf848ff3a3#import-the-ca-in-your-browser). This process can be done programmatically as explained in [Auto trust](#Auto-trust). @@ -227,11 +227,11 @@ Check if certificate is trusted by firefox... It's possible to trust root certificate programmatically using _tryToTrust_ ```js -import { installCertificateAuthority } from "@jsenv/https-local" +import { installCertificateAuthority } from "@jsenv/https-local"; await installCertificateAuthority({ tryToTrust: true, -}) +}); ```
@@ -360,12 +360,12 @@ Check if certificate is trusted by firefox... _requestCertificate_ function returns a certificate and private key that can be used to start a server in HTTPS. ```js -import { createServer } from "node:https" -import { requestCertificate } from "@jsenv/https-local" +import { createServer } from "node:https"; +import { requestCertificate } from "@jsenv/https-local"; const { certificate, privateKey } = requestCertificate({ altNames: ["localhost", "local.example"], -}) +}); ``` [installCertificateAuthority](#installCertificateAuthority) must be called before this function. @@ -376,13 +376,13 @@ This function is not mandatory to obtain the https certificates. But it is useful to programmatically verify ip mappings that are important for your local server are present in hosts file. ```js -import { verifyHostsFile } from "@jsenv/https-local" +import { verifyHostsFile } from "@jsenv/https-local"; await verifyHostsFile({ ipMappings: { "127.0.0.1": ["localhost", "local.example"], }, -}) +}); ``` Find below logs written in terminal when this function is executed. @@ -424,14 +424,14 @@ C:\\Windows\\System32\\Drivers\\etc\\hosts It's possible to update hosts file programmatically using _tryToUpdateHostsFile_. ```js -import { verifyHostsFile } from "@jsenv/https-local" +import { verifyHostsFile } from "@jsenv/https-local"; await verifyHostsFile({ ipMappings: { "127.0.0.1": ["localhost", "local.example"], }, tryToUpdateHostsFile: true, -}) +}); ```
diff --git a/packages/independent/https-local/docs/demo/install_certificate_authority.mjs b/packages/independent/https-local/docs/demo/install_certificate_authority.mjs index 55029b9e8d..4731cf1d75 100644 --- a/packages/independent/https-local/docs/demo/install_certificate_authority.mjs +++ b/packages/independent/https-local/docs/demo/install_certificate_authority.mjs @@ -1,3 +1,3 @@ -import { installCertificateAuthority } from "@jsenv/https-local" +import { installCertificateAuthority } from "@jsenv/https-local"; -await installCertificateAuthority() +await installCertificateAuthority(); diff --git a/packages/independent/https-local/docs/demo/install_certificate_authority_auto_trust.mjs b/packages/independent/https-local/docs/demo/install_certificate_authority_auto_trust.mjs index a10fbfb81a..eb8847e92c 100644 --- a/packages/independent/https-local/docs/demo/install_certificate_authority_auto_trust.mjs +++ b/packages/independent/https-local/docs/demo/install_certificate_authority_auto_trust.mjs @@ -1,5 +1,5 @@ -import { installCertificateAuthority } from "@jsenv/https-local" +import { installCertificateAuthority } from "@jsenv/https-local"; await installCertificateAuthority({ tryToTrust: true, -}) +}); diff --git a/packages/independent/https-local/scripts/certificate/install_ca.mjs b/packages/independent/https-local/scripts/certificate/install_ca.mjs index dcfe8d7ce2..12eb490a39 100644 --- a/packages/independent/https-local/scripts/certificate/install_ca.mjs +++ b/packages/independent/https-local/scripts/certificate/install_ca.mjs @@ -1,7 +1,7 @@ -import { installCertificateAuthority } from "@jsenv/https-local" +import { installCertificateAuthority } from "@jsenv/https-local"; await installCertificateAuthority({ logLevel: "debug", tryToTrust: false, NSSDynamicInstall: true, -}) +}); diff --git a/packages/independent/https-local/scripts/certificate/start_node_server.mjs b/packages/independent/https-local/scripts/certificate/start_node_server.mjs index 7a901f836b..6e21b54514 100644 --- a/packages/independent/https-local/scripts/certificate/start_node_server.mjs +++ b/packages/independent/https-local/scripts/certificate/start_node_server.mjs @@ -1,9 +1,9 @@ -import { createServer } from "node:https" -import { requestCertificate } from "@jsenv/https-local" +import { requestCertificate } from "@jsenv/https-local"; +import { createServer } from "node:https"; const { certificate, privateKey } = requestCertificate({ altNames: ["localhost", "local.example"], -}) +}); const server = createServer( { @@ -11,14 +11,14 @@ const server = createServer( key: privateKey, }, (request, response) => { - const body = "Hello world" + const body = "Hello world"; response.writeHead(200, { "content-type": "text/plain", "content-length": Buffer.byteLength(body), - }) - response.write(body) - response.end() + }); + response.write(body); + response.end(); }, -) -server.listen(8080) -console.log(`Server listening at https://local.example:8080`) +); +server.listen(8080); +console.log(`Server listening at https://local.example:8080`); diff --git a/packages/independent/https-local/scripts/certificate/uninstall_certificate_authority.mjs b/packages/independent/https-local/scripts/certificate/uninstall_certificate_authority.mjs index 9c493f1868..a7c5cc076e 100644 --- a/packages/independent/https-local/scripts/certificate/uninstall_certificate_authority.mjs +++ b/packages/independent/https-local/scripts/certificate/uninstall_certificate_authority.mjs @@ -1,5 +1,5 @@ -import { uninstallCertificateAuthority } from "@jsenv/https-local" +import { uninstallCertificateAuthority } from "@jsenv/https-local"; await uninstallCertificateAuthority({ logLevel: "debug", -}) +}); diff --git a/packages/independent/https-local/scripts/hosts/add_localhost_mappings.mjs b/packages/independent/https-local/scripts/hosts/add_localhost_mappings.mjs index 274dac81bf..266722716d 100644 --- a/packages/independent/https-local/scripts/hosts/add_localhost_mappings.mjs +++ b/packages/independent/https-local/scripts/hosts/add_localhost_mappings.mjs @@ -1,16 +1,16 @@ import { - readHostsFile, parseHosts, + readHostsFile, writeHostsFile, -} from "@jsenv/https-local/src/internal/hosts.js" +} from "@jsenv/https-local/src/internal/hosts.js"; -const hostsFileContent = await readHostsFile() -const hostnames = parseHosts(hostsFileContent) -const localIpHostnames = hostnames.getIpHostnames("127.0.0.1") +const hostsFileContent = await readHostsFile(); +const hostnames = parseHosts(hostsFileContent); +const localIpHostnames = hostnames.getIpHostnames("127.0.0.1"); if (!localIpHostnames.includes("localhost")) { - hostnames.addIpHostname("127.0.0.1", "localhost") + hostnames.addIpHostname("127.0.0.1", "localhost"); } if (!localIpHostnames.includes("local.example")) { - hostnames.addIpHostname("127.0.0.1", "local.example") + hostnames.addIpHostname("127.0.0.1", "local.example"); } -await writeHostsFile(hostnames.asFileContent()) +await writeHostsFile(hostnames.asFileContent()); diff --git a/packages/independent/https-local/scripts/hosts/ensure_localhost_mappings.mjs b/packages/independent/https-local/scripts/hosts/ensure_localhost_mappings.mjs index bc20b7cc7f..66b2c35420 100644 --- a/packages/independent/https-local/scripts/hosts/ensure_localhost_mappings.mjs +++ b/packages/independent/https-local/scripts/hosts/ensure_localhost_mappings.mjs @@ -1,4 +1,4 @@ -import { verifyHostsFile } from "@jsenv/https-local" +import { verifyHostsFile } from "@jsenv/https-local"; await verifyHostsFile({ logLevel: "debug", @@ -6,4 +6,4 @@ await verifyHostsFile({ "127.0.0.1": ["localhost", "local.example"], }, tryToUpdateHostsFile: true, -}) +}); diff --git a/packages/independent/https-local/scripts/hosts/remove_localhost_mappings.mjs b/packages/independent/https-local/scripts/hosts/remove_localhost_mappings.mjs index bcc8638a0b..d63df199f8 100644 --- a/packages/independent/https-local/scripts/hosts/remove_localhost_mappings.mjs +++ b/packages/independent/https-local/scripts/hosts/remove_localhost_mappings.mjs @@ -1,16 +1,16 @@ import { - readHostsFile, parseHosts, + readHostsFile, writeHostsFile, -} from "@jsenv/https-local/src/internal/hosts.js" +} from "@jsenv/https-local/src/internal/hosts.js"; -const hostsFileContent = await readHostsFile() -const hostnames = parseHosts(hostsFileContent) -const localIpHostnames = hostnames.getIpHostnames("127.0.0.1") +const hostsFileContent = await readHostsFile(); +const hostnames = parseHosts(hostsFileContent); +const localIpHostnames = hostnames.getIpHostnames("127.0.0.1"); if (localIpHostnames.includes("localhost")) { - hostnames.removeIpHostname("127.0.0.1", "localhost") + hostnames.removeIpHostname("127.0.0.1", "localhost"); } if (localIpHostnames.includes("local.example.com")) { - hostnames.removeIpHostname("127.0.0.1", "local.example") + hostnames.removeIpHostname("127.0.0.1", "local.example"); } -await writeHostsFile(hostnames.asFileContent()) +await writeHostsFile(hostnames.asFileContent()); diff --git a/packages/independent/https-local/scripts/hosts/verify_localhost_mappings.mjs b/packages/independent/https-local/scripts/hosts/verify_localhost_mappings.mjs index 582a33f4e9..2a3d7b15fc 100644 --- a/packages/independent/https-local/scripts/hosts/verify_localhost_mappings.mjs +++ b/packages/independent/https-local/scripts/hosts/verify_localhost_mappings.mjs @@ -1,7 +1,7 @@ -import { verifyHostsFile } from "@jsenv/https-local" +import { verifyHostsFile } from "@jsenv/https-local"; await verifyHostsFile({ ipMappings: { "127.0.0.1": ["localhost", "local.example"], }, -}) +}); diff --git a/packages/independent/https-local/scripts/performance/measure_package_import.mjs b/packages/independent/https-local/scripts/performance/measure_package_import.mjs index 6d03f6d9e7..0656aaa436 100644 --- a/packages/independent/https-local/scripts/performance/measure_package_import.mjs +++ b/packages/independent/https-local/scripts/performance/measure_package_import.mjs @@ -1,14 +1,14 @@ -import { startMeasures } from "@jsenv/performance-impact" +import { startMeasures } from "@jsenv/performance-impact"; const measures = startMeasures({ gc: true, memoryHeap: true, -}) +}); // eslint-disable-next-line no-unused-vars -let namespace = await import("@jsenv/https-local") +let namespace = await import("@jsenv/https-local"); -const { duration, memoryHeapUsed } = measures.stop() +const { duration, memoryHeapUsed } = measures.stop(); export const packageImportMetrics = { "import duration": { value: duration, unit: "ms" }, @@ -16,4 +16,4 @@ export const packageImportMetrics = { value: Math.max(memoryHeapUsed, 0), unit: "byte", }, -} +}; diff --git a/packages/independent/https-local/scripts/performance/measure_package_tarball.mjs b/packages/independent/https-local/scripts/performance/measure_package_tarball.mjs index 741538bdeb..c241b9e9cd 100644 --- a/packages/independent/https-local/scripts/performance/measure_package_tarball.mjs +++ b/packages/independent/https-local/scripts/performance/measure_package_tarball.mjs @@ -1,15 +1,15 @@ -import { exec } from "node:child_process" +import { exec } from "node:child_process"; const npmPackInfo = await new Promise((resolve, reject) => { exec(`npm pack --dry-run --json`, (error, stdout) => { if (error) { - reject(error) + reject(error); } else { - resolve(JSON.parse(stdout)) + resolve(JSON.parse(stdout)); } - }) -}) -const npmTarballInfo = npmPackInfo[0] + }); +}); +const npmTarballInfo = npmPackInfo[0]; export const packageTarballmetrics = { "npm tarball size": { value: npmTarballInfo.size, unit: "byte" }, @@ -18,4 +18,4 @@ export const packageTarballmetrics = { unit: "byte", }, "npm tarball file count": { value: npmTarballInfo.entryCount }, -} +}; diff --git a/packages/independent/https-local/scripts/performance/performance.mjs b/packages/independent/https-local/scripts/performance/performance.mjs index 091d83bc23..a210291947 100644 --- a/packages/independent/https-local/scripts/performance/performance.mjs +++ b/packages/independent/https-local/scripts/performance/performance.mjs @@ -11,7 +11,7 @@ * See https://github.com/jsenv/performance-impact */ -import { importMetricFromFiles } from "@jsenv/performance-impact" +import { importMetricFromFiles } from "@jsenv/performance-impact"; const { packageImportMetrics, packageTarballMetrics } = await importMetricFromFiles({ @@ -28,11 +28,11 @@ const { packageImportMetrics, packageTarballMetrics } = }, }, logLevel: process.argv.includes("--log") ? "info" : "warn", - }) + }); export const performanceReport = { "package metrics": { ...packageImportMetrics, ...packageTarballMetrics, }, -} +}; diff --git a/packages/independent/https-local/src/internal/browser_detection.js b/packages/independent/https-local/src/internal/browser_detection.js index e98dda112e..158f82d0e3 100644 --- a/packages/independent/https-local/src/internal/browser_detection.js +++ b/packages/independent/https-local/src/internal/browser_detection.js @@ -1,7 +1,7 @@ -import { existsSync } from "node:fs" +import { existsSync } from "node:fs"; export const detectBrowser = (pathCandidates) => { return pathCandidates.some((pathCandidate) => { - return existsSync(pathCandidate) - }) -} + return existsSync(pathCandidate); + }); +}; diff --git a/packages/independent/https-local/src/internal/certificate_authority_file_urls.js b/packages/independent/https-local/src/internal/certificate_authority_file_urls.js index 8a83ba0a58..02b4ce7a1b 100644 --- a/packages/independent/https-local/src/internal/certificate_authority_file_urls.js +++ b/packages/independent/https-local/src/internal/certificate_authority_file_urls.js @@ -1,33 +1,33 @@ -import { urlToFilename } from "@jsenv/urls" -import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem" +import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem"; +import { urlToFilename } from "@jsenv/urls"; export const getCertificateAuthorityFileUrls = () => { // we need a directory common to every instance of @jsenv/https-local // so that even if it's used multiple times, the certificate autority files // are reused - const applicationDirectoryUrl = getJsenvApplicationDirectoryUrl() + const applicationDirectoryUrl = getJsenvApplicationDirectoryUrl(); const certificateAuthorityJsonFileUrl = new URL( "./https_local_certificate_authority.json", applicationDirectoryUrl, - ) + ); const rootCertificateFileUrl = new URL( "./https_local_root_certificate.crt", applicationDirectoryUrl, - ) + ); const rootCertificatePrivateKeyFileUrl = new URL( "./https_local_root_certificate.key", applicationDirectoryUrl, - ).href + ).href; return { certificateAuthorityJsonFileUrl, rootCertificateFileUrl, rootCertificatePrivateKeyFileUrl, - } -} + }; +}; export const getRootCertificateSymlinkUrls = ({ rootCertificateFileUrl, @@ -35,34 +35,34 @@ export const getRootCertificateSymlinkUrls = ({ serverCertificateFileUrl, }) => { const serverCertificateDirectory = new URL("./", serverCertificateFileUrl) - .href + .href; - const rootCertificateFilename = urlToFilename(rootCertificateFileUrl) + const rootCertificateFilename = urlToFilename(rootCertificateFileUrl); const rootCertificateSymlinkUrl = new URL( rootCertificateFilename, serverCertificateDirectory, - ).href - const rootPrivateKeyFilename = urlToFilename(rootPrivateKeyFileUrl) + ).href; + const rootPrivateKeyFilename = urlToFilename(rootPrivateKeyFileUrl); const rootPrivateKeySymlinkUrl = new URL( rootPrivateKeyFilename, serverCertificateDirectory, - ).href + ).href; return { rootCertificateSymlinkUrl, rootPrivateKeySymlinkUrl, - } -} + }; +}; // https://github.com/LinusU/node-application-config-path/blob/master/index.js const getJsenvApplicationDirectoryUrl = () => { - const { platform } = process + const { platform } = process; if (platform === "darwin") { return new URL( `./Library/Application Support/https_local/`, assertAndNormalizeDirectoryUrl(process.env.HOME), - ).href + ).href; } if (platform === "linux") { @@ -70,12 +70,12 @@ const getJsenvApplicationDirectoryUrl = () => { return new URL( `./https_local/`, assertAndNormalizeDirectoryUrl(process.env.XDG_CONFIG_HOME), - ).href + ).href; } return new URL( `./.config/https_local/`, assertAndNormalizeDirectoryUrl(process.env.HOME), - ).href + ).href; } if (platform === "win32") { @@ -83,14 +83,14 @@ const getJsenvApplicationDirectoryUrl = () => { return new URL( `./https_local/`, assertAndNormalizeDirectoryUrl(process.env.LOCALAPPDATA), - ).href + ).href; } return new URL( `./Local Settings/Application Data/https_local/`, assertAndNormalizeDirectoryUrl(process.env.USERPROFILE), - ).href + ).href; } - throw new Error(`platform not supported`) -} + throw new Error(`platform not supported`); +}; diff --git a/packages/independent/https-local/src/internal/certificate_data_converter.js b/packages/independent/https-local/src/internal/certificate_data_converter.js index 72e6f5bf71..574a90cd4e 100644 --- a/packages/independent/https-local/src/internal/certificate_data_converter.js +++ b/packages/independent/https-local/src/internal/certificate_data_converter.js @@ -1,4 +1,4 @@ -import { isIP } from "node:net" +import { isIP } from "node:net"; export const subjectAltNamesFromAltNames = (altNames) => { const altNamesArray = altNames.map((altName) => { @@ -6,86 +6,86 @@ export const subjectAltNamesFromAltNames = (altNames) => { return { type: 7, ip: altName, - } + }; } if (isUrl(altName)) { return { type: 6, value: altName, - } + }; } // 2 is DNS (Domain Name Server) return { type: 2, value: altName, - } - }) + }; + }); - return altNamesArray -} + return altNamesArray; +}; const isUrl = (value) => { try { // eslint-disable-next-line no-new - new URL(value) - return true + new URL(value); + return true; } catch (e) { - return false + return false; } -} +}; export const extensionArrayFromExtensionDescription = ( extensionDescription, ) => { - const extensionArray = [] + const extensionArray = []; Object.keys(extensionDescription).forEach((key) => { - const value = extensionDescription[key] + const value = extensionDescription[key]; if (value) { extensionArray.push({ name: key, ...value, - }) + }); } - }) - return extensionArray -} + }); + return extensionArray; +}; export const extensionDescriptionFromExtensionArray = (extensionArray) => { - const extensionDescription = {} + const extensionDescription = {}; extensionArray.forEach((extension) => { - const { name, ...rest } = extension - extensionDescription[name] = rest - }) - return extensionDescription -} + const { name, ...rest } = extension; + extensionDescription[name] = rest; + }); + return extensionDescription; +}; export const attributeDescriptionFromAttributeArray = (attributeArray) => { - const attributeObject = {} + const attributeObject = {}; attributeArray.forEach((attribute) => { - attributeObject[attribute.name] = attribute.value - }) - return attributeObject -} + attributeObject[attribute.name] = attribute.value; + }); + return attributeObject; +}; export const attributeArrayFromAttributeDescription = ( attributeDescription, ) => { - const attributeArray = [] + const attributeArray = []; Object.keys(attributeDescription).forEach((key) => { - const value = attributeDescription[key] + const value = attributeDescription[key]; if (typeof value === "undefined") { - return + return; } attributeArray.push({ name: key, value, - }) - }) - return attributeArray -} + }); + }); + return attributeArray; +}; export const normalizeForgeAltNames = (forgeAltNames) => { return forgeAltNames.map((forgeAltName) => { - return forgeAltName.ip || forgeAltName.value - }) -} + return forgeAltName.ip || forgeAltName.value; + }); +}; diff --git a/packages/independent/https-local/src/internal/certificate_generator.js b/packages/independent/https-local/src/internal/certificate_generator.js index 0b2fbe48c4..36409387f2 100644 --- a/packages/independent/https-local/src/internal/certificate_generator.js +++ b/packages/independent/https-local/src/internal/certificate_generator.js @@ -1,13 +1,13 @@ // https://github.com/digitalbazaar/forge/blob/master/examples/create-cert.js // https://github.com/digitalbazaar/forge/issues/660#issuecomment-467145103 -import { forge } from "./forge.js" import { attributeArrayFromAttributeDescription, attributeDescriptionFromAttributeArray, - subjectAltNamesFromAltNames, extensionArrayFromExtensionDescription, -} from "./certificate_data_converter.js" + subjectAltNamesFromAltNames, +} from "./certificate_data_converter.js"; +import { forge } from "./forge.js"; export const createAuthorityRootCertificate = async ({ commonName, @@ -20,21 +20,21 @@ export const createAuthorityRootCertificate = async ({ serialNumber, } = {}) => { if (typeof serialNumber !== "number") { - throw new TypeError(`serial must be a number but received ${serialNumber}`) + throw new TypeError(`serial must be a number but received ${serialNumber}`); } - const { pki } = forge - const rootCertificateForgeObject = pki.createCertificate() - const keyPair = pki.rsa.generateKeyPair(2048) // TODO: use async version https://github.com/digitalbazaar/forge#rsa - const rootCertificatePublicKeyForgeObject = keyPair.publicKey - const rootCertificatePrivateKeyForgeObject = keyPair.privateKey + const { pki } = forge; + const rootCertificateForgeObject = pki.createCertificate(); + const keyPair = pki.rsa.generateKeyPair(2048); // TODO: use async version https://github.com/digitalbazaar/forge#rsa + const rootCertificatePublicKeyForgeObject = keyPair.publicKey; + const rootCertificatePrivateKeyForgeObject = keyPair.privateKey; - rootCertificateForgeObject.publicKey = rootCertificatePublicKeyForgeObject - rootCertificateForgeObject.serialNumber = serialNumber.toString(16) - rootCertificateForgeObject.validity.notBefore = new Date() + rootCertificateForgeObject.publicKey = rootCertificatePublicKeyForgeObject; + rootCertificateForgeObject.serialNumber = serialNumber.toString(16); + rootCertificateForgeObject.validity.notBefore = new Date(); rootCertificateForgeObject.validity.notAfter = new Date( Date.now() + validityDurationInMs, - ) + ); rootCertificateForgeObject.setSubject( attributeArrayFromAttributeDescription({ commonName, @@ -44,7 +44,7 @@ export const createAuthorityRootCertificate = async ({ organizationName, organizationalUnitName, }), - ) + ); rootCertificateForgeObject.setIssuer( attributeArrayFromAttributeDescription({ commonName, @@ -54,7 +54,7 @@ export const createAuthorityRootCertificate = async ({ organizationName, organizationalUnitName, }), - ) + ); rootCertificateForgeObject.setExtensions( extensionArrayFromExtensionDescription({ basicConstraints: { @@ -73,17 +73,17 @@ export const createAuthorityRootCertificate = async ({ // }, // subjectKeyIdentifier: {}, }), - ) + ); // self-sign certificate - rootCertificateForgeObject.sign(rootCertificatePrivateKeyForgeObject) // , forge.sha256.create()) + rootCertificateForgeObject.sign(rootCertificatePrivateKeyForgeObject); // , forge.sha256.create()) return { rootCertificateForgeObject, rootCertificatePublicKeyForgeObject, rootCertificatePrivateKeyForgeObject, - } -} + }; +}; export const requestCertificateFromAuthority = ({ authorityCertificateForgeObject, // could be intermediate or root certificate authority @@ -99,7 +99,7 @@ export const requestCertificateFromAuthority = ({ ) { throw new TypeError( `authorityCertificateForgeObject must be an object but received ${authorityCertificateForgeObject}`, - ) + ); } if ( typeof auhtorityCertificatePrivateKeyForgeObject !== "object" || @@ -107,26 +107,26 @@ export const requestCertificateFromAuthority = ({ ) { throw new TypeError( `auhtorityCertificatePrivateKeyForgeObject must be an object but received ${auhtorityCertificatePrivateKeyForgeObject}`, - ) + ); } if (typeof serialNumber !== "number") { throw new TypeError( `serialNumber must be a number but received ${serialNumber}`, - ) + ); } - const { pki } = forge - const certificateForgeObject = pki.createCertificate() - const keyPair = pki.rsa.generateKeyPair(2048) // TODO: use async version https://github.com/digitalbazaar/forge#rsa - const certificatePublicKeyForgeObject = keyPair.publicKey - const certificatePrivateKeyForgeObject = keyPair.privateKey + const { pki } = forge; + const certificateForgeObject = pki.createCertificate(); + const keyPair = pki.rsa.generateKeyPair(2048); // TODO: use async version https://github.com/digitalbazaar/forge#rsa + const certificatePublicKeyForgeObject = keyPair.publicKey; + const certificatePrivateKeyForgeObject = keyPair.privateKey; - certificateForgeObject.publicKey = certificatePublicKeyForgeObject - certificateForgeObject.serialNumber = serialNumber.toString(16) - certificateForgeObject.validity.notBefore = new Date() + certificateForgeObject.publicKey = certificatePublicKeyForgeObject; + certificateForgeObject.serialNumber = serialNumber.toString(16); + certificateForgeObject.validity.notBefore = new Date(); certificateForgeObject.validity.notAfter = new Date( Date.now() + validityDurationInMs, - ) + ); const attributeDescription = { ...attributeDescriptionFromAttributeArray( @@ -134,13 +134,13 @@ export const requestCertificateFromAuthority = ({ ), commonName, // organizationName: serverCertificateOrganizationName - } + }; const attributeArray = - attributeArrayFromAttributeDescription(attributeDescription) - certificateForgeObject.setSubject(attributeArray) + attributeArrayFromAttributeDescription(attributeDescription); + certificateForgeObject.setSubject(attributeArray); certificateForgeObject.setIssuer( authorityCertificateForgeObject.subject.attributes, - ) + ); certificateForgeObject.setExtensions( extensionArrayFromExtensionDescription({ basicConstraints: { @@ -167,15 +167,15 @@ export const requestCertificateFromAuthority = ({ altNames: subjectAltNamesFromAltNames(altNames), }, }), - ) + ); certificateForgeObject.sign( auhtorityCertificatePrivateKeyForgeObject, forge.sha256.create(), - ) + ); return { certificateForgeObject, certificatePublicKeyForgeObject, certificatePrivateKeyForgeObject, - } -} + }; +}; diff --git a/packages/independent/https-local/src/internal/command.js b/packages/independent/https-local/src/internal/command.js index 44c4037625..79471322a2 100644 --- a/packages/independent/https-local/src/internal/command.js +++ b/packages/independent/https-local/src/internal/command.js @@ -1,9 +1,9 @@ -import { createRequire } from "node:module" +import { createRequire } from "node:module"; -const require = createRequire(import.meta.url) +const require = createRequire(import.meta.url); export const commandExists = async (command) => { - const { sync } = require("command-exists") - const exists = sync(command) - return exists -} + const { sync } = require("command-exists"); + const exists = sync(command); + return exists; +}; diff --git a/packages/independent/https-local/src/internal/exec.js b/packages/independent/https-local/src/internal/exec.js index 61cf9da392..24a1ac3f1e 100644 --- a/packages/independent/https-local/src/internal/exec.js +++ b/packages/independent/https-local/src/internal/exec.js @@ -1,4 +1,4 @@ -import { exec as nodeExec } from "node:child_process" +import { exec as nodeExec } from "node:child_process"; export const exec = ( command, @@ -14,20 +14,20 @@ export const exec = ( }, (error, stdout) => { if (error) { - reject(error) + reject(error); } else { - resolve(stdout) + resolve(stdout); } }, - ) + ); commandProcess.stdout.on("data", (data) => { - onLog(data) - }) + onLog(data); + }); commandProcess.stderr.on("data", (data) => { // debug because this output is part of // the error message generated by a failing npm publish - onErrorLog(data) - }) - }) -} + onErrorLog(data); + }); + }); +}; diff --git a/packages/independent/https-local/src/internal/forge.js b/packages/independent/https-local/src/internal/forge.js index aaa8a529ab..ed3a9586df 100644 --- a/packages/independent/https-local/src/internal/forge.js +++ b/packages/independent/https-local/src/internal/forge.js @@ -1,5 +1,5 @@ -import { createRequire } from "node:module" +import { createRequire } from "node:module"; -const require = createRequire(import.meta.url) +const require = createRequire(import.meta.url); -export const forge = require("node-forge") +export const forge = require("node-forge"); diff --git a/packages/independent/https-local/src/internal/hosts.js b/packages/independent/https-local/src/internal/hosts.js index 4d32a4c468..61bd9213bd 100644 --- a/packages/independent/https-local/src/internal/hosts.js +++ b/packages/independent/https-local/src/internal/hosts.js @@ -1,9 +1,9 @@ -export { HOSTS_FILE_PATH } from "./hosts/hosts_utils.js" +export { HOSTS_FILE_PATH } from "./hosts/hosts_utils.js"; -export { readHostsFile } from "./hosts/read_hosts.js" +export { readHostsFile } from "./hosts/read_hosts.js"; -export { parseHosts } from "./hosts/parse_hosts.js" +export { parseHosts } from "./hosts/parse_hosts.js"; -export { writeHostsFile } from "./hosts/write_hosts.js" +export { writeHostsFile } from "./hosts/write_hosts.js"; -export { writeLineInHostsFile } from "./hosts/write_line_hosts.js" +export { writeLineInHostsFile } from "./hosts/write_line_hosts.js"; diff --git a/packages/independent/https-local/src/internal/hosts/hosts_utils.js b/packages/independent/https-local/src/internal/hosts/hosts_utils.js index 2da6db921b..38c500da39 100644 --- a/packages/independent/https-local/src/internal/hosts/hosts_utils.js +++ b/packages/independent/https-local/src/internal/hosts/hosts_utils.js @@ -1,5 +1,5 @@ -const IS_WINDOWS = process.platform === "win32" +const IS_WINDOWS = process.platform === "win32"; export const HOSTS_FILE_PATH = IS_WINDOWS ? "C:\\Windows\\System32\\Drivers\\etc\\hosts" - : "/etc/hosts" + : "/etc/hosts"; diff --git a/packages/independent/https-local/src/internal/linux/linux.js b/packages/independent/https-local/src/internal/linux/linux.js index a009362d90..ca4114ad9e 100644 --- a/packages/independent/https-local/src/internal/linux/linux.js +++ b/packages/independent/https-local/src/internal/linux/linux.js @@ -1,6 +1,6 @@ -import { executeTrustQueryOnLinux } from "./linux_trust_store.js" -import { executeTrustQueryOnChrome } from "./chrome_linux.js" -import { executeTrustQueryOnFirefox } from "./firefox_linux.js" +import { executeTrustQueryOnChrome } from "./chrome_linux.js"; +import { executeTrustQueryOnFirefox } from "./firefox_linux.js"; +import { executeTrustQueryOnLinux } from "./linux_trust_store.js"; export const executeTrustQuery = async ({ logger, @@ -18,7 +18,7 @@ export const executeTrustQuery = async ({ certificateIsNew, certificate, verb, - }) + }); const chromeTrustInfo = await executeTrustQueryOnChrome({ logger, @@ -28,7 +28,7 @@ export const executeTrustQuery = async ({ certificate, verb, NSSDynamicInstall, - }) + }); const firefoxTrustInfo = await executeTrustQueryOnFirefox({ logger, @@ -38,11 +38,11 @@ export const executeTrustQuery = async ({ certificate, verb, NSSDynamicInstall, - }) + }); return { linux: linuxTrustInfo, chrome: chromeTrustInfo, firefox: firefoxTrustInfo, - } -} + }; +}; diff --git a/packages/independent/https-local/src/internal/mac/mac.js b/packages/independent/https-local/src/internal/mac/mac.js index cee5a6ad2b..9584cb7bb5 100644 --- a/packages/independent/https-local/src/internal/mac/mac.js +++ b/packages/independent/https-local/src/internal/mac/mac.js @@ -4,10 +4,10 @@ * - https://www.unix.com/man-page/mojave/1/security/ */ -import { executeTrustQueryOnMacKeychain } from "./mac_keychain.js" -import { executeTrustQueryOnChrome } from "./chrome_mac.js" -import { executeTrustQueryOnFirefox } from "./firefox_mac.js" -import { executeTrustQueryOnSafari } from "./safari.js" +import { executeTrustQueryOnChrome } from "./chrome_mac.js"; +import { executeTrustQueryOnFirefox } from "./firefox_mac.js"; +import { executeTrustQueryOnMacKeychain } from "./mac_keychain.js"; +import { executeTrustQueryOnSafari } from "./safari.js"; export const executeTrustQuery = async ({ logger, @@ -25,13 +25,13 @@ export const executeTrustQuery = async ({ certificateIsNew, certificate, verb, - }) + }); const chromeTrustInfo = await executeTrustQueryOnChrome({ logger, // chrome needs macTrustInfo because it uses OS trust store macTrustInfo, - }) + }); const firefoxTrustInfo = await executeTrustQueryOnFirefox({ logger, @@ -41,18 +41,18 @@ export const executeTrustQuery = async ({ certificate, verb, NSSDynamicInstall, - }) + }); const safariTrustInfo = await executeTrustQueryOnSafari({ logger, // safari needs macTrustInfo because it uses OS trust store macTrustInfo, - }) + }); return { mac: macTrustInfo, chrome: chromeTrustInfo, firefox: firefoxTrustInfo, safari: safariTrustInfo, - } -} + }; +}; diff --git a/packages/independent/https-local/src/internal/mac/safari.js b/packages/independent/https-local/src/internal/mac/safari.js index 2356ce962a..32bbf59cd3 100644 --- a/packages/independent/https-local/src/internal/mac/safari.js +++ b/packages/independent/https-local/src/internal/mac/safari.js @@ -2,5 +2,5 @@ export const executeTrustQueryOnSafari = ({ macTrustInfo }) => { return { status: macTrustInfo.status, reason: macTrustInfo.reason, - } -} + }; +}; diff --git a/packages/independent/https-local/src/internal/memoize.js b/packages/independent/https-local/src/internal/memoize.js index c49b8708d3..272c782bf8 100644 --- a/packages/independent/https-local/src/internal/memoize.js +++ b/packages/independent/https-local/src/internal/memoize.js @@ -1,24 +1,24 @@ export const memoize = (compute) => { - let memoized = false - let memoizedValue + let memoized = false; + let memoizedValue; const fnWithMemoization = (...args) => { if (memoized) { - return memoizedValue + return memoizedValue; } // if compute is recursive wait for it to be fully done before storing the lockValue // so set locked later - memoizedValue = compute(...args) - memoized = true - return memoizedValue - } + memoizedValue = compute(...args); + memoized = true; + return memoizedValue; + }; fnWithMemoization.forget = () => { - const value = memoizedValue - memoized = false - memoizedValue = undefined - return value - } + const value = memoizedValue; + memoized = false; + memoizedValue = undefined; + return value; + }; - return fnWithMemoization -} + return fnWithMemoization; +}; diff --git a/packages/independent/https-local/src/internal/platform.js b/packages/independent/https-local/src/internal/platform.js index d2de6d1c35..4c00bc9028 100644 --- a/packages/independent/https-local/src/internal/platform.js +++ b/packages/independent/https-local/src/internal/platform.js @@ -1,13 +1,13 @@ export const importPlatformMethods = async () => { - const { platform } = process + const { platform } = process; if (platform === "darwin") { - return await import("./mac/mac.js") + return await import("./mac/mac.js"); } if (platform === "linux") { - return await import("./linux/linux.js") + return await import("./linux/linux.js"); } if (platform === "win32") { - return await import("./windows/windows.js") + return await import("./windows/windows.js"); } - return await import("./unsupported_platform/unsupported_platform.js") -} + return await import("./unsupported_platform/unsupported_platform.js"); +}; diff --git a/packages/independent/https-local/src/internal/search_certificate_in_command_output.js b/packages/independent/https-local/src/internal/search_certificate_in_command_output.js index 2df1af3f00..7a9ecff363 100644 --- a/packages/independent/https-local/src/internal/search_certificate_in_command_output.js +++ b/packages/independent/https-local/src/internal/search_certificate_in_command_output.js @@ -2,7 +2,7 @@ export const searchCertificateInCommandOutput = ( commandOutput, certificateAsPEM, ) => { - commandOutput = commandOutput.replace(/\r\n/g, "\n").trim() - certificateAsPEM = certificateAsPEM.replace(/\r\n/g, "\n").trim() - return commandOutput.includes(certificateAsPEM) -} + commandOutput = commandOutput.replace(/\r\n/g, "\n").trim(); + certificateAsPEM = certificateAsPEM.replace(/\r\n/g, "\n").trim(); + return commandOutput.includes(certificateAsPEM); +}; diff --git a/packages/independent/https-local/src/internal/trust_query.js b/packages/independent/https-local/src/internal/trust_query.js index bb48077b03..1a66eef7d4 100644 --- a/packages/independent/https-local/src/internal/trust_query.js +++ b/packages/independent/https-local/src/internal/trust_query.js @@ -1,4 +1,4 @@ -export const VERB_CHECK_TRUST = "CHECK_TRUST" -export const VERB_ADD_TRUST = "ADD_TRUST" -export const VERB_ENSURE_TRUST = "ENSURE_TRUST" -export const VERB_REMOVE_TRUST = "REMOVE_TRUST" +export const VERB_CHECK_TRUST = "CHECK_TRUST"; +export const VERB_ADD_TRUST = "ADD_TRUST"; +export const VERB_ENSURE_TRUST = "ENSURE_TRUST"; +export const VERB_REMOVE_TRUST = "REMOVE_TRUST"; diff --git a/packages/independent/https-local/src/internal/validity_formatting.js b/packages/independent/https-local/src/internal/validity_formatting.js index 4bd68a2762..cd91fa7750 100644 --- a/packages/independent/https-local/src/internal/validity_formatting.js +++ b/packages/independent/https-local/src/internal/validity_formatting.js @@ -1,8 +1,8 @@ export const formatStillValid = ({ certificateName, validityRemainingMs }) => { return `${certificateName} still valid for ${formatDuration( validityRemainingMs, - )}` -} + )}`; +}; export const formatAboutToExpire = ({ certificateName, @@ -11,8 +11,8 @@ export const formatAboutToExpire = ({ }) => { return `${certificateName} will expire ${formatTimeDelta( validityRemainingMs, - )}, it was valid during ${formatDuration(msEllapsedSinceValid)}` -} + )}, it was valid during ${formatDuration(msEllapsedSinceValid)}`; +}; export const formatExpired = ({ certificateName, @@ -21,59 +21,59 @@ export const formatExpired = ({ }) => { return `${certificateName} has expired ${formatTimeDelta( -msEllapsedSinceExpiration, - )}, it was valid during ${formatDuration(validityDurationInMs)}` -} + )}, it was valid during ${formatDuration(validityDurationInMs)}`; +}; export const formatTimeDelta = (deltaInMs) => { - const unit = pickUnit(Math.abs(deltaInMs)) - const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" }) - const msRounded = unit.min ? Math.floor(deltaInMs / unit.min) : deltaInMs - return rtf.format(msRounded, unit.name) -} + const unit = pickUnit(Math.abs(deltaInMs)); + const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" }); + const msRounded = unit.min ? Math.floor(deltaInMs / unit.min) : deltaInMs; + return rtf.format(msRounded, unit.name); +}; export const formatDuration = (ms) => { - const unit = pickUnit(ms) - const rtf = new Intl.RelativeTimeFormat("en", { numeric: "always" }) - const msRounded = unit.min ? Math.floor(ms / unit.min) : ms - const parts = rtf.formatToParts(msRounded, unit.name) + const unit = pickUnit(ms); + const rtf = new Intl.RelativeTimeFormat("en", { numeric: "always" }); + const msRounded = unit.min ? Math.floor(ms / unit.min) : ms; + const parts = rtf.formatToParts(msRounded, unit.name); if (parts.length > 1 && parts[0].type === "literal") { return parts .slice(1) .map((part) => part.value) - .join("") + .join(""); } - return parts.map((part) => part.value).join("") -} + return parts.map((part) => part.value).join(""); +}; const pickUnit = (ms) => { - const msPerSecond = 1000 - const msPerMinute = msPerSecond * 60 - const msPerHour = msPerMinute * 60 - const msPerDay = msPerHour * 24 - const msPerMonth = msPerDay * 30 - const msPerYear = msPerDay * 365 + const msPerSecond = 1000; + const msPerMinute = msPerSecond * 60; + const msPerHour = msPerMinute * 60; + const msPerDay = msPerHour * 24; + const msPerMonth = msPerDay * 30; + const msPerYear = msPerDay * 365; if (ms < msPerSecond) { return { name: "second", // min 0 to allow display of 0.01 second for example min: 0, - } + }; } if (ms < msPerMinute) { - return { name: "second", min: msPerSecond } + return { name: "second", min: msPerSecond }; } if (ms < msPerHour) { - return { name: "minute", min: msPerMinute } + return { name: "minute", min: msPerMinute }; } if (ms < msPerDay) { - return { name: "hour", min: msPerHour } + return { name: "hour", min: msPerHour }; } if (ms < msPerMonth) { - return { name: "day", min: msPerDay } + return { name: "day", min: msPerDay }; } if (ms < msPerYear) { - return { name: "month", min: msPerMonth } + return { name: "month", min: msPerMonth }; } - return { name: "year", min: msPerYear } -} + return { name: "year", min: msPerYear }; +}; diff --git a/packages/independent/https-local/src/internal/windows/edge.js b/packages/independent/https-local/src/internal/windows/edge.js index 888ffd24ad..032f37c009 100644 --- a/packages/independent/https-local/src/internal/windows/edge.js +++ b/packages/independent/https-local/src/internal/windows/edge.js @@ -2,5 +2,5 @@ export const executeTrustQueryOnEdge = ({ windowsTrustInfo }) => { return { status: windowsTrustInfo.status, reason: windowsTrustInfo.reason, - } -} + }; +}; diff --git a/packages/independent/https-local/src/internal/windows/windows.js b/packages/independent/https-local/src/internal/windows/windows.js index b4c04931d4..4686f2b389 100644 --- a/packages/independent/https-local/src/internal/windows/windows.js +++ b/packages/independent/https-local/src/internal/windows/windows.js @@ -4,10 +4,10 @@ * - https://www.unix.com/man-page/mojave/1/security/ */ -import { executeTrustQueryOnWindows } from "./windows_certutil.js" -import { executeTrustQueryOnChrome } from "./chrome_windows.js" -import { executeTrustQueryOnEdge } from "./edge.js" -import { executeTrustQueryOnFirefox } from "./firefox_windows.js" +import { executeTrustQueryOnChrome } from "./chrome_windows.js"; +import { executeTrustQueryOnEdge } from "./edge.js"; +import { executeTrustQueryOnFirefox } from "./firefox_windows.js"; +import { executeTrustQueryOnWindows } from "./windows_certutil.js"; export const executeTrustQuery = async ({ logger, @@ -24,26 +24,26 @@ export const executeTrustQuery = async ({ certificateIsNew, certificate, verb, - }) + }); const chromeTrustInfo = await executeTrustQueryOnChrome({ logger, windowsTrustInfo, - }) + }); const edgeTrustInfo = await executeTrustQueryOnEdge({ windowsTrustInfo, - }) + }); const firefoxTrustInfo = await executeTrustQueryOnFirefox({ logger, certificateIsNew, - }) + }); return { windows: windowsTrustInfo, chrome: chromeTrustInfo, edge: edgeTrustInfo, firefox: firefoxTrustInfo, - } -} + }; +}; diff --git a/packages/independent/https-local/src/jsenvParameters.js b/packages/independent/https-local/src/jsenvParameters.js index aec3ab8236..a8f6a71ee6 100644 --- a/packages/independent/https-local/src/jsenvParameters.js +++ b/packages/independent/https-local/src/jsenvParameters.js @@ -1,9 +1,9 @@ -import { createValidityDurationOfXYears } from "./validity_duration.js" +import { createValidityDurationOfXYears } from "./validity_duration.js"; export const jsenvParameters = { certificateCommonName: "https local root certificate", certificateValidityDurationInMs: createValidityDurationOfXYears(20), -} +}; // const jsenvCertificateParams = { // rootCertificateOrganizationName: "jsenv", diff --git a/packages/independent/https-local/src/validity_duration.js b/packages/independent/https-local/src/validity_duration.js index 4a4b26c364..7a4e156dbc 100644 --- a/packages/independent/https-local/src/validity_duration.js +++ b/packages/independent/https-local/src/validity_duration.js @@ -1,8 +1,8 @@ -const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000 -const MILLISECONDS_PER_YEAR = MILLISECONDS_PER_DAY * 365 +const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; +const MILLISECONDS_PER_YEAR = MILLISECONDS_PER_DAY * 365; export const verifyRootCertificateValidityDuration = (validityDurationInMs) => { - const durationInYears = validityDurationInMs / MILLISECONDS_PER_YEAR + const durationInYears = validityDurationInMs / MILLISECONDS_PER_YEAR; if (durationInYears > 25) { return { ok: false, @@ -10,15 +10,15 @@ export const verifyRootCertificateValidityDuration = (validityDurationInMs) => { message: `root certificate validity duration of ${durationInYears} years is too much, using the max recommended duration: 25 years`, details: "https://serverfault.com/questions/847190/in-theory-could-a-ca-make-a-certificate-that-is-valid-for-arbitrarily-long", - } + }; } - return { ok: true } -} + return { ok: true }; +}; export const verifyServerCertificateValidityDuration = ( validityDurationInMs, ) => { - const validityDurationInDays = validityDurationInMs / MILLISECONDS_PER_DAY + const validityDurationInDays = validityDurationInMs / MILLISECONDS_PER_DAY; if (validityDurationInDays > 397) { return { ok: false, @@ -26,13 +26,13 @@ export const verifyServerCertificateValidityDuration = ( message: `certificate validity duration of ${validityDurationInMs} days is too much, using the max recommended duration: 397 days`, details: "https://www.globalsign.com/en/blog/maximum-ssltls-certificate-validity-now-one-year", - } + }; } - return { ok: true } -} + return { ok: true }; +}; export const createValidityDurationOfXYears = (years) => - MILLISECONDS_PER_YEAR * years + 5000 + MILLISECONDS_PER_YEAR * years + 5000; export const createValidityDurationOfXDays = (days) => - MILLISECONDS_PER_DAY * days + 5000 + MILLISECONDS_PER_DAY * days + 5000; diff --git a/packages/independent/https-local/tests/__internal__/certificate_generation/certificate_generation.test.mjs b/packages/independent/https-local/tests/__internal__/certificate_generation/certificate_generation.test.mjs index bda40b1348..9462333ca8 100644 --- a/packages/independent/https-local/tests/__internal__/certificate_generation/certificate_generation.test.mjs +++ b/packages/independent/https-local/tests/__internal__/certificate_generation/certificate_generation.test.mjs @@ -1,11 +1,11 @@ -import { assert } from "@jsenv/assert" +import { assert } from "@jsenv/assert"; -import { forge } from "@jsenv/https-local/src/internal/forge.js" import { createAuthorityRootCertificate, requestCertificateFromAuthority, -} from "@jsenv/https-local/src/internal/certificate_generator.js" -import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs" +} from "@jsenv/https-local/src/internal/certificate_generator.js"; +import { forge } from "@jsenv/https-local/src/internal/forge.js"; +import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs"; const { rootCertificateForgeObject, @@ -21,38 +21,38 @@ const { organizationalUnitName: "jsenv server", validityDurationInMs: 100000, serialNumber: 0, -}) +}); { const actual = { rootCertificateForgeObject, rootCertificatePublicKeyForgeObject, rootCertificatePrivateKeyForgeObject, - } + }; const expected = { rootCertificateForgeObject: assert.any(Object), rootCertificatePublicKeyForgeObject: assert.any(Object), rootCertificatePrivateKeyForgeObject: assert.any(Object), - } - assert({ actual, expected }) + }; + assert({ actual, expected }); } { - const { pki } = forge + const { pki } = forge; // const rootCertificate = pki.certificateToPem(rootCertificateForgeObject) // const authorityCertificateForgeObject = pki.certificateFromPem(rootCertificate) const rootCertificatePrivateKey = pki.privateKeyToPem( rootCertificatePrivateKeyForgeObject, - ) + ); await new Promise((resolve) => { - setTimeout(resolve, 1000) - }) + setTimeout(resolve, 1000); + }); const auhtorityCertificatePrivateKeyForgeObject = pki.privateKeyFromPem( rootCertificatePrivateKey, - ) - const actual = auhtorityCertificatePrivateKeyForgeObject - const expected = auhtorityCertificatePrivateKeyForgeObject - assert({ actual, expected }) + ); + const actual = auhtorityCertificatePrivateKeyForgeObject; + const expected = auhtorityCertificatePrivateKeyForgeObject; + assert({ actual, expected }); } { @@ -67,18 +67,18 @@ const { serialNumber: 1, altNames: ["localhost"], validityDurationInMs: 10000, - }) + }); const actual = { certificateForgeObject, certificatePublicKeyForgeObject, certificatePrivateKeyForgeObject, - } + }; const expected = { certificateForgeObject: assert.any(Object), certificatePublicKeyForgeObject: assert.any(Object), certificatePrivateKeyForgeObject: assert.any(Object), - } - assert({ actual, expected }) + }; + assert({ actual, expected }); // ici ça serais bien de tester des truc de forge, // genre que le certificat issuer est bien l'authorité diff --git a/packages/independent/https-local/tests/__internal__/hosts_parser/hosts_parser.test.mjs b/packages/independent/https-local/tests/__internal__/hosts_parser/hosts_parser.test.mjs index df837a5b77..a1a88f5a97 100644 --- a/packages/independent/https-local/tests/__internal__/hosts_parser/hosts_parser.test.mjs +++ b/packages/independent/https-local/tests/__internal__/hosts_parser/hosts_parser.test.mjs @@ -1,55 +1,55 @@ -import { assert } from "@jsenv/assert" -import { readFile } from "@jsenv/filesystem" +import { assert } from "@jsenv/assert"; +import { readFile } from "@jsenv/filesystem"; -import { parseHosts } from "@jsenv/https-local/src/internal/hosts.js" +import { parseHosts } from "@jsenv/https-local/src/internal/hosts.js"; const hostsAContent = await readFile( new URL("./hosts_files/hosts", import.meta.url), { as: "string" }, -) -const hostsA = parseHosts(hostsAContent) +); +const hostsA = parseHosts(hostsAContent); { - const actual = hostsA.getAllIpHostnames() + const actual = hostsA.getAllIpHostnames(); const expected = { "127.0.0.1": ["localhost", "loopback", "tool.example.com", "jsenv"], "255.255.255.255": ["broadcasthost"], "::1": ["localhost"], - } - assert({ actual, expected }) + }; + assert({ actual, expected }); } { - const actual = hostsA.getIpHostnames("127.0.0.1") - const expected = ["localhost", "loopback", "tool.example.com", "jsenv"] - assert({ actual, expected }) + const actual = hostsA.getIpHostnames("127.0.0.1"); + const expected = ["localhost", "loopback", "tool.example.com", "jsenv"]; + assert({ actual, expected }); } // without touching anything output is the same { - const actual = hostsA.asFileContent() - const expected = hostsAContent - assert({ actual, expected }) + const actual = hostsA.asFileContent(); + const expected = hostsAContent; + assert({ actual, expected }); } // after removing loopback { - hostsA.removeIpHostname("127.0.0.1", "loopback") - const actual = hostsA.asFileContent() + hostsA.removeIpHostname("127.0.0.1", "loopback"); + const actual = hostsA.asFileContent(); const expected = await readFile( new URL("./hosts_files/hosts_after_removing_loopback", import.meta.url), { as: "string" }, - ) - assert({ actual, expected }) + ); + assert({ actual, expected }); } // after adding example { - hostsA.addIpHostname("127.0.0.1", "example") - const actual = hostsA.asFileContent() + hostsA.addIpHostname("127.0.0.1", "example"); + const actual = hostsA.asFileContent(); const expected = await readFile( new URL("./hosts_files/hosts_after_adding_example", import.meta.url), { as: "string" }, - ) - assert({ actual, expected }) + ); + assert({ actual, expected }); } diff --git a/packages/independent/https-local/tests/authority_certificate/install_authority_first_call.test.mjs b/packages/independent/https-local/tests/authority_certificate/install_authority_first_call.test.mjs index 8926b03ceb..67f8f856d8 100644 --- a/packages/independent/https-local/tests/authority_certificate/install_authority_first_call.test.mjs +++ b/packages/independent/https-local/tests/authority_certificate/install_authority_first_call.test.mjs @@ -1,19 +1,19 @@ -import { assert } from "@jsenv/assert" -import { UNICODE } from "@jsenv/log" +import { assert } from "@jsenv/assert"; +import { UNICODE } from "@jsenv/log"; import { installCertificateAuthority, uninstallCertificateAuthority, -} from "@jsenv/https-local" -import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs" +} from "@jsenv/https-local"; +import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs"; await uninstallCertificateAuthority({ logLevel: "warn", -}) +}); const loggerForTest = createLoggerForTest({ // logLevel: "info", // forwardToConsole: true, -}) +}); const { rootCertificateForgeObject, rootCertificatePrivateKeyForgeObject, @@ -23,12 +23,12 @@ const { trustInfo, } = await installCertificateAuthority({ logger: loggerForTest, -}) +}); const { infos, warns, errors } = loggerForTest.getLogs({ info: true, warn: true, error: true, -}) +}); const actual = { // assert what is logged @@ -42,7 +42,7 @@ const actual = { rootCertificatePrivateKey, rootCertificateFilePath, trustInfo, -} +}; const expected = { infos: [ `${UNICODE.INFO} authority root certificate not found in filesystem`, @@ -123,5 +123,5 @@ const expected = { }, }, }[process.platform], -} -assert({ actual, expected }) +}; +assert({ actual, expected }); diff --git a/packages/independent/https-local/tests/authority_certificate/install_authority_reuse.test.mjs b/packages/independent/https-local/tests/authority_certificate/install_authority_reuse.test.mjs index 45e64f8494..92d87beef3 100644 --- a/packages/independent/https-local/tests/authority_certificate/install_authority_reuse.test.mjs +++ b/packages/independent/https-local/tests/authority_certificate/install_authority_reuse.test.mjs @@ -1,38 +1,39 @@ -import { assert } from "@jsenv/assert" -import { UNICODE } from "@jsenv/log" +import { assert } from "@jsenv/assert"; +import { UNICODE } from "@jsenv/log"; import { installCertificateAuthority, uninstallCertificateAuthority, -} from "@jsenv/https-local" -import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs" +} from "@jsenv/https-local"; +import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs"; await uninstallCertificateAuthority({ logLevel: "warn", -}) +}); const firstCallReturnValue = await installCertificateAuthority({ logLevel: "warn", -}) +}); const loggerForTest = createLoggerForTest({ // logLevel: "info", // forwardToConsole: true, -}) +}); const secondCallReturnValue = await installCertificateAuthority({ logger: loggerForTest, -}) +}); const secondCallLogs = loggerForTest.getLogs({ info: true, warn: true, error: true, -}) +}); const sameCertificate = - firstCallReturnValue.rootCertificate === secondCallReturnValue.rootCertificate + firstCallReturnValue.rootCertificate === + secondCallReturnValue.rootCertificate; const actual = { sameCertificate, secondCallReturnValue, secondCallLogs, -} +}; const expected = { sameCertificate: true, secondCallReturnValue: { @@ -133,5 +134,5 @@ const expected = { warns: [], errors: [], }, -} -assert({ actual, expected }) +}; +assert({ actual, expected }); diff --git a/packages/independent/https-local/tests/authority_certificate/root_cert_about_to_expire.test.mjs b/packages/independent/https-local/tests/authority_certificate/root_cert_about_to_expire.test.mjs index 5300c21051..06bd9d0404 100644 --- a/packages/independent/https-local/tests/authority_certificate/root_cert_about_to_expire.test.mjs +++ b/packages/independent/https-local/tests/authority_certificate/root_cert_about_to_expire.test.mjs @@ -1,38 +1,38 @@ -import { assert } from "@jsenv/assert" -import { UNICODE } from "@jsenv/log" +import { assert } from "@jsenv/assert"; +import { UNICODE } from "@jsenv/log"; import { installCertificateAuthority, uninstallCertificateAuthority, -} from "@jsenv/https-local" -import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs" +} from "@jsenv/https-local"; +import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs"; await uninstallCertificateAuthority({ logLevel: "warn", -}) +}); await installCertificateAuthority({ logLevel: "warn", certificateValidityDurationInMs: 6000, -}) +}); await new Promise((resolve) => { - setTimeout(resolve, 1500) -}) + setTimeout(resolve, 1500); +}); const loggerForSecondCall = createLoggerForTest({ // forwardToConsole: true, -}) +}); const { rootCertificateFilePath } = await installCertificateAuthority({ logger: loggerForSecondCall, certificateValidityDurationInMs: 6000, aboutToExpireRatio: 0.95, -}) +}); { const { infos, warns, errors } = loggerForSecondCall.getLogs({ info: true, warn: true, error: true, - }) - const actual = { infos, warns, errors } + }); + const actual = { infos, warns, errors }; const expected = { infos: [ `${UNICODE.OK} authority root certificate found in filesystem`, @@ -58,6 +58,6 @@ const { rootCertificateFilePath } = await installCertificateAuthority({ ], warns: [], errors: [], - } - assert({ actual, expected }) + }; + assert({ actual, expected }); } diff --git a/packages/independent/https-local/tests/authority_certificate/root_cert_expired.test.mjs b/packages/independent/https-local/tests/authority_certificate/root_cert_expired.test.mjs index 3df946265b..f21417d26b 100644 --- a/packages/independent/https-local/tests/authority_certificate/root_cert_expired.test.mjs +++ b/packages/independent/https-local/tests/authority_certificate/root_cert_expired.test.mjs @@ -1,37 +1,37 @@ -import { assert } from "@jsenv/assert" -import { UNICODE } from "@jsenv/log" +import { assert } from "@jsenv/assert"; +import { UNICODE } from "@jsenv/log"; import { installCertificateAuthority, uninstallCertificateAuthority, -} from "@jsenv/https-local" -import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs" +} from "@jsenv/https-local"; +import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs"; await uninstallCertificateAuthority({ logLevel: "warn", -}) +}); await installCertificateAuthority({ logLevel: "warn", certificateValidityDurationInMs: 1000, -}) +}); await new Promise((resolve) => { - setTimeout(resolve, 2500) -}) + setTimeout(resolve, 2500); +}); const loggerForSecondCall = createLoggerForTest({ // forwardToConsole: true, -}) +}); const { rootCertificateFilePath } = await installCertificateAuthority({ logger: loggerForSecondCall, certificateValidityDurationInMs: 1000, -}) +}); { const { infos, warns, errors } = loggerForSecondCall.getLogs({ info: true, warn: true, error: true, - }) - const actual = { infos, warns, errors } + }); + const actual = { infos, warns, errors }; const expected = { infos: [ `${UNICODE.OK} authority root certificate found in filesystem`, @@ -57,6 +57,6 @@ const { rootCertificateFilePath } = await installCertificateAuthority({ ], warns: [], errors: [], - } - assert({ actual, expected }) + }; + assert({ actual, expected }); } diff --git a/packages/independent/https-local/tests/authority_certificate/try_to_trust.test_manual.mjs b/packages/independent/https-local/tests/authority_certificate/try_to_trust.test_manual.mjs index b2de1e6b98..08cf8c14c2 100644 --- a/packages/independent/https-local/tests/authority_certificate/try_to_trust.test_manual.mjs +++ b/packages/independent/https-local/tests/authority_certificate/try_to_trust.test_manual.mjs @@ -1,24 +1,24 @@ // https://github.com/nccgroup/wssip/blob/56d0d2c15a7c0fd4c99be445ec8d6c16571e81a0/lib/mitmengine.js#L450 -import { writeSymbolicLink } from "@jsenv/filesystem" +import { writeSymbolicLink } from "@jsenv/filesystem"; import { installCertificateAuthority, - uninstallCertificateAuthority, requestCertificate, -} from "@jsenv/https-local" -import { startServerForTest } from "@jsenv/https-local/tests/test_helpers.mjs" + uninstallCertificateAuthority, +} from "@jsenv/https-local"; +import { startServerForTest } from "@jsenv/https-local/tests/test_helpers.mjs"; await uninstallCertificateAuthority({ tryToUntrust: true, -}) +}); await installCertificateAuthority({ tryToTrust: true, -}) +}); const { certificate, privateKey, rootCertificateFilePath } = requestCertificate( { altNames: ["localhost", "*.localhost"], }, -) +); if (process.platform !== "win32") { // not on windows because symlink requires admin rights @@ -28,7 +28,7 @@ if (process.platform !== "win32") { type: "file", allowUseless: true, allowOverwrite: true, - }) + }); } const serverOrigin = await startServerForTest({ @@ -36,5 +36,5 @@ const serverOrigin = await startServerForTest({ certificate, privateKey, keepAlive: true, -}) -console.log(`Open ${serverOrigin} in a browser`) +}); +console.log(`Open ${serverOrigin} in a browser`); diff --git a/packages/independent/https-local/tests/hosts_file/try_to_update_hosts.test.mjs b/packages/independent/https-local/tests/hosts_file/try_to_update_hosts.test.mjs index 15b4a1bc35..4063fb3a59 100644 --- a/packages/independent/https-local/tests/hosts_file/try_to_update_hosts.test.mjs +++ b/packages/independent/https-local/tests/hosts_file/try_to_update_hosts.test.mjs @@ -1,20 +1,20 @@ -import { fileURLToPath } from "node:url" -import { assert } from "@jsenv/assert" -import { readFile, writeFile, removeEntry } from "@jsenv/filesystem" -import { UNICODE } from "@jsenv/log" +import { assert } from "@jsenv/assert"; +import { readFile, removeEntry, writeFile } from "@jsenv/filesystem"; +import { UNICODE } from "@jsenv/log"; +import { fileURLToPath } from "node:url"; -import { verifyHostsFile } from "@jsenv/https-local" -import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs" +import { verifyHostsFile } from "@jsenv/https-local"; +import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs"; -const hostFileUrl = new URL("./hosts", import.meta.url) -const hostsFilePath = fileURLToPath(hostFileUrl) +const hostFileUrl = new URL("./hosts", import.meta.url); +const hostsFilePath = fileURLToPath(hostFileUrl); // 1 ip mapping missing { - await writeFile(hostFileUrl, `127.0.0.1 localhost`) + await writeFile(hostFileUrl, `127.0.0.1 localhost`); const loggerForTest = createLoggerForTest({ // forwardToConsole: true, - }) + }); await verifyHostsFile({ logger: loggerForTest, ipMappings: { @@ -22,20 +22,20 @@ const hostsFilePath = fileURLToPath(hostFileUrl) }, tryToUpdateHostsFile: true, hostsFilePath, - }) + }); const { infos, warns, errors } = loggerForTest.getLogs({ info: true, warn: true, error: true, - }) - const hostsFileContent = await readFile(hostsFilePath, { as: "string" }) + }); + const hostsFileContent = await readFile(hostsFilePath, { as: "string" }); const actual = { hostsFileContent, infos, warns, errors, - } + }; const expected = { hostsFileContent: process.platform === "win32" @@ -52,13 +52,13 @@ const hostsFilePath = fileURLToPath(hostFileUrl) ], warns: [], errors: [], - } - assert({ actual, expected }) + }; + assert({ actual, expected }); } // 2 ip mapping missing { - await writeFile(hostFileUrl, ``) + await writeFile(hostFileUrl, ``); await verifyHostsFile({ logLevel: "warn", ipMappings: { @@ -67,22 +67,22 @@ const hostsFilePath = fileURLToPath(hostFileUrl) }, tryToUpdateHostsFile: true, hostsFilePath, - }) - const hostsFileContent = await readFile(hostsFilePath, { as: "string" }) - const actual = hostsFileContent + }); + const hostsFileContent = await readFile(hostsFilePath, { as: "string" }); + const actual = hostsFileContent; const expected = process.platform === "win32" ? `127.0.0.1 localhost jsenv\r\n192.168.1.1 toto\r\n` - : `127.0.0.1 localhost jsenv\n192.168.1.1 toto\n` - assert({ actual, expected }) + : `127.0.0.1 localhost jsenv\n192.168.1.1 toto\n`; + assert({ actual, expected }); } // all hostname there { const loggerForTest = createLoggerForTest({ // forwardToConsole: true, - }) - await writeFile(hostFileUrl, `127.0.0.1 jsenv`) + }); + await writeFile(hostFileUrl, `127.0.0.1 jsenv`); await verifyHostsFile({ logger: loggerForTest, ipMappings: { @@ -90,18 +90,18 @@ const hostsFilePath = fileURLToPath(hostFileUrl) }, tryToUpdateHostsFile: true, hostsFilePath, - }) + }); const { infos, warns, errors } = loggerForTest.getLogs({ info: true, warn: true, error: true, - }) + }); const actual = { infos, warns, errors, - } + }; const expected = { infos: [ `Check hosts file content...`, @@ -109,8 +109,8 @@ const hostsFilePath = fileURLToPath(hostFileUrl) ], warns: [], errors: [], - } - assert({ actual, expected }) + }; + assert({ actual, expected }); } -await removeEntry(hostFileUrl) +await removeEntry(hostFileUrl); diff --git a/packages/independent/https-local/tests/server_certificate/not_trusted_browsers.test.mjs b/packages/independent/https-local/tests/server_certificate/not_trusted_browsers.test.mjs index 779d947627..1f46c7386d 100644 --- a/packages/independent/https-local/tests/server_certificate/not_trusted_browsers.test.mjs +++ b/packages/independent/https-local/tests/server_certificate/not_trusted_browsers.test.mjs @@ -1,88 +1,88 @@ -import { assert } from "@jsenv/assert" +import { assert } from "@jsenv/assert"; import { installCertificateAuthority, - uninstallCertificateAuthority, requestCertificate, -} from "@jsenv/https-local" + uninstallCertificateAuthority, +} from "@jsenv/https-local"; import { - startServerForTest, launchChromium, launchFirefox, launchWebkit, requestServerUsingBrowser, -} from "@jsenv/https-local/tests/test_helpers.mjs" + startServerForTest, +} from "@jsenv/https-local/tests/test_helpers.mjs"; await uninstallCertificateAuthority({ logLevel: "warn", -}) +}); await installCertificateAuthority({ logLevel: "warn", -}) +}); const { certificate, privateKey } = requestCertificate({ logLevel: "warn", -}) +}); const serverOrigin = await startServerForTest({ certificate, privateKey, -}) +}); { - const browser = await launchChromium() + const browser = await launchChromium(); try { await requestServerUsingBrowser({ serverOrigin, browser, - }) - throw new Error("should throw") + }); + throw new Error("should throw"); } catch (e) { - const actual = e.errorText - const expected = "net::ERR_CERT_AUTHORITY_INVALID" - assert({ actual, expected }) + const actual = e.errorText; + const expected = "net::ERR_CERT_AUTHORITY_INVALID"; + assert({ actual, expected }); } finally { - browser.close() + browser.close(); } } // disabled on windows for now // there is a little something to change in the expected error to make it pass if (process.platform !== "win32") { - const browser = await launchFirefox() + const browser = await launchFirefox(); try { await requestServerUsingBrowser({ serverOrigin, browser, - }) - throw new Error("should throw") + }); + throw new Error("should throw"); } catch (e) { - const actual = e.errorText - const expected = "SEC_ERROR_UNKNOWN_ISSUER" - assert({ actual, expected, context: { browser: "firefox", error: e } }) + const actual = e.errorText; + const expected = "SEC_ERROR_UNKNOWN_ISSUER"; + assert({ actual, expected, context: { browser: "firefox", error: e } }); } finally { - browser.close() + browser.close(); } } // if (process.platform === "darwin") { { - const browser = await launchWebkit() + const browser = await launchWebkit(); try { await requestServerUsingBrowser({ serverOrigin, browser, - }) - throw new Error("should throw") + }); + throw new Error("should throw"); } catch (e) { - const actual = e.errorText + const actual = e.errorText; const expected = { win32: "SSL peer certificate or SSH remote key was not OK", linux: "Unacceptable TLS certificate", darwin: "The certificate for this server is invalid. You might be connecting to a server that is pretending to be “localhost” which could put your confidential information at risk.", - }[process.platform] - assert({ actual, expected }) + }[process.platform]; + assert({ actual, expected }); } finally { - browser.close() + browser.close(); } } diff --git a/packages/independent/https-local/tests/server_certificate/not_trusted_browsers.test_manual.mjs b/packages/independent/https-local/tests/server_certificate/not_trusted_browsers.test_manual.mjs index 941724307c..701b89f073 100644 --- a/packages/independent/https-local/tests/server_certificate/not_trusted_browsers.test_manual.mjs +++ b/packages/independent/https-local/tests/server_certificate/not_trusted_browsers.test_manual.mjs @@ -1,8 +1,8 @@ -import { requestCertificate } from "@jsenv/https-local" +import { requestCertificate } from "@jsenv/https-local"; import { // resetAllCertificateFiles, startServerForTest, -} from "@jsenv/https-local/tests/test_helpers.mjs" +} from "@jsenv/https-local/tests/test_helpers.mjs"; // await resetAllCertificateFiles() const { certificate, privateKey } = requestCertificate({ @@ -13,11 +13,11 @@ const { certificate, privateKey } = requestCertificate({ ), rootCertificateOrganizationName: "jsenv", rootCertificateOrganizationalUnitName: "https localhost", -}) +}); const serverOrigin = await startServerForTest({ certificate, privateKey, keepAlive: true, port: 5000, -}) -console.log(`Open ${serverOrigin} in a browser`) +}); +console.log(`Open ${serverOrigin} in a browser`); diff --git a/packages/independent/https-local/tests/server_certificate/request_server_certificate.test.mjs b/packages/independent/https-local/tests/server_certificate/request_server_certificate.test.mjs index 95b9e8c97a..133de4cef0 100644 --- a/packages/independent/https-local/tests/server_certificate/request_server_certificate.test.mjs +++ b/packages/independent/https-local/tests/server_certificate/request_server_certificate.test.mjs @@ -1,27 +1,27 @@ -import { assert } from "@jsenv/assert" -import { UNICODE } from "@jsenv/log" +import { assert } from "@jsenv/assert"; +import { UNICODE } from "@jsenv/log"; import { installCertificateAuthority, - uninstallCertificateAuthority, requestCertificate, -} from "@jsenv/https-local" -import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs" + uninstallCertificateAuthority, +} from "@jsenv/https-local"; +import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs"; const loggerDuringTest = createLoggerForTest({ // forwardToConsole: true, -}) +}); await uninstallCertificateAuthority({ logLevel: "warn", -}) +}); await installCertificateAuthority({ logLevel: "warn", -}) +}); const returnValue = await requestCertificate({ // logLevel: "warn", logger: loggerDuringTest, -}) +}); { const { debugs, infos, warns, errors } = loggerDuringTest.getLogs({ @@ -29,14 +29,14 @@ const returnValue = await requestCertificate({ info: true, warn: true, error: true, - }) + }); const actual = { debugs, infos, warns, errors, returnValue, - } + }; const expected = { debugs: [ `Restoring certificate authority from filesystem...`, @@ -52,6 +52,6 @@ const returnValue = await requestCertificate({ privateKey: assert.any(String), rootCertificateFilePath: assert.any(String), }, - } - assert({ actual, expected }) + }; + assert({ actual, expected }); } diff --git a/packages/independent/https-local/tests/test_helpers.mjs b/packages/independent/https-local/tests/test_helpers.mjs index cbb156ec26..530a7f784a 100644 --- a/packages/independent/https-local/tests/test_helpers.mjs +++ b/packages/independent/https-local/tests/test_helpers.mjs @@ -1,7 +1,7 @@ -import { createServer } from "node:https" -import { createRequire } from "node:module" +import { createServer } from "node:https"; +import { createRequire } from "node:module"; -const require = createRequire(import.meta.url) +const require = createRequire(import.meta.url); /* * Logs are an important part of this package @@ -9,34 +9,34 @@ const require = createRequire(import.meta.url) * and their content. This file provide a logger capable to do that. */ export const createLoggerForTest = ({ forwardToConsole = false } = {}) => { - const debugs = [] - const infos = [] - const warns = [] - const errors = [] + const debugs = []; + const infos = []; + const warns = []; + const errors = []; return { debug: (...args) => { - debugs.push(args.join("")) + debugs.push(args.join("")); if (forwardToConsole) { - console.debug(...args) + console.debug(...args); } }, info: (...args) => { - infos.push(args.join("")) + infos.push(args.join("")); if (forwardToConsole) { - console.info(...args) + console.info(...args); } }, warn: (...args) => { - warns.push(args.join("")) + warns.push(args.join("")); if (forwardToConsole) { - console.warn(...args) + console.warn(...args); } }, error: (...args) => { - errors.push(args.join("")) + errors.push(args.join("")); if (forwardToConsole) { - console.error(...args) + console.error(...args); } }, @@ -53,10 +53,10 @@ export const createLoggerForTest = ({ forwardToConsole = false } = {}) => { ...(info ? { infos } : {}), ...(warn ? { warns } : {}), ...(error ? { errors } : {}), - } + }; }, - } -} + }; +}; export const startServerForTest = async ({ certificate, @@ -70,55 +70,55 @@ export const startServerForTest = async ({ key: privateKey, }, (request, response) => { - const body = "Hello world" + const body = "Hello world"; response.writeHead(200, { "content-type": "text/plain", "content-length": Buffer.byteLength(body), - }) - response.write(body) - response.end() + }); + response.write(body); + response.end(); }, - ) + ); if (!keepAlive) { - server.unref() + server.unref(); } const serverPort = await new Promise((resolve) => { server.on("listening", () => { // in case port is 0 (randomly assign an available port) // https://nodejs.org/api/net.html#net_server_listen_port_host_backlog_callback - resolve(server.address().port) - }) - server.listen(port) - }) - return `https://localhost:${serverPort}` -} + resolve(server.address().port); + }); + server.listen(port); + }); + return `https://localhost:${serverPort}`; +}; export const launchChromium = () => { - const { chromium } = require("playwright") - return chromium.launch() -} + const { chromium } = require("playwright"); + return chromium.launch(); +}; export const launchFirefox = () => { - const { firefox } = require("playwright") - return firefox.launch() -} + const { firefox } = require("playwright"); + return firefox.launch(); +}; export const launchWebkit = () => { - const { webkit } = require("playwright") - return webkit.launch() -} + const { webkit } = require("playwright"); + return webkit.launch(); +}; export const requestServerUsingBrowser = async ({ serverOrigin, browser }) => { - const page = await browser.newPage() + const page = await browser.newPage(); return new Promise(async (resolve, reject) => { page.on("requestfailed", (request) => { - reject(request.failure()) - }) + reject(request.failure()); + }); page.on("load", () => { - setTimeout(resolve, 400) // this time is required for firefox to trigger "requestfailed" - }) + setTimeout(resolve, 400); // this time is required for firefox to trigger "requestfailed" + }); page.goto(serverOrigin).catch((e) => { // chrome @@ -126,17 +126,17 @@ export const requestServerUsingBrowser = async ({ serverOrigin, browser }) => { e.message.includes("ERR_CERT_INVALID") || e.message.includes("ERR_CERT_AUTHORITY_INVALID") ) { - return + return; } // firefox if (e.message.includes("SEC_ERROR_UNKNOWN_ISSUER")) { - return + return; } // webkit if (e.message.includes("The certificate for this server is invalid.")) { - return + return; } - throw e - }) - }) -} + throw e; + }); + }); +}; diff --git a/packages/independent/workflow/file-size-impact/bin/filesize.mjs b/packages/independent/workflow/file-size-impact/bin/filesize.mjs index 2fc56c3f5c..f41c570ab7 100755 --- a/packages/independent/workflow/file-size-impact/bin/filesize.mjs +++ b/packages/independent/workflow/file-size-impact/bin/filesize.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node -import { pathToFileURL } from "node:url"; import { generateFileSizeReport } from "@jsenv/file-size-impact"; +import { pathToFileURL } from "node:url"; const cwdUrl = `${pathToFileURL(process.cwd())}/`; const argv = process.argv.slice(2); diff --git a/packages/independent/workflow/file-size-impact/src/gzip_transformation.js b/packages/independent/workflow/file-size-impact/src/gzip_transformation.js index 0e49a8e191..f21f25dac1 100644 --- a/packages/independent/workflow/file-size-impact/src/gzip_transformation.js +++ b/packages/independent/workflow/file-size-impact/src/gzip_transformation.js @@ -1,4 +1,4 @@ -import { gzip, constants } from "node:zlib"; +import { constants, gzip } from "node:zlib"; export const name = "gzip"; diff --git a/packages/independent/workflow/file-size-impact/src/internal/apply_tracking_config.js b/packages/independent/workflow/file-size-impact/src/internal/apply_tracking_config.js index 12a484d833..a44846eb47 100644 --- a/packages/independent/workflow/file-size-impact/src/internal/apply_tracking_config.js +++ b/packages/independent/workflow/file-size-impact/src/internal/apply_tracking_config.js @@ -1,5 +1,5 @@ -import { fileURLToPath } from "node:url"; import { collectDirectoryMatchReport } from "@jsenv/filesystem"; +import { fileURLToPath } from "node:url"; export const applyTrackingConfig = async ( trackingConfig, diff --git a/packages/independent/workflow/file-size-impact/tests/comment/comment.test.mjs b/packages/independent/workflow/file-size-impact/tests/comment/comment.test.mjs index 7b58d35d16..877fe7b25a 100644 --- a/packages/independent/workflow/file-size-impact/tests/comment/comment.test.mjs +++ b/packages/independent/workflow/file-size-impact/tests/comment/comment.test.mjs @@ -9,8 +9,8 @@ The goal is to force user to regenerate comment_snapshot.md and ensure it looks */ -import { readFileSync } from "node:fs"; import { assert } from "@jsenv/assert"; +import { readFileSync } from "node:fs"; const commentSnapshotFileUrl = new URL( "./comment_snapshot.md", diff --git a/packages/independent/workflow/lighthouse-impact/tests/comment/comment.test.mjs b/packages/independent/workflow/lighthouse-impact/tests/comment/comment.test.mjs index 39ab842f0b..4575b013d6 100644 --- a/packages/independent/workflow/lighthouse-impact/tests/comment/comment.test.mjs +++ b/packages/independent/workflow/lighthouse-impact/tests/comment/comment.test.mjs @@ -10,8 +10,8 @@ before commiting it. */ -import { readFileSync } from "node:fs"; import { assert } from "@jsenv/assert"; +import { readFileSync } from "node:fs"; const commentSnapshotFileUrl = new URL( "./comment_snapshot.md", diff --git a/packages/independent/workflow/monorepo/src/internal/collect_workspace_packages.js b/packages/independent/workflow/monorepo/src/internal/collect_workspace_packages.js index 2a333d7a62..4d2089dae8 100644 --- a/packages/independent/workflow/monorepo/src/internal/collect_workspace_packages.js +++ b/packages/independent/workflow/monorepo/src/internal/collect_workspace_packages.js @@ -1,6 +1,6 @@ -import { readFileSync, writeFileSync } from "node:fs"; -import { urlToRelativeUrl } from "@jsenv/urls"; import { listFilesMatching } from "@jsenv/filesystem"; +import { urlToRelativeUrl } from "@jsenv/urls"; +import { readFileSync, writeFileSync } from "node:fs"; export const collectWorkspacePackages = async ({ directoryUrl }) => { const workspacePackages = {}; diff --git a/packages/independent/workflow/monorepo/src/main.js b/packages/independent/workflow/monorepo/src/main.js index d9ddfa609c..b039a34c98 100644 --- a/packages/independent/workflow/monorepo/src/main.js +++ b/packages/independent/workflow/monorepo/src/main.js @@ -1,3 +1,3 @@ -export { upgradeExternalVersions } from "./upgrade_external_versions.js"; -export { syncPackagesVersions } from "./sync_packages_versions.js"; export { publishPackages } from "./publish_packages.js"; +export { syncPackagesVersions } from "./sync_packages_versions.js"; +export { upgradeExternalVersions } from "./upgrade_external_versions.js"; diff --git a/packages/independent/workflow/monorepo/src/publish_packages.js b/packages/independent/workflow/monorepo/src/publish_packages.js index 86ee073875..4e56266c6a 100644 --- a/packages/independent/workflow/monorepo/src/publish_packages.js +++ b/packages/independent/workflow/monorepo/src/publish_packages.js @@ -1,11 +1,11 @@ import { createLogger, UNICODE } from "@jsenv/humanize"; import { publish } from "@jsenv/package-publish/src/internal/publish.js"; import { collectWorkspacePackages } from "./internal/collect_workspace_packages.js"; -import { fetchWorkspaceLatests } from "./internal/fetch_workspace_latests.js"; import { compareTwoPackageVersions, VERSION_COMPARE_RESULTS, } from "./internal/compare_two_package_versions.js"; +import { fetchWorkspaceLatests } from "./internal/fetch_workspace_latests.js"; export const publishPackages = async ({ directoryUrl }) => { const workspacePackages = await collectWorkspacePackages({ directoryUrl }); diff --git a/packages/independent/workflow/package-publish/src/internal/read_project_package.js b/packages/independent/workflow/package-publish/src/internal/read_project_package.js index dc05409650..85e0986357 100644 --- a/packages/independent/workflow/package-publish/src/internal/read_project_package.js +++ b/packages/independent/workflow/package-publish/src/internal/read_project_package.js @@ -1,5 +1,5 @@ -import { fileURLToPath } from "node:url"; import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; export const readProjectPackage = ({ rootDirectoryUrl }) => { const packageFileUrlObject = new URL("./package.json", rootDirectoryUrl); diff --git a/packages/independent/workflow/performance-impact/src/internal/format_metric_value.js b/packages/independent/workflow/performance-impact/src/internal/format_metric_value.js index c339075254..2a15391b01 100644 --- a/packages/independent/workflow/performance-impact/src/internal/format_metric_value.js +++ b/packages/independent/workflow/performance-impact/src/internal/format_metric_value.js @@ -1,4 +1,4 @@ -import { humanizeFileSize, humanizeDuration } from "@jsenv/humanize"; +import { humanizeDuration, humanizeFileSize } from "@jsenv/humanize"; export const formatMetricValue = ({ value, unit }) => { return formatters[unit](value); diff --git a/packages/independent/workflow/performance-impact/tests/comment_snapshot/comment_snapshot.test.mjs b/packages/independent/workflow/performance-impact/tests/comment_snapshot/comment_snapshot.test.mjs index 39ab842f0b..4575b013d6 100644 --- a/packages/independent/workflow/performance-impact/tests/comment_snapshot/comment_snapshot.test.mjs +++ b/packages/independent/workflow/performance-impact/tests/comment_snapshot/comment_snapshot.test.mjs @@ -10,8 +10,8 @@ before commiting it. */ -import { readFileSync } from "node:fs"; import { assert } from "@jsenv/assert"; +import { readFileSync } from "node:fs"; const commentSnapshotFileUrl = new URL( "./comment_snapshot.md", From 6f738456e5765c536e2ef4b8b31bfed912d309be Mon Sep 17 00:00:00 2001 From: dmail Date: Wed, 14 Aug 2024 10:13:29 +0200 Subject: [PATCH 3/7] update deps --- packages/independent/https-local/package.json | 8 ++++---- .../src/internal/certificate_data_converter.js | 2 +- .../install_authority_first_call.test.mjs | 3 +-- .../install_authority_reuse.test.mjs | 3 +-- .../root_cert_about_to_expire.test.mjs | 3 +-- .../authority_certificate/root_cert_expired.test.mjs | 3 +-- .../tests/hosts_file/try_to_update_hosts.test.mjs | 5 ++--- .../request_server_certificate.test.mjs | 3 +-- 8 files changed, 12 insertions(+), 18 deletions(-) diff --git a/packages/independent/https-local/package.json b/packages/independent/https-local/package.json index 45afe8a043..c5192f4b8a 100644 --- a/packages/independent/https-local/package.json +++ b/packages/independent/https-local/package.json @@ -40,12 +40,12 @@ "hosts:ensure-localhost-mappings": "node ./scripts/hosts/ensure_localhost_mappings.mjs" }, "dependencies": { - "@jsenv/filesystem": "4.1.9", - "@jsenv/log": "3.3.2", - "@jsenv/urls": "1.2.8", + "@jsenv/filesystem": "4.10.2", + "@jsenv/humanize": "1.2.8", + "@jsenv/urls": "2.5.2", "command-exists": "1.2.9", "node-forge": "1.3.1", "sudo-prompt": "9.2.1", - "which": "3.0.0" + "which": "4.0.0" } } diff --git a/packages/independent/https-local/src/internal/certificate_data_converter.js b/packages/independent/https-local/src/internal/certificate_data_converter.js index 574a90cd4e..c75cf583b6 100644 --- a/packages/independent/https-local/src/internal/certificate_data_converter.js +++ b/packages/independent/https-local/src/internal/certificate_data_converter.js @@ -29,7 +29,7 @@ const isUrl = (value) => { // eslint-disable-next-line no-new new URL(value); return true; - } catch (e) { + } catch { return false; } }; diff --git a/packages/independent/https-local/tests/authority_certificate/install_authority_first_call.test.mjs b/packages/independent/https-local/tests/authority_certificate/install_authority_first_call.test.mjs index 67f8f856d8..14621e45a4 100644 --- a/packages/independent/https-local/tests/authority_certificate/install_authority_first_call.test.mjs +++ b/packages/independent/https-local/tests/authority_certificate/install_authority_first_call.test.mjs @@ -1,11 +1,10 @@ import { assert } from "@jsenv/assert"; -import { UNICODE } from "@jsenv/log"; - import { installCertificateAuthority, uninstallCertificateAuthority, } from "@jsenv/https-local"; import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs"; +import { UNICODE } from "@jsenv/humanize"; await uninstallCertificateAuthority({ logLevel: "warn", diff --git a/packages/independent/https-local/tests/authority_certificate/install_authority_reuse.test.mjs b/packages/independent/https-local/tests/authority_certificate/install_authority_reuse.test.mjs index 92d87beef3..72f88d2c5b 100644 --- a/packages/independent/https-local/tests/authority_certificate/install_authority_reuse.test.mjs +++ b/packages/independent/https-local/tests/authority_certificate/install_authority_reuse.test.mjs @@ -1,11 +1,10 @@ import { assert } from "@jsenv/assert"; -import { UNICODE } from "@jsenv/log"; - import { installCertificateAuthority, uninstallCertificateAuthority, } from "@jsenv/https-local"; import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs"; +import { UNICODE } from "@jsenv/humanize"; await uninstallCertificateAuthority({ logLevel: "warn", diff --git a/packages/independent/https-local/tests/authority_certificate/root_cert_about_to_expire.test.mjs b/packages/independent/https-local/tests/authority_certificate/root_cert_about_to_expire.test.mjs index 06bd9d0404..e66b23358f 100644 --- a/packages/independent/https-local/tests/authority_certificate/root_cert_about_to_expire.test.mjs +++ b/packages/independent/https-local/tests/authority_certificate/root_cert_about_to_expire.test.mjs @@ -1,11 +1,10 @@ import { assert } from "@jsenv/assert"; -import { UNICODE } from "@jsenv/log"; - import { installCertificateAuthority, uninstallCertificateAuthority, } from "@jsenv/https-local"; import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs"; +import { UNICODE } from "@jsenv/humanize"; await uninstallCertificateAuthority({ logLevel: "warn", diff --git a/packages/independent/https-local/tests/authority_certificate/root_cert_expired.test.mjs b/packages/independent/https-local/tests/authority_certificate/root_cert_expired.test.mjs index f21417d26b..4d7af25546 100644 --- a/packages/independent/https-local/tests/authority_certificate/root_cert_expired.test.mjs +++ b/packages/independent/https-local/tests/authority_certificate/root_cert_expired.test.mjs @@ -1,11 +1,10 @@ import { assert } from "@jsenv/assert"; -import { UNICODE } from "@jsenv/log"; - import { installCertificateAuthority, uninstallCertificateAuthority, } from "@jsenv/https-local"; import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs"; +import { UNICODE } from "@jsenv/humanize"; await uninstallCertificateAuthority({ logLevel: "warn", diff --git a/packages/independent/https-local/tests/hosts_file/try_to_update_hosts.test.mjs b/packages/independent/https-local/tests/hosts_file/try_to_update_hosts.test.mjs index 4063fb3a59..1b2af484df 100644 --- a/packages/independent/https-local/tests/hosts_file/try_to_update_hosts.test.mjs +++ b/packages/independent/https-local/tests/hosts_file/try_to_update_hosts.test.mjs @@ -1,10 +1,9 @@ import { assert } from "@jsenv/assert"; import { readFile, removeEntry, writeFile } from "@jsenv/filesystem"; -import { UNICODE } from "@jsenv/log"; -import { fileURLToPath } from "node:url"; - import { verifyHostsFile } from "@jsenv/https-local"; import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs"; +import { UNICODE } from "@jsenv/humanize"; +import { fileURLToPath } from "node:url"; const hostFileUrl = new URL("./hosts", import.meta.url); const hostsFilePath = fileURLToPath(hostFileUrl); diff --git a/packages/independent/https-local/tests/server_certificate/request_server_certificate.test.mjs b/packages/independent/https-local/tests/server_certificate/request_server_certificate.test.mjs index 133de4cef0..7d4e4a2056 100644 --- a/packages/independent/https-local/tests/server_certificate/request_server_certificate.test.mjs +++ b/packages/independent/https-local/tests/server_certificate/request_server_certificate.test.mjs @@ -1,12 +1,11 @@ import { assert } from "@jsenv/assert"; -import { UNICODE } from "@jsenv/log"; - import { installCertificateAuthority, requestCertificate, uninstallCertificateAuthority, } from "@jsenv/https-local"; import { createLoggerForTest } from "@jsenv/https-local/tests/test_helpers.mjs"; +import { UNICODE } from "@jsenv/humanize"; const loggerDuringTest = createLoggerForTest({ // forwardToConsole: true, From 1f765e5c1d4456c6d4a70b836c09172dad3947ac Mon Sep 17 00:00:00 2001 From: dmail Date: Wed, 14 Aug 2024 10:27:02 +0200 Subject: [PATCH 4/7] update doc --- packages/independent/https-local/README.md | 68 +++++-------------- packages/independent/https-local/package.json | 1 + .../https-local/src/https_local_cli.mjs | 56 +++++++++++++++ 3 files changed, 74 insertions(+), 51 deletions(-) create mode 100755 packages/independent/https-local/src/https_local_cli.mjs diff --git a/packages/independent/https-local/README.md b/packages/independent/https-local/README.md index abb5b0e428..92493737e5 100644 --- a/packages/independent/https-local/README.md +++ b/packages/independent/https-local/README.md @@ -1,4 +1,6 @@ -# https local [![npm package](https://img.shields.io/npm/v/@jsenv/https-local.svg?logo=npm&label=package)](https://www.npmjs.com/package/@jsenv/https-local) +# https local + +[![npm package](https://img.shields.io/npm/v/@jsenv/https-local.svg?logo=npm&label=package)](https://www.npmjs.com/package/@jsenv/https-local) A programmatic way to generate locally trusted certificates. @@ -8,68 +10,32 @@ Works on mac, linux and windows. # How to use -1 - Install _@jsenv/https-local_ +The following steps can be taken to start a local server in https. -```console -npm install --save-dev @jsenv/https-local -``` +1. Install the root certificate using https-local +2. Request certificate for your server +3. Start that server -2 - Create _install_certificate_authority.mjs_ - -```js -/* - * This file needs to be executed once. - * After that the root certificate is valid for 20 years. - * Re-executing this file will log the current root certificate validity and trust status. - * Re-executing this file 20 years later would reinstall a root certificate and re-trust it. - * - * Read more in https://github.com/jsenv/https-local#installCertificateAuthority - */ - -import { - installCertificateAuthority, - verifyHostsFile, -} from "@jsenv/https-local"; +## 1. Install the root certificate -await installCertificateAuthority({ - tryToTrust: true, - NSSDynamicInstall: true, -}); -await verifyHostsFile({ - ipMappings: { - "127.0.0.1": ["localhost"], - }, - tryToUpdatesHostsFile: true, -}); +```console +npx @jsenv/https-local install --trust ``` -3 - Run with node +This will install a root certificate valid for 20 years. -```console -node ./install_certificate_authority.mjs -``` +- Re-executing this command will log the current root certificate validity and trust status. +- Re-executing this command 20 years later would reinstall a root certificate and re-trust it -4 - Create _start_dev_server.mjs_ +## 2. Request certificate for your server -```js -/* - * This file uses "@jsenv/https-local" to obtain a certificate used to start a server in https. - * The certificate is valid for 1 year (396 days) and is issued by a certificate authority trusted on this machine. - * If the certificate authority was not installed before executing this file, an error is thrown - * explaining that certificate authority must be installed first. - * - * To install the certificate authority, you can use the following command - * - * > node ./install_certificate_authority.mjs - * - * Read more in https://github.com/jsenv/https-local#requestCertificate - */ +_start_dev_server.mjs_ +```js import { createServer } from "node:https"; import { requestCertificate } from "@jsenv/https-local"; const { certificate, privateKey } = requestCertificate(); - const server = createServer( { cert: certificate, @@ -89,7 +55,7 @@ server.listen(8080); console.log(`Server listening at https://local.example:8080`); ``` -5 - Start server with node +## 3. Start the server ```console node ./start_dev_server.mjs diff --git a/packages/independent/https-local/package.json b/packages/independent/https-local/package.json index c5192f4b8a..7ad0df1551 100644 --- a/packages/independent/https-local/package.json +++ b/packages/independent/https-local/package.json @@ -21,6 +21,7 @@ }, "./*": "./*" }, + "bin": "./src/https_local_cli.mjs", "main": "./src/main.js", "files": [ "/src/" diff --git a/packages/independent/https-local/src/https_local_cli.mjs b/packages/independent/https-local/src/https_local_cli.mjs new file mode 100755 index 0000000000..40b40c5e34 --- /dev/null +++ b/packages/independent/https-local/src/https_local_cli.mjs @@ -0,0 +1,56 @@ +#!/usr/bin/env node + +import { + installCertificateAuthority, + uninstallCertificateAuthority, +} from "@jsenv/https-local"; +import { parseArgs } from "node:util"; + +const options = { + help: { + type: "boolean", + }, + trust: { + type: "boolean", + }, +}; +const { values, positionals } = parseArgs({ + options, + allowPositionals: true, +}); + +if (values.help || positionals.length === 0) { + console.log(`https-local: Generate https certificates to use on your machine. + +Usage: +npx @jsenv/https-local install --trust +npx @jsenv/https-local uninstall + +https://github.com/jsenv/core/tree/main/packages/independent/https-local + +`); + process.exit(0); +} + +const commandHandlers = { + install: async ({ tryToTrust }) => { + await installCertificateAuthority({ + tryToTrust, + NSSDynamicInstall: tryToTrust, + }); + }, + uninstall: async () => { + await uninstallCertificateAuthority({ + tryToUntrust: true, + }); + }, +}; + +const [command] = positionals; +const commandHandler = commandHandlers[command]; +if (!commandHandler) { + console.error(`Error: unknown command ${command}.`); + process.exit(1); +} + +await commandHandler(values); From 110a4006000d22e3ecae95dc31c8423bb90e991f Mon Sep 17 00:00:00 2001 From: dmail Date: Wed, 14 Aug 2024 10:30:43 +0200 Subject: [PATCH 5/7] add cli to https-local --- packages/independent/https-local/README.md | 256 +++++++++--------- .../https-local/src/https_local_cli.mjs | 3 + 2 files changed, 132 insertions(+), 127 deletions(-) diff --git a/packages/independent/https-local/README.md b/packages/independent/https-local/README.md index 92493737e5..8788c64e8c 100644 --- a/packages/independent/https-local/README.md +++ b/packages/independent/https-local/README.md @@ -77,7 +77,134 @@ In the unlikely scenario where a local server is running for more than a year wi The **authority root certificate** expires after 20 years which is close to the maximum allowed duration. In the very unlikely scenario where you are using the same machine for more than 20 years, re-execute [installCertificateAuthority](#installCertificateAuthority) to update certificate authority then restart your server. -# installCertificateAuthority +# JavaScript API + +## requestCertificate + +_requestCertificate_ function returns a certificate and private key that can be used to start a server in HTTPS. + +```js +import { createServer } from "node:https"; +import { requestCertificate } from "@jsenv/https-local"; + +const { certificate, privateKey } = requestCertificate({ + altNames: ["localhost", "local.example"], +}); +``` + +[installCertificateAuthority](#installCertificateAuthority) must be called before this function. + +## verifyHostsFile + +This function is not mandatory to obtain the https certificates. +But it is useful to programmatically verify ip mappings that are important for your local server are present in hosts file. + +```js +import { verifyHostsFile } from "@jsenv/https-local"; + +await verifyHostsFile({ + ipMappings: { + "127.0.0.1": ["localhost", "local.example"], + }, +}); +``` + +Find below logs written in terminal when this function is executed. + +
+ mac and linux + +```console +> node ./verify_hosts.mjs + +Check hosts file content... +⚠ 1 mapping is missing in hosts file +--- hosts file path --- +/etc/hosts +--- line(s) to add --- +127.0.0.1 localhost local.example +``` + +
+ +
+ windows + +```console +> node ./verify_hosts.mjs + +Check hosts file content... +⚠ 1 mapping is missing in hosts file +--- hosts file path --- +C:\\Windows\\System32\\Drivers\\etc\\hosts +--- line(s) to add --- +127.0.0.1 localhost local.example +``` + +
+ +### Auto update hosts + +It's possible to update hosts file programmatically using _tryToUpdateHostsFile_. + +```js +import { verifyHostsFile } from "@jsenv/https-local"; + +await verifyHostsFile({ + ipMappings: { + "127.0.0.1": ["localhost", "local.example"], + }, + tryToUpdateHostsFile: true, +}); +``` + +
+ mac and linux + +```console +Check hosts file content... +ℹ 1 mapping is missing in hosts file +Adding 1 mapping(s) in hosts file... +❯ echo "127.0.0.1 local.example" | sudo tee -a /etc/hosts +Password: +✔ mappings added to hosts file +``` + +_Second execution logs_ + +```console +> node ./verify_hosts.mjs + +Check hosts file content... +✔ all ip mappings found in hosts file +``` + +
+ +
+ windows + +```console +Check hosts file content... +ℹ 1 mapping is missing in hosts file +Adding 1 mapping(s) in hosts file... +❯ (echo 127.0.0.1 local.example) >> C:\\Windows\\System32\\Drivers\\etc\\hosts +Password: +✔ mappings added to hosts file +``` + +_Second execution logs_ + +```console +> node ./verify_hosts.mjs + +Check hosts file content... +✔ all ip mappings found in hosts file +``` + +
+ +## installCertificateAuthority _installCertificateAuthority_ function generates a certificate authority valid for 20 years. This certificate authority is needed to generate local certificates that will be trusted by the operating system and web browsers. @@ -188,7 +315,7 @@ Check if certificate is trusted by firefox...
-## Auto trust +### Auto trust It's possible to trust root certificate programmatically using _tryToTrust_ @@ -320,128 +447,3 @@ Check if certificate is trusted by firefox... ```
- -# requestCertificate - -_requestCertificate_ function returns a certificate and private key that can be used to start a server in HTTPS. - -```js -import { createServer } from "node:https"; -import { requestCertificate } from "@jsenv/https-local"; - -const { certificate, privateKey } = requestCertificate({ - altNames: ["localhost", "local.example"], -}); -``` - -[installCertificateAuthority](#installCertificateAuthority) must be called before this function. - -# verifyHostsFile - -This function is not mandatory to obtain the https certificates. -But it is useful to programmatically verify ip mappings that are important for your local server are present in hosts file. - -```js -import { verifyHostsFile } from "@jsenv/https-local"; - -await verifyHostsFile({ - ipMappings: { - "127.0.0.1": ["localhost", "local.example"], - }, -}); -``` - -Find below logs written in terminal when this function is executed. - -
- mac and linux - -```console -> node ./verify_hosts.mjs - -Check hosts file content... -⚠ 1 mapping is missing in hosts file ---- hosts file path --- -/etc/hosts ---- line(s) to add --- -127.0.0.1 localhost local.example -``` - -
- -
- windows - -```console -> node ./verify_hosts.mjs - -Check hosts file content... -⚠ 1 mapping is missing in hosts file ---- hosts file path --- -C:\\Windows\\System32\\Drivers\\etc\\hosts ---- line(s) to add --- -127.0.0.1 localhost local.example -``` - -
- -## Auto update hosts - -It's possible to update hosts file programmatically using _tryToUpdateHostsFile_. - -```js -import { verifyHostsFile } from "@jsenv/https-local"; - -await verifyHostsFile({ - ipMappings: { - "127.0.0.1": ["localhost", "local.example"], - }, - tryToUpdateHostsFile: true, -}); -``` - -
- mac and linux - -```console -Check hosts file content... -ℹ 1 mapping is missing in hosts file -Adding 1 mapping(s) in hosts file... -❯ echo "127.0.0.1 local.example" | sudo tee -a /etc/hosts -Password: -✔ mappings added to hosts file -``` - -_Second execution logs_ - -```console -> node ./verify_hosts.mjs - -Check hosts file content... -✔ all ip mappings found in hosts file -``` - -
- -
- windows - -```console -Check hosts file content... -ℹ 1 mapping is missing in hosts file -Adding 1 mapping(s) in hosts file... -❯ (echo 127.0.0.1 local.example) >> C:\\Windows\\System32\\Drivers\\etc\\hosts -Password: -✔ mappings added to hosts file -``` - -_Second execution logs_ - -```console -> node ./verify_hosts.mjs - -Check hosts file content... -✔ all ip mappings found in hosts file -``` - -
diff --git a/packages/independent/https-local/src/https_local_cli.mjs b/packages/independent/https-local/src/https_local_cli.mjs index 40b40c5e34..6063811049 100755 --- a/packages/independent/https-local/src/https_local_cli.mjs +++ b/packages/independent/https-local/src/https_local_cli.mjs @@ -26,9 +26,12 @@ Usage: npx @jsenv/https-local install --trust npx @jsenv/https-local uninstall +trust: Try to add root certificate to os and browser trusted stores. + https://github.com/jsenv/core/tree/main/packages/independent/https-local `); + process.exit(0); } From 3030be8aaa88fedbdb40cf21e32146bc92199e00 Mon Sep 17 00:00:00 2001 From: dmail Date: Wed, 14 Aug 2024 10:39:06 +0200 Subject: [PATCH 6/7] improve an error message --- .github/workflows/ci_eslint_and_test.yml | 2 +- .github/workflows/ci_test_workspace.yml | 2 +- package.json | 4 ++-- packages/independent/https-local/package.json | 2 +- .../https-local/src/certificate_request.js | 4 +++- .../https-local/src/https_local_cli.mjs | 17 ++++++++++++- scripts/dev/install_certificate_authority.mjs | 23 ------------------ scripts/test/certificate_install.mjs | 24 ------------------- .../manual/start_build_server.mjs | 3 +-- 9 files changed, 25 insertions(+), 56 deletions(-) delete mode 100644 scripts/dev/install_certificate_authority.mjs delete mode 100644 scripts/test/certificate_install.mjs diff --git a/.github/workflows/ci_eslint_and_test.yml b/.github/workflows/ci_eslint_and_test.yml index 4ebaa54c83..e78854e100 100644 --- a/.github/workflows/ci_eslint_and_test.yml +++ b/.github/workflows/ci_eslint_and_test.yml @@ -57,7 +57,7 @@ jobs: run: npx playwright install-deps - name: Install certificate # needed for @jsenv/service-worker tests if: runner.os == 'Linux' # https://docs.github.com/en/actions/learn-github-actions/contexts#runner-context - run: node ./scripts/test/certificate_install.mjs + run: npm run certificate:install - name: Fix lightningcss windows if: runner.os == 'Windows' run: npm install lightningcss-win32-x64-msvc diff --git a/.github/workflows/ci_test_workspace.yml b/.github/workflows/ci_test_workspace.yml index baa5bb0e58..4d0b55b879 100644 --- a/.github/workflows/ci_test_workspace.yml +++ b/.github/workflows/ci_test_workspace.yml @@ -55,7 +55,7 @@ jobs: run: npx playwright install-deps - name: Install certificate # needed for @jsenv/service-worker tests if: runner.os == 'Linux' # https://docs.github.com/en/actions/learn-github-actions/contexts#runner-context - run: node ./scripts/test/certificate_install.mjs + run: npm run certificate:install - name: Fix lightningcss windows if: runner.os == 'Windows' run: npm install lightningcss-win32-x64-msvc diff --git a/package.json b/package.json index 48d9795cf0..8250b159c7 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "workspace:test:ci": "CI=1 npm run workspace:test", "dev": "node --conditions=development ./scripts/dev/dev.mjs", "playwright:install": "npx playwright install-deps && npx playwright install", - "certificate:install": "node ./scripts/dev/install_certificate_authority.mjs", + "certificate:install": "npx @jsenv/https-local install", "prepublishOnly": "npm run build" }, "dependencies": { @@ -97,7 +97,7 @@ "@jsenv/core": "./", "@jsenv/eslint-config-relax": "workspace:*", "@jsenv/file-size-impact": "workspace:*", - "@jsenv/https-local": "3.0.7", + "@jsenv/https-local": "workspace:*", "@jsenv/monorepo": "workspace:*", "@jsenv/performance-impact": "workspace:*", "@jsenv/plugin-as-js-classic": "workspace:*", diff --git a/packages/independent/https-local/package.json b/packages/independent/https-local/package.json index 7ad0df1551..dafe532079 100644 --- a/packages/independent/https-local/package.json +++ b/packages/independent/https-local/package.json @@ -1,6 +1,6 @@ { "name": "@jsenv/https-local", - "version": "3.1.0", + "version": "3.1.1", "description": "A programmatic way to generate locally trusted certificates", "license": "MIT", "repository": { diff --git a/packages/independent/https-local/src/certificate_request.js b/packages/independent/https-local/src/certificate_request.js index 309542aab7..22657b5d09 100644 --- a/packages/independent/https-local/src/certificate_request.js +++ b/packages/independent/https-local/src/certificate_request.js @@ -46,7 +46,9 @@ export const requestCertificate = ({ } = getAuthorityFileInfos(); if (!rootCertificateFileInfo.exists) { throw new Error( - `Certificate authority not found, "installCertificateAuthority" must be called before "requestServerCertificate"`, + `Certificate authority not found, "installCertificateAuthority" must be called before "requestServerCertificate". +--- Suggested command to run --- +npx @jsenv/https-local install --trust`, ); } if (!rootCertificatePrivateKeyFileInfo.exists) { diff --git a/packages/independent/https-local/src/https_local_cli.mjs b/packages/independent/https-local/src/https_local_cli.mjs index 6063811049..12a6a68a5c 100755 --- a/packages/independent/https-local/src/https_local_cli.mjs +++ b/packages/independent/https-local/src/https_local_cli.mjs @@ -3,6 +3,7 @@ import { installCertificateAuthority, uninstallCertificateAuthority, + verifyHostsFile, } from "@jsenv/https-local"; import { parseArgs } from "node:util"; @@ -23,10 +24,16 @@ if (values.help || positionals.length === 0) { console.log(`https-local: Generate https certificates to use on your machine. Usage: + npx @jsenv/https-local install --trust + Install root certificate on the filesystem + - trust: Try to add root certificate to os and browser trusted stores. + npx @jsenv/https-local uninstall + Uninstall root certificate from the filesystem -trust: Try to add root certificate to os and browser trusted stores. +npx @jsenv/https-local localhost-mapping + Ensure localhost mapping to 127.0.0.1 is set on the filesystem https://github.com/jsenv/core/tree/main/packages/independent/https-local @@ -47,6 +54,14 @@ const commandHandlers = { tryToUntrust: true, }); }, + ["localhost-mapping"]: async () => { + await verifyHostsFile({ + ipMappings: { + "127.0.0.1": ["localhost"], + }, + tryToUpdatesHostsFile: true, + }); + }, }; const [command] = positionals; diff --git a/scripts/dev/install_certificate_authority.mjs b/scripts/dev/install_certificate_authority.mjs deleted file mode 100644 index 55fb08bbd0..0000000000 --- a/scripts/dev/install_certificate_authority.mjs +++ /dev/null @@ -1,23 +0,0 @@ -/* - * This file needs to be executed once. After that the root certificate is valid for 20 years. - * Re-executing this file will log the current root certificate validity and trust status. - * Re-executing this file 20 years later would reinstall a root certificate and re-trust it. - * - * Read more in https://github.com/jsenv/https-local#installCertificateAuthority - */ - -import { - installCertificateAuthority, - verifyHostsFile, -} from "@jsenv/https-local"; - -await installCertificateAuthority({ - tryToTrust: true, - NSSDynamicInstall: true, -}); -await verifyHostsFile({ - ipMappings: { - "127.0.0.1": ["localhost", "local"], - }, - tryToUpdatesHostsFile: true, -}); diff --git a/scripts/test/certificate_install.mjs b/scripts/test/certificate_install.mjs deleted file mode 100644 index 5f070e1cb7..0000000000 --- a/scripts/test/certificate_install.mjs +++ /dev/null @@ -1,24 +0,0 @@ -/* - * This file needs to be executed once. After that the root certificate is valid for 20 years. - * Re-executing this file will log the current root certificate validity and trust status. - * Re-executing this file 20 years later would reinstall a root certificate and re-trust it. - * - * Read more in https://github.com/jsenv/https-local#installCertificateAuthority - */ - -import { - installCertificateAuthority, - verifyHostsFile, -} from "@jsenv/https-local"; - -await installCertificateAuthority({ - logLevel: "debug", - tryToTrust: true, - NSSDynamicInstall: true, -}); -await verifyHostsFile({ - ipMappings: { - "127.0.0.1": ["localhost"], - }, - tryToUpdatesHostsFile: true, -}); diff --git a/tests/build_server/manual/start_build_server.mjs b/tests/build_server/manual/start_build_server.mjs index c37d07ea02..8ae1bf55aa 100644 --- a/tests/build_server/manual/start_build_server.mjs +++ b/tests/build_server/manual/start_build_server.mjs @@ -1,6 +1,5 @@ -import { requestCertificate } from "@jsenv/https-local"; - import { startBuildServer } from "@jsenv/core"; +import { requestCertificate } from "@jsenv/https-local"; const { certificate, privateKey } = requestCertificate({ altNames: ["local"] }); await startBuildServer({ From 437646454128e79eeb108f975b611015b61c591d Mon Sep 17 00:00:00 2001 From: dmail Date: Wed, 14 Aug 2024 10:43:01 +0200 Subject: [PATCH 7/7] add npx @jsenv/https-local setup command --- .github/workflows/ci_eslint_and_test.yml | 2 +- .github/workflows/ci_test_workspace.yml | 2 +- package.json | 2 +- packages/independent/https-local/package.json | 2 +- .../https-local/src/https_local_cli.mjs | 15 +++++++++++++++ 5 files changed, 19 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci_eslint_and_test.yml b/.github/workflows/ci_eslint_and_test.yml index e78854e100..775d5a5613 100644 --- a/.github/workflows/ci_eslint_and_test.yml +++ b/.github/workflows/ci_eslint_and_test.yml @@ -57,7 +57,7 @@ jobs: run: npx playwright install-deps - name: Install certificate # needed for @jsenv/service-worker tests if: runner.os == 'Linux' # https://docs.github.com/en/actions/learn-github-actions/contexts#runner-context - run: npm run certificate:install + run: npm run https:setup - name: Fix lightningcss windows if: runner.os == 'Windows' run: npm install lightningcss-win32-x64-msvc diff --git a/.github/workflows/ci_test_workspace.yml b/.github/workflows/ci_test_workspace.yml index 4d0b55b879..041374d91a 100644 --- a/.github/workflows/ci_test_workspace.yml +++ b/.github/workflows/ci_test_workspace.yml @@ -55,7 +55,7 @@ jobs: run: npx playwright install-deps - name: Install certificate # needed for @jsenv/service-worker tests if: runner.os == 'Linux' # https://docs.github.com/en/actions/learn-github-actions/contexts#runner-context - run: npm run certificate:install + run: npm run https:setup - name: Fix lightningcss windows if: runner.os == 'Windows' run: npm install lightningcss-win32-x64-msvc diff --git a/package.json b/package.json index 8250b159c7..757e5b18a3 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "workspace:test:ci": "CI=1 npm run workspace:test", "dev": "node --conditions=development ./scripts/dev/dev.mjs", "playwright:install": "npx playwright install-deps && npx playwright install", - "certificate:install": "npx @jsenv/https-local install", + "https:setup": "npx @jsenv/https-local setup", "prepublishOnly": "npm run build" }, "dependencies": { diff --git a/packages/independent/https-local/package.json b/packages/independent/https-local/package.json index dafe532079..260d6463d6 100644 --- a/packages/independent/https-local/package.json +++ b/packages/independent/https-local/package.json @@ -1,6 +1,6 @@ { "name": "@jsenv/https-local", - "version": "3.1.1", + "version": "3.2.0", "description": "A programmatic way to generate locally trusted certificates", "license": "MIT", "repository": { diff --git a/packages/independent/https-local/src/https_local_cli.mjs b/packages/independent/https-local/src/https_local_cli.mjs index 12a6a68a5c..aa79feeb65 100755 --- a/packages/independent/https-local/src/https_local_cli.mjs +++ b/packages/independent/https-local/src/https_local_cli.mjs @@ -25,6 +25,9 @@ if (values.help || positionals.length === 0) { Usage: +npx @jsenv/https-local setup + Install root certificate, try to trust it and ensure localhost is mapped to 127.0.0.1 + npx @jsenv/https-local install --trust Install root certificate on the filesystem - trust: Try to add root certificate to os and browser trusted stores. @@ -43,6 +46,18 @@ https://github.com/jsenv/core/tree/main/packages/independent/https-local } const commandHandlers = { + setup: async () => { + await installCertificateAuthority({ + tryToTrust: true, + NSSDynamicInstall: true, + }); + await verifyHostsFile({ + ipMappings: { + "127.0.0.1": ["localhost"], + }, + tryToUpdatesHostsFile: true, + }); + }, install: async ({ tryToTrust }) => { await installCertificateAuthority({ tryToTrust,