feat: custom TLD virtual hosts with SNI certs, dynamic PAC, and TLD validation
CI / Build & Test (push) Successful in 3m15s
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:
+51
-11
@@ -81,19 +81,31 @@ const DEFAULT_PROXY_PORT = 8443;
|
||||
/** CONNECT proxy port (browser must send CONNECT here, not to HTTPS port). Use when host omits connectProxyPort. */
|
||||
const DEFAULT_CONNECT_PROXY_PORT = 8442;
|
||||
|
||||
function applyPAC() {
|
||||
/** Extract unique two-label base domains from virtualHosts array, always including hole.sail */
|
||||
function getActiveTlds(virtualHosts) {
|
||||
const seen = new Set(['hole.sail']);
|
||||
for (const v of (virtualHosts || [])) {
|
||||
if (v && v.hostname) {
|
||||
const parts = v.hostname.split('.');
|
||||
if (parts.length >= 3) seen.add(parts.slice(-2).join('.'));
|
||||
}
|
||||
}
|
||||
return Array.from(seen).map(b => '.' + b);
|
||||
}
|
||||
|
||||
function applyPAC(tlds) {
|
||||
if (!browser.proxy || !browser.proxy.settings) {
|
||||
log('applyPAC: SKIPPED - no browser.proxy.settings API');
|
||||
return;
|
||||
}
|
||||
const port = extensionState.connectProxyPort ?? DEFAULT_CONNECT_PROXY_PORT ?? extensionState.proxyPort ?? DEFAULT_PROXY_PORT;
|
||||
log('applyPAC: setting PAC port=', port);
|
||||
const activeTlds = tlds || getActiveTlds(extensionState.virtualHosts);
|
||||
log('applyPAC: setting PAC port=', port, 'tlds=', activeTlds);
|
||||
|
||||
const pacData = `function FindProxyForURL(url, host) {
|
||||
if (dnsDomainIs(host, ".hole.sail"))
|
||||
return "PROXY 127.0.0.1:${port}";
|
||||
return "DIRECT";
|
||||
}`;
|
||||
const clauses = activeTlds.map(t =>
|
||||
` if (dnsDomainIs(host, "${t}")) return "PROXY 127.0.0.1:${port}";`
|
||||
).join('\n');
|
||||
const pacData = `function FindProxyForURL(url, host) {\n${clauses}\n return "DIRECT";\n}`;
|
||||
|
||||
browser.proxy.settings.set(
|
||||
{ value: { mode: 'pac_script', pacScript: { data: pacData, mandatory: false } }, scope: 'regular' },
|
||||
@@ -156,7 +168,8 @@ function retryGetStateForConnectProxy(delaySeconds, attempt = 0) {
|
||||
if (payload && payload.ok && payload.connectProxyPort != null) {
|
||||
extensionState.connectProxyPort = payload.connectProxyPort;
|
||||
extensionState.proxyPort = payload.proxyPort ?? null;
|
||||
applyPAC();
|
||||
extensionState.virtualHosts = payload.virtualHosts || extensionState.virtualHosts;
|
||||
applyPAC(getActiveTlds(extensionState.virtualHosts));
|
||||
} else if (payload && payload.ok && attempt + 1 < maxAttempts) {
|
||||
retryGetStateForConnectProxy(delaySeconds, attempt + 1);
|
||||
}
|
||||
@@ -182,11 +195,12 @@ function connect() {
|
||||
if (payload && payload.ok) {
|
||||
extensionState.proxyPort = payload.proxyPort ?? null;
|
||||
extensionState.connectProxyPort = payload.connectProxyPort ?? null;
|
||||
extensionState.virtualHosts = payload.virtualHosts || [];
|
||||
// Sync notifyOnDisconnect from persisted settings
|
||||
if (payload.settings && typeof payload.settings.notifyOnDisconnect === 'boolean') {
|
||||
notifyOnDisconnect = payload.settings.notifyOnDisconnect;
|
||||
}
|
||||
applyPAC();
|
||||
applyPAC(getActiveTlds(extensionState.virtualHosts));
|
||||
if (extensionState.connectProxyPort == null) retryGetStateForConnectProxy(2);
|
||||
} else {
|
||||
applyPAC(); // Apply PAC even if getState fails - use default port
|
||||
@@ -426,7 +440,32 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
|
||||
if (message.action === 'send') {
|
||||
send(message.payload)
|
||||
.then((r) => reply(r))
|
||||
.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
|
||||
) {
|
||||
const hostname = message.payload.payload.hostname;
|
||||
const parts = hostname.split('.');
|
||||
if (parts.length >= 3) {
|
||||
const baseDomain = parts.slice(-2).join('.');
|
||||
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');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
reply(r);
|
||||
})
|
||||
.catch((err) => reply({ error: err.message }));
|
||||
return true; // async response
|
||||
}
|
||||
@@ -466,12 +505,13 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (payload && payload.ok === true && (payload.servers || payload.virtualHosts || payload.proxyPort != null || payload.connectProxyPort != null)) {
|
||||
extensionState.proxyPort = payload.proxyPort ?? null;
|
||||
extensionState.connectProxyPort = payload.connectProxyPort ?? null;
|
||||
extensionState.virtualHosts = payload.virtualHosts || [];
|
||||
if (payload.settings && typeof payload.settings.notifyOnDisconnect === 'boolean') {
|
||||
notifyOnDisconnect = payload.settings.notifyOnDisconnect;
|
||||
}
|
||||
if (!pacConfirmedActive) {
|
||||
log('getState: PAC not confirmed active, re-applying');
|
||||
applyPAC();
|
||||
applyPAC(getActiveTlds(extensionState.virtualHosts));
|
||||
}
|
||||
reply({
|
||||
ok: true,
|
||||
|
||||
@@ -1580,8 +1580,9 @@
|
||||
<div class="modal-body">
|
||||
<p class="modal-desc">Assign a hostname to an hs:// tunnel URL. The host will be accessible at <code style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--cyan);">https://<hostname></code> via the proxy.</p>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="addVhostHostname">Hostname <span class="form-hint">(must end with .hole.sail)</span></label>
|
||||
<label class="form-label" for="addVhostHostname">Hostname <span class="form-hint">(e.g. myapp.hole.sail or api.my.internal — no real TLDs)</span></label>
|
||||
<input type="text" id="addVhostHostname" class="input mono" placeholder="myapp.hole.sail" autocomplete="off" spellcheck="false">
|
||||
<div style="font-size:11px;color:var(--text4);margin-top:4px;line-height:1.5;">Must be <code style="font-size:10px;">host.second.tld</code> format with a private TLD (3 labels minimum). Real TLDs like <code style="font-size:10px;">.com</code>, <code style="font-size:10px;">.co.uk</code> are not allowed.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="addVhostHsUrl">hs:// URL</label>
|
||||
|
||||
+351
-2
@@ -53,6 +53,355 @@ function escapeHtml(str) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// ── Virtual host TLD validator ────────────────────────────────────────────────
|
||||
// Real single-label TLDs that must be blocked (compact subset of IANA list)
|
||||
const REAL_TLDS = new Set([
|
||||
'ac','ad','ae','af','ag','ai','al','am','ao','aq','ar','as','at','au','aw','ax','az',
|
||||
'ba','bb','bd','be','bf','bg','bh','bi','bj','bm','bn','bo','br','bs','bt','bv','bw',
|
||||
'by','bz','ca','cc','cd','cf','cg','ch','ci','ck','cl','cm','cn','co','cr','cu','cv',
|
||||
'cw','cx','cy','cz','de','dj','dk','dm','do','dz','ec','ee','eg','er','es','et','eu',
|
||||
'fi','fj','fk','fm','fo','fr','ga','gb','gd','ge','gf','gg','gh','gi','gl','gm','gn',
|
||||
'gp','gq','gr','gs','gt','gu','gw','gy','hk','hm','hn','hr','ht','hu','id','ie','il',
|
||||
'im','in','io','iq','ir','is','it','je','jm','jo','jp','ke','kg','kh','ki','km','kn',
|
||||
'kp','kr','kw','ky','kz','la','lb','lc','li','lk','lr','ls','lt','lu','lv','ly','ma',
|
||||
'mc','md','me','mg','mh','mk','ml','mm','mn','mo','mp','mq','mr','ms','mt','mu','mv',
|
||||
'mw','mx','my','mz','na','nc','ne','nf','ng','ni','nl','no','np','nr','nu','nz','om',
|
||||
'pa','pe','pf','pg','ph','pk','pl','pm','pn','pr','ps','pt','pw','py','qa','re','ro',
|
||||
'rs','ru','rw','sa','sb','sc','sd','se','sg','sh','si','sj','sk','sl','sm','sn','so',
|
||||
'sr','ss','st','su','sv','sx','sy','sz','tc','td','tf','tg','th','tj','tk','tl','tm',
|
||||
'tn','to','tr','tt','tv','tw','tz','ua','ug','uk','us','uy','uz','va','vc','ve','vg',
|
||||
'vi','vn','vu','wf','ws','ye','yt','za','zm','zw',
|
||||
// Generic TLDs
|
||||
'aaa','aarp','abb','abc','able','abogado','abudhabi','academy','accenture','accountant',
|
||||
'accountants','aco','actor','ads','adult','aeg','aero','aetna','africa','agakhan','agency',
|
||||
'aig','airbus','airforce','airtel','akdn','alfaromeo','alibaba','alipay','allfinanz',
|
||||
'allstate','ally','alsace','alstom','amazon','americanexpress','americanfamily','amex',
|
||||
'amfam','amica','amsterdam','analytics','android','anquan','anz','aol','apartments',
|
||||
'app','apple','aquarelle','arab','aramco','archi','army','art','arte','asda','associates',
|
||||
'athleta','auction','audi','audible','audio','auspost','author','auto','autos','avianca',
|
||||
'aws','axa','azure','baby','baidu','banamex','band','bank','bar','barcelona','barclaycard',
|
||||
'barclays','barefoot','bargains','baseball','basketball','bauhaus','bayern','bbc','bbt',
|
||||
'bbva','bcg','bcn','beats','beauty','beer','bentley','berlin','best','bestbuy','bet',
|
||||
'bible','bid','bike','bing','bingo','bio','black','blackfriday','blockbuster','blog',
|
||||
'bloomberg','blue','bms','bmw','bnl','bnpparibas','boats','boehringer','bofa','bom',
|
||||
'bond','boo','book','booking','bosch','bostik','boston','bot','boutique','box','bradesco',
|
||||
'bridgestone','broadway','broker','brother','brussels','budapest','bugatti','build',
|
||||
'builders','business','buy','buzz','bzh','cab','cafe','cal','call','calvinklein','cam',
|
||||
'camera','camp','cancerresearch','canon','capetown','capital','capitalone','cards','care',
|
||||
'career','careers','cars','casa','case','cash','casino','cat','catering','catholic','cba',
|
||||
'cbn','cbre','cbs','center','ceo','cern','cfa','cfd','channel','charity','chase','chat',
|
||||
'cheap','chintai','christmas','chrome','church','cipriani','circle','cisco','citi',
|
||||
'citic','city','cityeats','claims','cleaning','click','clinic','clinique','clothing',
|
||||
'cloud','club','clubmed','codes','coffee','college','cologne','com','community','company',
|
||||
'compare','computer','comsec','condos','construction','consulting','contact','contractors',
|
||||
'cooking','cool','coop','corsica','country','coupon','coupons','courses','credit',
|
||||
'creditcard','creditunion','cricket','crown','crs','cruise','cruises','csc','cuisinella',
|
||||
'cymru','cyou','dabur','dad','dance','data','date','dating','datsun','day','dclk','dds',
|
||||
'deal','dealer','deals','degree','delivery','dell','deloitte','democrat','dental','design',
|
||||
'dev','dhl','diamonds','diet','digital','direct','directory','discount','discover',
|
||||
'dish','diy','dnp','docs','doctor','dog','domains','dot','download','drive','dtv','dubai',
|
||||
'dunlop','dupont','durban','dvag','dvr','earth','eat','eco','edeka','edu','education',
|
||||
'email','emerck','energy','engineering','enterprises','epson','equipment','ericsson',
|
||||
'erni','estate','esurance','etisalat','eurovision','eus','events','exchange','expert',
|
||||
'exposed','express','extraspace','fage','fail','fairwinds','faith','family','fan','fans',
|
||||
'farm','farmers','fashion','fast','fedex','feedback','ferrari','ferrero','fiat','fidelity',
|
||||
'fido','film','final','finance','financial','fire','firestone','firmdale','fish','fishing',
|
||||
'fit','fitness','flights','florist','flowers','fly','foo','food','foodnetwork','football',
|
||||
'ford','forex','forsale','forum','foundation','fox','free','fresenius','frl','frogans',
|
||||
'frontdoor','frontier','ftr','fujitsu','fun','fund','furniture','futbol','fyi','gal',
|
||||
'gallery','gallo','gallup','game','games','gap','garden','gay','gbiz','gdn','gea',
|
||||
'gent','genting','george','ggee','gift','gifts','gives','giving','glass','gle','global',
|
||||
'globo','gmail','gmbh','gold','goldpoint','golf','goo','goodyear','goog','google','gop',
|
||||
'got','gov','grainger','graphics','gratis','green','gripe','grocery','group','guardian',
|
||||
'gucci','guge','guide','guitars','guru','hair','hamburg','hangout','haus','hbo','hdfc',
|
||||
'hdfcbank','health','healthcare','help','helsinki','here','hermes','hgtv','hiphop',
|
||||
'hisamitsu','hitachi','hiv','hkt','hockey','holdings','holiday','homedepot','homegoods',
|
||||
'homes','homesense','honda','horse','hospital','host','hosting','hot','hoteles','hotels',
|
||||
'hotmail','house','how','hsbc','hughes','hyatt','hyundai','ibm','icbc','ice','icu','ieee',
|
||||
'ifm','ikano','imamat','imdb','immo','immobilien','industries','infiniti','info','ing',
|
||||
'ink','institute','insurance','insure','int','international','intuit','investments','ipiranga',
|
||||
'irish','ismaili','ist','istanbul','itau','itv','jaguar','java','jcb','jeep','jetzt',
|
||||
'jewelry','jio','jll','jobs','joburg','jot','joy','jpmorgan','jprs','juegos','juniper',
|
||||
'kaufen','kddi','kerryhotels','kerrylogistics','kerryproperties','kfh','kia','kids','kim',
|
||||
'kinder','kindle','kitchen','kiwi','koeln','komatsu','kosher','kpmg','kpn','krd','kred',
|
||||
'kuokgroup','kyoto','lacaixa','lamborghini','lamer','lancaster','land','landrover',
|
||||
'lanxess','lasalle','lat','latino','latrobe','law','lawyer','lds','lease','leclerc',
|
||||
'lefrak','legal','lego','lexus','lgbt','lidl','life','lifeinsurance','lifestyle','lighting',
|
||||
'like','lilly','limited','limo','lincoln','link','lipsy','live','living','llc','llp',
|
||||
'loan','loans','locker','locus','lol','london','lotte','lotto','love','lpl','lplfinancial',
|
||||
'ltd','ltda','lundbeck','luxe','luxury','madrid','maif','maison','makeup','man','management',
|
||||
'mango','map','market','marketing','markets','marriott','marshalls','mba','mckinsey','med',
|
||||
'media','meet','melbourne','meme','memorial','men','menu','merckmsd','miami','microsoft',
|
||||
'mil','mini','mint','mit','mitsubishi','mobi','mobile','moda','moe','moi','mom','monash',
|
||||
'money','monster','mormon','mortgage','moscow','moto','motorcycles','mov','movie','msd',
|
||||
'mtn','mtr','music','mutual','nab','nagoya','name','natura','navy','nba','net','netbank',
|
||||
'netflix','network','neustar','new','news','next','nextdirect','nexus','nfl','ngo','nhk',
|
||||
'nico','nike','nikon','ninja','nissan','nissay','nokia','norton','now','nowruz','nra',
|
||||
'nrw','ntt','nyc','obi','observer','office','okinawa','olayan','olayangroup','oldnavy',
|
||||
'ollo','omega','one','ong','onl','online','ooo','open','oracle','orange','org','organic',
|
||||
'origins','osaka','otsuka','ott','ovh','page','panasonic','paris','pars','partners','parts',
|
||||
'party','passagens','pay','pccw','pet','pfizer','pharmacy','phd','philips','phone','photo',
|
||||
'photography','photos','physio','pics','pictet','pictures','pid','pin','ping','pink',
|
||||
'pioneer','pizza','place','play','playstation','plumbing','plus','pnc','pohl','poker',
|
||||
'politie','porn','post','pramerica','praxi','press','prime','pro','prod','productions',
|
||||
'prof','progressive','promo','properties','property','protection','pru','prudential',
|
||||
'pub','pwc','qpon','quebec','quest','racing','radio','read','realestate','realtor',
|
||||
'realty','recipes','red','redstone','redumbrella','rehab','reise','reisen','reit','reliance',
|
||||
'ren','rent','rentals','repair','report','republican','rest','restaurant','review','reviews',
|
||||
'rexroth','rich','richardli','ricoh','rightathome','rio','rip','rocher','rocks','rodeo',
|
||||
'rogers','room','rsvp','rugby','ruhr','run','rwe','ryukyu','safe','safety','sakura','sale',
|
||||
'salon','samsclub','samsung','sandvik','sandvikcoromant','sanofi','sap','sarl','sas',
|
||||
'save','saxo','sbi','sbs','sca','scb','schaeffler','schmidt','scholarships','school',
|
||||
'schule','schwarz','science','scjohnson','scot','search','seat','secure','security',
|
||||
'seek','select','sener','services','ses','seven','sew','sex','sexy','sfr','shangrila',
|
||||
'sharp','shaw','shell','shia','shiksha','shoes','shop','shopping','shouji','show','silk',
|
||||
'sina','singles','ski','skin','sky','skype','smile','sncf','soccer','social','softbank',
|
||||
'software','sohu','solar','solutions','song','sony','soy','spa','space','sport','spot',
|
||||
'srl','stada','staples','star','statebank','statefarm','stc','stcgroup','stockholm',
|
||||
'storage','store','stream','studio','study','style','sucks','supplies','supply','support',
|
||||
'surf','surgery','suzuki','swatch','swiss','sydney','systems','tab','taipei','talk',
|
||||
'taobao','target','tatamotors','tatar','tattoo','tax','taxi','tci','tdk','team','tech',
|
||||
'technology','tel','temasek','tennis','teva','tiaa','tickets','tienda','tips','tires',
|
||||
'tirol','tjmaxx','tjx','tkmaxx','today','tokyo','tools','top','toray','toshiba','total',
|
||||
'tours','town','toyota','toys','trade','trading','training','travel','travelers',
|
||||
'travelersinsurance','trust','trv','tube','tui','tunes','tushu','tvs','ubank','unicom',
|
||||
'university','uno','uol','ups','vacations','vana','vanguard','vegas','ventures','verisign',
|
||||
'versicherung','vet','viajes','video','vig','viking','villas','vin','vip','virgin','visa',
|
||||
'vision','viva','vivo','vlaanderen','vodka','volkswagen','volvo','vote','voting','voto',
|
||||
'voyage','vuelos','wales','walmart','walter','wang','wanggou','watch','watches','weather',
|
||||
'weatherchannel','webcam','weber','website','wed','wedding','weibo','weir','whoswho',
|
||||
'wien','wiki','williamhill','win','windows','wine','winners','wme','wolterskluwer',
|
||||
'woodside','work','works','world','wow','wtc','wtf','xbox','xerox','xfinity','xihuan',
|
||||
'xin','xxx','xyz','yachts','yahoo','yamaxun','yandex','yodobashi','yoga','yokohama',
|
||||
'you','youtube','yun','zappos','zara','zero','zip','zone','zuerich'
|
||||
]);
|
||||
|
||||
// Real two-label public suffixes (e.g. co.uk) — base domain check
|
||||
const REAL_SLD_TLDS = new Set([
|
||||
'co.uk','org.uk','me.uk','net.uk','ltd.uk','plc.uk','sch.uk','gov.uk','nhs.uk','police.uk',
|
||||
'com.au','net.au','org.au','edu.au','gov.au','asn.au','id.au',
|
||||
'co.nz','net.nz','org.nz','edu.nz','govt.nz','geek.nz','gen.nz','maori.nz',
|
||||
'co.za','org.za','net.za','edu.za','gov.za','web.za',
|
||||
'com.br','net.br','org.br','edu.br','gov.br','mil.br',
|
||||
'com.ar','net.ar','org.ar','edu.ar','gov.ar',
|
||||
'com.mx','net.mx','org.mx','edu.mx','gob.mx',
|
||||
'com.cn','net.cn','org.cn','edu.cn','gov.cn','ac.cn',
|
||||
'co.jp','ne.jp','or.jp','ac.jp','go.jp','ad.jp',
|
||||
'co.in','net.in','org.in','edu.in','gov.in','ac.in','res.in',
|
||||
'co.ke','or.ke','ac.ke','go.ke','ne.ke',
|
||||
'com.sg','net.sg','org.sg','edu.sg','gov.sg',
|
||||
'com.hk','net.hk','org.hk','edu.hk','gov.hk',
|
||||
'com.tw','net.tw','org.tw','edu.tw','gov.tw',
|
||||
'com.my','net.my','org.my','edu.my','gov.my',
|
||||
'com.pk','net.pk','org.pk','edu.pk','gov.pk',
|
||||
'com.ng','net.ng','org.ng','edu.ng','gov.ng',
|
||||
'com.gh','net.gh','org.gh','edu.gh','gov.gh',
|
||||
'com.eg','net.eg','org.eg','edu.eg','gov.eg',
|
||||
'com.tr','net.tr','org.tr','edu.tr','gov.tr',
|
||||
'com.sa','net.sa','org.sa','edu.sa','gov.sa',
|
||||
'com.ae','net.ae','org.ae','edu.ae','gov.ae',
|
||||
'com.il','net.il','org.il','edu.il','gov.il',
|
||||
'co.il','ac.il',
|
||||
'com.ua','net.ua','org.ua','edu.ua','gov.ua',
|
||||
'com.pl','net.pl','org.pl','edu.pl','gov.pl',
|
||||
'com.de','net.de','org.de',
|
||||
'com.fr','net.fr','org.fr',
|
||||
'com.es','net.es','org.es','edu.es','gob.es',
|
||||
'com.it','net.it','org.it','edu.it','gov.it',
|
||||
'com.ru','net.ru','org.ru','edu.ru','gov.ru',
|
||||
'com.vn','net.vn','org.vn','edu.vn','gov.vn',
|
||||
'com.ph','net.ph','org.ph','edu.ph','gov.ph',
|
||||
'com.id','net.id','org.id','edu.id','go.id',
|
||||
'com.pe','net.pe','org.pe','edu.pe','gob.pe',
|
||||
'com.co','net.co','org.co','edu.co','gov.co',
|
||||
'com.ve','net.ve','org.ve','edu.ve','gov.ve',
|
||||
'com.ec','net.ec','org.ec','edu.ec','gov.ec',
|
||||
'com.bo','net.bo','org.bo','edu.bo','gov.bo',
|
||||
'com.py','net.py','org.py','edu.py','gov.py',
|
||||
'com.uy','net.uy','org.uy','edu.uy','gub.uy',
|
||||
'com.ni','net.ni','org.ni','edu.ni','gob.ni',
|
||||
'com.cr','net.cr','org.cr','edu.cr','go.cr',
|
||||
'com.gt','net.gt','org.gt','edu.gt','gob.gt',
|
||||
'com.hn','net.hn','org.hn','edu.hn','gob.hn',
|
||||
'com.sv','net.sv','org.sv','edu.sv','gob.sv',
|
||||
'com.pa','net.pa','org.pa','edu.pa','gob.pa',
|
||||
'com.do','net.do','org.do','edu.do','gob.do',
|
||||
'com.cu','net.cu','org.cu','edu.cu','inf.cu',
|
||||
'com.pr','net.pr','org.pr','edu.pr','gov.pr',
|
||||
'com.tt','net.tt','org.tt','edu.tt','gov.tt',
|
||||
'com.jm','net.jm','org.jm','edu.jm','gov.jm',
|
||||
'com.bb','net.bb','org.bb','edu.bb','gov.bb',
|
||||
'com.lc','net.lc','org.lc','edu.lc','gov.lc',
|
||||
'com.vc','net.vc','org.vc','edu.vc','gov.vc',
|
||||
'com.dm','net.dm','org.dm','edu.dm','gov.dm',
|
||||
'com.ag','net.ag','org.ag','edu.ag','gov.ag',
|
||||
'com.kn','net.kn','org.kn','edu.kn','gov.kn',
|
||||
'com.gd','net.gd','org.gd','edu.gd','gov.gd',
|
||||
'com.ms','net.ms','org.ms','edu.ms','gov.ms',
|
||||
'com.ai','net.ai','org.ai','edu.ai','gov.ai',
|
||||
'com.vg','net.vg','org.vg','edu.vg','gov.vg',
|
||||
'com.ky','net.ky','org.ky','edu.ky','gov.ky',
|
||||
'com.tc','net.tc','org.tc','edu.tc','gov.tc',
|
||||
'com.bm','net.bm','org.bm','edu.bm','gov.bm',
|
||||
'com.bs','net.bs','org.bs','edu.bs','gov.bs',
|
||||
'com.aw','net.aw','org.aw',
|
||||
'com.na','net.na','org.na','edu.na','gov.na',
|
||||
'com.zm','net.zm','org.zm','edu.zm','gov.zm',
|
||||
'com.zw','net.zw','org.zw','edu.zw','gov.zw',
|
||||
'com.mz','net.mz','org.mz','edu.mz','gov.mz',
|
||||
'com.tz','net.tz','org.tz','edu.tz','go.tz',
|
||||
'com.ug','net.ug','org.ug','edu.ug','go.ug',
|
||||
'com.rw','net.rw','org.rw','edu.rw','gov.rw',
|
||||
'com.et','net.et','org.et','edu.et','gov.et',
|
||||
'com.sd','net.sd','org.sd','edu.sd','gov.sd',
|
||||
'com.ly','net.ly','org.ly','edu.ly','gov.ly',
|
||||
'com.tn','net.tn','org.tn','edu.tn','gov.tn',
|
||||
'com.dz','net.dz','org.dz','edu.dz','gov.dz',
|
||||
'com.ma','net.ma','org.ma','edu.ma','gov.ma',
|
||||
'com.sn','net.sn','org.sn','edu.sn','gov.sn',
|
||||
'com.ci','net.ci','org.ci','edu.ci','gov.ci',
|
||||
'com.cm','net.cm','org.cm','edu.cm','gov.cm',
|
||||
'com.bf','net.bf','org.bf','edu.bf','gov.bf',
|
||||
'com.ml','net.ml','org.ml','edu.ml','gov.ml',
|
||||
'com.ne','net.ne','org.ne','edu.ne','gov.ne',
|
||||
'com.td','net.td','org.td','edu.td','gov.td',
|
||||
'com.mr','net.mr','org.mr','edu.mr','gov.mr',
|
||||
'com.gn','net.gn','org.gn','edu.gn','gov.gn',
|
||||
'com.sl','net.sl','org.sl','edu.sl','gov.sl',
|
||||
'com.lr','net.lr','org.lr','edu.lr','gov.lr',
|
||||
'com.gw','net.gw','org.gw','edu.gw','gov.gw',
|
||||
'com.gm','net.gm','org.gm','edu.gm','gov.gm',
|
||||
'com.cv','net.cv','org.cv','edu.cv','gov.cv',
|
||||
'com.st','net.st','org.st','edu.st','gov.st',
|
||||
'com.ga','net.ga','org.ga','edu.ga','gov.ga',
|
||||
'com.cg','net.cg','org.cg','edu.cg','gov.cg',
|
||||
'com.cd','net.cd','org.cd','edu.cd','gov.cd',
|
||||
'com.ao','net.ao','org.ao','edu.ao','gov.ao',
|
||||
'com.bw','net.bw','org.bw','edu.bw','gov.bw',
|
||||
'com.ls','net.ls','org.ls','edu.ls','gov.ls',
|
||||
'com.sz','net.sz','org.sz','edu.sz','gov.sz',
|
||||
'com.mg','net.mg','org.mg','edu.mg','gov.mg',
|
||||
'com.mu','net.mu','org.mu','edu.mu','gov.mu',
|
||||
'com.re','net.re','org.re','edu.re','gov.re',
|
||||
'com.yt','net.yt','org.yt',
|
||||
'com.sc','net.sc','org.sc','edu.sc','gov.sc',
|
||||
'com.km','net.km','org.km','edu.km','gov.km',
|
||||
'com.dj','net.dj','org.dj','edu.dj','gov.dj',
|
||||
'com.so','net.so','org.so','edu.so','gov.so',
|
||||
'com.er','net.er','org.er','edu.er','gov.er',
|
||||
'com.bi','net.bi','org.bi','edu.bi','gov.bi',
|
||||
'com.mw','net.mw','org.mw','edu.mw','gov.mw',
|
||||
'com.zm','net.zm','org.zm',
|
||||
'com.bd','net.bd','org.bd','edu.bd','gov.bd',
|
||||
'com.lk','net.lk','org.lk','edu.lk','gov.lk',
|
||||
'com.np','net.np','org.np','edu.np','gov.np',
|
||||
'com.mm','net.mm','org.mm','edu.mm','gov.mm',
|
||||
'com.kh','net.kh','org.kh','edu.kh','gov.kh',
|
||||
'com.la','net.la','org.la','edu.la','gov.la',
|
||||
'com.vn','net.vn','org.vn','edu.vn','gov.vn',
|
||||
'com.mn','net.mn','org.mn','edu.mn','gov.mn',
|
||||
'com.kz','net.kz','org.kz','edu.kz','gov.kz',
|
||||
'com.uz','net.uz','org.uz','edu.uz','gov.uz',
|
||||
'com.tm','net.tm','org.tm','edu.tm','gov.tm',
|
||||
'com.tj','net.tj','org.tj','edu.tj','gov.tj',
|
||||
'com.kg','net.kg','org.kg','edu.kg','gov.kg',
|
||||
'com.af','net.af','org.af','edu.af','gov.af',
|
||||
'com.pk','net.pk','org.pk','edu.pk','gov.pk',
|
||||
'com.ir','net.ir','org.ir','edu.ir','gov.ir',
|
||||
'com.iq','net.iq','org.iq','edu.iq','gov.iq',
|
||||
'com.sy','net.sy','org.sy','edu.sy','gov.sy',
|
||||
'com.lb','net.lb','org.lb','edu.lb','gov.lb',
|
||||
'com.jo','net.jo','org.jo','edu.jo','gov.jo',
|
||||
'com.ps','net.ps','org.ps','edu.ps','gov.ps',
|
||||
'com.ye','net.ye','org.ye','edu.ye','gov.ye',
|
||||
'com.om','net.om','org.om','edu.om','gov.om',
|
||||
'com.kw','net.kw','org.kw','edu.kw','gov.kw',
|
||||
'com.bh','net.bh','org.bh','edu.bh','gov.bh',
|
||||
'com.qa','net.qa','org.qa','edu.qa','gov.qa',
|
||||
'com.ge','net.ge','org.ge','edu.ge','gov.ge',
|
||||
'com.am','net.am','org.am','edu.am','gov.am',
|
||||
'com.az','net.az','org.az','edu.az','gov.az',
|
||||
'com.by','net.by','org.by','edu.by','gov.by',
|
||||
'com.ua','net.ua','org.ua','edu.ua','gov.ua',
|
||||
'com.md','net.md','org.md','edu.md','gov.md',
|
||||
'com.ro','net.ro','org.ro','edu.ro','gov.ro',
|
||||
'com.bg','net.bg','org.bg','edu.bg','gov.bg',
|
||||
'com.mk','net.mk','org.mk','edu.mk','gov.mk',
|
||||
'com.al','net.al','org.al','edu.al','gov.al',
|
||||
'com.rs','net.rs','org.rs','edu.rs','gov.rs',
|
||||
'com.hr','net.hr','org.hr','edu.hr','gov.hr',
|
||||
'com.ba','net.ba','org.ba','edu.ba','gov.ba',
|
||||
'com.me','net.me','org.me','edu.me','gov.me',
|
||||
'com.si','net.si','org.si','edu.si','gov.si',
|
||||
'com.sk','net.sk','org.sk','edu.sk','gov.sk',
|
||||
'com.cz','net.cz','org.cz','edu.cz','gov.cz',
|
||||
'com.pl','net.pl','org.pl','edu.pl','gov.pl',
|
||||
'com.hu','net.hu','org.hu','edu.hu','gov.hu',
|
||||
'com.at','net.at','org.at','edu.at','gov.at',
|
||||
'com.ch','net.ch','org.ch','edu.ch','gov.ch',
|
||||
'com.li','net.li','org.li','edu.li','gov.li',
|
||||
'com.be','net.be','org.be','edu.be','gov.be',
|
||||
'com.nl','net.nl','org.nl','edu.nl','gov.nl',
|
||||
'com.lu','net.lu','org.lu','edu.lu','gov.lu',
|
||||
'com.dk','net.dk','org.dk','edu.dk','gov.dk',
|
||||
'com.se','net.se','org.se','edu.se','gov.se',
|
||||
'com.no','net.no','org.no','edu.no','gov.no',
|
||||
'com.fi','net.fi','org.fi','edu.fi','gov.fi',
|
||||
'com.is','net.is','org.is','edu.is','gov.is',
|
||||
'com.ie','net.ie','org.ie','edu.ie','gov.ie',
|
||||
'com.pt','net.pt','org.pt','edu.pt','gov.pt',
|
||||
'com.gr','net.gr','org.gr','edu.gr','gov.gr',
|
||||
'com.cy','net.cy','org.cy','edu.cy','gov.cy',
|
||||
'com.mt','net.mt','org.mt','edu.mt','gov.mt',
|
||||
'com.ee','net.ee','org.ee','edu.ee','gov.ee',
|
||||
'com.lv','net.lv','org.lv','edu.lv','gov.lv',
|
||||
'com.lt','net.lt','org.lt','edu.lt','gov.lt'
|
||||
]);
|
||||
|
||||
/**
|
||||
* Validate a virtual host hostname.
|
||||
* Rules:
|
||||
* 1. Must have at least 2 dots (3 labels minimum: host.second.tld)
|
||||
* 2. Each label: only [a-z0-9-], no leading/trailing hyphen, non-empty
|
||||
* 3. The last two labels (base domain, e.g. "hole.sail") must not be a
|
||||
* real public TLD or second-level public suffix
|
||||
* Returns { ok: true } or { ok: false, error: string }
|
||||
*/
|
||||
function isValidVhostHostname(hostname) {
|
||||
if (!hostname) return { ok: false, error: 'Hostname is required' };
|
||||
const labels = hostname.split('.');
|
||||
if (labels.length < 3) {
|
||||
return { ok: false, error: 'Hostname must have the form host.second.tld (e.g. myapp.hole.sail) — single-dot names are not allowed' };
|
||||
}
|
||||
for (const label of labels) {
|
||||
if (!label) return { ok: false, error: 'Hostname contains empty labels' };
|
||||
if (!/^[a-z0-9-]+$/.test(label)) return { ok: false, error: 'Hostname contains invalid characters — only letters, digits and hyphens allowed' };
|
||||
if (label.startsWith('-') || label.endsWith('-')) return { ok: false, error: 'Hostname labels must not start or end with a hyphen' };
|
||||
}
|
||||
const tld = labels[labels.length - 1];
|
||||
const baseDomain = labels.slice(-2).join('.');
|
||||
if (REAL_TLDS.has(tld) || REAL_SLD_TLDS.has(baseDomain)) {
|
||||
return { ok: false, error: 'The TLD ".' + baseDomain + '" is a real registered domain — use a private TLD like .hole.sail or .my.internal' };
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
/** Extract the two-label base domain from a hostname (e.g. "hole.sail" from "myapp.hole.sail") */
|
||||
function extractBaseDomain(hostname) {
|
||||
const parts = hostname.split('.');
|
||||
return parts.slice(-2).join('.');
|
||||
}
|
||||
|
||||
/** Extract unique two-label base domains from a list of virtual host objects */
|
||||
function extractActiveTlds(virtualHosts) {
|
||||
const seen = new Set();
|
||||
seen.add('hole.sail'); // always include baseline
|
||||
for (const v of (virtualHosts || [])) {
|
||||
if (v.hostname) seen.add(extractBaseDomain(v.hostname));
|
||||
}
|
||||
return Array.from(seen).map(b => '.' + b);
|
||||
}
|
||||
|
||||
function stateTag(state) {
|
||||
const dot = (color) => `<span style="display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--${color});margin-right:5px;flex-shrink:0;${color === 'green' ? 'box-shadow:0 0 5px var(--green);' : ''}"></span>`;
|
||||
if (!state || state === '—') return `<span class="badge badge-neutral">${dot('text4')}—</span>`;
|
||||
@@ -1653,8 +2002,8 @@ function setupEvents() {
|
||||
let hostname = (hostnameEl?.value || '').trim();
|
||||
hostname = hostname.replace(/^https?:\/\//i, '').replace(/[/:?#].*$/, '').toLowerCase().trim();
|
||||
const hsUrl = (hsUrlEl?.value || '').trim();
|
||||
if (!hostname) { showModalError('modal-addVhost', 'addVhostError', 'Hostname is required'); return; }
|
||||
if (!hostname.endsWith('.hole.sail')) { showModalError('modal-addVhost', 'addVhostError', 'Hostname must end with .hole.sail (e.g. myapp.hole.sail)'); return; }
|
||||
const hostnameValidation = isValidVhostHostname(hostname);
|
||||
if (!hostnameValidation.ok) { showModalError('modal-addVhost', 'addVhostError', hostnameValidation.error); return; }
|
||||
if (!hsUrl || !hsUrl.startsWith('hs://')) { showModalError('modal-addVhost', 'addVhostError', 'Enter a valid hs:// URL'); return; }
|
||||
const btn = $('addVhostSubmit');
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Adding…'; }
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
"notifications",
|
||||
"tabs",
|
||||
"declarativeNetRequest",
|
||||
"proxy"
|
||||
"proxy",
|
||||
"permissions"
|
||||
],
|
||||
"action": {
|
||||
"default_title": "Holesail Dashboard",
|
||||
@@ -64,5 +65,8 @@
|
||||
"host_permissions": [
|
||||
"*://*.host.local/*",
|
||||
"*://*.hole.sail/*"
|
||||
],
|
||||
"optional_host_permissions": [
|
||||
"*://*/*"
|
||||
]
|
||||
}
|
||||
+47
-17
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user