fix(proxy): implement JS-layer SNI for per-TLD wildcard certs and fix chunked encoding
CI / Build & Test (push) Successful in 3m27s
CI / Build & Test (push) Successful in 3m27s
Replace the bare-https HTTPS proxy with a bare-tcp server that implements SNI entirely in JavaScript. A pure-JS TLS ClientHello parser extracts the SNI hostname from each incoming connection, derives the wildcard parent domain by stripping the leftmost label, and selects (or generates on demand) the correct wildcard cert via certificate-authority.getOrCreateWildcardCert(). This fixes ERR_SSL_SERVER_CERT_BAD_FORMAT for custom TLDs and supports hostnames of any depth (e.g. i.love.hole.sail → cert *.love.hole.sail). Also fixes ERR_INVALID_CHUNKED_ENCODING by stripping hop-by-hop headers (Transfer-Encoding, Connection, etc.) from proxied responses — bare-http1 decodes chunked bodies internally so forwarding the header caused Chrome to misinterpret the already-decoded body bytes. - https-proxy.js: rewrite using bare-tcp + JS SNI peek + bare-tls per conn - certificate-authority.js: add getOrCreateWildcardCert(parentDomain) - host.js: remove refreshProxyCert/getActiveBaseDomains (no longer needed) - background.js: PAC dnsDomainIs clauses already match any depth correctly - dashboard.html: update vhost hint text to show deep hostnames are supported - docs: update ARCHITECTURE, SECURITY, NATIVE-HOST; add VIRTUAL-HOSTS.md
This commit is contained in:
@@ -266,16 +266,28 @@ h1{color:#c0392b}code{background:#f4f4f4;padding:2px 6px;border-radius:3px;font-
|
||||
proxyPath = withoutHost || '/';
|
||||
}
|
||||
|
||||
// Hop-by-hop headers must not be forwarded — bare-http1 handles these
|
||||
// transparently. Forwarding Transfer-Encoding: chunked would cause
|
||||
// ERR_INVALID_CHUNKED_ENCODING because the HTTP library already decodes
|
||||
// the chunked body before emitting 'data' events.
|
||||
const HOP_BY_HOP = new Set([
|
||||
'transfer-encoding', 'connection', 'keep-alive', 'proxy-connection',
|
||||
'proxy-authorization', 'proxy-authenticate', 'te', 'trailer', 'upgrade'
|
||||
]);
|
||||
|
||||
const reqHeaders = {};
|
||||
for (const k of Object.keys(req.headers)) {
|
||||
if (!HOP_BY_HOP.has(k.toLowerCase())) reqHeaders[k] = req.headers[k];
|
||||
}
|
||||
reqHeaders.host = hostname + (hostHeader && hostHeader.includes(':') ? ':' + hostHeader.split(':')[1] : '');
|
||||
|
||||
const opts = {
|
||||
host: targetHost,
|
||||
port: targetPort,
|
||||
path: proxyPath,
|
||||
method: req.method || 'GET',
|
||||
headers: { ...req.headers }
|
||||
headers: reqHeaders
|
||||
};
|
||||
delete opts.headers['proxy-connection'];
|
||||
delete opts.headers['proxy-authorization'];
|
||||
opts.headers.host = hostname + (hostHeader && hostHeader.includes(':') ? ':' + hostHeader.split(':')[1] : '');
|
||||
|
||||
let timedOut = false;
|
||||
const timeoutId = setTimeout(() => {
|
||||
@@ -292,9 +304,14 @@ h1{color:#c0392b}code{background:#f4f4f4;padding:2px 6px;border-radius:3px;font-
|
||||
clearTimeout(timeoutId);
|
||||
if (timedOut) return;
|
||||
res.statusCode = proxyRes.statusCode;
|
||||
const headers = proxyRes.headers;
|
||||
for (const k in headers) {
|
||||
try { res.setHeader(k, headers[k]); } catch (_) {}
|
||||
// Strip hop-by-hop headers from the backend response before forwarding
|
||||
// to the browser. In particular, Transfer-Encoding must be removed because
|
||||
// bare-http1 decodes chunked bodies internally — the 'data' events already
|
||||
// contain the raw payload bytes, not chunked-encoded wire bytes.
|
||||
for (const k of Object.keys(proxyRes.headers)) {
|
||||
if (!HOP_BY_HOP.has(k.toLowerCase())) {
|
||||
try { res.setHeader(k, proxyRes.headers[k]); } catch (_) {}
|
||||
}
|
||||
}
|
||||
proxyRes.on('data', (chunk) => !timedOut && res.write(chunk));
|
||||
proxyRes.on('end', () => !timedOut && !res.writableEnded && res.end());
|
||||
|
||||
Reference in New Issue
Block a user