fix: correct SSL cert for custom TLDs and immediate PAC update on vhost changes
CI / Build & Test (push) Successful in 3m18s
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:
+17
-12
@@ -441,29 +441,34 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.action === 'send') {
|
||||
send(message.payload)
|
||||
.then((r) => {
|
||||
// After a successful setVirtualHost, request host permission for any new TLD
|
||||
if (
|
||||
message.payload?.type === 'setVirtualHost' &&
|
||||
r && r.ok &&
|
||||
message.payload?.payload?.hostname
|
||||
) {
|
||||
// After a successful setVirtualHost or removeVirtualHost, refresh virtualHosts
|
||||
// and re-apply the PAC so the new TLD is routed immediately
|
||||
if (r && r.ok && (
|
||||
message.payload?.type === 'setVirtualHost' ||
|
||||
message.payload?.type === 'removeVirtualHost'
|
||||
)) {
|
||||
send({ type: 'getState', payload: {} }).then((state) => {
|
||||
if (state && state.ok && Array.isArray(state.virtualHosts)) {
|
||||
extensionState.virtualHosts = state.virtualHosts;
|
||||
const newTlds = getActiveTlds(extensionState.virtualHosts);
|
||||
applyPAC(newTlds);
|
||||
// Request host permission for any new TLD not already covered
|
||||
if (message.payload?.type === 'setVirtualHost' && message.payload?.payload?.hostname) {
|
||||
const hostname = message.payload.payload.hostname;
|
||||
const parts = hostname.split('.');
|
||||
if (parts.length >= 3) {
|
||||
const baseDomain = parts.slice(-2).join('.');
|
||||
if (baseDomain !== 'hole.sail') {
|
||||
const origin = '*://*.' + baseDomain + '/*';
|
||||
// Only request if not already in extensionState virtualHosts
|
||||
const alreadyKnown = (extensionState.virtualHosts || []).some(v => {
|
||||
const vParts = (v.hostname || '').split('.');
|
||||
return vParts.length >= 3 && vParts.slice(-2).join('.') === baseDomain;
|
||||
});
|
||||
if (!alreadyKnown && baseDomain !== 'hole.sail') {
|
||||
browser.permissions.request({ origins: [origin] }, (granted) => {
|
||||
log('permissions.request for', origin, ':', granted ? 'granted' : 'denied');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
reply(r);
|
||||
})
|
||||
.catch((err) => reply({ error: err.message }));
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
"notifications",
|
||||
"tabs",
|
||||
"declarativeNetRequest",
|
||||
"proxy",
|
||||
"permissions"
|
||||
"proxy"
|
||||
],
|
||||
"action": {
|
||||
"default_title": "Holesail Dashboard",
|
||||
|
||||
@@ -36,6 +36,31 @@ backupManager.setCertsPath(certificateAuthority.getCertsDir());
|
||||
const rdpManager = require('./rdp-manager.js');
|
||||
|
||||
httpsProxy.setHostnameResolver((hostname) => holesailManager.getLocalBackend(hostname));
|
||||
|
||||
/** Extract unique two-label base domains from current virtual hosts for the multi-SAN cert */
|
||||
function getActiveBaseDomains() {
|
||||
const seen = new Set(['hole.sail']);
|
||||
for (const v of holesailManager.getVirtualHosts()) {
|
||||
if (v && v.hostname) {
|
||||
const parts = v.hostname.split('.');
|
||||
if (parts.length >= 3) seen.add(parts.slice(-2).join('.'));
|
||||
}
|
||||
}
|
||||
return Array.from(seen);
|
||||
}
|
||||
|
||||
/** Restart the HTTPS proxy with a fresh multi-SAN cert covering all current TLDs */
|
||||
function refreshProxyCert() {
|
||||
const domains = getActiveBaseDomains();
|
||||
httpsProxy.restart(domains, (err) => {
|
||||
if (err) {
|
||||
if (process.stderr) process.stderr.write('[host] proxy cert refresh failed: ' + err.message + '\n');
|
||||
} else {
|
||||
if (process.stderr) process.stderr.write('[host] proxy cert refreshed for: ' + domains.join(', ') + '\n');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const PROXY_PORT = 8443;
|
||||
const CONNECT_PROXY_PORT = 8442;
|
||||
|
||||
@@ -308,6 +333,7 @@ async function handleMessageAsync(send, msg) {
|
||||
const result = await holesailManager.setVirtualHost(payload);
|
||||
debugLog('setVirtualHost: result=', JSON.stringify(result));
|
||||
reply(result);
|
||||
if (result && result.ok) refreshProxyCert();
|
||||
break;
|
||||
}
|
||||
case 'removeVirtualHost': {
|
||||
@@ -315,6 +341,7 @@ async function handleMessageAsync(send, msg) {
|
||||
const result = await holesailManager.removeVirtualHost(payload);
|
||||
debugLog('removeVirtualHost: result=', JSON.stringify(result));
|
||||
reply(result);
|
||||
if (result && result.ok) refreshProxyCert();
|
||||
break;
|
||||
}
|
||||
case 'getProxyPort': {
|
||||
|
||||
+47
-40
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user