cloudflare/cloudflared

Public

mirrored from https://github.com/cloudflare/cloudflaredAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
cd4af5696d0db81caae903b33e4bbddfb5aff9ff

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

connection/http2.go

332lines · modecode

1package connection
2
3import (
4 "context"
5 "fmt"
6 "io"
7 "math"
8 "net"
9 "net/http"
10 "strings"
11 "sync"
12
13 "github.com/pkg/errors"
14 "github.com/rs/zerolog"
15 "golang.org/x/net/http2"
16
17 tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
18)
19
20// note: these constants are exported so we can reuse them in the edge-side code
21const (
22 InternalUpgradeHeader = "Cf-Cloudflared-Proxy-Connection-Upgrade"
23 InternalTCPProxySrcHeader = "Cf-Cloudflared-Proxy-Src"
24 WebsocketUpgrade = "websocket"
25 ControlStreamUpgrade = "control-stream"
26)
27
28var errEdgeConnectionClosed = fmt.Errorf("connection with edge closed")
29
30// HTTP2Connection represents a net.Conn that uses HTTP2 frames to proxy traffic from the edge to cloudflared on the
31// origin.
32type HTTP2Connection struct {
33 conn net.Conn
34 server *http2.Server
35 config *Config
36 namedTunnel *NamedTunnelConfig
37 connOptions *tunnelpogs.ConnectionOptions
38 observer *Observer
39 connIndexStr string
40 connIndex uint8
41 // newRPCClientFunc allows us to mock RPCs during testing
42 newRPCClientFunc func(context.Context, io.ReadWriteCloser, *zerolog.Logger) NamedTunnelRPCClient
43
44 log *zerolog.Logger
45 activeRequestsWG sync.WaitGroup
46 connectedFuse ConnectedFuse
47 gracefulShutdownC <-chan struct{}
48 stoppedGracefully bool
49 controlStreamErr error // result of running control stream handler
50}
51
52// NewHTTP2Connection returns a new instance of HTTP2Connection.
53func NewHTTP2Connection(
54 conn net.Conn,
55 config *Config,
56 namedTunnelConfig *NamedTunnelConfig,
57 connOptions *tunnelpogs.ConnectionOptions,
58 observer *Observer,
59 connIndex uint8,
60 connectedFuse ConnectedFuse,
61 log *zerolog.Logger,
62 gracefulShutdownC <-chan struct{},
63) *HTTP2Connection {
64 return &HTTP2Connection{
65 conn: conn,
66 server: &http2.Server{
67 MaxConcurrentStreams: math.MaxUint32,
68 },
69 config: config,
70 namedTunnel: namedTunnelConfig,
71 connOptions: connOptions,
72 observer: observer,
73 connIndexStr: uint8ToString(connIndex),
74 connIndex: connIndex,
75 newRPCClientFunc: newRegistrationRPCClient,
76 connectedFuse: connectedFuse,
77 log: log,
78 gracefulShutdownC: gracefulShutdownC,
79 }
80}
81
82// Serve serves an HTTP2 server that the edge can talk to.
83func (c *HTTP2Connection) Serve(ctx context.Context) error {
84 go func() {
85 <-ctx.Done()
86 c.close()
87 }()
88 c.server.ServeConn(c.conn, &http2.ServeConnOpts{
89 Context: ctx,
90 Handler: c,
91 })
92
93 switch {
94 case c.stoppedGracefully:
95 return nil
96 case c.controlStreamErr != nil:
97 return c.controlStreamErr
98 default:
99 c.observer.log.Info().Uint8(LogFieldConnIndex, c.connIndex).Msg("Lost connection with the edge")
100 return errEdgeConnectionClosed
101 }
102}
103
104func (c *HTTP2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
105 c.activeRequestsWG.Add(1)
106 defer c.activeRequestsWG.Done()
107
108 connType := determineHTTP2Type(r)
109 handleMissingRequestParts(connType, r)
110
111 respWriter, err := newHTTP2RespWriter(r, w, connType)
112 if err != nil {
113 c.observer.log.Error().Msg(err.Error())
114 return
115 }
116
117 switch connType {
118 case TypeControlStream:
119 if err := c.serveControlStream(r.Context(), respWriter); err != nil {
120 c.controlStreamErr = err
121 c.log.Error().Err(err)
122 respWriter.WriteErrorResponse()
123 }
124
125 case TypeWebsocket, TypeHTTP:
126 stripWebsocketUpgradeHeader(r)
127 if err := c.config.OriginProxy.ProxyHTTP(respWriter, r, connType == TypeWebsocket); err != nil {
128 err := fmt.Errorf("Failed to proxy HTTP: %w", err)
129 c.log.Error().Err(err)
130 respWriter.WriteErrorResponse()
131 }
132
133 case TypeTCP:
134 host, err := getRequestHost(r)
135 if err != nil {
136 err := fmt.Errorf(`cloudflared recieved a warp-routing request with an empty host value: %w`, err)
137 c.log.Error().Err(err)
138 respWriter.WriteErrorResponse()
139 }
140
141 rws := NewHTTPResponseReadWriterAcker(respWriter, r)
142 if err := c.config.OriginProxy.ProxyTCP(r.Context(), rws, &TCPRequest{
143 Dest: host,
144 CFRay: FindCfRayHeader(r),
145 LBProbe: IsLBProbeRequest(r),
146 }); err != nil {
147 respWriter.WriteErrorResponse()
148 }
149
150 default:
151 err := fmt.Errorf("Received unknown connection type: %s", connType)
152 c.log.Error().Err(err)
153 respWriter.WriteErrorResponse()
154 }
155}
156
157func (c *HTTP2Connection) serveControlStream(ctx context.Context, respWriter *http2RespWriter) error {
158 rpcClient := c.newRPCClientFunc(ctx, respWriter, c.observer.log)
159 defer rpcClient.Close()
160
161 if err := rpcClient.RegisterConnection(ctx, c.namedTunnel, c.connOptions, c.connIndex, c.observer); err != nil {
162 return err
163 }
164 c.connectedFuse.Connected()
165
166 // wait for connection termination or start of graceful shutdown
167 select {
168 case <-ctx.Done():
169 break
170 case <-c.gracefulShutdownC:
171 c.stoppedGracefully = true
172 }
173
174 c.observer.sendUnregisteringEvent(c.connIndex)
175 rpcClient.GracefulShutdown(ctx, c.config.GracePeriod)
176 c.observer.log.Info().Uint8(LogFieldConnIndex, c.connIndex).Msg("Unregistered tunnel connection")
177 return nil
178}
179
180func (c *HTTP2Connection) close() {
181 // Wait for all serve HTTP handlers to return
182 c.activeRequestsWG.Wait()
183 c.conn.Close()
184}
185
186type http2RespWriter struct {
187 r io.Reader
188 w http.ResponseWriter
189 flusher http.Flusher
190 shouldFlush bool
191}
192
193func newHTTP2RespWriter(r *http.Request, w http.ResponseWriter, connType Type) (*http2RespWriter, error) {
194 flusher, isFlusher := w.(http.Flusher)
195 if !isFlusher {
196 respWriter := &http2RespWriter{
197 r: r.Body,
198 w: w,
199 }
200 respWriter.WriteErrorResponse()
201 return nil, fmt.Errorf("%T doesn't implement http.Flusher", w)
202 }
203
204 return &http2RespWriter{
205 r: r.Body,
206 w: w,
207 flusher: flusher,
208 shouldFlush: connType.shouldFlush(),
209 }, nil
210}
211
212func (rp *http2RespWriter) WriteRespHeaders(status int, header http.Header) error {
213 dest := rp.w.Header()
214 userHeaders := make(http.Header, len(header))
215 for name, values := range header {
216 // Since these are http2 headers, they're required to be lowercase
217 h2name := strings.ToLower(name)
218 if h2name == "content-length" {
219 // This header has meaning in HTTP/2 and will be used by the edge,
220 // so it should be sent as an HTTP/2 response header.
221 dest[name] = values
222 // Since these are http2 headers, they're required to be lowercase
223 } else if !IsControlHeader(h2name) || IsWebsocketClientHeader(h2name) {
224 // User headers, on the other hand, must all be serialized so that
225 // HTTP/2 header validation won't be applied to HTTP/1 header values
226 userHeaders[name] = values
227 }
228 }
229
230 // Perform user header serialization and set them in the single header
231 dest.Set(CanonicalResponseUserHeaders, SerializeHeaders(userHeaders))
232 rp.setResponseMetaHeader(responseMetaHeaderOrigin)
233 // HTTP2 removes support for 101 Switching Protocols https://tools.ietf.org/html/rfc7540#section-8.1.1
234 if status == http.StatusSwitchingProtocols {
235 status = http.StatusOK
236 }
237 rp.w.WriteHeader(status)
238 if IsServerSentEvent(header) {
239 rp.shouldFlush = true
240 }
241 if rp.shouldFlush {
242 rp.flusher.Flush()
243 }
244 return nil
245}
246
247func (rp *http2RespWriter) WriteErrorResponse() {
248 rp.setResponseMetaHeader(responseMetaHeaderCfd)
249 rp.w.WriteHeader(http.StatusBadGateway)
250}
251
252func (rp *http2RespWriter) setResponseMetaHeader(value string) {
253 rp.w.Header().Set(CanonicalResponseMetaHeader, value)
254}
255
256func (rp *http2RespWriter) Read(p []byte) (n int, err error) {
257 return rp.r.Read(p)
258}
259
260func (rp *http2RespWriter) Write(p []byte) (n int, err error) {
261 defer func() {
262 // Implementer of OriginClient should make sure it doesn't write to the connection after Proxy returns
263 // Register a recover routine just in case.
264 if r := recover(); r != nil {
265 println("Recover from http2 response writer panic, error", r)
266 }
267 }()
268 n, err = rp.w.Write(p)
269 if err == nil && rp.shouldFlush {
270 rp.flusher.Flush()
271 }
272 return n, err
273}
274
275func (rp *http2RespWriter) Close() error {
276 return nil
277}
278
279func determineHTTP2Type(r *http.Request) Type {
280 switch {
281 case isWebsocketUpgrade(r):
282 return TypeWebsocket
283 case IsTCPStream(r):
284 return TypeTCP
285 case isControlStreamUpgrade(r):
286 return TypeControlStream
287 default:
288 return TypeHTTP
289 }
290}
291
292func handleMissingRequestParts(connType Type, r *http.Request) {
293 if connType == TypeHTTP {
294 // http library has no guarantees that we receive a filled URL. If not, then we fill it, as we reuse the request
295 // for proxying. We use the same values as we used to in h2mux. For proxying they should not matter since we
296 // control the dialer on every egress proxied.
297 if len(r.URL.Scheme) == 0 {
298 r.URL.Scheme = "http"
299 }
300 if len(r.URL.Host) == 0 {
301 r.URL.Host = "localhost:8080"
302 }
303 }
304}
305
306func isControlStreamUpgrade(r *http.Request) bool {
307 return r.Header.Get(InternalUpgradeHeader) == ControlStreamUpgrade
308}
309
310func isWebsocketUpgrade(r *http.Request) bool {
311 return r.Header.Get(InternalUpgradeHeader) == WebsocketUpgrade
312}
313
314// IsTCPStream discerns if the connection request needs a tcp stream proxy.
315func IsTCPStream(r *http.Request) bool {
316 return r.Header.Get(InternalTCPProxySrcHeader) != ""
317}
318
319func stripWebsocketUpgradeHeader(r *http.Request) {
320 r.Header.Del(InternalUpgradeHeader)
321}
322
323// getRequestHost returns the host of the http.Request.
324func getRequestHost(r *http.Request) (string, error) {
325 if r.Host != "" {
326 return r.Host, nil
327 }
328 if r.URL != nil {
329 return r.URL.Host, nil
330 }
331 return "", errors.New("host not set in incoming request")
332}
333