Route GitLab stats and health checks through a configurable proxy
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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 };
|
||||
+24
-8
@@ -13,6 +13,7 @@ const https = require("https");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { URL } = require("url");
|
||||
const { proxyFor, connectThroughProxy, proxiedRequestOptions } = require("./proxy");
|
||||
|
||||
const OUT_FILE = process.env.OUT_FILE || "/usr/share/nginx/html/data.json";
|
||||
const TZ = process.env.TZ || "UTC";
|
||||
@@ -41,18 +42,33 @@ const GITLAB_TOKEN = process.env.GITLAB_TOKEN || "";
|
||||
const OPENWEBUI_URL = process.env.OPENWEBUI_URL || "http://100.103.83.12:3000";
|
||||
const OPENWEBUI_TOKEN = process.env.OPENWEBUI_TOKEN || "";
|
||||
|
||||
function request(url, options = {}) {
|
||||
// Central outbound request helper: honors the outbound proxy (GITLAB_PROXY)
|
||||
// for the configured host — see api/proxy.js.
|
||||
async function request(url, options = {}) {
|
||||
const u = new URL(url);
|
||||
const client = u.protocol === "https:" ? https : http;
|
||||
const requestOptions = {
|
||||
method: options.method || "GET",
|
||||
headers: options.headers || {},
|
||||
timeout: options.timeout || 15000,
|
||||
};
|
||||
|
||||
const proxy = proxyFor(url);
|
||||
if (proxy) {
|
||||
// https is tunneled through the proxy via CONNECT; plain http uses an
|
||||
// absolute-form request URI addressed to the proxy.
|
||||
let socket = null;
|
||||
if (u.protocol === "https:") {
|
||||
socket = await connectThroughProxy(proxy, url, requestOptions.timeout);
|
||||
}
|
||||
Object.assign(requestOptions, proxiedRequestOptions(proxy, url, socket));
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const start = Date.now();
|
||||
const u = new URL(url);
|
||||
const client = u.protocol === "https:" ? https : http;
|
||||
const req = client.request(
|
||||
u,
|
||||
{
|
||||
method: options.method || "GET",
|
||||
headers: options.headers || {},
|
||||
timeout: options.timeout || 15000,
|
||||
},
|
||||
requestOptions,
|
||||
(res) => {
|
||||
let body = "";
|
||||
res.setEncoding("utf8");
|
||||
|
||||
+48
-31
@@ -15,6 +15,7 @@ const https = require("https");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { URL } = require("url");
|
||||
const { proxyFor, connectThroughProxy, proxiedRequestOptions } = require("./proxy");
|
||||
|
||||
const OUT_FILE = process.env.OUT_FILE || "/usr/share/nginx/html/health.json";
|
||||
const TIMEOUT = parseInt(process.env.HEALTH_TIMEOUT || "5000", 10);
|
||||
@@ -51,40 +52,56 @@ function checkUrl(url) {
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
let req;
|
||||
try {
|
||||
const u = new URL(url);
|
||||
const client = u.protocol === "https:" ? https : http;
|
||||
req = client.request(
|
||||
u,
|
||||
{
|
||||
(async () => {
|
||||
let req;
|
||||
try {
|
||||
const u = new URL(url);
|
||||
const client = u.protocol === "https:" ? https : http;
|
||||
const options = {
|
||||
method: "GET",
|
||||
agent: u.protocol === "https:" ? httpsAgent : httpAgent,
|
||||
timeout: TIMEOUT,
|
||||
},
|
||||
(res) => {
|
||||
const code = res.statusCode;
|
||||
res.destroy(); // response headers are all we need
|
||||
done({
|
||||
status: code >= 500 ? "error" : "online",
|
||||
code,
|
||||
latencyMs: Date.now() - started,
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
return done({ status: "offline", code: null, latencyMs: null, error: e.message });
|
||||
}
|
||||
};
|
||||
|
||||
req.on("timeout", () => {
|
||||
req.destroy();
|
||||
done({ status: "offline", code: null, latencyMs: null, error: "Timeout" });
|
||||
});
|
||||
req.on("error", (e) => {
|
||||
done({ status: "offline", code: null, latencyMs: null, error: e.message });
|
||||
});
|
||||
req.end();
|
||||
// Honor the outbound proxy (GITLAB_PROXY) so the GitLab badge checks
|
||||
// the same path the git stats actually take (see api/proxy.js).
|
||||
const proxy = proxyFor(url);
|
||||
if (proxy) {
|
||||
let socket = null;
|
||||
if (u.protocol === "https:") {
|
||||
socket = await connectThroughProxy(proxy, url, TIMEOUT, { rejectUnauthorized: false });
|
||||
}
|
||||
Object.assign(options, proxiedRequestOptions(proxy, url, socket));
|
||||
} else {
|
||||
options.agent = u.protocol === "https:" ? httpsAgent : httpAgent;
|
||||
}
|
||||
|
||||
req = client.request(
|
||||
u,
|
||||
options,
|
||||
(res) => {
|
||||
const code = res.statusCode;
|
||||
res.destroy(); // response headers are all we need
|
||||
done({
|
||||
status: code >= 500 ? "error" : "online",
|
||||
code,
|
||||
latencyMs: Date.now() - started,
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
return done({ status: "offline", code: null, latencyMs: null, error: e.message });
|
||||
}
|
||||
|
||||
req.on("timeout", () => {
|
||||
req.destroy();
|
||||
done({ status: "offline", code: null, latencyMs: null, error: "Timeout" });
|
||||
});
|
||||
req.on("error", (e) => {
|
||||
done({ status: "offline", code: null, latencyMs: null, error: e.message });
|
||||
});
|
||||
req.end();
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user