feat(virtual-hosts): add TLS option for HTTPS/443 backends
CI / Build & Test (push) Failing after 2m44s

- Add "Use TLS (secure connection)" checkbox in Add Virtual Host modal
- Persist and restore useTls in state; show TLS badge in table
- When enabled, HTTPS proxy connects to tunnel backend over TLS (SNI =
  hostname) for HTTP and WebSocket; supports services on port 443
This commit is contained in:
Raven Scott
2026-03-03 03:59:53 -05:00
parent 516d387aca
commit 3a13334779
6 changed files with 87 additions and 28 deletions
+7
View File
@@ -820,6 +820,13 @@
<label class="form-label" for="addVhostHsUrl">hs:// URL</label> <label class="form-label" for="addVhostHsUrl">hs:// URL</label>
<input type="text" id="addVhostHsUrl" class="input mono" placeholder="hs://s000..." autocomplete="off" spellcheck="false"> <input type="text" id="addVhostHsUrl" class="input mono" placeholder="hs://s000..." autocomplete="off" spellcheck="false">
</div> </div>
<div class="form-group">
<label class="form-checkbox">
<input type="checkbox" id="addVhostUseTls" autocomplete="off">
<span>Use TLS (secure connection)</span>
</label>
<div style="font-size:11px;color:var(--text4);margin-top:4px;">Enable if the service behind the tunnel uses HTTPS (port 443).</div>
</div>
<div class="modal-error" id="addVhostError"></div> <div class="modal-error" id="addVhostError"></div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
+9 -3
View File
@@ -54,6 +54,7 @@ function updateConnectionsTable(state) {
tbody.innerHTML = virtualHosts.map(v => { tbody.innerHTML = virtualHosts.map(v => {
const hostname = v.hostname || v.id || ''; const hostname = v.hostname || v.id || '';
const hsUrl = v.hsUrl || ''; const hsUrl = v.hsUrl || '';
const useTls = v.useTls === true;
const backend = (v.localHost && v.localPort != null) ? v.localHost + ':' + v.localPort : '—'; const backend = (v.localHost && v.localPort != null) ? v.localHost + ':' + v.localPort : '—';
const openUrl = `https://${hostname}`; const openUrl = `https://${hostname}`;
const safeHostname = hostname.replace(/"/g, '&quot;'); const safeHostname = hostname.replace(/"/g, '&quot;');
@@ -63,6 +64,7 @@ function updateConnectionsTable(state) {
<td style="width:32px;"><input type="checkbox" class="vhost-row-cb" data-hostname="${safeHostname}"></td> <td style="width:32px;"><input type="checkbox" class="vhost-row-cb" data-hostname="${safeHostname}"></td>
<td> <td>
<span class="mono-chip">${escapeHtml(hostname)}</span> <span class="mono-chip">${escapeHtml(hostname)}</span>
${useTls ? '<span class="badge badge-cyan" style="margin-left:6px;font-size:10px;">TLS</span>' : ''}
</td> </td>
<td> <td>
<div style="display:flex;align-items:center;gap:6px;"> <div style="display:flex;align-items:center;gap:6px;">
@@ -84,7 +86,7 @@ function updateConnectionsTable(state) {
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15,3 21,3 21,9"/><line x1="10" y1="14" x2="21" y2="3"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15,3 21,3 21,9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
Open Open
</a> </a>
${needsReconnect ? `<button class="btn btn-secondary btn-sm" data-reconnect-vhost="${safeHostname}" data-hs-url="${escapeHtml(hsUrl)}" title="Reconnect tunnel"> ${needsReconnect ? `<button class="btn btn-secondary btn-sm" data-reconnect-vhost="${safeHostname}" data-hs-url="${escapeHtml(hsUrl)}" data-use-tls="${useTls ? '1' : '0'}" title="Reconnect tunnel">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23,4 23,10 17,10"/><path d="M20.49 15a9 9 0 1 1-.07-8.13"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23,4 23,10 17,10"/><path d="M20.49 15a9 9 0 1 1-.07-8.13"/></svg>
Reconnect Reconnect
</button>` : ''} </button>` : ''}
@@ -119,10 +121,11 @@ function updateConnectionsTable(state) {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
const hostname = btn.dataset.reconnectVhost; const hostname = btn.dataset.reconnectVhost;
const hsUrl = btn.dataset.hsUrl; const hsUrl = btn.dataset.hsUrl;
const useTls = btn.dataset.useTls === '1';
btn.disabled = true; btn.disabled = true;
btn.textContent = 'Reconnecting…'; btn.textContent = 'Reconnecting…';
chrome.runtime.sendMessage( chrome.runtime.sendMessage(
{ target: 'holesail-native', action: 'send', payload: { type: 'setVirtualHost', payload: { hostname, hsUrl } } }, { target: 'holesail-native', action: 'send', payload: { type: 'setVirtualHost', payload: { hostname, hsUrl, useTls } } },
(response) => { (response) => {
if (response?.ok) { if (response?.ok) {
showToast('Tunnel reconnecting…', 'success'); showToast('Tunnel reconnecting…', 'success');
@@ -177,9 +180,11 @@ function setupVirtualHostEvents() {
$('addVhostSubmit')?.addEventListener('click', () => { $('addVhostSubmit')?.addEventListener('click', () => {
const hostnameEl = $('addVhostHostname'); const hostnameEl = $('addVhostHostname');
const hsUrlEl = $('addVhostHsUrl'); const hsUrlEl = $('addVhostHsUrl');
const useTlsEl = $('addVhostUseTls');
let hostname = (hostnameEl?.value || '').trim(); let hostname = (hostnameEl?.value || '').trim();
hostname = hostname.replace(/^https?:\/\//i, '').replace(/[/:?#].*$/, '').toLowerCase().trim(); hostname = hostname.replace(/^https?:\/\//i, '').replace(/[/:?#].*$/, '').toLowerCase().trim();
const hsUrl = (hsUrlEl?.value || '').trim(); const hsUrl = (hsUrlEl?.value || '').trim();
const useTls = useTlsEl ? useTlsEl.checked : false;
const hostnameValidation = isValidVhostHostname(hostname); const hostnameValidation = isValidVhostHostname(hostname);
if (!hostnameValidation.ok) { showModalError('modal-addVhost', 'addVhostError', hostnameValidation.error); return; } 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; } if (!hsUrl || !hsUrl.startsWith('hs://')) { showModalError('modal-addVhost', 'addVhostError', 'Enter a valid hs:// URL'); return; }
@@ -188,12 +193,13 @@ function setupVirtualHostEvents() {
function doAddVhost() { function doAddVhost() {
chrome.runtime.sendMessage( chrome.runtime.sendMessage(
{ target: 'holesail-native', action: 'send', payload: { type: 'setVirtualHost', payload: { hostname, hsUrl } } }, { target: 'holesail-native', action: 'send', payload: { type: 'setVirtualHost', payload: { hostname, hsUrl, useTls } } },
(response) => { (response) => {
if (btn) { btn.disabled = false; btn.textContent = 'Add Host'; } if (btn) { btn.disabled = false; btn.textContent = 'Add Host'; }
if (response?.ok) { if (response?.ok) {
if (hostnameEl) hostnameEl.value = ''; if (hostnameEl) hostnameEl.value = '';
if (hsUrlEl) hsUrlEl.value = ''; if (hsUrlEl) hsUrlEl.value = '';
if (useTlsEl) useTlsEl.checked = false;
closeModal('modal-addVhost'); closeModal('modal-addVhost');
showToast('Virtual host added', 'success'); showToast('Virtual host added', 'success');
refresh(); refresh();
+1 -1
View File
@@ -30,7 +30,7 @@ function saveState() {
id: s.id, port: s.port, host: s.host, secure: s.secure, udp: s.udp || false, label: s.label || '' id: s.id, port: s.port, host: s.host, secure: s.secure, udp: s.udp || false, label: s.label || ''
})); }));
const virtualHostsList = vhostsModule.getVirtualHosts() const virtualHostsList = vhostsModule.getVirtualHosts()
.map(v => ({ hostname: v.hostname, hsUrl: v.hsUrl })); .map(v => ({ hostname: v.hostname, hsUrl: v.hsUrl, useTls: v.useTls === true }));
const serviceTunnelsList = svcModule.getServiceTunnels().map(t => ({ const serviceTunnelsList = svcModule.getServiceTunnels().map(t => ({
id: t.id, label: t.label, hsUrl: t.hsUrl, localPort: t.localPort id: t.id, label: t.label, hsUrl: t.hsUrl, localPort: t.localPort
})); }));
+20 -10
View File
@@ -19,7 +19,7 @@ function debugLog(...args) {
if (process.stderr) process.stderr.write(msg + '\n'); if (process.stderr) process.stderr.write(msg + '\n');
} }
const virtualHosts = new Map(); // hostname -> { hsUrl, holesail, localHost, localPort, state, createdAt, reconnectTimer, reconnectDelay } const virtualHosts = new Map(); // hostname -> { hsUrl, useTls, holesail, localHost, localPort, state, createdAt, reconnectTimer, reconnectDelay }
let _saveState = null; let _saveState = null;
let _emit = null; let _emit = null;
let _getReadyTimeoutMs = null; let _getReadyTimeoutMs = null;
@@ -57,7 +57,7 @@ function _scheduleVhostReconnect(hostname) {
const cur = virtualHosts.get(hostname); const cur = virtualHosts.get(hostname);
if (!cur || cur.state === 'ready') return; if (!cur || cur.state === 'ready') return;
if (process.stderr) process.stderr.write('[holesail-manager] vhost ' + hostname + ' auto-reconnect attempt\n'); if (process.stderr) process.stderr.write('[holesail-manager] vhost ' + hostname + ' auto-reconnect attempt\n');
await setVirtualHost({ hostname, hsUrl: cur.hsUrl }); await setVirtualHost({ hostname, hsUrl: cur.hsUrl, useTls: cur.useTls });
}, delay); }, delay);
} }
@@ -78,13 +78,15 @@ function readyWithTimeout(hs, label) {
* @param {object} payload * @param {object} payload
* @param {string} payload.hostname - The virtual hostname (e.g. `myapp.hs`). Normalised to lowercase. * @param {string} payload.hostname - The virtual hostname (e.g. `myapp.hs`). Normalised to lowercase.
* @param {string} payload.hsUrl - The hs:// key of the remote peer. * @param {string} payload.hsUrl - The hs:// key of the remote peer.
* @param {boolean} [payload.useTls] - If true, proxy connects to backend over TLS (for HTTPS/443 backends).
* @returns {Promise<{ok: boolean, hostname?: string, localHost?: string, localPort?: number, state?: string, error?: string}>} * @returns {Promise<{ok: boolean, hostname?: string, localHost?: string, localPort?: number, state?: string, error?: string}>}
*/ */
async function setVirtualHost(payload) { async function setVirtualHost(payload) {
let hostname = (payload.hostname || payload.hostName || '').trim(); let hostname = (payload.hostname || payload.hostName || '').trim();
hostname = hostname.replace(/^https?:\/\//i, '').replace(/[/:?#].*$/, '').toLowerCase().trim(); hostname = hostname.replace(/^https?:\/\//i, '').replace(/[/:?#].*$/, '').toLowerCase().trim();
const hsUrl = payload.hsUrl || payload.url; const hsUrl = payload.hsUrl || payload.url;
debugLog('setVirtualHost: hostname=', hostname, 'hsUrl=', hsUrl); const useTls = payload.useTls === true;
debugLog('setVirtualHost: hostname=', hostname, 'hsUrl=', hsUrl, 'useTls=', useTls);
if (!Holesail) return { ok: false, error: 'Holesail module not installed' }; if (!Holesail) return { ok: false, error: 'Holesail module not installed' };
if (!hostname || !hsUrl) return { ok: false, error: 'hostname and hsUrl required' }; if (!hostname || !hsUrl) return { ok: false, error: 'hostname and hsUrl required' };
@@ -113,7 +115,8 @@ async function setVirtualHost(payload) {
}); });
} }
await readyWithTimeout(hs, 'vhost:' + hostname); await readyWithTimeout(hs, 'vhost:' + hostname);
virtualHosts.set(hostname, { hsUrl, holesail: hs, localHost: TUNNEL_HOST, localPort, state: 'ready', createdAt: (existing && existing.createdAt) || Date.now(), reconnectTimer: null, reconnectDelay: RECONNECT_BASE_MS }); const entryUseTls = existing ? (payload.useTls === true || existing.useTls === true) : useTls;
virtualHosts.set(hostname, { hsUrl, useTls: entryUseTls, holesail: hs, localHost: TUNNEL_HOST, localPort, state: 'ready', createdAt: (existing && existing.createdAt) || Date.now(), reconnectTimer: null, reconnectDelay: RECONNECT_BASE_MS });
if (_emit) _emit('tunnelReady', { hostname, hsUrl, localHost: TUNNEL_HOST, localPort }); if (_emit) _emit('tunnelReady', { hostname, hsUrl, localHost: TUNNEL_HOST, localPort });
if (_saveState) _saveState(); if (_saveState) _saveState();
debugLog('setVirtualHost: ok hostname=', hostname, 'localPort=', localPort); debugLog('setVirtualHost: ok hostname=', hostname, 'localPort=', localPort);
@@ -123,7 +126,8 @@ async function setVirtualHost(payload) {
try { hs.removeAllListeners(); } catch (_) {} try { hs.removeAllListeners(); } catch (_) {}
releaseTunnelPort(localPort); releaseTunnelPort(localPort);
const existingCreatedAt = existing ? existing.createdAt : undefined; const existingCreatedAt = existing ? existing.createdAt : undefined;
virtualHosts.set(hostname, { hsUrl, holesail: null, localHost: null, localPort: null, state: 'error', createdAt: existingCreatedAt || Date.now() }); const entryUseTls = existing ? (payload.useTls === true || existing.useTls === true) : useTls;
virtualHosts.set(hostname, { hsUrl, useTls: entryUseTls, holesail: null, localHost: null, localPort: null, state: 'error', createdAt: existingCreatedAt || Date.now() });
if (_emit) _emit('tunnelError', { hostname, error: e.message }); if (_emit) _emit('tunnelError', { hostname, error: e.message });
return { ok: false, error: e.message }; return { ok: false, error: e.message };
} }
@@ -151,12 +155,12 @@ async function removeVirtualHost(payload) {
/** /**
* Return a snapshot of all virtual hosts (including errored/closed ones). * Return a snapshot of all virtual hosts (including errored/closed ones).
* @returns {Array<{hostname: string, hsUrl: string, localHost: string|null, localPort: number|null, state: string, createdAt: number}>} * @returns {Array<{hostname: string, hsUrl: string, useTls: boolean, localHost: string|null, localPort: number|null, state: string, createdAt: number}>}
*/ */
function getVirtualHosts() { function getVirtualHosts() {
const list = []; const list = [];
for (const [hostname, v] of virtualHosts) { for (const [hostname, v] of virtualHosts) {
list.push({ hostname, hsUrl: v.hsUrl, localHost: v.localHost ?? null, localPort: v.localPort ?? null, state: v.state || 'unknown', createdAt: v.createdAt }); list.push({ hostname, hsUrl: v.hsUrl, useTls: v.useTls === true, localHost: v.localHost ?? null, localPort: v.localPort ?? null, state: v.state || 'unknown', createdAt: v.createdAt });
} }
return list; return list;
} }
@@ -172,14 +176,20 @@ function getLocalPortForHostname(hostname) {
} }
/** /**
* Return the `{ host, port }` backend object for the HTTPS proxy SNI resolver, * Return the `{ host, port, tls? }` backend object for the HTTPS proxy SNI resolver,
* or null if the virtual host does not exist or is not yet ready. * or null if the virtual host does not exist or is not yet ready.
* When useTls is true, the proxy should connect to the backend over TLS.
* @param {string} hostname * @param {string} hostname
* @returns {{host: string, port: number}|null} * @returns {{host: string, port: number, tls?: boolean}|null}
*/ */
function getLocalBackend(hostname) { function getLocalBackend(hostname) {
const v = virtualHosts.get(hostname); const v = virtualHosts.get(hostname);
const out = (!v || v.localPort == null) ? null : { host: v.localHost ?? '127.0.0.1', port: v.localPort }; if (!v || v.localPort == null) {
debugLog('getLocalBackend: hostname=', hostname, 'result=', null, 'virtualHostsKeys=', Array.from(virtualHosts.keys()));
return null;
}
const out = { host: v.localHost ?? '127.0.0.1', port: v.localPort };
if (v.useTls === true) out.tls = true;
debugLog('getLocalBackend: hostname=', hostname, 'result=', out, 'virtualHostsKeys=', Array.from(virtualHosts.keys())); debugLog('getLocalBackend: hostname=', hostname, 'result=', out, 'virtualHostsKeys=', Array.from(virtualHosts.keys()));
return out; return out;
} }
+1 -1
View File
@@ -39,7 +39,7 @@ async function restorePersistedTunnels(holesailManager, restored) {
} }
let vhostOk = 0, vhostFail = 0; let vhostOk = 0, vhostFail = 0;
for (const v of savedVhosts) { for (const v of savedVhosts) {
const result = await holesailManager.setVirtualHost({ hostname: v.hostname, hsUrl: v.hsUrl }); const result = await holesailManager.setVirtualHost({ hostname: v.hostname, hsUrl: v.hsUrl, useTls: v.useTls === true });
if (result.ok) vhostOk++; else vhostFail++; if (result.ok) vhostOk++; else vhostFail++;
} }
let svcOk = 0, svcFail = 0; let svcOk = 0, svcFail = 0;
+49 -13
View File
@@ -58,7 +58,7 @@ let getBackendForHostname = null;
/** /**
* Register the function used to resolve a virtual hostname to its local backend. * Register the function used to resolve a virtual hostname to its local backend.
* Must be called before `start`. Called by message-router with `holesailManager.getLocalBackend`. * Must be called before `start`. Called by message-router with `holesailManager.getLocalBackend`.
* @param {Function} fn - Called as `fn(hostname)` and should return `{host, port}` or null. * @param {Function} fn - Called as `fn(hostname)` and should return `{host, port, tls?: boolean}` or null.
*/ */
function setHostnameResolver (fn) { function setHostnameResolver (fn) {
getBackendForHostname = fn; getBackendForHostname = fn;
@@ -248,6 +248,7 @@ function onRequest (req, res) {
const backend = getBackendForHostname(hostname); const backend = getBackendForHostname(hostname);
let targetHost = '127.0.0.1'; let targetHost = '127.0.0.1';
let targetPort = null; let targetPort = null;
const backendTls = backend != null && typeof backend === 'object' && backend.tls === true;
if (backend != null && typeof backend === 'object' && typeof backend.port === 'number') { if (backend != null && typeof backend === 'object' && typeof backend.port === 'number') {
targetHost = backend.host ?? '127.0.0.1'; targetHost = backend.host ?? '127.0.0.1';
targetPort = backend.port; targetPort = backend.port;
@@ -306,11 +307,25 @@ h1{color:#c0392b}code{background:#f4f4f4;padding:2px 6px;border-radius:3px;font-
method: req.method || 'GET', method: req.method || 'GET',
headers: reqHeaders headers: reqHeaders
}; };
if (backendTls) {
opts.backendTls = true;
opts.servername = hostname;
const tlsAgent = new http1.Agent();
tlsAgent.createConnection = (o) => {
if (o.backendTls && o.servername) {
const tcp = net.createConnection(o.port, o.host);
return new tls.Socket(tcp, { host: o.servername });
}
return net.createConnection(o);
};
opts.agent = tlsAgent;
}
let timedOut = false; let timedOut = false;
let proxyReq;
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
timedOut = true; timedOut = true;
proxyReq.destroy(); if (proxyReq) proxyReq.destroy();
if (!res.writableEnded) { if (!res.writableEnded) {
res.statusCode = 504; res.statusCode = 504;
res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Type', 'text/plain');
@@ -318,7 +333,7 @@ h1{color:#c0392b}code{background:#f4f4f4;padding:2px 6px;border-radius:3px;font-
} }
}, BACKEND_TIMEOUT_MS); }, BACKEND_TIMEOUT_MS);
const proxyReq = http1.request(opts, (proxyRes) => { proxyReq = http1.request(opts, (proxyRes) => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
if (timedOut) return; if (timedOut) return;
res.statusCode = proxyRes.statusCode; res.statusCode = proxyRes.statusCode;
@@ -359,6 +374,7 @@ function onUpgrade (req, socket, head) {
const backend = getBackendForHostname(hostname); const backend = getBackendForHostname(hostname);
let targetHost = '127.0.0.1'; let targetHost = '127.0.0.1';
let targetPort = null; let targetPort = null;
const backendTls = backend != null && typeof backend === 'object' && backend.tls === true;
if (backend != null && typeof backend === 'object' && typeof backend.port === 'number') { if (backend != null && typeof backend === 'object' && typeof backend.port === 'number') {
targetHost = backend.host ?? '127.0.0.1'; targetHost = backend.host ?? '127.0.0.1';
targetPort = backend.port; targetPort = backend.port;
@@ -376,8 +392,35 @@ function onUpgrade (req, socket, head) {
try { bareTcp = require('bare-tcp'); } catch (_) {} try { bareTcp = require('bare-tcp'); } catch (_) {}
if (!bareTcp) { socket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n'); socket.destroy(); return; } if (!bareTcp) { socket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n'); socket.destroy(); return; }
// Register error handler synchronously before the connect callback can fire, function writeUpgradeAndPipe(upstream) {
// so an immediate ECONNREFUSED is never an unhandled error event. const headers = Object.entries(req.headers).map(([k, v]) => k + ': ' + v).join('\r\n');
const requestLine = (req.method || 'GET') + ' ' + (req.url || '/') + ' HTTP/1.1\r\n';
upstream.write(requestLine + headers + '\r\n\r\n');
if (head && head.length > 0) upstream.write(head);
socket.on('data', (c) => { trafficStats.bytesIn += c.length; });
upstream.on('data', (c) => { trafficStats.bytesOut += c.length; });
socket.pipe(upstream);
upstream.pipe(socket);
}
if (backendTls) {
const tcpSocket = bareTcp.connect(targetPort, targetHost);
const tlsSocket = new tls.Socket(tcpSocket, { host: hostname });
function _preConnectError() {
try { socket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n'); socket.destroy(); } catch (_) {}
try { tlsSocket.destroy(); } catch (_) {}
}
tlsSocket.on('error', _preConnectError);
socket.on('error', () => { try { tlsSocket.destroy(); } catch (_) {} });
tlsSocket.on('connect', () => {
tlsSocket.removeListener('error', _preConnectError);
tlsSocket.on('error', () => { try { socket.destroy(); } catch (_) {} });
writeUpgradeAndPipe(tlsSocket);
});
return;
}
// Plain TCP backend
const upstream = bareTcp.connect(targetPort, targetHost); const upstream = bareTcp.connect(targetPort, targetHost);
function _preConnectError() { function _preConnectError() {
try { socket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n'); socket.destroy(); } catch (_) {} try { socket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n'); socket.destroy(); } catch (_) {}
@@ -388,14 +431,7 @@ function onUpgrade (req, socket, head) {
upstream.on('connect', () => { upstream.on('connect', () => {
upstream.removeListener('error', _preConnectError); upstream.removeListener('error', _preConnectError);
upstream.on('error', () => { try { socket.destroy(); } catch (_) {} }); upstream.on('error', () => { try { socket.destroy(); } catch (_) {} });
const headers = Object.entries(req.headers).map(([k, v]) => k + ': ' + v).join('\r\n'); writeUpgradeAndPipe(upstream);
const requestLine = (req.method || 'GET') + ' ' + (req.url || '/') + ' HTTP/1.1\r\n';
upstream.write(requestLine + headers + '\r\n\r\n');
if (head && head.length > 0) upstream.write(head);
socket.on('data', (c) => { trafficStats.bytesIn += c.length; });
upstream.on('data', (c) => { trafficStats.bytesOut += c.length; });
socket.pipe(upstream);
upstream.pipe(socket);
}); });
} }