underpost 3.2.90 → 3.3.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/.github/workflows/ghpkg.ci.yml +7 -1
- package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -16
- package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
- package/.github/workflows/release.cd.yml +1 -9
- package/CHANGELOG.md +110 -1
- package/CLI-HELP.md +139 -9
- package/README.md +5 -2
- package/bin/build.js +7 -5
- package/bin/deploy.js +1 -1
- package/deploy/lib/logging.sh +96 -0
- package/deploy/pwa-microservices-template/deploy.sh +72 -0
- package/deploy/release/deploy.sh +62 -0
- package/docker-compose.yml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +5 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +52 -0
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/package.json +5 -5
- package/scripts/audit-selinux.sh +64 -0
- package/scripts/coverall-test.sh +24 -0
- package/scripts/gpu-diag.sh +0 -0
- package/scripts/ip-info.sh +0 -0
- package/scripts/k3s-node-setup.sh +18 -15
- package/scripts/kubeadm-node-setup.sh +12 -23
- package/scripts/link-local-underpost-cli.sh +0 -0
- package/scripts/lxd-vm-setup.sh +0 -0
- package/scripts/maas-nat-firewalld.sh +0 -0
- package/scripts/nat-iptables.sh +2 -0
- package/scripts/rhel-grpc-setup.sh +0 -0
- package/scripts/rocky-kickstart.sh +25 -9
- package/scripts/test-monitor.sh +1 -1
- package/src/cli/baremetal.js +1 -2
- package/src/cli/cloud-init.js +1 -1
- package/src/cli/cluster.js +73 -68
- package/src/cli/db.js +9 -2
- package/src/cli/deploy.js +21 -5
- package/src/cli/docker-compose.js +1 -1
- package/src/cli/env.js +1 -1
- package/src/cli/image.js +0 -1
- package/src/cli/index.js +121 -9
- package/src/cli/lxd.js +1 -1
- package/src/cli/monitor.js +1 -1
- package/src/cli/release.js +57 -22
- package/src/cli/repository.js +11 -9
- package/src/cli/run.js +36 -9
- package/src/cli/ssh.js +198 -77
- package/src/cli/system.js +26 -13
- package/src/cli/test.js +1 -1
- package/src/cli/vultr.js +583 -0
- package/src/cli/wireguard.js +2125 -0
- package/src/client-builder/client-build.js +20 -14
- package/src/db/mongo/MongooseDB.js +4 -0
- package/src/index.js +25 -1
- package/src/projects/underpost/catalog-underpost.js +4 -1
- package/src/server/backup.js +1 -1
- package/src/server/conf.js +18 -108
- package/src/server/cron.js +249 -51
- package/src/server/dns.js +100 -6
- package/src/server/environment.js +98 -0
- package/src/server/forward-proxy.js +549 -0
- package/src/server/middlewares.js +56 -1
- package/src/server/process.js +0 -1
- package/src/server/selinux.js +185 -0
- package/src/server/systemd.js +205 -0
- package/src/server/underpost-compression.js +186 -0
- package/src/server/underpost-gateway.js +20 -10
- package/src/server/underpost-ingress.js +18 -2
- package/test/selinux.test.js +71 -0
- package/test/underpost-gateway.test.js +41 -0
- package/test/underpost-ingress.test.js +52 -0
- package/test/wireguard-edge.test.js +1177 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Express middleware and controller/router helpers for engine APIs.
|
|
3
3
|
*
|
|
4
4
|
* @module src/server/middlewares.js
|
|
5
|
+
* @namespace Middlewares
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { loggerFactory } from './logger.js';
|
|
@@ -12,8 +13,11 @@ const logger = loggerFactory(import.meta);
|
|
|
12
13
|
/**
|
|
13
14
|
* The public-read CORS policy: reflect the request origin (or allow any)
|
|
14
15
|
* and mark the resource embeddable cross-origin.
|
|
16
|
+
* @method setCrossOriginHeaders
|
|
15
17
|
* @param {import('express').Request} req
|
|
16
18
|
* @param {import('express').Response} res
|
|
19
|
+
* @returns {void}
|
|
20
|
+
* @memberof Middlewares
|
|
17
21
|
*/
|
|
18
22
|
const setCrossOriginHeaders = (req, res) => {
|
|
19
23
|
if (req && req.headers && req.headers.origin) res.set('Access-Control-Allow-Origin', req.headers.origin);
|
|
@@ -21,7 +25,15 @@ const setCrossOriginHeaders = (req, res) => {
|
|
|
21
25
|
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
|
22
26
|
};
|
|
23
27
|
|
|
24
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Express middleware form of {@link setCrossOriginHeaders}.
|
|
30
|
+
* @method crossOriginMiddleware
|
|
31
|
+
* @param {import('express').Request} req
|
|
32
|
+
* @param {import('express').Response} res
|
|
33
|
+
* @param {import('express').NextFunction} next
|
|
34
|
+
* @returns {void}
|
|
35
|
+
* @memberof Middlewares
|
|
36
|
+
*/
|
|
25
37
|
const crossOriginMiddleware = (req, res, next) => {
|
|
26
38
|
setCrossOriginHeaders(req, res);
|
|
27
39
|
next();
|
|
@@ -31,7 +43,10 @@ const crossOriginMiddleware = (req, res, next) => {
|
|
|
31
43
|
* Shallow request copy with `page`/`limit` parsed to integers.
|
|
32
44
|
* `path` and `params` are copied explicitly because spreading an Express
|
|
33
45
|
* request drops prototype getters.
|
|
46
|
+
* @method withParsedPagination
|
|
34
47
|
* @param {import('express').Request} req
|
|
48
|
+
* @returns {import('express').Request} Request-like object with parsed pagination.
|
|
49
|
+
* @memberof Middlewares
|
|
35
50
|
*/
|
|
36
51
|
const withParsedPagination = (req) => {
|
|
37
52
|
const { page, limit } = req.query;
|
|
@@ -43,15 +58,35 @@ const withParsedPagination = (req) => {
|
|
|
43
58
|
};
|
|
44
59
|
};
|
|
45
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Sends the standard success response envelope.
|
|
63
|
+
* @method sendSuccess
|
|
64
|
+
* @param {import('express').Response} res
|
|
65
|
+
* @param {*} data - Response payload.
|
|
66
|
+
* @returns {import('express').Response} JSON response.
|
|
67
|
+
* @memberof Middlewares
|
|
68
|
+
*/
|
|
46
69
|
const sendSuccess = (res, data) => res.status(200).json({ status: 'success', data });
|
|
47
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Sends the standard error response envelope.
|
|
73
|
+
* @method sendError
|
|
74
|
+
* @param {import('express').Response} res
|
|
75
|
+
* @param {Error} error - Error to expose.
|
|
76
|
+
* @param {number} [status=400] - HTTP status code.
|
|
77
|
+
* @returns {import('express').Response} JSON response.
|
|
78
|
+
* @memberof Middlewares
|
|
79
|
+
*/
|
|
48
80
|
const sendError = (res, error, status = 400) => res.status(status).json({ status: 'error', message: error.message });
|
|
49
81
|
|
|
50
82
|
/**
|
|
51
83
|
* Binary response with cross-origin and content headers.
|
|
84
|
+
* @method sendBlob
|
|
52
85
|
* @param {import('express').Request} req
|
|
53
86
|
* @param {import('express').Response} res
|
|
54
87
|
* @param {{ buffer: Buffer, mimetype: string, filename: string, disposition?: 'inline'|'attachment' }} blob
|
|
88
|
+
* @returns {import('express').Response} Completed binary response.
|
|
89
|
+
* @memberof Middlewares
|
|
55
90
|
*/
|
|
56
91
|
const sendBlob = (req, res, { buffer, mimetype, filename, disposition = 'inline' }) => {
|
|
57
92
|
setCrossOriginHeaders(req, res);
|
|
@@ -63,8 +98,11 @@ const sendBlob = (req, res, { buffer, mimetype, filename, disposition = 'inline'
|
|
|
63
98
|
|
|
64
99
|
/**
|
|
65
100
|
* Wraps a controller body with error logging and the error response envelope.
|
|
101
|
+
* @method controllerHandler
|
|
66
102
|
* @param {(req, res, options) => Promise<any>} fn
|
|
67
103
|
* @param {{ errorStatus?: number }} [config]
|
|
104
|
+
* @returns {Function} Async Express-compatible controller handler.
|
|
105
|
+
* @memberof Middlewares
|
|
68
106
|
*/
|
|
69
107
|
const controllerHandler =
|
|
70
108
|
(fn, { errorStatus = 400 } = {}) =>
|
|
@@ -80,8 +118,11 @@ const controllerHandler =
|
|
|
80
118
|
/**
|
|
81
119
|
* Builds a controller method that delegates to a service method and wraps the
|
|
82
120
|
* result in the success envelope.
|
|
121
|
+
* @method serviceHandler
|
|
83
122
|
* @param {(req, res, options) => Promise<any>} serviceFn
|
|
84
123
|
* @param {{ errorStatus?: number, crossOrigin?: boolean, pagination?: boolean }} [config]
|
|
124
|
+
* @returns {Function} Async Express-compatible controller handler.
|
|
125
|
+
* @memberof Middlewares
|
|
85
126
|
*/
|
|
86
127
|
const serviceHandler = (serviceFn, { errorStatus = 400, crossOrigin = false, pagination = false } = {}) =>
|
|
87
128
|
controllerHandler(
|
|
@@ -96,14 +137,26 @@ const serviceHandler = (serviceFn, { errorStatus = 400, crossOrigin = false, pag
|
|
|
96
137
|
/**
|
|
97
138
|
* Builds a standard CRUD controller class (static post/get/put/delete) from a
|
|
98
139
|
* service exposing the same methods. `get` parses pagination.
|
|
140
|
+
* @method buildCrudController
|
|
99
141
|
* @param {{ post, get, put, delete }} service
|
|
100
142
|
* @param {Object<string, Function>} [extend] - Extra or overriding static handlers.
|
|
143
|
+
* @returns {Function} CRUD controller class.
|
|
144
|
+
* @memberof Middlewares
|
|
101
145
|
*/
|
|
102
146
|
const buildCrudController = (service, extend = {}) => {
|
|
147
|
+
/**
|
|
148
|
+
* Generated controller namespace containing static CRUD handlers.
|
|
149
|
+
* @class CrudController
|
|
150
|
+
* @memberof Middlewares
|
|
151
|
+
*/
|
|
103
152
|
class CrudController {
|
|
153
|
+
/** @static @memberof Middlewares */
|
|
104
154
|
static post = serviceHandler(service.post);
|
|
155
|
+
/** @static @memberof Middlewares */
|
|
105
156
|
static get = serviceHandler(service.get, { pagination: true });
|
|
157
|
+
/** @static @memberof Middlewares */
|
|
106
158
|
static put = serviceHandler(service.put);
|
|
159
|
+
/** @static @memberof Middlewares */
|
|
107
160
|
static delete = serviceHandler(service.delete);
|
|
108
161
|
}
|
|
109
162
|
Object.assign(CrudController, extend);
|
|
@@ -115,6 +168,7 @@ const buildCrudController = (service, extend = {}) => {
|
|
|
115
168
|
* public reads, moderator-guarded writes, admin-guarded collection delete.
|
|
116
169
|
* Custom routes must be registered before calling this (generic `/:id` routes
|
|
117
170
|
* capture everything).
|
|
171
|
+
* @method registerCrudRoutes
|
|
118
172
|
* @param {import('express').Router} router
|
|
119
173
|
* @param {{ post, get, put, delete }} Controller
|
|
120
174
|
* @param {import('../../api/types.js').RouterOptions} options
|
|
@@ -122,6 +176,7 @@ const buildCrudController = (service, extend = {}) => {
|
|
|
122
176
|
* Pass empty arrays for unguarded endpoints (e.g. player-written progress)
|
|
123
177
|
* or explicit guard chains (e.g. admin-only reads).
|
|
124
178
|
* @returns {import('express').Router}
|
|
179
|
+
* @memberof Middlewares
|
|
125
180
|
*/
|
|
126
181
|
const registerCrudRoutes = (router, Controller, options, { readGuards = [], writeGuards, deleteAllGuards } = {}) => {
|
|
127
182
|
const write = writeGuards ?? [options.authMiddleware, moderatorGuard];
|
package/src/server/process.js
CHANGED
|
@@ -25,7 +25,6 @@ import shell from 'shelljs';
|
|
|
25
25
|
import { loggerFactory } from './logger.js';
|
|
26
26
|
import clipboard from 'clipboardy';
|
|
27
27
|
import Underpost from '../index.js';
|
|
28
|
-
import { getNpmRootPath } from './conf.js';
|
|
29
28
|
const logger = loggerFactory(import.meta);
|
|
30
29
|
/**
|
|
31
30
|
* Gets the current working directory, replacing backslashes with forward slashes for consistency.
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SELinux policy, labeling, and enforcement command helpers.
|
|
3
|
+
*
|
|
4
|
+
* @module src/server/selinux.js
|
|
5
|
+
* @namespace SELinuxService
|
|
6
|
+
*/
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Main SELinux utility.
|
|
11
|
+
* @class SELinuxService
|
|
12
|
+
* @memberof SELinuxService
|
|
13
|
+
*/
|
|
14
|
+
class SELinuxService {
|
|
15
|
+
/** Shared container label every unprivileged container domain can read and write. */
|
|
16
|
+
static SHARED_CONTAINER_TYPE = 'container_file_t';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Quotes one shell argument used by generated SELinux commands.
|
|
20
|
+
* @param {*} value - Value to quote.
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
static shellArgumentFactory(value) {
|
|
24
|
+
return `'${`${value ?? ''}`.replaceAll("'", `'"'"'`)}'`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Builds the Rocky/RHEL SELinux userspace installation command.
|
|
29
|
+
* @param {{sudo?: boolean}} [options]
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
static selinuxPackagesCommandFactory({ sudo = true } = {}) {
|
|
33
|
+
return `${sudo ? 'sudo ' : ''}dnf install -y policycoreutils policycoreutils-python-utils selinux-policy-targeted audit`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Builds commands that make Enforcing mode persistent and active.
|
|
38
|
+
* @param {{sudo?: boolean, restorePaths?: string[]}} [options]
|
|
39
|
+
* @returns {string[]}
|
|
40
|
+
*/
|
|
41
|
+
static selinuxEnforcingCommandsFactory({ sudo = true, restorePaths = [] } = {}) {
|
|
42
|
+
const prefix = sudo ? 'sudo ' : '';
|
|
43
|
+
return [
|
|
44
|
+
`if [ -f /etc/selinux/config ]; then ${prefix}sed -i -E 's/^SELINUX=.*/SELINUX=enforcing/' /etc/selinux/config; fi`,
|
|
45
|
+
// A host running with SELinux Disabled has an unlabeled filesystem, so the
|
|
46
|
+
// config flip alone would boot it into Enforcing with nothing labeled.
|
|
47
|
+
// `setenforce` cannot activate the mode from Disabled either: the switch
|
|
48
|
+
// completes on the next boot, and only after this relabel pass.
|
|
49
|
+
`if command -v getenforce >/dev/null 2>&1 && [ "$(getenforce)" = "Disabled" ]; then ${prefix}touch /.autorelabel; fi`,
|
|
50
|
+
...(restorePaths.length > 0
|
|
51
|
+
? [SELinuxService.selinuxRestoreconCommandFactory(restorePaths, { sudo })]
|
|
52
|
+
: []),
|
|
53
|
+
`if command -v getenforce >/dev/null 2>&1 && [ "$(getenforce)" != "Disabled" ]; then ${prefix}setenforce 1; fi`,
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Builds a command that restores policy-defined file contexts.
|
|
59
|
+
* @param {string|string[]} paths - Files or directories to label.
|
|
60
|
+
* @param {{recursive?: boolean, sudo?: boolean}} [options]
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
static selinuxRestoreconCommandFactory(paths, { recursive = true, sudo = true } = {}) {
|
|
64
|
+
const values = (Array.isArray(paths) ? paths : [paths]).filter(Boolean);
|
|
65
|
+
if (values.length === 0) throw new TypeError('selinuxRestoreconCommandFactory requires at least one path');
|
|
66
|
+
const operations = values
|
|
67
|
+
.map(SELinuxService.shellArgumentFactory)
|
|
68
|
+
.map(
|
|
69
|
+
(path) =>
|
|
70
|
+
`{ [ ! -e ${path} ] || ${sudo ? 'sudo ' : ''}restorecon ${recursive ? '-RF ' : ''}${path}; }`,
|
|
71
|
+
)
|
|
72
|
+
.join(' && ');
|
|
73
|
+
return `if command -v restorecon >/dev/null 2>&1; then ${operations}; fi`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Builds an idempotent persistent file context mapping.
|
|
78
|
+
* @param {string} path - Directory or file prefix to map.
|
|
79
|
+
* @param {{type: string, sudo?: boolean}} options
|
|
80
|
+
* @returns {string}
|
|
81
|
+
*/
|
|
82
|
+
static selinuxFileContextCommandFactory(path, { type, sudo = true } = {}) {
|
|
83
|
+
if (!path) throw new TypeError('selinuxFileContextCommandFactory requires a path');
|
|
84
|
+
if (!type) throw new TypeError('selinuxFileContextCommandFactory requires a type');
|
|
85
|
+
const prefix = sudo ? 'sudo ' : '';
|
|
86
|
+
const expression = SELinuxService.shellArgumentFactory(`${path}(/.*)?`);
|
|
87
|
+
return `if command -v selinuxenabled >/dev/null 2>&1 && selinuxenabled; then command -v semanage >/dev/null 2>&1 || { echo 'semanage is required for persistent file contexts' >&2; exit 1; }; ${prefix}semanage fcontext -a -t ${type} ${expression} 2>/dev/null || ${prefix}semanage fcontext -m -t ${type} ${expression}; fi`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Builds persistent labeling commands for host paths bind-mounted into
|
|
92
|
+
* unprivileged containers. `container_t` cannot read the policy defaults of
|
|
93
|
+
* those trees (`kubernetes_file_t`, `var_lib_t`), and the mapping is
|
|
94
|
+
* registered before the files exist so entries created later inherit the
|
|
95
|
+
* shared label instead of requiring another relabel pass.
|
|
96
|
+
* @param {string|string[]} paths - Files or directories to share.
|
|
97
|
+
* @param {{sudo?: boolean}} [options]
|
|
98
|
+
* @returns {string[]}
|
|
99
|
+
*/
|
|
100
|
+
static selinuxContainerSharedContextCommandsFactory(paths, { sudo = true } = {}) {
|
|
101
|
+
const values = (Array.isArray(paths) ? paths : [paths]).filter(Boolean);
|
|
102
|
+
if (values.length === 0)
|
|
103
|
+
throw new TypeError('selinuxContainerSharedContextCommandsFactory requires at least one path');
|
|
104
|
+
return [
|
|
105
|
+
...values.map((path) =>
|
|
106
|
+
SELinuxService.selinuxFileContextCommandFactory(path, { type: SELinuxService.SHARED_CONTAINER_TYPE, sudo }),
|
|
107
|
+
),
|
|
108
|
+
SELinuxService.selinuxRestoreconCommandFactory(values, { sudo }),
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Builds persistent labeling commands for an SSH directory.
|
|
114
|
+
* Standard /root and /home locations already have policy mappings; custom
|
|
115
|
+
* home locations receive an explicit ssh_home_t mapping.
|
|
116
|
+
* @param {{sshDirectory: string, sudo?: boolean}} options
|
|
117
|
+
* @returns {string[]}
|
|
118
|
+
*/
|
|
119
|
+
static selinuxSshContextCommandsFactory({ sshDirectory, sudo = true } = {}) {
|
|
120
|
+
if (!sshDirectory) throw new TypeError('selinuxSshContextCommandsFactory requires sshDirectory');
|
|
121
|
+
const prefix = sudo ? 'sudo ' : '';
|
|
122
|
+
const standard = sshDirectory === '/root/.ssh' || /^\/home\/[^/]+\/\.ssh$/.test(sshDirectory);
|
|
123
|
+
const commands = [];
|
|
124
|
+
if (!standard) {
|
|
125
|
+
const expression = SELinuxService.shellArgumentFactory(`${sshDirectory}(/.*)?`);
|
|
126
|
+
commands.push(
|
|
127
|
+
`if command -v selinuxenabled >/dev/null 2>&1 && selinuxenabled; then command -v semanage >/dev/null 2>&1 || { echo 'semanage is required for a custom SSH home' >&2; exit 1; }; ${prefix}semanage fcontext -a -t ssh_home_t ${expression} 2>/dev/null || ${prefix}semanage fcontext -m -t ssh_home_t ${expression}; fi`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
commands.push(SELinuxService.selinuxRestoreconCommandFactory(sshDirectory, { sudo }));
|
|
131
|
+
return commands;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Builds an idempotent ssh_port_t assignment for a custom SSH port.
|
|
136
|
+
* @param {{port?: number|string, sudo?: boolean}} [options]
|
|
137
|
+
* @returns {string[]}
|
|
138
|
+
*/
|
|
139
|
+
static selinuxSshPortCommandsFactory({ port = 22, sudo = true } = {}) {
|
|
140
|
+
const value = Number(port);
|
|
141
|
+
if (!Number.isInteger(value) || value < 1 || value > 65535) throw new RangeError('SSH port must be 1-65535');
|
|
142
|
+
if (value === 22) return [];
|
|
143
|
+
const prefix = sudo ? 'sudo ' : '';
|
|
144
|
+
return [
|
|
145
|
+
`if command -v selinuxenabled >/dev/null 2>&1 && selinuxenabled; then command -v semanage >/dev/null 2>&1 || { echo 'semanage is required for a custom SSH port' >&2; exit 1; }; ${prefix}semanage port -a -t ssh_port_t -p tcp ${value} 2>/dev/null || ${prefix}semanage port -m -t ssh_port_t -p tcp ${value}; fi`,
|
|
146
|
+
];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Executes a generated command list.
|
|
151
|
+
* @param {string[]} [commands]
|
|
152
|
+
* @param {{execute: Function}} options
|
|
153
|
+
* @returns {*[]}
|
|
154
|
+
*/
|
|
155
|
+
static runSELinuxCommands(commands = [], { execute } = {}) {
|
|
156
|
+
if (typeof execute !== 'function') throw new TypeError('runSELinuxCommands requires an executor');
|
|
157
|
+
return commands.map((command) => execute(command));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const {
|
|
162
|
+
runSELinuxCommands,
|
|
163
|
+
selinuxContainerSharedContextCommandsFactory,
|
|
164
|
+
selinuxEnforcingCommandsFactory,
|
|
165
|
+
selinuxFileContextCommandFactory,
|
|
166
|
+
selinuxPackagesCommandFactory,
|
|
167
|
+
selinuxRestoreconCommandFactory,
|
|
168
|
+
selinuxSshContextCommandsFactory,
|
|
169
|
+
selinuxSshPortCommandsFactory,
|
|
170
|
+
shellArgumentFactory,
|
|
171
|
+
} = SELinuxService;
|
|
172
|
+
|
|
173
|
+
export default SELinuxService;
|
|
174
|
+
|
|
175
|
+
export {
|
|
176
|
+
runSELinuxCommands,
|
|
177
|
+
selinuxContainerSharedContextCommandsFactory,
|
|
178
|
+
selinuxEnforcingCommandsFactory,
|
|
179
|
+
selinuxFileContextCommandFactory,
|
|
180
|
+
selinuxPackagesCommandFactory,
|
|
181
|
+
selinuxRestoreconCommandFactory,
|
|
182
|
+
selinuxSshContextCommandsFactory,
|
|
183
|
+
selinuxSshPortCommandsFactory,
|
|
184
|
+
shellArgumentFactory,
|
|
185
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* General-purpose systemd unit rendering and service lifecycle helpers.
|
|
3
|
+
*
|
|
4
|
+
* Command construction is deterministic and separate from execution. Callers
|
|
5
|
+
* can execute commands explicitly or pass a list to {@link runSystemdCommands}.
|
|
6
|
+
*
|
|
7
|
+
* @module src/server/systemd.js
|
|
8
|
+
* @namespace SystemdService
|
|
9
|
+
*/
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Main systemd service utility.
|
|
14
|
+
* @class SystemdService
|
|
15
|
+
* @memberof SystemdService
|
|
16
|
+
*/
|
|
17
|
+
class SystemdService {
|
|
18
|
+
static #valuesFactory(value) {
|
|
19
|
+
return Array.isArray(value) ? value : [value];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static #sectionFactory(name, directives = {}) {
|
|
23
|
+
const lines = Object.entries(directives).flatMap(([directive, value]) =>
|
|
24
|
+
SystemdService.#valuesFactory(value)
|
|
25
|
+
.filter((entry) => entry !== undefined && entry !== null && `${entry}` !== '')
|
|
26
|
+
.map((entry) => `${directive}=${entry}`),
|
|
27
|
+
);
|
|
28
|
+
return lines.length > 0 ? [`[${name}]`, ...lines].join('\n') : '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static #daemonReloadCommandFactory({ sudo = true } = {}) {
|
|
32
|
+
return SystemdService.systemctlCommandFactory({ action: 'daemon-reload', sudo });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Checks whether a path is inside a user home directory.
|
|
37
|
+
* @param {string} path - Candidate path.
|
|
38
|
+
* @returns {boolean}
|
|
39
|
+
*/
|
|
40
|
+
static homeDirectoryPathFactory(path) {
|
|
41
|
+
return /^\/root(\/|$)|^\/home\//.test(`${path || ''}`.trim());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Renders a systemd unit from named sections and directives.
|
|
46
|
+
* @param {{header?: string, sections?: Object<string, Object<string, *>>}} [options]
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
static systemdUnitFactory({ header = '', sections = {} } = {}) {
|
|
50
|
+
const rendered = Object.entries(sections)
|
|
51
|
+
.map(([name, directives]) => SystemdService.#sectionFactory(name, directives))
|
|
52
|
+
.filter(Boolean);
|
|
53
|
+
return [...(`${header}`.trim() ? [`${header}`.trim()] : []), ...rendered].join('\n\n') + '\n';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Builds a systemctl command.
|
|
58
|
+
* @param {{action?: string, name?: string, sudo?: boolean, stderr?: boolean, allowFailure?: boolean}} [options]
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
static systemctlCommandFactory({ action, name = '', sudo = true, stderr = false, allowFailure = false } = {}) {
|
|
62
|
+
return [
|
|
63
|
+
sudo ? 'sudo' : '',
|
|
64
|
+
'systemctl',
|
|
65
|
+
`${action || ''}`.trim(),
|
|
66
|
+
`${name || ''}`.trim(),
|
|
67
|
+
stderr ? '2>/dev/null' : '',
|
|
68
|
+
allowFailure ? '|| true' : '',
|
|
69
|
+
]
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
.join(' ');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** @returns {string} Command that checks whether systemd-run is available. */
|
|
75
|
+
static systemdAvailableCommandFactory() {
|
|
76
|
+
return 'command -v systemd-run';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Builds a journalctl command for a service.
|
|
81
|
+
* @param {{name: string, lines?: number, follow?: boolean}} options
|
|
82
|
+
* @returns {string}
|
|
83
|
+
*/
|
|
84
|
+
static journalctlCommandFactory({ name, lines, follow = false } = {}) {
|
|
85
|
+
return ['journalctl', '-u', name, lines ? `-n ${lines}` : '', follow ? '-f' : ''].filter(Boolean).join(' ');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Builds a transient systemd-run command.
|
|
90
|
+
* @param {{command?: string, user?: string, properties?: Object<string, *>, quiet?: boolean, collect?: boolean, wait?: boolean, sudo?: boolean}} [options]
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
93
|
+
static systemdRunCommandFactory({
|
|
94
|
+
command,
|
|
95
|
+
user,
|
|
96
|
+
properties = {},
|
|
97
|
+
quiet = true,
|
|
98
|
+
collect = true,
|
|
99
|
+
wait = true,
|
|
100
|
+
sudo = true,
|
|
101
|
+
} = {}) {
|
|
102
|
+
return [
|
|
103
|
+
sudo ? 'sudo' : '',
|
|
104
|
+
'systemd-run',
|
|
105
|
+
quiet ? '--quiet' : '',
|
|
106
|
+
collect ? '--collect' : '',
|
|
107
|
+
wait ? '--wait' : '',
|
|
108
|
+
user ? `--uid=${user}` : '',
|
|
109
|
+
...Object.entries(properties).map(([name, value]) => `--property=${name}=${value}`),
|
|
110
|
+
`${command || ''}`.trim(),
|
|
111
|
+
]
|
|
112
|
+
.filter(Boolean)
|
|
113
|
+
.join(' ');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Builds convergent ensure and remove command lists for a service.
|
|
118
|
+
* @param {{changed?: boolean, name: string, unitPath: string}} options
|
|
119
|
+
* @returns {{ensure: string[], remove: string[]}}
|
|
120
|
+
*/
|
|
121
|
+
static systemdServiceCommandsFactory({ changed = false, name, unitPath } = {}) {
|
|
122
|
+
return {
|
|
123
|
+
ensure: [
|
|
124
|
+
...(changed ? [SystemdService.#daemonReloadCommandFactory()] : []),
|
|
125
|
+
SystemdService.systemctlCommandFactory({ action: 'enable', name, allowFailure: true }),
|
|
126
|
+
SystemdService.systemctlCommandFactory({
|
|
127
|
+
action: changed ? 'restart' : 'start',
|
|
128
|
+
name,
|
|
129
|
+
allowFailure: true,
|
|
130
|
+
}),
|
|
131
|
+
],
|
|
132
|
+
remove: [
|
|
133
|
+
SystemdService.systemctlCommandFactory({
|
|
134
|
+
action: 'disable --now',
|
|
135
|
+
name,
|
|
136
|
+
stderr: true,
|
|
137
|
+
allowFailure: true,
|
|
138
|
+
}),
|
|
139
|
+
`sudo rm -f ${unitPath}`,
|
|
140
|
+
SystemdService.#daemonReloadCommandFactory(),
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Builds service status and log commands.
|
|
147
|
+
* @param {string} name - Service name.
|
|
148
|
+
* @returns {{active: string, enabled: string, logs: string}}
|
|
149
|
+
*/
|
|
150
|
+
static systemdStatusCommandsFactory(name) {
|
|
151
|
+
return {
|
|
152
|
+
active: SystemdService.systemctlCommandFactory({ action: 'is-active', name, sudo: false }),
|
|
153
|
+
enabled: SystemdService.systemctlCommandFactory({ action: 'is-enabled', name, sudo: false }),
|
|
154
|
+
logs: SystemdService.journalctlCommandFactory({ name }),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Builds a guarded reload command.
|
|
160
|
+
* @param {string} name - Service name.
|
|
161
|
+
* @returns {string}
|
|
162
|
+
*/
|
|
163
|
+
static systemdReloadIfActiveCommandFactory(name) {
|
|
164
|
+
return `sudo sh -c 'systemctl is-active --quiet ${name} && systemctl reload ${name} || true'`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Executes or reports a sequence of generated commands.
|
|
169
|
+
* @param {string[]} [commands]
|
|
170
|
+
* @param {{dryRun?: boolean, execute?: Function, onDryRun?: Function}} [options]
|
|
171
|
+
* @returns {*[]}
|
|
172
|
+
*/
|
|
173
|
+
static runSystemdCommands(commands = [], { dryRun = false, execute, onDryRun = () => {} } = {}) {
|
|
174
|
+
if (!dryRun && typeof execute !== 'function') throw new TypeError('runSystemdCommands requires an executor');
|
|
175
|
+
return commands.map((command) => (dryRun ? onDryRun(command) : execute(command)));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const {
|
|
180
|
+
homeDirectoryPathFactory,
|
|
181
|
+
journalctlCommandFactory,
|
|
182
|
+
runSystemdCommands,
|
|
183
|
+
systemctlCommandFactory,
|
|
184
|
+
systemdAvailableCommandFactory,
|
|
185
|
+
systemdReloadIfActiveCommandFactory,
|
|
186
|
+
systemdRunCommandFactory,
|
|
187
|
+
systemdServiceCommandsFactory,
|
|
188
|
+
systemdStatusCommandsFactory,
|
|
189
|
+
systemdUnitFactory,
|
|
190
|
+
} = SystemdService;
|
|
191
|
+
|
|
192
|
+
export default SystemdService;
|
|
193
|
+
|
|
194
|
+
export {
|
|
195
|
+
homeDirectoryPathFactory,
|
|
196
|
+
journalctlCommandFactory,
|
|
197
|
+
runSystemdCommands,
|
|
198
|
+
systemctlCommandFactory,
|
|
199
|
+
systemdAvailableCommandFactory,
|
|
200
|
+
systemdReloadIfActiveCommandFactory,
|
|
201
|
+
systemdRunCommandFactory,
|
|
202
|
+
systemdServiceCommandsFactory,
|
|
203
|
+
systemdStatusCommandsFactory,
|
|
204
|
+
systemdUnitFactory,
|
|
205
|
+
};
|