@synopackageland/cli 0.3.10 → 0.4.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/README.md +20 -9
- package/dist/bin.js +1 -2
- package/dist/build-spk/generator.d.ts +0 -3
- package/dist/build-spk/generator.js +135 -287
- package/dist/build-spk/native-lifecycle.js +0 -3
- package/dist/build-spk/native-payload.js +0 -1
- package/dist/cli.js +171 -151
- package/dist/compose/analyze.d.ts +1 -2
- package/dist/compose/analyze.js +5 -48
- package/dist/contract/unsupported-types.d.ts +10 -0
- package/dist/contract/unsupported-types.js +49 -0
- package/dist/contract/validate.js +25 -2
- package/dist/contract/web-launch.js +0 -2
- package/dist/deploy/deploy.d.ts +1 -4
- package/dist/deploy/deploy.js +45 -156
- package/dist/deploy/homes-values.js +1 -1
- package/dist/deploy/plan.d.ts +18 -0
- package/dist/deploy/plan.js +54 -0
- package/dist/deploy/spk-version.d.ts +2 -14
- package/dist/deploy/spk-version.js +25 -111
- package/dist/dev.d.ts +1 -3
- package/dist/dev.js +14 -28
- package/dist/error-registry.js +810 -862
- package/dist/host/dsm-client.js +12 -272
- package/dist/host/dsm-port-profile.d.ts +0 -1
- package/dist/host/dsm-port-profile.js +0 -2
- package/dist/host/types.d.ts +1 -83
- package/dist/index.d.ts +0 -9
- package/dist/index.js +0 -6
- package/dist/info.d.ts +0 -1
- package/dist/info.js +3 -10
- package/dist/init.d.ts +11 -1
- package/dist/init.js +112 -4
- package/dist/log.js +2 -3
- package/dist/native/validate.js +0 -1
- package/dist/standalone/binary.d.ts +9 -0
- package/dist/standalone/binary.js +67 -0
- package/dist/standalone/build.d.ts +25 -0
- package/dist/standalone/build.js +108 -0
- package/dist/standalone/dsm-window.d.ts +26 -0
- package/dist/standalone/dsm-window.js +251 -0
- package/dist/standalone/helper.d.ts +31 -0
- package/dist/standalone/helper.js +104 -0
- package/dist/standalone/i18n.d.ts +28 -0
- package/dist/standalone/i18n.js +161 -0
- package/dist/standalone/inspect-spk.d.ts +21 -0
- package/dist/standalone/inspect-spk.js +160 -0
- package/dist/standalone/manifest.d.ts +71 -0
- package/dist/standalone/manifest.js +276 -0
- package/dist/standalone/root-messages.d.ts +7 -0
- package/dist/standalone/root-messages.js +250 -0
- package/dist/standalone/root-ops.d.ts +42 -0
- package/dist/standalone/root-ops.js +101 -0
- package/dist/standalone/root-window.d.ts +35 -0
- package/dist/standalone/root-window.js +290 -0
- package/dist/standalone/spk.d.ts +22 -0
- package/dist/standalone/spk.js +205 -0
- package/dist/templates/binary/synology-app.yaml +7 -0
- package/dist/templates/hello-files/compose.yaml +0 -1
- package/dist/templates/hello-web/compose.yaml +13 -10
- package/dist/templates/hello-web/html/app.js +1 -23
- package/dist/templates/hello-web/html/index.html +1 -3
- package/dist/templates/hello-web-page/compose.yaml +13 -10
- package/dist/templates/hello-web-page/html/app.js +1 -23
- package/dist/templates/hello-web-page/html/index.html +1 -3
- package/dist/templates/native-go/README.md +268 -0
- package/dist/templates/native-go/backend/cmd/app/main.go +49 -0
- package/dist/templates/native-go/backend/cmd/helper/main.go +38 -0
- package/dist/templates/native-go/backend/go.mod +3 -0
- package/dist/templates/native-go/backend/internal/api/errors.go +33 -0
- package/dist/templates/native-go/backend/internal/api/registry.go +94 -0
- package/dist/templates/native-go/backend/internal/api/registry_test.go +329 -0
- package/dist/templates/native-go/backend/internal/api/server.go +155 -0
- package/dist/templates/native-go/backend/internal/app/root_none.go +9 -0
- package/dist/templates/native-go/backend/internal/app/routes.go +127 -0
- package/dist/templates/native-go/backend/internal/app/routes_test.go +167 -0
- package/dist/templates/native-go/backend/internal/dsm/auth.go +118 -0
- package/dist/templates/native-go/backend/internal/dsm/auth_test.go +139 -0
- package/dist/templates/native-go/backend/internal/helper/control.go +219 -0
- package/dist/templates/native-go/backend/internal/helper/control_test.go +242 -0
- package/dist/templates/native-go/backend/internal/helper/peer_linux.go +44 -0
- package/dist/templates/native-go/backend/internal/helper/peer_other.go +7 -0
- package/dist/templates/native-go/backend/internal/helper/process_linux.go +167 -0
- package/dist/templates/native-go/backend/internal/helper/process_other.go +5 -0
- package/dist/templates/native-go/backend/internal/helper/server_linux.go +83 -0
- package/dist/templates/native-go/backend/internal/helper/status.go +271 -0
- package/dist/templates/native-go/backend/internal/helper/status_test.go +205 -0
- package/dist/templates/native-go/backend/internal/helper/supervision.go +84 -0
- package/dist/templates/native-go/backend/internal/helper/supervision_test.go +124 -0
- package/dist/templates/native-go/backend/internal/helper/supervisor_linux.go +192 -0
- package/dist/templates/native-go/backend/internal/helper/supervisor_linux_test.go +302 -0
- package/dist/templates/native-go/dsm-package.yaml +58 -0
- package/dist/templates/native-go/gitignore +1 -0
- package/dist/templates/native-go/i18n/glossary.json +662 -0
- package/dist/templates/native-go/i18n/messages/de.json +28 -0
- package/dist/templates/native-go/i18n/messages/en.json +28 -0
- package/dist/templates/native-go/i18n/messages/es.json +28 -0
- package/dist/templates/native-go/i18n/messages/fr.json +28 -0
- package/dist/templates/native-go/i18n/messages/it.json +28 -0
- package/dist/templates/native-go/i18n/messages/ja.json +28 -0
- package/dist/templates/native-go/i18n/messages/ko.json +28 -0
- package/dist/templates/native-go/i18n/messages/zh-CN.json +28 -0
- package/dist/templates/native-go/i18n/messages/zh-TW.json +28 -0
- package/dist/templates/native-go/ui/app.css +35 -0
- package/dist/templates/native-go/ui/app.js +94 -0
- package/dist/templates/native-go-better-auth/backend/internal/app/identity.go +208 -0
- package/dist/templates/native-go-better-auth/backend/internal/app/identity_test.go +113 -0
- package/dist/templates/native-go-better-auth/node/app.mjs +72 -0
- package/dist/templates/native-go-better-auth/node/build.mjs +2 -0
- package/dist/templates/native-go-better-auth/node/package-lock.json +862 -0
- package/dist/templates/native-go-better-auth/node/package.json +1 -0
- package/dist/templates/native-go-better-auth/node/test/auth.test.mjs +31 -0
- package/dist/templates/native-go-integration/backend/internal/app/identity_none.go +6 -0
- package/dist/templates/native-go-integration/backend/internal/app/personal.go +203 -0
- package/dist/templates/native-go-integration/backend/internal/app/personal_test.go +119 -0
- package/dist/templates/native-go-root/backend/internal/app/root.go +107 -0
- package/dist/templates/native-go-root/backend/internal/app/root_test.go +124 -0
- package/dist/templates/native-go-root/root/whoami.sh +6 -0
- package/dist/templates/native-go-vue/README.md +110 -0
- package/dist/templates/native-go-vue/frontend/DESIGN.md +78 -0
- package/dist/templates/native-go-vue/frontend/package-lock.json +2507 -0
- package/dist/templates/native-go-vue/frontend/package.json +6 -0
- package/dist/templates/native-go-vue/frontend/src/App.vue +132 -0
- package/dist/templates/native-go-vue/frontend/src/context.ts +17 -0
- package/dist/templates/native-go-vue/frontend/src/main.ts +26 -0
- package/dist/templates/native-go-vue/frontend/src/style.css +14 -0
- package/dist/templates/native-go-vue/frontend/test/apps.test.ts +148 -0
- package/dist/templates/native-go-vue/frontend/tsconfig.json +1 -0
- package/dist/templates/native-go-vue/frontend/vite.config.ts +8 -0
- package/dist/templates/native-go-vue/gitignore +2 -0
- package/dist/types.d.ts +1 -1
- package/dist/validate.js +11 -7
- package/package.json +3 -7
- package/schema/synology-app.schema.json +36 -37
- package/skills/SKILL.md +4 -48
- package/skills/reference/create.md +11 -83
- package/skills/reference/deploy.md +7 -78
- package/skills/reference/diagnose.md +4 -101
- package/skills/reference/files.md +5 -41
- package/skills/reference/identity.md +3 -29
- package/skills/reference/wrap.md +0 -2
- package/templates/binary/synology-app.yaml +7 -0
- package/templates/hello-files/compose.yaml +0 -1
- package/templates/hello-web/compose.yaml +13 -10
- package/templates/hello-web/html/app.js +1 -23
- package/templates/hello-web/html/index.html +1 -3
- package/templates/hello-web-page/compose.yaml +13 -10
- package/templates/hello-web-page/html/app.js +1 -23
- package/templates/hello-web-page/html/index.html +1 -3
- package/templates/native-go/README.md +268 -0
- package/templates/native-go/backend/cmd/app/main.go +49 -0
- package/templates/native-go/backend/cmd/helper/main.go +38 -0
- package/templates/native-go/backend/go.mod +3 -0
- package/templates/native-go/backend/internal/api/errors.go +33 -0
- package/templates/native-go/backend/internal/api/registry.go +94 -0
- package/templates/native-go/backend/internal/api/registry_test.go +329 -0
- package/templates/native-go/backend/internal/api/server.go +155 -0
- package/templates/native-go/backend/internal/app/root_none.go +9 -0
- package/templates/native-go/backend/internal/app/routes.go +127 -0
- package/templates/native-go/backend/internal/app/routes_test.go +167 -0
- package/templates/native-go/backend/internal/dsm/auth.go +118 -0
- package/templates/native-go/backend/internal/dsm/auth_test.go +139 -0
- package/templates/native-go/backend/internal/helper/control.go +219 -0
- package/templates/native-go/backend/internal/helper/control_test.go +242 -0
- package/templates/native-go/backend/internal/helper/peer_linux.go +44 -0
- package/templates/native-go/backend/internal/helper/peer_other.go +7 -0
- package/templates/native-go/backend/internal/helper/process_linux.go +167 -0
- package/templates/native-go/backend/internal/helper/process_other.go +5 -0
- package/templates/native-go/backend/internal/helper/server_linux.go +83 -0
- package/templates/native-go/backend/internal/helper/status.go +271 -0
- package/templates/native-go/backend/internal/helper/status_test.go +205 -0
- package/templates/native-go/backend/internal/helper/supervision.go +84 -0
- package/templates/native-go/backend/internal/helper/supervision_test.go +124 -0
- package/templates/native-go/backend/internal/helper/supervisor_linux.go +192 -0
- package/templates/native-go/backend/internal/helper/supervisor_linux_test.go +302 -0
- package/templates/native-go/dsm-package.yaml +58 -0
- package/templates/native-go/gitignore +1 -0
- package/templates/native-go/i18n/glossary.json +662 -0
- package/templates/native-go/i18n/messages/de.json +28 -0
- package/templates/native-go/i18n/messages/en.json +28 -0
- package/templates/native-go/i18n/messages/es.json +28 -0
- package/templates/native-go/i18n/messages/fr.json +28 -0
- package/templates/native-go/i18n/messages/it.json +28 -0
- package/templates/native-go/i18n/messages/ja.json +28 -0
- package/templates/native-go/i18n/messages/ko.json +28 -0
- package/templates/native-go/i18n/messages/zh-CN.json +28 -0
- package/templates/native-go/i18n/messages/zh-TW.json +28 -0
- package/templates/native-go/ui/app.css +35 -0
- package/templates/native-go/ui/app.js +94 -0
- package/templates/native-go-better-auth/backend/internal/app/identity.go +208 -0
- package/templates/native-go-better-auth/backend/internal/app/identity_test.go +113 -0
- package/templates/native-go-better-auth/node/app.mjs +72 -0
- package/templates/native-go-better-auth/node/build.mjs +2 -0
- package/templates/native-go-better-auth/node/package-lock.json +862 -0
- package/templates/native-go-better-auth/node/package.json +1 -0
- package/templates/native-go-better-auth/node/test/auth.test.mjs +31 -0
- package/templates/native-go-integration/backend/internal/app/identity_none.go +6 -0
- package/templates/native-go-integration/backend/internal/app/personal.go +203 -0
- package/templates/native-go-integration/backend/internal/app/personal_test.go +119 -0
- package/templates/native-go-root/backend/internal/app/root.go +107 -0
- package/templates/native-go-root/backend/internal/app/root_test.go +124 -0
- package/templates/native-go-root/root/whoami.sh +6 -0
- package/templates/native-go-vue/README.md +110 -0
- package/templates/native-go-vue/frontend/DESIGN.md +78 -0
- package/templates/native-go-vue/frontend/package-lock.json +2507 -0
- package/templates/native-go-vue/frontend/package.json +6 -0
- package/templates/native-go-vue/frontend/src/App.vue +132 -0
- package/templates/native-go-vue/frontend/src/context.ts +17 -0
- package/templates/native-go-vue/frontend/src/main.ts +26 -0
- package/templates/native-go-vue/frontend/src/style.css +14 -0
- package/templates/native-go-vue/frontend/test/apps.test.ts +148 -0
- package/templates/native-go-vue/frontend/tsconfig.json +1 -0
- package/templates/native-go-vue/frontend/vite.config.ts +8 -0
- package/templates/native-go-vue/gitignore +2 -0
- package/dist/attestation/app-package.d.ts +0 -86
- package/dist/attestation/app-package.js +0 -206
- package/dist/attestation/install-on-host.d.ts +0 -14
- package/dist/attestation/install-on-host.js +0 -52
- package/dist/attestation/keyring.d.ts +0 -21
- package/dist/attestation/keyring.js +0 -168
- package/dist/attestation/platform-service.d.ts +0 -70
- package/dist/attestation/platform-service.js +0 -250
- package/dist/build-spk/broker-glue.d.ts +0 -16
- package/dist/build-spk/broker-glue.js +0 -40
- package/dist/deploy/catalog-publish.d.ts +0 -3
- package/dist/deploy/catalog-publish.js +0 -7
- package/dist/doctor.d.ts +0 -22
- package/dist/doctor.js +0 -822
- package/dist/error-collector.d.ts +0 -2
- package/dist/error-collector.js +0 -46
- package/dist/host/broker-paths.d.ts +0 -1
- package/dist/host/broker-paths.js +0 -1
- package/dist/host/broker-upgrade-preflight.d.ts +0 -61
- package/dist/host/broker-upgrade-preflight.js +0 -156
- package/dist/host/digests.d.ts +0 -5
- package/dist/host/digests.js +0 -63
- package/dist/host/fake-nas.d.ts +0 -49
- package/dist/host/fake-nas.js +0 -193
- package/dist/mock-broker/bootstrap.d.ts +0 -2
- package/dist/mock-broker/bootstrap.js +0 -8
- package/dist/mock-broker/files.d.ts +0 -60
- package/dist/mock-broker/files.js +0 -408
- package/dist/mock-broker/oidc.d.ts +0 -31
- package/dist/mock-broker/oidc.js +0 -161
- package/dist/mock-broker/path.d.ts +0 -9
- package/dist/mock-broker/path.js +0 -50
- package/dist/mock-broker/server.d.ts +0 -19
- package/dist/mock-broker/server.js +0 -350
- package/dist/publish/build-ledger.d.ts +0 -22
- package/dist/publish/build-ledger.js +0 -26
- package/dist/publish/community-path.d.ts +0 -6
- package/dist/publish/community-path.js +0 -41
- package/dist/publish/curated-release.d.ts +0 -78
- package/dist/publish/curated-release.js +0 -883
- package/dist/publish/file-build-ledger.d.ts +0 -18
- package/dist/publish/file-build-ledger.js +0 -268
- package/dist/publish/github-pr.d.ts +0 -25
- package/dist/publish/github-pr.js +0 -85
- package/dist/publish/github-repo.d.ts +0 -8
- package/dist/publish/github-repo.js +0 -21
- package/dist/publish/platform-service-release.d.ts +0 -60
- package/dist/publish/platform-service-release.js +0 -333
- package/dist/publish/preflight.d.ts +0 -24
- package/dist/publish/preflight.js +0 -138
- package/dist/publish/publish.d.ts +0 -27
- package/dist/publish/publish.js +0 -136
- package/dist/publish/sync-definition.d.ts +0 -4
- package/dist/publish/sync-definition.js +0 -48
- package/dist/release-bin.d.ts +0 -2
- package/dist/release-bin.js +0 -23
- package/dist/templates/hello-web/html/sdk.js +0 -68
- package/dist/templates/hello-web-page/html/sdk.js +0 -68
- package/dist/templates/native/synology-app.yaml +0 -7
- package/dist/test-host.d.ts +0 -15
- package/dist/test-host.js +0 -37
- package/templates/hello-web/html/sdk.js +0 -68
- package/templates/hello-web-page/html/sdk.js +0 -68
- package/templates/native/synology-app.yaml +0 -7
- /package/dist/templates/{native → binary}/native.yaml +0 -0
- /package/templates/{native → binary}/native.yaml +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
package helper
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"crypto/sha256"
|
|
6
|
+
"encoding/hex"
|
|
7
|
+
"errors"
|
|
8
|
+
"fmt"
|
|
9
|
+
"io"
|
|
10
|
+
"os"
|
|
11
|
+
"os/exec"
|
|
12
|
+
"path/filepath"
|
|
13
|
+
"runtime"
|
|
14
|
+
"strconv"
|
|
15
|
+
"strings"
|
|
16
|
+
"syscall"
|
|
17
|
+
"time"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
func executableDigest(path string) (string, error) {
|
|
21
|
+
f, err := os.Open(path)
|
|
22
|
+
if err != nil {
|
|
23
|
+
return "", err
|
|
24
|
+
}
|
|
25
|
+
defer f.Close()
|
|
26
|
+
st, err := f.Stat()
|
|
27
|
+
if err != nil {
|
|
28
|
+
return "", err
|
|
29
|
+
}
|
|
30
|
+
s, ok := st.Sys().(*syscall.Stat_t)
|
|
31
|
+
if !ok || s.Uid != 0 || st.Mode().Perm()&0022 != 0 || !st.Mode().IsRegular() || st.Size() > 64<<20 {
|
|
32
|
+
return "", errTrust
|
|
33
|
+
}
|
|
34
|
+
h := sha256.New()
|
|
35
|
+
if _, err := io.Copy(h, f); err != nil {
|
|
36
|
+
return "", err
|
|
37
|
+
}
|
|
38
|
+
return hex.EncodeToString(h.Sum(nil)), nil
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func inspectSupervisor(pid int, i Inventory) (Process, error) {
|
|
42
|
+
base := fmt.Sprintf("/proc/%d/", pid)
|
|
43
|
+
b, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
|
|
44
|
+
if err != nil {
|
|
45
|
+
return Process{}, err
|
|
46
|
+
}
|
|
47
|
+
stat, err := os.ReadFile(base + "stat")
|
|
48
|
+
if err != nil {
|
|
49
|
+
return Process{}, err
|
|
50
|
+
}
|
|
51
|
+
end := strings.LastIndex(string(stat), ") ")
|
|
52
|
+
if end < 0 {
|
|
53
|
+
return Process{}, errTrust
|
|
54
|
+
}
|
|
55
|
+
fields := strings.Fields(string(stat)[end+2:])
|
|
56
|
+
if len(fields) < 20 {
|
|
57
|
+
return Process{}, errTrust
|
|
58
|
+
}
|
|
59
|
+
status, err := os.ReadFile(base + "status")
|
|
60
|
+
if err != nil {
|
|
61
|
+
return Process{}, err
|
|
62
|
+
}
|
|
63
|
+
root := false
|
|
64
|
+
for _, line := range strings.Split(string(status), "\n") {
|
|
65
|
+
if strings.HasPrefix(line, "Uid:") {
|
|
66
|
+
u := strings.Fields(line)
|
|
67
|
+
root = len(u) == 5 && u[1] == "0" && u[2] == "0" && u[3] == "0" && u[4] == "0"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if !root {
|
|
71
|
+
return Process{}, errTrust
|
|
72
|
+
}
|
|
73
|
+
cmd, err := os.ReadFile(base + "cmdline")
|
|
74
|
+
if err != nil {
|
|
75
|
+
return Process{}, err
|
|
76
|
+
}
|
|
77
|
+
wantExe := "/var/lib/synopkgland-" + i.Package + "/payload-" + i.PayloadDigest
|
|
78
|
+
if string(cmd) != wantExe+"\x00supervise\x00" {
|
|
79
|
+
return Process{}, errTrust
|
|
80
|
+
}
|
|
81
|
+
st, err := os.Stat(base + "exe")
|
|
82
|
+
if err != nil {
|
|
83
|
+
return Process{}, err
|
|
84
|
+
}
|
|
85
|
+
s := st.Sys().(*syscall.Stat_t)
|
|
86
|
+
digest, err := executableDigest(base + "exe")
|
|
87
|
+
if err != nil || digest != i.PayloadDigest {
|
|
88
|
+
return Process{}, errTrust
|
|
89
|
+
}
|
|
90
|
+
return Process{Inventory: i, Mode: "supervised", PID: pid, Boot: strings.TrimSpace(string(b)), Start: fields[19], Device: uint64(s.Dev), Inode: s.Ino}, nil
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type LinuxProcesses struct {
|
|
94
|
+
Client
|
|
95
|
+
Binary string
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
func (p LinuxProcesses) Start(i Inventory) error {
|
|
99
|
+
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
100
|
+
defer cancel()
|
|
101
|
+
if i != p.Expected {
|
|
102
|
+
return errTrust
|
|
103
|
+
}
|
|
104
|
+
if err := protected(p.RunDir, 0, true); err != nil {
|
|
105
|
+
return err
|
|
106
|
+
}
|
|
107
|
+
cmd := exec.Command(p.Binary, "supervise")
|
|
108
|
+
cmd.Env = []string{"PATH=/usr/sbin:/usr/bin:/sbin:/bin"}
|
|
109
|
+
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
|
110
|
+
if err := cmd.Start(); err != nil {
|
|
111
|
+
return err
|
|
112
|
+
}
|
|
113
|
+
go cmd.Wait()
|
|
114
|
+
if waitHealthy(ctx, func(ctx context.Context) bool {
|
|
115
|
+
state, _ := p.probe(ctx, filepath.Join(p.RunDir, "helper.sock"))
|
|
116
|
+
return state == "ready"
|
|
117
|
+
}) == nil {
|
|
118
|
+
return nil
|
|
119
|
+
}
|
|
120
|
+
return errors.New("helper start timeout; residual preserved")
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// RunLinux runs a fixed control action or the health-only server. Its caller
|
|
124
|
+
// must be the hash-verified, root-owned copy, never the SPK staging executable.
|
|
125
|
+
func RunLinux(base Inventory, action string) error {
|
|
126
|
+
if os.Geteuid() != 0 {
|
|
127
|
+
return errTrust
|
|
128
|
+
}
|
|
129
|
+
exe, err := os.Executable()
|
|
130
|
+
if err != nil {
|
|
131
|
+
return err
|
|
132
|
+
}
|
|
133
|
+
digest, err := executableDigest(exe)
|
|
134
|
+
if err != nil {
|
|
135
|
+
return err
|
|
136
|
+
}
|
|
137
|
+
base.PayloadDigest = digest
|
|
138
|
+
if !base.Valid() || exe != "/var/lib/synopkgland-"+base.Package+"/payload-"+digest {
|
|
139
|
+
return errTrust
|
|
140
|
+
}
|
|
141
|
+
c := NewClient(base)
|
|
142
|
+
if err := protected(c.StateDir, 0, true); err != nil {
|
|
143
|
+
return err
|
|
144
|
+
}
|
|
145
|
+
if err := protected(c.RunDir, 0, true); err != nil {
|
|
146
|
+
return err
|
|
147
|
+
}
|
|
148
|
+
switch action {
|
|
149
|
+
case "supervise":
|
|
150
|
+
return superviseLinux(c, exe)
|
|
151
|
+
case "serve-child":
|
|
152
|
+
parent, err := strconv.Atoi(os.Getenv("SYNOPKGLAND_PARENT"))
|
|
153
|
+
if err != nil {
|
|
154
|
+
return errTrust
|
|
155
|
+
}
|
|
156
|
+
runtime.LockOSThread()
|
|
157
|
+
defer runtime.UnlockOSThread()
|
|
158
|
+
if err := armParentDeath(parent, setParentDeath, os.Getppid); err != nil {
|
|
159
|
+
return err
|
|
160
|
+
}
|
|
161
|
+
if _, err := inspectSupervisor(parent, base); err != nil {
|
|
162
|
+
return err
|
|
163
|
+
}
|
|
164
|
+
return serveLinux(c)
|
|
165
|
+
}
|
|
166
|
+
return (Controller{Client: c, Processes: LinuxProcesses{Client: c, Binary: exe}}).Control(context.Background(), action)
|
|
167
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
package helper
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"io"
|
|
6
|
+
"net"
|
|
7
|
+
"os"
|
|
8
|
+
"os/user"
|
|
9
|
+
"path/filepath"
|
|
10
|
+
"strconv"
|
|
11
|
+
"time"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
func serveLinux(c Client) error {
|
|
15
|
+
r, err := readRecord(c.StateDir, 0)
|
|
16
|
+
if err != nil || !r.Enabled || r.Inventory != c.Expected {
|
|
17
|
+
return errTrust
|
|
18
|
+
}
|
|
19
|
+
// Only the supervisor writes process.json.
|
|
20
|
+
// The package group can connect; ordinary users reach only the authenticated
|
|
21
|
+
// CGI status route. SO_PEERCRED also checks the package's current numeric UID.
|
|
22
|
+
u, err := user.Lookup(c.Expected.Package)
|
|
23
|
+
if err != nil {
|
|
24
|
+
return err
|
|
25
|
+
}
|
|
26
|
+
uid, err := strconv.ParseUint(u.Uid, 10, 32)
|
|
27
|
+
if err != nil {
|
|
28
|
+
return err
|
|
29
|
+
}
|
|
30
|
+
group, err := user.LookupGroup(c.Expected.Package)
|
|
31
|
+
if err != nil {
|
|
32
|
+
return err
|
|
33
|
+
}
|
|
34
|
+
gid, err := strconv.Atoi(group.Gid)
|
|
35
|
+
if err != nil {
|
|
36
|
+
return err
|
|
37
|
+
}
|
|
38
|
+
socket := filepath.Join(c.RunDir, "helper.sock")
|
|
39
|
+
l, err := net.ListenUnix("unix", &net.UnixAddr{Name: socket, Net: "unix"})
|
|
40
|
+
if err != nil {
|
|
41
|
+
return err
|
|
42
|
+
}
|
|
43
|
+
l.SetUnlinkOnClose(false)
|
|
44
|
+
defer l.Close()
|
|
45
|
+
if err := os.Chown(socket, 0, gid); err != nil {
|
|
46
|
+
return err
|
|
47
|
+
}
|
|
48
|
+
if err := os.Chmod(socket, 0660); err != nil {
|
|
49
|
+
return err
|
|
50
|
+
}
|
|
51
|
+
// Bound concurrency and each frame's size/time. No privileged mutations are
|
|
52
|
+
// accepted over this socket; lifecycle control remains fixed admin tasks.
|
|
53
|
+
slots := make(chan struct{}, 8)
|
|
54
|
+
for {
|
|
55
|
+
conn, err := l.AcceptUnix()
|
|
56
|
+
if err != nil {
|
|
57
|
+
return err
|
|
58
|
+
}
|
|
59
|
+
select {
|
|
60
|
+
case slots <- struct{}{}:
|
|
61
|
+
default:
|
|
62
|
+
conn.Close()
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
go func() {
|
|
66
|
+
defer func() { conn.Close(); <-slots }()
|
|
67
|
+
peer, err := peerUID(conn)
|
|
68
|
+
if err != nil || (peer != 0 && peer != uint32(uid)) {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
conn.SetDeadline(time.Now().Add(time.Second))
|
|
72
|
+
var req Request
|
|
73
|
+
d := json.NewDecoder(io.LimitReader(conn, 4097))
|
|
74
|
+
d.DisallowUnknownFields()
|
|
75
|
+
if d.Decode(&req) != nil {
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
r, err := readRecord(c.StateDir, 0)
|
|
79
|
+
disabled := err != nil || !r.Enabled || r.Inventory != c.Expected
|
|
80
|
+
json.NewEncoder(conn).Encode(Respond(c.Expected, disabled, req))
|
|
81
|
+
}()
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// Package helper implements the opt-in per-package lifecycle contract. It has
|
|
2
|
+
// no file/grant/job API: all unimplemented operations fail closed.
|
|
3
|
+
package helper
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"context"
|
|
7
|
+
"crypto/rand"
|
|
8
|
+
"crypto/sha256"
|
|
9
|
+
"encoding/hex"
|
|
10
|
+
"encoding/json"
|
|
11
|
+
"errors"
|
|
12
|
+
"io"
|
|
13
|
+
"io/fs"
|
|
14
|
+
"net"
|
|
15
|
+
"os"
|
|
16
|
+
"path/filepath"
|
|
17
|
+
"regexp"
|
|
18
|
+
"syscall"
|
|
19
|
+
"time"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
type Inventory struct {
|
|
23
|
+
Package string `json:"package"`
|
|
24
|
+
Version string `json:"version"`
|
|
25
|
+
Protocol int `json:"protocol"`
|
|
26
|
+
PayloadDigest string `json:"payloadDigest"`
|
|
27
|
+
RegistryDigest string `json:"registryDigest"`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var digestPattern = regexp.MustCompile(`^[a-f0-9]{64}$`)
|
|
31
|
+
var packagePattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9]{1,63}$`)
|
|
32
|
+
var versionPattern = regexp.MustCompile(`^\d+(\.\d+){0,3}-\d{4}$`)
|
|
33
|
+
|
|
34
|
+
func (i Inventory) Valid() bool {
|
|
35
|
+
return packagePattern.MatchString(i.Package) && versionPattern.MatchString(i.Version) && i.Protocol > 0 && digestPattern.MatchString(i.PayloadDigest) && digestPattern.MatchString(i.RegistryDigest)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type Record struct {
|
|
39
|
+
Inventory Inventory `json:"inventory"`
|
|
40
|
+
Enabled bool `json:"enabled"`
|
|
41
|
+
Residual bool `json:"residual,omitempty"`
|
|
42
|
+
}
|
|
43
|
+
type Request struct {
|
|
44
|
+
Inventory Inventory `json:"inventory"`
|
|
45
|
+
Op string `json:"op"`
|
|
46
|
+
Nonce string `json:"nonce"`
|
|
47
|
+
}
|
|
48
|
+
type Response struct {
|
|
49
|
+
Inventory Inventory `json:"inventory"`
|
|
50
|
+
Nonce string `json:"nonce"`
|
|
51
|
+
OK bool `json:"ok"`
|
|
52
|
+
Error string `json:"error,omitempty"`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func Respond(i Inventory, disabled bool, req Request) Response {
|
|
56
|
+
r := Response{Inventory: i, Nonce: req.Nonce}
|
|
57
|
+
switch {
|
|
58
|
+
case disabled:
|
|
59
|
+
r.Error = "disabled"
|
|
60
|
+
case !i.Valid() || req.Inventory != i:
|
|
61
|
+
r.Error = "incompatible"
|
|
62
|
+
case req.Op != "health":
|
|
63
|
+
r.Error = "unsupported"
|
|
64
|
+
default:
|
|
65
|
+
r.OK = true
|
|
66
|
+
}
|
|
67
|
+
return r
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// State is safe to expose to any authenticated DSM user. Paths, PIDs, worker
|
|
71
|
+
// internals, scheduler definitions and raw errors never enter this envelope.
|
|
72
|
+
type State struct {
|
|
73
|
+
State string `json:"state"`
|
|
74
|
+
Reason string `json:"reason"`
|
|
75
|
+
PackageRunning string `json:"packageRunning"`
|
|
76
|
+
Scheduler string `json:"scheduler"`
|
|
77
|
+
Cleanup string `json:"cleanup,omitempty"`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type Client struct {
|
|
81
|
+
Expected Inventory
|
|
82
|
+
StateDir, RunDir string
|
|
83
|
+
// Production uses UID 0 and RootPeer. Tests inject only local fixture identity.
|
|
84
|
+
Owner uint32
|
|
85
|
+
Peer func(*net.UnixConn) error
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func NewClient(i Inventory) Client {
|
|
89
|
+
return Client{Expected: i, StateDir: "/var/lib/synopkgland-" + i.Package, RunDir: "/run/synopkgland-helper-" + i.Package, Peer: RootPeer}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// LocalClient is a nonprivileged diagnostic entry point for package hooks.
|
|
93
|
+
// Hashing the local executable establishes only the expected build inventory;
|
|
94
|
+
// readiness still requires the protected record and root socket handshake.
|
|
95
|
+
func LocalClient(i Inventory) (Client, error) {
|
|
96
|
+
exe, err := os.Executable()
|
|
97
|
+
if err != nil {
|
|
98
|
+
return Client{}, err
|
|
99
|
+
}
|
|
100
|
+
f, err := os.Open(exe)
|
|
101
|
+
if err != nil {
|
|
102
|
+
return Client{}, err
|
|
103
|
+
}
|
|
104
|
+
defer f.Close()
|
|
105
|
+
h := sha256.New()
|
|
106
|
+
if _, err := io.Copy(h, io.LimitReader(f, (64<<20)+1)); err != nil {
|
|
107
|
+
return Client{}, err
|
|
108
|
+
}
|
|
109
|
+
i.PayloadDigest = hex.EncodeToString(h.Sum(nil))
|
|
110
|
+
if !i.Valid() {
|
|
111
|
+
return Client{}, errTrust
|
|
112
|
+
}
|
|
113
|
+
return NewClient(i), nil
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
func (c Client) CheckStopped(ctx context.Context) error {
|
|
117
|
+
s := c.Status(ctx)
|
|
118
|
+
if s.Cleanup != "" || (s.State != "not-enabled" && s.State != "stopped" && s.State != "needs-approval") {
|
|
119
|
+
return errors.New("approved helper teardown required")
|
|
120
|
+
}
|
|
121
|
+
for _, file := range []string{"process.json", "helper.sock", "supervisor.sock"} {
|
|
122
|
+
if _, err := os.Lstat(filepath.Join(c.RunDir, file)); !errors.Is(err, fs.ErrNotExist) {
|
|
123
|
+
return errors.New("helper residue; approved teardown required")
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return nil
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
var errTrust = errors.New("untrusted helper state")
|
|
130
|
+
|
|
131
|
+
func protected(path string, owner uint32, directory bool) error {
|
|
132
|
+
st, err := os.Lstat(path)
|
|
133
|
+
if err != nil {
|
|
134
|
+
return err
|
|
135
|
+
}
|
|
136
|
+
s, ok := st.Sys().(*syscall.Stat_t)
|
|
137
|
+
if !ok || s.Uid != owner || st.Mode().Perm()&0022 != 0 || (directory && !st.IsDir()) || (!directory && !st.Mode().IsRegular()) {
|
|
138
|
+
return errTrust
|
|
139
|
+
}
|
|
140
|
+
return nil
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func readRecord(dir string, owner uint32) (Record, error) {
|
|
144
|
+
var r Record
|
|
145
|
+
if err := protected(dir, owner, true); err != nil {
|
|
146
|
+
return r, err
|
|
147
|
+
}
|
|
148
|
+
f, err := os.OpenFile(filepath.Join(dir, "approval.json"), os.O_RDONLY|syscall.O_NOFOLLOW, 0)
|
|
149
|
+
if err != nil {
|
|
150
|
+
return r, err
|
|
151
|
+
}
|
|
152
|
+
defer f.Close()
|
|
153
|
+
st, err := f.Stat()
|
|
154
|
+
if err != nil {
|
|
155
|
+
return r, err
|
|
156
|
+
}
|
|
157
|
+
s, ok := st.Sys().(*syscall.Stat_t)
|
|
158
|
+
if !ok || s.Uid != owner || st.Mode().Perm()&0022 != 0 || !st.Mode().IsRegular() || st.Size() > 4096 {
|
|
159
|
+
return r, errTrust
|
|
160
|
+
}
|
|
161
|
+
d := json.NewDecoder(io.LimitReader(f, 4097))
|
|
162
|
+
d.DisallowUnknownFields()
|
|
163
|
+
var decoded struct {
|
|
164
|
+
Inventory Inventory `json:"inventory"`
|
|
165
|
+
Enabled *bool `json:"enabled"`
|
|
166
|
+
Residual bool `json:"residual,omitempty"`
|
|
167
|
+
}
|
|
168
|
+
if d.Decode(&decoded) != nil || d.Decode(new(any)) != io.EOF || !decoded.Inventory.Valid() || decoded.Enabled == nil {
|
|
169
|
+
return r, errTrust
|
|
170
|
+
}
|
|
171
|
+
r = Record{Inventory: decoded.Inventory, Enabled: *decoded.Enabled, Residual: decoded.Residual}
|
|
172
|
+
return r, nil
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
func (c Client) Status(ctx context.Context) State {
|
|
176
|
+
result := func(state, reason string) State {
|
|
177
|
+
return State{State: state, Reason: reason, PackageRunning: "unknown", Scheduler: "not-probed"}
|
|
178
|
+
}
|
|
179
|
+
if c.Expected.Package == "" {
|
|
180
|
+
return result("not-enabled", "not-requested")
|
|
181
|
+
}
|
|
182
|
+
if !c.Expected.Valid() {
|
|
183
|
+
return result("incompatible", "invalid-inventory")
|
|
184
|
+
}
|
|
185
|
+
r, err := readRecord(c.StateDir, c.Owner)
|
|
186
|
+
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
187
|
+
return result("incompatible", "untrusted-approval")
|
|
188
|
+
}
|
|
189
|
+
if err == nil && !r.Enabled {
|
|
190
|
+
s := result("not-enabled", "disabled")
|
|
191
|
+
if r.Residual {
|
|
192
|
+
s.Cleanup = "required"
|
|
193
|
+
}
|
|
194
|
+
return s
|
|
195
|
+
}
|
|
196
|
+
if err == nil && r.Inventory != c.Expected {
|
|
197
|
+
return result("incompatible", "inventory-mismatch")
|
|
198
|
+
}
|
|
199
|
+
if err == nil && r.Residual {
|
|
200
|
+
s := result("failed", "incomplete-transition")
|
|
201
|
+
s.Cleanup = "required"
|
|
202
|
+
return s
|
|
203
|
+
}
|
|
204
|
+
// Conflict has precedence over missing approval. Even an unapproved leftover
|
|
205
|
+
// socket is probed before claiming that authorization alone can fix it.
|
|
206
|
+
if e := protected(c.RunDir, c.Owner, true); e != nil && !errors.Is(e, fs.ErrNotExist) {
|
|
207
|
+
return result("incompatible", "untrusted-runtime")
|
|
208
|
+
}
|
|
209
|
+
socket := filepath.Join(c.RunDir, "helper.sock")
|
|
210
|
+
st, e := os.Lstat(socket)
|
|
211
|
+
if e == nil {
|
|
212
|
+
s, ok := st.Sys().(*syscall.Stat_t)
|
|
213
|
+
if !ok || s.Uid != c.Owner || st.Mode()&os.ModeSocket == 0 || st.Mode().Perm()&0002 != 0 {
|
|
214
|
+
return result("incompatible", "untrusted-socket")
|
|
215
|
+
}
|
|
216
|
+
state, reason := c.probe(ctx, socket)
|
|
217
|
+
if state != "ready" {
|
|
218
|
+
return result(state, reason)
|
|
219
|
+
}
|
|
220
|
+
} else if !errors.Is(e, fs.ErrNotExist) {
|
|
221
|
+
return result("failed", "runtime-unreadable")
|
|
222
|
+
}
|
|
223
|
+
if err != nil {
|
|
224
|
+
return result("needs-approval", "approval-missing")
|
|
225
|
+
}
|
|
226
|
+
if e != nil {
|
|
227
|
+
return result("stopped", "runtime-missing")
|
|
228
|
+
}
|
|
229
|
+
return result("ready", "live-handshake")
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
func (c Client) probe(ctx context.Context, socket string) (string, string) {
|
|
233
|
+
deadline := time.Now().Add(time.Second)
|
|
234
|
+
if d, ok := ctx.Deadline(); ok && d.Before(deadline) {
|
|
235
|
+
deadline = d
|
|
236
|
+
}
|
|
237
|
+
dialer := net.Dialer{Deadline: deadline}
|
|
238
|
+
conn, err := dialer.DialContext(ctx, "unix", socket)
|
|
239
|
+
if err != nil {
|
|
240
|
+
if errors.Is(err, syscall.ECONNREFUSED) || errors.Is(err, fs.ErrNotExist) {
|
|
241
|
+
return "stopped", "runtime-not-listening"
|
|
242
|
+
}
|
|
243
|
+
return "failed", "probe-failed"
|
|
244
|
+
}
|
|
245
|
+
defer conn.Close()
|
|
246
|
+
if c.Peer == nil || c.Peer(conn.(*net.UnixConn)) != nil {
|
|
247
|
+
return "incompatible", "untrusted-peer"
|
|
248
|
+
}
|
|
249
|
+
conn.SetDeadline(deadline)
|
|
250
|
+
var nonce [32]byte
|
|
251
|
+
if _, err := rand.Read(nonce[:]); err != nil {
|
|
252
|
+
return "failed", "probe-failed"
|
|
253
|
+
}
|
|
254
|
+
req := Request{c.Expected, "health", hex.EncodeToString(nonce[:])}
|
|
255
|
+
if json.NewEncoder(conn).Encode(req) != nil {
|
|
256
|
+
return "failed", "probe-failed"
|
|
257
|
+
}
|
|
258
|
+
var res Response
|
|
259
|
+
d := json.NewDecoder(io.LimitReader(conn, 4097))
|
|
260
|
+
d.DisallowUnknownFields()
|
|
261
|
+
if d.Decode(&res) != nil {
|
|
262
|
+
return "failed", "invalid-handshake"
|
|
263
|
+
}
|
|
264
|
+
if res.Nonce != req.Nonce || res.Inventory != c.Expected || res.Error == "incompatible" {
|
|
265
|
+
return "incompatible", "handshake-mismatch"
|
|
266
|
+
}
|
|
267
|
+
if !res.OK || res.Error != "" {
|
|
268
|
+
return "failed", "health-refused"
|
|
269
|
+
}
|
|
270
|
+
return "ready", "live-handshake"
|
|
271
|
+
}
|