Reverts dependencies changes
This commit is contained in:
parent
0c049fd2cb
commit
f691328476
4 changed files with 131 additions and 165 deletions
14
package.json
14
package.json
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "domain-forward",
|
"name": "domain-forward",
|
||||||
"version": "1.2.0",
|
"version": "1.1.0",
|
||||||
"description": "Service to forward domain via Hosterra DoH",
|
"description": "Service to forward domain via Hosterra DoH",
|
||||||
"main": "app.js",
|
"main": "app.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
@ -18,17 +18,11 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async-lock": "^1.4.0",
|
"async-lock": "^1.4.0",
|
||||||
"await-lock": "^2.2.2",
|
"await-lock": "^2.2.2",
|
||||||
"base32-encode": "^1.1.1",
|
|
||||||
"dns-packet": "^5.2.1",
|
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"jose": "^6.2.2",
|
"jose": "^3.20.4",
|
||||||
"rsa-csr": "^1.0.6",
|
|
||||||
"pem": "^1.14.7",
|
"pem": "^1.14.7",
|
||||||
"better-sqlite3": "^11.1.2",
|
"base32-encode": "^1.1.1",
|
||||||
"chokidar": "^3.5.3",
|
"dns-packet": "^5.2.1"
|
||||||
"lru-cache": "^11.0.0",
|
|
||||||
"node-forge": "^1.3.1",
|
|
||||||
"validator": "^13.9.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"pm2": "^5.3.0"
|
"pm2": "^5.3.0"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,17 @@
|
||||||
import rsacsr from "rsa-csr";
|
import fs from "fs";
|
||||||
import { exportJWK, generateKeyPair, calculateJwkThumbprint, SignJWT, CompactSign } from "jose";
|
import path from "path";
|
||||||
|
import {promisify} from "util";
|
||||||
|
import {exportJWK} from "jose";
|
||||||
|
import {generateKeyPair} from "jose";
|
||||||
|
import {calculateJwkThumbprint} from "jose";
|
||||||
|
import {SignJWT} from "jose";
|
||||||
|
import {CompactSign} from "jose";
|
||||||
|
import pem from "pem";
|
||||||
import * as common from "./common.js";
|
import * as common from "./common.js";
|
||||||
import request from "./request.js";
|
import request from "./request.js";
|
||||||
|
|
||||||
|
const createCsr = promisify(pem.createCSR);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a Let's Encrypt account and
|
* Represents a Let's Encrypt account and
|
||||||
* sends requests to get valid TLS certificates.
|
* sends requests to get valid TLS certificates.
|
||||||
|
|
@ -14,7 +24,7 @@ class Client {
|
||||||
this.accountPrivateJwk = null;
|
this.accountPrivateJwk = null;
|
||||||
/** @type {import('crypto').KeyObject|null} */
|
/** @type {import('crypto').KeyObject|null} */
|
||||||
this.accountPrivateKey = null;
|
this.accountPrivateKey = null;
|
||||||
/** @type {import("jose").JWK | undefined} */
|
/** @type {import("jose/types.js").JWK | undefined} */
|
||||||
this.accountPublicJwk = undefined;
|
this.accountPublicJwk = undefined;
|
||||||
/** @type {import('crypto').KeyObject|null} */
|
/** @type {import('crypto').KeyObject|null} */
|
||||||
this.accountPublicKey = null;
|
this.accountPublicKey = null;
|
||||||
|
|
@ -28,81 +38,88 @@ class Client {
|
||||||
this.replayNonce = '';
|
this.replayNonce = '';
|
||||||
this.thumbprint = '';
|
this.thumbprint = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export account public and private keys to a directory.
|
* Export account public and private keys to a directory.
|
||||||
*
|
*
|
||||||
|
* @param {String} dirname - name of directory to write key files to
|
||||||
* @param {String} [passphrase] - optional passphrase to encrypt private key with
|
* @param {String} [passphrase] - optional passphrase to encrypt private key with
|
||||||
*
|
*
|
||||||
* @return {{ privateKey: string, publicKey: string }}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
exportAccountKeyPair(passphrase) {
|
exportAccountKeyPair(dirname, passphrase) {
|
||||||
if (this.accountPrivateKey == null || this.accountPublicKey == null) {
|
if (this.accountPrivateKey == null || this.accountPublicKey == null) {
|
||||||
throw new Error('Account key pair not generated');
|
return Promise.reject(new Error('Account key pair not generated'));
|
||||||
}
|
|
||||||
return {
|
|
||||||
// @ts-ignore
|
|
||||||
accountPrivateKey: common.exportPrivateKey(this.accountPrivateKey, passphrase),
|
|
||||||
// @ts-ignore
|
|
||||||
accountPublicKey: common.exportPublicKey(this.accountPublicKey),
|
|
||||||
}
|
}
|
||||||
|
const privateKeyFile = path.join(dirname, 'privateKey.pem');
|
||||||
|
const publicKeyFile = path.join(dirname, 'publicKey.pem');
|
||||||
|
return Promise.all([
|
||||||
|
common.writeKeyToFile(privateKeyFile, this.accountPrivateKey, passphrase),
|
||||||
|
common.writeKeyToFile(publicKeyFile, this.accountPublicKey)
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate new account public and private keys.
|
* Generate new account public and private keys.
|
||||||
*
|
*
|
||||||
* @return {Promise<void>}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
async generateAccountKeyPair() {
|
async generateAccountKeyPair() {
|
||||||
const { privateKey, publicKey } = await generateKeyPair(common.ACCOUNT_KEY_ALGORITHM);
|
const {privateKey, publicKey} = await generateKeyPair(common.ACCOUNT_KEY_ALGORITHM);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.accountPrivateKey = privateKey;
|
this.accountPrivateKey = privateKey;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.accountPublicKey = publicKey;
|
this.accountPublicKey = publicKey;
|
||||||
await this.initAccountJwks();
|
await this.initAccountJwks();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a certificate from Let's Encrypt for your domain.
|
* Generate a certificate from Let's Encrypt for your domain.
|
||||||
*
|
*
|
||||||
* @param {String} domain - the domain you want a certificate for
|
* @param {String} domain - the domain you want a certificate for
|
||||||
*
|
*
|
||||||
* @return {Promise<{cert: string, key: string}>}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
async generateCertificate(domain) {
|
async generateCertificate(domain) {
|
||||||
await this.directory();
|
await this.directory();
|
||||||
await this.newNonce();
|
await this.newNonce();
|
||||||
if (!this.myAccountUrl)
|
if (!this.myAccountUrl)
|
||||||
await this.newAccount();
|
await this.newAccount();
|
||||||
const { authzUrls, finalizeUrl } = await this.newOrder(domain);
|
const {authzUrls, finalizeUrl} = await this.newOrder(domain);
|
||||||
const { challenge } = await this.authz(authzUrls[0]);
|
const {challenge} = await this.authz(authzUrls[0]);
|
||||||
await this.completeChallenge(challenge, domain);
|
await this.completeChallenge(challenge, domain);
|
||||||
await this.pollAuthz(authzUrls[0]);
|
await this.pollAuthz(authzUrls[0]);
|
||||||
const { certificate, privateKeyData } = await this.finalizeOrder(finalizeUrl, domain);
|
const {certificate, privateKeyData} = await this.finalizeOrder(finalizeUrl, domain);
|
||||||
return {
|
return {
|
||||||
cert: certificate,
|
certificate,
|
||||||
key: privateKeyData
|
privateKeyData
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Import account from PEM public and private keys.
|
* Import account public and private keys from a directory.
|
||||||
* @param {string} privateKey
|
*
|
||||||
* @param {string} publicKey
|
* @param {String} dirname - name of directory to read key files from
|
||||||
* @param {string | undefined} [passphrase]
|
* @param {String} [passphrase] - optional passphrase to decrypt private key with
|
||||||
* @return {Promise<void>}
|
*
|
||||||
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
async importAccountKeyPair(privateKey, publicKey, passphrase) {
|
async importAccountKeyPair(dirname, passphrase) {
|
||||||
this.accountPrivateKey = common.importPrivateKey(privateKey, passphrase);
|
const [privateKeyData, publicKeyData] = await Promise.all([
|
||||||
this.accountPublicKey = common.importPublicKey(publicKey);
|
fs.promises.readFile(path.join(dirname, 'privateKey.pem'), 'utf8'),
|
||||||
|
fs.promises.readFile(path.join(dirname, 'publicKey.pem'), 'utf8')
|
||||||
|
]);
|
||||||
|
this.accountPrivateKey = common.importPrivateKey(privateKeyData, passphrase);
|
||||||
|
this.accountPublicKey = common.importPublicKey(publicKeyData);
|
||||||
await this.initAccountJwks();
|
await this.initAccountJwks();
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* @param {string | URL} authzUrl
|
|
||||||
*/
|
|
||||||
async authz(authzUrl) {
|
async authz(authzUrl) {
|
||||||
const data = await this.sign({
|
const data = await this.sign({
|
||||||
kid: this.myAccountUrl,
|
kid: this.myAccountUrl,
|
||||||
nonce: this.replayNonce,
|
nonce: this.replayNonce,
|
||||||
url: authzUrl
|
url: authzUrl
|
||||||
});
|
});
|
||||||
/** @type {import("./request.js").Response<{challenges: {type: string}[], identifier: {value: string}, status: string}>} */
|
|
||||||
const res = await request(authzUrl, {
|
const res = await request(authzUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -114,22 +131,20 @@ class Client {
|
||||||
if (res.statusCode !== 200) {
|
if (res.statusCode !== 200) {
|
||||||
throw new Error(`authz() Status Code: ${res.statusCode} Data: ${res.data}`);
|
throw new Error(`authz() Status Code: ${res.statusCode} Data: ${res.data}`);
|
||||||
}
|
}
|
||||||
const { challenges, identifier, ...rest } = res.data;
|
const {challenges, identifier, ...rest} = res.data;
|
||||||
const challenge = challenges.find(({ type }) => type === 'http-01');
|
const challenge = challenges.find(({type}) => type === 'http-01');
|
||||||
return {
|
return {
|
||||||
challenge,
|
challenge,
|
||||||
domain: identifier.value,
|
domain: identifier.value,
|
||||||
...rest
|
...rest
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* @param {any} challenge
|
|
||||||
* @param {string} domain
|
|
||||||
*/
|
|
||||||
async completeChallenge(challenge, domain) {
|
async completeChallenge(challenge, domain) {
|
||||||
await this.readyChallenge(challenge);
|
await this.readyChallenge(challenge);
|
||||||
await this.receiveServerRequest(challenge, domain);
|
await this.receiveServerRequest(challenge, domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
async directory() {
|
async directory() {
|
||||||
if (this.hasDirectory)
|
if (this.hasDirectory)
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -143,9 +158,7 @@ class Client {
|
||||||
this.newOrderUrl = res.data.newOrder;
|
this.newOrderUrl = res.data.newOrder;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* @param {string | URL} certificateUrl
|
|
||||||
*/
|
|
||||||
async fetchCertificate(certificateUrl) {
|
async fetchCertificate(certificateUrl) {
|
||||||
const data = await this.sign({
|
const data = await this.sign({
|
||||||
kid: this.myAccountUrl,
|
kid: this.myAccountUrl,
|
||||||
|
|
@ -167,20 +180,17 @@ class Client {
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {any} finalizeUrl
|
|
||||||
* @param {string} domain
|
|
||||||
*/
|
|
||||||
async finalizeOrder(finalizeUrl, domain) {
|
async finalizeOrder(finalizeUrl, domain) {
|
||||||
const { privateKey } = await generateKeyPair(common.CERTIFICATE_KEY_ALGORITHM);
|
const {privateKey} = await generateKeyPair(common.CERTIFICATE_KEY_ALGORITHM);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const privateKeyData = common.exportPrivateKey(privateKey);
|
const clientKey = common.exportPrivateKey(privateKey);
|
||||||
|
let {
|
||||||
let csr = await rsacsr({
|
csr
|
||||||
key: await exportJWK(privateKey),
|
// @ts-ignore
|
||||||
domains: [domain],
|
} = await createCsr({
|
||||||
|
clientKey,
|
||||||
|
commonName: domain,
|
||||||
});
|
});
|
||||||
|
|
||||||
// "The CSR is sent in the base64url-encoded version of the DER format.
|
// "The CSR is sent in the base64url-encoded version of the DER format.
|
||||||
// (Note: Because this field uses base64url, and does not include headers,
|
// (Note: Because this field uses base64url, and does not include headers,
|
||||||
// it is different from PEM.)"
|
// it is different from PEM.)"
|
||||||
|
|
@ -192,48 +202,31 @@ class Client {
|
||||||
.replace(/\+/g, '-')
|
.replace(/\+/g, '-')
|
||||||
.replace(/\//g, '_')
|
.replace(/\//g, '_')
|
||||||
.replace(/=/g, '');
|
.replace(/=/g, '');
|
||||||
|
const data = await this.sign({
|
||||||
const sendFinalizeRequest = async (/** @type {string | URL} */ finalizeUrl, /** @type {import("jose").JWTPayload | undefined} */ payload) => {
|
kid: this.myAccountUrl,
|
||||||
const data = await this.sign({
|
nonce: this.replayNonce,
|
||||||
kid: this.myAccountUrl,
|
url: finalizeUrl
|
||||||
nonce: this.replayNonce,
|
}, {
|
||||||
url: finalizeUrl
|
csr
|
||||||
}, payload);
|
});
|
||||||
|
const res = await request(finalizeUrl, {
|
||||||
const res = await request(finalizeUrl, {
|
method: 'POST',
|
||||||
method: 'POST',
|
headers: {
|
||||||
headers: {
|
'content-type': 'application/jose+json'
|
||||||
'content-type': 'application/jose+json'
|
},
|
||||||
},
|
data
|
||||||
data
|
});
|
||||||
});
|
this.setReplayNonce(res);
|
||||||
this.setReplayNonce(res);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
let res = await sendFinalizeRequest(finalizeUrl, { csr });
|
|
||||||
// Let's encrypt actually want this to work!
|
|
||||||
// https://community.letsencrypt.org/t/enabling-asynchronous-order-finalization/193522
|
|
||||||
while (res.data.status === 'processing') {
|
|
||||||
let retryUrl = res.headers.location || '';
|
|
||||||
let retryTime = parseInt(res.headers["retry-after"] || '1') * 1000
|
|
||||||
// sleep, retry
|
|
||||||
await new Promise(resolve => setTimeout(resolve, retryTime))
|
|
||||||
res = await sendFinalizeRequest(retryUrl, undefined);
|
|
||||||
if (res.data.status == "ready") {
|
|
||||||
res = await sendFinalizeRequest(res.data.finalize, undefined);
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (res.statusCode !== 200) {
|
if (res.statusCode !== 200) {
|
||||||
throw new Error(`finalizeOrder() Status Code: ${res.statusCode} Data: ${res.data}`);
|
throw new Error(`finalizeOrder() Status Code: ${res.statusCode} Data: ${res.data}`);
|
||||||
}
|
}
|
||||||
const certificate = await this.fetchCertificate(res.data.certificate);
|
const certificate = await this.fetchCertificate(res.data.certificate);
|
||||||
return {
|
return {
|
||||||
certificate,
|
certificate,
|
||||||
privateKeyData
|
privateKeyData: clientKey
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async initAccountJwks() {
|
async initAccountJwks() {
|
||||||
if (this.accountPrivateKey == null || this.accountPublicKey == null) {
|
if (this.accountPrivateKey == null || this.accountPublicKey == null) {
|
||||||
return Promise.reject(new Error('Account key pair not generated'));
|
return Promise.reject(new Error('Account key pair not generated'));
|
||||||
|
|
@ -246,9 +239,7 @@ class Client {
|
||||||
this.accountPrivateJwk = accountPrivateJwk;
|
this.accountPrivateJwk = accountPrivateJwk;
|
||||||
this.thumbprint = await calculateJwkThumbprint(publicJwk);
|
this.thumbprint = await calculateJwkThumbprint(publicJwk);
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* @param {undefined[]} emails
|
|
||||||
*/
|
|
||||||
async newAccount(...emails) {
|
async newAccount(...emails) {
|
||||||
const data = await this.sign({
|
const data = await this.sign({
|
||||||
jwk: this.accountPublicJwk,
|
jwk: this.accountPublicJwk,
|
||||||
|
|
@ -268,9 +259,10 @@ class Client {
|
||||||
if (![200, 201].includes(res.statusCode)) {
|
if (![200, 201].includes(res.statusCode)) {
|
||||||
throw new Error(`newAccount() Status Code: ${res.statusCode} Data: ${res.data}`);
|
throw new Error(`newAccount() Status Code: ${res.statusCode} Data: ${res.data}`);
|
||||||
}
|
}
|
||||||
this.myAccountUrl = res.headers.location || "";
|
this.myAccountUrl = res.headers.location;
|
||||||
return res.statusCode === 201;
|
return res.statusCode === 201;
|
||||||
}
|
}
|
||||||
|
|
||||||
async newNonce() {
|
async newNonce() {
|
||||||
if (this.replayNonce)
|
if (this.replayNonce)
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -283,9 +275,7 @@ class Client {
|
||||||
this.setReplayNonce(res);
|
this.setReplayNonce(res);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* @param {string[]} domains
|
|
||||||
*/
|
|
||||||
async newOrder(...domains) {
|
async newOrder(...domains) {
|
||||||
const identifiers = domains.map(domain => ({
|
const identifiers = domains.map(domain => ({
|
||||||
type: 'dns',
|
type: 'dns',
|
||||||
|
|
@ -310,7 +300,7 @@ class Client {
|
||||||
throw new Error(`newOrder() Status Code: ${res.statusCode} Data: ${res.data}`);
|
throw new Error(`newOrder() Status Code: ${res.statusCode} Data: ${res.data}`);
|
||||||
}
|
}
|
||||||
const orderUrl = res.headers.location;
|
const orderUrl = res.headers.location;
|
||||||
const { authorizations: authzUrls, finalize: finalizeUrl } = res.data;
|
const {authorizations: authzUrls, finalize: finalizeUrl} = res.data;
|
||||||
return {
|
return {
|
||||||
authzUrls,
|
authzUrls,
|
||||||
domains,
|
domains,
|
||||||
|
|
@ -318,14 +308,12 @@ class Client {
|
||||||
orderUrl
|
orderUrl
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* @param {any} authzUrl
|
|
||||||
*/
|
|
||||||
async pollAuthz(authzUrl) {
|
async pollAuthz(authzUrl) {
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
const result = await this.authz(authzUrl);
|
const result = await this.authz(authzUrl);
|
||||||
if (result.status === 'pending') {
|
if (result.status === 'pending') {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1e3));
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (result.status === 'invalid') {
|
if (result.status === 'invalid') {
|
||||||
|
|
@ -335,9 +323,7 @@ class Client {
|
||||||
}
|
}
|
||||||
throw new Error('pollAuthz() timed out');
|
throw new Error('pollAuthz() timed out');
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* @param {{ url: string | URL; }} challenge
|
|
||||||
*/
|
|
||||||
async readyChallenge(challenge) {
|
async readyChallenge(challenge) {
|
||||||
const data = await this.sign({
|
const data = await this.sign({
|
||||||
kid: this.myAccountUrl,
|
kid: this.myAccountUrl,
|
||||||
|
|
@ -356,19 +342,16 @@ class Client {
|
||||||
throw new Error(`readyChallenge() Status Code: ${res.statusCode} Data: ${res.data}`);
|
throw new Error(`readyChallenge() Status Code: ${res.statusCode} Data: ${res.data}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* @param {{ token: string; }} challenge
|
|
||||||
* @param {any} domain
|
|
||||||
*/
|
|
||||||
receiveServerRequest(challenge, domain) {
|
receiveServerRequest(challenge, domain) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const time = setTimeout(() => {
|
const time = setTimeout(() => {
|
||||||
reject(new Error('Timed out waiting for server request'));
|
reject(new Error('Timed out waiting for server request'));
|
||||||
}, 10e3);
|
}, 5000);
|
||||||
let hasResolved = false;
|
let hasResolved = false;
|
||||||
this.challengeCallbacks = () => {
|
this.challengeCallbacks = () => {
|
||||||
if (!hasResolved)
|
if (!hasResolved)
|
||||||
setTimeout(resolve, 100);
|
setTimeout(resolve, 5000);
|
||||||
else
|
else
|
||||||
return challenge.token + '.' + this.thumbprint;
|
return challenge.token + '.' + this.thumbprint;
|
||||||
hasResolved = true;
|
hasResolved = true;
|
||||||
|
|
@ -379,9 +362,7 @@ class Client {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* @param {{ data?: any; headers: any; statusCode?: number; }} res
|
|
||||||
*/
|
|
||||||
setReplayNonce(res) {
|
setReplayNonce(res) {
|
||||||
const replayNonce = (res.headers['replay-nonce'] || '').trim();
|
const replayNonce = (res.headers['replay-nonce'] || '').trim();
|
||||||
if (!replayNonce) {
|
if (!replayNonce) {
|
||||||
|
|
@ -389,9 +370,10 @@ class Client {
|
||||||
}
|
}
|
||||||
this.replayNonce = replayNonce;
|
this.replayNonce = replayNonce;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import("jose").JWSHeaderParameters} header
|
* @param {import("jose/types.js").JWSHeaderParameters} header
|
||||||
* @param {import("jose").JWTPayload | undefined} [payload]
|
* @param {import("jose/types.js").JWTPayload | undefined} [payload]
|
||||||
*/
|
*/
|
||||||
async sign(header, payload) {
|
async sign(header, payload) {
|
||||||
if (this.accountPrivateKey == null) {
|
if (this.accountPrivateKey == null) {
|
||||||
|
|
@ -400,14 +382,12 @@ class Client {
|
||||||
let data;
|
let data;
|
||||||
if (payload) {
|
if (payload) {
|
||||||
data = await new SignJWT(payload)
|
data = await new SignJWT(payload)
|
||||||
// @ts-ignore
|
|
||||||
.setProtectedHeader({
|
.setProtectedHeader({
|
||||||
alg: common.ACCOUNT_KEY_ALGORITHM,
|
alg: common.ACCOUNT_KEY_ALGORITHM,
|
||||||
...header
|
...header
|
||||||
})
|
})
|
||||||
.sign(this.accountPrivateKey);
|
.sign(this.accountPrivateKey);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// SignJWT constructor only accepts object but RFC8555 requires empty payload
|
// SignJWT constructor only accepts object but RFC8555 requires empty payload
|
||||||
// Workaround: manually pass empty Uint8Array to CompactSign constructor
|
// Workaround: manually pass empty Uint8Array to CompactSign constructor
|
||||||
const sig = new CompactSign(new Uint8Array());
|
const sig = new CompactSign(new Uint8Array());
|
||||||
|
|
@ -425,4 +405,5 @@ class Client {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Client;
|
export default Client;
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import crypto from "node:crypto";
|
import crypto from "crypto";
|
||||||
import fs from "node:fs";
|
import fs from "fs";
|
||||||
|
|
||||||
export const ACCOUNT_KEY_ALGORITHM = 'ES256';
|
export const ACCOUNT_KEY_ALGORITHM = 'ES256';
|
||||||
export const CERTIFICATE_KEY_ALGORITHM = 'RS256';
|
export const CERTIFICATE_KEY_ALGORITHM = 'RS256';
|
||||||
export const DIRECTORY_URL = {
|
const env = (process.env.NODE_ENV || '').trim().toLowerCase();
|
||||||
'production': 'https://acme-v02.api.letsencrypt.org/directory',
|
export const DIRECTORY_URL = ['development', 'test'].includes(env)
|
||||||
'development': 'https://acme-staging-v02.api.letsencrypt.org/directory',
|
? 'https://acme-staging-v02.api.letsencrypt.org/directory'
|
||||||
'test': 'https://localhost:14000/dir',
|
: 'https://acme-v02.api.letsencrypt.org/directory';
|
||||||
}[(process.env.NODE_ENV || 'development').trim().toLowerCase()] || ""
|
|
||||||
export const PRIVATE_KEY_CIPHER = 'aes-256-cbc';
|
export const PRIVATE_KEY_CIPHER = 'aes-256-cbc';
|
||||||
export const PRIVATE_KEY_FORMAT = 'pem';
|
export const PRIVATE_KEY_FORMAT = 'pem';
|
||||||
export const PRIVATE_KEY_PERMISSIONS = 0o600;
|
export const PRIVATE_KEY_PERMISSIONS = 0o600;
|
||||||
|
|
@ -14,10 +14,11 @@ export const PRIVATE_KEY_TYPE = 'pkcs8';
|
||||||
export const PUBLIC_KEY_FORMAT = 'pem';
|
export const PUBLIC_KEY_FORMAT = 'pem';
|
||||||
export const PUBLIC_KEY_PERMISSIONS = 0o666;
|
export const PUBLIC_KEY_PERMISSIONS = 0o666;
|
||||||
export const PUBLIC_KEY_TYPE = 'spki';
|
export const PUBLIC_KEY_TYPE = 'spki';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {crypto.KeyObject} privateKey
|
* @param {crypto.KeyObject} privateKey
|
||||||
* @param {String} [passphrase]
|
* @param {String} [passphrase]
|
||||||
* @returns {string}
|
*
|
||||||
*/
|
*/
|
||||||
export const exportPrivateKey = (privateKey, passphrase) => {
|
export const exportPrivateKey = (privateKey, passphrase) => {
|
||||||
/** @type {crypto.KeyExportOptions<'pem'>} */
|
/** @type {crypto.KeyExportOptions<'pem'>} */
|
||||||
|
|
@ -29,21 +30,20 @@ export const exportPrivateKey = (privateKey, passphrase) => {
|
||||||
privateKeyOpts.cipher = PRIVATE_KEY_CIPHER;
|
privateKeyOpts.cipher = PRIVATE_KEY_CIPHER;
|
||||||
privateKeyOpts.passphrase = passphrase;
|
privateKeyOpts.passphrase = passphrase;
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
|
||||||
return privateKey.export(privateKeyOpts);
|
return privateKey.export(privateKeyOpts);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {crypto.KeyObject} publicKey
|
* @param {crypto.KeyObject} publicKey
|
||||||
* @returns {string}
|
|
||||||
*/
|
*/
|
||||||
export const exportPublicKey = publicKey => {
|
export const exportPublicKey = publicKey => {
|
||||||
/** @type {crypto.KeyExportOptions<'pem'>} */
|
/** @type {crypto.KeyExportOptions<'pem'>} */
|
||||||
// @ts-ignore
|
|
||||||
return publicKey.export({
|
return publicKey.export({
|
||||||
type: PUBLIC_KEY_TYPE,
|
type: PUBLIC_KEY_TYPE,
|
||||||
format: PUBLIC_KEY_FORMAT
|
format: PUBLIC_KEY_FORMAT
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {String} privateKeyData
|
* @param {String} privateKeyData
|
||||||
* @param {String} [passphrase]
|
* @param {String} [passphrase]
|
||||||
|
|
@ -62,11 +62,11 @@ export const importPrivateKey = (privateKeyData, passphrase) => {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return crypto.createPrivateKey(privateKeyOpts);
|
return crypto.createPrivateKey(privateKeyOpts);
|
||||||
}
|
} catch {
|
||||||
catch {
|
|
||||||
throw new Error('Failed to import private key');
|
throw new Error('Failed to import private key');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {String} publicKeyData
|
* @param {String} publicKeyData
|
||||||
*
|
*
|
||||||
|
|
@ -79,25 +79,24 @@ export const importPublicKey = publicKeyData => {
|
||||||
format: PUBLIC_KEY_FORMAT,
|
format: PUBLIC_KEY_FORMAT,
|
||||||
type: PUBLIC_KEY_TYPE
|
type: PUBLIC_KEY_TYPE
|
||||||
});
|
});
|
||||||
}
|
} catch {
|
||||||
catch {
|
|
||||||
throw new Error('Failed to import public key');
|
throw new Error('Failed to import public key');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {String} filename
|
* @param {String} filename
|
||||||
* @param {crypto.KeyObject|string} key
|
* @param {crypto.KeyObject|string} key
|
||||||
* @param {String} [passphrase]
|
* @param {String} [passphrase]
|
||||||
*
|
*
|
||||||
* @return {Promise<void>}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
export const writeKeyToFile = async (filename, key, passphrase) => {
|
export const writeKeyToFile = async (filename, key, passphrase) => {
|
||||||
if (typeof key === 'string') {
|
if (typeof key === 'string') {
|
||||||
key = key.includes('PRIVATE KEY')
|
key = key.includes('PRIVATE KEY')
|
||||||
? importPrivateKey(key, passphrase)
|
? importPrivateKey(key, passphrase)
|
||||||
: importPublicKey(key);
|
: importPublicKey(key);
|
||||||
}
|
} else if (!(key instanceof crypto.KeyObject)) {
|
||||||
else if (!(key instanceof crypto.KeyObject)) {
|
|
||||||
throw new Error('Expected "key" to be crypto.KeyObject or string');
|
throw new Error('Expected "key" to be crypto.KeyObject or string');
|
||||||
}
|
}
|
||||||
const isPrivateKey = key.type === 'private';
|
const isPrivateKey = key.type === 'private';
|
||||||
|
|
@ -105,5 +104,6 @@ export const writeKeyToFile = async (filename, key, passphrase) => {
|
||||||
? exportPrivateKey(key, passphrase)
|
? exportPrivateKey(key, passphrase)
|
||||||
: exportPublicKey(key);
|
: exportPublicKey(key);
|
||||||
const mode = isPrivateKey ? PRIVATE_KEY_PERMISSIONS : PUBLIC_KEY_PERMISSIONS;
|
const mode = isPrivateKey ? PRIVATE_KEY_PERMISSIONS : PUBLIC_KEY_PERMISSIONS;
|
||||||
await fs.promises.writeFile(filename, keyData, { mode });
|
await fs.promises.writeFile(filename, keyData, {mode});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,20 @@
|
||||||
import http from "node:http";
|
import https from "https";
|
||||||
import https from "node:https";
|
|
||||||
/**
|
|
||||||
* @template T
|
|
||||||
* @typedef {{data: T, headers: import('http').IncomingHttpHeaders, statusCode: number}} Response
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @template T
|
|
||||||
* @param {string | URL} url
|
* @param {string | URL} url
|
||||||
* @param {import('https').RequestOptions & {data?: string}} [options]
|
* @param {import('https').RequestOptions & {data?: string}} [options]
|
||||||
* @return {Promise<Response<T>>}
|
* @param {() => void} [cb]
|
||||||
|
* @return {Promise<{data: any, headers: import('http').IncomingHttpHeaders, statusCode: number}>}
|
||||||
*/
|
*/
|
||||||
const request = (url, { data = '', ...options } = {}) => {
|
const request = (url, {data = '', ...options} = {}, cb) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
url = new URL(url);
|
url = new URL(url);
|
||||||
}
|
} catch (err) {
|
||||||
catch (err) {
|
|
||||||
return reject(err);
|
return reject(err);
|
||||||
}
|
}
|
||||||
(url.protocol == 'https:' ? https : http).request(url, options, res => {
|
https.request(url, options, res => {
|
||||||
const { statusCode, headers } = res;
|
const {statusCode, headers} = res;
|
||||||
/**
|
|
||||||
* @type {any}
|
|
||||||
*/
|
|
||||||
let data = '';
|
let data = '';
|
||||||
res
|
res
|
||||||
.on('data', chunk => {
|
.on('data', chunk => {
|
||||||
|
|
@ -33,13 +24,12 @@ const request = (url, { data = '', ...options } = {}) => {
|
||||||
if (headers['content-type']?.includes('application/json')) {
|
if (headers['content-type']?.includes('application/json')) {
|
||||||
try {
|
try {
|
||||||
data = JSON.parse(data);
|
data = JSON.parse(data);
|
||||||
}
|
} catch (err) {
|
||||||
catch (err) {
|
|
||||||
reject(err);
|
reject(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resolve({ data, headers, statusCode: statusCode || 0 });
|
resolve({data, headers, statusCode: statusCode || 0});
|
||||||
})
|
})
|
||||||
.once('error', reject);
|
.once('error', reject);
|
||||||
})
|
})
|
||||||
|
|
@ -49,6 +39,7 @@ const request = (url, { data = '', ...options } = {}) => {
|
||||||
const method = options.method || 'GET';
|
const method = options.method || 'GET';
|
||||||
reject(new Error(`${method} request to "${url}" timed out`));
|
reject(new Error(`${method} request to "${url}" timed out`));
|
||||||
}, 10e3);
|
}, 10e3);
|
||||||
|
cb && cb();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
export default request;
|
export default request;
|
||||||
Loading…
Add table
editor.link_modal.header
Reference in a new issue