import { Elysia, t, type Context } from 'elysia' import { version } from './package.json'; import { basicAuth } from '@eelkevdbos/elysia-basic-auth' import net from 'net' import { appendFile } from "node:fs/promises"; if (!process.env.WL_USERNAME) throw new Error('WL_USERNAME missing in env'); if (!process.env.WL_PASSWORD) throw new Error('WL_PASSWORD missing in env'); const whitelistFilePath = process.env.WL_FILE_PATH || "/etc/opentracker/whitelist" const username = process.env.WL_USERNAME! const password = process.env.WL_PASSWORD! interface DockerContainer { Id: string; Command: string; } const authOpts = { scope: [ "/whitelist", "/version" ], credentials: [ { username: username, password: password } ] } const startupChecks = async function startupChecks() { if (!process.env.WL_FILE_PATH) { console.warn(`WL_FILE_PATH is missing in env. Using default ${whitelistFilePath}`) } } const getWhitelist = async function getWhitelist(ctx: Context) { const wl = Bun.file(whitelistFilePath); // relative to cwd console.debug(`read from whitelist file at ${whitelistFilePath}. size=${wl.size}, type=${wl.type}`) return wl.text() } async function findOpentrackerContainer(socketPath = "/var/run/docker.sock"): Promise { return new Promise((resolve, reject) => { console.debug(`opening net client at socketPath=${socketPath}`) const client = net.createConnection(socketPath, () => { const request = 'GET /containers/json HTTP/1.0\r\n\r\n'; client.write(request); }); console.debug(`waiting for response from socket`) let response = ''; client.on('data', (data) => { console.debug(`client got data`) response += data.toString(); }); console.debug(`waiting for connection end`) client.on('end', () => { console.debug(`client end detected`) try { const body = response.split('\r\n\r\n')[1]; const containers: DockerContainer[] = JSON.parse(body); const container = containers.find(c => c.Command.includes('/bin/opentracker')); resolve(container || null); } catch (err) { reject(err); } }); client.on('error', (err) => { console.error(`net client encountered error ${err}`) reject(err); }); }); } async function killContainer(socketPath = "/var/run/docker.sock", containerId: string, signal = "SIGTERM") { const request = `POST /containers/${containerId}/kill?signal=${signal} HTTP/1.0\r\n\r\n`; return new Promise((resolve, reject) => { const client = net.createConnection(socketPath, () => { client.write(request); }); client.on('data', (data: any) => { // console.log(data.toString()); client.end(); resolve(data.toString()); }); client.on('error', (err: any) => { console.error('Error:', err); reject(err); }); }); } const maybeKillContainer = async function maybeKillContainer(signal: string = "SIGUSR1") { const sockFile = Bun.file('/var/run/docker.sock') const sockFileExists = await sockFile.exists() if (!sockFileExists) { console.warn("⚠️ docker sock file not found. skipping.") } else { console.debug('looking for opentracker container') const container = await findOpentrackerContainer() if (!container) { console.warn('⚠️ failed to find opentracker container'); } else { await killContainer(undefined, container.Id, signal) console.debug('sending SIGUSR1 to container ' + container.Id) } } } const postWhitelist = async function postWhitelist(ctx: Context) { let body = ctx.body console.debug('appending to whitelist at ' + whitelistFilePath) await appendFile(whitelistFilePath, body + "\n"); await maybeKillContainer("SIGUSR1") ctx.set.status = 201 return body } await startupChecks() const app = new Elysia() .use(basicAuth(authOpts)) .get('/health', () => 'OK') .get('/version', () => `version ${version}`) .get('/whitelist', getWhitelist) .post('/whitelist', postWhitelist, { body: t.String() }) export default app