feat(virtual-hosts): add TLS option for HTTPS/443 backends
CI / Build & Test (push) Failing after 2m44s
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:
@@ -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">
|
||||||
|
|||||||
@@ -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, '"');
|
const safeHostname = hostname.replace(/"/g, '"');
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user