@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

master
go 365 lines 9.93 KB
Raw
1 package reverseproxy
2
3 import (
4 "encoding/base64"
5 "io"
6 "net/http"
7 "net/http/httptest"
8 "strconv"
9 "strings"
10 "sync"
11 "testing"
12
13 "go.uber.org/zap"
14
15 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
16 "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
17 )
18
19 type extendedConnectCapture struct {
20 method string
21 headers http.Header
22 body []byte
23 extendedBodyPresent bool
24 extendedConnectBody []byte
25 }
26
27 type extendedConnectCaptureTransport struct {
28 mu sync.Mutex
29 capture extendedConnectCapture
30 }
31
32 func (tr *extendedConnectCaptureTransport) RoundTrip(req *http.Request) (*http.Response, error) {
33 body, err := io.ReadAll(req.Body)
34 if err != nil {
35 return nil, err
36 }
37
38 c := extendedConnectCapture{
39 method: req.Method,
40 headers: req.Header.Clone(),
41 body: body,
42 }
43 if rc, ok := caddyhttp.GetVar(req.Context(), "extended_connect_websocket_body").(io.ReadCloser); ok {
44 c.extendedBodyPresent = true
45 c.extendedConnectBody, err = io.ReadAll(rc)
46 if err != nil {
47 return nil, err
48 }
49 _ = rc.Close()
50 }
51
52 tr.mu.Lock()
53 tr.capture = c
54 tr.mu.Unlock()
55
56 return &http.Response{
57 StatusCode: http.StatusOK,
58 Header: make(http.Header),
59 Body: io.NopCloser(strings.NewReader("ok")),
60 Request: req,
61 }, nil
62 }
63
64 func (tr *extendedConnectCaptureTransport) Snapshot() extendedConnectCapture {
65 tr.mu.Lock()
66 defer tr.mu.Unlock()
67 return tr.capture
68 }
69
70 func TestServeHTTPRewritesExtendedConnectWebsocketRequest(t *testing.T) {
71 tests := []struct {
72 name string
73 protoMajor int
74 proto string
75 headers map[string]string
76 }{
77 {
78 name: "h2 extended connect",
79 protoMajor: 2,
80 proto: "HTTP/2.0",
81 headers: map[string]string{
82 ":protocol": "websocket",
83 },
84 },
85 {
86 name: "h3 extended connect",
87 protoMajor: 3,
88 proto: "websocket",
89 headers: map[string]string{},
90 },
91 }
92
93 for _, tc := range tests {
94 t.Run(tc.name, func(t *testing.T) {
95 const payload = "extended-connect-body"
96
97 transport := new(extendedConnectCaptureTransport)
98 h := &Handler{
99 logger: zap.NewNop(),
100 Transport: transport,
101 Upstreams: UpstreamPool{
102 &Upstream{Host: new(Host), Dial: "127.0.0.1:8443"},
103 },
104 LoadBalancing: &LoadBalancing{
105 SelectionPolicy: &RoundRobinSelection{},
106 },
107 }
108
109 req := httptest.NewRequest(http.MethodConnect, "http://example.test/upgrade", strings.NewReader(payload))
110 req.ProtoMajor = tc.protoMajor
111 req.Proto = tc.proto
112 for key, value := range tc.headers {
113 req.Header.Set(key, value)
114 }
115 req = prepareTestRequest(req)
116
117 rr := httptest.NewRecorder()
118 err := h.ServeHTTP(rr, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
119 return nil
120 }))
121 if err != nil {
122 t.Fatalf("ServeHTTP() error = %v", err)
123 }
124
125 captured := transport.Snapshot()
126 if captured.method != http.MethodGet {
127 t.Fatalf("upstream method = %s, want %s", captured.method, http.MethodGet)
128 }
129 if got := captured.headers.Get("Upgrade"); !strings.EqualFold(got, "websocket") {
130 t.Fatalf("Upgrade header = %q, want websocket", got)
131 }
132 if got := captured.headers.Get("Connection"); !strings.EqualFold(got, "Upgrade") {
133 t.Fatalf("Connection header = %q, want Upgrade", got)
134 }
135 if got := captured.headers.Get(":protocol"); got != "" {
136 t.Fatalf(":protocol header should be removed, got %q", got)
137 }
138 if len(captured.body) != 0 {
139 t.Fatalf("upstream request body length = %d, want 0", len(captured.body))
140 }
141 if !captured.extendedBodyPresent {
142 t.Fatal("extended_connect_websocket_body variable missing from request context")
143 }
144 if string(captured.extendedConnectBody) != payload {
145 t.Fatalf("extended_connect_websocket_body = %q, want %q", string(captured.extendedConnectBody), payload)
146 }
147 })
148 }
149 }
150
151 func TestServeHTTPRewritesExtendedConnectWebsocketSecWebSocketKeyMatrix(t *testing.T) {
152 const clientKey = "dGhlIHNhbXBsZSBub25jZQ=="
153
154 protoCases := []struct {
155 protoName string
156 protoMajor int
157 proto string
158 headers map[string]string
159 }{
160 {
161 protoName: "h2",
162 protoMajor: 2,
163 proto: "HTTP/2.0",
164 headers: map[string]string{
165 ":protocol": "websocket",
166 },
167 },
168 {
169 protoName: "h3",
170 protoMajor: 3,
171 proto: "websocket",
172 headers: map[string]string{},
173 },
174 }
175
176 headerOpCases := []struct {
177 opName string
178 userOps *headers.HeaderOps
179 transportOps *headers.HeaderOps
180 }{
181 {
182 opName: "no header ops",
183 },
184 {
185 opName: "user ops",
186 userOps: &headers.HeaderOps{
187 Set: http.Header{"X-User-Op": []string{"yes"}},
188 },
189 },
190 {
191 opName: "transport ops",
192 transportOps: &headers.HeaderOps{
193 Set: http.Header{"X-Transport-Op": []string{"yes"}},
194 },
195 },
196 {
197 opName: "both user and transport ops",
198 userOps: &headers.HeaderOps{
199 Set: http.Header{"X-User-Op": []string{"yes"}},
200 },
201 transportOps: &headers.HeaderOps{
202 Set: http.Header{"X-Transport-Op": []string{"yes"}},
203 },
204 },
205 }
206
207 clientKeyCases := []struct {
208 clientKeyName string
209 sendClientKey bool
210 }{
211 {
212 clientKeyName: "client sent key",
213 sendClientKey: true,
214 },
215 {
216 clientKeyName: "client without key",
217 sendClientKey: false,
218 },
219 }
220
221 for _, pc := range protoCases {
222 for _, hoc := range headerOpCases {
223 for _, ckc := range clientKeyCases {
224 testName := pc.protoName + "/" + hoc.opName + "/" + ckc.clientKeyName
225 t.Run(testName, func(t *testing.T) {
226 transport := new(extendedConnectCaptureTransport)
227 h := &Handler{
228 logger: zap.NewNop(),
229 Transport: transport,
230 Upstreams: UpstreamPool{
231 &Upstream{Host: new(Host), Dial: "127.0.0.1:8443"},
232 },
233 LoadBalancing: &LoadBalancing{
234 SelectionPolicy: &RoundRobinSelection{},
235 },
236 transportHeaderOps: hoc.transportOps,
237 }
238 if hoc.userOps != nil {
239 h.Headers = &headers.Handler{
240 Request: hoc.userOps,
241 }
242 }
243
244 req := httptest.NewRequest(http.MethodConnect, "http://example.test/upgrade", strings.NewReader("ws-data"))
245 req.ProtoMajor = pc.protoMajor
246 req.Proto = pc.proto
247 for k, v := range pc.headers {
248 req.Header.Set(k, v)
249 }
250 if ckc.sendClientKey {
251 req.Header.Set("Sec-WebSocket-Key", clientKey)
252 }
253 req = prepareTestRequest(req)
254
255 rr := httptest.NewRecorder()
256 err := h.ServeHTTP(rr, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
257 return nil
258 }))
259 if err != nil {
260 t.Fatalf("ServeHTTP() error = %v", err)
261 }
262
263 captured := transport.Snapshot()
264
265 // Collect all Sec-WebSocket-Key values across canonical and non-canonical headers
266 var keyValues []string
267 for k, v := range captured.headers {
268 if strings.EqualFold(k, "Sec-WebSocket-Key") {
269 if k != "Sec-WebSocket-Key" {
270 t.Fatalf("upstream WebSocket key spelling = %q, want Sec-WebSocket-Key", k)
271 }
272 keyValues = append(keyValues, v...)
273 }
274 }
275
276 if len(keyValues) != 1 {
277 t.Fatalf("expected exactly 1 Sec-WebSocket-Key value, got %d: %v (headers: %#v)",
278 len(keyValues), keyValues, captured.headers)
279 }
280
281 upstreamKey := keyValues[0]
282 if ckc.sendClientKey && upstreamKey == clientKey {
283 t.Fatalf("upstream Sec-WebSocket-Key was not replaced by generated key; still got client key %q", clientKey)
284 }
285
286 decoded, err := base64.StdEncoding.DecodeString(upstreamKey)
287 if err != nil {
288 t.Fatalf("upstream Sec-WebSocket-Key %q is not valid base64: %v", upstreamKey, err)
289 }
290 if len(decoded) != 16 {
291 t.Fatalf("upstream Sec-WebSocket-Key decoded length = %d, want 16 bytes", len(decoded))
292 }
293
294 if hoc.userOps != nil {
295 if got := captured.headers.Get("X-User-Op"); got != "yes" {
296 t.Fatalf("X-User-Op header = %q, want 'yes'", got)
297 }
298 }
299 if hoc.transportOps != nil {
300 if got := captured.headers.Get("X-Transport-Op"); got != "yes" {
301 t.Fatalf("X-Transport-Op header = %q, want 'yes'", got)
302 }
303 }
304 })
305 }
306 }
307 }
308 }
309
310 func TestUpgradeResponseWebsocketHeaders(t *testing.T) {
311 for _, proto := range []int{1, 2, 3} {
312 t.Run("h"+strconv.Itoa(proto), func(t *testing.T) {
313 h := &Handler{logger: zap.NewNop()}
314 req := prepareTestRequest(httptest.NewRequest(http.MethodGet, "http://example.test/upgrade", nil))
315 req.ProtoMajor = proto
316 req.Header.Set("Connection", "Upgrade")
317 req.Header.Set("Upgrade", "websocket")
318 wantStatus := http.StatusSwitchingProtocols
319 if proto > 1 {
320 caddyhttp.SetVar(req.Context(), "extended_connect_websocket_body", http.NoBody)
321 wantStatus = http.StatusOK
322 }
323 res := &http.Response{
324 StatusCode: http.StatusSwitchingProtocols,
325 Header: req.Header.Clone(),
326 Body: fakeRWC{strings.NewReader("")},
327 }
328 res.Header.Set("Sec-WebSocket-Accept", "backend-accept")
329 res.Header.Set("Sec-WebSocket-Protocol", "chat")
330 rw := upgradeHeaderRecorder{httptest.NewRecorder()}
331 h.handleUpgradeResponse(h.logger, rw, req, res, "test-upstream")
332 response := rw.Result()
333 defer response.Body.Close()
334 if response.StatusCode != wantStatus {
335 t.Fatalf("status = %d, want %d", response.StatusCode, wantStatus)
336 }
337 var accept, protocol string
338 for key, values := range response.Header {
339 if strings.EqualFold(key, "Sec-WebSocket-Accept") {
340 if proto > 1 {
341 t.Fatalf("extended CONNECT response contains %s", key)
342 }
343 if key != "Sec-WebSocket-Accept" {
344 t.Fatalf("WebSocket accept spelling = %q, want Sec-WebSocket-Accept", key)
345 }
346 accept = strings.Join(values, ",")
347 }
348 if strings.EqualFold(key, "Sec-WebSocket-Protocol") {
349 protocol = strings.Join(values, ",")
350 }
351 }
352 if proto == 1 && accept != "backend-accept" {
353 t.Fatalf("accept = %q, want backend-accept", accept)
354 }
355 if protocol != "chat" {
356 t.Fatalf("subprotocol = %q, want chat", protocol)
357 }
358 })
359 }
360 }
361
362 type upgradeHeaderRecorder struct{ *httptest.ResponseRecorder }
363
364 // Stop after the headers are committed, before the tunnel starts.
365 func (upgradeHeaderRecorder) FlushError() error { return io.ErrClosedPipe }