@synopackageland/cli 0.17.4 → 0.17.6
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/CHANGELOG.md +19 -1
- package/README.md +27 -1
- package/dist/build-spk/compose-retained-data.d.ts +11 -0
- package/dist/build-spk/compose-retained-data.js +34 -0
- package/dist/build-spk/delete-app-data.d.ts +1 -1
- package/dist/build-spk/delete-app-data.js +1 -1
- package/dist/build-spk/uninstall-wizard.d.ts +2 -0
- package/dist/build-spk/uninstall-wizard.js +2 -1
- package/dist/build-spk/wizard-messages.d.ts +22 -0
- package/dist/build-spk/wizard-messages.js +22 -0
- package/dist/ctl/cli.js +10 -6
- package/dist/ctl/flows.js +1 -1
- package/dist/ctl/service.d.ts +4 -1
- package/dist/ctl/service.js +20 -6
- package/dist/ctl/transfers.d.ts +1 -1
- package/dist/ctl/transfers.js +5 -2
- package/dist/ctl/transport.d.ts +2 -1
- package/dist/ctl/transport.js +7 -3
- package/dist/runtime/compose-data.d.ts +10 -0
- package/dist/runtime/compose-data.js +20 -0
- package/dist/runtime/executor-digests.json +11 -0
- package/dist/runtime/executor-policy.d.ts +25 -0
- package/dist/runtime/executor-policy.js +10 -1
- package/dist/runtime/runtime-budgets.d.ts +24 -0
- package/dist/runtime/runtime-budgets.js +13 -0
- package/dist/standalone/background.d.ts +3 -3
- package/dist/standalone/background.js +12 -11
- package/dist/standalone/compose.d.ts +3 -1
- package/dist/standalone/compose.js +8 -1
- package/dist/standalone/daemon.d.ts +4 -0
- package/dist/standalone/daemon.js +36 -23
- package/dist/standalone/dsm-window.js +2 -1
- package/dist/standalone/identity-gateway.js +9 -1
- package/dist/standalone/inspect-spk.js +122 -35
- package/dist/standalone/lifecycle.js +16 -9
- package/dist/standalone/manifest.d.ts +1 -0
- package/dist/standalone/manifest.js +8 -5
- package/dist/standalone/native-gateway.d.ts +17 -0
- package/dist/standalone/native-gateway.js +115 -0
- package/dist/standalone/resource.d.ts +3 -0
- package/dist/standalone/resource.js +3 -1
- package/dist/standalone/spk.js +14 -3
- package/dist/standalone/uninstall-hook.js +21 -8
- package/dist/standalone/uninstall.d.ts +1 -1
- package/dist/standalone/uninstall.js +2 -2
- package/package.json +2 -2
- package/skills/reference/compose.md +31 -0
- package/skills/reference/create.md +1 -1
- package/skills/reference/lifecycle.md +25 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { nativePortal } from "./portal.js";
|
|
2
|
+
import { nativeGatewaySupported, renderNativeGatewayConfig, renderNativeGatewayPrepare, NATIVE_GATEWAY_CONFIG, gatewayEntryPath } from './native-gateway.js';
|
|
1
3
|
import { inspectPortalInfo } from "../portal/info.js";
|
|
2
4
|
import { isDeepStrictEqual } from "node:util";
|
|
3
5
|
import { inspectApiDescription } from "./api-description-inspect.js";
|
|
@@ -18,7 +20,7 @@ import { familyOfArchToken, NATIVE_ARCH_FAMILIES, } from "../build-spk/dsm-arch.
|
|
|
18
20
|
import { CONTAINER_MIN_DSM, isDsmAtLeast, PORTAL_EXECUTOR_MIN_DSM, PORTAL_MIN_DSM, PORTAL_MIN_DSM_REASON, } from "../build-spk/dsm-version.js";
|
|
19
21
|
import { ELF_MACHINE } from "./binary.js";
|
|
20
22
|
import { CGI_PAYLOAD_PATH, entryLauncherAccess } from "./dsm-window.js";
|
|
21
|
-
import { daemonConfig, readDaemon } from "./daemon.js";
|
|
23
|
+
import { daemonConfig, readDaemon, readLifecycleTimeouts } from "./daemon.js";
|
|
22
24
|
import { ROOT_SCRIPT_HINTS } from "./root-ops.js";
|
|
23
25
|
import { DELETE_STATE_KEY } from "../build-spk/uninstall-wizard.js";
|
|
24
26
|
import { readUninstallRecord, stateItemTexts } from "./uninstall.js";
|
|
@@ -26,7 +28,7 @@ import { BACKGROUND_UNIT_DIR, BACKGROUND_WORKER_PAYLOAD, BRIDGE_WORKER_ARGS, bac
|
|
|
26
28
|
import { renderStandaloneResource } from "./resource.js";
|
|
27
29
|
import { recoverSharedFolder, renderSharedFolderUpgradeWizardFiles, sharedFolderInstallSteps, SHARED_FOLDER_MARKER, } from "./shared-folder.js";
|
|
28
30
|
import { renderInstallWizardFiles, renderNativeInstallWizard, } from "../build-spk/install-wizard.js";
|
|
29
|
-
import { renderDelegateScopeSteps,
|
|
31
|
+
import { renderDelegateScopeSteps, } from "../build-spk/delegate-scope.js";
|
|
30
32
|
import { renderUninstallWizardFiles } from "../build-spk/uninstall-wizard.js";
|
|
31
33
|
import { renderNativePrivilege } from "../build-spk/native-lifecycle.js";
|
|
32
34
|
import { renderUiConfig } from "./dsm-window.js";
|
|
@@ -110,6 +112,10 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
110
112
|
(raw.entry !== undefined &&
|
|
111
113
|
!["admin-only", "app-privilege"].includes(raw.entry)))
|
|
112
114
|
throw new Error();
|
|
115
|
+
if (raw.lifecycleTimeouts !== undefined && (!readLifecycleTimeouts(raw.lifecycleTimeouts, () => { }) || Object.keys(raw.lifecycleTimeouts).length !== 4))
|
|
116
|
+
throw new Error();
|
|
117
|
+
if (raw.operationSeconds !== undefined && (!Number.isInteger(raw.operationSeconds) || raw.operationSeconds < 1 || raw.operationSeconds > 3600))
|
|
118
|
+
throw new Error();
|
|
113
119
|
metadata = raw;
|
|
114
120
|
}
|
|
115
121
|
catch {
|
|
@@ -145,6 +151,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
145
151
|
(!info.os_min_ver || !isDsmAtLeast(info.os_min_ver, CONTAINER_MIN_DSM)))
|
|
146
152
|
findings.push("compose identity_gateway requires DSM >= 7.2.1");
|
|
147
153
|
await checkArch(dir, info, kind, paths.has(CGI_PAYLOAD_PATH), findings);
|
|
154
|
+
const nativeGateway = await recoverNativeGateway(dir, metadata.native_gateway, info, findings);
|
|
148
155
|
const background = compiledKind && paths.has(BACKGROUND_WORKER_PAYLOAD);
|
|
149
156
|
// Before any fixed-lifecycle comparison: those see the SDK steps alone.
|
|
150
157
|
const appScripts = standalone || kind === "container"
|
|
@@ -155,7 +162,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
155
162
|
: undefined;
|
|
156
163
|
// Resource-worker portals use DSM-owned INFO reload, with no executor requirement.
|
|
157
164
|
const portal = standalone
|
|
158
|
-
? await recoverPortal(dir, info, `SYNO.SDS.${info.package}.Instance`, paths, payload, findings)
|
|
165
|
+
? await recoverPortal(dir, info, `SYNO.SDS.${info.package}.Instance`, paths, payload, findings, nativeGateway)
|
|
159
166
|
: undefined;
|
|
160
167
|
if (portal &&
|
|
161
168
|
info.os_min_ver &&
|
|
@@ -163,7 +170,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
163
170
|
!isDsmAtLeast(info.os_min_ver, PORTAL_MIN_DSM)) {
|
|
164
171
|
findings.push(`INFO os_min_ver "${info.os_min_ver}" is below the DSM ${PORTAL_MIN_DSM} a portal needs; ${PORTAL_MIN_DSM_REASON}`);
|
|
165
172
|
}
|
|
166
|
-
checkConf(dir, findings, kind, background, sharedFolder, info.package, portal, combined?.compose);
|
|
173
|
+
checkConf(dir, findings, kind, background, sharedFolder, info.package, portal, combined?.compose, nativeGateway, info.os_min_ver);
|
|
167
174
|
checkScripts(dir, findings);
|
|
168
175
|
checkUninstall(dir, findings);
|
|
169
176
|
if (kind === "container")
|
|
@@ -191,10 +198,10 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
191
198
|
if (standalone && portal)
|
|
192
199
|
findings.push(...inspectPortalInfo(info, hasExecutor));
|
|
193
200
|
const daemon = standalone
|
|
194
|
-
? await checkDaemon(dir, info, paths, payload, findings, hasExecutor, background, combined)
|
|
201
|
+
? await checkDaemon(dir, info, paths, payload, findings, hasExecutor, background, combined, nativeGateway)
|
|
195
202
|
: undefined;
|
|
196
203
|
if (background)
|
|
197
|
-
checkBackground(dir, info, payload, findings, hasExecutor, daemon, kind !== "native-go");
|
|
204
|
+
checkBackground(dir, info, payload, findings, hasExecutor, daemon, kind !== "native-go", metadata.lifecycleTimeouts);
|
|
198
205
|
let executor;
|
|
199
206
|
if ((metadata.capabilities ?? []).some((name) => name === "rootOps" || name === "delegate" || name === "webapi") ||
|
|
200
207
|
paths.has("executor/executor")) {
|
|
@@ -224,6 +231,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
224
231
|
if (![...paths].some((path) => path.startsWith("daemon/"))) {
|
|
225
232
|
const lifecycleManifest = {
|
|
226
233
|
package: info.package,
|
|
234
|
+
lifecycleTimeouts: metadata.lifecycleTimeouts,
|
|
227
235
|
capabilities: {
|
|
228
236
|
rootOps: true,
|
|
229
237
|
...(background ? { jobs: { worker: "sha256-v1" } } : {}),
|
|
@@ -261,7 +269,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
261
269
|
? await inspectLoginStyle(dir, info.package, Boolean(portal || executor?.policy.portal), findings)
|
|
262
270
|
: undefined;
|
|
263
271
|
if (standalone)
|
|
264
|
-
checkSharedFolder(dir, findings, info.package, sharedFolder, executor?.policy.scope === "install-selected", portal, hasExecutor);
|
|
272
|
+
checkSharedFolder(dir, findings, info.package, sharedFolder, executor?.policy.scope === "install-selected", portal, hasExecutor, nativeGateway, info.os_min_ver);
|
|
265
273
|
const icon = inspectPackageIcons(dir, findings);
|
|
266
274
|
const cgiEntry = payload.find((entry) => entry.path === CGI_PAYLOAD_PATH);
|
|
267
275
|
if (standalone && !cgiEntry) {
|
|
@@ -622,7 +630,8 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
|
|
|
622
630
|
os_min_ver: info.os_min_ver,
|
|
623
631
|
build: Number(info.version?.split("-").pop()),
|
|
624
632
|
arches: [arch],
|
|
625
|
-
backend: { kind: "binary", files: {} },
|
|
633
|
+
backend: { kind: "binary", files: {}, ...(metadata.operationSeconds ? { timeoutSeconds: metadata.operationSeconds } : {}) },
|
|
634
|
+
lifecycleTimeouts: metadata.lifecycleTimeouts,
|
|
626
635
|
ui: { dir: root, launch: metadata.launch },
|
|
627
636
|
capabilities: {
|
|
628
637
|
...(metadata.capabilities?.includes("composeMounts")
|
|
@@ -635,7 +644,7 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
|
|
|
635
644
|
};
|
|
636
645
|
if (Boolean(c.web) !== Boolean(g))
|
|
637
646
|
throw new Error("Web ingress must have a complete identity gateway");
|
|
638
|
-
if (gateway && gateway
|
|
647
|
+
if (gateway && !matchesGatewayConfig(gateway, manifest))
|
|
639
648
|
throw new Error("gateway configuration differs from the declared package contract");
|
|
640
649
|
if (readOptional(join(dir, "conf/privilege")) !== renderNativePrivilege())
|
|
641
650
|
throw new Error("Compose hooks must run as the package account");
|
|
@@ -648,8 +657,15 @@ async function inspectUnifiedCompose(dir, payload, metadata, findings) {
|
|
|
648
657
|
throw new Error("LAUNCHER_TYPE_MISMATCH classless Compose must use the shared url launcher");
|
|
649
658
|
if (g &&
|
|
650
659
|
(cfg.package !== info.package ||
|
|
651
|
-
info.adminurl !==
|
|
660
|
+
info.adminurl !== gatewayEntryPath(manifest)))
|
|
652
661
|
throw new Error("gateway entry mismatch");
|
|
662
|
+
for (const [name, text] of Object.entries(renderUninstallWizardFiles(info.package, "native", {
|
|
663
|
+
composeData: c.plan.app_data,
|
|
664
|
+
portal: Boolean(nativePortal(manifest)),
|
|
665
|
+
}))) {
|
|
666
|
+
if (readOptional(join(dir, "WIZARD_UIFILES", name)) !== text)
|
|
667
|
+
throw new Error(`unexpected WIZARD_UIFILES/${name}; retained-volume guidance must match the plan`);
|
|
668
|
+
}
|
|
653
669
|
const appScripts = unwrapAppScripts(join(dir, "scripts"), findings);
|
|
654
670
|
manifest.appScripts = undefined;
|
|
655
671
|
for (const hook of [
|
|
@@ -771,7 +787,7 @@ async function recoverCompose(dir, value, findings) {
|
|
|
771
787
|
return undefined;
|
|
772
788
|
}
|
|
773
789
|
}
|
|
774
|
-
async function checkDaemon(dir, info, paths, payload, findings, executor, background, combined) {
|
|
790
|
+
async function checkDaemon(dir, info, paths, payload, findings, executor, background, combined, nativeGateway) {
|
|
775
791
|
if (![...paths].some((path) => path.startsWith("daemon/")))
|
|
776
792
|
return;
|
|
777
793
|
if (info.precheckstartstop !== "yes")
|
|
@@ -796,6 +812,7 @@ async function checkDaemon(dir, info, paths, payload, findings, executor, backgr
|
|
|
796
812
|
"args",
|
|
797
813
|
"env",
|
|
798
814
|
"timeouts",
|
|
815
|
+
"operation_seconds",
|
|
799
816
|
"uninstall_args",
|
|
800
817
|
...(combined ? ["network"] : []),
|
|
801
818
|
].includes(key)))
|
|
@@ -810,6 +827,8 @@ async function checkDaemon(dir, info, paths, payload, findings, executor, backgr
|
|
|
810
827
|
}, dir, ["x86_64"], () => { });
|
|
811
828
|
if (!daemon)
|
|
812
829
|
throw new Error("invalid config");
|
|
830
|
+
if (config.operation_seconds !== undefined && (!Number.isInteger(config.operation_seconds) || config.operation_seconds < 1 || config.operation_seconds > 3600))
|
|
831
|
+
throw new Error('invalid operation budget');
|
|
813
832
|
if (combined &&
|
|
814
833
|
(daemon.exec !== "_sso/gateway" ||
|
|
815
834
|
JSON.stringify(daemon.args) !== '["run"]' ||
|
|
@@ -820,6 +839,9 @@ async function checkDaemon(dir, info, paths, payload, findings, executor, backgr
|
|
|
820
839
|
throw new Error("missing executable");
|
|
821
840
|
const manifest = {
|
|
822
841
|
package: info.package,
|
|
842
|
+
backend: { kind: 'go', dir: dir, main: './cmd/app', timeoutSeconds: config.operation_seconds ?? 60 },
|
|
843
|
+
root: dir,
|
|
844
|
+
...(combined ? { lifecycleTimeouts: daemon.timeouts } : {}),
|
|
823
845
|
daemon,
|
|
824
846
|
...combined,
|
|
825
847
|
capabilities: {
|
|
@@ -841,11 +863,14 @@ async function checkDaemon(dir, info, paths, payload, findings, executor, backgr
|
|
|
841
863
|
},
|
|
842
864
|
}
|
|
843
865
|
: manifest);
|
|
844
|
-
|
|
866
|
+
const expectedConfig = JSON.parse(canonical);
|
|
867
|
+
if (config.operation_seconds === undefined)
|
|
868
|
+
delete expectedConfig.operation_seconds;
|
|
869
|
+
if (readFileSync(join(unpacked, "daemon/config.json"), "utf8") !== JSON.stringify(expectedConfig) + '\n')
|
|
845
870
|
throw new Error("non-canonical daemon config");
|
|
846
871
|
for (const hook of ["start-stop-status", "preupgrade", "preuninst"]) {
|
|
847
872
|
if (readFileSync(join(dir, "scripts", hook), "utf8") !==
|
|
848
|
-
renderStandaloneScript(hook, manifest))
|
|
873
|
+
renderStandaloneScript(hook, nativeGateway ? { ...manifest, os_min_ver: info.os_min_ver, identityGateway: nativeGateway } : manifest))
|
|
849
874
|
findings.push(`daemon scripts/${hook} does not match the fixed lifecycle`);
|
|
850
875
|
}
|
|
851
876
|
return daemon;
|
|
@@ -917,12 +942,50 @@ async function checkContainerExposure(dir, packageTgz, findings) {
|
|
|
917
942
|
function readOptional(path) {
|
|
918
943
|
return existsSync(path) ? readFileSync(path, "utf8") : undefined;
|
|
919
944
|
}
|
|
920
|
-
function
|
|
945
|
+
async function recoverNativeGateway(dir, value, info, findings) {
|
|
946
|
+
if (value === undefined)
|
|
947
|
+
return undefined;
|
|
948
|
+
try {
|
|
949
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
950
|
+
throw new Error('invalid native gateway record');
|
|
951
|
+
const plan = value;
|
|
952
|
+
if (typeof plan.sdk !== 'string' || !plan.listen || typeof plan.listen !== 'object' || Array.isArray(plan.listen)
|
|
953
|
+
|| !Object.keys(plan.listen).length || Object.keys(plan).some(k => !['sdk', 'listen'].includes(k))
|
|
954
|
+
|| Object.entries(plan.listen).some(([k, v]) => !['http', 'https'].includes(k) || !Number.isInteger(v) || v < 1 || v > 65535))
|
|
955
|
+
throw new Error('invalid native gateway ports');
|
|
956
|
+
const target = { package: info.package, os_min_ver: info.os_min_ver, identityGateway: plan };
|
|
957
|
+
if (!nativeGatewaySupported(target))
|
|
958
|
+
throw new Error('unsupported native gateway SDK/DSM floor');
|
|
959
|
+
const root = join(dir, 'native-gateway-inspection');
|
|
960
|
+
mkdirSync(root);
|
|
961
|
+
await tar.x({ file: join(dir, 'package.tgz'), cwd: root, filter: path => ['sso/gateway.json', NATIVE_GATEWAY_CONFIG].includes(path.replace(/^\.\//, '')) });
|
|
962
|
+
const config = JSON.parse(readFileSync(join(root, 'sso/gateway.json'), 'utf8'));
|
|
963
|
+
if (config.native_ingress !== true || config.package !== info.package || !isDeepStrictEqual(config.listen, plan.listen)
|
|
964
|
+
|| config.state_dir !== `/var/packages/${info.package}/var/sso`)
|
|
965
|
+
throw new Error('native gateway config does not match its package');
|
|
966
|
+
if (readOptional(join(root, NATIVE_GATEWAY_CONFIG)) !== renderNativeGatewayConfig(target))
|
|
967
|
+
throw new Error('native gateway ingress differs from its fixed proxy');
|
|
968
|
+
for (const key of ['instuninst_restart_services', 'startstop_restart_services'])
|
|
969
|
+
if (!(info[key] ?? '').split(/\s+/).includes('nginx.service'))
|
|
970
|
+
throw new Error(`native gateway requires INFO ${key}`);
|
|
971
|
+
for (const hook of ['postinst', 'postupgrade'])
|
|
972
|
+
if (!readFileSync(join(dir, 'scripts', hook), 'utf8').includes(renderNativeGatewayPrepare(target)))
|
|
973
|
+
throw new Error('native gateway credential preparation is missing');
|
|
974
|
+
return plan;
|
|
975
|
+
}
|
|
976
|
+
catch (e) {
|
|
977
|
+
findings.push(`NATIVE_GATEWAY_INVALID ${e.message}`);
|
|
978
|
+
return undefined;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
function checkConf(dir, findings, kind, background, sharedFolder, pkg, portal, compose, nativeGateway, osMinVer) {
|
|
921
982
|
const confDir = join(dir, "conf");
|
|
922
983
|
const entries = existsSync(confDir) ? readdirSync(confDir) : [];
|
|
923
984
|
const nativeResource = kind !== "container"
|
|
924
985
|
? renderStandaloneResource({
|
|
925
986
|
package: pkg ?? "",
|
|
987
|
+
os_min_ver: osMinVer,
|
|
988
|
+
...(nativeGateway ? { identityGateway: nativeGateway } : {}),
|
|
926
989
|
...(compose ? { compose } : {}),
|
|
927
990
|
capabilities: background
|
|
928
991
|
? { jobs: { worker: "sha256-v1" } }
|
|
@@ -979,7 +1042,7 @@ function checkConf(dir, findings, kind, background, sharedFolder, pkg, portal, c
|
|
|
979
1042
|
}
|
|
980
1043
|
}
|
|
981
1044
|
/** The opt-in package user unit must be exactly what the generator writes. */
|
|
982
|
-
function checkBackground(dir, info, payload, findings, executor, daemon, bridge = false) {
|
|
1045
|
+
function checkBackground(dir, info, payload, findings, executor, daemon, bridge = false, lifecycleTimeouts) {
|
|
983
1046
|
const pkg = info.package;
|
|
984
1047
|
if (!pkg)
|
|
985
1048
|
return;
|
|
@@ -997,12 +1060,13 @@ function checkBackground(dir, info, payload, findings, executor, daemon, bridge
|
|
|
997
1060
|
if (units.length !== 1 ||
|
|
998
1061
|
units[0] !== unit ||
|
|
999
1062
|
read(`${BACKGROUND_UNIT_DIR}/${unit}`) !==
|
|
1000
|
-
renderBackgroundUnit(pkg, bridge ? BRIDGE_WORKER_ARGS : [])) {
|
|
1063
|
+
renderBackgroundUnit(pkg, bridge ? BRIDGE_WORKER_ARGS : [], daemon?.timeouts.stop ?? lifecycleTimeouts?.stop ?? 30)) {
|
|
1001
1064
|
findings.push(`${BACKGROUND_UNIT_DIR} must contain only the fixed ${unit} (runs as the package account, no User=, no root)`);
|
|
1002
1065
|
}
|
|
1003
1066
|
if (read("scripts/start-stop-status") !==
|
|
1004
1067
|
renderStandaloneScript("start-stop-status", {
|
|
1005
1068
|
package: pkg,
|
|
1069
|
+
lifecycleTimeouts,
|
|
1006
1070
|
...(daemon ? { daemon } : {}),
|
|
1007
1071
|
capabilities: {
|
|
1008
1072
|
jobs: { worker: "sha256-v1" },
|
|
@@ -1017,7 +1081,7 @@ function checkBackground(dir, info, payload, findings, executor, daemon, bridge
|
|
|
1017
1081
|
* declaration (without one, any present file must be the default native
|
|
1018
1082
|
* install wizard), and no other install/upgrade wizard file may exist.
|
|
1019
1083
|
*/
|
|
1020
|
-
function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal, executor = false) {
|
|
1084
|
+
function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal, executor = false, nativeGateway, osMinVer) {
|
|
1021
1085
|
const wizardDir = join(dir, "WIZARD_UIFILES");
|
|
1022
1086
|
const present = (existsSync(wizardDir) ? readdirSync(wizardDir) : [])
|
|
1023
1087
|
.filter((name) => /^(install|upgrade)_uifile(_|\.|$)/.test(name))
|
|
@@ -1027,6 +1091,22 @@ function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal,
|
|
|
1027
1091
|
findings.push("the install.sharedFolder declaration in scripts/postinst is invalid");
|
|
1028
1092
|
if (!pkg)
|
|
1029
1093
|
return;
|
|
1094
|
+
// App wrappers have already been validated and unwrapped. Reconstruct the
|
|
1095
|
+
// remaining install-hook inputs from INFO, the payload and its build record.
|
|
1096
|
+
const lifecycleManifest = {
|
|
1097
|
+
package: pkg,
|
|
1098
|
+
os_min_ver: osMinVer,
|
|
1099
|
+
sharedFolder: folder,
|
|
1100
|
+
capabilities: selected
|
|
1101
|
+
? { delegate: { scope: "install-selected" } }
|
|
1102
|
+
: executor
|
|
1103
|
+
? { rootOps: true }
|
|
1104
|
+
: {},
|
|
1105
|
+
...(nativeGateway
|
|
1106
|
+
? { identityGateway: nativeGateway }
|
|
1107
|
+
: {}),
|
|
1108
|
+
...portalManifest(portal),
|
|
1109
|
+
};
|
|
1030
1110
|
const wizards = expectedWizardFiles(pkg, folder, selected, portal);
|
|
1031
1111
|
const source = selected
|
|
1032
1112
|
? "the install-selected policy"
|
|
@@ -1046,25 +1126,17 @@ function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal,
|
|
|
1046
1126
|
findings.push(`WIZARD_UIFILES/${name} is not a wizard file produced by ${source}`);
|
|
1047
1127
|
if (selected) {
|
|
1048
1128
|
for (const hook of ["preinst", "postinst"]) {
|
|
1049
|
-
if (
|
|
1050
|
-
|
|
1129
|
+
if (readOptional(join(dir, "scripts", hook)) !==
|
|
1130
|
+
renderStandaloneScript(hook, lifecycleManifest))
|
|
1131
|
+
findings.push(`scripts/${hook} does not match the fixed install-selected draft handling`);
|
|
1051
1132
|
}
|
|
1052
1133
|
}
|
|
1053
|
-
if (!folder)
|
|
1134
|
+
if (!folder && !portal)
|
|
1054
1135
|
return;
|
|
1055
1136
|
for (const hook of ["postinst", "postupgrade"]) {
|
|
1056
1137
|
if (readOptional(join(dir, "scripts", hook)) !==
|
|
1057
|
-
renderStandaloneScript(hook, {
|
|
1058
|
-
|
|
1059
|
-
sharedFolder: folder,
|
|
1060
|
-
...(selected
|
|
1061
|
-
? { capabilities: { delegate: { scope: "install-selected" } } }
|
|
1062
|
-
: executor
|
|
1063
|
-
? { capabilities: { rootOps: true } }
|
|
1064
|
-
: {}),
|
|
1065
|
-
...portalManifest(portal),
|
|
1066
|
-
})) {
|
|
1067
|
-
findings.push(`scripts/${hook} does not match the fixed script declared by install.sharedFolder`);
|
|
1138
|
+
renderStandaloneScript(hook, lifecycleManifest)) {
|
|
1139
|
+
findings.push(`scripts/${hook} does not match the fixed script declared by ${folder ? "install.sharedFolder" : "portal"}`);
|
|
1068
1140
|
}
|
|
1069
1141
|
}
|
|
1070
1142
|
}
|
|
@@ -1096,7 +1168,7 @@ function portalManifest(portal) {
|
|
|
1096
1168
|
* files and alias lifecycle must be exactly what the generator writes for the
|
|
1097
1169
|
* package's own DSM window (native AppInstance or Compose `.url` entry); activation is never part of the package.
|
|
1098
1170
|
*/
|
|
1099
|
-
async function recoverPortal(dir, info, appId, paths, payload, findings) {
|
|
1171
|
+
async function recoverPortal(dir, info, appId, paths, payload, findings, nativeGateway) {
|
|
1100
1172
|
const files = [PORTAL_JSON_RELPATH, PORTAL_MUSTACHE_RELPATH];
|
|
1101
1173
|
if (!files.some((path) => paths.has(path)))
|
|
1102
1174
|
return;
|
|
@@ -1120,7 +1192,14 @@ async function recoverPortal(dir, info, appId, paths, payload, findings) {
|
|
|
1120
1192
|
renderPortalMustache())
|
|
1121
1193
|
throw new Error("content");
|
|
1122
1194
|
for (const hook of ["preinst", "postinst", "postupgrade"]) {
|
|
1123
|
-
|
|
1195
|
+
const prepare = ["postinst", "postupgrade"].includes(hook)
|
|
1196
|
+
? renderNativeGatewayPrepare({
|
|
1197
|
+
package: info.package,
|
|
1198
|
+
os_min_ver: info.os_min_ver,
|
|
1199
|
+
identityGateway: nativeGateway,
|
|
1200
|
+
})
|
|
1201
|
+
: "";
|
|
1202
|
+
if (!readOptional(join(dir, "scripts", hook))?.startsWith(`#!/bin/sh\n${prepare}${renderPortalLifecycle(hook, portal)}`))
|
|
1124
1203
|
throw new Error(hook);
|
|
1125
1204
|
}
|
|
1126
1205
|
return portal;
|
|
@@ -1149,7 +1228,7 @@ function checkScripts(dir, findings) {
|
|
|
1149
1228
|
}
|
|
1150
1229
|
const DATA_KEYS = ["pkgwizard_remove_additional", "delete_app_data"];
|
|
1151
1230
|
const DELETE_COMMAND = /\brm\s+-[a-zA-Z]*[rR]|\bfind\b[^\n]*(-delete|-exec\s+rm)/;
|
|
1152
|
-
const UPGRADE_GUARD = /SYNOPKG_PKG_STATUS[^\n]
|
|
1231
|
+
const UPGRADE_GUARD = /SYNOPKG_PKG_STATUS[^\n]*(?:!=\s*"?UPGRADE"?|=\s*"?UNINSTALL"?)/;
|
|
1153
1232
|
function uninstallBoxes(file, keys) {
|
|
1154
1233
|
try {
|
|
1155
1234
|
const steps = JSON.parse(readFileSync(file, "utf8"));
|
|
@@ -1206,7 +1285,7 @@ function checkUninstall(dir, findings) {
|
|
|
1206
1285
|
const deletes = DELETE_COMMAND.test(body);
|
|
1207
1286
|
if (deletes) {
|
|
1208
1287
|
if (!UPGRADE_GUARD.test(body))
|
|
1209
|
-
findings.push('UNINSTALL_NO_UPGRADE_GUARD scripts/postuninst deletes data without
|
|
1288
|
+
findings.push('UNINSTALL_NO_UPGRADE_GUARD scripts/postuninst deletes data without an UNINSTALL-only or != UPGRADE guard, so an upgrade deletes it by mistake');
|
|
1210
1289
|
if (!/is_package_area\(\)\s*\{/.test(body) ||
|
|
1211
1290
|
!/if ! is_package_area "\$real"/.test(body))
|
|
1212
1291
|
findings.push("UNINSTALL_UNSAFE_DELETE scripts/postuninst does not verify the deleted paths are under /volumeN/@appdata|@apphome|@appconf|@appstore/<package>; an empty or redirected variable deletes the wrong data");
|
|
@@ -1265,3 +1344,11 @@ export async function listPayload(packageTgz, findings) {
|
|
|
1265
1344
|
});
|
|
1266
1345
|
return payload;
|
|
1267
1346
|
}
|
|
1347
|
+
function matchesGatewayConfig(actual, manifest) {
|
|
1348
|
+
const saved = JSON.parse(actual), expected = JSON.parse(identityGatewayConfig(manifest));
|
|
1349
|
+
// Older artifacts omit both work-budget members; do not reinterpret their bytes.
|
|
1350
|
+
for (const key of ['operation_seconds', 'stop_seconds'])
|
|
1351
|
+
if (saved[key] === undefined)
|
|
1352
|
+
delete expected[key];
|
|
1353
|
+
return actual === JSON.stringify(expected) + '\n';
|
|
1354
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { renderComposeRetainedData } from "../build-spk/compose-retained-data.js";
|
|
2
|
+
import { renderNativeGatewayPrepare } from './native-gateway.js';
|
|
1
3
|
import { renderComposeDiagnosis } from "../build-spk/compose-diagnosis.js";
|
|
2
4
|
import { composeDaemon, renderComposeInstall } from "./compose.js";
|
|
3
5
|
import { renderBackgroundControl, renderBackgroundWorkerStart, renderBackgroundWorkerStop, renderUserManagerRecovery, backgroundUnitName, } from "./background.js";
|
|
@@ -11,6 +13,14 @@ import { composeAppScript } from "./app-scripts.js";
|
|
|
11
13
|
const UNINSTALL_ONLY = '[ "${SYNOPKG_PKG_STATUS:-}" = UPGRADE ] || synopkgland_uninstall_hook\n';
|
|
12
14
|
/** Package-account lifecycle hooks for CGI, daemon and background packages. */
|
|
13
15
|
export function renderStandaloneScript(name, manifest) {
|
|
16
|
+
const script = renderStandaloneScriptInternal(name, manifest);
|
|
17
|
+
const prepared = ['postinst', 'postupgrade'].includes(name) ? renderNativeGatewayPrepare(manifest) : '';
|
|
18
|
+
const sdk = prepared ? script.replace('#!/bin/sh\n', '#!/bin/sh\n' + prepared) : script;
|
|
19
|
+
return manifest.appScripts?.some((app) => app === name)
|
|
20
|
+
? composeAppScript(name, sdk)
|
|
21
|
+
: sdk;
|
|
22
|
+
}
|
|
23
|
+
function renderStandaloneScriptInternal(name, manifest) {
|
|
14
24
|
if (manifest.compose) {
|
|
15
25
|
const native = {
|
|
16
26
|
...manifest,
|
|
@@ -19,7 +29,7 @@ export function renderStandaloneScript(name, manifest) {
|
|
|
19
29
|
};
|
|
20
30
|
const body = name === "preinst"
|
|
21
31
|
? renderComposeInstall(manifest.package)
|
|
22
|
-
:
|
|
32
|
+
: renderStandaloneScriptInternal(name, { ...native, appScripts: undefined });
|
|
23
33
|
const control = `"/var/packages/${manifest.package}/target/daemon/control"`;
|
|
24
34
|
const paths = `"\${SYNOPKG_PKGDEST:-/var/packages/${manifest.package}/target}" "\${SYNOPKG_PKGVAR:-/var/packages/${manifest.package}/var}"`;
|
|
25
35
|
const preflight = name === "start-stop-status"
|
|
@@ -37,6 +47,7 @@ export function renderStandaloneScript(name, manifest) {
|
|
|
37
47
|
? `if [ "$1" = prestart ]; then\n ${renderComposeDiagnosis(project, "prestart")}fi\n`
|
|
38
48
|
: "";
|
|
39
49
|
const ledger = diagnosis + preflight + remember +
|
|
50
|
+
(name === "postuninst" ? renderComposeRetainedData(manifest.compose.plan.app_data) : "") +
|
|
40
51
|
(manifest.identityGateway && !manifest.compose.plan.mounts_blocked
|
|
41
52
|
? name === "start-stop-status"
|
|
42
53
|
? `if [ "$1" = restart ]; then\n "$0" stop || exit $?\n exec "$0" start\nfi\nif [ "$1" = start ]; then\n ${control} acquire ${paths} || exit $?\nelif [ "$1" = stop ]; then\n ${control} release ${paths} || exit $?\nfi\n`
|
|
@@ -47,18 +58,14 @@ export function renderStandaloneScript(name, manifest) {
|
|
|
47
58
|
const withLedger = ledger
|
|
48
59
|
? body.replace("#!/bin/sh\n", "#!/bin/sh\n" + ledger)
|
|
49
60
|
: body;
|
|
50
|
-
return
|
|
51
|
-
? composeAppScript(name, withLedger)
|
|
52
|
-
: withLedger;
|
|
61
|
+
return withLedger;
|
|
53
62
|
}
|
|
54
63
|
const portal = nativePortal(manifest);
|
|
55
64
|
const script = renderCapabilityScript(name, manifest);
|
|
56
65
|
const sdk = portal
|
|
57
66
|
? script.replace("#!/bin/sh\n", () => `#!/bin/sh\n${renderPortalLifecycle(name, portal)}`)
|
|
58
67
|
: script;
|
|
59
|
-
return
|
|
60
|
-
? composeAppScript(name, sdk)
|
|
61
|
-
: sdk;
|
|
68
|
+
return sdk;
|
|
62
69
|
}
|
|
63
70
|
function renderCapabilityScript(name, manifest) {
|
|
64
71
|
if (name === "preupgrade" &&
|
|
@@ -159,7 +166,7 @@ function renderStandaloneScriptBody(name, manifest) {
|
|
|
159
166
|
if (manifest.daemon)
|
|
160
167
|
return renderDaemonScript(name, manifest);
|
|
161
168
|
if (manifest.capabilities.jobs && name === "start-stop-status")
|
|
162
|
-
return renderBackgroundControl(manifest.package);
|
|
169
|
+
return renderBackgroundControl(manifest.package, manifest.lifecycleTimeouts?.start ?? 30);
|
|
163
170
|
if (name === "start-stop-status") {
|
|
164
171
|
return `#!/bin/sh
|
|
165
172
|
case "$1" in
|
|
@@ -191,7 +198,7 @@ function renderDelegatedDaemonScript(name, manifest) {
|
|
|
191
198
|
return `#!/bin/sh\n${workerStop}${renderUninstallHook(manifest)}if [ "\${SYNOPKG_PKG_STATUS:-}" = UPGRADE ]; then\n${stop("stop", "stop")}fi\n${purge}`;
|
|
192
199
|
}
|
|
193
200
|
return `#!/bin/sh
|
|
194
|
-
${workerStop}${renderUserManagerRecovery()}${renderBackgroundWorkerStart(pkg)}case "$1" in
|
|
201
|
+
${workerStop}${renderUserManagerRecovery(manifest.daemon?.timeouts?.start ?? manifest.lifecycleTimeouts?.start ?? 30)}${renderBackgroundWorkerStart(pkg)}case "$1" in
|
|
195
202
|
prestart) recover_user_manager ;;
|
|
196
203
|
prestop) exit 0 ;;
|
|
197
204
|
start)
|
|
@@ -86,6 +86,7 @@ export interface StandaloneManifest {
|
|
|
86
86
|
tasks: ExecutorTask[];
|
|
87
87
|
};
|
|
88
88
|
daemon?: Daemon;
|
|
89
|
+
lifecycleTimeouts?: Daemon["timeouts"];
|
|
89
90
|
/** `identity_gateway`: signs DSM users in to the daemon's own web UI. */
|
|
90
91
|
identityGateway?: IdentityGateway;
|
|
91
92
|
compose?: ComposeApp;
|
|
@@ -9,7 +9,7 @@ import { isNativeArchFamily, } from "../build-spk/dsm-arch.js";
|
|
|
9
9
|
import { checkGlossary, loadCatalogs, readInfoTranslations, } from "./i18n.js";
|
|
10
10
|
import { loadRootOps, readRootSettings, rootBinaryProblem, ROOT_OPS_DIR, } from "./root-ops.js";
|
|
11
11
|
import { readComposeApp } from "./compose.js";
|
|
12
|
-
import { readDaemon } from "./daemon.js";
|
|
12
|
+
import { readDaemon, readLifecycleTimeouts } from "./daemon.js";
|
|
13
13
|
import { readIdentityGateway, } from "./identity-gateway.js";
|
|
14
14
|
import { readUninstall } from "./uninstall.js";
|
|
15
15
|
import { readAppScripts } from "./app-scripts.js";
|
|
@@ -231,8 +231,10 @@ function readManifest(raw, c) {
|
|
|
231
231
|
c.fail("STANDALONE_API_PERMISSIONS_INVALID", `backend/${API_PERMISSIONS_FILE[backend.kind]} must define the centralized API policy: ${String(error)}`);
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
|
+
const timeoutOnly = isRecord(raw.daemon) && Object.keys(raw.daemon).length === 1 && 'timeouts' in raw.daemon;
|
|
235
|
+
const lifecycleTimeouts = timeoutOnly ? readLifecycleTimeouts(raw.daemon.timeouts, (e) => c.fail(e.code, e.message)) : undefined;
|
|
234
236
|
const daemon = arches &&
|
|
235
|
-
readDaemon(raw.daemon, c.root, arches, (e) => c.fail(e.code, e.message));
|
|
237
|
+
readDaemon(timeoutOnly ? undefined : raw.daemon, c.root, arches, (e) => c.fail(e.code, e.message));
|
|
236
238
|
const gatewayRaw = isRecord(raw.identity_gateway) ? raw.identity_gateway : {};
|
|
237
239
|
if (raw.compose !== undefined && isRecord(raw.ctl) && raw.ctl.enabled === true)
|
|
238
240
|
c.fail("CTL_UNSUPPORTED_BACKEND", "Compose does not support automatic ctl descriptions; disable ctl.");
|
|
@@ -246,7 +248,7 @@ function readManifest(raw, c) {
|
|
|
246
248
|
composeMounts: capabilities.composeMounts,
|
|
247
249
|
});
|
|
248
250
|
if (raw.compose !== undefined &&
|
|
249
|
-
(raw.daemon !== undefined ||
|
|
251
|
+
((raw.daemon !== undefined && !timeoutOnly) ||
|
|
250
252
|
Object.keys(capabilities).some((k) => k !== "composeMounts") ||
|
|
251
253
|
!osMinVer ||
|
|
252
254
|
!isDsmAtLeast(osMinVer, "7.2.1-69057")))
|
|
@@ -280,7 +282,7 @@ function readManifest(raw, c) {
|
|
|
280
282
|
s.port === identityGateway.oidc?.issuer_port))))
|
|
281
283
|
c.fail("STANDALONE_IDENTITY_GATEWAY_INVALID", "compose service ports must contain upstream and not overlap gateway or issuer ports.");
|
|
282
284
|
if (capabilities.jobs &&
|
|
283
|
-
raw.daemon !== undefined &&
|
|
285
|
+
raw.daemon !== undefined && !timeoutOnly &&
|
|
284
286
|
capabilities.delegate?.lifetime !== "until-revoked") {
|
|
285
287
|
c.fail("CAPABILITY_COMBINATION_INVALID", "daemon can only coexist with an explicit until-revoked delegate fixed worker; plain capabilities.jobs stays mutually exclusive.");
|
|
286
288
|
}
|
|
@@ -299,7 +301,7 @@ function readManifest(raw, c) {
|
|
|
299
301
|
delegate.destinations?.length)) {
|
|
300
302
|
c.fail("CAPABILITY_UNSUPPORTED", `For backend.runtime: ${backend.kind}, capabilities.delegate supports only operations: [files.moveFromStaging], destinations and lifetime: until-revoked.`);
|
|
301
303
|
}
|
|
302
|
-
if (raw.daemon !== undefined)
|
|
304
|
+
if (raw.daemon !== undefined && !timeoutOnly)
|
|
303
305
|
c.fail("CAPABILITY_UNSUPPORTED", `backend.runtime: ${backend.kind} does not support daemon; a v1 App runs once per request (exec mode).`);
|
|
304
306
|
if (permissions)
|
|
305
307
|
checkBridgeMethods(backend, permissions, c);
|
|
@@ -438,6 +440,7 @@ function readManifest(raw, c) {
|
|
|
438
440
|
...(rootOps ? { rootOps } : {}),
|
|
439
441
|
...(rootPrograms ? { rootPrograms } : {}),
|
|
440
442
|
...(daemon ? { daemon } : {}),
|
|
443
|
+
...(lifecycleTimeouts ? { lifecycleTimeouts } : {}),
|
|
441
444
|
...(identityGateway ? { identityGateway } : {}),
|
|
442
445
|
...(compose ? { compose } : {}),
|
|
443
446
|
...(uninstall ? { uninstall } : {}),
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { IdentityGateway } from './identity-gateway.js';
|
|
2
|
+
export type NativeGatewayPlan = Pick<IdentityGateway, 'sdk' | 'listen'>;
|
|
3
|
+
type Target = {
|
|
4
|
+
package: string;
|
|
5
|
+
os_min_ver?: string;
|
|
6
|
+
identityGateway?: NativeGatewayPlan;
|
|
7
|
+
};
|
|
8
|
+
export declare const NATIVE_GATEWAY_CONFIG = "sso/native-nginx.conf";
|
|
9
|
+
export declare function nativeGatewaySdk(sdk: string): boolean;
|
|
10
|
+
export declare function nativeGatewaySupported(m: Target): boolean;
|
|
11
|
+
export declare function gatewayEntryPath(m: Target): string;
|
|
12
|
+
export declare function legacyGatewayNotice(): string;
|
|
13
|
+
export declare function nativeGatewayResource(m: Target): Record<string, unknown> | undefined;
|
|
14
|
+
export declare function renderNativeGatewayConfig(m: Target): string;
|
|
15
|
+
/** Runs after DSM creates pkgvar, before package start activates the nginx resource. */
|
|
16
|
+
export declare function renderNativeGatewayPrepare(m: Target): string;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { isDsmAtLeast, PORTAL_MIN_DSM } from '../build-spk/dsm-version.js';
|
|
2
|
+
export const NATIVE_GATEWAY_CONFIG = 'sso/native-nginx.conf';
|
|
3
|
+
// Earlier SDK releases do not understand native_ingress. Keep their JSON ABI
|
|
4
|
+
// and put an explanation in front of their existing direct-port launcher.
|
|
5
|
+
export function nativeGatewaySdk(sdk) {
|
|
6
|
+
const [major, minor, patch] = sdk.split('.').map(Number);
|
|
7
|
+
return major > 0 || minor > 17 || minor === 17 && patch >= 5;
|
|
8
|
+
}
|
|
9
|
+
export function nativeGatewaySupported(m) {
|
|
10
|
+
return Boolean(m.identityGateway && nativeGatewaySdk(m.identityGateway.sdk)
|
|
11
|
+
&& m.os_min_ver && isDsmAtLeast(m.os_min_ver, PORTAL_MIN_DSM));
|
|
12
|
+
}
|
|
13
|
+
export function gatewayEntryPath(m) {
|
|
14
|
+
return `/webman/3rdparty/${m.package}/sso/open.${m.identityGateway && !nativeGatewaySdk(m.identityGateway.sdk) ? 'html' : 'cgi'}`;
|
|
15
|
+
}
|
|
16
|
+
export function legacyGatewayNotice() {
|
|
17
|
+
return '<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="default-src \'none\'; frame-ancestors \'none\'"><title>Direct App connection</title><p>DSM-managed HTTPS is unavailable for this App. The direct connection may show a certificate warning or use unencrypted HTTP.</p><a href="open.cgi">Continue to the direct connection</a></html>\n';
|
|
18
|
+
}
|
|
19
|
+
export function nativeGatewayResource(m) {
|
|
20
|
+
if (!nativeGatewaySupported(m))
|
|
21
|
+
return undefined;
|
|
22
|
+
const g = m.identityGateway, https = g.listen.https ?? g.listen.http;
|
|
23
|
+
const ports = [https, ...(g.listen.http && g.listen.http !== https ? [g.listen.http] : [])];
|
|
24
|
+
return { 'nginx-static-config': { enable: [{ type: 'server', relpath: NATIVE_GATEWAY_CONFIG,
|
|
25
|
+
ports: ports.map(port => ({ port, protocol: 'tcp', schema: port === https ? 'https' : 'http' })) }] } };
|
|
26
|
+
}
|
|
27
|
+
export function renderNativeGatewayConfig(m) {
|
|
28
|
+
if (!nativeGatewaySupported(m))
|
|
29
|
+
return '';
|
|
30
|
+
const g = m.identityGateway, port = g.listen.https ?? g.listen.http;
|
|
31
|
+
const state = `/var/packages/${m.package}/var/sso`;
|
|
32
|
+
// Public ssl_certificate is deliberately inherited from DSM's http context.
|
|
33
|
+
// Only nginx's master reads the package's private upstream key. No DSM key
|
|
34
|
+
// is read or copied by the package, and no secret appears in this config.
|
|
35
|
+
return `server {
|
|
36
|
+
listen ${port} ssl;
|
|
37
|
+
listen [::]:${port} ssl;
|
|
38
|
+
server_name _;
|
|
39
|
+
client_max_body_size 0;
|
|
40
|
+
location / {
|
|
41
|
+
proxy_pass https://unix:/tmp/synopkgland-gateway-${m.package}.sock:;
|
|
42
|
+
proxy_http_version 1.1;
|
|
43
|
+
proxy_ssl_verify on;
|
|
44
|
+
proxy_ssl_verify_depth 1;
|
|
45
|
+
proxy_ssl_server_name on;
|
|
46
|
+
proxy_ssl_name gateway.invalid;
|
|
47
|
+
proxy_ssl_trusted_certificate ${state}/native-cert.pem;
|
|
48
|
+
proxy_ssl_certificate ${state}/native-cert.pem;
|
|
49
|
+
proxy_ssl_certificate_key ${state}/native-key.pem;
|
|
50
|
+
proxy_set_header Host $http_host;
|
|
51
|
+
proxy_set_header X-Synopkgland-Client-IP $remote_addr;
|
|
52
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
53
|
+
proxy_set_header Connection $http_connection;
|
|
54
|
+
proxy_request_buffering off;
|
|
55
|
+
proxy_buffering off;
|
|
56
|
+
proxy_read_timeout 3600s;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
${g.listen.http && g.listen.http !== port ? `server {
|
|
60
|
+
listen ${g.listen.http};
|
|
61
|
+
listen [::]:${g.listen.http};
|
|
62
|
+
server_name _;
|
|
63
|
+
return 308 https://$host:${port}$request_uri;
|
|
64
|
+
}
|
|
65
|
+
` : ''}`;
|
|
66
|
+
}
|
|
67
|
+
/** Runs after DSM creates pkgvar, before package start activates the nginx resource. */
|
|
68
|
+
export function renderNativeGatewayPrepare(m) {
|
|
69
|
+
if (!nativeGatewaySupported(m))
|
|
70
|
+
return '';
|
|
71
|
+
return `synopkgland_native_keys() (
|
|
72
|
+
set -eu
|
|
73
|
+
umask 077
|
|
74
|
+
v="\${SYNOPKG_PKGVAR:-/var/packages/${m.package}/var}/sso"
|
|
75
|
+
if [ -L "$v" ]; then echo 'Native HTTPS state must not be a symlink.' >&2; exit 1; fi
|
|
76
|
+
mkdir -p "$v"
|
|
77
|
+
if [ "$(stat -c %a "$v")" != 700 ]; then echo 'Native HTTPS state must have mode 0700.' >&2; exit 1; fi
|
|
78
|
+
c="$v/native-cert.pem"; k="$v/native-key.pem"
|
|
79
|
+
if [ -e "$c" ] || [ -L "$c" ] || [ -e "$k" ] || [ -L "$k" ]; then
|
|
80
|
+
for f in "$c" "$k"; do
|
|
81
|
+
if [ -L "$f" ] || [ ! -f "$f" ] || [ "$(stat -c %a "$f")" != 600 ]; then
|
|
82
|
+
echo 'Native HTTPS requires a complete private keypair with mode 0600.' >&2; exit 1
|
|
83
|
+
fi
|
|
84
|
+
done
|
|
85
|
+
/usr/bin/openssl x509 -in "$c" -noout -checkend 0 >/dev/null || exit 1
|
|
86
|
+
cp=$(/usr/bin/openssl x509 -in "$c" -pubkey -noout | /usr/bin/openssl sha256)
|
|
87
|
+
kp=$(/usr/bin/openssl pkey -in "$k" -pubout | /usr/bin/openssl sha256)
|
|
88
|
+
[ "$cp" = "$kp" ] || { echo 'Native HTTPS keypair does not match.' >&2; exit 1; }
|
|
89
|
+
exit 0
|
|
90
|
+
fi
|
|
91
|
+
t=$(mktemp -d "$v/.native-tls.XXXXXX")
|
|
92
|
+
trap 'rm -rf "$t"' EXIT HUP INT TERM
|
|
93
|
+
cat > "$t/openssl.cnf" <<'NATIVE_TLS_CONFIG'
|
|
94
|
+
[req]
|
|
95
|
+
distinguished_name=subject
|
|
96
|
+
x509_extensions=extensions
|
|
97
|
+
prompt=no
|
|
98
|
+
[subject]
|
|
99
|
+
CN=gateway.invalid
|
|
100
|
+
[extensions]
|
|
101
|
+
basicConstraints=critical,CA:TRUE
|
|
102
|
+
keyUsage=critical,digitalSignature,keyEncipherment,keyCertSign
|
|
103
|
+
extendedKeyUsage=serverAuth,clientAuth
|
|
104
|
+
subjectAltName=DNS:gateway.invalid
|
|
105
|
+
NATIVE_TLS_CONFIG
|
|
106
|
+
/usr/bin/openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -config "$t/openssl.cnf" -keyout "$t/key.pem" -out "$t/cert.pem" >/dev/null 2>&1 || {
|
|
107
|
+
echo 'Could not prepare the private native HTTPS connection.' >&2; exit 1;
|
|
108
|
+
}
|
|
109
|
+
chmod 600 "$t/key.pem" "$t/cert.pem"
|
|
110
|
+
mv "$t/key.pem" "$k"
|
|
111
|
+
mv "$t/cert.pem" "$c"
|
|
112
|
+
)
|
|
113
|
+
synopkgland_native_keys || exit $?
|
|
114
|
+
`;
|
|
115
|
+
}
|