Delete server.js
This commit is contained in:
@@ -1,216 +0,0 @@
|
|||||||
import express from "express";
|
|
||||||
import fs from "fs/promises";
|
|
||||||
import path from "path";
|
|
||||||
import Docker from "dockerode";
|
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
||||||
|
|
||||||
const PORT = process.env.PORT || 8080;
|
|
||||||
const SQUID_CONTAINER = process.env.SQUID_CONTAINER || "squid";
|
|
||||||
const STATE_FILE = "/data/state.json";
|
|
||||||
const SQUID_CONFIG_DIR = "/squid-config";
|
|
||||||
const WPAD_DIR = "/wpad";
|
|
||||||
|
|
||||||
const docker = new Docker({ socketPath: "/var/run/docker.sock" });
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
app.use(express.json({ limit: "100kb" }));
|
|
||||||
app.use(express.static(path.join(__dirname, "public")));
|
|
||||||
|
|
||||||
const DEFAULTS = {
|
|
||||||
networks: ["192.168.0.0/24", "192.168.1.0/24", "192.168.2.0/24"],
|
|
||||||
domains: ["*.netflix.com", "*.spotify.com"],
|
|
||||||
upstreamHost: "gluetun",
|
|
||||||
upstreamPort: "8888",
|
|
||||||
proxyHost: "cerberus.lan",
|
|
||||||
proxyPort: "3128",
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------- State persistence ----------
|
|
||||||
|
|
||||||
async function loadState() {
|
|
||||||
try {
|
|
||||||
const raw = await fs.readFile(STATE_FILE, "utf-8");
|
|
||||||
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
||||||
} catch {
|
|
||||||
return { ...DEFAULTS };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function writeAtomic(filePath, content) {
|
|
||||||
const tmp = filePath + ".tmp";
|
|
||||||
await fs.writeFile(tmp, content, { mode: 0o644 });
|
|
||||||
await fs.rename(tmp, filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveState(state) {
|
|
||||||
await writeAtomic(STATE_FILE, JSON.stringify(state, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- Validation ----------
|
|
||||||
|
|
||||||
const CIDR_RE = /^\d{1,3}(\.\d{1,3}){3}\/\d{1,2}$/;
|
|
||||||
const HOST_RE = /^[a-zA-Z0-9.\-*_]+$/;
|
|
||||||
const PORT_RE = /^\d{1,5}$/;
|
|
||||||
|
|
||||||
function validate(state) {
|
|
||||||
if (!state || typeof state !== "object") throw new Error("invalid payload");
|
|
||||||
if (!Array.isArray(state.networks)) throw new Error("networks must be array");
|
|
||||||
if (!Array.isArray(state.domains)) throw new Error("domains must be array");
|
|
||||||
for (const n of state.networks) {
|
|
||||||
if (!CIDR_RE.test(n)) throw new Error(`invalid network: ${n}`);
|
|
||||||
}
|
|
||||||
for (const d of state.domains) {
|
|
||||||
if (!HOST_RE.test(d)) throw new Error(`invalid domain: ${d}`);
|
|
||||||
}
|
|
||||||
for (const f of ["upstreamHost", "proxyHost"]) {
|
|
||||||
if (typeof state[f] !== "string" || !HOST_RE.test(state[f])) {
|
|
||||||
throw new Error(`invalid ${f}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const f of ["upstreamPort", "proxyPort"]) {
|
|
||||||
if (!PORT_RE.test(String(state[f])) || +state[f] < 1 || +state[f] > 65535) {
|
|
||||||
throw new Error(`invalid ${f}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- Generators ----------
|
|
||||||
|
|
||||||
function generateSquidConf(s) {
|
|
||||||
return `http_port 3128
|
|
||||||
|
|
||||||
# Erlaubte Subnetze
|
|
||||||
${s.networks.map((n) => `acl localnet src ${n}`).join("\n")}
|
|
||||||
|
|
||||||
http_access allow localnet
|
|
||||||
http_access deny all
|
|
||||||
|
|
||||||
# Domain-Whitelist
|
|
||||||
acl vpn_domains dstdomain "/etc/squid/vpn_domains.txt"
|
|
||||||
|
|
||||||
# Upstream zu Gluetun
|
|
||||||
cache_peer ${s.upstreamHost} parent ${s.upstreamPort} 0 no-query default name=gluetun
|
|
||||||
never_direct allow vpn_domains
|
|
||||||
cache_peer_access gluetun allow vpn_domains
|
|
||||||
cache_peer_access gluetun deny all
|
|
||||||
|
|
||||||
never_direct deny all
|
|
||||||
forwarded_for delete
|
|
||||||
|
|
||||||
access_log /var/log/squid/access.log
|
|
||||||
cache_log /var/log/squid/cache.log
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateVpnDomains(s) {
|
|
||||||
return (
|
|
||||||
s.domains
|
|
||||||
.map((d) => (d.startsWith("*.") ? "." + d.slice(2) : d))
|
|
||||||
.join("\n") + "\n"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateWpadDat(s) {
|
|
||||||
return `function FindProxyForURL(url, host) {
|
|
||||||
var vpn_domains = [
|
|
||||||
${s.domains.map((d) => ` "${d}"`).join(",\n")}
|
|
||||||
];
|
|
||||||
|
|
||||||
for (var i = 0; i < vpn_domains.length; i++) {
|
|
||||||
if (shExpMatch(host, vpn_domains[i])) {
|
|
||||||
return "PROXY ${s.proxyHost}:${s.proxyPort}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "DIRECT";
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function applyConfig(state) {
|
|
||||||
await writeAtomic(
|
|
||||||
path.join(SQUID_CONFIG_DIR, "squid.conf"),
|
|
||||||
generateSquidConf(state)
|
|
||||||
);
|
|
||||||
await writeAtomic(
|
|
||||||
path.join(SQUID_CONFIG_DIR, "vpn_domains.txt"),
|
|
||||||
generateVpnDomains(state)
|
|
||||||
);
|
|
||||||
await writeAtomic(path.join(WPAD_DIR, "wpad.dat"), generateWpadDat(state));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- Squid reload ----------
|
|
||||||
|
|
||||||
async function reloadSquid() {
|
|
||||||
const container = docker.getContainer(SQUID_CONTAINER);
|
|
||||||
const exec = await container.exec({
|
|
||||||
Cmd: ["squid", "-k", "reconfigure"],
|
|
||||||
AttachStdout: true,
|
|
||||||
AttachStderr: true,
|
|
||||||
});
|
|
||||||
const stream = await exec.start({ hijack: true, stdin: false });
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let out = "";
|
|
||||||
stream.on("data", (chunk) => (out += chunk.toString()));
|
|
||||||
stream.on("end", async () => {
|
|
||||||
const info = await exec.inspect();
|
|
||||||
if (info.ExitCode === 0) resolve(out);
|
|
||||||
else reject(new Error(`squid -k reconfigure exit ${info.ExitCode}: ${out}`));
|
|
||||||
});
|
|
||||||
stream.on("error", reject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- API ----------
|
|
||||||
|
|
||||||
app.get("/api/config", async (req, res) => {
|
|
||||||
try {
|
|
||||||
res.json(await loadState());
|
|
||||||
} catch (e) {
|
|
||||||
res.status(500).json({ error: e.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post("/api/config", async (req, res) => {
|
|
||||||
try {
|
|
||||||
validate(req.body);
|
|
||||||
await saveState(req.body);
|
|
||||||
await applyConfig(req.body);
|
|
||||||
let reloaded = false;
|
|
||||||
let reloadError = null;
|
|
||||||
try {
|
|
||||||
await reloadSquid();
|
|
||||||
reloaded = true;
|
|
||||||
} catch (e) {
|
|
||||||
reloadError = e.message;
|
|
||||||
console.error("[reload]", e.message);
|
|
||||||
}
|
|
||||||
res.json({ ok: true, reloaded, reloadError });
|
|
||||||
} catch (e) {
|
|
||||||
res.status(400).json({ error: e.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post("/api/reload", async (req, res) => {
|
|
||||||
try {
|
|
||||||
const out = await reloadSquid();
|
|
||||||
res.json({ ok: true, output: out });
|
|
||||||
} catch (e) {
|
|
||||||
res.status(500).json({ error: e.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get("/api/health", (req, res) => res.json({ ok: true }));
|
|
||||||
|
|
||||||
// Initial bootstrap: ensure files exist on first start
|
|
||||||
(async () => {
|
|
||||||
const state = await loadState();
|
|
||||||
await applyConfig(state).catch((e) =>
|
|
||||||
console.error("[bootstrap] could not write configs:", e.message)
|
|
||||||
);
|
|
||||||
app.listen(PORT, () =>
|
|
||||||
console.log(`squid-configurator listening on :${PORT}`)
|
|
||||||
);
|
|
||||||
})();
|
|
||||||
Reference in New Issue
Block a user