@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,86 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { AUTHELIA_USER, CHISEL_GROUP } from './service-config.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Service accounts create-lamaste manages. All functions are idempotent and
|
|
6
|
+
* run as root.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
async function groupExists(name) {
|
|
10
|
+
try {
|
|
11
|
+
await execa('getent', ['group', name]);
|
|
12
|
+
return true;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function userExists(name) {
|
|
19
|
+
try {
|
|
20
|
+
await execa('getent', ['passwd', name]);
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create the group the chisel server runs as and add the lamaste user to it.
|
|
29
|
+
* Run after the lamaste user exists and before the panel service (re)starts —
|
|
30
|
+
* the service unit names the group in SupplementaryGroups=, and the panel
|
|
31
|
+
* writes the chisel authfile and key with it.
|
|
32
|
+
*
|
|
33
|
+
* @returns {Promise<{ created: boolean }>}
|
|
34
|
+
*/
|
|
35
|
+
export async function ensureChiselGroup() {
|
|
36
|
+
let created = false;
|
|
37
|
+
if (!(await groupExists(CHISEL_GROUP))) {
|
|
38
|
+
await execa('groupadd', ['--system', CHISEL_GROUP]);
|
|
39
|
+
created = true;
|
|
40
|
+
}
|
|
41
|
+
await execa('usermod', ['--append', '--groups', CHISEL_GROUP, 'lamaste']);
|
|
42
|
+
return { created };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create the system account Authelia runs as. Authelia is reachable from
|
|
47
|
+
* the internet (auth.<domain>), so it runs as neither root nor lamaste: it
|
|
48
|
+
* can read its configuration (group lamaste-authelia) and write only its
|
|
49
|
+
* database, notification file and log.
|
|
50
|
+
*
|
|
51
|
+
* @returns {Promise<{ created: boolean }>}
|
|
52
|
+
*/
|
|
53
|
+
export async function ensureAutheliaUser() {
|
|
54
|
+
if (await userExists(AUTHELIA_USER)) return { created: false };
|
|
55
|
+
if (!(await groupExists(AUTHELIA_USER))) {
|
|
56
|
+
await execa('groupadd', ['--system', AUTHELIA_USER]);
|
|
57
|
+
}
|
|
58
|
+
await execa('useradd', [
|
|
59
|
+
'--system',
|
|
60
|
+
'--gid',
|
|
61
|
+
AUTHELIA_USER,
|
|
62
|
+
'--no-create-home',
|
|
63
|
+
'--home-dir',
|
|
64
|
+
'/nonexistent',
|
|
65
|
+
'--shell',
|
|
66
|
+
'/usr/sbin/nologin',
|
|
67
|
+
'--comment',
|
|
68
|
+
'Lamaste Authelia',
|
|
69
|
+
AUTHELIA_USER,
|
|
70
|
+
]);
|
|
71
|
+
return { created: true };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Numeric uid/gid of a user (primary group) or of a group.
|
|
76
|
+
*
|
|
77
|
+
* @param {'user'|'group'} kind
|
|
78
|
+
* @param {string} name
|
|
79
|
+
* @returns {Promise<number>}
|
|
80
|
+
*/
|
|
81
|
+
export async function lookupId(kind, name) {
|
|
82
|
+
const { stdout } = await execa('getent', [kind === 'user' ? 'passwd' : 'group', name]);
|
|
83
|
+
const id = Number.parseInt(stdout.split(':')[2] ?? '', 10);
|
|
84
|
+
if (!Number.isInteger(id)) throw new Error(`Cannot resolve ${kind} ${name}`);
|
|
85
|
+
return id;
|
|
86
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { mkdtemp, rename, rm } from 'node:fs/promises';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import {
|
|
7
|
+
AUTHELIA_RELEASE,
|
|
8
|
+
CHISEL_RELEASE,
|
|
9
|
+
curlDownloadArgs,
|
|
10
|
+
downloadVerifiedAuthelia,
|
|
11
|
+
downloadVerifiedChisel,
|
|
12
|
+
isPinnedChiselVersion,
|
|
13
|
+
normaliseAutheliaVersion,
|
|
14
|
+
} from '@lamalibre/lamaste';
|
|
15
|
+
import { AUTHELIA_BIN, CHISEL_BIN } from './service-config.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Install the pinned chisel and Authelia releases under /usr/local/bin.
|
|
19
|
+
*
|
|
20
|
+
* Downloads are verified against the SHA-256 digests pinned in
|
|
21
|
+
* @lamalibre/lamaste before anything is unpacked, staged in a root-private
|
|
22
|
+
* temporary directory, and moved into place with a rename, so a running
|
|
23
|
+
* service never sees a half-written binary. Runs as root; the panel never
|
|
24
|
+
* installs a binary.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const CHISEL_ARCH = { x64: 'linux_amd64', arm64: 'linux_arm64' };
|
|
28
|
+
const AUTHELIA_ARCH = { x64: 'linux-amd64', arm64: 'linux-arm64' };
|
|
29
|
+
|
|
30
|
+
function archFor(table, what) {
|
|
31
|
+
const arch = table[process.arch];
|
|
32
|
+
if (!arch) throw new Error(`Unsupported architecture for ${what}: ${process.arch}`);
|
|
33
|
+
return arch;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function fetchToFile(url, outPath) {
|
|
37
|
+
await execa('curl', curlDownloadArgs(url, outPath));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Put `src` at `dest` as a root-owned 0755 executable, atomically. */
|
|
41
|
+
async function installExecutable(src, dest) {
|
|
42
|
+
const staged = `${dest}.lamaste-new`;
|
|
43
|
+
await execa('install', ['-o', 'root', '-g', 'root', '-m', '0755', src, staged]);
|
|
44
|
+
await rename(staged, dest);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function versionOutput(bin, args) {
|
|
48
|
+
if (!existsSync(bin)) return null;
|
|
49
|
+
try {
|
|
50
|
+
const { stdout, stderr } = await execa(bin, args, { timeout: 15_000 });
|
|
51
|
+
return (stdout || stderr || '').trim() || null;
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Install the pinned chisel release unless it is already installed.
|
|
59
|
+
*
|
|
60
|
+
* @returns {Promise<{ changed: boolean, version: string, previous: string | null }>}
|
|
61
|
+
*/
|
|
62
|
+
export async function installPinnedChisel() {
|
|
63
|
+
const previous = await versionOutput(CHISEL_BIN, ['--version']);
|
|
64
|
+
if (isPinnedChiselVersion(previous)) {
|
|
65
|
+
return { changed: false, version: CHISEL_RELEASE.version, previous };
|
|
66
|
+
}
|
|
67
|
+
const work = await mkdtemp(join(tmpdir(), 'lamaste-chisel-'));
|
|
68
|
+
try {
|
|
69
|
+
const bin = join(work, 'chisel');
|
|
70
|
+
await downloadVerifiedChisel(
|
|
71
|
+
archFor(CHISEL_ARCH, 'chisel'),
|
|
72
|
+
join(work, 'chisel.gz'),
|
|
73
|
+
bin,
|
|
74
|
+
fetchToFile,
|
|
75
|
+
);
|
|
76
|
+
await installExecutable(bin, CHISEL_BIN);
|
|
77
|
+
} finally {
|
|
78
|
+
await rm(work, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
const installed = await versionOutput(CHISEL_BIN, ['--version']);
|
|
81
|
+
if (!isPinnedChiselVersion(installed)) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`chisel was installed but reports ${installed ?? 'nothing'}, expected ${CHISEL_RELEASE.version}`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return { changed: true, version: CHISEL_RELEASE.version, previous };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function compareVersions(a, b) {
|
|
90
|
+
const pa = a.split('.').map(Number);
|
|
91
|
+
const pb = b.split('.').map(Number);
|
|
92
|
+
for (let i = 0; i < 3; i++) {
|
|
93
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
94
|
+
if (d !== 0) return d;
|
|
95
|
+
}
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Install the pinned Authelia release unless it (or a newer release) is
|
|
101
|
+
* already installed. A newer Authelia is kept: its database may already use
|
|
102
|
+
* a schema the pinned release cannot read, and Authelia refuses to start on
|
|
103
|
+
* a database newer than itself.
|
|
104
|
+
*
|
|
105
|
+
* @returns {Promise<{ changed: boolean, version: string, previous: string | null, keptNewer: boolean }>}
|
|
106
|
+
*/
|
|
107
|
+
export async function installPinnedAuthelia() {
|
|
108
|
+
const previousOutput = await versionOutput(AUTHELIA_BIN, ['--version']);
|
|
109
|
+
const previous = previousOutput ? normaliseAutheliaVersion(previousOutput) : null;
|
|
110
|
+
if (previous !== null && compareVersions(previous, AUTHELIA_RELEASE.version) >= 0) {
|
|
111
|
+
return {
|
|
112
|
+
changed: false,
|
|
113
|
+
version: previous,
|
|
114
|
+
previous,
|
|
115
|
+
keptNewer: previous !== AUTHELIA_RELEASE.version,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const work = await mkdtemp(join(tmpdir(), 'lamaste-authelia-'));
|
|
119
|
+
try {
|
|
120
|
+
const tgz = join(work, 'authelia.tar.gz');
|
|
121
|
+
await downloadVerifiedAuthelia(archFor(AUTHELIA_ARCH, 'Authelia'), tgz, fetchToFile);
|
|
122
|
+
// The tarball holds the binary at its top level, named `authelia`, next
|
|
123
|
+
// to upstream packaging files (units, sysusers, a config template).
|
|
124
|
+
await execa('tar', ['-xzf', tgz, '-C', work, '--no-same-owner', 'authelia']);
|
|
125
|
+
await installExecutable(join(work, 'authelia'), AUTHELIA_BIN);
|
|
126
|
+
} finally {
|
|
127
|
+
await rm(work, { recursive: true, force: true });
|
|
128
|
+
}
|
|
129
|
+
const installedOutput = await versionOutput(AUTHELIA_BIN, ['--version']);
|
|
130
|
+
const installed = installedOutput ? normaliseAutheliaVersion(installedOutput) : null;
|
|
131
|
+
if (installed !== AUTHELIA_RELEASE.version) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Authelia was installed but reports ${installed ?? 'nothing'}, expected ${AUTHELIA_RELEASE.version}`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return { changed: true, version: AUTHELIA_RELEASE.version, previous, keptNewer: false };
|
|
137
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates the static HTML page shown when a browser visits without a client certificate.
|
|
3
|
+
* nginx serves this via error_page 495/496 so the user sees helpful instructions
|
|
4
|
+
* instead of a raw "400 Bad Request".
|
|
5
|
+
*
|
|
6
|
+
* @param {{ ip: string, pkiDir: string }} ctx - Installer context with IP and PKI path.
|
|
7
|
+
* @returns {string} Complete HTML document.
|
|
8
|
+
*/
|
|
9
|
+
export function generateCertHelpPage(ctx) {
|
|
10
|
+
const ip = ctx.ip;
|
|
11
|
+
const scpCmd = `scp root@${ip}:${ctx.pkiDir}/client.p12 .`;
|
|
12
|
+
|
|
13
|
+
return `<!DOCTYPE html>
|
|
14
|
+
<html lang="en">
|
|
15
|
+
<head>
|
|
16
|
+
<meta charset="utf-8">
|
|
17
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
18
|
+
<title>Lamaste — Certificate Required</title>
|
|
19
|
+
<style>
|
|
20
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
21
|
+
body {
|
|
22
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
23
|
+
background: #09090b; color: #a1a1aa;
|
|
24
|
+
min-height: 100vh; display: flex; align-items: center; justify-content: center;
|
|
25
|
+
padding: 1.5rem;
|
|
26
|
+
}
|
|
27
|
+
.card {
|
|
28
|
+
background: #18181b; border: 1px solid #27272a; border-radius: 0.75rem;
|
|
29
|
+
max-width: 640px; width: 100%; padding: 2.5rem;
|
|
30
|
+
}
|
|
31
|
+
.icon { text-align: center; font-size: 3rem; margin-bottom: 1rem; }
|
|
32
|
+
h1 { color: #f4f4f5; font-size: 1.25rem; text-align: center; margin-bottom: 0.5rem; }
|
|
33
|
+
.subtitle { text-align: center; margin-bottom: 2rem; font-size: 0.875rem; }
|
|
34
|
+
.step { margin-bottom: 1.75rem; }
|
|
35
|
+
.step-header {
|
|
36
|
+
display: flex; align-items: center; gap: 0.625rem;
|
|
37
|
+
margin-bottom: 0.625rem;
|
|
38
|
+
}
|
|
39
|
+
.step-num {
|
|
40
|
+
background: #22d3ee; color: #09090b; font-weight: 700;
|
|
41
|
+
width: 1.5rem; height: 1.5rem; border-radius: 50%;
|
|
42
|
+
display: flex; align-items: center; justify-content: center;
|
|
43
|
+
font-size: 0.75rem; flex-shrink: 0;
|
|
44
|
+
}
|
|
45
|
+
.step-title { color: #e4e4e7; font-size: 0.9375rem; font-weight: 600; }
|
|
46
|
+
.step-body { margin-left: 2.125rem; font-size: 0.8125rem; line-height: 1.6; }
|
|
47
|
+
code {
|
|
48
|
+
background: #09090b; border: 1px solid #27272a; border-radius: 0.375rem;
|
|
49
|
+
padding: 0.625rem 0.875rem; display: block; margin: 0.5rem 0;
|
|
50
|
+
color: #22d3ee; font-size: 0.8125rem; word-break: break-all;
|
|
51
|
+
cursor: pointer; position: relative;
|
|
52
|
+
}
|
|
53
|
+
code:hover { border-color: #22d3ee; }
|
|
54
|
+
code::after {
|
|
55
|
+
content: 'click to copy'; position: absolute; right: 0.625rem; top: 50%;
|
|
56
|
+
transform: translateY(-50%); color: #52525b; font-size: 0.6875rem;
|
|
57
|
+
}
|
|
58
|
+
code.copied::after { content: 'copied!'; color: #22d3ee; }
|
|
59
|
+
.platform { margin-bottom: 0.375rem; }
|
|
60
|
+
.platform strong { color: #e4e4e7; }
|
|
61
|
+
.note {
|
|
62
|
+
background: #1c1917; border: 1px solid #422006; border-radius: 0.5rem;
|
|
63
|
+
padding: 0.875rem 1rem; margin-top: 1.5rem; font-size: 0.8125rem;
|
|
64
|
+
line-height: 1.5;
|
|
65
|
+
}
|
|
66
|
+
.note strong { color: #fbbf24; }
|
|
67
|
+
.refresh {
|
|
68
|
+
display: block; width: 100%; margin-top: 1.5rem;
|
|
69
|
+
background: #22d3ee; color: #09090b; font-weight: 700;
|
|
70
|
+
border: none; border-radius: 0.5rem; padding: 0.75rem;
|
|
71
|
+
font-size: 0.875rem; cursor: pointer; font-family: inherit;
|
|
72
|
+
}
|
|
73
|
+
.refresh:hover { background: #06b6d4; }
|
|
74
|
+
</style>
|
|
75
|
+
</head>
|
|
76
|
+
<body>
|
|
77
|
+
<div class="card">
|
|
78
|
+
<div class="icon">\u{1f510}</div>
|
|
79
|
+
<h1>Client Certificate Required</h1>
|
|
80
|
+
<p class="subtitle">
|
|
81
|
+
This Lamaste panel is protected by mTLS.<br>
|
|
82
|
+
Import your client certificate to continue.
|
|
83
|
+
</p>
|
|
84
|
+
|
|
85
|
+
<div class="step">
|
|
86
|
+
<div class="step-header">
|
|
87
|
+
<span class="step-num">1</span>
|
|
88
|
+
<span class="step-title">Download your certificate</span>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="step-body">
|
|
91
|
+
Run this on your local machine:
|
|
92
|
+
<code id="scp-cmd">${escapeHtml(scpCmd)}</code>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div class="step">
|
|
97
|
+
<div class="step-header">
|
|
98
|
+
<span class="step-num">2</span>
|
|
99
|
+
<span class="step-title">Import into your browser</span>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="step-body">
|
|
102
|
+
<div class="platform">
|
|
103
|
+
<strong>macOS:</strong> Double-click <code style="display:inline;padding:0.125rem 0.375rem;cursor:default" class="no-copy">client.p12</code>
|
|
104
|
+
→ Keychain Access opens → <strong style="color:#e4e4e7">select “System” keychain</strong> (not “login”)
|
|
105
|
+
→ enter the password from the installer output → authenticate with your Mac password.
|
|
106
|
+
If you see an “Unable to import” error for Local Items, ignore it — the System import succeeds.
|
|
107
|
+
Then find the certificate in the <strong style="color:#e4e4e7">System</strong> keychain →
|
|
108
|
+
double-click it → expand <strong style="color:#e4e4e7">Trust</strong> → set to <strong style="color:#e4e4e7">Always Trust</strong> → close and authenticate.
|
|
109
|
+
</div>
|
|
110
|
+
<div class="platform">
|
|
111
|
+
<strong>Linux (Chrome):</strong> Settings → Privacy & Security → Security → Manage certificates → Import
|
|
112
|
+
</div>
|
|
113
|
+
<div class="platform">
|
|
114
|
+
<strong>Windows:</strong> Double-click the file → Certificate Import Wizard → enter the password
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div class="step">
|
|
120
|
+
<div class="step-header">
|
|
121
|
+
<span class="step-num">3</span>
|
|
122
|
+
<span class="step-title">Reload this page</span>
|
|
123
|
+
</div>
|
|
124
|
+
<div class="step-body">
|
|
125
|
+
After importing, your browser will present the certificate automatically.
|
|
126
|
+
You may need to restart your browser first.
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div class="note">
|
|
131
|
+
<strong>Where is the password?</strong> The certificate password was printed
|
|
132
|
+
in your terminal when the installer finished. It is also stored on the server
|
|
133
|
+
at <code style="display:inline;padding:0.125rem 0.375rem;color:#fbbf24;cursor:default" class="no-copy">${escapeHtml(ctx.pkiDir)}/.p12-password</code>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<button class="refresh" onclick="location.reload()">
|
|
137
|
+
Reload Page
|
|
138
|
+
</button>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<script>
|
|
142
|
+
document.querySelectorAll('code:not(.no-copy)').forEach(function(el) {
|
|
143
|
+
el.addEventListener('click', function() {
|
|
144
|
+
navigator.clipboard.writeText(el.textContent.trim());
|
|
145
|
+
el.classList.add('copied');
|
|
146
|
+
setTimeout(function() { el.classList.remove('copied'); }, 2000);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
</script>
|
|
150
|
+
</body>
|
|
151
|
+
</html>`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function escapeHtml(str) {
|
|
155
|
+
return str
|
|
156
|
+
.replace(/&/g, '&')
|
|
157
|
+
.replace(/</g, '<')
|
|
158
|
+
.replace(/>/g, '>')
|
|
159
|
+
.replace(/"/g, '"');
|
|
160
|
+
}
|
package/src/lib/env.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { execa } from 'execa';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse /etc/os-release and validate that we are on Ubuntu 24.04.
|
|
6
|
+
* @returns {{ id: string, versionId: string, prettyName: string }}
|
|
7
|
+
*/
|
|
8
|
+
export async function detectOS() {
|
|
9
|
+
let content;
|
|
10
|
+
try {
|
|
11
|
+
content = await readFile('/etc/os-release', 'utf8');
|
|
12
|
+
} catch {
|
|
13
|
+
throw new Error('Could not read /etc/os-release. Lamaste requires Ubuntu 24.04.');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const fields = {};
|
|
17
|
+
for (const line of content.split('\n')) {
|
|
18
|
+
const match = line.match(/^(\w+)=(.*)$/);
|
|
19
|
+
if (match) {
|
|
20
|
+
// Strip surrounding quotes if present
|
|
21
|
+
fields[match[1]] = match[2].replace(/^"|"$/g, '');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const id = fields.ID || '';
|
|
26
|
+
const versionId = fields.VERSION_ID || '';
|
|
27
|
+
const prettyName = fields.PRETTY_NAME || `${id} ${versionId}`;
|
|
28
|
+
|
|
29
|
+
if (id !== 'ubuntu' || !versionId.startsWith('24.04')) {
|
|
30
|
+
throw new Error(`Lamaste requires Ubuntu 24.04. Detected: ${prettyName}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { id, versionId, prettyName };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Detect the public IP address of this machine.
|
|
38
|
+
* Tries the DigitalOcean metadata API first, falls back to hostname -I.
|
|
39
|
+
* When allowPrivate is true (--dev mode), accepts RFC 1918 addresses.
|
|
40
|
+
* @param {{ allowPrivate?: boolean }} options
|
|
41
|
+
* @returns {string} The IPv4 address.
|
|
42
|
+
*/
|
|
43
|
+
export async function detectIP({ allowPrivate = false } = {}) {
|
|
44
|
+
const isAcceptable = (ip) => (allowPrivate ? isValidIPv4(ip) : isPublicIP(ip));
|
|
45
|
+
|
|
46
|
+
// Try DigitalOcean metadata API first
|
|
47
|
+
try {
|
|
48
|
+
const { stdout } = await execa('curl', [
|
|
49
|
+
'-s',
|
|
50
|
+
'--max-time',
|
|
51
|
+
'2',
|
|
52
|
+
'http://169.254.169.254/metadata/v1/interfaces/public/0/ipv4/address',
|
|
53
|
+
]);
|
|
54
|
+
const ip = stdout.trim();
|
|
55
|
+
if (ip && isAcceptable(ip)) {
|
|
56
|
+
return ip;
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// Metadata API not available, fall back
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Fallback: hostname -I
|
|
63
|
+
try {
|
|
64
|
+
const { stdout } = await execa('hostname', ['-I']);
|
|
65
|
+
const addresses = stdout.trim().split(/\s+/);
|
|
66
|
+
for (const addr of addresses) {
|
|
67
|
+
if (isAcceptable(addr)) {
|
|
68
|
+
return addr;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// hostname -I failed
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
throw new Error(
|
|
76
|
+
'Could not detect a public IP address. Lamaste must be installed on a server with a public IP.',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check whether the given string is a valid IPv4 address (any routable address, not loopback/link-local).
|
|
82
|
+
* @param {string} ip
|
|
83
|
+
* @returns {boolean}
|
|
84
|
+
*/
|
|
85
|
+
function isValidIPv4(ip) {
|
|
86
|
+
const parts = ip.split('.');
|
|
87
|
+
if (parts.length !== 4) return false;
|
|
88
|
+
|
|
89
|
+
const octets = parts.map(Number);
|
|
90
|
+
if (octets.some((o) => Number.isNaN(o) || o < 0 || o > 255)) return false;
|
|
91
|
+
|
|
92
|
+
const [a, b] = octets;
|
|
93
|
+
|
|
94
|
+
// Reject loopback, link-local, and 0.0.0.0
|
|
95
|
+
if (a === 127) return false;
|
|
96
|
+
if (a === 169 && b === 254) return false;
|
|
97
|
+
if (octets.every((o) => o === 0)) return false;
|
|
98
|
+
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check whether the given IPv4 address is public (not RFC 1918 / loopback / link-local).
|
|
104
|
+
* @param {string} ip
|
|
105
|
+
* @returns {boolean}
|
|
106
|
+
*/
|
|
107
|
+
function isPublicIP(ip) {
|
|
108
|
+
if (!isValidIPv4(ip)) return false;
|
|
109
|
+
|
|
110
|
+
const [a, b] = ip.split('.').map(Number);
|
|
111
|
+
|
|
112
|
+
// 10.0.0.0/8
|
|
113
|
+
if (a === 10) return false;
|
|
114
|
+
// 172.16.0.0/12
|
|
115
|
+
if (a === 172 && b >= 16 && b <= 31) return false;
|
|
116
|
+
// 192.168.0.0/16
|
|
117
|
+
if (a === 192 && b === 168) return false;
|
|
118
|
+
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Verify the process is running as root.
|
|
124
|
+
* @throws {Error} if not root
|
|
125
|
+
*/
|
|
126
|
+
export function checkRoot() {
|
|
127
|
+
if (process.getuid() !== 0) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
'Lamaste installer must be run as root. Try: sudo npx @lamalibre/create-lamaste',
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { readFile, rm, symlink, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { HTTP_REDIRECT_SITE, generateHttpRedirectVhost } from './service-config.js';
|
|
5
|
+
|
|
6
|
+
const AVAILABLE = `/etc/nginx/sites-available/${HTTP_REDIRECT_SITE}`;
|
|
7
|
+
const ENABLED = `/etc/nginx/sites-enabled/${HTTP_REDIRECT_SITE}`;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Install or refresh the port-80 → HTTPS redirect site, test nginx, and
|
|
11
|
+
* reload it. Idempotent. Run as root. On a failed `nginx -t` the previous
|
|
12
|
+
* state is restored and the error is thrown.
|
|
13
|
+
*
|
|
14
|
+
* @param {{ reload?: boolean }} [options] - reload nginx afterwards (default true)
|
|
15
|
+
* @returns {Promise<{ changed: boolean }>}
|
|
16
|
+
*/
|
|
17
|
+
export async function ensureHttpRedirect({ reload = true } = {}) {
|
|
18
|
+
const content = generateHttpRedirectVhost();
|
|
19
|
+
const previous = existsSync(AVAILABLE) ? await readFile(AVAILABLE, 'utf8') : null;
|
|
20
|
+
const wasEnabled = existsSync(ENABLED);
|
|
21
|
+
if (previous === content && wasEnabled) return { changed: false };
|
|
22
|
+
|
|
23
|
+
await writeFile(AVAILABLE, content, { mode: 0o644 });
|
|
24
|
+
if (!wasEnabled) await symlink(AVAILABLE, ENABLED);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
await execa('nginx', ['-t']);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (previous === null) await rm(AVAILABLE, { force: true });
|
|
30
|
+
else await writeFile(AVAILABLE, previous, { mode: 0o644 });
|
|
31
|
+
if (!wasEnabled) await rm(ENABLED, { force: true });
|
|
32
|
+
throw new Error(
|
|
33
|
+
`nginx rejected the HTTP → HTTPS redirect site; left as it was:\n${err.stderr || err.message}`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
if (reload) await execa('systemctl', ['reload', 'nginx']);
|
|
37
|
+
return { changed: true };
|
|
38
|
+
}
|