@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 449 lines 12.8 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package reverseproxy
16
17 import (
18 "context"
19 "crypto/ecdsa"
20 "crypto/elliptic"
21 "crypto/rand"
22 "crypto/tls"
23 "crypto/x509"
24 "crypto/x509/pkix"
25 "fmt"
26 "io"
27 "math/big"
28 "net"
29 "net/http"
30 "net/http/httptest"
31 "testing"
32 "time"
33
34 "github.com/quic-go/quic-go"
35 "github.com/quic-go/quic-go/http3"
36 "github.com/quic-go/webtransport-go"
37
38 "github.com/caddyserver/caddy/v2"
39 "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
40 )
41
42 // startTestWebTransportServer starts an in-process WebTransport server on a
43 // random UDP port with a freshly minted self-signed certificate. handler is
44 // invoked once the CONNECT request has been upgraded to a session.
45 //
46 // Returns the UDP addr and a shutdown func. Tests should call the shutdown
47 // func via t.Cleanup (or explicitly with defer).
48 func startTestWebTransportServer(t *testing.T, handler func(s *webtransport.Session, r *http.Request), protocols ...string) (addr *net.UDPAddr, trustRoot *x509.Certificate, shutdown func()) {
49 t.Helper()
50
51 trustRoot, tlsCfg := generateSelfSignedTLS(t, "localhost")
52
53 mux := http.NewServeMux()
54 h3 := &http3.Server{
55 TLSConfig: tlsCfg,
56 Handler: mux,
57 QUICConfig: &quic.Config{
58 EnableDatagrams: true,
59 EnableStreamResetPartialDelivery: true,
60 },
61 }
62 // Advertise WebTransport in SETTINGS so the client's requirement
63 // checks pass. (This is what caddyhttp.Server.buildHTTP3Server does
64 // internally for the real server.)
65 webtransport.ConfigureHTTP3Server(h3)
66
67 wtServer := &webtransport.Server{H3: h3, ApplicationProtocols: protocols}
68 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
69 sess, err := wtServer.Upgrade(w, r)
70 if err != nil {
71 t.Logf("test server upgrade failed: %v", err)
72 w.WriteHeader(http.StatusBadRequest)
73 return
74 }
75 handler(sess, r)
76 })
77
78 udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
79 if err != nil {
80 t.Fatal(err)
81 }
82 udpConn, err := net.ListenUDP("udp", udpAddr)
83 if err != nil {
84 t.Fatal(err)
85 }
86
87 servErr := make(chan error, 1)
88 go func() {
89 servErr <- wtServer.Serve(udpConn)
90 }()
91
92 shutdown = func() {
93 _ = wtServer.Close()
94 <-servErr
95 _ = udpConn.Close()
96 }
97 return udpConn.LocalAddr().(*net.UDPAddr), trustRoot, shutdown
98 }
99
100 // startRejectingWebTransportServer advertises WebTransport but answers the
101 // Extended CONNECT with status instead of upgrading. Transport.Dial then
102 // returns a non-nil response and an error.
103 func startRejectingWebTransportServer(t *testing.T, status int) (addr *net.UDPAddr, trustRoot *x509.Certificate, shutdown func()) {
104 t.Helper()
105
106 trustRoot, tlsCfg := generateSelfSignedTLS(t, "localhost")
107 h3 := &http3.Server{
108 TLSConfig: tlsCfg,
109 Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
110 w.WriteHeader(status)
111 }),
112 QUICConfig: &quic.Config{
113 EnableDatagrams: true,
114 EnableStreamResetPartialDelivery: true,
115 },
116 }
117 webtransport.ConfigureHTTP3Server(h3)
118 wtServer := &webtransport.Server{H3: h3}
119
120 udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
121 if err != nil {
122 t.Fatal(err)
123 }
124 udpConn, err := net.ListenUDP("udp", udpAddr)
125 if err != nil {
126 t.Fatal(err)
127 }
128 servErr := make(chan error, 1)
129 go func() {
130 servErr <- wtServer.Serve(udpConn)
131 }()
132 shutdown = func() {
133 _ = wtServer.Close()
134 <-servErr
135 _ = udpConn.Close()
136 }
137 return udpConn.LocalAddr().(*net.UDPAddr), trustRoot, shutdown
138 }
139
140 type bodyCloseRecorder struct {
141 closed bool
142 }
143
144 func (b *bodyCloseRecorder) Read([]byte) (int, error) { return 0, io.EOF }
145
146 func (b *bodyCloseRecorder) Close() error {
147 b.closed = true
148 return nil
149 }
150
151 func generateSelfSignedTLS(t *testing.T, commonName string) (*x509.Certificate, *tls.Config) {
152 t.Helper()
153 priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
154 if err != nil {
155 t.Fatal(err)
156 }
157 template := x509.Certificate{
158 SerialNumber: big.NewInt(1),
159 Subject: pkix.Name{CommonName: commonName},
160 NotBefore: time.Now().Add(-time.Minute),
161 NotAfter: time.Now().Add(time.Hour),
162 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
163 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
164 DNSNames: []string{commonName},
165 IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")},
166 IsCA: true,
167 }
168 der, err := x509.CreateCertificate(rand.Reader, &template, &template, priv.Public(), priv)
169 if err != nil {
170 t.Fatal(err)
171 }
172 cert, err := x509.ParseCertificate(der)
173 if err != nil {
174 t.Fatal(err)
175 }
176 tlsCert := tls.Certificate{Certificate: [][]byte{der}, PrivateKey: priv, Leaf: cert}
177 return cert, &tls.Config{Certificates: []tls.Certificate{tlsCert}, NextProtos: []string{http3.NextProtoH3}}
178 }
179
180 func clientTLSFor(cert *x509.Certificate) *tls.Config {
181 pool := x509.NewCertPool()
182 pool.AddCert(cert)
183 return &tls.Config{RootCAs: pool, NextProtos: []string{http3.NextProtoH3}}
184 }
185
186 func TestDialUpstreamWebTransport_Succeeds(t *testing.T) {
187 if testing.Short() {
188 t.Skip()
189 }
190 addr, root, shutdown := startTestWebTransportServer(t, func(sess *webtransport.Session, _ *http.Request) {
191 _ = sess.CloseWithError(0, "")
192 })
193 t.Cleanup(shutdown)
194
195 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
196 defer cancel()
197
198 url := fmt.Sprintf("https://localhost:%d/", addr.Port)
199 rsp, sess, err := dialUpstreamWebTransport(ctx, clientTLSFor(root), url, nil, nil, "")
200 if err != nil {
201 t.Fatalf("dial failed: %v", err)
202 }
203 defer sess.CloseWithError(0, "")
204 if rsp != nil {
205 defer rsp.Body.Close()
206 }
207 if rsp.StatusCode != http.StatusOK {
208 t.Fatalf("unexpected status %d", rsp.StatusCode)
209 }
210 }
211
212 func TestDialUpstreamWebTransport_RejectedConnectClosesBody(t *testing.T) {
213 body := &bodyCloseRecorder{}
214 rsp, sess, err := finishUpstreamWebTransportDial(&http.Response{
215 StatusCode: http.StatusForbidden,
216 Body: body,
217 }, nil, fmt.Errorf("received status %d", http.StatusForbidden))
218 if err == nil {
219 t.Fatal("expected dial error")
220 }
221 if rsp != nil || sess != nil {
222 t.Fatalf("rejected dial returned rsp=%v sess=%v", rsp, sess)
223 }
224 if !body.closed {
225 t.Fatal("response body was not closed")
226 }
227
228 if testing.Short() {
229 t.Skip()
230 }
231 addr, root, shutdown := startRejectingWebTransportServer(t, http.StatusForbidden)
232 t.Cleanup(shutdown)
233
234 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
235 defer cancel()
236 url := fmt.Sprintf("https://localhost:%d/", addr.Port)
237 rsp, sess, err = dialUpstreamWebTransport(ctx, clientTLSFor(root), url, nil, nil, "")
238 if err == nil {
239 t.Fatal("expected rejected CONNECT to fail")
240 }
241 if rsp != nil || sess != nil {
242 t.Fatalf("rejected CONNECT returned rsp=%v sess=%v; body must be closed before return", rsp, sess)
243 }
244 }
245
246 func TestDialUpstreamWebTransport_ForwardsHeaders(t *testing.T) {
247 if testing.Short() {
248 t.Skip()
249 }
250 gotUA := make(chan string, 1)
251 addr, root, shutdown := startTestWebTransportServer(t, func(sess *webtransport.Session, r *http.Request) {
252 gotUA <- r.Header.Get("User-Agent")
253 _ = sess.CloseWithError(0, "")
254 })
255 t.Cleanup(shutdown)
256
257 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
258 defer cancel()
259 hdr := http.Header{"User-Agent": []string{"caddy-wt-test"}}
260
261 url := fmt.Sprintf("https://localhost:%d/", addr.Port)
262 rsp, sess, err := dialUpstreamWebTransport(ctx, clientTLSFor(root), url, hdr, nil, "")
263 if err != nil {
264 t.Fatalf("dial failed: %v", err)
265 }
266 defer sess.CloseWithError(0, "")
267 if rsp != nil {
268 defer rsp.Body.Close()
269 }
270
271 select {
272 case got := <-gotUA:
273 if got != "caddy-wt-test" {
274 t.Errorf("User-Agent not forwarded; got %q", got)
275 }
276 case <-time.After(time.Second):
277 t.Fatal("server handler did not observe User-Agent header in time")
278 }
279 }
280
281 func TestDialUpstreamWebTransport_BadAddress(t *testing.T) {
282 if testing.Short() {
283 t.Skip()
284 }
285 // No server on this port; expect a dial error within the context deadline.
286 ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
287 defer cancel()
288 // Use a loopback port we picked at random and left unbound.
289 freePort := pickFreeUDPPort(t)
290 url := fmt.Sprintf("https://127.0.0.1:%d/", freePort)
291 _, _, err := dialUpstreamWebTransport(ctx, &tls.Config{InsecureSkipVerify: true}, url, nil, nil, "") //nolint:gosec // test only
292 if err == nil {
293 t.Fatal("expected error dialing unbound port, got nil")
294 }
295 }
296
297 func TestParseWTAvailableProtocols(t *testing.T) {
298 header := func(v string) http.Header {
299 h := make(http.Header)
300 if v != "" {
301 h.Set(wtAvailableProtocolsHeader, v)
302 }
303 return h
304 }
305 for i, tc := range []struct {
306 name string
307 hdr http.Header
308 want []string
309 }{
310 {name: "missing", hdr: http.Header{}},
311 {
312 name: "single",
313 hdr: header(`"moqt-19"`),
314 want: []string{"moqt-19"},
315 },
316 {
317 name: "preference order",
318 hdr: header(`"moqt-19", "moqt-18"`),
319 want: []string{"moqt-19", "moqt-18"},
320 },
321 {
322 name: "malformed",
323 hdr: header("not a structured field"),
324 },
325 } {
326 got := parseWTAvailableProtocols(tc.hdr)
327 if len(got) != len(tc.want) {
328 t.Errorf("test %d (%s): got %v, want %v", i, tc.name, got, tc.want)
329 continue
330 }
331 for j := range got {
332 if got[j] != tc.want[j] {
333 t.Errorf("test %d (%s): got %v, want %v", i, tc.name, got, tc.want)
334 break
335 }
336 }
337 }
338 }
339
340 func TestDialUpstreamWebTransport_NegotiatesProtocol(t *testing.T) {
341 if testing.Short() {
342 t.Skip()
343 }
344 addr, root, shutdown := startTestWebTransportServer(t, func(sess *webtransport.Session, _ *http.Request) {
345 _ = sess.CloseWithError(0, "")
346 }, "moqt-19")
347 t.Cleanup(shutdown)
348
349 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
350 defer cancel()
351
352 url := fmt.Sprintf("https://localhost:%d/", addr.Port)
353 rsp, sess, err := dialUpstreamWebTransport(ctx, clientTLSFor(root), url, nil, []string{"moqt-19"}, "")
354 if err != nil {
355 t.Fatalf("dial failed: %v", err)
356 }
357 defer sess.CloseWithError(0, "")
358 if rsp != nil {
359 defer rsp.Body.Close()
360 }
361 if got := sess.SessionState().ApplicationProtocol; got != "moqt-19" {
362 t.Errorf("ApplicationProtocol = %q, want moqt-19", got)
363 }
364 if got := rsp.Header.Get(wtProtocolHeader); got == "" {
365 t.Error("expected WT-Protocol on the response")
366 }
367 }
368
369 func TestDialUpstreamWebTransport_PreparedHost(t *testing.T) {
370 if testing.Short() {
371 t.Skip()
372 }
373 gotHost := make(chan string, 1)
374 addr, root, shutdown := startTestWebTransportServer(t, func(sess *webtransport.Session, r *http.Request) {
375 gotHost <- r.Host
376 _ = sess.CloseWithError(0, "")
377 })
378 t.Cleanup(shutdown)
379
380 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
381 defer cancel()
382 url := fmt.Sprintf("https://localhost:%d/", addr.Port)
383 wantHost := "wt.example.test"
384 rsp, sess, err := dialUpstreamWebTransport(ctx, clientTLSFor(root), url, nil, nil, wantHost)
385 if err != nil {
386 t.Fatalf("dial failed: %v", err)
387 }
388 defer sess.CloseWithError(0, "")
389 if rsp != nil {
390 defer rsp.Body.Close()
391 }
392 select {
393 case got := <-gotHost:
394 if got != wantHost {
395 t.Errorf("Host = %q, want %q", got, wantHost)
396 }
397 case <-time.After(time.Second):
398 t.Fatal("server handler did not observe Host in time")
399 }
400 }
401
402 func TestApplyWebTransportResponseHeaders(t *testing.T) {
403 rw := httptest.NewRecorder()
404 up := &http.Response{
405 StatusCode: http.StatusOK,
406 Header: http.Header{
407 "X-Upstream": []string{"yes"},
408 "Connection": []string{"keep-alive"},
409 "Keep-Alive": []string{"timeout=5"},
410 wtProtocolHeader: []string{"moqt-19"},
411 },
412 }
413 h := &Handler{
414 Headers: &headers.Handler{
415 Response: &headers.RespHeaderOps{
416 HeaderOps: &headers.HeaderOps{
417 Set: http.Header{"X-Down": []string{"from-caddy"}},
418 },
419 },
420 },
421 }
422 applyWebTransportResponseHeaders(rw, up, h, caddy.NewReplacer())
423 if got := rw.Header().Get("X-Upstream"); got != "yes" {
424 t.Errorf("X-Upstream = %q, want yes", got)
425 }
426 if got := rw.Header().Get("X-Down"); got != "from-caddy" {
427 t.Errorf("X-Down = %q, want from-caddy", got)
428 }
429 if got := rw.Header().Get(wtProtocolHeader); got != "moqt-19" {
430 t.Errorf("WT-Protocol = %q, want moqt-19", got)
431 }
432 if got := rw.Header().Get("Keep-Alive"); got != "" {
433 t.Errorf("hop-by-hop Keep-Alive leaked: %q", got)
434 }
435 }
436
437 // pickFreeUDPPort returns a local UDP port that was free when picked. The
438 // caller should use it immediately — there's no guarantee another process
439 // hasn't bound it in the interim.
440 func pickFreeUDPPort(t *testing.T) int {
441 t.Helper()
442 l, err := net.ListenPacket("udp", "127.0.0.1:0")
443 if err != nil {
444 t.Fatal(err)
445 }
446 port := l.LocalAddr().(*net.UDPAddr).Port
447 _ = l.Close()
448 return port
449 }