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