@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 300 lines 12 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/tls"
20 "errors"
21 "fmt"
22 "net/http"
23 "net/url"
24 "strings"
25 "time"
26
27 "github.com/dunglas/httpsfv"
28 "github.com/quic-go/quic-go"
29 "github.com/quic-go/quic-go/http3"
30 "github.com/quic-go/webtransport-go"
31
32 "github.com/caddyserver/caddy/v2"
33 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
34 )
35
36 // Go canonical MIME keys (http.CanonicalHeaderKey). Spec names are
37 // WT-Available-Protocols / WT-Protocol; HTTP/3 on the wire is lowercase.
38 const (
39 wtAvailableProtocolsHeader = "Wt-Available-Protocols"
40 wtProtocolHeader = "Wt-Protocol"
41 )
42
43 // webtransportProtocol values are the :protocol pseudo-header tokens sent
44 // by a client that wants to establish a WebTransport session over an HTTP/3
45 // Extended CONNECT. draft-15 (webtransport-go v0.11.0+) uses "webtransport-h3";
46 // older draft clients use the legacy "webtransport" token, which the
47 // webtransport-go server still accepts. Detect both.
48 const (
49 webtransportProtocol = "webtransport"
50 webtransportProtocolDraft15 = "webtransport-h3"
51 )
52
53 // webtransportWriter is the naked HTTP/3 response-writer shape that
54 // webtransport.Server.Upgrade type-asserts on. Caddy's
55 // UnwrapResponseWriterAs walks the ResponseWriter wrapper chain to this
56 // type before calling Upgrade.
57 type webtransportWriter interface {
58 http.ResponseWriter
59 http3.Settingser
60 http3.HTTPStreamer
61 }
62
63 // isWebTransportExtendedConnect reports whether r is an HTTP/3 Extended
64 // CONNECT that requests a WebTransport session. Does not check whether
65 // WebTransport proxying is configured; callers gate on Handler state.
66 func isWebTransportExtendedConnect(r *http.Request) bool {
67 return r.ProtoMajor == 3 && r.Method == http.MethodConnect &&
68 (r.Proto == webtransportProtocol || r.Proto == webtransportProtocolDraft15)
69 }
70
71 // webTransportHijack runs inside reverseProxy in place of RoundTrip when
72 // the request is a WebTransport Extended CONNECT. The outer proxy loop
73 // has already resolved the upstream set, selected an upstream, filled
74 // DialInfo, published reverse_proxy.upstream.* placeholders, applied
75 // transport and user request-header ops, cloned the request, directed
76 // the request URL at the upstream, and bumped in-flight counters — so
77 // this function only does WT-specific plumbing: upstream WT dial,
78 // client upgrade, and session pumping.
79 //
80 // Error semantics match the outer loop's retry contract:
81 // - Pre-dial misconfiguration (WT not enabled on the server, writer
82 // stack unsupported, handler transport is not HTTP/3) returns
83 // terminalError — no upstream can fix these conditions.
84 // - Upstream dial failure returns DialError — safe to retry across
85 // upstreams because no client-visible bytes have been written.
86 // - Post-upgrade failures return terminalError because the 200 OK
87 // has been flushed and the stream is hijacked.
88 //
89 // Requests that reach this function are already known to be WebTransport;
90 // callers should gate with isWebTransportExtendedConnect. origReq is the
91 // downstream request (Upgrade / CheckOrigin); req is the upstream-directed
92 // clone (URL, header_up, SNI expansion).
93 func (h *Handler) webTransportHijack(rw http.ResponseWriter, req *http.Request, origReq *http.Request, repl *caddy.Replacer, di DialInfo, server *caddyhttp.Server) error {
94 wtServer, ok := server.WebTransportServer().(*webtransport.Server)
95 if !ok || wtServer == nil {
96 return terminalError{caddyhttp.Error(http.StatusInternalServerError,
97 errors.New("webtransport: server has webtransport disabled or HTTP/3 is not enabled"))}
98 }
99
100 // Reach the naked http3 response writer so Upgrade's type assertions
101 // succeed through Caddy's wrapper chain. Done before dialing so we
102 // fail fast if the writer stack is unexpectedly incompatible.
103 naked, ok := caddyhttp.UnwrapResponseWriterAs[webtransportWriter](rw)
104 if !ok {
105 return terminalError{caddyhttp.Error(http.StatusInternalServerError,
106 errors.New("webtransport: response writer does not support WebTransport upgrade"))}
107 }
108
109 // A WT CONNECT reached this handler because the parent server has
110 // webtransport enabled. The upstream dial still needs the HTTP/3
111 // transport's TLS config (versions must include "3").
112 ht, ok := h.Transport.(*HTTPTransport)
113 if !ok || ht.h3Transport == nil {
114 return terminalError{caddyhttp.Error(http.StatusBadGateway,
115 errors.New("webtransport: requires the http transport with versions [\"3\"]"))}
116 }
117 tlsCfg := ht.h3Transport.TLSClientConfig
118 if tlsCfg == nil {
119 tlsCfg = new(tls.Config)
120 }
121
122 // Expand SNI placeholders (e.g. tls_server_name {http.request.host}) per
123 // session. The normal HTTP/3 path does this via a custom h3Transport.Dial
124 // hook (#7737); the WebTransport path dials through its own Transport and
125 // bypasses that hook, so expand here. Clone first — the transport's TLS
126 // config is shared across sessions and must not be mutated in place.
127 if strings.Contains(tlsCfg.ServerName, "{") {
128 tlsCfg = tlsCfg.Clone()
129 tlsCfg.ServerName = repl.ReplaceAll(tlsCfg.ServerName, "")
130 }
131
132 // Dial the upstream BEFORE upgrading the client. If the upstream is
133 // unreachable or refuses the CONNECT, a proper 5xx goes back over the
134 // H3 stream and the client's Dial sees the real status — instead of
135 // an already-upgraded session closing immediately. DialError so the
136 // outer proxy loop can fail over to another upstream, same as any
137 // other dial failure.
138 //
139 // WebTransport over HTTP/3 always uses https; RequestURI preserves
140 // the request's encoded path and query.
141 //
142 // Application protocol negotiation (draft-ietf-webtrans-http3 §3.3)
143 // is relayed, not chosen here: parse the offer from the prepared
144 // request (so transport and header_up ops apply), put it on the
145 // upstream Transport, and copy the upstream's WT-Protocol onto the
146 // client 200. Strip the offer from a clone of the forwarded headers
147 // so the Transport remashals a spec-correct list. The proxy loop
148 // reuses this request on retries and only recopies headers when
149 // header ops are configured, so the original map has to stay intact
150 // for the next upstream. The shared webtransport.Server keeps
151 // ApplicationProtocols empty so Caddy does not independently select
152 // a protocol.
153 offered := parseWTAvailableProtocols(req.Header)
154 dialHdr := req.Header.Clone()
155 dialHdr.Del(wtAvailableProtocolsHeader)
156 upstreamURL := "https://" + di.Address + req.URL.RequestURI()
157 dialStart := time.Now()
158 upstreamResp, upstreamSess, err := dialUpstreamWebTransport(req.Context(), tlsCfg, upstreamURL, dialHdr, offered, req.Host)
159 if err != nil {
160 return DialError{fmt.Errorf("webtransport upstream dial: %w", err)}
161 }
162 defer upstreamResp.Body.Close()
163 latency := time.Since(dialStart)
164
165 // Copy filtered upstream headers, then apply header_down. Upgrade
166 // flushes rw.Header() with the 200, so this must happen first.
167 applyWebTransportResponseHeaders(rw, upstreamResp, h, repl)
168
169 clientSess, err := wtServer.Upgrade(naked, origReq)
170 if err != nil {
171 _ = upstreamSess.CloseWithError(0, "client upgrade failed")
172 return terminalError{caddyhttp.Error(http.StatusBadRequest,
173 fmt.Errorf("webtransport upgrade: %w", err))}
174 }
175 unregister := server.RegisterWebTransportSession(webTransportSessionCloser{sess: clientSess})
176 defer unregister()
177 caddyhttp.RecordHijackedStatus(rw, http.StatusOK)
178
179 runWebTransportPump(clientSess, upstreamSess, h.logger)
180 h.recordUpstreamRoundTrip(di, repl, upstreamResp.StatusCode, latency)
181 repl.Set("http.reverse_proxy.upstream.duration", time.Since(dialStart))
182 repl.Set("http.reverse_proxy.upstream.duration_ms", time.Since(dialStart).Seconds()*1e3)
183 return nil
184 }
185
186 // webTransportSessionCloser closes one downstream WebTransport session
187 // during server shutdown. io.Closer keeps *webtransport.Session out of
188 // caddyhttp's exported API.
189 type webTransportSessionCloser struct {
190 sess *webtransport.Session
191 }
192
193 func (w webTransportSessionCloser) Close() error {
194 return w.sess.CloseWithError(0, "server shutting down")
195 }
196
197 // dialUpstreamWebTransport opens a WebTransport session to the upstream at
198 // urlStr (an https URL), forwarding reqHdr as headers on the Extended
199 // CONNECT request. The returned session is owned by the caller and must be
200 // closed when no longer in use. Return-value order matches
201 // webtransport.Transport.Dial: (response, session, error).
202 // applicationProtocols is the client's WT-Available-Protocols offer,
203 // forwarded so the Transport will accept the upstream's WT-Protocol choice.
204 // host, if non-empty, is the prepared Host / :authority for the CONNECT
205 // (header_up Host); the QUIC dial still uses the URL host.
206 //
207 // EXPERIMENTAL: this helper is an internal building block for the upcoming
208 // WebTransport reverse-proxy transport. Shape and behavior may change.
209 func dialUpstreamWebTransport(ctx context.Context, tlsCfg *tls.Config, urlStr string, reqHdr http.Header, applicationProtocols []string, host string) (*http.Response, *webtransport.Session, error) {
210 u, err := url.Parse(urlStr)
211 if err != nil {
212 return nil, nil, err
213 }
214 dialAddr := u.Host
215 if host != "" {
216 // Transport.Dial always sets req.Host from the URL host, so put the
217 // prepared Host there. DialAddr keeps the QUIC dial on the real
218 // upstream address.
219 u.Host = host
220 urlStr = u.String()
221 }
222 d := &webtransport.Transport{
223 TLSClientConfig: tlsCfg,
224 ApplicationProtocols: applicationProtocols,
225 QUICConfig: &quic.Config{
226 EnableDatagrams: true,
227 EnableStreamResetPartialDelivery: true,
228 },
229 DialAddr: func(ctx context.Context, _ string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) {
230 return quic.DialAddrEarly(ctx, dialAddr, tlsCfg, cfg)
231 },
232 }
233 return finishUpstreamWebTransportDial(d.Dial(ctx, urlStr, reqHdr))
234 }
235
236 // finishUpstreamWebTransportDial closes a rejected dial. Transport.Dial
237 // returns a non-nil response with an error when the peer rejects the
238 // Extended CONNECT, and the body must be closed. The session is nil on
239 // that path; close it too if a future dial returns both.
240 func finishUpstreamWebTransportDial(rsp *http.Response, sess *webtransport.Session, err error) (*http.Response, *webtransport.Session, error) {
241 if err == nil {
242 return rsp, sess, nil
243 }
244 if rsp != nil && rsp.Body != nil {
245 _ = rsp.Body.Close()
246 }
247 if sess != nil {
248 _ = sess.CloseWithError(0, "")
249 }
250 return nil, nil, err
251 }
252
253 // applyWebTransportResponseHeaders copies non-hop upstream response headers
254 // onto the client writer and then applies configured header_down ops, matching
255 // finalizeResponse for regular proxy responses.
256 func applyWebTransportResponseHeaders(rw http.ResponseWriter, upstream *http.Response, h *Handler, repl *caddy.Replacer) {
257 hdr := upstream.Header.Clone()
258 if hdr == nil {
259 hdr = make(http.Header)
260 }
261 removeConnectionHeaders(hdr)
262 for _, hop := range hopHeaders {
263 hdr.Del(hop)
264 }
265 if h.Headers != nil && h.Headers.Response != nil {
266 if h.Headers.Response.Require == nil ||
267 h.Headers.Response.Require.Match(upstream.StatusCode, hdr) {
268 h.Headers.Response.ApplyTo(hdr, repl)
269 }
270 }
271 copyHeader(rw.Header(), hdr)
272 }
273
274 // parseWTAvailableProtocols extracts application-protocol tokens from a
275 // WT-Available-Protocols structured-field list. Missing or malformed
276 // values are ignored, matching draft-ietf-webtrans-http3 (treat the
277 // field as absent).
278 func parseWTAvailableProtocols(h http.Header) []string {
279 vals := h.Values(wtAvailableProtocolsHeader)
280 if len(vals) == 0 {
281 return nil
282 }
283 list, err := httpsfv.UnmarshalList(vals)
284 if err != nil {
285 return nil
286 }
287 var out []string
288 for _, item := range list {
289 i, ok := item.(httpsfv.Item)
290 if !ok {
291 return nil
292 }
293 p, ok := i.Value.(string)
294 if !ok {
295 return nil
296 }
297 out = append(out, p)
298 }
299 return out
300 }