158 lines
4.2 KiB
TypeScript
158 lines
4.2 KiB
TypeScript
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.TRACKER_HELPER_USERNAME) throw new Error('TRACKER_HELPER_USERNAME missing in env');
|
|
if (!process.env.TRACKER_HELPER_PASSWORD) throw new Error('TRACKER_HELPER_PASSWORD missing in env');
|
|
|
|
const accesslistFilePath = process.env.TRACKER_HELPER_ACCESSLIST_PATH || "/var/lib/aquatic/accesslist"
|
|
const username = process.env.TRACKER_HELPER_USERNAME!
|
|
const password = process.env.TRACKER_HELPER_PASSWORD!
|
|
|
|
interface DockerContainer {
|
|
Id: string;
|
|
Command: string;
|
|
}
|
|
|
|
const authOpts = {
|
|
scope: [
|
|
"/accesslist",
|
|
"/version"
|
|
],
|
|
credentials: [
|
|
{
|
|
username: username,
|
|
password: password
|
|
}
|
|
]
|
|
}
|
|
|
|
const startupChecks = async function startupChecks() {
|
|
|
|
|
|
if (!process.env.TRACKER_HELPER_ACCESSLIST_PATH) {
|
|
console.warn(`TRACKER_HELPER_ACCESSLIST_PATH is missing in env. Using default ${accesslistFilePath}`)
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getAccesslist = async function getAccesslist(ctx: Context) {
|
|
const wl = Bun.file(accesslistFilePath); // relative to cwd
|
|
console.debug(`read from accesslist file at ${accesslistFilePath}. size=${wl.size}, type=${wl.type}`)
|
|
return wl.text()
|
|
}
|
|
|
|
|
|
|
|
|
|
async function findOpentrackerContainer(socketPath = "/var/run/docker.sock"): Promise<DockerContainer | null> {
|
|
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 postAccesslist = async function postAccesslist(ctx: Context) {
|
|
let body = ctx.body
|
|
|
|
console.debug('appending to accesslist at ' + accesslistFilePath)
|
|
await appendFile(accesslistFilePath, 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('/accesslist', getAccesslist)
|
|
.post('/accesslist', postAccesslist, {
|
|
body: t.String()
|
|
})
|
|
|
|
|
|
export default app
|