@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 204 lines 6.63 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 integration
16
17 import (
18 "errors"
19 "fmt"
20 "io"
21 "net/http"
22 "net/http/httptest"
23 "testing"
24
25 "github.com/quic-go/quic-go/http3"
26 "github.com/quic-go/webtransport-go"
27 "go.uber.org/zap"
28
29 "github.com/caddyserver/caddy/v2"
30 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
31 )
32
33 // This file provides a terminating WebTransport handler used exclusively
34 // as a test upstream for the WebTransport reverse-proxy integration
35 // tests in webtransport_test.go. Keeping it in a _test.go file (mirroring
36 // mockdns_test.go) means the http.handlers.webtransport module is only
37 // registered in the integration test binary — it does not ship in
38 // production Caddy builds.
39
40 func init() {
41 caddy.RegisterModule(WebTransportEcho{})
42 }
43
44 // webtransportEchoWriter is the naked HTTP/3 response-writer shape that
45 // webtransport.Server.Upgrade type-asserts on.
46 type webtransportEchoWriter interface {
47 http.ResponseWriter
48 http3.Settingser
49 http3.HTTPStreamer
50 }
51
52 // WebTransportEcho terminates an incoming WebTransport session and echoes
53 // bytes on each accepted bidirectional stream. Registered as
54 // `http.handlers.webtransport` in the integration test binary.
55 type WebTransportEcho struct {
56 logger *zap.Logger
57 }
58
59 // CaddyModule returns the Caddy module information.
60 func (WebTransportEcho) CaddyModule() caddy.ModuleInfo {
61 return caddy.ModuleInfo{
62 ID: "http.handlers.webtransport",
63 New: func() caddy.Module { return new(WebTransportEcho) },
64 }
65 }
66
67 // Provision sets up the handler.
68 func (h *WebTransportEcho) Provision(ctx caddy.Context) error {
69 h.logger = ctx.Logger()
70 return nil
71 }
72
73 // ServeHTTP upgrades the request to a WebTransport session and echoes
74 // bytes on each accepted bidirectional stream. Non-WebTransport requests
75 // are passed through to the next handler.
76 func (h *WebTransportEcho) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
77 if !isWebTransportEchoUpgrade(r) {
78 return next.ServeHTTP(w, r)
79 }
80
81 srv, ok := r.Context().Value(caddyhttp.ServerCtxKey).(*caddyhttp.Server)
82 if !ok || srv == nil {
83 return caddyhttp.Error(http.StatusInternalServerError,
84 errors.New("webtransport: caddyhttp.Server not in request context"))
85 }
86 wtServer, ok := srv.WebTransportServer().(*webtransport.Server)
87 if !ok || wtServer == nil {
88 return caddyhttp.Error(http.StatusInternalServerError,
89 errors.New("webtransport: HTTP/3 is not enabled on this server"))
90 }
91
92 naked, ok := caddyhttp.UnwrapResponseWriterAs[webtransportEchoWriter](w)
93 if !ok {
94 return caddyhttp.Error(http.StatusInternalServerError,
95 errors.New("webtransport: underlying writer does not support WebTransport upgrade"))
96 }
97
98 session, err := wtServer.Upgrade(naked, r)
99 if err != nil {
100 h.logger.Debug("webtransport upgrade failed", zap.Error(err))
101 return caddyhttp.Error(http.StatusBadRequest,
102 fmt.Errorf("webtransport upgrade: %w", err))
103 }
104 unregister := srv.RegisterWebTransportSession(webTransportEchoCloser{sess: session})
105 defer unregister()
106
107 h.echoStreams(session)
108 return nil
109 }
110
111 // webTransportEchoCloser closes a terminating test session on shutdown.
112 type webTransportEchoCloser struct {
113 sess *webtransport.Session
114 }
115
116 func (w webTransportEchoCloser) Close() error {
117 return w.sess.CloseWithError(0, "server shutting down")
118 }
119
120 // echoStreams accepts bidirectional streams on session until the session
121 // ends, and echoes bytes on each one.
122 func (h *WebTransportEcho) echoStreams(session *webtransport.Session) {
123 ctx := session.Context()
124 for {
125 str, err := session.AcceptStream(ctx)
126 if err != nil {
127 return
128 }
129 go func(s *webtransport.Stream) {
130 // io.Copy from the stream back to itself echoes everything
131 // received on this bidirectional stream. When the peer closes
132 // its send side we observe EOF and close our send side too.
133 if _, err := io.Copy(s, s); err != nil && h.logger != nil {
134 h.logger.Debug("webtransport echo stream error", zap.Error(err))
135 }
136 _ = s.Close()
137 }(str)
138 }
139 }
140
141 // isWebTransportEchoUpgrade reports whether r is an HTTP/3 Extended
142 // CONNECT that requests a WebTransport session. The quic-go/http3 server
143 // places the :protocol pseudo-header value in r.Proto for CONNECT requests.
144 func isWebTransportEchoUpgrade(r *http.Request) bool {
145 return r.ProtoMajor == 3 && r.Method == http.MethodConnect &&
146 (r.Proto == "webtransport" || r.Proto == "webtransport-h3")
147 }
148
149 // Interface guards.
150 var (
151 _ caddy.Provisioner = (*WebTransportEcho)(nil)
152 _ caddyhttp.MiddlewareHandler = (*WebTransportEcho)(nil)
153 )
154
155 // --- unit tests ------------------------------------------------------------
156
157 func TestIsWebTransportEchoUpgrade(t *testing.T) {
158 cases := []struct {
159 name string
160 proto string
161 major int
162 meth string
163 want bool
164 }{
165 {"h3 connect webtransport", "webtransport", 3, http.MethodConnect, true},
166 {"h3 connect webtransport-h3", "webtransport-h3", 3, http.MethodConnect, true},
167 {"h3 connect websocket", "websocket", 3, http.MethodConnect, false},
168 {"h2 connect webtransport", "webtransport", 2, http.MethodConnect, false},
169 {"h3 GET", "HTTP/3.0", 3, http.MethodGet, false},
170 {"h3 connect missing :protocol", "HTTP/3.0", 3, http.MethodConnect, false},
171 }
172 for _, tc := range cases {
173 t.Run(tc.name, func(t *testing.T) {
174 r := httptest.NewRequest(tc.meth, "/", nil)
175 r.ProtoMajor = tc.major
176 r.Proto = tc.proto
177 if got := isWebTransportEchoUpgrade(r); got != tc.want {
178 t.Errorf("isWebTransportEchoUpgrade = %v, want %v", got, tc.want)
179 }
180 })
181 }
182 }
183
184 // echoNextNoop is a stand-in for the next handler. It records whether it
185 // was invoked, used to assert that non-WebTransport requests pass through.
186 type echoNextNoop struct{ called bool }
187
188 func (n *echoNextNoop) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
189 n.called = true
190 return nil
191 }
192
193 func TestWebTransportEcho_PassesThroughNonWebTransportRequests(t *testing.T) {
194 h := &WebTransportEcho{}
195 r := httptest.NewRequest(http.MethodGet, "/", nil)
196 w := httptest.NewRecorder()
197 nx := &echoNextNoop{}
198 if err := h.ServeHTTP(w, r, nx); err != nil {
199 t.Fatalf("ServeHTTP returned error: %v", err)
200 }
201 if !nx.called {
202 t.Error("expected next handler to be invoked for non-WebTransport request")
203 }
204 }