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:
+26
-21
@@ -441,28 +441,33 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
if (message.action === 'send') {
|
if (message.action === 'send') {
|
||||||
send(message.payload)
|
send(message.payload)
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
// After a successful setVirtualHost, request host permission for any new TLD
|
// After a successful setVirtualHost or removeVirtualHost, refresh virtualHosts
|
||||||
if (
|
// and re-apply the PAC so the new TLD is routed immediately
|
||||||
message.payload?.type === 'setVirtualHost' &&
|
if (r && r.ok && (
|
||||||
r && r.ok &&
|
message.payload?.type === 'setVirtualHost' ||
|
||||||
message.payload?.payload?.hostname
|
message.payload?.type === 'removeVirtualHost'
|
||||||
) {
|
)) {
|
||||||
const hostname = message.payload.payload.hostname;
|
send({ type: 'getState', payload: {} }).then((state) => {
|
||||||
const parts = hostname.split('.');
|
if (state && state.ok && Array.isArray(state.virtualHosts)) {
|
||||||
if (parts.length >= 3) {
|
extensionState.virtualHosts = state.virtualHosts;
|
||||||
const baseDomain = parts.slice(-2).join('.');
|
const newTlds = getActiveTlds(extensionState.virtualHosts);
|
||||||
const origin = '*://*.' + baseDomain + '/*';
|
applyPAC(newTlds);
|
||||||
// Only request if not already in extensionState virtualHosts
|
// Request host permission for any new TLD not already covered
|
||||||
const alreadyKnown = (extensionState.virtualHosts || []).some(v => {
|
if (message.payload?.type === 'setVirtualHost' && message.payload?.payload?.hostname) {
|
||||||
const vParts = (v.hostname || '').split('.');
|
const hostname = message.payload.payload.hostname;
|
||||||
return vParts.length >= 3 && vParts.slice(-2).join('.') === baseDomain;
|
const parts = hostname.split('.');
|
||||||
});
|
if (parts.length >= 3) {
|
||||||
if (!alreadyKnown && baseDomain !== 'hole.sail') {
|
const baseDomain = parts.slice(-2).join('.');
|
||||||
browser.permissions.request({ origins: [origin] }, (granted) => {
|
if (baseDomain !== 'hole.sail') {
|
||||||
log('permissions.request for', origin, ':', granted ? 'granted' : 'denied');
|
const origin = '*://*.' + baseDomain + '/*';
|
||||||
});
|
browser.permissions.request({ origins: [origin] }, (granted) => {
|
||||||
|
log('permissions.request for', origin, ':', granted ? 'granted' : 'denied');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
reply(r);
|
reply(r);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,8 +9,7 @@
|
|||||||
"notifications",
|
"notifications",
|
||||||
"tabs",
|
"tabs",
|
||||||
"declarativeNetRequest",
|
"declarativeNetRequest",
|
||||||
"proxy",
|
"proxy"
|
||||||
"permissions"
|
|
||||||
],
|
],
|
||||||
"action": {
|
"action": {
|
||||||
"default_title": "Holesail Dashboard",
|
"default_title": "Holesail Dashboard",
|
||||||
|
|||||||
@@ -36,6 +36,31 @@ backupManager.setCertsPath(certificateAuthority.getCertsDir());
|
|||||||
const rdpManager = require('./rdp-manager.js');
|
const rdpManager = require('./rdp-manager.js');
|
||||||
|
|
||||||
httpsProxy.setHostnameResolver((hostname) => holesailManager.getLocalBackend(hostname));
|
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 PROXY_PORT = 8443;
|
||||||
const CONNECT_PROXY_PORT = 8442;
|
const CONNECT_PROXY_PORT = 8442;
|
||||||
|
|
||||||
@@ -308,6 +333,7 @@ async function handleMessageAsync(send, msg) {
|
|||||||
const result = await holesailManager.setVirtualHost(payload);
|
const result = await holesailManager.setVirtualHost(payload);
|
||||||
debugLog('setVirtualHost: result=', JSON.stringify(result));
|
debugLog('setVirtualHost: result=', JSON.stringify(result));
|
||||||
reply(result);
|
reply(result);
|
||||||
|
if (result && result.ok) refreshProxyCert();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'removeVirtualHost': {
|
case 'removeVirtualHost': {
|
||||||
@@ -315,6 +341,7 @@ async function handleMessageAsync(send, msg) {
|
|||||||
const result = await holesailManager.removeVirtualHost(payload);
|
const result = await holesailManager.removeVirtualHost(payload);
|
||||||
debugLog('removeVirtualHost: result=', JSON.stringify(result));
|
debugLog('removeVirtualHost: result=', JSON.stringify(result));
|
||||||
reply(result);
|
reply(result);
|
||||||
|
if (result && result.ok) refreshProxyCert();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'getProxyPort': {
|
case 'getProxyPort': {
|
||||||
|
|||||||
+47
-40
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
const path = require('bare-path');
|
const path = require('bare-path');
|
||||||
const fs = require('bare-fs');
|
const fs = require('bare-fs');
|
||||||
const tls = require('tls');
|
|
||||||
|
|
||||||
let https = null;
|
let https = null;
|
||||||
let http = null;
|
let http = null;
|
||||||
@@ -29,6 +28,7 @@ function debugLog(...args) {
|
|||||||
|
|
||||||
let proxyServer = null;
|
let proxyServer = null;
|
||||||
let proxyPort = 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 */
|
/** Resolver: hostname -> { host, port } or port number (then host defaults to 127.0.0.1) or null */
|
||||||
let getBackendForHostname = null;
|
let getBackendForHostname = null;
|
||||||
|
|
||||||
@@ -36,7 +36,27 @@ function setHostnameResolver(fn) {
|
|||||||
getBackendForHostname = 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 (!https || !http) {
|
||||||
if (callback) callback(new Error('bare-https or bare-http1 not available'));
|
if (callback) callback(new Error('bare-https or bare-http1 not available'));
|
||||||
return null;
|
return null;
|
||||||
@@ -51,48 +71,17 @@ function start(port, certsDirOrCA, callback) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-generate the baseline *.hole.sail cert so the server can start immediately.
|
proxyCertsDirOrCA = certsDirOrCA;
|
||||||
// Additional TLD certs are generated lazily via SNICallback on first connection.
|
|
||||||
const baseResult = certsDirOrCA.getOrCreateDomainCert('*.hole.sail', [
|
const result = buildMultiSanCert(certsDirOrCA, baseDomains || []);
|
||||||
{ type: 2, value: '*.hole.sail' },
|
if (!result) {
|
||||||
{ type: 2, value: 'hole.sail' }
|
if (callback) callback(new Error('Could not generate multi-SAN wildcard cert'));
|
||||||
]);
|
|
||||||
if (!baseResult) {
|
|
||||||
if (callback) callback(new Error('Could not get baseline wildcard cert for *.hole.sail'));
|
|
||||||
return null;
|
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 = {
|
const opts = {
|
||||||
cert: typeof baseResult.cert === 'string' ? Buffer.from(baseResult.cert) : baseResult.cert,
|
cert: typeof result.cert === 'string' ? Buffer.from(result.cert) : result.cert,
|
||||||
key: typeof baseResult.key === 'string' ? Buffer.from(baseResult.key) : baseResult.key,
|
key: typeof result.key === 'string' ? Buffer.from(result.key) : result.key
|
||||||
SNICallback: sniCallback
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let callbackCalled = false;
|
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() {
|
function getPort() {
|
||||||
return proxyPort;
|
return proxyPort;
|
||||||
}
|
}
|
||||||
@@ -350,5 +356,6 @@ module.exports = {
|
|||||||
setHostnameResolver,
|
setHostnameResolver,
|
||||||
start,
|
start,
|
||||||
stop,
|
stop,
|
||||||
|
restart,
|
||||||
getPort
|
getPort
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user