@synopackageland/cli 0.14.1 → 0.15.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/CHANGELOG.md +10 -0
- package/README.md +26 -16
- 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/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/error-registry.js +1020 -834
- package/dist/info.js +120 -28
- package/dist/init.js +188 -64
- package/dist/inspect.js +21 -6
- package/dist/runtime/executor-digests.json +1 -12
- 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 +3 -1
- package/skills/reference/compose.md +349 -0
- package/skills/reference/create.md +8 -17
- package/skills/reference/ctl.md +41 -0
- package/skills/reference/deploy.md +1 -1
- package/skills/reference/files.md +4 -0
- 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,676 +0,0 @@
|
|
|
1
|
-
package main
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"bufio"
|
|
5
|
-
"errors"
|
|
6
|
-
"io"
|
|
7
|
-
"net"
|
|
8
|
-
"net/http"
|
|
9
|
-
"net/http/httptest"
|
|
10
|
-
"slices"
|
|
11
|
-
"strconv"
|
|
12
|
-
"strings"
|
|
13
|
-
"sync/atomic"
|
|
14
|
-
"testing"
|
|
15
|
-
"time"
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
const base = "/webman/3rdparty/App/cgi/gateway.cgi"
|
|
19
|
-
|
|
20
|
-
type fakeAuth struct {
|
|
21
|
-
id identity
|
|
22
|
-
err error
|
|
23
|
-
calls int
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
func (f *fakeAuth) Authenticate(*http.Request) (identity, error) {
|
|
27
|
-
f.calls++
|
|
28
|
-
return f.id, f.err
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// upstream records what reached the container.
|
|
32
|
-
type upstream struct {
|
|
33
|
-
*httptest.Server
|
|
34
|
-
hits atomic.Int32
|
|
35
|
-
request atomic.Pointer[http.Request]
|
|
36
|
-
body atomic.Pointer[string]
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
func newUpstream(t *testing.T, handler http.HandlerFunc) *upstream {
|
|
40
|
-
t.Helper()
|
|
41
|
-
u := &upstream{}
|
|
42
|
-
u.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
43
|
-
u.hits.Add(1)
|
|
44
|
-
u.request.Store(r)
|
|
45
|
-
b, _ := io.ReadAll(r.Body)
|
|
46
|
-
s := string(b)
|
|
47
|
-
u.body.Store(&s)
|
|
48
|
-
handler(w, r)
|
|
49
|
-
}))
|
|
50
|
-
t.Cleanup(u.Close)
|
|
51
|
-
return u
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
func hello(w http.ResponseWriter, r *http.Request) {
|
|
55
|
-
w.Header().Set("Content-Type", "text/plain")
|
|
56
|
-
io.WriteString(w, "container says hi: "+r.URL.RequestURI())
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
func newTestGateway(t *testing.T, cfg config, auth authenticator, up *upstream) http.Handler {
|
|
60
|
-
t.Helper()
|
|
61
|
-
cfg.Upstream = up.Listener.Addr().String()
|
|
62
|
-
g, err := newGateway(cfg, auth)
|
|
63
|
-
if err != nil {
|
|
64
|
-
t.Fatal(err)
|
|
65
|
-
}
|
|
66
|
-
return g
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
func do(h http.Handler, method, target string, hdr map[string]string, body string) *httptest.ResponseRecorder {
|
|
70
|
-
r := httptest.NewRequest(method, target, strings.NewReader(body))
|
|
71
|
-
r.RemoteAddr = "192.0.2.7:51234"
|
|
72
|
-
for k, v := range hdr {
|
|
73
|
-
r.Header.Set(k, v)
|
|
74
|
-
}
|
|
75
|
-
w := httptest.NewRecorder()
|
|
76
|
-
h.ServeHTTP(w, r)
|
|
77
|
-
return w
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
var signedIn = map[string]string{"Cookie": "id=abc"}
|
|
81
|
-
|
|
82
|
-
func TestSignedInUserReachesContainer(t *testing.T) {
|
|
83
|
-
up := newUpstream(t, hello)
|
|
84
|
-
auth := &fakeAuth{id: identity{User: "alice"}}
|
|
85
|
-
w := do(newTestGateway(t, config{Access: "users"}, auth, up), "GET", base+"/index.html?x=1", signedIn, "")
|
|
86
|
-
if w.Code != 200 || w.Body.String() != "container says hi: /index.html?x=1" {
|
|
87
|
-
t.Fatalf("%d %q", w.Code, w.Body.String())
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
func TestAnonymousRequestIsRejectedAndNeverReachesContainer(t *testing.T) {
|
|
92
|
-
up := newUpstream(t, hello)
|
|
93
|
-
auth := &fakeAuth{err: errNoSession}
|
|
94
|
-
w := do(newTestGateway(t, config{Access: "users"}, auth, up), "GET", base+"/", nil, "")
|
|
95
|
-
if w.Code != 401 || up.hits.Load() != 0 {
|
|
96
|
-
t.Fatalf("code=%d hits=%d", w.Code, up.hits.Load())
|
|
97
|
-
}
|
|
98
|
-
if !strings.Contains(w.Body.String(), "session_required") {
|
|
99
|
-
t.Errorf("body %q", w.Body.String())
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
func TestExpiredSessionIsRejected(t *testing.T) {
|
|
104
|
-
up := newUpstream(t, hello)
|
|
105
|
-
auth := &fakeAuth{err: errNoSession}
|
|
106
|
-
w := do(newTestGateway(t, config{Access: "users"}, auth, up), "GET", base+"/", signedIn, "")
|
|
107
|
-
if w.Code != 401 || up.hits.Load() != 0 {
|
|
108
|
-
t.Fatalf("code=%d hits=%d", w.Code, up.hits.Load())
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
func TestAuthenticatorFailureFailsClosed(t *testing.T) {
|
|
113
|
-
up := newUpstream(t, hello)
|
|
114
|
-
auth := &fakeAuth{err: errors.New("authenticate.cgi missing")}
|
|
115
|
-
w := do(newTestGateway(t, config{Access: "users"}, auth, up), "GET", base+"/", signedIn, "")
|
|
116
|
-
if w.Code != 520 || up.hits.Load() != 0 {
|
|
117
|
-
t.Fatalf("code=%d hits=%d", w.Code, up.hits.Load())
|
|
118
|
-
}
|
|
119
|
-
if got := w.Header().Get("X-Synopkgland-Error"); got != "auth_unavailable" {
|
|
120
|
-
t.Errorf("X-Synopkgland-Error = %q", got)
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
func TestForgedIdentityHeadersDoNotAuthenticate(t *testing.T) {
|
|
125
|
-
up := newUpstream(t, hello)
|
|
126
|
-
auth := &fakeAuth{err: errNoSession}
|
|
127
|
-
forged := map[string]string{
|
|
128
|
-
"X-Syno-User": "admin",
|
|
129
|
-
"X-Syno-Admin": "true",
|
|
130
|
-
"X-Forwarded-User": "admin",
|
|
131
|
-
"X-Remote-User": "admin",
|
|
132
|
-
"Remote-User": "admin",
|
|
133
|
-
"X-Forwarded-For": "127.0.0.1",
|
|
134
|
-
"X-Real-IP": "127.0.0.1",
|
|
135
|
-
"Authorization": "Basic YWRtaW46YWRtaW4=",
|
|
136
|
-
"X-Syno-Token": "bogus",
|
|
137
|
-
"Cookie": "id=forged; did=forged",
|
|
138
|
-
}
|
|
139
|
-
w := do(newTestGateway(t, config{Access: "users"}, auth, up), "GET", base+"/", forged, "")
|
|
140
|
-
if w.Code != 401 || up.hits.Load() != 0 {
|
|
141
|
-
t.Fatalf("code=%d hits=%d", w.Code, up.hits.Load())
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
func TestContainerNeverSeesClientIdentityOrSessionHeaders(t *testing.T) {
|
|
146
|
-
up := newUpstream(t, hello)
|
|
147
|
-
auth := &fakeAuth{id: identity{User: "bob"}}
|
|
148
|
-
hdr := map[string]string{
|
|
149
|
-
"Cookie": "id=abc; did=xyz",
|
|
150
|
-
"X-Syno-Token": "tok",
|
|
151
|
-
"X-Syno-User": "admin",
|
|
152
|
-
"X-Syno-Admin": "true",
|
|
153
|
-
"X-Forwarded-User": "admin",
|
|
154
|
-
"X-Remote-User": "admin",
|
|
155
|
-
"Remote-User": "admin",
|
|
156
|
-
"Remote-Groups": "administrators",
|
|
157
|
-
"X-Forwarded-For": "10.0.0.9",
|
|
158
|
-
"Forwarded": "for=10.0.0.9",
|
|
159
|
-
"X-Real-IP": "10.0.0.9",
|
|
160
|
-
"X-Auth-Request-User": "admin",
|
|
161
|
-
"Accept-Language": "zh-TW",
|
|
162
|
-
}
|
|
163
|
-
w := do(newTestGateway(t, config{Access: "users"}, auth, up), "GET", base+"/?SynoToken=secret&a=b", hdr, "")
|
|
164
|
-
if w.Code != 200 {
|
|
165
|
-
t.Fatalf("code=%d", w.Code)
|
|
166
|
-
}
|
|
167
|
-
got := up.request.Load()
|
|
168
|
-
for _, name := range []string{"Cookie", "X-Syno-Token", "X-Forwarded-User", "X-Remote-User", "Remote-User", "Remote-Groups", "X-Forwarded-For", "Forwarded", "X-Real-Ip", "X-Auth-Request-User"} {
|
|
169
|
-
if v := got.Header.Get(name); v != "" {
|
|
170
|
-
t.Errorf("%s reached the container: %q", name, v)
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
if got.Header.Get("X-Syno-User") != "bob" || got.Header.Get("X-Syno-Admin") != "false" {
|
|
174
|
-
t.Errorf("identity = %q / %q, want gateway-set bob/false", got.Header.Get("X-Syno-User"), got.Header.Get("X-Syno-Admin"))
|
|
175
|
-
}
|
|
176
|
-
if got.Header.Get("Accept-Language") != "zh-TW" {
|
|
177
|
-
t.Errorf("ordinary header dropped")
|
|
178
|
-
}
|
|
179
|
-
if strings.Contains(got.URL.RawQuery, "SynoToken") || got.URL.Query().Get("a") != "b" {
|
|
180
|
-
t.Errorf("query = %q", got.URL.RawQuery)
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
func TestAdminHeaderIsTrueForAdministrators(t *testing.T) {
|
|
185
|
-
up := newUpstream(t, hello)
|
|
186
|
-
auth := &fakeAuth{id: identity{User: "alice", Groups: []string{"administrators"}}}
|
|
187
|
-
do(newTestGateway(t, config{Access: "users"}, auth, up), "GET", base+"/", signedIn, "")
|
|
188
|
-
if got := up.request.Load().Header.Get("X-Syno-Admin"); got != "true" {
|
|
189
|
-
t.Errorf("X-Syno-Admin = %q", got)
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
func TestAdminsOnlyRejectsOrdinaryUsers(t *testing.T) {
|
|
194
|
-
up := newUpstream(t, hello)
|
|
195
|
-
cfg := config{Access: "admins"}
|
|
196
|
-
g := newTestGateway(t, cfg, &fakeAuth{id: identity{User: "bob", Groups: []string{"staff"}}}, up)
|
|
197
|
-
if w := do(g, "GET", base+"/", signedIn, ""); w.Code != 401 || up.hits.Load() != 0 {
|
|
198
|
-
t.Fatalf("ordinary: code=%d hits=%d", w.Code, up.hits.Load())
|
|
199
|
-
} else if w.Header().Get("X-Synopkgland-Error") != "forbidden" || w.Header().Get("X-Synopkgland-Status") != "403" {
|
|
200
|
-
t.Errorf("headers: %v", w.Header())
|
|
201
|
-
}
|
|
202
|
-
g = newTestGateway(t, cfg, &fakeAuth{id: identity{User: "alice", Groups: []string{"administrators"}}}, up)
|
|
203
|
-
if w := do(g, "GET", base+"/", signedIn, ""); w.Code != 200 {
|
|
204
|
-
t.Fatalf("admin: code=%d", w.Code)
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
func TestGroupsRejectUsersOutsideEveryListedGroup(t *testing.T) {
|
|
209
|
-
up := newUpstream(t, hello)
|
|
210
|
-
cfg := config{Access: "groups", Groups: []string{"staff", "ops"}}
|
|
211
|
-
g := newTestGateway(t, cfg, &fakeAuth{id: identity{User: "bob", Groups: []string{"users"}}}, up)
|
|
212
|
-
if w := do(g, "GET", base+"/", signedIn, ""); w.Code != 401 || up.hits.Load() != 0 || !strings.Contains(w.Body.String(), "forbidden") {
|
|
213
|
-
t.Fatalf("outsider: code=%d hits=%d body=%q", w.Code, up.hits.Load(), w.Body.String())
|
|
214
|
-
}
|
|
215
|
-
g = newTestGateway(t, cfg, &fakeAuth{id: identity{User: "carol", Groups: []string{"ops"}}}, up)
|
|
216
|
-
if w := do(g, "GET", base+"/", signedIn, ""); w.Code != 200 {
|
|
217
|
-
t.Fatalf("member: code=%d", w.Code)
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
func TestGroupsAccessWithoutGroupsIsRefusedAtStartup(t *testing.T) {
|
|
222
|
-
for _, cfg := range []config{
|
|
223
|
-
{Upstream: "127.0.0.1:1", Access: "groups"},
|
|
224
|
-
{Upstream: "127.0.0.1:1", Access: "everyone"},
|
|
225
|
-
{Upstream: "127.0.0.1:1", Access: ""},
|
|
226
|
-
{Upstream: "10.0.0.5:80", Access: "users"},
|
|
227
|
-
{Upstream: "localhost:80", Access: "users"},
|
|
228
|
-
} {
|
|
229
|
-
if _, err := newGateway(cfg, &fakeAuth{}); err == nil {
|
|
230
|
-
t.Errorf("config %+v accepted", cfg)
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
if _, err := newGateway(config{Upstream: "127.0.0.1:18333", Access: "users"}, nil); err == nil {
|
|
234
|
-
t.Error("nil authenticator accepted")
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
func TestStateChangingRequestNeedsSynoToken(t *testing.T) {
|
|
239
|
-
up := newUpstream(t, hello)
|
|
240
|
-
g := newTestGateway(t, config{Access: "users"}, &fakeAuth{id: identity{User: "bob"}}, up)
|
|
241
|
-
for _, method := range []string{"POST", "PUT", "PATCH", "DELETE"} {
|
|
242
|
-
if w := do(g, method, base+"/", signedIn, "x"); w.Code != 401 || !strings.Contains(w.Body.String(), "token_required") || w.Header().Get("X-Synopkgland-Error") != "token_required" {
|
|
243
|
-
t.Errorf("%s without token: %d %q", method, w.Code, w.Body.String())
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
if up.hits.Load() != 0 {
|
|
247
|
-
t.Fatalf("hits=%d", up.hits.Load())
|
|
248
|
-
}
|
|
249
|
-
hdr := browserPost("tok")
|
|
250
|
-
if w := do(g, "POST", base+"/save", hdr, "payload"); w.Code != 200 || *up.body.Load() != "payload" {
|
|
251
|
-
t.Fatalf("with token: %d body=%q", w.Code, *up.body.Load())
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
func TestQueryTokenDoesNotSatisfyStateChangingRequests(t *testing.T) {
|
|
256
|
-
up := newUpstream(t, hello)
|
|
257
|
-
g := newTestGateway(t, config{Access: "users"}, &fakeAuth{id: identity{User: "bob"}}, up)
|
|
258
|
-
hdr := browserPost("")
|
|
259
|
-
delete(hdr, "X-SYNO-TOKEN")
|
|
260
|
-
for _, method := range []string{"POST", "PUT", "PATCH", "DELETE"} {
|
|
261
|
-
w := do(g, method, base+"/?SynoToken=secret", hdr, "x")
|
|
262
|
-
if w.Code != 401 || w.Header().Get("X-Synopkgland-Error") != "token_required" || strings.Contains(w.Body.String(), "secret") {
|
|
263
|
-
t.Errorf("%s with query token only: %d %q", method, w.Code, w.Body.String())
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
if up.hits.Load() != 0 {
|
|
267
|
-
t.Fatalf("hits=%d", up.hits.Load())
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
func TestSafeRequestsNeedNoSynoToken(t *testing.T) {
|
|
272
|
-
up := newUpstream(t, hello)
|
|
273
|
-
g := newTestGateway(t, config{Access: "users"}, &fakeAuth{id: identity{User: "bob"}}, up)
|
|
274
|
-
for _, method := range []string{"GET", "HEAD"} {
|
|
275
|
-
if w := do(g, method, base+"/", signedIn, ""); w.Code != 200 {
|
|
276
|
-
t.Errorf("%s: %d", method, w.Code)
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
func TestOversizedBodyIsRefused(t *testing.T) {
|
|
282
|
-
up := newUpstream(t, func(w http.ResponseWriter, r *http.Request) {
|
|
283
|
-
if _, err := io.ReadAll(r.Body); err != nil {
|
|
284
|
-
w.WriteHeader(413)
|
|
285
|
-
}
|
|
286
|
-
})
|
|
287
|
-
g := newTestGateway(t, config{Access: "users", MaxBodyBytes: 4}, &fakeAuth{id: identity{User: "bob"}}, up)
|
|
288
|
-
hdr := browserPost("tok")
|
|
289
|
-
if w := do(g, "POST", base+"/", hdr, "0123456789"); w.Code != 413 {
|
|
290
|
-
t.Fatalf("code=%d", w.Code)
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
func TestContainerDownIsServiceUnavailable(t *testing.T) {
|
|
295
|
-
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
296
|
-
if err != nil {
|
|
297
|
-
t.Fatal(err)
|
|
298
|
-
}
|
|
299
|
-
addr := l.Addr().String()
|
|
300
|
-
l.Close()
|
|
301
|
-
g, err := newGateway(config{Upstream: addr, Access: "users"}, &fakeAuth{id: identity{User: "bob"}})
|
|
302
|
-
if err != nil {
|
|
303
|
-
t.Fatal(err)
|
|
304
|
-
}
|
|
305
|
-
w := do(g, "GET", base+"/", signedIn, "")
|
|
306
|
-
if w.Code != 521 || w.Header().Get("Retry-After") == "" || !strings.Contains(w.Body.String(), "upstream_unavailable") {
|
|
307
|
-
t.Fatalf("code=%d retry=%q body=%q", w.Code, w.Header().Get("Retry-After"), w.Body.String())
|
|
308
|
-
}
|
|
309
|
-
if w.Header().Get("X-Synopkgland-Error") != "upstream_unavailable" || w.Header().Get("X-Synopkgland-Status") != "503" {
|
|
310
|
-
t.Errorf("headers: %v", w.Header())
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
func TestSlowContainerIsGatewayTimeout(t *testing.T) {
|
|
315
|
-
up := newUpstream(t, func(w http.ResponseWriter, r *http.Request) { time.Sleep(300 * time.Millisecond) })
|
|
316
|
-
g := newTestGateway(t, config{Access: "users", ResponseTimeoutMillis: 50}, &fakeAuth{id: identity{User: "bob"}}, up)
|
|
317
|
-
w := do(g, "GET", base+"/", signedIn, "")
|
|
318
|
-
if w.Code != 524 || !strings.Contains(w.Body.String(), "upstream_timeout") || w.Header().Get("X-Synopkgland-Status") != "504" {
|
|
319
|
-
t.Fatalf("code=%d body=%q", w.Code, w.Body.String())
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
func TestContainerCannotSetCookiesOnTheDSMOrigin(t *testing.T) {
|
|
324
|
-
up := newUpstream(t, func(w http.ResponseWriter, r *http.Request) {
|
|
325
|
-
http.SetCookie(w, &http.Cookie{Name: "id", Value: "evil"})
|
|
326
|
-
io.WriteString(w, "ok")
|
|
327
|
-
})
|
|
328
|
-
g := newTestGateway(t, config{Access: "users"}, &fakeAuth{id: identity{User: "bob"}}, up)
|
|
329
|
-
w := do(g, "GET", base+"/", signedIn, "")
|
|
330
|
-
if w.Code != 200 || w.Header().Get("Set-Cookie") != "" {
|
|
331
|
-
t.Fatalf("code=%d set-cookie=%q", w.Code, w.Header().Get("Set-Cookie"))
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
func TestRedirectsStayInsideTheGateway(t *testing.T) {
|
|
336
|
-
up := newUpstream(t, func(w http.ResponseWriter, r *http.Request) {
|
|
337
|
-
switch r.URL.Path {
|
|
338
|
-
case "/abs":
|
|
339
|
-
http.Redirect(w, r, "http://"+r.Host+"/next?q=1", http.StatusFound)
|
|
340
|
-
case "/root":
|
|
341
|
-
http.Redirect(w, r, "/next", http.StatusFound)
|
|
342
|
-
case "/dir/rel":
|
|
343
|
-
http.Redirect(w, r, "next", http.StatusFound)
|
|
344
|
-
case "/rel":
|
|
345
|
-
w.Header().Set("Location", "next")
|
|
346
|
-
w.WriteHeader(http.StatusFound)
|
|
347
|
-
default:
|
|
348
|
-
http.Redirect(w, r, "https://example.org/away", http.StatusFound)
|
|
349
|
-
}
|
|
350
|
-
})
|
|
351
|
-
g := newTestGateway(t, config{Access: "users"}, &fakeAuth{id: identity{User: "bob"}}, up)
|
|
352
|
-
for path, want := range map[string]string{
|
|
353
|
-
"/abs": base + "/next?q=1",
|
|
354
|
-
"/root": base + "/next",
|
|
355
|
-
"/rel": "next",
|
|
356
|
-
"/dir/rel": base + "/dir/next",
|
|
357
|
-
"/ext": "https://example.org/away",
|
|
358
|
-
} {
|
|
359
|
-
w := do(g, "GET", base+path, signedIn, "")
|
|
360
|
-
if got := w.Header().Get("Location"); w.Code != 302 || got != want {
|
|
361
|
-
t.Errorf("%s: %d Location=%q want %q", path, w.Code, got, want)
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
func TestPathsCannotEscapeTheUpstream(t *testing.T) {
|
|
367
|
-
up := newUpstream(t, hello)
|
|
368
|
-
g := newTestGateway(t, config{Access: "users"}, &fakeAuth{id: identity{User: "bob"}}, up)
|
|
369
|
-
for _, target := range []string{
|
|
370
|
-
base + "/../../etc/passwd",
|
|
371
|
-
base + "/%2e%2e/%2e%2e/x",
|
|
372
|
-
base + "//evil.example/x",
|
|
373
|
-
base + "/a/../../x",
|
|
374
|
-
base + "/x%00y",
|
|
375
|
-
} {
|
|
376
|
-
if w := do(g, "GET", target, signedIn, ""); w.Code != 400 {
|
|
377
|
-
t.Errorf("%s: code=%d", target, w.Code)
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
if up.hits.Load() != 0 {
|
|
381
|
-
t.Fatalf("hits=%d", up.hits.Load())
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
func TestPathWithoutTrailingSlashRedirectsSoRelativeLinksWork(t *testing.T) {
|
|
386
|
-
up := newUpstream(t, hello)
|
|
387
|
-
g := newTestGateway(t, config{Access: "users"}, &fakeAuth{id: identity{User: "bob"}}, up)
|
|
388
|
-
w := do(g, "GET", base+"?x=1", signedIn, "")
|
|
389
|
-
if w.Code != 308 || w.Header().Get("Location") != "gateway.cgi/?x=1" {
|
|
390
|
-
t.Fatalf("code=%d location=%q", w.Code, w.Header().Get("Location"))
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
func TestRequestOutsideTheGatewayIsNotFound(t *testing.T) {
|
|
395
|
-
up := newUpstream(t, hello)
|
|
396
|
-
g := newTestGateway(t, config{Access: "users"}, &fakeAuth{id: identity{User: "bob"}}, up)
|
|
397
|
-
if w := do(g, "GET", "/somewhere/else", signedIn, ""); w.Code != 410 || w.Header().Get("X-Synopkgland-Status") != "404" {
|
|
398
|
-
t.Fatalf("code=%d headers=%v", w.Code, w.Header())
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
func TestResponsesAreNotSharedCacheable(t *testing.T) {
|
|
403
|
-
up := newUpstream(t, hello)
|
|
404
|
-
g := newTestGateway(t, config{Access: "users"}, &fakeAuth{id: identity{User: "bob"}}, up)
|
|
405
|
-
w := do(g, "GET", base+"/", signedIn, "")
|
|
406
|
-
if cc := w.Header().Get("Cache-Control"); !strings.Contains(cc, "private") {
|
|
407
|
-
t.Errorf("Cache-Control = %q", cc)
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// DSM's nginx has `scgi_intercept_errors on` and `error_page 403 404 500 502 503 504`: a CGI reply with one of
|
|
412
|
-
// these statuses loses its body and headers to DSM's generic error page. Measured on DSM 7.4-90069.
|
|
413
|
-
func TestNoGatewayStatusIsOneThatDSMReplaces(t *testing.T) {
|
|
414
|
-
replaced := []int{403, 404, 500, 502, 503, 504}
|
|
415
|
-
for _, intended := range replaced {
|
|
416
|
-
w := httptest.NewRecorder()
|
|
417
|
-
reply(w, intended, "some_code", "message")
|
|
418
|
-
if slices.Contains(replaced, w.Code) || w.Code < 400 {
|
|
419
|
-
t.Errorf("reply(%d) sent %d, which DSM replaces or is not an error", intended, w.Code)
|
|
420
|
-
}
|
|
421
|
-
if got := w.Header().Get("X-Synopkgland-Status"); got != strconv.Itoa(intended) {
|
|
422
|
-
t.Errorf("reply(%d): X-Synopkgland-Status = %q", intended, got)
|
|
423
|
-
}
|
|
424
|
-
if w.Header().Get("X-Synopkgland-Error") != "some_code" || !strings.HasPrefix(w.Body.String(), "some_code\n") {
|
|
425
|
-
t.Errorf("reply(%d): error code lost: %v %q", intended, w.Header(), w.Body.String())
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
func TestStatusesDSMKeepsAreSentAsTheyAre(t *testing.T) {
|
|
431
|
-
for _, status := range []int{400, 401, 413, 501, 308} {
|
|
432
|
-
w := httptest.NewRecorder()
|
|
433
|
-
reply(w, status, "some_code", "message")
|
|
434
|
-
if w.Code != status || w.Header().Get("X-Synopkgland-Status") != "" || w.Header().Get("X-Synopkgland-Error") != "some_code" {
|
|
435
|
-
t.Errorf("reply(%d): sent %d headers=%v", status, w.Code, w.Header())
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
func TestContainerErrorBodiesSurviveDSMWhenTheAppAsks(t *testing.T) {
|
|
441
|
-
up := newUpstream(t, func(w http.ResponseWriter, r *http.Request) {
|
|
442
|
-
w.Header().Set("Content-Type", "application/json")
|
|
443
|
-
w.Header().Set("Retry-After", "7")
|
|
444
|
-
w.Header().Set("X-App-Reason", "no-such-note")
|
|
445
|
-
w.WriteHeader(map[string]int{"/missing": 404, "/boom": 500, "/busy": 503, "/denied": 401}[r.URL.Path])
|
|
446
|
-
io.WriteString(w, `{"error":"x"}`)
|
|
447
|
-
})
|
|
448
|
-
g := newTestGateway(t, config{Access: "users"}, &fakeAuth{id: identity{User: "bob"}}, up)
|
|
449
|
-
keep := map[string]string{"Cookie": "id=abc", "X-Synopkgland-Keep-Error-Body": "1"}
|
|
450
|
-
for path, want := range map[string][2]int{"/missing": {404, 410}, "/boom": {500, 520}, "/busy": {503, 521}, "/denied": {401, 401}} {
|
|
451
|
-
plain := do(g, "GET", base+path, signedIn, "")
|
|
452
|
-
if plain.Code != want[0] || plain.Header().Get("X-Synopkgland-Status") != "" {
|
|
453
|
-
t.Errorf("%s without the header: %d %v, want the container's own %d", path, plain.Code, plain.Header(), want[0])
|
|
454
|
-
}
|
|
455
|
-
w := do(g, "GET", base+path, keep, "")
|
|
456
|
-
if w.Code != want[1] || w.Body.String() != `{"error":"x"}` || w.Header().Get("Retry-After") != "7" || w.Header().Get("X-App-Reason") != "no-such-note" {
|
|
457
|
-
t.Errorf("%s with the header: %d %q %v", path, w.Code, w.Body.String(), w.Header())
|
|
458
|
-
}
|
|
459
|
-
if want[0] != want[1] && w.Header().Get("X-Synopkgland-Status") != strconv.Itoa(want[0]) {
|
|
460
|
-
t.Errorf("%s: X-Synopkgland-Status = %q", path, w.Header().Get("X-Synopkgland-Status"))
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
func TestKeepErrorBodyLeavesSuccessAndRedirectsAlone(t *testing.T) {
|
|
466
|
-
up := newUpstream(t, hello)
|
|
467
|
-
g := newTestGateway(t, config{Access: "users"}, &fakeAuth{id: identity{User: "bob"}}, up)
|
|
468
|
-
w := do(g, "GET", base+"/", map[string]string{"Cookie": "id=abc", "X-Synopkgland-Keep-Error-Body": "1"}, "")
|
|
469
|
-
if w.Code != 200 || w.Header().Get("X-Synopkgland-Status") != "" {
|
|
470
|
-
t.Fatalf("%d %v", w.Code, w.Header())
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Server-Sent Events work through DSM (nginx does not buffer SCGI replies there), provided the gateway hands
|
|
475
|
-
// each event on as it arrives instead of holding it until the container finishes.
|
|
476
|
-
func TestServerSentEventsAreForwardedAsTheyArrive(t *testing.T) {
|
|
477
|
-
release := make(chan struct{})
|
|
478
|
-
gone := make(chan struct{})
|
|
479
|
-
up := newUpstream(t, func(w http.ResponseWriter, r *http.Request) {
|
|
480
|
-
w.Header().Set("Content-Type", "text/event-stream")
|
|
481
|
-
io.WriteString(w, "data: one\n\n")
|
|
482
|
-
w.(http.Flusher).Flush()
|
|
483
|
-
select {
|
|
484
|
-
case <-release:
|
|
485
|
-
io.WriteString(w, "data: two\n\n")
|
|
486
|
-
case <-r.Context().Done():
|
|
487
|
-
close(gone)
|
|
488
|
-
}
|
|
489
|
-
})
|
|
490
|
-
srv := httptest.NewServer(newTestGateway(t, config{Access: "users"}, &fakeAuth{id: identity{User: "bob"}}, up))
|
|
491
|
-
defer srv.Close()
|
|
492
|
-
req, _ := http.NewRequest("GET", srv.URL+base+"/events", nil)
|
|
493
|
-
req.Header.Set("Cookie", "id=abc")
|
|
494
|
-
resp, err := http.DefaultClient.Do(req)
|
|
495
|
-
if err != nil {
|
|
496
|
-
t.Fatal(err)
|
|
497
|
-
}
|
|
498
|
-
line := make(chan string, 1)
|
|
499
|
-
go func() {
|
|
500
|
-
buf := make([]byte, 64)
|
|
501
|
-
n, _ := resp.Body.Read(buf)
|
|
502
|
-
line <- string(buf[:n])
|
|
503
|
-
}()
|
|
504
|
-
select {
|
|
505
|
-
case got := <-line:
|
|
506
|
-
if got != "data: one\n\n" {
|
|
507
|
-
t.Fatalf("first event = %q", got)
|
|
508
|
-
}
|
|
509
|
-
case <-time.After(2 * time.Second):
|
|
510
|
-
t.Fatal("the first event was held back while the container kept the stream open")
|
|
511
|
-
}
|
|
512
|
-
resp.Body.Close()
|
|
513
|
-
select {
|
|
514
|
-
case <-gone:
|
|
515
|
-
case <-time.After(2 * time.Second):
|
|
516
|
-
t.Fatal("the container was not told the client went away")
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// wsEchoHandler is a container that accepts a WebSocket upgrade and then answers every chunk with "echo:"+chunk.
|
|
521
|
-
func wsEchoHandler(w http.ResponseWriter, r *http.Request) {
|
|
522
|
-
conn, brw, err := w.(http.Hijacker).Hijack()
|
|
523
|
-
if err != nil {
|
|
524
|
-
return
|
|
525
|
-
}
|
|
526
|
-
defer conn.Close()
|
|
527
|
-
io.WriteString(brw, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n")
|
|
528
|
-
brw.Flush()
|
|
529
|
-
buf := make([]byte, 64)
|
|
530
|
-
for {
|
|
531
|
-
n, err := brw.Read(buf)
|
|
532
|
-
if n > 0 {
|
|
533
|
-
io.WriteString(conn, "echo:"+string(buf[:n]))
|
|
534
|
-
}
|
|
535
|
-
if err != nil {
|
|
536
|
-
return
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
func dialUpgrade(t *testing.T, srv *httptest.Server, headers string) (net.Conn, *bufio.Reader, string) {
|
|
542
|
-
t.Helper()
|
|
543
|
-
conn, err := net.Dial("tcp", srv.Listener.Addr().String())
|
|
544
|
-
if err != nil {
|
|
545
|
-
t.Fatal(err)
|
|
546
|
-
}
|
|
547
|
-
t.Cleanup(func() { conn.Close() })
|
|
548
|
-
conn.SetDeadline(time.Now().Add(5 * time.Second))
|
|
549
|
-
io.WriteString(conn, "GET "+base+"/ws HTTP/1.1\r\nHost: "+srv.Listener.Addr().String()+"\r\nCookie: id=abc\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n"+headers+"\r\n")
|
|
550
|
-
r := bufio.NewReader(conn)
|
|
551
|
-
status, err := r.ReadString('\n')
|
|
552
|
-
if err != nil {
|
|
553
|
-
t.Fatal(err)
|
|
554
|
-
}
|
|
555
|
-
return conn, r, strings.TrimSpace(status)
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
func TestWebSocketUpgradeIsProxiedBothWays(t *testing.T) {
|
|
559
|
-
up := newUpstream(t, wsEchoHandler)
|
|
560
|
-
srv := httptest.NewServer(newTestGateway(t, config{Access: "users"}, &fakeAuth{id: identity{User: "bob"}}, up))
|
|
561
|
-
defer srv.Close()
|
|
562
|
-
conn, r, status := dialUpgrade(t, srv, "Origin: http://"+srv.Listener.Addr().String()+"\r\n")
|
|
563
|
-
if status != "HTTP/1.1 101 Switching Protocols" {
|
|
564
|
-
t.Fatalf("status = %q", status)
|
|
565
|
-
}
|
|
566
|
-
for line, err := r.ReadString('\n'); strings.TrimSpace(line) != ""; line, err = r.ReadString('\n') {
|
|
567
|
-
if err != nil {
|
|
568
|
-
t.Fatal(err)
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
io.WriteString(conn, "hello")
|
|
572
|
-
got := make([]byte, len("echo:hello"))
|
|
573
|
-
if _, err := io.ReadFull(r, got); err != nil || string(got) != "echo:hello" {
|
|
574
|
-
t.Fatalf("echo = %q, %v", got, err)
|
|
575
|
-
}
|
|
576
|
-
h := up.request.Load().Header
|
|
577
|
-
if h.Get("X-Syno-User") != "bob" || h.Get("Cookie") != "" || h.Get("Upgrade") != "websocket" {
|
|
578
|
-
t.Errorf("headers at the container: %v", h)
|
|
579
|
-
}
|
|
580
|
-
if up.request.Load().Host != srv.Listener.Addr().String() {
|
|
581
|
-
t.Errorf("Host at the container = %q, want the one the browser used so that same-origin checks pass", up.request.Load().Host)
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// upgradeRefusal reads the rest of a refused handshake and returns its X-Synopkgland-Error and -Status values.
|
|
586
|
-
func upgradeRefusal(t *testing.T, r *bufio.Reader) (code, intended string) {
|
|
587
|
-
t.Helper()
|
|
588
|
-
for line, err := r.ReadString('\n'); strings.TrimSpace(line) != ""; line, err = r.ReadString('\n') {
|
|
589
|
-
if err != nil {
|
|
590
|
-
t.Fatal(err)
|
|
591
|
-
}
|
|
592
|
-
name, value, _ := strings.Cut(strings.TrimSpace(line), ": ")
|
|
593
|
-
switch strings.ToLower(name) {
|
|
594
|
-
case "x-synopkgland-error":
|
|
595
|
-
code = value
|
|
596
|
-
case "x-synopkgland-status":
|
|
597
|
-
intended = value
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
return code, intended
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
func TestWebSocketFromAnotherOriginIsRefused(t *testing.T) {
|
|
604
|
-
up := newUpstream(t, wsEchoHandler)
|
|
605
|
-
srv := httptest.NewServer(newTestGateway(t, config{Access: "users"}, &fakeAuth{id: identity{User: "bob"}}, up))
|
|
606
|
-
defer srv.Close()
|
|
607
|
-
own := srv.Listener.Addr().String()
|
|
608
|
-
for name, headers := range map[string]string{
|
|
609
|
-
"foreign origin": "Origin: https://evil.example\r\n",
|
|
610
|
-
"sub-domain of host": "Origin: http://" + own + ".evil.example\r\n",
|
|
611
|
-
"origin null": "Origin: null\r\n",
|
|
612
|
-
"fetch cross-site": "Sec-Fetch-Site: cross-site\r\n",
|
|
613
|
-
"fetch same-site": "Sec-Fetch-Site: same-site\r\n",
|
|
614
|
-
"cross-site, own host": "Sec-Fetch-Site: cross-site\r\nOrigin: http://" + own + "\r\n",
|
|
615
|
-
} {
|
|
616
|
-
_, r, status := dialUpgrade(t, srv, headers)
|
|
617
|
-
code, intended := upgradeRefusal(t, r)
|
|
618
|
-
if status != "HTTP/1.1 401 Unauthorized" || code != "cross_site_blocked" || intended != "403" {
|
|
619
|
-
t.Errorf("%s: %q %q %q", name, status, code, intended)
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
if up.hits.Load() != 0 {
|
|
623
|
-
t.Fatalf("hits=%d", up.hits.Load())
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// Browsers always send Origin on a WebSocket handshake, so a handshake with neither Origin nor
|
|
628
|
-
// Sec-Fetch-Site is not from a browser that can be trusted to scope it: the rule is the one state-changing requests use.
|
|
629
|
-
func TestWebSocketWithoutAnyOriginEvidenceIsRefused(t *testing.T) {
|
|
630
|
-
up := newUpstream(t, wsEchoHandler)
|
|
631
|
-
srv := httptest.NewServer(newTestGateway(t, config{Access: "users"}, &fakeAuth{id: identity{User: "bob"}}, up))
|
|
632
|
-
defer srv.Close()
|
|
633
|
-
_, r, status := dialUpgrade(t, srv, "")
|
|
634
|
-
code, _ := upgradeRefusal(t, r)
|
|
635
|
-
if status != "HTTP/1.1 401 Unauthorized" || code != "cross_site_blocked" || up.hits.Load() != 0 {
|
|
636
|
-
t.Fatalf("%q %q hits=%d", status, code, up.hits.Load())
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
func TestWebSocketWithOneKindOfSameOriginEvidenceIsAllowed(t *testing.T) {
|
|
641
|
-
up := newUpstream(t, wsEchoHandler)
|
|
642
|
-
srv := httptest.NewServer(newTestGateway(t, config{Access: "users"}, &fakeAuth{id: identity{User: "bob"}}, up))
|
|
643
|
-
defer srv.Close()
|
|
644
|
-
for name, headers := range map[string]string{
|
|
645
|
-
"origin only": "Origin: http://" + srv.Listener.Addr().String() + "\r\n",
|
|
646
|
-
"fetch metadata only": "Sec-Fetch-Site: same-origin\r\n",
|
|
647
|
-
} {
|
|
648
|
-
if _, _, status := dialUpgrade(t, srv, headers); status != "HTTP/1.1 101 Switching Protocols" {
|
|
649
|
-
t.Errorf("%s: %q", name, status)
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
func TestWebSocketNeedsTheSameAccessAsAnyRequest(t *testing.T) {
|
|
655
|
-
up := newUpstream(t, wsEchoHandler)
|
|
656
|
-
for name, auth := range map[string]*fakeAuth{"anonymous": {err: errNoSession}, "outsider": {id: identity{User: "bob", Groups: []string{"users"}}}} {
|
|
657
|
-
srv := httptest.NewServer(newTestGateway(t, config{Access: "admins"}, auth, up))
|
|
658
|
-
_, _, status := dialUpgrade(t, srv, "")
|
|
659
|
-
srv.Close()
|
|
660
|
-
if status != "HTTP/1.1 401 Unauthorized" {
|
|
661
|
-
t.Errorf("%s: %q", name, status)
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
if up.hits.Load() != 0 {
|
|
665
|
-
t.Fatalf("hits=%d", up.hits.Load())
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
func TestOtherProtocolUpgradesAreNotSupported(t *testing.T) {
|
|
670
|
-
up := newUpstream(t, hello)
|
|
671
|
-
g := newTestGateway(t, config{Access: "users"}, &fakeAuth{id: identity{User: "bob"}}, up)
|
|
672
|
-
w := do(g, "GET", base+"/", map[string]string{"Cookie": "id=abc", "Connection": "Upgrade", "Upgrade": "h2c"}, "")
|
|
673
|
-
if w.Code != 501 || up.hits.Load() != 0 || w.Header().Get("X-Synopkgland-Error") != "upgrade_unsupported" {
|
|
674
|
-
t.Fatalf("code=%d hits=%d headers=%v", w.Code, up.hits.Load(), w.Header())
|
|
675
|
-
}
|
|
676
|
-
}
|