From dea2c3ccecb7e80a780dfb67ffbed867b8e941df Mon Sep 17 00:00:00 2001 From: DeBuXer Date: Thu, 1 Aug 2024 11:57:32 +0200 Subject: [PATCH 01/13] Reload .env file on change without restarting the application --- app.js | 26 ++++++++++++++++++++++++++ package.json | 2 ++ 2 files changed, 28 insertions(+) diff --git a/app.js b/app.js index 0bb0be3..84de168 100644 --- a/app.js +++ b/app.js @@ -1,4 +1,30 @@ import {plainServer, secureServer} from "./index.js"; +import fs from "fs"; +import { watch } from "chokidar"; +import dotenv from "dotenv"; + +// Function to reload the .env variables +function reloadEnv() { + if (fs.existsSync('.env')) { + const envConfig = dotenv.parse(fs.readFileSync('.env')); + for (const k in envConfig) { + process.env[k] = envConfig[k]; + } + console.log('Environment variables reloaded.'); + } else { + console.warn('.env file does not exist.'); + } +} + +// Watch the .env file for changes +watch('.env').on('change', () => { + console.log('.env file changed, reloading...'); + reloadEnv(); +}); + +// Initial load +reloadEnv(); + const port80 = parseInt(process.env.HTTP_PORT || "8080"); const port443 = parseInt(process.env.HTTPS_PORT || "8443"); diff --git a/package.json b/package.json index 9c88109..b4e2df2 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "dependencies": { "async-lock": "^1.4.1", "better-sqlite3": "^11.1.2", + "chokidar": "^3.5.3", + "dotenv": "^16.0.3", "hashmap-with-ttl": "^1.0.0", "jose": "^5.6.3", "rsa-csr": "^1.0.6" From 952f0f6bd934ded85155b617590fd0780038f562 Mon Sep 17 00:00:00 2001 From: DeBuXer Date: Wed, 31 Jul 2024 15:33:16 +0200 Subject: [PATCH 02/13] Ability to use the local DNS resolver instead of Google DNS --- HOSTING.md | 1 + src/util.js | 89 +++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 63 insertions(+), 27 deletions(-) diff --git a/HOSTING.md b/HOSTING.md index ba3ddbf..7441533 100644 --- a/HOSTING.md +++ b/HOSTING.md @@ -27,6 +27,7 @@ This guide will walk you through the process of setting up your own instance of `BLACKLIST_HOSTS` | A comma-separated list of root domains to blacklist `BLACKLIST_REDIRECT` | The URL to redirect to when a blacklisted host is accessed `HOME_DOMAIN` | The host to enable `/stat` endpoint +`USE_LOCAL_DNS` | Default is `false`, so the Google DNS is used. Set it to `true` if you want to use the DNS resolver of your own host If `WHITELIST_HOSTS` is set, `BLACKLIST_HOSTS` is ignored. Both is mutually exclusive. diff --git a/src/util.js b/src/util.js index 510b7c0..e1bc28d 100644 --- a/src/util.js +++ b/src/util.js @@ -2,9 +2,11 @@ import request from "./certnode/lib/request.js"; import fs from "node:fs"; import { isIPv4, isIPv6 } from "node:net"; import { fileURLToPath } from "node:url"; +import dns from 'dns/promises'; const recordParamDestUrl = 'forward-domain'; const recordParamHttpStatus = 'http-status'; const caaRegex = /^0 issue (")?letsencrypt\.org(;validationmethods=http-01)?\1$/; +const useLocalDNS = process.env.USE_LOCAL_DNS || false /** * @type {Record} */ @@ -169,23 +171,42 @@ const parseTxtRecordData = (value) => { * @return {Promise} */ export async function validateCAARecords(host, mockResolve = undefined) { - /** - * @type {{data: {Answer: {data: string, type: number}[]}}} - */ - const resolve = mockResolve || await request(`https://dns.google/resolve?name=${encodeURIComponent(host)}&type=CAA`); - if (!resolve.data.Answer) { - return null; - } + if (useLocalDNS) { + const records = mockResolve || await dns.resolveCaa(`${encodeURIComponent(host)}`); + if (!records || records.length === 0) { + return null; + } - const issueRecords = resolve.data.Answer.filter((x) => - x.type == 257 && typeof x.data === 'string' && x.data.startsWith('0 issue ') - ).map(x => x.data); + const issueRecords = records + .filter(record => record.type === 257) + .map(record => record.data) + .filter(data => data.startsWith('0 issue ')); - // check if any record allows Let'sEncrypt (or no record at all) - if (issueRecords.length == 0 || issueRecords.some(x => caaRegex.test(x))) { - return null; + // Check if any record allows Let's Encrypt (or no record at all) + if (issueRecords.length === 0 || issueRecords.some(record => caaRegex.test(record))) { + return null; + } + return issueRecords; + + } else { + /** + * @type {{data: {Answer: {data: string, type: number}[]}}} + */ + const resolve = mockResolve || await request(`https://dns.google/resolve?name=${encodeURIComponent(host)}&type=CAA`); + if (!resolve.data.Answer) { + return null; + } + + const issueRecords = resolve.data.Answer.filter((x) => + x.type == 257 && typeof x.data === 'string' && x.data.startsWith('0 issue ') + ).map(x => x.data); + + // check if any record allows Let'sEncrypt (or no record at all) + if (issueRecords.length == 0 || issueRecords.some(x => caaRegex.test(x))) { + return null; + } + return issueRecords; } - return issueRecords; } /** @@ -194,22 +215,36 @@ export async function validateCAARecords(host, mockResolve = undefined) { * @return {Promise<{url: string, httpStatus?: string} | null>} */ export async function findTxtRecord(host, mockResolve = undefined) { - const resolve = mockResolve || await request(`https://dns.google/resolve?name=_.${encodeURIComponent(host)}&type=TXT`); - if (!resolve.data.Answer) { + if (useLocalDNS) { + const resolve = mockResolve || await dns.resolveTxt(`_.${encodeURIComponent(host)}`); + for (const record of resolve) { + const joinedRecord = record.join(''); + const txtData = parseTxtRecordData(joinedRecord); + if (!txtData[recordParamDestUrl]) continue; + return { + url: txtData[recordParamDestUrl], + httpStatus: txtData[recordParamHttpStatus], + }; + } return null; - } - for (const head of resolve.data.Answer) { - if (head.type !== 16) { // RR type of TXT is 16 - continue; + } else { + const resolve = mockResolve || await request(`https://dns.google/resolve?name=_.${encodeURIComponent(host)}&type=TXT`); + if (!resolve.data.Answer) { + return null; } - const txtData = parseTxtRecordData(head.data); - if (!txtData[recordParamDestUrl]) continue; - return { - url: txtData[recordParamDestUrl], - httpStatus: txtData[recordParamHttpStatus], - }; + for (const head of resolve.data.Answer) { + if (head.type !== 16) { // RR type of TXT is 16 + continue; + } + const txtData = parseTxtRecordData(head.data); + if (!txtData[recordParamDestUrl]) continue; + return { + url: txtData[recordParamDestUrl], + httpStatus: txtData[recordParamHttpStatus], + }; + } + return null; } - return null; } From ab2b29a367ff6b149b2e1fa900b2a047200b27a2 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Sat, 3 Aug 2024 11:34:47 +0700 Subject: [PATCH 03/13] Fix using local DNS --- src/util.js | 50 +++++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/src/util.js b/src/util.js index e1bc28d..20305d7 100644 --- a/src/util.js +++ b/src/util.js @@ -6,7 +6,6 @@ import dns from 'dns/promises'; const recordParamDestUrl = 'forward-domain'; const recordParamHttpStatus = 'http-status'; const caaRegex = /^0 issue (")?letsencrypt\.org(;validationmethods=http-01)?\1$/; -const useLocalDNS = process.env.USE_LOCAL_DNS || false /** * @type {Record} */ @@ -17,6 +16,11 @@ const allowedHttpStatusCodes = { "308": true, // Modern Permanent Redirect with POST -> POST } + +/** + * @type {boolean | null} + */ +let useLocalDNS = null /** * @type {Record | null} */ @@ -171,23 +175,17 @@ const parseTxtRecordData = (value) => { * @return {Promise} */ export async function validateCAARecords(host, mockResolve = undefined) { + if (useLocalDNS === null) { + useLocalDNS = process.env.USE_LOCAL_DNS == 'true'; + } + let issueRecords; if (useLocalDNS) { - const records = mockResolve || await dns.resolveCaa(`${encodeURIComponent(host)}`); + const records = await dns.resolveCaa(host); if (!records || records.length === 0) { return null; } - const issueRecords = records - .filter(record => record.type === 257) - .map(record => record.data) - .filter(data => data.startsWith('0 issue ')); - - // Check if any record allows Let's Encrypt (or no record at all) - if (issueRecords.length === 0 || issueRecords.some(record => caaRegex.test(record))) { - return null; - } - return issueRecords; - + issueRecords = records.filter(record => record.issue).map(record => `0 issue "${record.issue}"`) } else { /** * @type {{data: {Answer: {data: string, type: number}[]}}} @@ -197,16 +195,17 @@ export async function validateCAARecords(host, mockResolve = undefined) { return null; } - const issueRecords = resolve.data.Answer.filter((x) => + issueRecords = resolve.data.Answer.filter((x) => x.type == 257 && typeof x.data === 'string' && x.data.startsWith('0 issue ') ).map(x => x.data); + } - // check if any record allows Let'sEncrypt (or no record at all) - if (issueRecords.length == 0 || issueRecords.some(x => caaRegex.test(x))) { - return null; - } - return issueRecords; + // Check if any record allows Let's Encrypt (or no record at all) + if (issueRecords.length === 0 || issueRecords.some(record => caaRegex.test(record))) { + return null; } + + return issueRecords; } /** @@ -215,10 +214,13 @@ export async function validateCAARecords(host, mockResolve = undefined) { * @return {Promise<{url: string, httpStatus?: string} | null>} */ export async function findTxtRecord(host, mockResolve = undefined) { + if (useLocalDNS === null) { + useLocalDNS = process.env.USE_LOCAL_DNS == 'true'; + } if (useLocalDNS) { - const resolve = mockResolve || await dns.resolveTxt(`_.${encodeURIComponent(host)}`); + const resolve = await dns.resolveTxt(`_.${host}`); for (const record of resolve) { - const joinedRecord = record.join(''); + const joinedRecord = record.join(';'); const txtData = parseTxtRecordData(joinedRecord); if (!txtData[recordParamDestUrl]) continue; return { @@ -226,8 +228,10 @@ export async function findTxtRecord(host, mockResolve = undefined) { httpStatus: txtData[recordParamHttpStatus], }; } - return null; } else { + /** + * @type {{data: {Answer: {data: string, type: number}[]}}} + */ const resolve = mockResolve || await request(`https://dns.google/resolve?name=_.${encodeURIComponent(host)}&type=TXT`); if (!resolve.data.Answer) { return null; @@ -243,8 +247,8 @@ export async function findTxtRecord(host, mockResolve = undefined) { httpStatus: txtData[recordParamHttpStatus], }; } - return null; } + return null; } From 3143ebc08935ebb273f4286720af3f73a13b4b19 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Sat, 3 Aug 2024 14:28:53 +0700 Subject: [PATCH 04/13] Save CA to DB Fix #26 --- .vscode/launch.json | 17 +++++++++ package-lock.json | 53 ++++++++++++-------------- package.json | 5 +-- src/client.js | 10 ++--- src/db.js | 52 +++++++++++++------------ src/sni.js | 15 ++++---- src/tools/migrate.js | 13 +++++++ src/util.js | 23 ++++++++++- test/unit.test.js | 90 ++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 210 insertions(+), 68 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..f486d4c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}\\app.js" + } + ] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 624921a..98aa157 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,9 @@ "dependencies": { "async-lock": "^1.4.1", "better-sqlite3": "^11.1.2", - "hashmap-with-ttl": "^1.0.0", "jose": "^5.6.3", + "lru-cache": "^11.0.0", + "node-forge": "^1.3.1", "rsa-csr": "^1.0.6" }, "devDependencies": { @@ -194,17 +195,6 @@ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" }, - "node_modules/hashmap-with-ttl": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hashmap-with-ttl/-/hashmap-with-ttl-1.0.0.tgz", - "integrity": "sha512-oZQOF6ptWpOw8L9kgPLTbpTMbdzzg0uFvMqzfXLNuQ/w/QHD9boLuR7k5dlxHERdStxIL6hg0FGn7G8Xm2IDDg==", - "dependencies": { - "linked-list": "^3.1.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -242,24 +232,12 @@ "url": "https://github.com/sponsors/panva" } }, - "node_modules/linked-list": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/linked-list/-/linked-list-3.1.0.tgz", - "integrity": "sha512-8fTIVBkKRqJ9MPEwggRkzYsOGcicCC4qSXm6iANVJOGwXrkew85kL7zFztQHSOQBbM6zG+vqtC/cTHm4nuUCSg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", + "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", "engines": { - "node": ">=10" + "node": "20 || >=22" } }, "node_modules/mimic-response": { @@ -302,6 +280,14 @@ "node": ">=10" } }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "engines": { + "node": ">= 6.13.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -413,6 +399,17 @@ "node": ">=10" } }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", diff --git a/package.json b/package.json index b4e2df2..ca89593 100644 --- a/package.json +++ b/package.json @@ -21,10 +21,9 @@ "dependencies": { "async-lock": "^1.4.1", "better-sqlite3": "^11.1.2", - "chokidar": "^3.5.3", - "dotenv": "^16.0.3", - "hashmap-with-ttl": "^1.0.0", "jose": "^5.6.3", + "lru-cache": "^11.0.0", + "node-forge": "^1.3.1", "rsa-csr": "^1.0.6" }, "devDependencies": { diff --git a/src/client.js b/src/client.js index 14e76fb..14ba96c 100644 --- a/src/client.js +++ b/src/client.js @@ -1,5 +1,5 @@ +import { LRUCache } from "lru-cache"; import { client, getStat } from "./sni.js"; -import { HashMap } from "hashmap-with-ttl"; import { findTxtRecord, isHostBlacklisted, @@ -21,12 +21,12 @@ import { * @property {number} httpStatus */ /** - * @type {HashMap} + * @type {LRUCache} */ -let resolveCache = new HashMap({ capacity: 10000 }); +let resolveCache = new LRUCache({ max: 10000 }); function pruneCache() { - resolveCache = new HashMap({ capacity: 10000 }); + resolveCache = new LRUCache({ max: 10000 }); } /** @@ -128,7 +128,7 @@ const listener = async function (req, res) { let cache = resolveCache.get(host); if (!cache || (Date.now() > cache.expire)) { cache = await buildCache(host); - resolveCache.update(host, cache); + resolveCache.set(host, cache); } if (cache.blacklisted) { if (blacklistRedirectUrl) { diff --git a/src/db.js b/src/db.js index 274b3ba..233386e 100644 --- a/src/db.js +++ b/src/db.js @@ -1,8 +1,7 @@ import sqlite from "better-sqlite3"; -import { derToPem, initMap } from "./util.js"; -import { X509Certificate, createPrivateKey } from "node:crypto"; -import { migrateFromV2 } from "./tools/migrate.js"; +import { derToPem, getCertExpiry, initMap, pemToDer } from "./util.js"; +import { migrateFromV2, migrateFromV3 } from "./tools/migrate.js"; import { dirname } from "node:path"; /** @@ -26,6 +25,7 @@ import { dirname } from "node:path"; * @property {string} domain * @property {Buffer} key DER * @property {Buffer} cert DER + * @property {Buffer} ca DER * @property {number} expire */ @@ -40,6 +40,7 @@ export class CertsDB { domain TEXT UNIQUE, key BLOB, cert BLOB, + ca BLOB, expire INTEGER )`).run(); db.prepare(`CREATE TABLE IF NOT EXISTS config ( @@ -47,20 +48,25 @@ export class CertsDB { value TEXT )`).run(); - this.save_cert_stmt = db.prepare(`INSERT OR REPLACE INTO certs (domain, key, cert, expire) VALUES (?, ?, ?, ?)`) - this.load_cert_stmt = db.prepare(`SELECT * FROM certs WHERE domain = ?`) - this.count_cert_stmt = db.prepare(`SELECT COUNT(*) as domains FROM certs`) + this.load_conf_stmt = db.prepare(`SELECT * FROM config`) this.save_conf_stmt = db.prepare(`INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)`) this.db = db; this.config = this.loadConfig(); - if (!this.config.version) { migrateFromV2(dirname(path), this); - this.config.version = '3'; + this.config.version = '4'; + this.saveConfig('version', this.config.version); + } else if (this.config.version === '3') { + migrateFromV3(this); + this.config.version = '4'; this.saveConfig('version', this.config.version); } + + this.save_cert_stmt = db.prepare(`INSERT OR REPLACE INTO certs (domain, key, cert, ca, expire) VALUES (?, ?, ?, ?, ?)`) + this.load_cert_stmt = db.prepare(`SELECT * FROM certs WHERE domain = ?`) + this.count_cert_stmt = db.prepare(`SELECT COUNT(*) as domains FROM certs`) } close() { this.db.close(); @@ -68,7 +74,7 @@ export class CertsDB { loadConfig() { const keys = initMap(); - for (const row of this.db.prepare('SELECT * FROM config').all()) { + for (const row of this.load_conf_stmt.all()) { // @ts-ignore keys[row.key] = row.value; } @@ -116,7 +122,7 @@ export class CertsDB { throw new Error("Domain not found") } return { - cert: derToPem(row.cert, "certificate"), + cert: derToPem([row.cert, row.ca], "certificate"), key: derToPem(row.key, "private"), expire: row.expire, }; @@ -125,20 +131,22 @@ export class CertsDB { * @param {string} domain * @param {Buffer} key * @param {Buffer} cert + * @param {Buffer} ca * @param {number} expire * @returns {CertRow} */ - saveCert(domain, key, cert, expire) { + saveCert(domain, key, cert, ca, expire) { if (!this.save_cert_stmt) { throw new Error("DB is not initialized") } this.save_cert_stmt.run([domain, key, cert, + ca, expire, ]); return { - domain, key, cert, expire + domain, key, cert, ca, expire } } /** @@ -147,17 +155,13 @@ export class CertsDB { * @param {string} cert */ saveCertFromCache(domain, key, cert) { - const x509 = new X509Certificate(cert); - return this.saveCert(domain, createPrivateKey({ - key: key, - type: "pkcs8", - format: "pem", - }).export({ - format: "der", - type: "pkcs8", - }), - x509.raw, - Date.parse(x509.validTo), + const keyBuffer = pemToDer(key)[0]; + const certBuffers = pemToDer(cert); + return this.saveCert(domain, + keyBuffer, + certBuffers[0], + certBuffers[1], + getCertExpiry(cert), ) } -} \ No newline at end of file +} diff --git a/src/sni.js b/src/sni.js index 4f1a3cc..dc2ccc2 100644 --- a/src/sni.js +++ b/src/sni.js @@ -3,7 +3,7 @@ import path from "path"; import AsyncLock from 'async-lock'; import { Client } from "./certnode/lib/index.js"; import { CertsDB } from "./db.js"; -import { HashMap } from "hashmap-with-ttl"; +import { LRUCache } from 'lru-cache' import { blacklistRedirectUrl, isIpAddress, @@ -11,7 +11,8 @@ import { ensureDirSync, isExceedHostLimit, isExceedLabelLimit, - validateCAARecords + validateCAARecords, + derToPem } from "./util.js"; const lock = new AsyncLock(); @@ -24,9 +25,9 @@ ensureDirSync(certsDir); const db = new CertsDB(dbFile); /** - * @type {HashMap} + * @type {LRUCache} */ -let resolveCache = new HashMap({ capacity: 10000 }); +let resolveCache = new LRUCache({ max: 10000 }); /** @@ -35,7 +36,7 @@ let resolveCache = new HashMap({ capacity: 10000 }); let statCache; function pruneCache() { - resolveCache = new HashMap({ capacity: 10000 }); + resolveCache = new LRUCache({ max: 10000 }); } function getStat() { @@ -44,7 +45,7 @@ function getStat() { } statCache = { domains: db.countCert(), - in_mem: resolveCache.length(), + in_mem: resolveCache.size, iat: Date.now(), exp: Date.now() + 1000 * 60 * 60, }; @@ -93,7 +94,7 @@ async function getKeyCert(servername) { if (!cacheNew) { return undefined; } - resolveCache.update(servername, cacheNew); + resolveCache.set(servername, cacheNew); return { key: cacheNew.key, cert: cacheNew.cert, diff --git a/src/tools/migrate.js b/src/tools/migrate.js index 7d43830..7062200 100644 --- a/src/tools/migrate.js +++ b/src/tools/migrate.js @@ -63,3 +63,16 @@ export function migrateFromV2(dir, db) { migrateWalkDir(dir, db); process.stdout.write(`\nImport completed\n`); } + +/** + * @param {import('../db.js').CertsDB} db + */ +export function migrateFromV3({ db }) { + // check if v2 account exists, try migrate then. + + process.stdout.write(`Begin v3 -> v4 migration sessions\n`); + + db.prepare(`ALTER TABLE certs ADD COLUMN ca BLOB`).run(); + + process.stdout.write(`\nImport completed\n`); +} diff --git a/src/util.js b/src/util.js index 20305d7..b7d820d 100644 --- a/src/util.js +++ b/src/util.js @@ -3,6 +3,7 @@ import fs from "node:fs"; import { isIPv4, isIPv6 } from "node:net"; import { fileURLToPath } from "node:url"; import dns from 'dns/promises'; +import forge from "node-forge"; const recordParamDestUrl = 'forward-domain'; const recordParamHttpStatus = 'http-status'; const caaRegex = /^0 issue (")?letsencrypt\.org(;validationmethods=http-01)?\1$/; @@ -251,13 +252,33 @@ export async function findTxtRecord(host, mockResolve = undefined) { return null; } +/** + * + * @param {string} cert + */ +export function getCertExpiry(cert) { + const x509 = forge.pki.certificateFromPem(cert); + return x509.validity.notAfter.getTime() +} + +/** + * + * @param {string} key + */ +export function pemToDer(key) { + const keys = forge.pem.decode(key); + return keys.map(x => Buffer.from(x.body, 'binary')); +} /** - * @param {Buffer} derBuffer + * @param {Buffer|Buffer[]} derBuffer * @param {"public"|"private"|"certificate"} type * @returns {string} */ export function derToPem(derBuffer, type) { + if (Array.isArray(derBuffer)) { + return derBuffer.filter(x => x && x.length > 0).map(x => derToPem(x, type)).join(''); + } const prefix = { 'certificate': 'CERTIFICATE', 'public': 'PUBLIC KEY', diff --git a/test/unit.test.js b/test/unit.test.js index 2e2a126..14c6bc5 100644 --- a/test/unit.test.js +++ b/test/unit.test.js @@ -6,6 +6,9 @@ import { isHostBlacklisted, validateCAARecords, isExceedHostLimit, + derToPem, + pemToDer, + getCertExpiry, } from "../src/util.js"; test('blacklist works', () => { @@ -60,4 +63,91 @@ test('txt resolver works', async () => { }); }); +test('pem der conversion for cert works', async () => { + let cert = `-----BEGIN CERTIFICATE----- +MIIDTjCCAjagAwIBAgIIBgAQy/0qNmkwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE +AxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSA2NjRhMmMwHhcNMjQwODAzMDYyMzI3 +WhcNMjkwODAzMDYyMzI2WjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAxcls2hAh1rgy7cfjklKeIvsj5hmUhKWPUG/9CBERWiTlJS04vJdWW6/w8f38 +CN/fttWlWVYeCag+hGVmShUjBUnuYrzeCO2rq1dkgqhrTW9bFpAhkmtVxieSrXuU +mugG+Q7MW/KwyQxz5fThbNM8Fjn97eerZgzSLDj90BgYVB7Bm5FmqfpoDJRBKayA +W5wrzDgmwUkbsu8ji1Dx9NTjoEhl3+7sjTCxVVJYHPdb/ISPcE5bRcVLT/oA5xXt +L+f4n+/Lydnc9wwcB4cUo2X2Y17CtdxGLkn2XJcjh1Ca0iMZ+1oaSQPRF4MqsmHV +4ojXW5TeRNw0S8Ogbf3x0HTrEwIDAQABo4GjMIGgMA4GA1UdDwEB/wQEAwIFoDAd +BgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNV +HQ4EFgQUfWQn1UGpENf3nNNzo2B+2SJBvFowHwYDVR0jBBgwFoAUwJ7TbCOeTnd2 +4R9a31dcDsTM+V0wIQYDVR0RAQH/BBcwFYITci5mb3J3YXJkZG9tYWluLm5ldDAN +BgkqhkiG9w0BAQsFAAOCAQEAxhkKy9meD5MmvuOU0E0uFFNwmGM5u24lnuZ/RcAM +Xp9jhyIfWamTs4xH7EXzMsForLAudQNwvdi7ewUgXnDRRAuBy79tRN4YwsxzYUBu +sKVpB+c99uAcEufdMjUUQqgDhtVWcVesrvx65EM4/r2YbseDduZEAIyvlZAxNNo/ +1Ox2m/V7yYmlnJ4WKPF5JrkKXLIfVc/puKwk16tEK6SM78hzS5HTUfksBzMauk32 +9mvKLjflTx3sQseVqDX9m3l7pLagM15nL8IoBw+M4Hqx/E21CG89okL1PrmBDM+K +izrL6bydOGcPEbqwRk5V0rm7w6qBYHN6OWAypSQ0UYYG+w== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDUDCCAjigAwIBAgIIVd1bNP8Jx4IwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE +AxMVUGViYmxlIFJvb3QgQ0EgMzJiNzJkMCAXDTI0MDgwMzA2MjAyMFoYDzIwNTQw +ODAzMDYyMDIwWjAoMSYwJAYDVQQDEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDY2 +NGEyYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM4MmkMcMy1HmfaB +uOGsnI2CA2m2sVm/QXi0mfZeffmaH8kz3zfqKzjm9Y19+4dHOOjwm8wrgxqKRLjQ +SEnF30Lhb1RmnfX9loe2vkP66wAxhW3/Up+zBxSUJa5LsnzTy229aY1Y8Mbb6ENC +bYIC0ZxBCwo/szvdxnaOPzAxeoV1/UsWkNbp0/Huu7WpnqhoPTrjRHNCU1xIv1ZE +rKTig5NHwTqjzeqt3vT21inUmmwmEl5+HQFYWBpZyIKhfC0JaK4mjd3efMnUcqG8 +Ws1NvmSWkBc9JsL4JmWGEjv7aX6roCxrpOfFhWtog7BDcZ+EM25DCgxM5WxUGdrc +VN1kZ8ECAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYB +BQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMCe02wj +nk53duEfWt9XXA7EzPldMB8GA1UdIwQYMBaAFPkmTsD1ef0Otc7VLA4MxnJK12fP +MA0GCSqGSIb3DQEBCwUAA4IBAQAFNlkf6EdNFcfwuSANT6OM9g72Yt0jRZxzlWO2 +bCO+TqSjoF3TKNpTwgi+aXQ+/mjKSrv8y8jRw/QM4brLpFDJ+L4KNEt6KNEeIL2F +87fuUihlTGjD34KKAwI+OfgfIaNpkcshlLs+V4hWXoROVdioLAj+rOExDjuriu/L +5v7Ke4iqIIoeoiWTjbmqxpOyYmfWm+kfo7vbR8uSjK2LQx7uWqgRCFAhwPbEFuEy +JHTqTbvIWRYsjyFiMEIUh10sLqqMYcZoJBgmX//KH8Iw0PDEam+RGhod1Rb46Sm+ +7rOGV9tVAr/t6UsRAImu/Rrpst817EhR6vv07GiBDAH65XdZ +-----END CERTIFICATE----- +` + let certs = pemToDer(cert); + let newCert = derToPem(certs, "certificate"); + + expect(newCert).toEqual(cert); + expect(getCertExpiry(cert)).toEqual(1880432606000); + expect(getCertExpiry(newCert)).toEqual(1880432606000); +}) + +test('pem der conversion for key works', async () => { + let key = `-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDFyWzaECHWuDLt +x+OSUp4i+yPmGZSEpY9Qb/0IERFaJOUlLTi8l1Zbr/Dx/fwI39+21aVZVh4JqD6E +ZWZKFSMFSe5ivN4I7aurV2SCqGtNb1sWkCGSa1XGJ5Kte5Sa6Ab5Dsxb8rDJDHPl +9OFs0zwWOf3t56tmDNIsOP3QGBhUHsGbkWap+mgMlEEprIBbnCvMOCbBSRuy7yOL +UPH01OOgSGXf7uyNMLFVUlgc91v8hI9wTltFxUtP+gDnFe0v5/if78vJ2dz3DBwH +hxSjZfZjXsK13EYuSfZclyOHUJrSIxn7WhpJA9EXgyqyYdXiiNdblN5E3DRLw6Bt +/fHQdOsTAgMBAAECggEAJsUdk78uyuavgQG6P6/3NJczCcNA5CGJ7rQNDvw9gQST +cE6lfP5TXMSnv9/P/DNaKH5Hm7PwTmdO3ef8fZAYHczIsE0iXvCrwnnuh1gZNIQc +AFe/ZPKqTR3ruBrt3dGWsFJwx6NSeQ56V3zBhXIAqMC0YGKVq/reZfHD+vsGJdLL +PVabyGQugZWJrqvhJMv8Nmj0nvg9ostL+qA1MKAZRNc5GURL4w6f147IE5Qbebhx +daulsXnCfcP86OIPV1rDu8btj58exLLIsCyNtwDe0X/LHcKx6m5EX1yjBN7zssCL +0uwuc5MYYrwEg7PoiNL7y2PKmjD3roFqoWpH72W9iQKBgQDtFP53gJF9wjQrUIw1 +YY6NS9EVs6zwRyXjYSMOxm6bSNSdfwzRASpBTtaGMTFWMlPH0ADJ48oWeC86NbqA +HNs85vAjlWfLF1jbGGkww4L4isxh3ie67XDnyTWEuZxDA1tsX17qoeYQJapxkUaR +4cNA6BQGikJ9aunELClJTwAKSQKBgQDVkbn63oXISeqG7uLHsK/ozcIa8t60roPb +dk8XKm2eOIjlsTGOUB9eebZsptSPCN/cMvzscIJqd7YoVKeOFhEQ17QxxiwtCez/ +A61fAZAJWMimGZRoPO2vmSFYoNkWj4jss2LVBuMq1VyrObVrvnZc9mp+eTdxo/C5 +fy36C8AqewKBgA/OE3zB/HEGzlWI5B/25frzb/fjZ4cJJzR2WFD2147QlyP8wUz5 +p+h8qf5+LwzRBBbQ/gx3fBRtZLCbvlgmFFOGDcJBho7aepj4kqKmlgedsSxhFAL5 +K0q4djHn8cvh4GlkHj7EFkNDT46Moci95TdhgVxCQVZ9FyJ10zbI5nbJAoGBAJnM +5D5B2d4vPPIHPtHH8CabZtm5ZaCAvPxi6von1+FFnXCsdp+iG7URucntKs4G+g+9 +uF8ddw3tQAUzUacFRSz36hCeQln89+t+XnA4092nTngvm6yllBYNFPKagzu4CkdL +uDTpTNcf6Ch22qvI8bxoyLBj4wW3pjgv2pBjvfPZAoGBAOzrNQ6lWJow1rG8sQAF +bfoxVpX6y9JJArJuT7bQO0TZOtPTTwjo94EyPUMpOlRLHko8r5buEl9EPW/mCOrX +gLIo1xC6QTia+mZ/aK+vHuFhuGy37az8nJzR/1Ud61C7og+BoXEsJZi7+J0M/6Nr +vV1rSGF5+dMeo8CldZlyMHDi +-----END PRIVATE KEY----- +` + + let keys = pemToDer(key); + let newKey = derToPem(keys, "private"); + + expect(newKey).toEqual(key); + +}) From 631b5e80ae68b8fc0d0c79c43aedcd2152012df6 Mon Sep 17 00:00:00 2001 From: DeBuXer Date: Thu, 1 Aug 2024 11:57:32 +0200 Subject: [PATCH 05/13] Reload .env file on change without restarting the application --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index ca89593..02460b8 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "dependencies": { "async-lock": "^1.4.1", "better-sqlite3": "^11.1.2", + "chokidar": "^3.5.3", + "dotenv": "^16.0.3", "jose": "^5.6.3", "lru-cache": "^11.0.0", "node-forge": "^1.3.1", From f71cafa2685a17daa4bb8440286471f458525d04 Mon Sep 17 00:00:00 2001 From: Jeffrey Date: Tue, 6 Aug 2024 13:45:19 +0200 Subject: [PATCH 06/13] Add clear config function --- app.js | 2 ++ src/util.js | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/app.js b/app.js index 84de168..59dc88a 100644 --- a/app.js +++ b/app.js @@ -1,4 +1,5 @@ import {plainServer, secureServer} from "./index.js"; +import { clearConfig } from "./src/util.js"; import fs from "fs"; import { watch } from "chokidar"; import dotenv from "dotenv"; @@ -19,6 +20,7 @@ function reloadEnv() { // Watch the .env file for changes watch('.env').on('change', () => { console.log('.env file changed, reloading...'); + clearConfig(); reloadEnv(); }); diff --git a/src/util.js b/src/util.js index b7d820d..e898bda 100644 --- a/src/util.js +++ b/src/util.js @@ -75,6 +75,16 @@ function csvToMap(str) { }, initMap()) } +/** +* @return {void} +*/ +export function clearConfig() { + whitelistMap = null; + blacklistMap = null; + useLocalDNS = null; + blacklistRedirectUrl = null; +} + /** * @param {Record} [mockEnv] */ From 706034536ac6259b9bc6314f6c454bf95b8f166d Mon Sep 17 00:00:00 2001 From: DeBuXer Date: Thu, 8 Aug 2024 17:04:20 +0200 Subject: [PATCH 07/13] Option to override the default cache TTL of 86400 seconds --- HOSTING.md | 1 + src/client.js | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/HOSTING.md b/HOSTING.md index 7441533..4fd8f5f 100644 --- a/HOSTING.md +++ b/HOSTING.md @@ -28,6 +28,7 @@ This guide will walk you through the process of setting up your own instance of `BLACKLIST_REDIRECT` | The URL to redirect to when a blacklisted host is accessed `HOME_DOMAIN` | The host to enable `/stat` endpoint `USE_LOCAL_DNS` | Default is `false`, so the Google DNS is used. Set it to `true` if you want to use the DNS resolver of your own host +`CACHE_EXPIRY_SECONDS` | Option to override the default cache TTL of 86400 seconds (1 day) If `WHITELIST_HOSTS` is set, `BLACKLIST_HOSTS` is ignored. Both is mutually exclusive. diff --git a/src/client.js b/src/client.js index 14ba96c..13a0770 100644 --- a/src/client.js +++ b/src/client.js @@ -25,6 +25,11 @@ import { */ let resolveCache = new LRUCache({ max: 10000 }); +/** + * @type {int | 86400} + */ +const cacheExpirySeconds = parseInt(process.env.CACHE_EXPIRY_SECONDS, 10) || 86400; + function pruneCache() { resolveCache = new LRUCache({ max: 10000 }); } @@ -71,7 +76,7 @@ async function buildCache(host) { url, expand, blacklisted: isHostBlacklisted(host), - expire: Date.now() + 86400 * 1000, + expire: Date.now() + cacheExpirySeconds * 1000, httpStatus: parseInt(httpStatus), }; } From 5f735897fcc9f5666c4d3cb4ce4138d4bb6a12d0 Mon Sep 17 00:00:00 2001 From: DeBuXer Date: Thu, 8 Aug 2024 16:08:43 +0200 Subject: [PATCH 08/13] Option to delete the cache for a specific domain via a POST request --- package.json | 3 ++- src/client.js | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 02460b8..a64965f 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "jose": "^5.6.3", "lru-cache": "^11.0.0", "node-forge": "^1.3.1", - "rsa-csr": "^1.0.6" + "rsa-csr": "^1.0.6", + "validator": "^13.9.0" }, "devDependencies": { "@types/bun": "^1.1.6" diff --git a/src/client.js b/src/client.js index 13a0770..0012fd1 100644 --- a/src/client.js +++ b/src/client.js @@ -1,5 +1,7 @@ import { LRUCache } from "lru-cache"; import { client, getStat } from "./sni.js"; +import querystring from 'querystring'; +import validator from 'validator'; import { findTxtRecord, isHostBlacklisted, @@ -12,6 +14,8 @@ import { isHttpCodeAllowed } from "./util.js"; +const MAX_DATA_SIZE = 10 * 1024; // 10 KB + /** * @typedef {Object} Cache * @property {string} url @@ -128,7 +132,42 @@ const listener = async function (req, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.write("ok"); return; + case '/flushcache': + if (req.method === 'POST') { + let body = ''; + let totalSize = 0; + + req.on('data', chunk => { + totalSize += chunk.length; + // Disconnect if the data stream is too large + if (totalSize > MAX_DATA_SIZE) { + req.destroy(); + return; + } + + body += chunk.toString(); + }); + + req.on('end', () => { + if (totalSize <= MAX_DATA_SIZE) { + const parsedData = querystring.parse(body); + const domain = parsedData.domain; + + if (validator.isFQDN(domain)) { + // Overwrite the cache for the domain with nothing + resolveCache.set(domain, ``); + } + } + }); + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.write("Cache cleared"); + return; + } + res.writeHead(405, {'Content-Type': 'text/plain'}); + res.write("Method Not Allowed"); + return; } + } let cache = resolveCache.get(host); if (!cache || (Date.now() > cache.expire)) { From ece96d27c656954442a30e51039d7b64d01d4d53 Mon Sep 17 00:00:00 2001 From: DeBuXer Date: Thu, 8 Aug 2024 16:37:14 +0200 Subject: [PATCH 09/13] Clear the cache only if it already exists --- src/client.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/client.js b/src/client.js index 0012fd1..cd476d2 100644 --- a/src/client.js +++ b/src/client.js @@ -154,8 +154,11 @@ const listener = async function (req, res) { const domain = parsedData.domain; if (validator.isFQDN(domain)) { - // Overwrite the cache for the domain with nothing - resolveCache.set(domain, ``); + const cacheExists = resolveCache.get(domain); + if (cacheExists !== null && cacheExists !== undefined && cacheExists !== '') { + // Overwrite the cache for the domain with nothing + resolveCache.set(domain, ``); + } } } }); From 847f4f1f734884f0ef09a713c77af821e17739b4 Mon Sep 17 00:00:00 2001 From: DeBuXer Date: Thu, 8 Aug 2024 17:13:16 +0200 Subject: [PATCH 10/13] When domain does not exist, then do not proceed --- src/client.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/client.js b/src/client.js index cd476d2..3496f09 100644 --- a/src/client.js +++ b/src/client.js @@ -153,6 +153,10 @@ const listener = async function (req, res) { const parsedData = querystring.parse(body); const domain = parsedData.domain; + if (!domain) { + return; + } + if (validator.isFQDN(domain)) { const cacheExists = resolveCache.get(domain); if (cacheExists !== null && cacheExists !== undefined && cacheExists !== '') { From 3b3fdd599b91b09698b9ad12fcd6882b41cfc1b9 Mon Sep 17 00:00:00 2001 From: DeBuXer Date: Thu, 8 Aug 2024 20:40:19 +0200 Subject: [PATCH 11/13] Update package-lock.json & use delete instead of set for clearing cache --- package-lock.json | 12 +++++++++++- src/client.js | 4 ++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 98aa157..94ab30c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,8 @@ "jose": "^5.6.3", "lru-cache": "^11.0.0", "node-forge": "^1.3.1", - "rsa-csr": "^1.0.6" + "rsa-csr": "^1.0.6", + "validator": "^13.9.0" }, "devDependencies": { "@types/bun": "^1.1.6" @@ -522,6 +523,15 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/src/client.js b/src/client.js index 3496f09..923321b 100644 --- a/src/client.js +++ b/src/client.js @@ -160,8 +160,8 @@ const listener = async function (req, res) { if (validator.isFQDN(domain)) { const cacheExists = resolveCache.get(domain); if (cacheExists !== null && cacheExists !== undefined && cacheExists !== '') { - // Overwrite the cache for the domain with nothing - resolveCache.set(domain, ``); + // Remove the cache entry + resolveCache.delete(domain); } } } From 697d0c9ec0734163e0e3ef4d9617c04cf4199827 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Sat, 3 Aug 2024 14:28:53 +0700 Subject: [PATCH 12/13] Save CA to DB Fix #26 --- package-lock.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 94ab30c..91cbfb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,8 +14,7 @@ "jose": "^5.6.3", "lru-cache": "^11.0.0", "node-forge": "^1.3.1", - "rsa-csr": "^1.0.6", - "validator": "^13.9.0" + "rsa-csr": "^1.0.6" }, "devDependencies": { "@types/bun": "^1.1.6" From 6f41e73e2918066afd9d5294d33921602d4846a4 Mon Sep 17 00:00:00 2001 From: DeBuXer Date: Mon, 12 Aug 2024 08:58:24 +0200 Subject: [PATCH 13/13] Update package-lock.json --- package-lock.json | 203 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 202 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 91cbfb2..7a06ae4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,13 @@ "dependencies": { "async-lock": "^1.4.1", "better-sqlite3": "^11.1.2", + "chokidar": "^3.5.3", + "dotenv": "^16.0.3", "jose": "^5.6.3", "lru-cache": "^11.0.0", "node-forge": "^1.3.1", - "rsa-csr": "^1.0.6" + "rsa-csr": "^1.0.6", + "validator": "^13.9.0" }, "devDependencies": { "@types/bun": "^1.1.6" @@ -50,6 +53,19 @@ "@types/node": "*" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/async-lock": { "version": "1.4.1", "license": "MIT" @@ -83,6 +99,18 @@ "prebuild-install": "^7.1.1" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -101,6 +129,18 @@ "readable-stream": "^3.4.0" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -134,6 +174,30 @@ "@types/ws": "~8.5.10" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -164,6 +228,18 @@ "node": ">=8" } }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -185,16 +261,54 @@ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -224,6 +338,48 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/jose": { "version": "5.6.3", "resolved": "https://registry.npmjs.org/jose/-/jose-5.6.3.tgz", @@ -288,6 +444,15 @@ "node": ">= 6.13.0" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -296,6 +461,18 @@ "wrappy": "1" } }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/prebuild-install": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", @@ -357,6 +534,18 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/rsa-csr": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/rsa-csr/-/rsa-csr-1.0.6.tgz", @@ -500,6 +689,18 @@ "node": ">=6" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",