@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 232 lines 7.79 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 "errors"
20 "io"
21 "sync"
22 "time"
23
24 "github.com/quic-go/webtransport-go"
25 "go.uber.org/zap"
26 )
27
28 // runWebTransportPump bridges two WebTransport sessions so that every
29 // bidirectional stream, unidirectional stream, and datagram opened on one
30 // side is mirrored onto the other. It blocks until both sessions end.
31 // logger must be non-nil; callers pass either h.logger or zap.NewNop().
32 //
33 // Close propagation: when either session ends with a SessionError, the
34 // error code and message are forwarded to the peer via CloseWithError.
35 // When a session ends without a SessionError (context cancelled or
36 // connection dropped), the peer is closed with code 0. Each side's close
37 // is propagated at most once.
38 //
39 // EXPERIMENTAL: this helper is an internal building block for the
40 // WebTransport reverse-proxy transport and may change.
41 func runWebTransportPump(clientSess, upstreamSess *webtransport.Session, logger *zap.Logger) {
42 p := &webtransportPump{
43 client: clientSess,
44 upstream: upstreamSess,
45 logger: logger,
46 }
47 p.run()
48 }
49
50 type webtransportPump struct {
51 client, upstream *webtransport.Session
52 logger *zap.Logger
53
54 closeClientOnce sync.Once
55 closeUpstreamOnce sync.Once
56 // streams owns every bidi and uni splice. run waits for them after
57 // the accept loops return so the handler does not unregister the
58 // session while a copy is still unwinding.
59 streams sync.WaitGroup
60 }
61
62 func (p *webtransportPump) run() {
63 var wg sync.WaitGroup
64 closeClient := func(cause error) {
65 p.propagateClose(p.client, p.upstream, &p.closeClientOnce, cause)
66 }
67 closeUpstream := func(cause error) {
68 p.propagateClose(p.upstream, p.client, &p.closeUpstreamOnce, cause)
69 }
70
71 wg.Go(func() { p.acceptBidi(p.client, p.upstream, closeUpstream) })
72 wg.Go(func() { p.acceptBidi(p.upstream, p.client, closeClient) })
73 wg.Go(func() { p.acceptUni(p.client, p.upstream, closeUpstream) })
74 wg.Go(func() { p.acceptUni(p.upstream, p.client, closeClient) })
75 wg.Go(func() { p.pumpDatagrams(p.client, p.upstream, closeUpstream) })
76 wg.Go(func() { p.pumpDatagrams(p.upstream, p.client, closeClient) })
77 wg.Wait()
78 p.streams.Wait()
79 }
80
81 // propagateClose closes target once with a code/message derived from
82 // cause. If cause carries a *webtransport.SessionError (the common case —
83 // Accept{,Uni}Stream returns it directly when the peer closed the
84 // session), its code/message are used. Otherwise, typically the
85 // datagram loop won the race to detect the close and its error lacks
86 // the code, so we fall back to probing peer for its stored close state
87 // via a short AcceptStream.
88 func (p *webtransportPump) propagateClose(target, peer *webtransport.Session, once *sync.Once, cause error) {
89 once.Do(func() {
90 code, msg, ok := closeCodeFromErr(cause)
91 if !ok {
92 code, msg, _ = codeFromSession(peer)
93 }
94 _ = target.CloseWithError(code, msg)
95 })
96 }
97
98 // codeFromSession reads the peer's stored SessionError by waiting for the
99 // session's context to be cancelled — by that point webtransport-go has
100 // set closeErr — and then calling AcceptStream, which returns it via its
101 // initial closeErr check without blocking. Used only on the close path
102 // when the caller's own error didn't carry the code (e.g. ReceiveDatagram
103 // returned a context error).
104 func codeFromSession(sess *webtransport.Session) (webtransport.SessionErrorCode, string, bool) {
105 select {
106 case <-sess.Context().Done():
107 case <-time.After(200 * time.Millisecond):
108 return 0, "", false
109 }
110 _, err := sess.AcceptStream(context.Background())
111 return closeCodeFromErr(err)
112 }
113
114 // acceptBidi loops on src.AcceptStream and, for each accepted
115 // bidirectional stream, opens a matching stream on dst and pipes bytes in
116 // both directions. When src ends, it invokes propagate to close dst.
117 func (p *webtransportPump) acceptBidi(src, dst *webtransport.Session, propagate func(error)) {
118 ctx := src.Context()
119 for {
120 srcStr, err := src.AcceptStream(ctx)
121 if err != nil {
122 propagate(err)
123 return
124 }
125 dstStr, err := dst.OpenStreamSync(ctx)
126 if err != nil {
127 p.logger.Debug("webtransport: open upstream bidi failed", zap.Error(err))
128 srcStr.CancelRead(0)
129 srcStr.CancelWrite(0)
130 propagate(err)
131 return
132 }
133 p.streams.Go(func() { p.spliceBidi(srcStr, dstStr) })
134 }
135 }
136
137 // spliceBidi copies bytes between two bidirectional streams until both
138 // sides observe EOF or an error.
139 func (p *webtransportPump) spliceBidi(a, b *webtransport.Stream) {
140 var wg sync.WaitGroup
141 wg.Go(func() {
142 p.spliceOne(b, a, "webtransport bidi splice a->b")
143 })
144 wg.Go(func() {
145 p.spliceOne(a, b, "webtransport bidi splice b->a")
146 })
147 wg.Wait()
148 }
149
150 // acceptUni loops on src.AcceptUniStream and, for each stream, opens a
151 // matching unidirectional stream on dst and pipes bytes through.
152 func (p *webtransportPump) acceptUni(src, dst *webtransport.Session, propagate func(error)) {
153 ctx := src.Context()
154 for {
155 recv, err := src.AcceptUniStream(ctx)
156 if err != nil {
157 propagate(err)
158 return
159 }
160 send, err := dst.OpenUniStreamSync(ctx)
161 if err != nil {
162 p.logger.Debug("webtransport: open upstream uni failed", zap.Error(err))
163 recv.CancelRead(0)
164 propagate(err)
165 return
166 }
167 p.streams.Go(func() {
168 if _, err := streamCopy(send, recv); err != nil && !isExpectedEOF(err) {
169 p.logger.Debug("webtransport uni splice", zap.Error(err))
170 recv.CancelRead(0)
171 send.CancelWrite(0)
172 }
173 _ = send.Close()
174 })
175 }
176 }
177
178 // spliceOne copies src into dst. A hard error cancels both reads so the
179 // opposite copy, which may be blocked in Read, can finish. A clean EOF
180 // only closes dst's write side; the other direction may still be sending.
181 func (p *webtransportPump) spliceOne(dst, src *webtransport.Stream, failMsg string) {
182 if _, err := streamCopy(dst, src); err != nil && !isExpectedEOF(err) {
183 p.logger.Debug(failMsg, zap.Error(err))
184 src.CancelRead(0)
185 dst.CancelRead(0)
186 }
187 _ = dst.Close()
188 }
189
190 // pumpDatagrams forwards datagrams from src to dst until src ends. Unlike
191 // streams, datagrams are unreliable, so SendDatagram errors are best-effort
192 // and are not treated as fatal for the session.
193 func (p *webtransportPump) pumpDatagrams(src, dst *webtransport.Session, propagate func(error)) {
194 ctx := src.Context()
195 for {
196 data, err := src.ReceiveDatagram(ctx)
197 if err != nil {
198 propagate(err)
199 return
200 }
201 if err := dst.SendDatagram(data); err != nil {
202 p.logger.Debug("webtransport send datagram", zap.Error(err))
203 }
204 }
205 }
206
207 // streamCopy is io.Copy. Tests replace it to hold a worker open across shutdown.
208 var streamCopy = io.Copy
209
210 // closeCodeFromErr extracts a SessionErrorCode + message from err if it
211 // represents a session close. The bool is false when err is nil or not
212 // a *webtransport.SessionError.
213 func closeCodeFromErr(err error) (webtransport.SessionErrorCode, string, bool) {
214 sessErr, ok := errors.AsType[*webtransport.SessionError](err)
215 if ok {
216 return sessErr.ErrorCode, sessErr.Message, true
217 }
218 return 0, "", false
219 }
220
221 // isExpectedEOF reports whether err is one we don't need to log: plain
222 // EOF, context cancellation, or an already-closed session.
223 func isExpectedEOF(err error) bool {
224 if err == nil {
225 return true
226 }
227 if errors.Is(err, io.EOF) || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
228 return true
229 }
230 var sessErr *webtransport.SessionError
231 return errors.As(err, &sessErr)
232 }