@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 app
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"net/http"
|
|
6
|
+
"net/http/httptest"
|
|
7
|
+
"os"
|
|
8
|
+
"strings"
|
|
9
|
+
"testing"
|
|
10
|
+
|
|
11
|
+
"dsmapp/internal/api"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
type stubAuth struct {
|
|
15
|
+
identity api.Identity
|
|
16
|
+
err error
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func (s stubAuth) Authenticate(*http.Request) (api.Identity, error) { return s.identity, s.err }
|
|
20
|
+
|
|
21
|
+
func testConfig(t *testing.T) Config {
|
|
22
|
+
return Config{PackageID: "HelloGo", Version: "0.1.0-0001", DataDir: t.TempDir()}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// call returns the envelope's error code ("" on success) and the envelope.
|
|
26
|
+
func call(t *testing.T, cfg Config, auth api.Authenticator, method, params string) (string, map[string]any) {
|
|
27
|
+
t.Helper()
|
|
28
|
+
srv, err := api.NewServer(auth, Routes(cfg)...)
|
|
29
|
+
if err != nil {
|
|
30
|
+
t.Fatal(err)
|
|
31
|
+
}
|
|
32
|
+
body := `{"method":"` + method + `","params":` + params + `}`
|
|
33
|
+
req := httptest.NewRequest(http.MethodPost, "/api.cgi", strings.NewReader(body))
|
|
34
|
+
req.Header.Set("Content-Type", "application/json")
|
|
35
|
+
rec := httptest.NewRecorder()
|
|
36
|
+
srv.ServeHTTP(rec, req)
|
|
37
|
+
var out map[string]any
|
|
38
|
+
if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil {
|
|
39
|
+
t.Fatalf("not JSON: %s", rec.Body.String())
|
|
40
|
+
}
|
|
41
|
+
if rec.Code != http.StatusOK {
|
|
42
|
+
t.Fatalf("HTTP %d: DSM replaces non-200 CGI bodies", rec.Code)
|
|
43
|
+
}
|
|
44
|
+
code := ""
|
|
45
|
+
if e, ok := out["error"].(map[string]any); ok {
|
|
46
|
+
code, _ = e["code"].(string)
|
|
47
|
+
}
|
|
48
|
+
return code, out
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
var (
|
|
52
|
+
alice = stubAuth{identity: api.Identity{User: "alice", Admin: true}}
|
|
53
|
+
bob = stubAuth{identity: api.Identity{User: "bob"}}
|
|
54
|
+
nobody = stubAuth{err: api.ErrNoSession}
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
// sampleParams holds valid params for every route, so the denial test below
|
|
58
|
+
// proves the refusal came from the policy and not from bad input.
|
|
59
|
+
var sampleParams = map[string]string{
|
|
60
|
+
"integration.status": `{}`,
|
|
61
|
+
"app.status": `{}`,
|
|
62
|
+
"greeting.get": `{}`,
|
|
63
|
+
"greeting.set": `{"text":"Hi"}`,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func TestIntegrationStatusIsReadableByOrdinaryUsers(t *testing.T) {
|
|
67
|
+
code, out := call(t, testConfig(t), bob, "integration.status", `{}`)
|
|
68
|
+
if code != "" || out["data"].(map[string]any)["state"] != "not-enabled" {
|
|
69
|
+
t.Fatalf("%s: %v", code, out)
|
|
70
|
+
}
|
|
71
|
+
code, _ = call(t, testConfig(t), bob, "integration.status", `{"state":"ready"}`)
|
|
72
|
+
if code != "bad_request" {
|
|
73
|
+
t.Fatal("browser cannot supply readiness")
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
func TestEveryRouteHasSampleParamsAndNoneIsPublic(t *testing.T) {
|
|
78
|
+
routes := Routes(testConfig(t))
|
|
79
|
+
if len(routes) != len(sampleParams) {
|
|
80
|
+
t.Fatalf("%d routes but %d sample params: add the new route to sampleParams", len(routes), len(sampleParams))
|
|
81
|
+
}
|
|
82
|
+
for _, r := range routes {
|
|
83
|
+
if _, ok := sampleParams[r.Method()]; !ok {
|
|
84
|
+
t.Errorf("route %s has no sample params", r.Method())
|
|
85
|
+
}
|
|
86
|
+
if r.Public() {
|
|
87
|
+
t.Errorf("route %s is public; this template ships none", r.Method())
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func TestEveryRouteRefusesCallersWithoutASessionAndChangesNothing(t *testing.T) {
|
|
93
|
+
for _, r := range Routes(testConfig(t)) {
|
|
94
|
+
cfg := testConfig(t)
|
|
95
|
+
code, _ := call(t, cfg, nobody, r.Method(), sampleParams[r.Method()])
|
|
96
|
+
if code != "session_required" {
|
|
97
|
+
t.Errorf("%s: %q, want session_required", r.Method(), code)
|
|
98
|
+
}
|
|
99
|
+
assertDataDirEmpty(t, cfg)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
func TestEveryAdminRouteRefusesOrdinaryUsersAndChangesNothing(t *testing.T) {
|
|
104
|
+
for _, r := range Routes(testConfig(t)) {
|
|
105
|
+
if !r.AdminOnly() {
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
cfg := testConfig(t)
|
|
109
|
+
code, _ := call(t, cfg, bob, r.Method(), sampleParams[r.Method()])
|
|
110
|
+
if code != "admin_required" {
|
|
111
|
+
t.Errorf("%s: %q, want admin_required", r.Method(), code)
|
|
112
|
+
}
|
|
113
|
+
assertDataDirEmpty(t, cfg)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
func TestEveryRouteSucceedsForAnAdmin(t *testing.T) {
|
|
118
|
+
for _, r := range Routes(testConfig(t)) {
|
|
119
|
+
code, out := call(t, testConfig(t), alice, r.Method(), sampleParams[r.Method()])
|
|
120
|
+
if code != "" {
|
|
121
|
+
t.Errorf("%s: %q %v", r.Method(), code, out)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
func TestStatusReportsPackageAndCaller(t *testing.T) {
|
|
127
|
+
code, out := call(t, testConfig(t), bob, "app.status", `{}`)
|
|
128
|
+
data := out["data"].(map[string]any)
|
|
129
|
+
if code != "" || data["package"] != "HelloGo" || data["version"] != "0.1.0-0001" || data["user"] != "bob" || data["admin"] != false {
|
|
130
|
+
t.Fatalf("got %q %v", code, out)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func TestGreetingDefaultsAndAdminCanChangeIt(t *testing.T) {
|
|
135
|
+
cfg := testConfig(t)
|
|
136
|
+
_, out := call(t, cfg, bob, "greeting.get", `{}`)
|
|
137
|
+
if got := out["data"].(map[string]any)["text"]; got != "Hello" {
|
|
138
|
+
t.Fatalf("default greeting = %v", got)
|
|
139
|
+
}
|
|
140
|
+
if code, out := call(t, cfg, alice, "greeting.set", `{"text":"Hi from DSM"}`); code != "" {
|
|
141
|
+
t.Fatalf("set: %q %v", code, out)
|
|
142
|
+
}
|
|
143
|
+
_, out = call(t, cfg, bob, "greeting.get", `{}`)
|
|
144
|
+
data := out["data"].(map[string]any)
|
|
145
|
+
if data["text"] != "Hi from DSM" || data["updated_by"] != "alice" {
|
|
146
|
+
t.Fatalf("after set: %v", data)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
func TestGreetingSetValidatesText(t *testing.T) {
|
|
151
|
+
for _, params := range []string{`{}`, `{"text":""}`, `{"text":"` + strings.Repeat("x", 201) + `"}`, `{"text":7}`, `{"text":"ok","extra":1}`} {
|
|
152
|
+
cfg := testConfig(t)
|
|
153
|
+
code, _ := call(t, cfg, alice, "greeting.set", params)
|
|
154
|
+
if code != "bad_request" {
|
|
155
|
+
t.Errorf("%s: %q, want bad_request", params, code)
|
|
156
|
+
}
|
|
157
|
+
assertDataDirEmpty(t, cfg)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
func assertDataDirEmpty(t *testing.T, cfg Config) {
|
|
162
|
+
t.Helper()
|
|
163
|
+
entries, err := os.ReadDir(cfg.DataDir)
|
|
164
|
+
if err != nil || len(entries) != 0 {
|
|
165
|
+
t.Fatalf("data dir changed: %v %v", entries, err)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Package dsm verifies the DSM login behind a package CGI request.
|
|
2
|
+
//
|
|
3
|
+
// DSM runs a package's CGI as the package account and forwards the browser's
|
|
4
|
+
// cookies. This package hands only the session carriers (cookie, SynoToken,
|
|
5
|
+
// client address) to DSM's own authenticate.cgi, which prints the signed-in
|
|
6
|
+
// user name, then reads administrators membership from DSM's group file. It
|
|
7
|
+
// never parses the session cookie or trusts a user name from the request.
|
|
8
|
+
package dsm
|
|
9
|
+
|
|
10
|
+
import (
|
|
11
|
+
"bufio"
|
|
12
|
+
"context"
|
|
13
|
+
"errors"
|
|
14
|
+
"fmt"
|
|
15
|
+
"net"
|
|
16
|
+
"net/http"
|
|
17
|
+
"net/url"
|
|
18
|
+
"os"
|
|
19
|
+
"os/exec"
|
|
20
|
+
"regexp"
|
|
21
|
+
"slices"
|
|
22
|
+
"strings"
|
|
23
|
+
"time"
|
|
24
|
+
|
|
25
|
+
"dsmapp/internal/api"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
// AuthenticateCGI is DSM's session check for third-party CGIs.
|
|
29
|
+
const AuthenticateCGI = "/usr/syno/synoman/webman/modules/authenticate.cgi"
|
|
30
|
+
|
|
31
|
+
// Authenticator implements api.Authenticator against DSM.
|
|
32
|
+
type Authenticator struct {
|
|
33
|
+
GroupFile string
|
|
34
|
+
AdminGroup string
|
|
35
|
+
Timeout time.Duration
|
|
36
|
+
run func(ctx context.Context, path string, env []string) ([]byte, error)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func NewAuthenticator() *Authenticator {
|
|
40
|
+
return &Authenticator{
|
|
41
|
+
GroupFile: "/etc/group",
|
|
42
|
+
AdminGroup: "administrators",
|
|
43
|
+
Timeout: 10 * time.Second,
|
|
44
|
+
run: runCGI,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
var userName = regexp.MustCompile(`^[A-Za-z0-9._@\\-]{1,64}$`)
|
|
49
|
+
|
|
50
|
+
func (a *Authenticator) Authenticate(r *http.Request) (api.Identity, error) {
|
|
51
|
+
cookie := r.Header.Get("Cookie")
|
|
52
|
+
if cookie == "" {
|
|
53
|
+
return api.Identity{}, api.ErrNoSession
|
|
54
|
+
}
|
|
55
|
+
ctx, cancel := context.WithTimeout(r.Context(), a.Timeout)
|
|
56
|
+
defer cancel()
|
|
57
|
+
out, err := a.run(ctx, AuthenticateCGI, sessionEnv(r, cookie))
|
|
58
|
+
var exit interface{ ExitCode() int }
|
|
59
|
+
if errors.As(err, &exit) {
|
|
60
|
+
return api.Identity{}, api.ErrNoSession
|
|
61
|
+
}
|
|
62
|
+
if err != nil {
|
|
63
|
+
return api.Identity{}, fmt.Errorf("run %s: %w", AuthenticateCGI, err)
|
|
64
|
+
}
|
|
65
|
+
user := strings.TrimSpace(string(out))
|
|
66
|
+
if !userName.MatchString(user) {
|
|
67
|
+
return api.Identity{}, api.ErrNoSession
|
|
68
|
+
}
|
|
69
|
+
admin, err := a.isMember(user)
|
|
70
|
+
if err != nil {
|
|
71
|
+
return api.Identity{}, err
|
|
72
|
+
}
|
|
73
|
+
return api.Identity{User: user, Admin: admin}, nil
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// sessionEnv is the complete environment authenticate.cgi sees: nothing from
|
|
77
|
+
// this process and no request header besides the session carriers.
|
|
78
|
+
func sessionEnv(r *http.Request, cookie string) []string {
|
|
79
|
+
token := r.Header.Get("X-SYNO-TOKEN")
|
|
80
|
+
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
81
|
+
if err != nil {
|
|
82
|
+
host = r.RemoteAddr
|
|
83
|
+
}
|
|
84
|
+
return []string{
|
|
85
|
+
"REQUEST_METHOD=GET",
|
|
86
|
+
"HTTP_COOKIE=" + cookie,
|
|
87
|
+
"HTTP_X_SYNO_TOKEN=" + token,
|
|
88
|
+
"QUERY_STRING=SynoToken=" + url.QueryEscape(token),
|
|
89
|
+
"REMOTE_ADDR=" + host,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func runCGI(ctx context.Context, path string, env []string) ([]byte, error) {
|
|
94
|
+
cmd := exec.CommandContext(ctx, path)
|
|
95
|
+
cmd.Env = env
|
|
96
|
+
return cmd.Output()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// isMember reads the group file fresh on every request, so removing someone
|
|
100
|
+
// from administrators takes effect immediately.
|
|
101
|
+
func (a *Authenticator) isMember(user string) (bool, error) {
|
|
102
|
+
f, err := os.Open(a.GroupFile)
|
|
103
|
+
if err != nil {
|
|
104
|
+
return false, fmt.Errorf("read groups: %w", err)
|
|
105
|
+
}
|
|
106
|
+
defer f.Close()
|
|
107
|
+
scanner := bufio.NewScanner(f)
|
|
108
|
+
for scanner.Scan() {
|
|
109
|
+
fields := strings.Split(scanner.Text(), ":")
|
|
110
|
+
if len(fields) == 4 && fields[0] == a.AdminGroup {
|
|
111
|
+
return slices.Contains(strings.Split(fields[3], ","), user), nil
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if err := scanner.Err(); err != nil {
|
|
115
|
+
return false, fmt.Errorf("read groups: %w", err)
|
|
116
|
+
}
|
|
117
|
+
return false, nil
|
|
118
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
package dsm
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"errors"
|
|
6
|
+
"net/http"
|
|
7
|
+
"net/http/httptest"
|
|
8
|
+
"os"
|
|
9
|
+
"path/filepath"
|
|
10
|
+
"slices"
|
|
11
|
+
"strings"
|
|
12
|
+
"testing"
|
|
13
|
+
|
|
14
|
+
"dsmapp/internal/api"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const groupFile = `root:x:0:
|
|
18
|
+
users:x:100:
|
|
19
|
+
administrators:x:101:admin,alice
|
|
20
|
+
http:x:1023:
|
|
21
|
+
`
|
|
22
|
+
|
|
23
|
+
type fakeRunner struct {
|
|
24
|
+
out string
|
|
25
|
+
err error
|
|
26
|
+
env []string
|
|
27
|
+
path string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func (f *fakeRunner) run(_ context.Context, path string, env []string) ([]byte, error) {
|
|
31
|
+
f.path, f.env = path, env
|
|
32
|
+
return []byte(f.out), f.err
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func newTestAuth(t *testing.T, runner *fakeRunner) *Authenticator {
|
|
36
|
+
t.Helper()
|
|
37
|
+
groups := filepath.Join(t.TempDir(), "group")
|
|
38
|
+
if err := os.WriteFile(groups, []byte(groupFile), 0o644); err != nil {
|
|
39
|
+
t.Fatal(err)
|
|
40
|
+
}
|
|
41
|
+
a := NewAuthenticator()
|
|
42
|
+
a.GroupFile = groups
|
|
43
|
+
a.run = runner.run
|
|
44
|
+
return a
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func sessionRequest() *http.Request {
|
|
48
|
+
r := httptest.NewRequest(http.MethodPost, "/webman/3rdparty/App/cgi/api.cgi", nil)
|
|
49
|
+
r.RemoteAddr = "192.0.2.7:51234"
|
|
50
|
+
r.Header.Set("Cookie", "id=abc; did=xyz")
|
|
51
|
+
r.Header.Set("X-SYNO-TOKEN", "tok+en/=")
|
|
52
|
+
r.Header.Set("User-Agent", "should-not-pass")
|
|
53
|
+
return r
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func TestAdminUserIsResolvedFromAuthenticateCGIAndGroupFile(t *testing.T) {
|
|
57
|
+
runner := &fakeRunner{out: "alice\n"}
|
|
58
|
+
id, err := newTestAuth(t, runner).Authenticate(sessionRequest())
|
|
59
|
+
if err != nil || id.User != "alice" || !id.Admin {
|
|
60
|
+
t.Fatalf("got %+v, %v", id, err)
|
|
61
|
+
}
|
|
62
|
+
if runner.path != AuthenticateCGI {
|
|
63
|
+
t.Errorf("ran %q", runner.path)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func TestOrdinaryUserIsNotAdmin(t *testing.T) {
|
|
68
|
+
id, err := newTestAuth(t, &fakeRunner{out: "bob\n"}).Authenticate(sessionRequest())
|
|
69
|
+
if err != nil || id.User != "bob" || id.Admin {
|
|
70
|
+
t.Fatalf("got %+v, %v", id, err)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
func TestOnlySessionCarriersReachAuthenticateCGI(t *testing.T) {
|
|
75
|
+
runner := &fakeRunner{out: "alice"}
|
|
76
|
+
if _, err := newTestAuth(t, runner).Authenticate(sessionRequest()); err != nil {
|
|
77
|
+
t.Fatal(err)
|
|
78
|
+
}
|
|
79
|
+
want := []string{
|
|
80
|
+
"HTTP_COOKIE=id=abc; did=xyz",
|
|
81
|
+
"HTTP_X_SYNO_TOKEN=tok+en/=",
|
|
82
|
+
"QUERY_STRING=SynoToken=tok%2Ben%2F%3D",
|
|
83
|
+
"REMOTE_ADDR=192.0.2.7",
|
|
84
|
+
"REQUEST_METHOD=GET",
|
|
85
|
+
}
|
|
86
|
+
got := slices.Clone(runner.env)
|
|
87
|
+
slices.Sort(got)
|
|
88
|
+
if !slices.Equal(got, want) {
|
|
89
|
+
t.Fatalf("env = %q\nwant %q", got, want)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func TestMissingCookieNeverRunsAuthenticateCGI(t *testing.T) {
|
|
94
|
+
runner := &fakeRunner{out: "alice"}
|
|
95
|
+
r := sessionRequest()
|
|
96
|
+
r.Header.Del("Cookie")
|
|
97
|
+
_, err := newTestAuth(t, runner).Authenticate(r)
|
|
98
|
+
if !errors.Is(err, api.ErrNoSession) || runner.path != "" {
|
|
99
|
+
t.Fatalf("err=%v ran=%q", err, runner.path)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
func TestRejectedOrMalformedSessionsAreNoSession(t *testing.T) {
|
|
104
|
+
for name, runner := range map[string]*fakeRunner{
|
|
105
|
+
"exit status": {err: &exitError{}},
|
|
106
|
+
"empty output": {out: "\n"},
|
|
107
|
+
"two lines": {out: "alice\nadmin\n"},
|
|
108
|
+
"html output": {out: "<html>login</html>"},
|
|
109
|
+
"overlong name": {out: strings.Repeat("a", 200)},
|
|
110
|
+
"space in name": {out: "alice admin"},
|
|
111
|
+
} {
|
|
112
|
+
t.Run(name, func(t *testing.T) {
|
|
113
|
+
_, err := newTestAuth(t, runner).Authenticate(sessionRequest())
|
|
114
|
+
if !errors.Is(err, api.ErrNoSession) {
|
|
115
|
+
t.Fatalf("err = %v", err)
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
func TestBrokenAuthenticatorIsUnavailableNotNoSession(t *testing.T) {
|
|
122
|
+
_, err := newTestAuth(t, &fakeRunner{err: errors.New("exec: not found")}).Authenticate(sessionRequest())
|
|
123
|
+
if err == nil || errors.Is(err, api.ErrNoSession) {
|
|
124
|
+
t.Fatalf("err = %v", err)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
func TestUnreadableGroupFileFailsClosed(t *testing.T) {
|
|
129
|
+
a := newTestAuth(t, &fakeRunner{out: "alice"})
|
|
130
|
+
a.GroupFile = filepath.Join(t.TempDir(), "missing")
|
|
131
|
+
if _, err := a.Authenticate(sessionRequest()); err == nil || errors.Is(err, api.ErrNoSession) {
|
|
132
|
+
t.Fatalf("err = %v, want unavailable", err)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
type exitError struct{}
|
|
137
|
+
|
|
138
|
+
func (*exitError) Error() string { return "exit status 1" }
|
|
139
|
+
func (*exitError) ExitCode() int { return 1 }
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
package helper
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"errors"
|
|
7
|
+
"io"
|
|
8
|
+
"io/fs"
|
|
9
|
+
"net"
|
|
10
|
+
"os"
|
|
11
|
+
"path/filepath"
|
|
12
|
+
"syscall"
|
|
13
|
+
"time"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
// Process identity rejects reboot/PID reuse and executable replacement.
|
|
17
|
+
// Linux Stop authenticates the owning supervisor socket; only that parent signals its child.
|
|
18
|
+
type Process struct {
|
|
19
|
+
Inventory Inventory `json:"inventory"`
|
|
20
|
+
Mode string `json:"mode,omitempty"` // must be supervised; old unsupervised records fail closed
|
|
21
|
+
PID int `json:"pid"`
|
|
22
|
+
Boot string `json:"boot"`
|
|
23
|
+
Start string `json:"start"`
|
|
24
|
+
Device uint64 `json:"device"`
|
|
25
|
+
Inode uint64 `json:"inode"`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type Processes interface {
|
|
29
|
+
Stop(Process) error
|
|
30
|
+
Start(Inventory) error
|
|
31
|
+
}
|
|
32
|
+
type Controller struct {
|
|
33
|
+
Client
|
|
34
|
+
Processes Processes
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func waitHealthy(ctx context.Context, probe func(context.Context) bool) error {
|
|
38
|
+
for ctx.Err() == nil {
|
|
39
|
+
if probe(ctx) {
|
|
40
|
+
return nil
|
|
41
|
+
}
|
|
42
|
+
timer := time.NewTimer(20 * time.Millisecond)
|
|
43
|
+
select {
|
|
44
|
+
case <-ctx.Done():
|
|
45
|
+
timer.Stop()
|
|
46
|
+
return ctx.Err()
|
|
47
|
+
case <-timer.C:
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return ctx.Err()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func writeProtected(dir, name string, value any) error {
|
|
54
|
+
b, err := json.Marshal(value)
|
|
55
|
+
if err != nil {
|
|
56
|
+
return err
|
|
57
|
+
}
|
|
58
|
+
f, err := os.CreateTemp(dir, ".transition-*")
|
|
59
|
+
if err != nil {
|
|
60
|
+
return err
|
|
61
|
+
}
|
|
62
|
+
defer os.Remove(f.Name())
|
|
63
|
+
mode := fs.FileMode(0644)
|
|
64
|
+
if name == "process.json" {
|
|
65
|
+
mode = 0600
|
|
66
|
+
}
|
|
67
|
+
if err := f.Chmod(mode); err != nil {
|
|
68
|
+
f.Close()
|
|
69
|
+
return err
|
|
70
|
+
}
|
|
71
|
+
if _, err := f.Write(b); err != nil {
|
|
72
|
+
f.Close()
|
|
73
|
+
return err
|
|
74
|
+
}
|
|
75
|
+
if err := f.Sync(); err != nil {
|
|
76
|
+
f.Close()
|
|
77
|
+
return err
|
|
78
|
+
}
|
|
79
|
+
if err := f.Close(); err != nil {
|
|
80
|
+
return err
|
|
81
|
+
}
|
|
82
|
+
if err := os.Rename(f.Name(), filepath.Join(dir, name)); err != nil {
|
|
83
|
+
return err
|
|
84
|
+
}
|
|
85
|
+
d, err := os.Open(dir)
|
|
86
|
+
if err != nil {
|
|
87
|
+
return err
|
|
88
|
+
}
|
|
89
|
+
defer d.Close()
|
|
90
|
+
return d.Sync()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func writeProcess(dir string, p Process) error { return writeProtected(dir, "process.json", p) }
|
|
94
|
+
|
|
95
|
+
func readProcess(dir string, owner uint32) (Process, error) {
|
|
96
|
+
var p Process
|
|
97
|
+
if err := protected(dir, owner, true); err != nil {
|
|
98
|
+
return p, err
|
|
99
|
+
}
|
|
100
|
+
path := filepath.Join(dir, "process.json")
|
|
101
|
+
if err := protected(path, owner, false); err != nil {
|
|
102
|
+
return p, err
|
|
103
|
+
}
|
|
104
|
+
f, err := os.OpenFile(path, os.O_RDONLY|syscall.O_NOFOLLOW, 0)
|
|
105
|
+
if err != nil {
|
|
106
|
+
return p, err
|
|
107
|
+
}
|
|
108
|
+
defer f.Close()
|
|
109
|
+
d := json.NewDecoder(io.LimitReader(f, 4097))
|
|
110
|
+
d.DisallowUnknownFields()
|
|
111
|
+
if d.Decode(&p) != nil || d.Decode(new(any)) != io.EOF || !p.Inventory.Valid() || p.Mode != "supervised" || p.PID <= 1 || p.Boot == "" || p.Start == "" || p.Inode == 0 {
|
|
112
|
+
return p, errTrust
|
|
113
|
+
}
|
|
114
|
+
return p, nil
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Control is called only by the verified root executable from a fixed DSM
|
|
118
|
+
// task. Package hooks and CGI handlers never call this function.
|
|
119
|
+
func (c Controller) Control(ctx context.Context, action string) error {
|
|
120
|
+
if ctx.Err() != nil {
|
|
121
|
+
return ctx.Err()
|
|
122
|
+
}
|
|
123
|
+
if action != "enable" && action != "recover" && action != "teardown" && action != "disable" {
|
|
124
|
+
return errors.New("unsupported lifecycle action")
|
|
125
|
+
}
|
|
126
|
+
if !c.Expected.Valid() || c.Processes == nil {
|
|
127
|
+
return errTrust
|
|
128
|
+
}
|
|
129
|
+
if err := protected(c.StateDir, c.Owner, true); err != nil {
|
|
130
|
+
return err
|
|
131
|
+
}
|
|
132
|
+
f, err := os.OpenFile(filepath.Join(c.StateDir, "control.lock"), os.O_CREATE|os.O_RDWR|syscall.O_NOFOLLOW, 0600)
|
|
133
|
+
if err != nil {
|
|
134
|
+
return err
|
|
135
|
+
}
|
|
136
|
+
defer f.Close()
|
|
137
|
+
if err := protected(f.Name(), c.Owner, false); err != nil {
|
|
138
|
+
return err
|
|
139
|
+
}
|
|
140
|
+
if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
|
|
141
|
+
return errors.New("lifecycle busy")
|
|
142
|
+
}
|
|
143
|
+
defer syscall.Flock(int(f.Fd()), syscall.LOCK_UN)
|
|
144
|
+
r, err := readRecord(c.StateDir, c.Owner)
|
|
145
|
+
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
146
|
+
return err
|
|
147
|
+
}
|
|
148
|
+
if action == "recover" && (err != nil || !r.Enabled) {
|
|
149
|
+
return errors.New("explicit enable approval required")
|
|
150
|
+
}
|
|
151
|
+
if err != nil {
|
|
152
|
+
r = Record{Inventory: c.Expected}
|
|
153
|
+
}
|
|
154
|
+
if action == "disable" {
|
|
155
|
+
r.Enabled = false
|
|
156
|
+
}
|
|
157
|
+
// Publish the durable gate first. A crashed/failed disable still refuses
|
|
158
|
+
// requests and exposes required cleanup; never report cleanup as successful.
|
|
159
|
+
r.Residual = true
|
|
160
|
+
if err := writeProtected(c.StateDir, "approval.json", r); err != nil {
|
|
161
|
+
return err
|
|
162
|
+
}
|
|
163
|
+
if err := c.teardown(); err != nil {
|
|
164
|
+
return err
|
|
165
|
+
}
|
|
166
|
+
if action == "enable" || action == "recover" {
|
|
167
|
+
r = Record{Inventory: c.Expected, Enabled: true, Residual: true}
|
|
168
|
+
if err := writeProtected(c.StateDir, "approval.json", r); err != nil {
|
|
169
|
+
return err
|
|
170
|
+
}
|
|
171
|
+
if err := c.Processes.Start(c.Expected); err != nil {
|
|
172
|
+
return err
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
r.Residual = false
|
|
176
|
+
return writeProtected(c.StateDir, "approval.json", r)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
func (c Controller) teardown() error {
|
|
180
|
+
p, err := readProcess(c.RunDir, c.Owner)
|
|
181
|
+
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
182
|
+
return err
|
|
183
|
+
}
|
|
184
|
+
if err == nil {
|
|
185
|
+
if p.Inventory.Package != c.Expected.Package {
|
|
186
|
+
return errTrust
|
|
187
|
+
}
|
|
188
|
+
if err := c.Processes.Stop(p); err != nil {
|
|
189
|
+
return err
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
for _, name := range []string{"helper.sock", "supervisor.sock"} {
|
|
193
|
+
path := filepath.Join(c.RunDir, name)
|
|
194
|
+
st, e := os.Lstat(path)
|
|
195
|
+
if e == nil {
|
|
196
|
+
s, ok := st.Sys().(*syscall.Stat_t)
|
|
197
|
+
if !ok || s.Uid != c.Owner || st.Mode()&os.ModeSocket == 0 || st.Mode().Perm()&0002 != 0 {
|
|
198
|
+
return errTrust
|
|
199
|
+
}
|
|
200
|
+
conn, err := net.DialTimeout("unix", path, 100*time.Millisecond)
|
|
201
|
+
if err == nil {
|
|
202
|
+
conn.Close()
|
|
203
|
+
return errors.New("unidentified live socket")
|
|
204
|
+
}
|
|
205
|
+
if !errors.Is(err, syscall.ECONNREFUSED) && !errors.Is(err, fs.ErrNotExist) {
|
|
206
|
+
return err
|
|
207
|
+
}
|
|
208
|
+
if err := os.Remove(path); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
209
|
+
return err
|
|
210
|
+
}
|
|
211
|
+
} else if !errors.Is(e, fs.ErrNotExist) {
|
|
212
|
+
return e
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if err == nil {
|
|
216
|
+
return os.Remove(filepath.Join(c.RunDir, "process.json"))
|
|
217
|
+
}
|
|
218
|
+
return nil
|
|
219
|
+
}
|