feat: custom TLD virtual hosts with SNI certs, dynamic PAC, and TLD validation
CI / Build & Test (push) Successful in 3m15s

- Add isValidVhostHostname() to dashboard.js with embedded REAL_TLDS and
  REAL_SLD_TLDS blocklists; enforces 3-label minimum (two-tier TLD requirement),
  valid label characters, and blocks real public TLDs/SLDs (e.g. .com, co.uk)
- Replace hardcoded .hole.sail validation in vhost submit handler with new validator
- Update Add Virtual Host modal hint text and add inline format explanation
- Update applyPAC() in background.js to accept a tlds array, generating one
  dnsDomainIs clause per unique two-label base domain; .hole.sail always included
- Store virtualHosts in extensionState and pass derived TLD list to applyPAC at
  every getState response and retryGetStateForConnectProxy call
- Replace single upfront *.hole.sail cert in https-proxy.js with SNICallback that
  lazily generates a wildcard cert per two-label base domain on first connection;
  baseline *.hole.sail cert still pre-generated at startup
- Add chrome.permissions.request() in background.js send handler to grant host
  permissions for new TLDs dynamically after successful setVirtualHost
- Add optional_host_permissions: ["*://*/*"] and "permissions" to manifest.json
  to enable runtime host permission grants for custom TLDs
This commit is contained in:
Raven Scott
2026-02-28 20:20:28 -05:00
parent 8727809b12
commit 00d9c958ca
5 changed files with 456 additions and 32 deletions
+47 -17
View File
@@ -5,6 +5,7 @@
const path = require('bare-path');
const fs = require('bare-fs');
const tls = require('tls');
let https = null;
let http = null;
@@ -45,26 +46,55 @@ function start(port, certsDirOrCA, callback) {
return proxyServer;
}
// Wildcard cert for *.hole.sail — Chrome accepts wildcards when base domain has 2+ labels
const certDomain = '*.hole.sail';
let cert = null;
let key = null;
if (typeof certsDirOrCA === 'object' && certsDirOrCA.getOrCreateDomainCert) {
const result = certsDirOrCA.getOrCreateDomainCert(certDomain, [
{ type: 2, value: '*.hole.sail' },
{ type: 2, value: 'hole.sail' }
]);
if (result) {
cert = typeof result.cert === 'string' ? Buffer.from(result.cert) : result.cert;
key = typeof result.key === 'string' ? Buffer.from(result.key) : result.key;
}
}
if (!cert || !key) {
if (callback) callback(new Error('Could not get wildcard cert for ' + certDomain));
if (typeof certsDirOrCA !== 'object' || !certsDirOrCA.getOrCreateDomainCert) {
if (callback) callback(new Error('certsDirOrCA must provide getOrCreateDomainCert'));
return null;
}
const opts = { cert, key };
// 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'));
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
};
let callbackCalled = false;
function done(err) {
if (callbackCalled) return;