Skip to content

Commit

Permalink
Save CA to DB
Browse files Browse the repository at this point in the history
Fix #26
  • Loading branch information
willnode committed Aug 3, 2024
1 parent a9ed8b7 commit 33d5621
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 66 deletions.
17 changes: 17 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -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": [
"<node_internals>/**"
],
"program": "${workspaceFolder}\\app.js"
}
]
}
53 changes: 25 additions & 28 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,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": {
Expand Down
10 changes: 5 additions & 5 deletions src/client.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { LRUCache } from "lru-cache";
import { client, getStat } from "./sni.js";
import { HashMap } from "hashmap-with-ttl";
import {
findTxtRecord,
isHostBlacklisted,
Expand All @@ -21,12 +21,12 @@ import {
* @property {number} httpStatus
*/
/**
* @type {HashMap<Cache>}
* @type {LRUCache<string, Cache>}
*/
let resolveCache = new HashMap({ capacity: 10000 });
let resolveCache = new LRUCache({ max: 10000 });

function pruneCache() {
resolveCache = new HashMap({ capacity: 10000 });
resolveCache = new LRUCache({ max: 10000 });
}

/**
Expand Down Expand Up @@ -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) {
Expand Down
52 changes: 28 additions & 24 deletions src/db.js
Original file line number Diff line number Diff line change
@@ -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";

/**
Expand All @@ -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
*/

Expand All @@ -40,35 +40,41 @@ export class CertsDB {
domain TEXT UNIQUE,
key BLOB,
cert BLOB,
ca BLOB,
expire INTEGER
)`).run();
db.prepare(`CREATE TABLE IF NOT EXISTS config (
key TEXT PRIMARY KEY,
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();
}
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;
}
Expand Down Expand Up @@ -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,
};
Expand All @@ -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
}
}
/**
Expand All @@ -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),
)
}
}
}
15 changes: 8 additions & 7 deletions src/sni.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ 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,
isHostBlacklisted,
ensureDirSync,
isExceedHostLimit,
isExceedLabelLimit,
validateCAARecords
validateCAARecords,
derToPem
} from "./util.js";

const lock = new AsyncLock();
Expand All @@ -24,9 +25,9 @@ ensureDirSync(certsDir);
const db = new CertsDB(dbFile);

/**
* @type {HashMap<import("./db.js").CertCache>}
* @type {LRUCache<string, import("./db.js").CertCache>}
*/
let resolveCache = new HashMap({ capacity: 10000 });
let resolveCache = new LRUCache({ max: 10000 });


/**
Expand All @@ -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() {
Expand All @@ -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,
};
Expand Down Expand Up @@ -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,
Expand Down
13 changes: 13 additions & 0 deletions src/tools/migrate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
}
23 changes: 22 additions & 1 deletion src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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$/;
Expand Down Expand Up @@ -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',
Expand Down
Loading

0 comments on commit 33d5621

Please sign in to comment.