@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,693 @@
|
|
|
1
|
+
#!/usr/bin/node
|
|
2
|
+
/**
|
|
3
|
+
* lamaste-priv — the root half of the Lamaste panel.
|
|
4
|
+
*
|
|
5
|
+
* The panel runs as the unprivileged `lamaste` user. Everything it must do as
|
|
6
|
+
* root that is not a fixed command line goes through this one program, which
|
|
7
|
+
* sudoers allows it to run (`/etc/sudoers.d/lamaste`). Each operation takes a
|
|
8
|
+
* fixed, validated set of arguments; content (vhost text, TOTP secrets) comes
|
|
9
|
+
* on stdin, never on the command line, so it is not written to the sudo log.
|
|
10
|
+
*
|
|
11
|
+
* nginx-site write <name> stdin: vhost text (validated, see below)
|
|
12
|
+
* nginx-site backup <name> copy <name> to <name>.bak
|
|
13
|
+
* nginx-site restore <name> move <name>.bak back to <name>
|
|
14
|
+
* nginx-site discard-backup <name> delete <name>.bak
|
|
15
|
+
* nginx-site enable <name> link <name> into sites-enabled
|
|
16
|
+
* nginx-site disable <name> unlink <name> from sites-enabled
|
|
17
|
+
* nginx-site remove <name> delete <name>, its link and its backup
|
|
18
|
+
* authelia-totp <username> stdin: base32 secret
|
|
19
|
+
* self-update <version> run create-lamaste@<version> detached
|
|
20
|
+
*
|
|
21
|
+
* Why vhosts are validated: nginx's master process runs as root and opens the
|
|
22
|
+
* files a configuration names — `access_log /etc/cron.d/x` with a crafted
|
|
23
|
+
* `log_format` is a root shell. A vhost is accepted only if every directive is
|
|
24
|
+
* on the allow-list below, in the context it belongs to, with arguments that
|
|
25
|
+
* cannot name a file outside the paths Lamaste serves. Anything else —
|
|
26
|
+
* including a directive nginx would accept — is refused.
|
|
27
|
+
*
|
|
28
|
+
* This file is installed root-owned (0755) at /usr/local/sbin/lamaste-priv by
|
|
29
|
+
* create-lamaste. It uses only Node.js built-ins and reads nothing the
|
|
30
|
+
* lamaste user can write, so it cannot be steered by the panel's own files.
|
|
31
|
+
* It is CommonJS on purpose: an extension-less script outside any package is
|
|
32
|
+
* loaded as CommonJS by every supported Node.js version.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
'use strict';
|
|
36
|
+
|
|
37
|
+
const fs = require('node:fs');
|
|
38
|
+
const path = require('node:path');
|
|
39
|
+
const crypto = require('node:crypto');
|
|
40
|
+
const { spawnSync } = require('node:child_process');
|
|
41
|
+
|
|
42
|
+
const SITES_AVAILABLE = '/etc/nginx/sites-available';
|
|
43
|
+
const SITES_ENABLED = '/etc/nginx/sites-enabled';
|
|
44
|
+
const MAX_VHOST_BYTES = 256 * 1024;
|
|
45
|
+
|
|
46
|
+
const AUTHELIA_BIN = '/usr/local/bin/authelia';
|
|
47
|
+
const AUTHELIA_CONFIG = '/etc/authelia/configuration.yml';
|
|
48
|
+
const AUTHELIA_USER = 'lamaste-authelia';
|
|
49
|
+
|
|
50
|
+
const SYSTEMD_RUN = '/usr/bin/systemd-run';
|
|
51
|
+
const NPX = '/usr/bin/npx';
|
|
52
|
+
|
|
53
|
+
class UsageError extends Error {}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Site names
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
const LABEL = '[a-z0-9](?:[a-z0-9-]{0,78}[a-z0-9])?';
|
|
60
|
+
const UUID = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';
|
|
61
|
+
|
|
62
|
+
/** Vhosts the panel writes. */
|
|
63
|
+
const PANEL_SITE_RE = new RegExp(
|
|
64
|
+
`^lamalibre-lamaste-(?:panel-domain|auth|tunnel|app-${LABEL}|agent-panel-${LABEL}|site-${UUID})$`,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
/** Vhosts create-lamaste writes that the panel may only switch on and off. */
|
|
68
|
+
const TOGGLE_ONLY_SITES = new Set(['lamalibre-lamaste-panel-ip']);
|
|
69
|
+
|
|
70
|
+
function assertSiteName(name, action) {
|
|
71
|
+
if (typeof name !== 'string' || name.length > 128) {
|
|
72
|
+
throw new UsageError('invalid site name');
|
|
73
|
+
}
|
|
74
|
+
if (PANEL_SITE_RE.test(name)) return;
|
|
75
|
+
if ((action === 'enable' || action === 'disable') && TOGGLE_ONLY_SITES.has(name)) return;
|
|
76
|
+
throw new UsageError(`site name not allowed for ${action}: ${name}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// nginx configuration: tokenizer and parser
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Split nginx configuration text into tokens: words (quoted or bare), `{`,
|
|
85
|
+
* `}` and `;`. Comments run from a `#` at the start of a token to the end of
|
|
86
|
+
* the line, as in nginx.
|
|
87
|
+
*
|
|
88
|
+
* Anything whose meaning could differ between this tokenizer and nginx's is
|
|
89
|
+
* refused rather than interpreted: backslashes, `${`, control characters, and
|
|
90
|
+
* non-ASCII text outside comments.
|
|
91
|
+
*/
|
|
92
|
+
function tokenize(text) {
|
|
93
|
+
const tokens = [];
|
|
94
|
+
let i = 0;
|
|
95
|
+
let line = 1;
|
|
96
|
+
const n = text.length;
|
|
97
|
+
const isSpace = (c) => c === ' ' || c === '\t' || c === '\r' || c === '\n';
|
|
98
|
+
const fail = (msg) => {
|
|
99
|
+
throw new UsageError(`vhost line ${line}: ${msg}`);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
while (i < n) {
|
|
103
|
+
const c = text[i];
|
|
104
|
+
if (c === '\n') {
|
|
105
|
+
line++;
|
|
106
|
+
i++;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (isSpace(c)) {
|
|
110
|
+
i++;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (c === '#') {
|
|
114
|
+
while (i < n && text[i] !== '\n') i++;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (c === '{' || c === '}' || c === ';') {
|
|
118
|
+
tokens.push({ type: c, line });
|
|
119
|
+
i++;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let value = '';
|
|
124
|
+
let quoted = false;
|
|
125
|
+
if (c === '"' || c === "'") {
|
|
126
|
+
quoted = true;
|
|
127
|
+
const quote = c;
|
|
128
|
+
i++;
|
|
129
|
+
for (;;) {
|
|
130
|
+
if (i >= n) fail('unterminated quoted string');
|
|
131
|
+
const d = text[i];
|
|
132
|
+
if (d === quote) {
|
|
133
|
+
i++;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
if (d === '\n') fail('newline inside a quoted string');
|
|
137
|
+
value += d;
|
|
138
|
+
i++;
|
|
139
|
+
}
|
|
140
|
+
if (i < n && !isSpace(text[i]) && text[i] !== ';' && text[i] !== '{' && text[i] !== '}') {
|
|
141
|
+
fail('unexpected character after a quoted string');
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
while (i < n) {
|
|
145
|
+
const d = text[i];
|
|
146
|
+
if (isSpace(d) || d === ';' || d === '{' || d === '}') break;
|
|
147
|
+
if (d === '"' || d === "'") fail('quote inside a word');
|
|
148
|
+
value += d;
|
|
149
|
+
i++;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (let k = 0; k < value.length; k++) {
|
|
154
|
+
const code = value.charCodeAt(k);
|
|
155
|
+
if (code < 0x20 || code > 0x7e) fail('non-printable or non-ASCII character');
|
|
156
|
+
}
|
|
157
|
+
if (value.includes('\\')) fail('backslash');
|
|
158
|
+
if (value.includes('${')) fail('"${" variable syntax');
|
|
159
|
+
tokens.push({ type: 'word', value, quoted, line });
|
|
160
|
+
}
|
|
161
|
+
return tokens;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Parse tokens into directives: `{ name, args, block, line }`, where `block`
|
|
166
|
+
* is an array of child directives or null for a simple directive.
|
|
167
|
+
*/
|
|
168
|
+
function parse(tokens) {
|
|
169
|
+
let pos = 0;
|
|
170
|
+
const parseBlock = (depth) => {
|
|
171
|
+
const out = [];
|
|
172
|
+
for (;;) {
|
|
173
|
+
const t = tokens[pos];
|
|
174
|
+
if (t === undefined) {
|
|
175
|
+
if (depth > 0) throw new UsageError('vhost: unexpected end of file, missing "}"');
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
if (t.type === '}') {
|
|
179
|
+
if (depth === 0) throw new UsageError(`vhost line ${t.line}: unexpected "}"`);
|
|
180
|
+
pos++;
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
if (t.type !== 'word') {
|
|
184
|
+
throw new UsageError(`vhost line ${t.line}: unexpected "${t.type}"`);
|
|
185
|
+
}
|
|
186
|
+
const name = t.value;
|
|
187
|
+
const args = [];
|
|
188
|
+
pos++;
|
|
189
|
+
for (;;) {
|
|
190
|
+
const a = tokens[pos];
|
|
191
|
+
if (a === undefined)
|
|
192
|
+
throw new UsageError(`vhost line ${t.line}: "${name}" is not terminated`);
|
|
193
|
+
if (a.type === 'word') {
|
|
194
|
+
args.push(a.value);
|
|
195
|
+
pos++;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (a.type === ';') {
|
|
199
|
+
pos++;
|
|
200
|
+
out.push({ name, args, block: null, line: t.line });
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
if (a.type === '{') {
|
|
204
|
+
if (depth >= 8) throw new UsageError(`vhost line ${t.line}: blocks nested too deeply`);
|
|
205
|
+
pos++;
|
|
206
|
+
const block = parseBlock(depth + 1);
|
|
207
|
+
out.push({ name, args, block, line: t.line });
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
throw new UsageError(`vhost line ${a.line}: unexpected "}"`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
return parseBlock(0);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// nginx configuration: allow-list
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
const HOSTNAME_RE =
|
|
222
|
+
/^(?=.{1,253}$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/;
|
|
223
|
+
const CERT_FILE_RE =
|
|
224
|
+
/^\/etc\/letsencrypt\/live\/[a-z0-9][a-z0-9.-]{0,252}\/(?:fullchain|privkey)\.pem$/;
|
|
225
|
+
const SNIPPET_RE = /^\/etc\/nginx\/snippets\/lamalibre-lamaste-[a-z0-9-]{1,64}\.conf$/;
|
|
226
|
+
const WEBROOT_RE = /^\/var\/www\/lamaste\/[A-Za-z0-9._/-]*$/;
|
|
227
|
+
const HEADER_NAME_RE = /^[A-Za-z0-9-]{1,64}$/;
|
|
228
|
+
const VARIABLE_RE = /^\$[A-Za-z_][A-Za-z0-9_]{0,63}$/;
|
|
229
|
+
const URI_PATH_RE = /^\/[A-Za-z0-9._~/-]*$/;
|
|
230
|
+
|
|
231
|
+
function hasDotDotSegment(p) {
|
|
232
|
+
return p.split('/').some((seg) => seg === '..' || seg === '.');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function webrootPath(p) {
|
|
236
|
+
return WEBROOT_RE.test(p) && !hasDotDotSegment(p);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const argc = (min, max) => (args) => args.length >= min && args.length <= max;
|
|
240
|
+
const each = (re) => (args) => args.length > 0 && args.every((a) => re.test(a));
|
|
241
|
+
const oneOf =
|
|
242
|
+
(...values) =>
|
|
243
|
+
(args) =>
|
|
244
|
+
args.length === 1 && values.includes(args[0]);
|
|
245
|
+
const any = () => true;
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Directives allowed in each context, with a check of their arguments. A
|
|
249
|
+
* directive not listed for its context is refused. `block: true` marks a
|
|
250
|
+
* block directive and names the context of its children.
|
|
251
|
+
*/
|
|
252
|
+
const RULES = {
|
|
253
|
+
// http{} level — the top of a sites-enabled file
|
|
254
|
+
main: {
|
|
255
|
+
server: { block: 'server', args: argc(0, 0) },
|
|
256
|
+
map: { block: 'map', args: (a) => a.length === 2 && a.every((v) => VARIABLE_RE.test(v)) },
|
|
257
|
+
limit_req_zone: {
|
|
258
|
+
args: (a) =>
|
|
259
|
+
a.length === 3 &&
|
|
260
|
+
VARIABLE_RE.test(a[0]) &&
|
|
261
|
+
/^zone=[a-z_]{1,32}:\d{1,4}[km]$/.test(a[1]) &&
|
|
262
|
+
/^rate=\d{1,6}r\/[sm]$/.test(a[2]),
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
server: {
|
|
266
|
+
// Port 80 belongs to create-lamaste's HTTPS redirect and to certbot's
|
|
267
|
+
// HTTP-01 challenges; panel vhosts serve TLS only.
|
|
268
|
+
listen: {
|
|
269
|
+
args: (a) =>
|
|
270
|
+
a.length >= 1 &&
|
|
271
|
+
/^(?:\[::\]:)?443$/.test(a[0]) &&
|
|
272
|
+
a.slice(1).every((f) => f === 'ssl' || f === 'http2'),
|
|
273
|
+
},
|
|
274
|
+
server_name: { args: each(HOSTNAME_RE) },
|
|
275
|
+
ssl_certificate: {
|
|
276
|
+
args: (a) => a.length === 1 && CERT_FILE_RE.test(a[0]) && !hasDotDotSegment(a[0]),
|
|
277
|
+
},
|
|
278
|
+
ssl_certificate_key: {
|
|
279
|
+
args: (a) => a.length === 1 && CERT_FILE_RE.test(a[0]) && !hasDotDotSegment(a[0]),
|
|
280
|
+
},
|
|
281
|
+
include: { args: (a) => a.length === 1 && SNIPPET_RE.test(a[0]) },
|
|
282
|
+
ssl_protocols: { args: each(/^TLSv1\.[23]$/) },
|
|
283
|
+
ssl_ciphers: { args: (a) => a.length === 1 && /^[A-Za-z0-9:!+@_-]{1,512}$/.test(a[0]) },
|
|
284
|
+
ssl_prefer_server_ciphers: { args: oneOf('on', 'off') },
|
|
285
|
+
client_max_body_size: { args: (a) => a.length === 1 && /^\d{1,6}[kKmMgG]?$/.test(a[0]) },
|
|
286
|
+
root: { args: (a) => a.length === 1 && webrootPath(a[0]) },
|
|
287
|
+
index: { args: each(/^[A-Za-z0-9._-]{1,64}$/) },
|
|
288
|
+
add_header: {
|
|
289
|
+
args: (a) =>
|
|
290
|
+
(a.length === 2 || (a.length === 3 && a[2] === 'always')) && HEADER_NAME_RE.test(a[0]),
|
|
291
|
+
},
|
|
292
|
+
proxy_set_header: { args: (a) => a.length === 2 && HEADER_NAME_RE.test(a[0]) },
|
|
293
|
+
error_page: { args: errorPageArgs },
|
|
294
|
+
return: { args: argc(1, 2) },
|
|
295
|
+
rewrite: { args: rewriteArgs },
|
|
296
|
+
location: { block: 'location', args: locationArgs },
|
|
297
|
+
},
|
|
298
|
+
location: {
|
|
299
|
+
location: { block: 'location', args: locationArgs },
|
|
300
|
+
if: { block: 'if', args: argc(1, 5) },
|
|
301
|
+
internal: { args: argc(0, 0) },
|
|
302
|
+
return: { args: argc(1, 2) },
|
|
303
|
+
rewrite: { args: rewriteArgs },
|
|
304
|
+
root: { args: (a) => a.length === 1 && webrootPath(a[0]) },
|
|
305
|
+
alias: { args: (a) => a.length === 1 && webrootPath(a[0]) },
|
|
306
|
+
index: { args: each(/^[A-Za-z0-9._-]{1,64}$/) },
|
|
307
|
+
try_files: { args: each(/^[A-Za-z0-9._/$=-]{1,256}$/) },
|
|
308
|
+
add_header: {
|
|
309
|
+
args: (a) =>
|
|
310
|
+
(a.length === 2 || (a.length === 3 && a[2] === 'always')) && HEADER_NAME_RE.test(a[0]),
|
|
311
|
+
},
|
|
312
|
+
error_page: { args: errorPageArgs },
|
|
313
|
+
client_max_body_size: { args: (a) => a.length === 1 && /^\d{1,6}[kKmMgG]?$/.test(a[0]) },
|
|
314
|
+
limit_req: {
|
|
315
|
+
args: (a) =>
|
|
316
|
+
a.length >= 1 &&
|
|
317
|
+
a.length <= 3 &&
|
|
318
|
+
/^zone=[a-z_]{1,32}$/.test(a[0]) &&
|
|
319
|
+
a.slice(1).every((f) => /^burst=\d{1,6}$/.test(f) || f === 'nodelay'),
|
|
320
|
+
},
|
|
321
|
+
proxy_pass: {
|
|
322
|
+
args: (a) =>
|
|
323
|
+
a.length === 1 && /^http:\/\/127\.0\.0\.1:\d{1,5}(?:\/[A-Za-z0-9._~/-]*)?$/.test(a[0]),
|
|
324
|
+
},
|
|
325
|
+
proxy_set_header: { args: (a) => a.length === 2 && HEADER_NAME_RE.test(a[0]) },
|
|
326
|
+
proxy_http_version: { args: oneOf('1.0', '1.1') },
|
|
327
|
+
proxy_read_timeout: { args: (a) => a.length === 1 && /^\d{1,7}[smhd]?$/.test(a[0]) },
|
|
328
|
+
proxy_send_timeout: { args: (a) => a.length === 1 && /^\d{1,7}[smhd]?$/.test(a[0]) },
|
|
329
|
+
proxy_buffers: {
|
|
330
|
+
args: (a) => a.length === 2 && /^\d{1,4}$/.test(a[0]) && /^\d{1,6}[kKmM]?$/.test(a[1]),
|
|
331
|
+
},
|
|
332
|
+
proxy_next_upstream: {
|
|
333
|
+
args: each(
|
|
334
|
+
/^(?:error|timeout|invalid_header|http_(?:500|502|503|504|403|404|429)|non_idempotent|off)$/,
|
|
335
|
+
),
|
|
336
|
+
},
|
|
337
|
+
proxy_pass_request_body: { args: oneOf('on', 'off') },
|
|
338
|
+
auth_request: { args: (a) => a.length === 1 && (a[0] === 'off' || URI_PATH_RE.test(a[0])) },
|
|
339
|
+
auth_request_set: { args: (a) => a.length === 2 && VARIABLE_RE.test(a[0]) },
|
|
340
|
+
},
|
|
341
|
+
if: {
|
|
342
|
+
return: { args: argc(1, 2) },
|
|
343
|
+
rewrite: { args: rewriteArgs },
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
function errorPageArgs(a) {
|
|
348
|
+
if (a.length < 2) return false;
|
|
349
|
+
let k = 0;
|
|
350
|
+
while (k < a.length - 1 && /^\d{3}$/.test(a[k])) k++;
|
|
351
|
+
if (k === 0) return false;
|
|
352
|
+
if (k < a.length - 1 && /^=\d{0,3}$/.test(a[k])) k++;
|
|
353
|
+
return k === a.length - 1;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function rewriteArgs(a) {
|
|
357
|
+
return (
|
|
358
|
+
(a.length === 2 ||
|
|
359
|
+
(a.length === 3 && ['last', 'break', 'redirect', 'permanent'].includes(a[2]))) &&
|
|
360
|
+
a[0].length <= 512 &&
|
|
361
|
+
a[1].length <= 512
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function locationArgs(a) {
|
|
366
|
+
if (a.length === 1) return a[0].length <= 512 && a[0].startsWith('/');
|
|
367
|
+
return a.length === 2 && ['=', '~', '~*', '^~'].includes(a[0]) && a[1].length <= 512;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function validateDirectives(directives, context) {
|
|
371
|
+
const rules = RULES[context];
|
|
372
|
+
for (const d of directives) {
|
|
373
|
+
if (context === 'map') {
|
|
374
|
+
// map entries are "key value;" pairs. `include` inside a map reads a
|
|
375
|
+
// file (as root) into the map's values — refused, like any block.
|
|
376
|
+
if (d.block !== null) throw new UsageError(`vhost line ${d.line}: block inside map`);
|
|
377
|
+
if (d.name === 'include') throw new UsageError(`vhost line ${d.line}: include inside map`);
|
|
378
|
+
if (
|
|
379
|
+
d.args.length !== 1 &&
|
|
380
|
+
!(d.args.length === 0 && ['hostnames', 'volatile'].includes(d.name))
|
|
381
|
+
) {
|
|
382
|
+
throw new UsageError(`vhost line ${d.line}: malformed map entry`);
|
|
383
|
+
}
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
const rule = rules[d.name];
|
|
387
|
+
if (!rule) {
|
|
388
|
+
throw new UsageError(
|
|
389
|
+
`vhost line ${d.line}: directive "${d.name}" is not allowed in ${context}`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
if (!rule.args(d.args)) {
|
|
393
|
+
throw new UsageError(`vhost line ${d.line}: arguments of "${d.name}" are not allowed`);
|
|
394
|
+
}
|
|
395
|
+
if (rule.block) {
|
|
396
|
+
if (d.block === null) throw new UsageError(`vhost line ${d.line}: "${d.name}" needs a block`);
|
|
397
|
+
validateDirectives(d.block, rule.block);
|
|
398
|
+
} else if (d.block !== null) {
|
|
399
|
+
throw new UsageError(`vhost line ${d.line}: "${d.name}" takes no block`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Throws UsageError unless `text` is a vhost Lamaste may install. */
|
|
405
|
+
function validateVhost(text) {
|
|
406
|
+
if (Buffer.byteLength(text, 'utf8') > MAX_VHOST_BYTES) {
|
|
407
|
+
throw new UsageError('vhost is too large');
|
|
408
|
+
}
|
|
409
|
+
// Comments may carry any text; strip them before the ASCII checks by
|
|
410
|
+
// letting the tokenizer skip them.
|
|
411
|
+
const directives = parse(tokenize(text));
|
|
412
|
+
if (directives.length === 0) throw new UsageError('vhost is empty');
|
|
413
|
+
validateDirectives(directives, 'main');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
417
|
+
// Filesystem helpers (all paths are in root-owned directories)
|
|
418
|
+
// ---------------------------------------------------------------------------
|
|
419
|
+
|
|
420
|
+
function fsyncDir(dir) {
|
|
421
|
+
const fd = fs.openSync(dir, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY);
|
|
422
|
+
try {
|
|
423
|
+
fs.fsyncSync(fd);
|
|
424
|
+
} finally {
|
|
425
|
+
fs.closeSync(fd);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Atomically replace `dest` with `content` (temp file, fsync, rename). */
|
|
430
|
+
function atomicWrite(dest, content, mode) {
|
|
431
|
+
const dir = path.dirname(dest);
|
|
432
|
+
const tmp = path.join(dir, `.lamaste-priv-${crypto.randomBytes(8).toString('hex')}`);
|
|
433
|
+
const fd = fs.openSync(
|
|
434
|
+
tmp,
|
|
435
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW,
|
|
436
|
+
mode,
|
|
437
|
+
);
|
|
438
|
+
try {
|
|
439
|
+
fs.writeSync(fd, content);
|
|
440
|
+
fs.fchmodSync(fd, mode);
|
|
441
|
+
fs.fsyncSync(fd);
|
|
442
|
+
} catch (err) {
|
|
443
|
+
fs.closeSync(fd);
|
|
444
|
+
fs.rmSync(tmp, { force: true });
|
|
445
|
+
throw err;
|
|
446
|
+
}
|
|
447
|
+
fs.closeSync(fd);
|
|
448
|
+
try {
|
|
449
|
+
fs.renameSync(tmp, dest);
|
|
450
|
+
} catch (err) {
|
|
451
|
+
fs.rmSync(tmp, { force: true });
|
|
452
|
+
throw err;
|
|
453
|
+
}
|
|
454
|
+
fsyncDir(dir);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function unlinkIfPresent(p) {
|
|
458
|
+
try {
|
|
459
|
+
fs.unlinkSync(p);
|
|
460
|
+
return true;
|
|
461
|
+
} catch (err) {
|
|
462
|
+
if (err.code === 'ENOENT') return false;
|
|
463
|
+
throw err;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function isRegularFile(p) {
|
|
468
|
+
try {
|
|
469
|
+
return fs.lstatSync(p).isFile();
|
|
470
|
+
} catch (err) {
|
|
471
|
+
if (err.code === 'ENOENT') return false;
|
|
472
|
+
throw err;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function readStdin(maxBytes) {
|
|
477
|
+
const chunks = [];
|
|
478
|
+
let total = 0;
|
|
479
|
+
const buf = Buffer.alloc(65536);
|
|
480
|
+
for (;;) {
|
|
481
|
+
let read;
|
|
482
|
+
try {
|
|
483
|
+
read = fs.readSync(0, buf, 0, buf.length, null);
|
|
484
|
+
} catch (err) {
|
|
485
|
+
if (err.code === 'EAGAIN') continue;
|
|
486
|
+
if (err.code === 'EOF') break;
|
|
487
|
+
throw err;
|
|
488
|
+
}
|
|
489
|
+
if (read === 0) break;
|
|
490
|
+
total += read;
|
|
491
|
+
if (total > maxBytes) throw new UsageError('input is too large');
|
|
492
|
+
chunks.push(Buffer.from(buf.subarray(0, read)));
|
|
493
|
+
}
|
|
494
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ---------------------------------------------------------------------------
|
|
498
|
+
// Operations
|
|
499
|
+
// ---------------------------------------------------------------------------
|
|
500
|
+
|
|
501
|
+
function nginxSite(action, name) {
|
|
502
|
+
assertSiteName(name, action);
|
|
503
|
+
const available = path.join(SITES_AVAILABLE, name);
|
|
504
|
+
const enabled = path.join(SITES_ENABLED, name);
|
|
505
|
+
const backup = `${available}.bak`;
|
|
506
|
+
|
|
507
|
+
switch (action) {
|
|
508
|
+
case 'write': {
|
|
509
|
+
const text = readStdin(MAX_VHOST_BYTES + 1);
|
|
510
|
+
validateVhost(text);
|
|
511
|
+
atomicWrite(available, text, 0o644);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
case 'backup': {
|
|
515
|
+
if (!isRegularFile(available)) throw new UsageError(`${name} does not exist`);
|
|
516
|
+
atomicWrite(backup, fs.readFileSync(available), 0o644);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
case 'restore': {
|
|
520
|
+
if (!isRegularFile(backup)) throw new UsageError(`${name} has no backup`);
|
|
521
|
+
fs.renameSync(backup, available);
|
|
522
|
+
fsyncDir(SITES_AVAILABLE);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
case 'discard-backup':
|
|
526
|
+
unlinkIfPresent(backup);
|
|
527
|
+
return;
|
|
528
|
+
case 'enable': {
|
|
529
|
+
if (!isRegularFile(available)) throw new UsageError(`${name} does not exist`);
|
|
530
|
+
const tmp = path.join(
|
|
531
|
+
SITES_ENABLED,
|
|
532
|
+
`.lamaste-priv-${crypto.randomBytes(8).toString('hex')}`,
|
|
533
|
+
);
|
|
534
|
+
fs.symlinkSync(available, tmp);
|
|
535
|
+
try {
|
|
536
|
+
fs.renameSync(tmp, enabled);
|
|
537
|
+
} catch (err) {
|
|
538
|
+
unlinkIfPresent(tmp);
|
|
539
|
+
throw err;
|
|
540
|
+
}
|
|
541
|
+
fsyncDir(SITES_ENABLED);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
case 'disable':
|
|
545
|
+
unlinkIfPresent(enabled);
|
|
546
|
+
return;
|
|
547
|
+
case 'remove':
|
|
548
|
+
unlinkIfPresent(enabled);
|
|
549
|
+
unlinkIfPresent(available);
|
|
550
|
+
unlinkIfPresent(backup);
|
|
551
|
+
return;
|
|
552
|
+
default:
|
|
553
|
+
throw new UsageError(`unknown nginx-site action: ${action}`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function lookupId(flag, name) {
|
|
558
|
+
const result = spawnSync('/usr/bin/id', [flag, name], { encoding: 'utf8', env: {} });
|
|
559
|
+
const id = Number.parseInt((result.stdout || '').trim(), 10);
|
|
560
|
+
if (result.status !== 0 || !Number.isInteger(id) || id <= 0) {
|
|
561
|
+
throw new Error(`user ${name} does not exist — run create-lamaste to repair the installation`);
|
|
562
|
+
}
|
|
563
|
+
return id;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Store a TOTP secret in Authelia's database with Authelia's own CLI, run as
|
|
568
|
+
* the Authelia service user (the database is its file).
|
|
569
|
+
*/
|
|
570
|
+
function autheliaTotp(username) {
|
|
571
|
+
// Same shape as the panel's username schema; a leading "-" would read as a flag.
|
|
572
|
+
if (typeof username !== 'string' || !/^[a-z0-9_][a-z0-9_-]{1,31}$/.test(username)) {
|
|
573
|
+
throw new UsageError('invalid username');
|
|
574
|
+
}
|
|
575
|
+
const secret = readStdin(1024).trim();
|
|
576
|
+
if (!/^[A-Z2-7]{16,128}$/.test(secret)) throw new UsageError('invalid TOTP secret');
|
|
577
|
+
|
|
578
|
+
const uid = lookupId('-u', AUTHELIA_USER);
|
|
579
|
+
const gid = lookupId('-g', AUTHELIA_USER);
|
|
580
|
+
const result = spawnSync(
|
|
581
|
+
AUTHELIA_BIN,
|
|
582
|
+
[
|
|
583
|
+
'storage',
|
|
584
|
+
'user',
|
|
585
|
+
'totp',
|
|
586
|
+
'generate',
|
|
587
|
+
'--secret',
|
|
588
|
+
secret,
|
|
589
|
+
'--force',
|
|
590
|
+
'--issuer',
|
|
591
|
+
'Lamaste',
|
|
592
|
+
'--algorithm',
|
|
593
|
+
'SHA1',
|
|
594
|
+
'--digits',
|
|
595
|
+
'6',
|
|
596
|
+
'--period',
|
|
597
|
+
'30',
|
|
598
|
+
'--config',
|
|
599
|
+
AUTHELIA_CONFIG,
|
|
600
|
+
'--',
|
|
601
|
+
username,
|
|
602
|
+
],
|
|
603
|
+
{
|
|
604
|
+
uid,
|
|
605
|
+
gid,
|
|
606
|
+
cwd: '/',
|
|
607
|
+
env: { PATH: '/usr/bin:/bin', HOME: '/nonexistent' },
|
|
608
|
+
encoding: 'utf8',
|
|
609
|
+
timeout: 60_000,
|
|
610
|
+
},
|
|
611
|
+
);
|
|
612
|
+
if (result.error) throw result.error;
|
|
613
|
+
if (result.status !== 0) {
|
|
614
|
+
// Authelia's error output does not echo the secret; its success output
|
|
615
|
+
// (a QR code / otpauth URI) does, so only failures are relayed.
|
|
616
|
+
throw new Error(
|
|
617
|
+
`authelia exited with status ${result.status}: ${(result.stderr || '').trim()}`,
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Start `create-lamaste@<version> --yes` (the redeploy path) in a transient
|
|
624
|
+
* systemd unit, two seconds from now: it stops and restarts the panel, so it
|
|
625
|
+
* must not run inside the panel's service, and the delay lets the panel's
|
|
626
|
+
* HTTP response reach the browser first.
|
|
627
|
+
*/
|
|
628
|
+
function selfUpdate(version) {
|
|
629
|
+
if (typeof version !== 'string' || !/^\d{1,4}\.\d{1,4}\.\d{1,6}$/.test(version)) {
|
|
630
|
+
throw new UsageError('version must be MAJOR.MINOR.PATCH');
|
|
631
|
+
}
|
|
632
|
+
const unit = `lamalibre-lamaste-update-${crypto.randomBytes(8).toString('hex')}`;
|
|
633
|
+
const result = spawnSync(
|
|
634
|
+
SYSTEMD_RUN,
|
|
635
|
+
[
|
|
636
|
+
'--unit',
|
|
637
|
+
unit,
|
|
638
|
+
'--description',
|
|
639
|
+
`Lamaste update to ${version}`,
|
|
640
|
+
'--on-active=2',
|
|
641
|
+
'--timer-property=AccuracySec=100ms',
|
|
642
|
+
'--property=Type=oneshot',
|
|
643
|
+
'--setenv=HOME=/root',
|
|
644
|
+
NPX,
|
|
645
|
+
'--yes',
|
|
646
|
+
`@lamalibre/create-lamaste@${version}`,
|
|
647
|
+
'--yes',
|
|
648
|
+
],
|
|
649
|
+
{ cwd: '/', env: { PATH: '/usr/sbin:/usr/bin:/sbin:/bin' }, encoding: 'utf8', timeout: 30_000 },
|
|
650
|
+
);
|
|
651
|
+
if (result.error) throw result.error;
|
|
652
|
+
if (result.status !== 0) {
|
|
653
|
+
throw new Error(
|
|
654
|
+
`systemd-run exited with status ${result.status}: ${(result.stderr || '').trim()}`,
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
process.stdout.write(`${unit}\n`);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function main(argv) {
|
|
661
|
+
if (typeof process.getuid !== 'function' || process.getuid() !== 0) {
|
|
662
|
+
throw new UsageError('must run as root (via sudo)');
|
|
663
|
+
}
|
|
664
|
+
process.umask(0o022);
|
|
665
|
+
const [op, ...rest] = argv;
|
|
666
|
+
switch (op) {
|
|
667
|
+
case 'nginx-site':
|
|
668
|
+
if (rest.length !== 2) throw new UsageError('usage: nginx-site <action> <name>');
|
|
669
|
+
nginxSite(rest[0], rest[1]);
|
|
670
|
+
return;
|
|
671
|
+
case 'authelia-totp':
|
|
672
|
+
if (rest.length !== 1) throw new UsageError('usage: authelia-totp <username>');
|
|
673
|
+
autheliaTotp(rest[0]);
|
|
674
|
+
return;
|
|
675
|
+
case 'self-update':
|
|
676
|
+
if (rest.length !== 1) throw new UsageError('usage: self-update <version>');
|
|
677
|
+
selfUpdate(rest[0]);
|
|
678
|
+
return;
|
|
679
|
+
default:
|
|
680
|
+
throw new UsageError(`unknown operation: ${op ?? '(none)'}`);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (require.main === module) {
|
|
685
|
+
try {
|
|
686
|
+
main(process.argv.slice(2));
|
|
687
|
+
} catch (err) {
|
|
688
|
+
process.stderr.write(`lamaste-priv: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
689
|
+
process.exit(err instanceof UsageError ? 2 : 1);
|
|
690
|
+
}
|
|
691
|
+
} else {
|
|
692
|
+
module.exports = { validateVhost, tokenize, parse, assertSiteName, PANEL_SITE_RE, UsageError };
|
|
693
|
+
}
|