fix: correct SSL cert for custom TLDs and immediate PAC update on vhost changes
CI / Build & Test (push) Successful in 3m18s

- Replace SNI callback approach (unsupported by bare-tls) with a multi-SAN
  wildcard cert covering all active base domains (e.g. *.hole.sail, *.heheh.jungle)
- Add buildMultiSanCert() in https-proxy.js — cert key encodes sorted domain list
  so cache invalidates automatically when TLDs are added or removed
- Add restart(baseDomains, callback) to https-proxy.js to stop and restart the
  proxy with a fresh cert without requiring a native host restart
- Add getActiveBaseDomains() and refreshProxyCert() in host.js; call after every
  successful setVirtualHost and removeVirtualHost
- Fix background.js send handler to fetch updated virtualHosts via getState and
  re-apply PAC immediately after setVirtualHost or removeVirtualHost succeeds,
  so new TLDs are routed without waiting for the next dashboard refresh
- Remove unused tls require from https-proxy.js
- Remove invalid "permissions" entry from manifest.json permissions array
This commit is contained in:
Raven Scott
2026-02-28 20:30:26 -05:00
parent 00d9c958ca
commit 82c75b289e
4 changed files with 101 additions and 63 deletions
+47 -40
View File
@@ -5,7 +5,6 @@
const path = require('bare-path');
const fs = require('bare-fs');
const tls = require('tls');
let https = null;
let http = null;
@@ -29,6 +28,7 @@ function debugLog(...args) {
let proxyServer = null;
let proxyPort = null;
let proxyCertsDirOrCA = null; // saved so restart() can regenerate the cert
/** Resolver: hostname -> { host, port } or port number (then host defaults to 127.0.0.1) or null */
let getBackendForHostname = null;
@@ -36,7 +36,27 @@ function setHostnameResolver(fn) {
getBackendForHostname = fn;
}
function start(port, certsDirOrCA, callback) {
/**
* Build a multi-SAN wildcard cert covering all given base domains.
* e.g. baseDomains = ['hole.sail', 'heheh.jungle']
* produces SANs: *.hole.sail, hole.sail, *.heheh.jungle, heheh.jungle
* The cert is keyed by a stable name "multi-wildcard-<sorted-domains>" so it
* is regenerated whenever the domain list changes.
*/
function buildMultiSanCert(certsDirOrCA, baseDomains) {
const sorted = Array.from(new Set(['hole.sail', ...baseDomains])).sort();
// The cert key encodes the exact domain list — a different list gets a different
// directory, so the cache naturally invalidates when TLDs are added/removed.
const certKey = 'multi-wildcard-' + sorted.join('_');
const altNames = [];
for (const bd of sorted) {
altNames.push({ type: 2, value: '*.' + bd });
altNames.push({ type: 2, value: bd });
}
return certsDirOrCA.getOrCreateDomainCert(certKey, altNames);
}
function start(port, certsDirOrCA, callback, baseDomains) {
if (!https || !http) {
if (callback) callback(new Error('bare-https or bare-http1 not available'));
return null;
@@ -51,48 +71,17 @@ function start(port, certsDirOrCA, callback) {
return null;
}
// Pre-generate the baseline *.hole.sail cert so the server can start immediately.
// Additional TLD certs are generated lazily via SNICallback on first connection.
const baseResult = certsDirOrCA.getOrCreateDomainCert('*.hole.sail', [
{ type: 2, value: '*.hole.sail' },
{ type: 2, value: 'hole.sail' }
]);
if (!baseResult) {
if (callback) callback(new Error('Could not get baseline wildcard cert for *.hole.sail'));
proxyCertsDirOrCA = certsDirOrCA;
const result = buildMultiSanCert(certsDirOrCA, baseDomains || []);
if (!result) {
if (callback) callback(new Error('Could not generate multi-SAN wildcard cert'));
return null;
}
// SNI callback — lazily issues a wildcard cert per two-label base domain
function sniCallback(servername, cb) {
const parts = (servername || '').split('.');
// Need at least 3 labels (host.second.tld) to derive a 2-label base domain
const baseDomain = parts.length >= 3 ? parts.slice(-2).join('.') : 'hole.sail';
const wildcard = '*.' + baseDomain;
debugLog('sniCallback: servername=', servername, 'baseDomain=', baseDomain, 'wildcard=', wildcard);
const result = certsDirOrCA.getOrCreateDomainCert(wildcard, [
{ type: 2, value: wildcard },
{ type: 2, value: baseDomain }
]);
if (!result) {
if (process.stderr) process.stderr.write('[https-proxy] SNI: no cert for ' + wildcard + '\n');
return cb(new Error('No cert for ' + wildcard));
}
try {
const ctx = tls.createSecureContext({
cert: typeof result.cert === 'string' ? result.cert : result.cert.toString(),
key: typeof result.key === 'string' ? result.key : result.key.toString()
});
cb(null, ctx);
} catch (e) {
if (process.stderr) process.stderr.write('[https-proxy] SNI createSecureContext error: ' + e.message + '\n');
cb(e);
}
}
const opts = {
cert: typeof baseResult.cert === 'string' ? Buffer.from(baseResult.cert) : baseResult.cert,
key: typeof baseResult.key === 'string' ? Buffer.from(baseResult.key) : baseResult.key,
SNICallback: sniCallback
cert: typeof result.cert === 'string' ? Buffer.from(result.cert) : result.cert,
key: typeof result.key === 'string' ? Buffer.from(result.key) : result.key
};
let callbackCalled = false;
@@ -342,6 +331,23 @@ function stop(callback) {
});
}
/**
* Restart the HTTPS proxy with an updated multi-SAN cert covering the given
* base domains (e.g. ['hole.sail', 'heheh.jungle']). Existing connections
* finish naturally; new connections get the new cert immediately.
*/
function restart(baseDomains, callback) {
const savedPort = proxyPort || DEFAULT_PORT;
const ca = proxyCertsDirOrCA;
if (!ca) {
if (callback) callback(new Error('proxy not yet started'));
return;
}
stop(() => {
start(savedPort, ca, callback, baseDomains);
});
}
function getPort() {
return proxyPort;
}
@@ -350,5 +356,6 @@ module.exports = {
setHostnameResolver,
start,
stop,
restart,
getPort
};