@lamalibre/create-lamaste 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +118 -0
- package/README.md +114 -0
- package/bin/create-lamaste.js +15 -0
- package/package.json +54 -0
- package/scripts/bundle-vendor.js +110 -0
- package/scripts/lamaste-cert-info +62 -0
- package/scripts/lamaste-certbot +96 -0
- package/scripts/lamaste-priv +693 -0
- package/src/index.js +646 -0
- package/src/lib/accounts.js +86 -0
- package/src/lib/binaries.js +137 -0
- package/src/lib/cert-help-page.js +160 -0
- package/src/lib/env.js +132 -0
- package/src/lib/http-redirect.js +38 -0
- package/src/lib/ownership.js +231 -0
- package/src/lib/relay-services.js +103 -0
- package/src/lib/secrets.js +19 -0
- package/src/lib/service-config.js +311 -0
- package/src/lib/summary.js +131 -0
- package/src/lib/wrappers.js +63 -0
- package/src/tasks/gatekeeper.js +233 -0
- package/src/tasks/harden.js +293 -0
- package/src/tasks/mtls.js +193 -0
- package/src/tasks/nginx.js +265 -0
- package/src/tasks/node.js +110 -0
- package/src/tasks/panel.js +408 -0
- package/src/tasks/redeploy.js +438 -0
- package/vendor/docs/00-introduction/how-it-works.md +447 -0
- package/vendor/docs/00-introduction/quickstart.md +472 -0
- package/vendor/docs/00-introduction/what-is-lamaste.md +112 -0
- package/vendor/docs/01-concepts/authentication.md +464 -0
- package/vendor/docs/01-concepts/authorization.md +242 -0
- package/vendor/docs/01-concepts/certificates.md +452 -0
- package/vendor/docs/01-concepts/dns-and-domains.md +343 -0
- package/vendor/docs/01-concepts/mtls.md +490 -0
- package/vendor/docs/01-concepts/nginx-reverse-proxy.md +540 -0
- package/vendor/docs/01-concepts/security-model.md +600 -0
- package/vendor/docs/01-concepts/tickets.md +414 -0
- package/vendor/docs/01-concepts/tunneling.md +367 -0
- package/vendor/docs/02-guides/agent-setup.md +194 -0
- package/vendor/docs/02-guides/certificate-management.md +341 -0
- package/vendor/docs/02-guides/cloud-provisioning.md +406 -0
- package/vendor/docs/02-guides/desktop-app-setup.md +226 -0
- package/vendor/docs/02-guides/disaster-recovery.md +464 -0
- package/vendor/docs/02-guides/first-tunnel.md +230 -0
- package/vendor/docs/02-guides/installation.md +393 -0
- package/vendor/docs/02-guides/managing-users.md +255 -0
- package/vendor/docs/02-guides/onboarding.md +244 -0
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +375 -0
- package/vendor/docs/02-guides/static-sites.md +364 -0
- package/vendor/docs/03-architecture/e2e-three-vm-sequences.md +457 -0
- package/vendor/docs/03-architecture/installer-flow.md +151 -0
- package/vendor/docs/03-architecture/installer.md +445 -0
- package/vendor/docs/03-architecture/management-flow.md +288 -0
- package/vendor/docs/03-architecture/nginx-configuration.md +830 -0
- package/vendor/docs/03-architecture/onboarding-flow.md +164 -0
- package/vendor/docs/03-architecture/overview.md +541 -0
- package/vendor/docs/03-architecture/panel-client.md +416 -0
- package/vendor/docs/03-architecture/panel-server.md +695 -0
- package/vendor/docs/03-architecture/state-management.md +529 -0
- package/vendor/docs/03-architecture/system-overview.md +167 -0
- package/vendor/docs/04-api-reference/certificates.md +1141 -0
- package/vendor/docs/04-api-reference/gatekeeper.md +592 -0
- package/vendor/docs/04-api-reference/onboarding.md +473 -0
- package/vendor/docs/04-api-reference/overview.md +404 -0
- package/vendor/docs/04-api-reference/services.md +267 -0
- package/vendor/docs/04-api-reference/settings.md +343 -0
- package/vendor/docs/04-api-reference/sites.md +716 -0
- package/vendor/docs/04-api-reference/system.md +268 -0
- package/vendor/docs/04-api-reference/tickets.md +618 -0
- package/vendor/docs/04-api-reference/tunnels.md +417 -0
- package/vendor/docs/04-api-reference/users.md +333 -0
- package/vendor/docs/05-operations/backup-and-restore.md +342 -0
- package/vendor/docs/05-operations/monitoring.md +285 -0
- package/vendor/docs/05-operations/uninstalling.md +344 -0
- package/vendor/docs/05-operations/upgrades.md +270 -0
- package/vendor/docs/06-reference/config-files.md +1080 -0
- package/vendor/docs/06-reference/glossary.md +528 -0
- package/vendor/docs/06-reference/installer-flags.md +260 -0
- package/vendor/docs/06-reference/ports-and-services.md +244 -0
- package/vendor/docs/06-reference/troubleshooting.md +665 -0
- package/vendor/docs/_index.json +233 -0
- package/vendor/docs/gatekeeper-design.md +953 -0
- package/vendor/docs/package.json +15 -0
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +641 -0
- package/vendor/gatekeeper/dist/boot.d.ts +1 -0
- package/vendor/gatekeeper/dist/boot.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/boot.js +17 -0
- package/vendor/gatekeeper/dist/boot.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/access.d.ts +2 -0
- package/vendor/gatekeeper/dist/cli/commands/access.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/access.js +33 -0
- package/vendor/gatekeeper/dist/cli/commands/access.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.d.ts +2 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.js +110 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.d.ts +2 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.js +98 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/index.d.ts +3 -0
- package/vendor/gatekeeper/dist/cli/index.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/index.js +45 -0
- package/vendor/gatekeeper/dist/cli/index.js.map +1 -0
- package/vendor/gatekeeper/dist/index.d.ts +9 -0
- package/vendor/gatekeeper/dist/index.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/index.js +13 -0
- package/vendor/gatekeeper/dist/index.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/authz.d.ts +16 -0
- package/vendor/gatekeeper/dist/lib/authz.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/authz.js +42 -0
- package/vendor/gatekeeper/dist/lib/authz.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.d.ts +15 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.js +19 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/constants.d.ts +82 -0
- package/vendor/gatekeeper/dist/lib/constants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/constants.js +79 -0
- package/vendor/gatekeeper/dist/lib/constants.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/grants.d.ts +32 -0
- package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/grants.js +314 -0
- package/vendor/gatekeeper/dist/lib/grants.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/groups.d.ts +10 -0
- package/vendor/gatekeeper/dist/lib/groups.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/groups.js +247 -0
- package/vendor/gatekeeper/dist/lib/groups.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/migration.d.ts +17 -0
- package/vendor/gatekeeper/dist/lib/migration.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/migration.js +79 -0
- package/vendor/gatekeeper/dist/lib/migration.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0001_groups.sql +7 -0
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0002_access_grants.sql +14 -0
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0003_access_request_log.sql +9 -0
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0001_groups.sql +7 -0
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0002_access_grants.sql +14 -0
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0003_access_request_log.sql +9 -0
- package/vendor/gatekeeper/dist/lib/state-db.d.ts +16 -0
- package/vendor/gatekeeper/dist/lib/state-db.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/state-db.js +154 -0
- package/vendor/gatekeeper/dist/lib/state-db.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts +21 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/templates.js +151 -0
- package/vendor/gatekeeper/dist/lib/templates.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts +178 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/types.js +2 -0
- package/vendor/gatekeeper/dist/lib/types.js.map +1 -0
- package/vendor/gatekeeper/dist/server/index.d.ts +15 -0
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/index.js +251 -0
- package/vendor/gatekeeper/dist/server/index.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.d.ts +7 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.js +17 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/authz.js +279 -0
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts +13 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js +249 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/grants.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/grants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/grants.js +112 -0
- package/vendor/gatekeeper/dist/server/routes/grants.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/groups.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/groups.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/groups.js +139 -0
- package/vendor/gatekeeper/dist/server/routes/groups.js.map +1 -0
- package/vendor/gatekeeper/package.json +49 -0
- package/vendor/server/bin/lamaste-server.js +204 -0
- package/vendor/server/package.json +31 -0
- package/vendor/server/src/commands/certs.js +224 -0
- package/vendor/server/src/commands/chisel.js +132 -0
- package/vendor/server/src/commands/logs.js +49 -0
- package/vendor/server/src/commands/plugins.js +321 -0
- package/vendor/server/src/commands/reset-admin.js +478 -0
- package/vendor/server/src/commands/restart.js +94 -0
- package/vendor/server/src/commands/sites.js +257 -0
- package/vendor/server/src/commands/status.js +158 -0
- package/vendor/server/src/commands/tunnels.js +365 -0
- package/vendor/server/src/commands/uninstall.js +326 -0
- package/vendor/server/src/config.js +38 -0
- package/vendor/server/src/exec.js +26 -0
- package/vendor/server/src/logger.js +39 -0
- package/vendor/server/src/ndjson.js +56 -0
- package/vendor/server/src/panel-api.js +85 -0
- package/vendor/server/src/state.js +45 -0
- package/vendor/server-ui/dist/assets/index-B0nWuUyL.js +257 -0
- package/vendor/server-ui/dist/assets/index-BqWWFlTU.css +1 -0
- package/vendor/server-ui/dist/index.html +13 -0
- package/vendor/serverd/package.json +44 -0
- package/vendor/serverd/src/boot.js +13 -0
- package/vendor/serverd/src/index.js +191 -0
- package/vendor/serverd/src/lib/app-error.js +14 -0
- package/vendor/serverd/src/lib/authelia.js +85 -0
- package/vendor/serverd/src/lib/certbot.js +74 -0
- package/vendor/serverd/src/lib/chisel-args.js +6 -0
- package/vendor/serverd/src/lib/chisel-reconcile.js +232 -0
- package/vendor/serverd/src/lib/chisel-runtime.js +46 -0
- package/vendor/serverd/src/lib/chisel-users.js +109 -0
- package/vendor/serverd/src/lib/chisel.js +81 -0
- package/vendor/serverd/src/lib/config.js +190 -0
- package/vendor/serverd/src/lib/constants.js +8 -0
- package/vendor/serverd/src/lib/csr-signing.js +331 -0
- package/vendor/serverd/src/lib/enrollment.js +302 -0
- package/vendor/serverd/src/lib/files.js +19 -0
- package/vendor/serverd/src/lib/gatekeeper-client.js +135 -0
- package/vendor/serverd/src/lib/invite-page.js +281 -0
- package/vendor/serverd/src/lib/ip-utils.js +79 -0
- package/vendor/serverd/src/lib/migrations/state/0001_enrollment_tokens.sql +17 -0
- package/vendor/serverd/src/lib/migrations/state/0002_tickets.sql +70 -0
- package/vendor/serverd/src/lib/migrations/state/0003_plugins.sql +15 -0
- package/vendor/serverd/src/lib/migrations/state/0004_agents.sql +19 -0
- package/vendor/serverd/src/lib/mtls.js +795 -0
- package/vendor/serverd/src/lib/nginx.js +1090 -0
- package/vendor/serverd/src/lib/pki-paths.js +41 -0
- package/vendor/serverd/src/lib/pki-sign.js +103 -0
- package/vendor/serverd/src/lib/plugins.js +538 -0
- package/vendor/serverd/src/lib/push-install.js +390 -0
- package/vendor/serverd/src/lib/revocation.js +112 -0
- package/vendor/serverd/src/lib/services.js +131 -0
- package/vendor/serverd/src/lib/session.js +151 -0
- package/vendor/serverd/src/lib/state-db.js +130 -0
- package/vendor/serverd/src/lib/state.js +140 -0
- package/vendor/serverd/src/lib/storage.js +63 -0
- package/vendor/serverd/src/lib/system-stats.js +58 -0
- package/vendor/serverd/src/lib/tickets.js +1556 -0
- package/vendor/serverd/src/lib/totp.js +76 -0
- package/vendor/serverd/src/lib/tunnel-deps.js +41 -0
- package/vendor/serverd/src/lib/user-access-session.js +142 -0
- package/vendor/serverd/src/lib/user-access.js +52 -0
- package/vendor/serverd/src/middleware/errors.js +63 -0
- package/vendor/serverd/src/middleware/mtls.js +120 -0
- package/vendor/serverd/src/middleware/onboarding-guard.js +30 -0
- package/vendor/serverd/src/middleware/role-guard.js +58 -0
- package/vendor/serverd/src/middleware/twofa-session.js +65 -0
- package/vendor/serverd/src/middleware/user-access-session.js +52 -0
- package/vendor/serverd/src/routes/enrollment.js +120 -0
- package/vendor/serverd/src/routes/health.js +22 -0
- package/vendor/serverd/src/routes/invite.js +163 -0
- package/vendor/serverd/src/routes/management/agents.js +182 -0
- package/vendor/serverd/src/routes/management/certs.js +911 -0
- package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +160 -0
- package/vendor/serverd/src/routes/management/identity.js +107 -0
- package/vendor/serverd/src/routes/management/invitations.js +166 -0
- package/vendor/serverd/src/routes/management/logs.js +218 -0
- package/vendor/serverd/src/routes/management/plugins.js +560 -0
- package/vendor/serverd/src/routes/management/services.js +63 -0
- package/vendor/serverd/src/routes/management/settings.js +325 -0
- package/vendor/serverd/src/routes/management/sites.js +522 -0
- package/vendor/serverd/src/routes/management/storage.js +192 -0
- package/vendor/serverd/src/routes/management/system.js +201 -0
- package/vendor/serverd/src/routes/management/tickets.js +457 -0
- package/vendor/serverd/src/routes/management/tunnels.js +709 -0
- package/vendor/serverd/src/routes/management/users.js +377 -0
- package/vendor/serverd/src/routes/management.js +36 -0
- package/vendor/serverd/src/routes/onboarding/dns.js +101 -0
- package/vendor/serverd/src/routes/onboarding/domain.js +42 -0
- package/vendor/serverd/src/routes/onboarding/index.js +18 -0
- package/vendor/serverd/src/routes/onboarding/provision.js +327 -0
- package/vendor/serverd/src/routes/onboarding/status.js +12 -0
- package/vendor/serverd/src/routes/plugin-router.js +79 -0
- package/vendor/serverd/src/routes/user-access.js +389 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy routes for Gatekeeper API.
|
|
3
|
+
*
|
|
4
|
+
* These routes forward requests from lamaste-serverd's mTLS-protected
|
|
5
|
+
* /api/gatekeeper/* path to the Gatekeeper service on 127.0.0.1:9294.
|
|
6
|
+
* This allows the desktop app (and web panel) to reach Gatekeeper on
|
|
7
|
+
* remote servers through the existing mTLS tunnel — no additional port
|
|
8
|
+
* forwarding needed.
|
|
9
|
+
*
|
|
10
|
+
* Admin-only (lamaste-serverd's mTLS + roleGuard protect these routes).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { gatekeeperRequest } from '../../lib/gatekeeper-client.js';
|
|
14
|
+
|
|
15
|
+
export default async function gatekeeperProxyRoutes(fastify, _opts) {
|
|
16
|
+
// All gatekeeper management routes are admin-only
|
|
17
|
+
fastify.addHook('preHandler', fastify.requireRole(['admin']));
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Forward to gatekeeper via the shared client (which reads the secret
|
|
21
|
+
* from either `gatekeeper-secret.json` or the legacy plain-text file).
|
|
22
|
+
* Normalises the response to the proxy's historical shape.
|
|
23
|
+
*/
|
|
24
|
+
const proxy = async (method, gatekeeperPath, body) => {
|
|
25
|
+
const result = await gatekeeperRequest(method, gatekeeperPath, body);
|
|
26
|
+
if (result.ok) {
|
|
27
|
+
return { statusCode: result.statusCode, data: result.data };
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
statusCode: result.statusCode,
|
|
31
|
+
data: { error: result.error },
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// --- Groups ---
|
|
36
|
+
|
|
37
|
+
fastify.get('/gatekeeper/groups', async (_request, reply) => {
|
|
38
|
+
const { statusCode, data } = await proxy('GET', '/api/groups');
|
|
39
|
+
return reply.code(statusCode).send(data);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
fastify.post('/gatekeeper/groups', async (request, reply) => {
|
|
43
|
+
const { statusCode, data } = await proxy('POST', '/api/groups', request.body);
|
|
44
|
+
return reply.code(statusCode).send(data);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
fastify.get('/gatekeeper/groups/:name', async (request, reply) => {
|
|
48
|
+
const { name } = request.params;
|
|
49
|
+
const { statusCode, data } = await proxy('GET', `/api/groups/${encodeURIComponent(name)}`);
|
|
50
|
+
return reply.code(statusCode).send(data);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
fastify.patch('/gatekeeper/groups/:name', async (request, reply) => {
|
|
54
|
+
const { name } = request.params;
|
|
55
|
+
const { statusCode, data } = await proxy(
|
|
56
|
+
'PATCH',
|
|
57
|
+
`/api/groups/${encodeURIComponent(name)}`,
|
|
58
|
+
request.body,
|
|
59
|
+
);
|
|
60
|
+
return reply.code(statusCode).send(data);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
fastify.delete('/gatekeeper/groups/:name', async (request, reply) => {
|
|
64
|
+
const { name } = request.params;
|
|
65
|
+
const { statusCode, data } = await proxy('DELETE', `/api/groups/${encodeURIComponent(name)}`);
|
|
66
|
+
return reply.code(statusCode).send(data);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
fastify.post('/gatekeeper/groups/:name/members', async (request, reply) => {
|
|
70
|
+
const { name } = request.params;
|
|
71
|
+
const { statusCode, data } = await proxy(
|
|
72
|
+
'POST',
|
|
73
|
+
`/api/groups/${encodeURIComponent(name)}/members`,
|
|
74
|
+
request.body,
|
|
75
|
+
);
|
|
76
|
+
return reply.code(statusCode).send(data);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
fastify.delete('/gatekeeper/groups/:name/members/:username', async (request, reply) => {
|
|
80
|
+
const { name, username } = request.params;
|
|
81
|
+
const { statusCode, data } = await proxy(
|
|
82
|
+
'DELETE',
|
|
83
|
+
`/api/groups/${encodeURIComponent(name)}/members/${encodeURIComponent(username)}`,
|
|
84
|
+
);
|
|
85
|
+
return reply.code(statusCode).send(data);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// --- Grants ---
|
|
89
|
+
|
|
90
|
+
fastify.get('/gatekeeper/grants', async (request, reply) => {
|
|
91
|
+
const qs = new URLSearchParams(request.query).toString();
|
|
92
|
+
const grantPath = qs ? `/api/grants?${qs}` : '/api/grants';
|
|
93
|
+
const { statusCode, data } = await proxy('GET', grantPath);
|
|
94
|
+
return reply.code(statusCode).send(data);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
fastify.post('/gatekeeper/grants', async (request, reply) => {
|
|
98
|
+
const { statusCode, data } = await proxy('POST', '/api/grants', request.body);
|
|
99
|
+
return reply.code(statusCode).send(data);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
fastify.get('/gatekeeper/grants/:grantId', async (request, reply) => {
|
|
103
|
+
const { grantId } = request.params;
|
|
104
|
+
const { statusCode, data } = await proxy('GET', `/api/grants/${encodeURIComponent(grantId)}`);
|
|
105
|
+
return reply.code(statusCode).send(data);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
fastify.delete('/gatekeeper/grants/:grantId', async (request, reply) => {
|
|
109
|
+
const { grantId } = request.params;
|
|
110
|
+
const { statusCode, data } = await proxy(
|
|
111
|
+
'DELETE',
|
|
112
|
+
`/api/grants/${encodeURIComponent(grantId)}`,
|
|
113
|
+
);
|
|
114
|
+
return reply.code(statusCode).send(data);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Bulk-revoke every grant for a principal. Forwarded to gatekeeper as
|
|
118
|
+
// DELETE /api/grants?principalType=...&principalId=...
|
|
119
|
+
fastify.delete('/gatekeeper/grants', async (request, reply) => {
|
|
120
|
+
const qs = new URLSearchParams(request.query).toString();
|
|
121
|
+
const grantPath = qs ? `/api/grants?${qs}` : '/api/grants';
|
|
122
|
+
const { statusCode, data } = await proxy('DELETE', grantPath);
|
|
123
|
+
return reply.code(statusCode).send(data);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// --- Diagnostics / Settings / Cache ---
|
|
127
|
+
|
|
128
|
+
fastify.get('/gatekeeper/access/check', async (request, reply) => {
|
|
129
|
+
const qs = new URLSearchParams(request.query).toString();
|
|
130
|
+
const { statusCode, data } = await proxy('GET', `/api/access/check?${qs}`);
|
|
131
|
+
return reply.code(statusCode).send(data);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
fastify.post('/gatekeeper/cache/bust', async (_request, reply) => {
|
|
135
|
+
const { statusCode, data } = await proxy('POST', '/api/cache/bust');
|
|
136
|
+
return reply.code(statusCode).send(data);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
fastify.get('/gatekeeper/settings', async (_request, reply) => {
|
|
140
|
+
const { statusCode, data } = await proxy('GET', '/api/settings');
|
|
141
|
+
return reply.code(statusCode).send(data);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
fastify.patch('/gatekeeper/settings', async (request, reply) => {
|
|
145
|
+
const { statusCode, data } = await proxy('PATCH', '/api/settings', request.body);
|
|
146
|
+
return reply.code(statusCode).send(data);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
fastify.get('/gatekeeper/access-log', async (request, reply) => {
|
|
150
|
+
const qs = new URLSearchParams(request.query).toString();
|
|
151
|
+
const logPath = qs ? `/api/access-log?${qs}` : '/api/access-log';
|
|
152
|
+
const { statusCode, data } = await proxy('GET', logPath);
|
|
153
|
+
return reply.code(statusCode).send(data);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
fastify.delete('/gatekeeper/access-log', async (_request, reply) => {
|
|
157
|
+
const { statusCode, data } = await proxy('DELETE', '/api/access-log');
|
|
158
|
+
return reply.code(statusCode).send(data);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { readUsers } from '../../lib/authelia.js';
|
|
3
|
+
|
|
4
|
+
const UsernameParamSchema = z.object({
|
|
5
|
+
username: z
|
|
6
|
+
.string()
|
|
7
|
+
.min(1)
|
|
8
|
+
.max(100)
|
|
9
|
+
.regex(/^[a-zA-Z0-9_.-]+$/),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extract a single string value from a header that may be string, string[], or undefined.
|
|
14
|
+
*
|
|
15
|
+
* @param {string | string[] | undefined} value
|
|
16
|
+
* @returns {string | undefined}
|
|
17
|
+
*/
|
|
18
|
+
function headerValue(value) {
|
|
19
|
+
if (Array.isArray(value)) return value[0];
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default async function identityRoutes(fastify, _opts) {
|
|
24
|
+
// GET /api/identity/self — Returns caller's identity from Authelia headers.
|
|
25
|
+
// Only meaningful behind an Authelia-protected vhost. On the mTLS panel vhost,
|
|
26
|
+
// nginx strips Remote-* headers so this will return 400.
|
|
27
|
+
fastify.get(
|
|
28
|
+
'/identity/self',
|
|
29
|
+
{ preHandler: fastify.requireRole(['admin']) },
|
|
30
|
+
async (request, reply) => {
|
|
31
|
+
const user = headerValue(request.headers['remote-user']);
|
|
32
|
+
if (!user) {
|
|
33
|
+
return reply.code(400).send({
|
|
34
|
+
error:
|
|
35
|
+
'Identity headers not present — this endpoint requires an Authelia-protected vhost',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const groupsRaw = headerValue(request.headers['remote-groups']) || '';
|
|
40
|
+
const groups = groupsRaw
|
|
41
|
+
.split(',')
|
|
42
|
+
.map((g) => g.trim())
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
const name = headerValue(request.headers['remote-name']) || '';
|
|
45
|
+
const email = headerValue(request.headers['remote-email']) || '';
|
|
46
|
+
|
|
47
|
+
return { username: user, displayName: name, email, groups };
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// GET /api/identity/users — List Authelia users
|
|
52
|
+
fastify.get(
|
|
53
|
+
'/identity/users',
|
|
54
|
+
{ preHandler: fastify.requireRole(['admin', 'agent'], { capability: 'identity:query' }) },
|
|
55
|
+
async (request, reply) => {
|
|
56
|
+
try {
|
|
57
|
+
const users = await readUsers();
|
|
58
|
+
return { users };
|
|
59
|
+
} catch (err) {
|
|
60
|
+
request.log.error({ err }, 'Failed to read Authelia users');
|
|
61
|
+
return reply.code(500).send({ error: 'Failed to read users' });
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// GET /api/identity/users/:username — Single user metadata
|
|
67
|
+
fastify.get(
|
|
68
|
+
'/identity/users/:username',
|
|
69
|
+
{ preHandler: fastify.requireRole(['admin', 'agent'], { capability: 'identity:query' }) },
|
|
70
|
+
async (request, reply) => {
|
|
71
|
+
const { username } = UsernameParamSchema.parse(request.params);
|
|
72
|
+
try {
|
|
73
|
+
const users = await readUsers();
|
|
74
|
+
const user = users.find((u) => u.username === username);
|
|
75
|
+
if (!user) {
|
|
76
|
+
return reply.code(404).send({ error: 'User not found' });
|
|
77
|
+
}
|
|
78
|
+
return { user };
|
|
79
|
+
} catch (err) {
|
|
80
|
+
request.log.error({ err }, 'Failed to read Authelia users');
|
|
81
|
+
return reply.code(500).send({ error: 'Failed to read users' });
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// GET /api/identity/groups — List all groups
|
|
87
|
+
fastify.get(
|
|
88
|
+
'/identity/groups',
|
|
89
|
+
{ preHandler: fastify.requireRole(['admin', 'agent'], { capability: 'identity:query' }) },
|
|
90
|
+
async (request, reply) => {
|
|
91
|
+
try {
|
|
92
|
+
const users = await readUsers();
|
|
93
|
+
/** @type {Set<string>} */
|
|
94
|
+
const groupSet = new Set();
|
|
95
|
+
for (const u of users) {
|
|
96
|
+
for (const g of u.groups) {
|
|
97
|
+
groupSet.add(g);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { groups: [...groupSet].sort() };
|
|
101
|
+
} catch (err) {
|
|
102
|
+
request.log.error({ err }, 'Failed to read Authelia users');
|
|
103
|
+
return reply.code(500).send({ error: 'Failed to read groups' });
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { readInvitations, writeInvitations } from '../../lib/state.js';
|
|
4
|
+
import { readUsersRaw } from '../../lib/authelia.js';
|
|
5
|
+
import { getConfig } from '../../lib/config.js';
|
|
6
|
+
|
|
7
|
+
const IdParamSchema = z.object({ id: z.string().uuid() });
|
|
8
|
+
|
|
9
|
+
const CreateInvitationSchema = z.object({
|
|
10
|
+
username: z
|
|
11
|
+
.string()
|
|
12
|
+
.min(2)
|
|
13
|
+
.max(32)
|
|
14
|
+
.regex(
|
|
15
|
+
// Same rule as the users API. A leading "-" would read as a flag to
|
|
16
|
+
// Authelia's CLI when a TOTP secret is stored for the user.
|
|
17
|
+
/^[a-z0-9][a-z0-9_-]*$/,
|
|
18
|
+
'Username must start with a lowercase letter or digit and may contain underscores or hyphens',
|
|
19
|
+
),
|
|
20
|
+
email: z.string().email(),
|
|
21
|
+
groups: z.array(z.string()).optional().default([]),
|
|
22
|
+
expiresInDays: z.number().int().min(1).max(30).optional().default(7),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export default async function invitationRoutes(fastify, _opts) {
|
|
26
|
+
// GET /api/invitations — list all invitations
|
|
27
|
+
fastify.get(
|
|
28
|
+
'/invitations',
|
|
29
|
+
{
|
|
30
|
+
preHandler: fastify.requireRole(['admin']),
|
|
31
|
+
},
|
|
32
|
+
async (request, reply) => {
|
|
33
|
+
try {
|
|
34
|
+
const invitations = await readInvitations();
|
|
35
|
+
const now = new Date();
|
|
36
|
+
const enriched = invitations.map((inv) => ({
|
|
37
|
+
...inv,
|
|
38
|
+
token: undefined,
|
|
39
|
+
status: inv.used ? 'accepted' : new Date(inv.expiresAt) < now ? 'expired' : 'pending',
|
|
40
|
+
}));
|
|
41
|
+
return { invitations: enriched };
|
|
42
|
+
} catch (err) {
|
|
43
|
+
request.log.error(err, 'Failed to read invitations');
|
|
44
|
+
return reply.code(500).send({ error: 'Failed to read invitations' });
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// POST /api/invitations — create a new invitation
|
|
50
|
+
fastify.post(
|
|
51
|
+
'/invitations',
|
|
52
|
+
{
|
|
53
|
+
preHandler: fastify.requireRole(['admin']),
|
|
54
|
+
},
|
|
55
|
+
async (request, reply) => {
|
|
56
|
+
const body = CreateInvitationSchema.parse(request.body);
|
|
57
|
+
|
|
58
|
+
// Check username doesn't already exist in Authelia
|
|
59
|
+
let usersData;
|
|
60
|
+
try {
|
|
61
|
+
usersData = await readUsersRaw();
|
|
62
|
+
} catch {
|
|
63
|
+
usersData = { users: {} };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (usersData.users[body.username]) {
|
|
67
|
+
return reply.code(409).send({ error: 'Username already exists as an Authelia user' });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check username doesn't already have a pending invitation
|
|
71
|
+
let invitations;
|
|
72
|
+
try {
|
|
73
|
+
invitations = await readInvitations();
|
|
74
|
+
} catch (err) {
|
|
75
|
+
request.log.error(err, 'Failed to read invitations');
|
|
76
|
+
return reply.code(500).send({ error: 'Failed to read invitations' });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const now = new Date();
|
|
80
|
+
const existingPending = invitations.find(
|
|
81
|
+
(inv) => inv.username === body.username && !inv.used && new Date(inv.expiresAt) > now,
|
|
82
|
+
);
|
|
83
|
+
if (existingPending) {
|
|
84
|
+
return reply
|
|
85
|
+
.code(409)
|
|
86
|
+
.send({ error: 'A pending invitation already exists for this username' });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const config = getConfig();
|
|
90
|
+
const token = crypto.randomBytes(32).toString('hex');
|
|
91
|
+
const expiresAt = new Date(now.getTime() + body.expiresInDays * 24 * 60 * 60 * 1000);
|
|
92
|
+
|
|
93
|
+
const invitation = {
|
|
94
|
+
id: crypto.randomUUID(),
|
|
95
|
+
token,
|
|
96
|
+
username: body.username,
|
|
97
|
+
email: body.email,
|
|
98
|
+
groups: body.groups,
|
|
99
|
+
createdAt: now.toISOString(),
|
|
100
|
+
expiresAt: expiresAt.toISOString(),
|
|
101
|
+
used: false,
|
|
102
|
+
acceptedAt: null,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
invitations.push(invitation);
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
await writeInvitations(invitations);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
request.log.error(err, 'Failed to write invitations');
|
|
111
|
+
return reply.code(500).send({ error: 'Failed to save invitation' });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const inviteUrl = config.domain ? `https://auth.${config.domain}/invite/${token}` : null;
|
|
115
|
+
|
|
116
|
+
return reply.code(201).send({
|
|
117
|
+
ok: true,
|
|
118
|
+
invitation: {
|
|
119
|
+
id: invitation.id,
|
|
120
|
+
username: invitation.username,
|
|
121
|
+
email: invitation.email,
|
|
122
|
+
groups: invitation.groups,
|
|
123
|
+
createdAt: invitation.createdAt,
|
|
124
|
+
expiresAt: invitation.expiresAt,
|
|
125
|
+
},
|
|
126
|
+
inviteUrl,
|
|
127
|
+
token,
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// DELETE /api/invitations/:id — revoke an invitation
|
|
133
|
+
fastify.delete(
|
|
134
|
+
'/invitations/:id',
|
|
135
|
+
{
|
|
136
|
+
preHandler: fastify.requireRole(['admin']),
|
|
137
|
+
},
|
|
138
|
+
async (request, reply) => {
|
|
139
|
+
const { id } = IdParamSchema.parse(request.params);
|
|
140
|
+
|
|
141
|
+
let invitations;
|
|
142
|
+
try {
|
|
143
|
+
invitations = await readInvitations();
|
|
144
|
+
} catch (err) {
|
|
145
|
+
request.log.error(err, 'Failed to read invitations');
|
|
146
|
+
return reply.code(500).send({ error: 'Failed to read invitations' });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const index = invitations.findIndex((inv) => inv.id === id);
|
|
150
|
+
if (index === -1) {
|
|
151
|
+
return reply.code(404).send({ error: 'Invitation not found' });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
invitations.splice(index, 1);
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
await writeInvitations(invitations);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
request.log.error(err, 'Failed to write invitations');
|
|
160
|
+
return reply.code(500).send({ error: 'Failed to revoke invitation' });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { ok: true };
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { ALLOWED_SERVICES } from '../../lib/services.js';
|
|
3
|
+
|
|
4
|
+
// Track active journalctl processes for cleanup on server shutdown
|
|
5
|
+
const activeProcesses = new Set();
|
|
6
|
+
|
|
7
|
+
// Backpressure: max bytes buffered before we pause the child stdout stream
|
|
8
|
+
const MAX_SEND_BUFFER_BYTES = 64 * 1024; // 64 KB
|
|
9
|
+
|
|
10
|
+
export default async function logsRoutes(fastify, _opts) {
|
|
11
|
+
fastify.get(
|
|
12
|
+
'/services/:name/logs',
|
|
13
|
+
{
|
|
14
|
+
websocket: true,
|
|
15
|
+
preHandler: fastify.requireRole(['admin']),
|
|
16
|
+
// The rate-limit only applies to the HTTP upgrade handshake — once
|
|
17
|
+
// the WebSocket is established it streams freely. Empty config
|
|
18
|
+
// inherits the globally-registered limiter (100 connection attempts
|
|
19
|
+
// per minute) and satisfies CodeQL.
|
|
20
|
+
config: { rateLimit: {} },
|
|
21
|
+
},
|
|
22
|
+
(socket, request) => {
|
|
23
|
+
const { name } = request.params;
|
|
24
|
+
|
|
25
|
+
// Validate service name
|
|
26
|
+
if (!ALLOWED_SERVICES.includes(name)) {
|
|
27
|
+
socket.close(1008, 'Unknown service');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let child = null;
|
|
32
|
+
let lineBuffer = '';
|
|
33
|
+
let sendBuffer = [];
|
|
34
|
+
let sendBufferBytes = 0;
|
|
35
|
+
let stdoutPaused = false;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
child = execa('journalctl', ['-u', name, '-f', '-n', '100', '--output=short-iso'], {
|
|
39
|
+
reject: false,
|
|
40
|
+
buffer: false,
|
|
41
|
+
stdout: 'pipe',
|
|
42
|
+
stderr: 'pipe',
|
|
43
|
+
});
|
|
44
|
+
} catch (err) {
|
|
45
|
+
request.log.error({ err }, 'Failed to spawn journalctl');
|
|
46
|
+
socket.send(
|
|
47
|
+
JSON.stringify({
|
|
48
|
+
timestamp: new Date().toISOString(),
|
|
49
|
+
message: `[Error: Failed to start log stream for ${name}]`,
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
socket.close(1011, 'Failed to spawn journalctl');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
activeProcesses.add(child);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Attempt to flush buffered messages to the WebSocket.
|
|
60
|
+
* Drops oldest messages if buffer exceeds the size limit.
|
|
61
|
+
* Pauses/resumes child stdout based on buffer pressure.
|
|
62
|
+
*/
|
|
63
|
+
function flushSendBuffer() {
|
|
64
|
+
while (sendBuffer.length > 0) {
|
|
65
|
+
if (socket.readyState !== 1 /* OPEN */) {
|
|
66
|
+
// Socket not open — discard everything
|
|
67
|
+
sendBuffer.length = 0;
|
|
68
|
+
sendBufferBytes = 0;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const msg = sendBuffer[0];
|
|
73
|
+
try {
|
|
74
|
+
socket.send(msg);
|
|
75
|
+
sendBufferBytes -= msg.length;
|
|
76
|
+
sendBuffer.shift();
|
|
77
|
+
} catch {
|
|
78
|
+
// Socket may have closed mid-send
|
|
79
|
+
sendBuffer.length = 0;
|
|
80
|
+
sendBufferBytes = 0;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Buffer drained — resume child stdout if it was paused
|
|
86
|
+
if (stdoutPaused && child?.stdout) {
|
|
87
|
+
stdoutPaused = false;
|
|
88
|
+
child.stdout.resume();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Enqueue a serialized message for sending.
|
|
94
|
+
* Enforces the buffer size limit by dropping oldest messages.
|
|
95
|
+
*/
|
|
96
|
+
function enqueueMessage(serialized) {
|
|
97
|
+
sendBuffer.push(serialized);
|
|
98
|
+
sendBufferBytes += serialized.length;
|
|
99
|
+
|
|
100
|
+
// Drop oldest messages while buffer exceeds the limit
|
|
101
|
+
while (sendBufferBytes > MAX_SEND_BUFFER_BYTES && sendBuffer.length > 1) {
|
|
102
|
+
const dropped = sendBuffer.shift();
|
|
103
|
+
sendBufferBytes -= dropped.length;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Pause child stdout when buffer is full to apply backpressure
|
|
107
|
+
if (sendBufferBytes > MAX_SEND_BUFFER_BYTES && !stdoutPaused && child?.stdout) {
|
|
108
|
+
stdoutPaused = true;
|
|
109
|
+
child.stdout.pause();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Handle WebSocket drain — buffer has been flushed by the network layer
|
|
114
|
+
socket.on('drain', () => {
|
|
115
|
+
flushSendBuffer();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Parse and send lines from journalctl stdout
|
|
119
|
+
if (child.stdout) {
|
|
120
|
+
child.stdout.on('data', (chunk) => {
|
|
121
|
+
lineBuffer += chunk.toString();
|
|
122
|
+
const lines = lineBuffer.split('\n');
|
|
123
|
+
// Keep the last incomplete line in the buffer
|
|
124
|
+
lineBuffer = lines.pop() || '';
|
|
125
|
+
|
|
126
|
+
for (const line of lines) {
|
|
127
|
+
if (!line.trim()) continue;
|
|
128
|
+
const parsed = parseLine(line);
|
|
129
|
+
const serialized = JSON.stringify(parsed);
|
|
130
|
+
|
|
131
|
+
if (socket.readyState === 1 /* OPEN */ && sendBuffer.length === 0) {
|
|
132
|
+
// Fast path: send directly when buffer is empty and socket is open
|
|
133
|
+
try {
|
|
134
|
+
socket.send(serialized);
|
|
135
|
+
} catch {
|
|
136
|
+
// Socket closed — will be cleaned up by close event
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
enqueueMessage(serialized);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Try to flush any queued messages
|
|
144
|
+
if (sendBuffer.length > 0) {
|
|
145
|
+
flushSendBuffer();
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Handle journalctl exit
|
|
151
|
+
child.then(() => {
|
|
152
|
+
activeProcesses.delete(child);
|
|
153
|
+
try {
|
|
154
|
+
if (socket.readyState === 1 /* OPEN */) {
|
|
155
|
+
socket.send(
|
|
156
|
+
JSON.stringify({
|
|
157
|
+
timestamp: new Date().toISOString(),
|
|
158
|
+
message: '[Log stream ended]',
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
socket.close(1000, 'Log stream ended');
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
// Socket may already be closed
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Clean up on WebSocket close
|
|
169
|
+
socket.on('close', () => {
|
|
170
|
+
sendBuffer.length = 0;
|
|
171
|
+
sendBufferBytes = 0;
|
|
172
|
+
if (child) {
|
|
173
|
+
activeProcesses.delete(child);
|
|
174
|
+
child.kill('SIGTERM');
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Clean up on WebSocket error
|
|
179
|
+
socket.on('error', (err) => {
|
|
180
|
+
request.log.error({ err }, 'WebSocket error for log stream');
|
|
181
|
+
sendBuffer.length = 0;
|
|
182
|
+
sendBufferBytes = 0;
|
|
183
|
+
if (child) {
|
|
184
|
+
activeProcesses.delete(child);
|
|
185
|
+
child.kill('SIGTERM');
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// Clean up all journalctl processes on server shutdown
|
|
192
|
+
fastify.addHook('onClose', async () => {
|
|
193
|
+
for (const proc of activeProcesses) {
|
|
194
|
+
proc.kill('SIGTERM');
|
|
195
|
+
}
|
|
196
|
+
activeProcesses.clear();
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Parse a journalctl --output=short-iso line into { timestamp, message }.
|
|
202
|
+
* Format: 2024-03-15T10:30:45+0000 hostname servicename[pid]: log message
|
|
203
|
+
*/
|
|
204
|
+
function parseLine(line) {
|
|
205
|
+
// ISO timestamp is at the beginning, match until first space after the ISO pattern
|
|
206
|
+
const isoMatch = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[^\s]*)\s+(.*)$/);
|
|
207
|
+
if (isoMatch) {
|
|
208
|
+
return {
|
|
209
|
+
timestamp: isoMatch[1],
|
|
210
|
+
message: isoMatch[2],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
timestamp: '',
|
|
216
|
+
message: line,
|
|
217
|
+
};
|
|
218
|
+
}
|