Files
dashboard/api/proxy.js
T

94 lines
3.3 KiB
JavaScript

// ---------------------------------------------------------------------------
// Outbound proxy support for the container backend scripts.
//
// Node's core http/https modules ignore proxy env vars, so requests that
// should go through a forward proxy are tunneled manually via HTTP CONNECT.
// Currently used for GitLab access (see GITLAB_PROXY below).
//
// Configure in .env / docker-compose.yml (no rebuild needed, container
// restart is enough):
// GITLAB_PROXY=http://127.0.0.1:8118 (updater run on the host)
// GITLAB_PROXY=http://host.docker.internal:8118 (proxy on the docker host)
// GITLAB_PROXY=http://10.0.0.5:8118 (proxy moved elsewhere)
// Empty/unset disables proxying (direct connection).
// ---------------------------------------------------------------------------
const http = require("http");
const tls = require("tls");
const { URL } = require("url");
const GITLAB_PROXY = process.env.GITLAB_PROXY || "";
const GITLAB_HOST = (() => {
try {
return new URL(process.env.GITLAB_URL || "https://gitlab.ixsol.wien").hostname;
} catch {
return "gitlab.ixsol.wien";
}
})();
// Returns the proxy URL to use for a given target URL, or null for direct.
function proxyFor(url) {
if (!GITLAB_PROXY) return null;
try {
if (new URL(url).hostname === GITLAB_HOST) return GITLAB_PROXY;
} catch {
// unparseable URL — no proxy
}
return null;
}
// Opens a CONNECT tunnel through the proxy and resolves to a socket usable
// as the connection for a request to targetUrl: a TLS socket for https
// targets, the raw tunnel socket otherwise.
function connectThroughProxy(proxyUrl, targetUrl, timeoutMs, tlsOptions = {}) {
return new Promise((resolve, reject) => {
const target = new URL(targetUrl);
const proxy = new URL(proxyUrl);
const port = target.port || (target.protocol === "https:" ? "443" : "80");
const req = http.request({
host: proxy.hostname,
port: proxy.port || 80,
method: "CONNECT",
path: `${target.hostname}:${port}`,
headers: { Host: `${target.hostname}:${port}` },
timeout: timeoutMs || 10000,
});
req.on("connect", (res, socket) => {
if (res.statusCode !== 200) {
socket.destroy();
reject(new Error(`Proxy CONNECT to ${target.hostname} failed: HTTP ${res.statusCode}`));
return;
}
resolve(
target.protocol === "https:"
? tls.connect({ socket, servername: target.hostname, ...tlsOptions })
: socket
);
});
req.on("timeout", () => {
req.destroy();
reject(new Error(`Proxy CONNECT to ${target.hostname} timed out`));
});
req.on("error", reject);
req.end();
});
}
// Request options for client.request() that route the request through the
// proxy: https via the CONNECT tunnel (caller passes the socket returned by
// connectThroughProxy), plain http via an absolute-form request URI.
function proxiedRequestOptions(proxyUrl, targetUrl, socket) {
const u = new URL(targetUrl);
if (u.protocol === "https:") {
return { agent: false, createConnection: () => socket };
}
const p = new URL(proxyUrl);
return {
host: p.hostname,
port: p.port || 80,
path: u.href,
headers: { Host: u.host },
};
}
module.exports = { proxyFor, connectThroughProxy, proxiedRequestOptions };