@marcel91200 / caddy Mirror · read-only / commits / 9c0c71e5
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 12, 05:45 UTC

reverseproxy: Rewrite requests and responses for websocket over http2 (#6567)

* reverse proxy: rewrite requests and responses for websocket over http2 * delete protocol pseudo-header * modify cloned requests * set request variable to track if it's a h2 websocket * use request bodu * rewrite request body * use WebSocket instead of Websocket in the headers * use logger check for zap loggers * fix lint

WeidiDeng committed Dec 7, 2024 at 04:23 UTC 9c0c71e5774593e15a555d5c88de0af011e46394
2 files changed +88 -13
modules/caddyhttp/reverseproxy/reverseproxy.go
+19
@@ -17,6 +17,8 @@ package reverseproxy
17 import (
18 "bytes"
19 "context"
20 + "crypto/rand"
21 + "encoding/base64"
22 "encoding/json"
23 "errors"
24 "fmt"
@@ -394,6 +396,23 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
396 return caddyhttp.Error(http.StatusInternalServerError,
397 fmt.Errorf("preparing request for upstream round-trip: %v", err))
398 }
399 + // websocket over http2, assuming backend doesn't support this, the request will be modified to http1.1 upgrade
400 + // TODO: once we can reliably detect backend support this, it can be removed for those backends
401 + if r.ProtoMajor == 2 && r.Method == http.MethodConnect && r.Header.Get(":protocol") != "" {
402 + clonedReq.Header.Del(":protocol")
403 + // keep the body for later use. http1.1 upgrade uses http.NoBody
404 + caddyhttp.SetVar(clonedReq.Context(), "h2_websocket_body", clonedReq.Body)
405 + clonedReq.Body = http.NoBody
406 + clonedReq.Method = http.MethodGet
407 + clonedReq.Header.Set("Upgrade", r.Header.Get(":protocol"))
408 + clonedReq.Header.Set("Connection", "Upgrade")
409 + key := make([]byte, 16)
410 + _, randErr := rand.Read(key)
411 + if randErr != nil {
412 + return randErr
413 + }
414 + clonedReq.Header["Sec-WebSocket-Key"] = []string{base64.StdEncoding.EncodeToString(key)}
415 + }
416
417 // we will need the original headers and Host value if
418 // header operations are configured; this is so that each
modules/caddyhttp/reverseproxy/streaming.go
+69 -13
@@ -19,6 +19,7 @@
19 package reverseproxy
20
21 import (
22 + "bufio"
23 "context"
24 "errors"
25 "fmt"
@@ -33,8 +34,29 @@ import (
34 "go.uber.org/zap"
35 "go.uber.org/zap/zapcore"
36 "golang.org/x/net/http/httpguts"
37 +
38 + "github.com/caddyserver/caddy/v2/modules/caddyhttp"
39 )
40
41 +type h2ReadWriteCloser struct {
42 + io.ReadCloser
43 + http.ResponseWriter
44 +}
45 +
46 +func (rwc h2ReadWriteCloser) Write(p []byte) (n int, err error) {
47 + n, err = rwc.ResponseWriter.Write(p)
48 + if err != nil {
49 + return 0, err
50 + }
51 +
52 + //nolint:bodyclose
53 + err = http.NewResponseController(rwc.ResponseWriter).Flush()
54 + if err != nil {
55 + return 0, err
56 + }
57 + return n, nil
58 +}
59 +
60 func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup, rw http.ResponseWriter, req *http.Request, res *http.Response) {
61 reqUpType := upgradeType(req.Header)
62 resUpType := upgradeType(res.Header)
@@ -67,24 +89,58 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup,
89 // like the rest of handler chain.
90 copyHeader(rw.Header(), res.Header)
91 normalizeWebsocketHeaders(rw.Header())
70 - rw.WriteHeader(res.StatusCode)
92
72 - logger.Debug("upgrading connection")
93 + var (
94 + conn io.ReadWriteCloser
95 + brw *bufio.ReadWriter
96 + )
97 + // websocket over http2, assuming backend doesn't support this, the request will be modified to http1.1 upgrade
98 + // TODO: once we can reliably detect backend support this, it can be removed for those backends
99 + if body, ok := caddyhttp.GetVar(req.Context(), "h2_websocket_body").(io.ReadCloser); ok {
100 + req.Body = body
101 + rw.Header().Del("Upgrade")
102 + rw.Header().Del("Connection")
103 + delete(rw.Header(), "Sec-WebSocket-Accept")
104 + rw.WriteHeader(http.StatusOK)
105 +
106 + if c := logger.Check(zap.DebugLevel, "upgrading connection"); c != nil {
107 + c.Write(zap.Int("http_version", 2))
108 + }
109
74 - //nolint:bodyclose
75 - conn, brw, hijackErr := http.NewResponseController(rw).Hijack()
76 - if errors.Is(hijackErr, http.ErrNotSupported) {
77 - if c := logger.Check(zapcore.ErrorLevel, "can't switch protocols using non-Hijacker ResponseWriter"); c != nil {
78 - c.Write(zap.String("type", fmt.Sprintf("%T", rw)))
110 + //nolint:bodyclose
111 + flushErr := http.NewResponseController(rw).Flush()
112 + if flushErr != nil {
113 + if c := h.logger.Check(zap.ErrorLevel, "failed to flush http2 websocket response"); c != nil {
114 + c.Write(zap.Error(flushErr))
115 + }
116 + return
117 }
80 - return
81 - }
118 + conn = h2ReadWriteCloser{req.Body, rw}
119 + // bufio is not needed, use minimal buffer
120 + brw = bufio.NewReadWriter(bufio.NewReaderSize(conn, 1), bufio.NewWriterSize(conn, 1))
121 + } else {
122 + rw.WriteHeader(res.StatusCode)
123
83 - if hijackErr != nil {
84 - if c := logger.Check(zapcore.ErrorLevel, "hijack failed on protocol switch"); c != nil {
85 - c.Write(zap.Error(hijackErr))
124 + if c := logger.Check(zap.DebugLevel, "upgrading connection"); c != nil {
125 + c.Write(zap.Int("http_version", req.ProtoMajor))
126 + }
127 +
128 + var hijackErr error
129 + //nolint:bodyclose
130 + conn, brw, hijackErr = http.NewResponseController(rw).Hijack()
131 + if errors.Is(hijackErr, http.ErrNotSupported) {
132 + if c := h.logger.Check(zap.ErrorLevel, "can't switch protocols using non-Hijacker ResponseWriter"); c != nil {
133 + c.Write(zap.String("type", fmt.Sprintf("%T", rw)))
134 + }
135 + return
136 + }
137 +
138 + if hijackErr != nil {
139 + if c := h.logger.Check(zap.ErrorLevel, "hijack failed on protocol switch"); c != nil {
140 + c.Write(zap.Error(hijackErr))
141 + }
142 + return
143 }
87 - return
144 }
145
146 // adopted from https://github.com/golang/go/commit/8bcf2834afdf6a1f7937390903a41518715ef6f5