tangled
alpha
login
or
join now
dunkirk.sh
/
control
0
fork
atom
a control panel for my server
0
fork
atom
overview
issues
pulls
pipelines
chore: migrate to sqlite
dunkirk.sh
2 months ago
28e96f2e
e5083536
verified
This commit was signed with the committer's
known signature
.
dunkirk.sh
SSH Key Fingerprint:
SHA256:DqcG0RXYExE26KiWo3VxJnsxswN1QNfTBvB+bdSpk80=
+89
-30
3 changed files
expand all
collapse all
unified
split
flags.json
src
flags.ts
index.ts
+2
-1
flags.json
···
5
5
"flags": {
6
6
"block-map-sse": {
7
7
"name": "Block SSE Endpoint",
8
8
-
"description": "Disable /sse Server-Sent Events"
8
8
+
"description": "Disable /sse Server-Sent Events",
9
9
+
"path": "/sse"
9
10
}
10
11
}
11
12
}
+65
-22
src/flags.ts
···
1
1
-
import { join } from "path";
2
2
-
import { unlink } from "fs/promises";
1
1
+
import { Database } from "bun:sqlite";
3
2
import flagsConfig from "../flags.json";
4
3
5
5
-
async function exists(path: string): Promise<boolean> {
6
6
-
return Bun.file(path).exists();
7
7
-
}
4
4
+
const DB_PATH = process.env.DATABASE_PATH || "./data/control.db";
5
5
+
6
6
+
// Initialize database
7
7
+
const db = new Database(DB_PATH, { create: true });
8
8
+
db.exec(`
9
9
+
CREATE TABLE IF NOT EXISTS flags (
10
10
+
id TEXT PRIMARY KEY,
11
11
+
enabled INTEGER NOT NULL DEFAULT 0,
12
12
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
13
13
+
)
14
14
+
`);
15
15
+
16
16
+
// Prepared statements for performance
17
17
+
const getFlag = db.prepare<{ enabled: number }, [string]>(
18
18
+
"SELECT enabled FROM flags WHERE id = ?"
19
19
+
);
20
20
+
const setFlagStmt = db.prepare(
21
21
+
`INSERT INTO flags (id, enabled, updated_at) VALUES (?, ?, datetime('now'))
22
22
+
ON CONFLICT(id) DO UPDATE SET enabled = excluded.enabled, updated_at = datetime('now')`
23
23
+
);
24
24
+
const getAllFlags = db.prepare<{ id: string; enabled: number }, []>(
25
25
+
"SELECT id, enabled FROM flags"
26
26
+
);
8
27
9
28
export interface FlagDefinition {
10
29
name: string;
11
30
description: string;
31
31
+
path?: string; // The path this flag blocks (e.g., "/sse")
12
32
}
13
33
14
34
export interface ServiceDefinition {
···
27
47
enabled: boolean;
28
48
service: string;
29
49
}
30
30
-
31
31
-
const FLAGS_DIR = process.env.FLAGS_DIR || "/var/lib/caddy/flags";
32
50
33
51
export function getConfig(): FlagsConfig {
34
52
return flagsConfig as FlagsConfig;
···
57
75
return null;
58
76
}
59
77
60
60
-
export async function getFlagStatus(flagId: string): Promise<boolean> {
61
61
-
const path = join(FLAGS_DIR, flagId);
62
62
-
return exists(path);
78
78
+
export function getFlagStatus(flagId: string): boolean {
79
79
+
const row = getFlag.get(flagId);
80
80
+
return row?.enabled === 1;
63
81
}
64
82
65
65
-
export async function setFlag(flagId: string, enabled: boolean): Promise<void> {
83
83
+
export function setFlag(flagId: string, enabled: boolean): void {
66
84
if (!getFlagDefinition(flagId)) {
67
85
throw new Error(`Unknown flag: ${flagId}`);
68
86
}
69
69
-
70
70
-
const path = join(FLAGS_DIR, flagId);
71
71
-
if (enabled) {
72
72
-
await Bun.write(path, "");
73
73
-
} else {
74
74
-
if (await exists(path)) {
75
75
-
await unlink(path);
76
76
-
}
77
77
-
}
87
87
+
setFlagStmt.run(flagId, enabled ? 1 : 0);
78
88
}
79
89
80
80
-
export async function getAllFlagsStatus(): Promise<Record<string, FlagStatus[]>> {
90
90
+
export function getAllFlagsStatus(): Record<string, FlagStatus[]> {
81
91
const config = getConfig();
82
92
const result: Record<string, FlagStatus[]> = {};
83
93
94
94
+
// Get all current flag states from DB
95
95
+
const dbFlags = new Map<string, boolean>();
96
96
+
for (const row of getAllFlags.all()) {
97
97
+
dbFlags.set(row.id, row.enabled === 1);
98
98
+
}
99
99
+
84
100
for (const [serviceId, service] of Object.entries(config.services)) {
85
101
const flags: FlagStatus[] = [];
86
102
for (const [flagId, flag] of Object.entries(service.flags)) {
···
88
104
id: flagId,
89
105
name: flag.name,
90
106
description: flag.description,
91
91
-
enabled: await getFlagStatus(flagId),
107
107
+
enabled: dbFlags.get(flagId) ?? false,
92
108
service: serviceId,
93
109
});
94
110
}
···
97
113
98
114
return result;
99
115
}
116
116
+
117
117
+
// Check if a request should be blocked based on host and path
118
118
+
export function shouldBlock(host: string, path: string): boolean {
119
119
+
const config = getConfig();
120
120
+
121
121
+
for (const [serviceId, service] of Object.entries(config.services)) {
122
122
+
// Check if this request matches a service
123
123
+
if (!host.includes(serviceId) && !serviceId.includes(host)) {
124
124
+
continue;
125
125
+
}
126
126
+
127
127
+
for (const [flagId, flag] of Object.entries(service.flags)) {
128
128
+
// Check if flag is enabled (blocking)
129
129
+
if (!getFlagStatus(flagId)) {
130
130
+
continue;
131
131
+
}
132
132
+
133
133
+
// Check if the flag applies to this path
134
134
+
const flagPath = flag.path || `/${flagId.split("-").pop()}`;
135
135
+
if (path === flagPath || path.startsWith(flagPath + "/") || path.startsWith(flagPath + "?")) {
136
136
+
return true;
137
137
+
}
138
138
+
}
139
139
+
}
140
140
+
141
141
+
return false;
142
142
+
}
+22
-7
src/index.ts
···
18
18
getFlagStatus,
19
19
setFlag,
20
20
getFlagDefinition,
21
21
+
shouldBlock,
21
22
} from "./flags";
22
23
23
24
import homepage from "../public/index.html";
···
35
36
logo_uri: "https://hc-cdn.hel1.your-objectstorage.com/s/v3/d19f900e04238dcd_control.png",
36
37
redirect_uris: [REDIRECT_URI],
37
38
});
39
39
+
});
40
40
+
41
41
+
// Kill-check endpoint for Caddy to call before proxying protected routes
42
42
+
// Returns 200 to allow, 503 to block
43
43
+
// No auth required - this is called by Caddy internally
44
44
+
app.get("/kill-check", (c) => {
45
45
+
const host = c.req.header("X-Orig-Host") || c.req.header("Host") || "";
46
46
+
const path = c.req.header("X-Orig-Path") || "/";
47
47
+
48
48
+
if (shouldBlock(host, path)) {
49
49
+
return c.text("Temporarily disabled", 503);
50
50
+
}
51
51
+
52
52
+
return c.text("OK", 200);
38
53
});
39
54
40
55
app.get("/auth/login", (c) => {
···
99
114
api.use("/flags/*", apiAuthMiddleware);
100
115
api.use("/flags", apiAuthMiddleware);
101
116
102
102
-
api.get("/flags", async (c) => {
103
103
-
const flags = await getAllFlagsStatus();
117
117
+
api.get("/flags", (c) => {
118
118
+
const flags = getAllFlagsStatus();
104
119
return c.json(flags);
105
120
});
106
121
107
107
-
api.get("/flags/:name", async (c) => {
122
122
+
api.get("/flags/:name", (c) => {
108
123
const name = c.req.param("name");
109
124
const definition = getFlagDefinition(name);
110
125
···
112
127
return c.json({ error: "Unknown flag" }, 404);
113
128
}
114
129
115
115
-
const enabled = await getFlagStatus(name);
130
130
+
const enabled = getFlagStatus(name);
116
131
return c.json({
117
132
id: name,
118
133
name: definition.flag.name,
···
135
150
return c.json({ error: "Invalid body: enabled must be boolean" }, 400);
136
151
}
137
152
138
138
-
await setFlag(name, body.enabled);
153
153
+
setFlag(name, body.enabled);
139
154
return c.json({ id: name, enabled: body.enabled });
140
155
});
141
156
142
142
-
api.delete("/flags/:name", async (c) => {
157
157
+
api.delete("/flags/:name", (c) => {
143
158
const name = c.req.param("name");
144
159
const definition = getFlagDefinition(name);
145
160
···
147
162
return c.json({ error: "Unknown flag" }, 404);
148
163
}
149
164
150
150
-
await setFlag(name, false);
165
165
+
setFlag(name, false);
151
166
return c.json({ id: name, enabled: false });
152
167
});
153
168