@synopackageland/cli 0.14.1 → 0.15.1
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 +20 -1
- package/README.md +33 -18
- package/dist/build-spk/generator.d.ts +3 -2
- package/dist/build-spk/generator.js +6 -846
- package/dist/build-spk/resource-worker.d.ts +2 -14
- package/dist/build-spk/resource-worker.js +3 -166
- package/dist/cli.js +208 -56
- package/dist/compose/analyze.d.ts +3 -61
- package/dist/compose/analyze.js +35 -776
- package/dist/compose/env.d.ts +5 -3
- package/dist/compose/env.js +17 -25
- package/dist/compose/runner.js +23 -6
- package/dist/contract/unsupported-types.js +1 -1
- package/dist/contract/validate.js +2 -2
- package/dist/contract/web-launch.d.ts +1 -12
- package/dist/contract/web-launch.js +10 -74
- package/dist/ctl/cli.d.ts +36 -0
- package/dist/ctl/cli.js +218 -0
- package/dist/ctl/descriptor.d.ts +81 -0
- package/dist/ctl/descriptor.js +93 -0
- package/dist/ctl/dsm-auth.d.ts +10 -0
- package/dist/ctl/dsm-auth.js +30 -0
- package/dist/ctl/errors.d.ts +17 -0
- package/dist/ctl/errors.js +32 -0
- package/dist/ctl/flows.d.ts +11 -0
- package/dist/ctl/flows.js +52 -0
- package/dist/ctl/input.d.ts +20 -0
- package/dist/ctl/input.js +128 -0
- package/dist/ctl/json.d.ts +2 -0
- package/dist/ctl/json.js +38 -0
- package/dist/ctl/redaction.d.ts +15 -0
- package/dist/ctl/redaction.js +198 -0
- package/dist/ctl/service.d.ts +38 -0
- package/dist/ctl/service.js +156 -0
- package/dist/ctl/store.d.ts +51 -0
- package/dist/ctl/store.js +93 -0
- package/dist/ctl/transfers.d.ts +14 -0
- package/dist/ctl/transfers.js +74 -0
- package/dist/ctl/transport.d.ts +11 -0
- package/dist/ctl/transport.js +79 -0
- package/dist/deploy/deploy.d.ts +4 -3
- package/dist/deploy/deploy.js +5 -210
- package/dist/deploy/plan.js +26 -7
- package/dist/dev.js +5 -4
- package/dist/e2e/dsm.d.ts +2 -2
- package/dist/e2e/dsm.js +10 -6
- package/dist/error-registry.js +1020 -834
- package/dist/host/share-permission-lookup.d.ts +1 -1
- package/dist/host/share-permission.d.ts +1 -1
- package/dist/host/share-permission.js +1 -1
- package/dist/info.js +120 -28
- package/dist/init.js +188 -64
- package/dist/inspect.js +21 -6
- package/dist/runtime/executor-digests.json +5 -5
- package/dist/skill.js +51 -9
- package/dist/standalone/api-description-inspect.d.ts +4 -0
- package/dist/standalone/api-description-inspect.js +12 -0
- package/dist/standalone/api-description.d.ts +7 -0
- package/dist/standalone/api-description.js +73 -0
- package/dist/standalone/build.d.ts +3 -0
- package/dist/standalone/build.js +140 -33
- package/dist/standalone/compose-errors.d.ts +3 -0
- package/dist/standalone/compose-errors.js +243 -0
- package/dist/standalone/compose.d.ts +97 -10
- package/dist/standalone/compose.js +306 -75
- package/dist/standalone/daemon.js +111 -29
- package/dist/standalone/dsm-window.js +39 -8
- package/dist/standalone/identity-gateway.d.ts +9 -2
- package/dist/standalone/identity-gateway.js +282 -66
- package/dist/standalone/inspect-spk.d.ts +12 -0
- package/dist/standalone/inspect-spk.js +666 -136
- package/dist/standalone/lifecycle.js +124 -42
- package/dist/standalone/manifest.d.ts +8 -1
- package/dist/standalone/manifest.js +303 -86
- package/dist/standalone/resource.js +6 -2
- package/dist/standalone/spk.d.ts +4 -1
- package/dist/standalone/spk.js +138 -39
- package/dist/template-paths.d.ts +0 -2
- package/dist/template-paths.js +0 -5
- package/dist/test-command.d.ts +1 -1
- package/dist/test-command.js +1 -1
- package/dist/validate.d.ts +3 -3
- package/dist/validate.js +32 -140
- package/package.json +5 -3
- package/schema/synopkgland-api-v1.schema.json +423 -0
- package/skills/SKILL.md +4 -2
- package/skills/reference/compose.md +349 -0
- package/skills/reference/create.md +9 -18
- package/skills/reference/ctl.md +41 -0
- package/skills/reference/deploy.md +1 -1
- package/skills/reference/files.md +5 -1
- package/skills/reference/identity-gateway.md +8 -144
- package/skills/reference/wrap.md +10 -39
- package/assets/go/daemon/fixture_linux_test.go +0 -15
- package/assets/go/daemon/fixture_other_test.go +0 -7
- package/assets/go/daemon/go.mod +0 -5
- package/assets/go/daemon/main.go +0 -636
- package/assets/go/daemon/main_test.go +0 -315
- package/assets/go/daemon/network.go +0 -107
- package/assets/go/daemon/network_test.go +0 -86
- package/assets/go/daemon/process_linux.go +0 -55
- package/assets/go/daemon/process_linux_test.go +0 -161
- package/assets/go/daemon/process_other.go +0 -14
- package/assets/go/daemon/testdata/child.go +0 -44
- package/assets/go/gateway/auth.go +0 -101
- package/assets/go/gateway/auth_test.go +0 -216
- package/assets/go/gateway/cgi.go +0 -133
- package/assets/go/gateway/cgi_test.go +0 -156
- package/assets/go/gateway/csrf.go +0 -73
- package/assets/go/gateway/csrf_test.go +0 -155
- package/assets/go/gateway/directory.go +0 -108
- package/assets/go/gateway/directory_test.go +0 -202
- package/assets/go/gateway/gateway.go +0 -412
- package/assets/go/gateway/gateway_test.go +0 -676
- package/assets/go/gateway/go.mod +0 -5
- package/assets/go/gateway/groups.go +0 -66
- package/assets/go/gateway/main.go +0 -45
- package/dist/build-spk/gateway.d.ts +0 -31
- package/dist/build-spk/gateway.js +0 -60
- package/dist/compose/privilege.d.ts +0 -14
- package/dist/compose/privilege.js +0 -102
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
package main
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"bufio"
|
|
5
|
-
"bytes"
|
|
6
|
-
"io"
|
|
7
|
-
"net/http"
|
|
8
|
-
"net/http/httptest"
|
|
9
|
-
"strings"
|
|
10
|
-
"testing"
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
func cgiRequest(t *testing.T) *http.Request {
|
|
14
|
-
t.Helper()
|
|
15
|
-
return httptest.NewRequest("GET", "/webman/3rdparty/App/cgi/gateway.cgi/", nil)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
func TestCGIReplyIsAStatusLineThenHeadersThenBody(t *testing.T) {
|
|
19
|
-
var out bytes.Buffer
|
|
20
|
-
err := serveCGI(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
21
|
-
w.Header().Set("Content-Type", "text/plain")
|
|
22
|
-
w.Header().Set("X-Thing", "1")
|
|
23
|
-
w.WriteHeader(404)
|
|
24
|
-
io.WriteString(w, "nope")
|
|
25
|
-
}), cgiRequest(t), strings.NewReader(""), &out)
|
|
26
|
-
if err != nil {
|
|
27
|
-
t.Fatal(err)
|
|
28
|
-
}
|
|
29
|
-
if want := "Status: 404 Not Found\r\nContent-Type: text/plain\r\nX-Thing: 1\r\n\r\nnope"; out.String() != want {
|
|
30
|
-
t.Fatalf("got %q\nwant %q", out.String(), want)
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
func TestCGIReplyWithoutAWriteStillCarriesAStatus(t *testing.T) {
|
|
35
|
-
var out bytes.Buffer
|
|
36
|
-
if err := serveCGI(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}), cgiRequest(t), strings.NewReader(""), &out); err != nil {
|
|
37
|
-
t.Fatal(err)
|
|
38
|
-
}
|
|
39
|
-
if !strings.HasPrefix(out.String(), "Status: 200 OK\r\n") || !strings.HasSuffix(out.String(), "\r\n\r\n") {
|
|
40
|
-
t.Fatalf("got %q", out.String())
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
func TestCGIReplySniffsAMissingContentType(t *testing.T) {
|
|
45
|
-
var out bytes.Buffer
|
|
46
|
-
serveCGI(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "<html></html>") }), cgiRequest(t), strings.NewReader(""), &out)
|
|
47
|
-
if !strings.Contains(out.String(), "Content-Type: text/html; charset=utf-8\r\n") {
|
|
48
|
-
t.Fatalf("got %q", out.String())
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// flushRecorder notes how many bytes were out when the handler was still running.
|
|
53
|
-
type flushRecorder struct {
|
|
54
|
-
bytes.Buffer
|
|
55
|
-
atFlush int
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
func TestCGIFlushSendsWhatIsBufferedAtOnce(t *testing.T) {
|
|
59
|
-
var out flushRecorder
|
|
60
|
-
serveCGI(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
61
|
-
w.Header().Set("Content-Type", "text/event-stream")
|
|
62
|
-
io.WriteString(w, "data: one\n\n")
|
|
63
|
-
w.(http.Flusher).Flush()
|
|
64
|
-
out.atFlush = out.Len()
|
|
65
|
-
io.WriteString(w, "data: two\n\n")
|
|
66
|
-
}), cgiRequest(t), strings.NewReader(""), &out)
|
|
67
|
-
if !strings.HasSuffix(out.String(), "data: one\n\ndata: two\n\n") || out.atFlush < len("data: one\n\n") {
|
|
68
|
-
t.Fatalf("flushed %d bytes before the handler ended; total %q", out.atFlush, out.String())
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// After a hijack stdin and stdout belong to the handler. What it writes first is an HTTP/1.1 status line;
|
|
73
|
-
// DSM's CGI interface wants "Status:" instead.
|
|
74
|
-
func TestCGIHijackTurnsTheStatusLineIntoACGIHeaderAndPassesBytesBothWays(t *testing.T) {
|
|
75
|
-
in, inW := io.Pipe()
|
|
76
|
-
outR, out := io.Pipe()
|
|
77
|
-
done := make(chan error, 1)
|
|
78
|
-
go func() {
|
|
79
|
-
done <- serveCGI(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
80
|
-
conn, brw, err := w.(http.Hijacker).Hijack()
|
|
81
|
-
if err != nil {
|
|
82
|
-
t.Error(err)
|
|
83
|
-
return
|
|
84
|
-
}
|
|
85
|
-
defer conn.Close()
|
|
86
|
-
io.WriteString(brw, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n")
|
|
87
|
-
brw.Flush()
|
|
88
|
-
buf := make([]byte, 16)
|
|
89
|
-
n, _ := brw.Read(buf)
|
|
90
|
-
io.WriteString(conn, "got:"+string(buf[:n]))
|
|
91
|
-
}), cgiRequest(t), in, out)
|
|
92
|
-
out.Close()
|
|
93
|
-
}()
|
|
94
|
-
r := bufio.NewReader(outR)
|
|
95
|
-
for _, want := range []string{"Status: 101 Switching Protocols", "Upgrade: websocket", "Connection: Upgrade", ""} {
|
|
96
|
-
if line, _ := r.ReadString('\n'); strings.TrimRight(line, "\r\n") != want {
|
|
97
|
-
t.Fatalf("header line %q, want %q", line, want)
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
io.WriteString(inW, "ping")
|
|
101
|
-
rest, _ := io.ReadAll(r)
|
|
102
|
-
if string(rest) != "got:ping" {
|
|
103
|
-
t.Fatalf("after the header block: %q (nothing, not even a second status, may follow the handler's bytes)", rest)
|
|
104
|
-
}
|
|
105
|
-
if err := <-done; err != nil {
|
|
106
|
-
t.Fatal(err)
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
func TestCGIReplyCannotBeWrittenAfterAHijack(t *testing.T) {
|
|
111
|
-
var out bytes.Buffer
|
|
112
|
-
serveCGI(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
113
|
-
if _, _, err := w.(http.Hijacker).Hijack(); err != nil {
|
|
114
|
-
t.Fatal(err)
|
|
115
|
-
}
|
|
116
|
-
if _, err := w.Write([]byte("late")); err == nil {
|
|
117
|
-
t.Error("Write after Hijack succeeded")
|
|
118
|
-
}
|
|
119
|
-
}), cgiRequest(t), strings.NewReader(""), &out)
|
|
120
|
-
if out.Len() != 0 {
|
|
121
|
-
t.Fatalf("output after a bare hijack: %q", out.String())
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// The whole chain as DSM runs it: a WebSocket upgrade enters through the CGI environment and the container's
|
|
126
|
-
// bytes come back on stdout.
|
|
127
|
-
func TestWebSocketThroughTheCGIEnvironment(t *testing.T) {
|
|
128
|
-
up := newUpstream(t, wsEchoHandler)
|
|
129
|
-
g := newTestGateway(t, config{Access: "users"}, &fakeAuth{id: identity{User: "bob"}}, up)
|
|
130
|
-
req := httptest.NewRequest("GET", base+"/ws", nil)
|
|
131
|
-
req.Host = "nas.example:5001"
|
|
132
|
-
req.Header.Set("Cookie", "id=abc")
|
|
133
|
-
req.Header.Set("Connection", "Upgrade")
|
|
134
|
-
req.Header.Set("Upgrade", "websocket")
|
|
135
|
-
req.Header.Set("Origin", "https://nas.example:5001")
|
|
136
|
-
in, inW := io.Pipe()
|
|
137
|
-
outR, out := io.Pipe()
|
|
138
|
-
done := make(chan error, 1)
|
|
139
|
-
go func() { done <- serveCGI(g, req, in, out); out.Close() }()
|
|
140
|
-
r := bufio.NewReader(outR)
|
|
141
|
-
if line, _ := r.ReadString('\n'); strings.TrimSpace(line) != "Status: 101 Switching Protocols" {
|
|
142
|
-
t.Fatalf("first line %q", line)
|
|
143
|
-
}
|
|
144
|
-
for line, _ := r.ReadString('\n'); strings.TrimSpace(line) != ""; line, _ = r.ReadString('\n') {
|
|
145
|
-
}
|
|
146
|
-
io.WriteString(inW, "hello")
|
|
147
|
-
got := make([]byte, len("echo:hello"))
|
|
148
|
-
if _, err := io.ReadFull(r, got); err != nil || string(got) != "echo:hello" {
|
|
149
|
-
t.Fatalf("echo = %q, %v", got, err)
|
|
150
|
-
}
|
|
151
|
-
inW.Close() // DSM closes stdin when the browser goes away
|
|
152
|
-
io.Copy(io.Discard, r)
|
|
153
|
-
if err := <-done; err != nil {
|
|
154
|
-
t.Fatal(err)
|
|
155
|
-
}
|
|
156
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
package main
|
|
2
|
-
|
|
3
|
-
// CSRF defence for state-changing requests. DSM issues a SynoToken only when
|
|
4
|
-
// "Enhance CSRF protection" (enable_syno_token in synoinfo.conf) is on; when it
|
|
5
|
-
// is off, DSM hands every session the same placeholder and validates nothing,
|
|
6
|
-
// so the token alone proves nothing. The gateway therefore needs evidence that
|
|
7
|
-
// the browser itself made the request from the gateway's own origin, and it
|
|
8
|
-
// never stores or trusts a token value of its own.
|
|
9
|
-
|
|
10
|
-
import (
|
|
11
|
-
"net/http"
|
|
12
|
-
"net/url"
|
|
13
|
-
"strings"
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
const maxTokenLen = 256
|
|
17
|
-
|
|
18
|
-
// wellFormedToken accepts any printable ASCII value of sane length. Whether the
|
|
19
|
-
// value is the right one is DSM's call (authenticate.cgi gets it on every
|
|
20
|
-
// request); the gateway only refuses requests that carry nothing usable.
|
|
21
|
-
func wellFormedToken(token string) bool {
|
|
22
|
-
if token == "" || len(token) > maxTokenLen {
|
|
23
|
-
return false
|
|
24
|
-
}
|
|
25
|
-
for i := 0; i < len(token); i++ {
|
|
26
|
-
if token[i] < 0x20 || token[i] > 0x7e {
|
|
27
|
-
return false
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return true
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// sameOrigin reports whether the browser says the request came from the page's
|
|
34
|
-
// own origin. Sec-Fetch-Site, when present, must say same-origin; Origin, when
|
|
35
|
-
// present, must be the request's own host. At least one must be present: a
|
|
36
|
-
// browser sends Origin on every cross-origin and every non-GET request, so a
|
|
37
|
-
// state-changing request with neither is not from a browser that can be trusted
|
|
38
|
-
// to scope it (browsers without either, such as IE 11, are refused).
|
|
39
|
-
func sameOrigin(r *http.Request) bool {
|
|
40
|
-
site := r.Header.Get("Sec-Fetch-Site")
|
|
41
|
-
origin := r.Header.Get("Origin")
|
|
42
|
-
if site == "" && origin == "" {
|
|
43
|
-
return false
|
|
44
|
-
}
|
|
45
|
-
if site != "" && site != "same-origin" {
|
|
46
|
-
return false
|
|
47
|
-
}
|
|
48
|
-
return origin == "" || originMatchesHost(origin, r.Host)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// originMatchesHost compares an Origin header (scheme://host[:port], no path)
|
|
52
|
-
// with the Host the request was addressed to. The scheme only decides which
|
|
53
|
-
// port is the default, because a CGI does not reliably learn whether the
|
|
54
|
-
// connection was TLS.
|
|
55
|
-
func originMatchesHost(origin, host string) bool {
|
|
56
|
-
u, err := url.Parse(origin)
|
|
57
|
-
if err != nil || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") || (u.Path != "" && u.Path != "/") || u.User != nil || u.RawQuery != "" || u.Fragment != "" {
|
|
58
|
-
return false
|
|
59
|
-
}
|
|
60
|
-
return withDefaultPort(u.Host, u.Scheme) == withDefaultPort(host, u.Scheme)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
func withDefaultPort(host, scheme string) string {
|
|
64
|
-
host = strings.ToLower(host)
|
|
65
|
-
i := strings.LastIndex(host, ":")
|
|
66
|
-
if i >= 0 && !strings.Contains(host[i:], "]") {
|
|
67
|
-
return host
|
|
68
|
-
}
|
|
69
|
-
if scheme == "https" {
|
|
70
|
-
return host + ":443"
|
|
71
|
-
}
|
|
72
|
-
return host + ":80"
|
|
73
|
-
}
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
package main
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"net/http"
|
|
5
|
-
"net/http/httptest"
|
|
6
|
-
"strings"
|
|
7
|
-
"testing"
|
|
8
|
-
)
|
|
9
|
-
|
|
10
|
-
// browserPost is what a browser sends for a same-origin fetch to the gateway
|
|
11
|
-
// (httptest requests are addressed to example.com).
|
|
12
|
-
func browserPost(token string) map[string]string {
|
|
13
|
-
return map[string]string{
|
|
14
|
-
"Cookie": "id=abc",
|
|
15
|
-
"X-SYNO-TOKEN": token,
|
|
16
|
-
"Origin": "http://example.com",
|
|
17
|
-
"Sec-Fetch-Site": "same-origin",
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
func with(hdr map[string]string, kv ...string) map[string]string {
|
|
22
|
-
out := map[string]string{}
|
|
23
|
-
for k, v := range hdr {
|
|
24
|
-
out[k] = v
|
|
25
|
-
}
|
|
26
|
-
for i := 0; i < len(kv); i += 2 {
|
|
27
|
-
if kv[i+1] == "" {
|
|
28
|
-
delete(out, kv[i])
|
|
29
|
-
} else {
|
|
30
|
-
out[kv[i]] = kv[i+1]
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return out
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
func csrfGateway(t *testing.T) (http.Handler, *upstream) {
|
|
37
|
-
t.Helper()
|
|
38
|
-
up := newUpstream(t, hello)
|
|
39
|
-
return newTestGateway(t, config{Access: "users"}, &fakeAuth{id: identity{User: "bob"}}, up), up
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
func TestSameOriginStateChangeReachesContainer(t *testing.T) {
|
|
43
|
-
g, up := csrfGateway(t)
|
|
44
|
-
cases := map[string]map[string]string{
|
|
45
|
-
"origin and fetch metadata": browserPost("tok"),
|
|
46
|
-
"origin only (no fetch metadata, e.g. Safari < 16.4)": with(browserPost("tok"), "Sec-Fetch-Site", ""),
|
|
47
|
-
"fetch metadata only": with(browserPost("tok"), "Origin", ""),
|
|
48
|
-
"host case and default port": with(browserPost("tok"), "Origin", "http://EXAMPLE.com:80"),
|
|
49
|
-
}
|
|
50
|
-
for name, hdr := range cases {
|
|
51
|
-
if w := do(g, "POST", base+"/save", hdr, "x"); w.Code != 200 {
|
|
52
|
-
t.Errorf("%s: %d %q", name, w.Code, w.Body.String())
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
if up.hits.Load() != int32(len(cases)) {
|
|
56
|
-
t.Fatalf("hits=%d", up.hits.Load())
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
func TestCrossSiteStateChangeIsRefusedBeforeTheContainer(t *testing.T) {
|
|
61
|
-
g, up := csrfGateway(t)
|
|
62
|
-
cases := map[string]map[string]string{
|
|
63
|
-
"foreign origin": with(browserPost("tok"), "Origin", "https://evil.example", "Sec-Fetch-Site", ""),
|
|
64
|
-
"foreign origin, claims same": with(browserPost("tok"), "Origin", "https://evil.example"),
|
|
65
|
-
"origin null": with(browserPost("tok"), "Origin", "null", "Sec-Fetch-Site", ""),
|
|
66
|
-
"origin is not a URL": with(browserPost("tok"), "Origin", "::::", "Sec-Fetch-Site", ""),
|
|
67
|
-
"origin has a path": with(browserPost("tok"), "Origin", "http://example.com/x", "Sec-Fetch-Site", ""),
|
|
68
|
-
"same host other port": with(browserPost("tok"), "Origin", "http://example.com:5001", "Sec-Fetch-Site", ""),
|
|
69
|
-
"sub-domain of the host": with(browserPost("tok"), "Origin", "http://evil.example.com", "Sec-Fetch-Site", ""),
|
|
70
|
-
"fetch metadata cross-site": with(browserPost("tok"), "Sec-Fetch-Site", "cross-site"),
|
|
71
|
-
"fetch metadata same-site": with(browserPost("tok"), "Sec-Fetch-Site", "same-site"),
|
|
72
|
-
"fetch metadata none": with(browserPost("tok"), "Sec-Fetch-Site", "none"),
|
|
73
|
-
"fetch metadata unknown": with(browserPost("tok"), "Sec-Fetch-Site", "whatever"),
|
|
74
|
-
"fetch cross-site, origin same": with(browserPost("tok"), "Sec-Fetch-Site", "cross-site", "Origin", "http://example.com"),
|
|
75
|
-
}
|
|
76
|
-
for name, hdr := range cases {
|
|
77
|
-
w := do(g, "POST", base+"/save", hdr, "x")
|
|
78
|
-
if !refusedAsForbidden(w, "cross_site_blocked") {
|
|
79
|
-
t.Errorf("%s: %d %q", name, w.Code, w.Body.String())
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
if up.hits.Load() != 0 {
|
|
83
|
-
t.Fatalf("hits=%d", up.hits.Load())
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
func TestStateChangeWithoutAnyOriginEvidenceIsRefused(t *testing.T) {
|
|
88
|
-
g, up := csrfGateway(t)
|
|
89
|
-
hdr := with(browserPost("tok"), "Origin", "", "Sec-Fetch-Site", "")
|
|
90
|
-
w := do(g, "POST", base+"/save", hdr, "x")
|
|
91
|
-
if !refusedAsForbidden(w, "cross_site_blocked") || up.hits.Load() != 0 {
|
|
92
|
-
t.Fatalf("%d %q hits=%d", w.Code, w.Body.String(), up.hits.Load())
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
func TestStateChangeNeedsAWellFormedToken(t *testing.T) {
|
|
97
|
-
g, up := csrfGateway(t)
|
|
98
|
-
for name, token := range map[string]string{
|
|
99
|
-
"missing": "",
|
|
100
|
-
"too long": strings.Repeat("a", 257),
|
|
101
|
-
"control bytes": "to\x01ken",
|
|
102
|
-
"non ASCII": "tök",
|
|
103
|
-
} {
|
|
104
|
-
hdr := with(browserPost("tok"), "X-SYNO-TOKEN", "")
|
|
105
|
-
if token != "" {
|
|
106
|
-
hdr["X-SYNO-TOKEN"] = token
|
|
107
|
-
}
|
|
108
|
-
w := do(g, "POST", base+"/save", hdr, "x")
|
|
109
|
-
if !refusedAsForbidden(w, "token_required") {
|
|
110
|
-
t.Errorf("%s: %d %q", name, w.Code, w.Body.String())
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
if up.hits.Load() != 0 {
|
|
114
|
-
t.Fatalf("hits=%d", up.hits.Load())
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
func TestSafeRequestsIgnoreOriginEvidence(t *testing.T) {
|
|
119
|
-
g, _ := csrfGateway(t)
|
|
120
|
-
hdr := map[string]string{"Cookie": "id=abc", "Origin": "https://evil.example", "Sec-Fetch-Site": "cross-site"}
|
|
121
|
-
for _, method := range []string{"GET", "HEAD"} {
|
|
122
|
-
if w := do(g, method, base+"/", hdr, ""); w.Code != 200 {
|
|
123
|
-
t.Errorf("%s: %d", method, w.Code)
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
func TestOriginMatchIgnoresDefaultPortsOnly(t *testing.T) {
|
|
129
|
-
cases := []struct {
|
|
130
|
-
origin, host string
|
|
131
|
-
want bool
|
|
132
|
-
}{
|
|
133
|
-
{"http://nas.local:5000", "nas.local:5000", true},
|
|
134
|
-
{"https://nas.local:5001", "nas.local:5001", true},
|
|
135
|
-
{"https://nas.local", "nas.local:443", true},
|
|
136
|
-
{"http://nas.local", "nas.local", true},
|
|
137
|
-
{"https://nas.local:5001", "nas.local:5000", false},
|
|
138
|
-
{"http://[::1]:5000", "[::1]:5000", true},
|
|
139
|
-
{"http://nas.local", "nas.local:5000", false},
|
|
140
|
-
{"", "nas.local", false},
|
|
141
|
-
}
|
|
142
|
-
for _, c := range cases {
|
|
143
|
-
if got := originMatchesHost(c.origin, c.host); got != c.want {
|
|
144
|
-
t.Errorf("originMatchesHost(%q, %q) = %v", c.origin, c.host, got)
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// refusedAsForbidden: the gateway means 403, but DSM would replace a 403 body, so it sends 401
|
|
150
|
-
// with the intended status and the reason in headers.
|
|
151
|
-
func refusedAsForbidden(w *httptest.ResponseRecorder, code string) bool {
|
|
152
|
-
h := w.Header()
|
|
153
|
-
return w.Code == http.StatusUnauthorized && h.Get("X-Synopkgland-Status") == "403" &&
|
|
154
|
-
h.Get("X-Synopkgland-Error") == code && strings.Contains(w.Body.String(), code)
|
|
155
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
package main
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"context"
|
|
5
|
-
"errors"
|
|
6
|
-
"fmt"
|
|
7
|
-
"os/exec"
|
|
8
|
-
"regexp"
|
|
9
|
-
"strconv"
|
|
10
|
-
"strings"
|
|
11
|
-
"time"
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
const maxDirectoryOutput = 64 << 10
|
|
15
|
-
const maxDirectoryGroups = 256
|
|
16
|
-
|
|
17
|
-
// DSM's dynamic /usr/bin/id uses libc NSS; os/user in the static CGI does not.
|
|
18
|
-
// A fixed executable, a single validated username argument and a clean locale
|
|
19
|
-
// environment keep the browser's credentials and process environment out of NSS.
|
|
20
|
-
// No SDK cache: query once per authenticated request, with a two-second limit.
|
|
21
|
-
type directoryLookup struct {
|
|
22
|
-
timeout time.Duration
|
|
23
|
-
run func(context.Context, string, []string, []string) ([]byte, error)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
func (d directoryLookup) groups(ctx context.Context, user string) ([]string, error) {
|
|
27
|
-
if !userName.MatchString(user) || strings.HasPrefix(user, "-") {
|
|
28
|
-
return nil, errors.New("invalid directory account")
|
|
29
|
-
}
|
|
30
|
-
ctx, cancel := context.WithTimeout(ctx, d.timeout)
|
|
31
|
-
defer cancel()
|
|
32
|
-
out, err := d.run(ctx, "/usr/bin/id", []string{user}, []string{"LC_ALL=C"})
|
|
33
|
-
if ctx.Err() != nil {
|
|
34
|
-
return nil, fmt.Errorf("directory groups: %w", ctx.Err())
|
|
35
|
-
}
|
|
36
|
-
if err != nil {
|
|
37
|
-
return nil, errors.New("directory group lookup failed")
|
|
38
|
-
}
|
|
39
|
-
return parseDirectoryGroups(out)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Bounded stdout also bounds memory if a directory returns an excessive list.
|
|
43
|
-
// Never include command output in errors: it is not a diagnostic credential path.
|
|
44
|
-
type directoryOutput struct{ data []byte }
|
|
45
|
-
|
|
46
|
-
func (b *directoryOutput) Write(p []byte) (int, error) {
|
|
47
|
-
if len(p) > maxDirectoryOutput-len(b.data) {
|
|
48
|
-
return 0, errors.New("directory output exceeds limit")
|
|
49
|
-
}
|
|
50
|
-
b.data = append(b.data, p...)
|
|
51
|
-
return len(p), nil
|
|
52
|
-
}
|
|
53
|
-
func runDirectoryCommand(ctx context.Context, path string, args, env []string) ([]byte, error) {
|
|
54
|
-
cmd := exec.CommandContext(ctx, path, args...)
|
|
55
|
-
cmd.Env = env
|
|
56
|
-
cmd.WaitDelay = 100 * time.Millisecond
|
|
57
|
-
var out directoryOutput
|
|
58
|
-
cmd.Stdout = &out
|
|
59
|
-
err := cmd.Run()
|
|
60
|
-
return out.data, err
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
var directoryIDLine = regexp.MustCompile(`^uid=([0-9]+)\(([^()\r\n]+)\) gid=([0-9]+)\(([^()\r\n]+)\) groups=([^\r\n]+)\n?$`)
|
|
64
|
-
var directoryGroupEntry = regexp.MustCompile(`^([0-9]+)\(([^()\r\n]+)\)$`)
|
|
65
|
-
|
|
66
|
-
// Parse full id output, not id -Gn: the latter loses boundaries when AD group
|
|
67
|
-
// names contain spaces (for example VT-NAS\Domain Users). Require every gid to
|
|
68
|
-
// resolve to a bounded valid name, and require the primary group in the list.
|
|
69
|
-
// Unresolved numeric gids, partial/malformed output and ambiguous delimiters deny.
|
|
70
|
-
func parseDirectoryGroups(out []byte) ([]string, error) {
|
|
71
|
-
invalid := errors.New("incomplete or invalid directory groups")
|
|
72
|
-
if len(out) > maxDirectoryOutput {
|
|
73
|
-
return nil, invalid
|
|
74
|
-
}
|
|
75
|
-
m := directoryIDLine.FindStringSubmatch(string(out))
|
|
76
|
-
if m == nil || !validDirectoryID(m[1]) || !validDirectoryID(m[3]) || !userName.MatchString(m[2]) || !groupName.MatchString(m[4]) {
|
|
77
|
-
return nil, invalid
|
|
78
|
-
}
|
|
79
|
-
entries := strings.Split(m[5], ",")
|
|
80
|
-
if len(entries) > maxDirectoryGroups {
|
|
81
|
-
return nil, invalid
|
|
82
|
-
}
|
|
83
|
-
groups := make([]string, 0, len(entries))
|
|
84
|
-
seen := map[string]bool{}
|
|
85
|
-
primary := false
|
|
86
|
-
for _, entry := range entries {
|
|
87
|
-
g := directoryGroupEntry.FindStringSubmatch(entry)
|
|
88
|
-
if g == nil || !validDirectoryID(g[1]) || !groupName.MatchString(g[2]) || seen[g[1]] {
|
|
89
|
-
return nil, invalid
|
|
90
|
-
}
|
|
91
|
-
seen[g[1]] = true
|
|
92
|
-
if g[1] == m[3] {
|
|
93
|
-
if g[2] != m[4] {
|
|
94
|
-
return nil, invalid
|
|
95
|
-
}
|
|
96
|
-
primary = true
|
|
97
|
-
}
|
|
98
|
-
groups = append(groups, g[2])
|
|
99
|
-
}
|
|
100
|
-
if !primary {
|
|
101
|
-
return nil, invalid
|
|
102
|
-
}
|
|
103
|
-
return groups, nil
|
|
104
|
-
}
|
|
105
|
-
func validDirectoryID(value string) bool {
|
|
106
|
-
n, err := strconv.ParseUint(value, 10, 32)
|
|
107
|
-
return err == nil && n < 1<<32-1 && strconv.FormatUint(n, 10) == value
|
|
108
|
-
}
|
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
package main
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"context"
|
|
5
|
-
"errors"
|
|
6
|
-
"net/http/httptest"
|
|
7
|
-
"os"
|
|
8
|
-
"slices"
|
|
9
|
-
"strings"
|
|
10
|
-
"testing"
|
|
11
|
-
"time"
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
const directoryID = `uid=2969567732(VT-NAS\Administrator) gid=2969567745(VT-NAS\Domain Users) groups=2969567745(VT-NAS\Domain Users),2969567751(VT-NAS\Enterprise Admins)`
|
|
15
|
-
|
|
16
|
-
func TestDirectoryIdentityUsesNSSIncludingPrimaryAndSupplementaryGroups(t *testing.T) {
|
|
17
|
-
a := newTestAuth(t, &fakeRunner{out: `VT-NAS\Administrator`})
|
|
18
|
-
var gotUser string
|
|
19
|
-
a.directoryGroups = func(ctx context.Context, user string) ([]string, error) {
|
|
20
|
-
gotUser = user
|
|
21
|
-
if _, ok := ctx.Deadline(); !ok {
|
|
22
|
-
t.Fatal("missing deadline")
|
|
23
|
-
}
|
|
24
|
-
return parseDirectoryGroups([]byte(directoryID))
|
|
25
|
-
}
|
|
26
|
-
id, err := a.Authenticate(sessionRequest())
|
|
27
|
-
if err != nil || gotUser != `VT-NAS\Administrator` || !slices.Equal(id.Groups, []string{`VT-NAS\Domain Users`, `VT-NAS\Enterprise Admins`}) {
|
|
28
|
-
t.Fatalf("id=%+v user=%q err=%v", id, gotUser, err)
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
func TestLocalIdentityNeverNeedsDirectoryLookup(t *testing.T) {
|
|
33
|
-
a := newTestAuth(t, &fakeRunner{out: "alice"})
|
|
34
|
-
a.directoryGroups = func(context.Context, string) ([]string, error) {
|
|
35
|
-
t.Fatal("directory lookup for local account")
|
|
36
|
-
return nil, nil
|
|
37
|
-
}
|
|
38
|
-
id, err := a.Authenticate(sessionRequest())
|
|
39
|
-
if err != nil || !slices.Contains(id.Groups, "staff") {
|
|
40
|
-
t.Fatalf("id=%+v err=%v", id, err)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
func TestDirectoryFailureCannotFallBackToLocalMemberLists(t *testing.T) {
|
|
45
|
-
a := newTestAuth(t, &fakeRunner{out: `OTHER\alice`})
|
|
46
|
-
if err := os.WriteFile(a.groupFile, []byte("staff:x:1030:OTHER\\alice\n"), 0o644); err != nil {
|
|
47
|
-
t.Fatal(err)
|
|
48
|
-
}
|
|
49
|
-
a.directoryGroups = func(context.Context, string) ([]string, error) { return nil, errors.New("directory unavailable") }
|
|
50
|
-
if _, err := a.Authenticate(sessionRequest()); err == nil || errors.Is(err, errNoSession) {
|
|
51
|
-
t.Fatalf("err=%v", err)
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
func TestDirectoryMembershipAndSessionAreRecheckedEachRequest(t *testing.T) {
|
|
56
|
-
runner := &fakeRunner{out: `VT-NAS\Administrator`}
|
|
57
|
-
a := newTestAuth(t, runner)
|
|
58
|
-
calls := 0
|
|
59
|
-
a.directoryGroups = func(context.Context, string) ([]string, error) {
|
|
60
|
-
calls++
|
|
61
|
-
if calls == 1 {
|
|
62
|
-
return []string{`VT-NAS\Domain Users`}, nil
|
|
63
|
-
}
|
|
64
|
-
return []string{`VT-NAS\Other`}, nil
|
|
65
|
-
}
|
|
66
|
-
id, _ := a.Authenticate(sessionRequest())
|
|
67
|
-
if !slices.Contains(id.Groups, `VT-NAS\Domain Users`) {
|
|
68
|
-
t.Fatal(id)
|
|
69
|
-
}
|
|
70
|
-
id, _ = a.Authenticate(sessionRequest())
|
|
71
|
-
if slices.Contains(id.Groups, `VT-NAS\Domain Users`) || calls != 2 {
|
|
72
|
-
t.Fatal(id, calls)
|
|
73
|
-
}
|
|
74
|
-
runner.err = &exitError{}
|
|
75
|
-
if _, err := a.Authenticate(sessionRequest()); !errors.Is(err, errNoSession) || calls != 2 {
|
|
76
|
-
t.Fatal(err, calls)
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
func TestNSSParserPreservesQualifiedNamesAndFailsClosed(t *testing.T) {
|
|
81
|
-
for _, text := range []string{directoryID + "\n", `uid=123(alice@example.test) gid=200(staff@example.test) groups=200(staff@example.test),201(Domain Users@example.test)`} {
|
|
82
|
-
groups, err := parseDirectoryGroups([]byte(text))
|
|
83
|
-
if err != nil || len(groups) != 2 {
|
|
84
|
-
t.Fatalf("groups=%q err=%v", groups, err)
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
for _, text := range []string{"", "100 101", `uid=1(alice) gid=2(staff) groups=2`, `uid=1(alice) gid=2(staff) groups=3(ops)`, `uid=1(alice) gid=2(staff) groups=2(staff),3`, `uid=1(alice) gid=2(staff) groups=2(staff),3(bad(name))`, directoryID + "\n" + directoryID, `uid=1(alice) gid=2(staff) groups=2(staff),2(ops)`} {
|
|
88
|
-
if _, err := parseDirectoryGroups([]byte(text)); err == nil {
|
|
89
|
-
t.Fatalf("accepted %q", text)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
func TestDirectoryCommandIsBoundedAndHasNoSessionEnvironment(t *testing.T) {
|
|
95
|
-
var gotPath string
|
|
96
|
-
var gotArgs, gotEnv []string
|
|
97
|
-
lookup := directoryLookup{timeout: 20 * time.Millisecond, run: func(ctx context.Context, path string, args, env []string) ([]byte, error) {
|
|
98
|
-
gotPath, gotArgs, gotEnv = path, args, env
|
|
99
|
-
if _, ok := ctx.Deadline(); !ok {
|
|
100
|
-
t.Fatal("missing deadline")
|
|
101
|
-
}
|
|
102
|
-
return []byte(directoryID), nil
|
|
103
|
-
}}
|
|
104
|
-
if _, err := lookup.groups(context.Background(), `VT-NAS\Administrator`); err != nil {
|
|
105
|
-
t.Fatal(err)
|
|
106
|
-
}
|
|
107
|
-
if gotPath != "/usr/bin/id" || !slices.Equal(gotArgs, []string{`VT-NAS\Administrator`}) || !slices.Equal(gotEnv, []string{"LC_ALL=C"}) {
|
|
108
|
-
t.Fatal(gotPath, gotArgs, gotEnv)
|
|
109
|
-
}
|
|
110
|
-
for _, user := range []string{"", "-G", "--help", "alice\nadmin", "alice;id"} {
|
|
111
|
-
gotPath = ""
|
|
112
|
-
if _, err := lookup.groups(context.Background(), user); err == nil || gotPath != "" {
|
|
113
|
-
t.Fatal(user, err, gotPath)
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
lookup.run = func(ctx context.Context, _ string, _, _ []string) ([]byte, error) {
|
|
117
|
-
<-ctx.Done()
|
|
118
|
-
return nil, ctx.Err()
|
|
119
|
-
}
|
|
120
|
-
if _, err := lookup.groups(context.Background(), "alice"); !errors.Is(err, context.DeadlineExceeded) {
|
|
121
|
-
t.Fatal(err)
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
func TestDirectoryRulesKeepTheDomainAndExactCase(t *testing.T) {
|
|
126
|
-
for _, name := range []string{`VT-NAS\Domain Users`, `Domain Users@example.test`, `DOMAIN\staff`, `staff@example.test`} {
|
|
127
|
-
cfg := config{Upstream: "127.0.0.1:1", Access: accessGroups, Groups: []string{name}}
|
|
128
|
-
g, err := newGateway(cfg, &fakeAuth{})
|
|
129
|
-
if err != nil {
|
|
130
|
-
t.Fatal(err)
|
|
131
|
-
}
|
|
132
|
-
if !g.permits(identity{Groups: []string{name}}) {
|
|
133
|
-
t.Fatal(name)
|
|
134
|
-
}
|
|
135
|
-
for _, other := range []string{"Domain Users", "staff", `OTHER\Domain Users`, `VT-NAS\domain users`} {
|
|
136
|
-
if other != name && g.permits(identity{Groups: []string{other}}) {
|
|
137
|
-
t.Fatal(name, other)
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
g := &gateway{cfg: config{Access: accessAdmins}}
|
|
142
|
-
if g.permits(identity{Groups: []string{`DOMAIN\administrators`}}) {
|
|
143
|
-
t.Fatal("domain group became a local administrator")
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
func TestDirectoryLookupFailureNeverReachesUpstream(t *testing.T) {
|
|
148
|
-
a := newTestAuth(t, &fakeRunner{out: `DOMAIN\alice`})
|
|
149
|
-
a.directoryGroups = func(context.Context, string) ([]string, error) { return nil, errors.New("offline") }
|
|
150
|
-
g, err := newGateway(config{Upstream: "127.0.0.1:1", Access: accessGroups, Groups: []string{`DOMAIN\staff`}}, a)
|
|
151
|
-
if err != nil {
|
|
152
|
-
t.Fatal(err)
|
|
153
|
-
}
|
|
154
|
-
w := httptest.NewRecorder()
|
|
155
|
-
g.ServeHTTP(w, sessionRequest())
|
|
156
|
-
if w.Header().Get("X-Synopkgland-Error") != "auth_unavailable" {
|
|
157
|
-
t.Fatal(w.Code, w.Body.String())
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
func TestDirectoryIdentitySupportsQualifiedAccountLength(t *testing.T) {
|
|
162
|
-
user := "DOMAIN\\" + strings.Repeat("a", 100)
|
|
163
|
-
a := newTestAuth(t, &fakeRunner{out: user})
|
|
164
|
-
a.directoryGroups = func(context.Context, string) ([]string, error) { return []string{`DOMAIN\staff`}, nil }
|
|
165
|
-
id, err := a.Authenticate(sessionRequest())
|
|
166
|
-
if err != nil || id.User != user {
|
|
167
|
-
t.Fatal(id, err)
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
func TestNSSParserBoundsIDsGroupsAndOutput(t *testing.T) {
|
|
172
|
-
base := `uid=1(alice) gid=2(staff) groups=2(staff)`
|
|
173
|
-
for _, text := range []string{strings.Replace(base, "uid=1", "uid=4294967295", 1), strings.Replace(base, "gid=2", "gid=4294967296", 1), base + strings.Repeat(",3(ops)", 256), base + strings.Repeat(" ", maxDirectoryOutput), strings.Replace(base, "2(staff)", "02(staff)", -1)} {
|
|
174
|
-
if _, err := parseDirectoryGroups([]byte(text)); err == nil {
|
|
175
|
-
t.Fatal("accepted out-of-bounds output")
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
var output directoryOutput
|
|
179
|
-
if _, err := output.Write(make([]byte, maxDirectoryOutput)); err != nil {
|
|
180
|
-
t.Fatal(err)
|
|
181
|
-
}
|
|
182
|
-
if _, err := output.Write([]byte{1}); err == nil || len(output.data) != maxDirectoryOutput {
|
|
183
|
-
t.Fatal("output not bounded")
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
func TestNSSNonzeroExitAndCancellationDiscardEvenValidGroups(t *testing.T) {
|
|
188
|
-
for _, cancelled := range []bool{false, true} {
|
|
189
|
-
ctx, cancel := context.WithCancel(context.Background())
|
|
190
|
-
defer cancel()
|
|
191
|
-
d := directoryLookup{timeout: time.Second, run: func(context.Context, string, []string, []string) ([]byte, error) {
|
|
192
|
-
if cancelled {
|
|
193
|
-
cancel()
|
|
194
|
-
return []byte(directoryID), nil
|
|
195
|
-
}
|
|
196
|
-
return []byte(directoryID), errors.New("exit 1")
|
|
197
|
-
}}
|
|
198
|
-
if groups, err := d.groups(ctx, `VT-NAS\Administrator`); err == nil || len(groups) != 0 {
|
|
199
|
-
t.Fatal(groups, err)
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|