cloudflare/cloudflared

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
2e2fa296379f04e959b9b2c07899c65e738bdfd0

Branches

Tags

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

Clone

HTTPS

Download ZIP

origin/tunnel.go

780lines · modecode

1package origin
2
3import (
4 "bufio"
5 "context"
6 "crypto/tls"
7 "fmt"
8 "io"
9 "net"
10 "net/http"
11 "net/url"
12 "strconv"
13 "strings"
14 "sync"
15 "time"
16
17 "github.com/cloudflare/cloudflared/connection"
18 "github.com/cloudflare/cloudflared/h2mux"
19 "github.com/cloudflare/cloudflared/signal"
20 "github.com/cloudflare/cloudflared/tunnelrpc"
21 tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
22 "github.com/cloudflare/cloudflared/validation"
23 "github.com/cloudflare/cloudflared/websocket"
24
25 raven "github.com/getsentry/raven-go"
26 "github.com/google/uuid"
27 "github.com/pkg/errors"
28 "github.com/prometheus/client_golang/prometheus"
29 _ "github.com/prometheus/client_golang/prometheus"
30 log "github.com/sirupsen/logrus"
31 "golang.org/x/sync/errgroup"
32 rpc "zombiezen.com/go/capnproto2/rpc"
33)
34
35const (
36 dialTimeout = 15 * time.Second
37 openStreamTimeout = 30 * time.Second
38 lbProbeUserAgentPrefix = "Mozilla/5.0 (compatible; Cloudflare-Traffic-Manager/1.0; +https://www.cloudflare.com/traffic-manager/;"
39 TagHeaderNamePrefix = "Cf-Warp-Tag-"
40 DuplicateConnectionError = "EDUPCONN"
41)
42
43type TunnelConfig struct {
44 BuildInfo *BuildInfo
45 ClientID string
46 ClientTlsConfig *tls.Config
47 CloseConnOnce *sync.Once // Used to close connectedSignal no more than once
48 CompressionQuality uint64
49 EdgeAddrs []string
50 GracePeriod time.Duration
51 HAConnections int
52 HTTPTransport http.RoundTripper
53 HeartbeatInterval time.Duration
54 Hostname string
55 IncidentLookup IncidentLookup
56 IsAutoupdated bool
57 IsFreeTunnel bool
58 LBPool string
59 Logger *log.Logger
60 MaxHeartbeats uint64
61 Metrics *TunnelMetrics
62 MetricsUpdateFreq time.Duration
63 NoChunkedEncoding bool
64 OriginCert []byte
65 ReportedVersion string
66 Retries uint
67 RunFromTerminal bool
68 Tags []tunnelpogs.Tag
69 TlsConfig *tls.Config
70 TransportLogger *log.Logger
71 UseDeclarativeTunnel bool
72 WSGI bool
73 // OriginUrl may not be used if a user specifies a unix socket.
74 OriginUrl string
75}
76
77type dialError struct {
78 cause error
79}
80
81func (e dialError) Error() string {
82 return e.cause.Error()
83}
84
85type dupConnRegisterTunnelError struct{}
86
87func (e dupConnRegisterTunnelError) Error() string {
88 return "already connected to this server"
89}
90
91type muxerShutdownError struct{}
92
93func (e muxerShutdownError) Error() string {
94 return "muxer shutdown"
95}
96
97// RegisterTunnel error from server
98type serverRegisterTunnelError struct {
99 cause error
100 permanent bool
101}
102
103func (e serverRegisterTunnelError) Error() string {
104 return e.cause.Error()
105}
106
107// RegisterTunnel error from client
108type clientRegisterTunnelError struct {
109 cause error
110}
111
112func newClientRegisterTunnelError(cause error, counter *prometheus.CounterVec) clientRegisterTunnelError {
113 counter.WithLabelValues(cause.Error()).Inc()
114 return clientRegisterTunnelError{cause: cause}
115}
116
117func (e clientRegisterTunnelError) Error() string {
118 return e.cause.Error()
119}
120
121func (c *TunnelConfig) RegistrationOptions(connectionID uint8, OriginLocalIP string, uuid uuid.UUID) *tunnelpogs.RegistrationOptions {
122 policy := tunnelrpc.ExistingTunnelPolicy_balance
123 if c.HAConnections <= 1 && c.LBPool == "" {
124 policy = tunnelrpc.ExistingTunnelPolicy_disconnect
125 }
126 return &tunnelpogs.RegistrationOptions{
127 ClientID: c.ClientID,
128 Version: c.ReportedVersion,
129 OS: fmt.Sprintf("%s_%s", c.BuildInfo.GoOS, c.BuildInfo.GoArch),
130 ExistingTunnelPolicy: policy,
131 PoolName: c.LBPool,
132 Tags: c.Tags,
133 ConnectionID: connectionID,
134 OriginLocalIP: OriginLocalIP,
135 IsAutoupdated: c.IsAutoupdated,
136 RunFromTerminal: c.RunFromTerminal,
137 CompressionQuality: c.CompressionQuality,
138 UUID: uuid.String(),
139 }
140}
141
142func StartTunnelDaemon(config *TunnelConfig, shutdownC <-chan struct{}, connectedSignal *signal.Signal) error {
143 ctx, cancel := context.WithCancel(context.Background())
144 go func() {
145 <-shutdownC
146 cancel()
147 }()
148
149 u, err := uuid.NewRandom()
150 if err != nil {
151 return err
152 }
153
154 // If a user specified negative HAConnections, we will treat it as requesting 1 connection
155 if config.HAConnections > 1 {
156 if config.UseDeclarativeTunnel {
157 return connection.NewSupervisor(&connection.CloudflaredConfig{
158 ConnectionConfig: &connection.ConnectionConfig{
159 TLSConfig: config.TlsConfig,
160 HeartbeatInterval: config.HeartbeatInterval,
161 MaxHeartbeats: config.MaxHeartbeats,
162 Logger: config.Logger.WithField("subsystem", "connection_supervisor"),
163 },
164 OriginCert: config.OriginCert,
165 Tags: config.Tags,
166 EdgeAddrs: config.EdgeAddrs,
167 HAConnections: uint(config.HAConnections),
168 Logger: config.Logger,
169 }).Run(ctx)
170 }
171 return NewSupervisor(config).Run(ctx, connectedSignal, u)
172 } else {
173 addrs, err := connection.ResolveEdgeIPs(config.Logger, config.EdgeAddrs)
174 if err != nil {
175 return err
176 }
177 return ServeTunnelLoop(ctx, config, addrs[0], 0, connectedSignal, u)
178 }
179}
180
181func ServeTunnelLoop(ctx context.Context,
182 config *TunnelConfig,
183 addr *net.TCPAddr,
184 connectionID uint8,
185 connectedSignal *signal.Signal,
186 u uuid.UUID,
187) error {
188 logger := config.Logger
189 config.Metrics.incrementHaConnections()
190 defer config.Metrics.decrementHaConnections()
191 backoff := BackoffHandler{MaxRetries: config.Retries}
192 connectedFuse := h2mux.NewBooleanFuse()
193 go func() {
194 if connectedFuse.Await() {
195 connectedSignal.Notify()
196 }
197 }()
198 // Ensure the above goroutine will terminate if we return without connecting
199 defer connectedFuse.Fuse(false)
200 for {
201 err, recoverable := ServeTunnel(ctx, config, addr, connectionID, connectedFuse, &backoff, u)
202 if recoverable {
203 if duration, ok := backoff.GetBackoffDuration(ctx); ok {
204 logger.Infof("Retrying in %s seconds", duration)
205 backoff.Backoff(ctx)
206 continue
207 }
208 }
209 return err
210 }
211}
212
213func ServeTunnel(
214 ctx context.Context,
215 config *TunnelConfig,
216 addr *net.TCPAddr,
217 connectionID uint8,
218 connectedFuse *h2mux.BooleanFuse,
219 backoff *BackoffHandler,
220 u uuid.UUID,
221) (err error, recoverable bool) {
222 // Treat panics as recoverable errors
223 defer func() {
224 if r := recover(); r != nil {
225 var ok bool
226 err, ok = r.(error)
227 if !ok {
228 err = fmt.Errorf("ServeTunnel: %v", r)
229 }
230 recoverable = true
231 }
232 }()
233
234 connectionTag := uint8ToString(connectionID)
235 logger := config.Logger.WithField("connectionID", connectionTag)
236
237 // additional tags to send other than hostname which is set in cloudflared main package
238 tags := make(map[string]string)
239 tags["ha"] = connectionTag
240
241 // Returns error from parsing the origin URL or handshake errors
242 handler, originLocalIP, err := NewTunnelHandler(ctx, config, addr.String(), connectionID)
243 if err != nil {
244 errLog := config.Logger.WithError(err)
245 switch err.(type) {
246 case dialError:
247 errLog.Error("Unable to dial edge")
248 case h2mux.MuxerHandshakeError:
249 errLog.Error("Handshake failed with edge server")
250 default:
251 errLog.Error("Tunnel creation failure")
252 return err, false
253 }
254 return err, true
255 }
256
257 errGroup, serveCtx := errgroup.WithContext(ctx)
258
259 errGroup.Go(func() error {
260 err := RegisterTunnel(serveCtx, handler.muxer, config, connectionID, originLocalIP, u)
261 if err == nil {
262 connectedFuse.Fuse(true)
263 backoff.SetGracePeriod()
264 }
265 return err
266 })
267
268 errGroup.Go(func() error {
269 updateMetricsTickC := time.Tick(config.MetricsUpdateFreq)
270 for {
271 select {
272 case <-serveCtx.Done():
273 // UnregisterTunnel blocks until the RPC call returns
274 err := UnregisterTunnel(handler.muxer, config.GracePeriod, config.TransportLogger)
275 handler.muxer.Shutdown()
276 return err
277 case <-updateMetricsTickC:
278 handler.UpdateMetrics(connectionTag)
279 }
280 }
281 })
282
283 errGroup.Go(func() error {
284 // All routines should stop when muxer finish serving. When muxer is shutdown
285 // gracefully, it doesn't return an error, so we need to return errMuxerShutdown
286 // here to notify other routines to stop
287 err := handler.muxer.Serve(serveCtx)
288 if err == nil {
289 return muxerShutdownError{}
290 }
291 return err
292 })
293
294 err = errGroup.Wait()
295 if err != nil {
296 switch castedErr := err.(type) {
297 case dupConnRegisterTunnelError:
298 logger.Info("Already connected to this server, selecting a different one")
299 return err, true
300 case serverRegisterTunnelError:
301 logger.WithError(castedErr.cause).Error("Register tunnel error from server side")
302 // Don't send registration error return from server to Sentry. They are
303 // logged on server side
304 if incidents := config.IncidentLookup.ActiveIncidents(); len(incidents) > 0 {
305 logger.Error(activeIncidentsMsg(incidents))
306 }
307 return castedErr.cause, !castedErr.permanent
308 case clientRegisterTunnelError:
309 logger.WithError(castedErr.cause).Error("Register tunnel error on client side")
310 raven.CaptureError(castedErr.cause, tags)
311 return err, true
312 case muxerShutdownError:
313 logger.Infof("Muxer shutdown")
314 return err, true
315 default:
316 logger.WithError(err).Error("Serve tunnel error")
317 raven.CaptureError(err, tags)
318 return err, true
319 }
320 }
321 return nil, true
322}
323
324func IsRPCStreamResponse(headers []h2mux.Header) bool {
325 if len(headers) != 1 {
326 return false
327 }
328 if headers[0].Name != ":status" || headers[0].Value != "200" {
329 return false
330 }
331 return true
332}
333
334func RegisterTunnel(
335 ctx context.Context,
336 muxer *h2mux.Muxer,
337 config *TunnelConfig,
338 connectionID uint8,
339 originLocalIP string,
340 uuid uuid.UUID,
341) error {
342 config.TransportLogger.Debug("initiating RPC stream to register")
343 stream, err := openStream(ctx, muxer)
344 if err != nil {
345 // RPC stream open error
346 return newClientRegisterTunnelError(err, config.Metrics.rpcFail)
347 }
348 if !IsRPCStreamResponse(stream.Headers) {
349 // stream response error
350 return newClientRegisterTunnelError(err, config.Metrics.rpcFail)
351 }
352 conn := rpc.NewConn(
353 tunnelrpc.NewTransportLogger(config.TransportLogger.WithField("subsystem", "rpc-register"), rpc.StreamTransport(stream)),
354 tunnelrpc.ConnLog(config.TransportLogger.WithField("subsystem", "rpc-transport")),
355 )
356 defer conn.Close()
357 ts := tunnelpogs.TunnelServer_PogsClient{Client: conn.Bootstrap(ctx)}
358 // Request server info without blocking tunnel registration; must use capnp library directly.
359 tsClient := tunnelrpc.TunnelServer{Client: ts.Client}
360 serverInfoPromise := tsClient.GetServerInfo(ctx, func(tunnelrpc.TunnelServer_getServerInfo_Params) error {
361 return nil
362 })
363 registration, err := ts.RegisterTunnel(
364 ctx,
365 config.OriginCert,
366 config.Hostname,
367 config.RegistrationOptions(connectionID, originLocalIP, uuid),
368 )
369 LogServerInfo(serverInfoPromise.Result(), connectionID, config.Metrics, config.Logger)
370 if err != nil {
371 // RegisterTunnel RPC failure
372 return newClientRegisterTunnelError(err, config.Metrics.regFail)
373 }
374 for _, logLine := range registration.LogLines {
375 config.Logger.Info(logLine)
376 }
377
378 if regErr := processRegisterTunnelError(registration.Err, registration.PermanentFailure, config.Metrics); regErr != nil {
379 return regErr
380 }
381
382 if registration.TunnelID != "" {
383 config.Metrics.tunnelsHA.AddTunnelID(connectionID, registration.TunnelID)
384 config.Logger.Infof("Each HA connection's tunnel IDs: %v", config.Metrics.tunnelsHA.String())
385 }
386
387 // Print out the user's trial zone URL in a nice box (if they requested and got one)
388 if isTrialTunnel := config.Hostname == ""; isTrialTunnel {
389 if url, err := url.Parse(registration.Url); err == nil {
390 for _, line := range asciiBox(trialZoneMsg(url.String()), 2) {
391 config.Logger.Infoln(line)
392 }
393 } else {
394 config.Logger.Errorln("Failed to connect tunnel, please try again.")
395 return fmt.Errorf("empty URL in response from Cloudflare edge")
396 }
397 }
398
399 config.Metrics.userHostnamesCounts.WithLabelValues(registration.Url).Inc()
400
401 config.Logger.Infof("Route propagating, it may take up to 1 minute for your new route to become functional")
402 return nil
403}
404
405func processRegisterTunnelError(err string, permanentFailure bool, metrics *TunnelMetrics) error {
406 if err == "" {
407 metrics.regSuccess.Inc()
408 return nil
409 }
410
411 metrics.regFail.WithLabelValues(err).Inc()
412 if err == DuplicateConnectionError {
413 return dupConnRegisterTunnelError{}
414 }
415 return serverRegisterTunnelError{
416 cause: fmt.Errorf("Server error: %s", err),
417 permanent: permanentFailure,
418 }
419}
420
421func UnregisterTunnel(muxer *h2mux.Muxer, gracePeriod time.Duration, logger *log.Logger) error {
422 logger.Debug("initiating RPC stream to unregister")
423 ctx := context.Background()
424 stream, err := openStream(ctx, muxer)
425 if err != nil {
426 // RPC stream open error
427 return err
428 }
429 if !IsRPCStreamResponse(stream.Headers) {
430 // stream response error
431 return err
432 }
433 conn := rpc.NewConn(
434 tunnelrpc.NewTransportLogger(logger.WithField("subsystem", "rpc-unregister"), rpc.StreamTransport(stream)),
435 tunnelrpc.ConnLog(logger.WithField("subsystem", "rpc-transport")),
436 )
437 defer conn.Close()
438 ts := tunnelpogs.TunnelServer_PogsClient{Client: conn.Bootstrap(ctx)}
439 // gracePeriod is encoded in int64 using capnproto
440 return ts.UnregisterTunnel(ctx, gracePeriod.Nanoseconds())
441}
442
443func openStream(ctx context.Context, muxer *h2mux.Muxer) (*h2mux.MuxedStream, error) {
444 openStreamCtx, cancel := context.WithTimeout(ctx, openStreamTimeout)
445 defer cancel()
446 return muxer.OpenStream(openStreamCtx, []h2mux.Header{
447 {Name: ":method", Value: "RPC"},
448 {Name: ":scheme", Value: "capnp"},
449 {Name: ":path", Value: "*"},
450 }, nil)
451}
452
453func LogServerInfo(
454 promise tunnelrpc.ServerInfo_Promise,
455 connectionID uint8,
456 metrics *TunnelMetrics,
457 logger *log.Logger,
458) {
459 serverInfoMessage, err := promise.Struct()
460 if err != nil {
461 logger.WithError(err).Warn("Failed to retrieve server information")
462 return
463 }
464 serverInfo, err := tunnelpogs.UnmarshalServerInfo(serverInfoMessage)
465 if err != nil {
466 logger.WithError(err).Warn("Failed to retrieve server information")
467 return
468 }
469 logger.Infof("Connected to %s", serverInfo.LocationName)
470 metrics.registerServerLocation(uint8ToString(connectionID), serverInfo.LocationName)
471}
472
473func H2RequestHeadersToH1Request(h2 []h2mux.Header, h1 *http.Request) error {
474 for _, header := range h2 {
475 switch header.Name {
476 case ":method":
477 h1.Method = header.Value
478 case ":scheme":
479 case ":authority":
480 // Otherwise the host header will be based on the origin URL
481 h1.Host = header.Value
482 case ":path":
483 u, err := url.Parse(header.Value)
484 if err != nil {
485 return fmt.Errorf("unparseable path")
486 }
487 resolved := h1.URL.ResolveReference(u)
488 // prevent escaping base URL
489 if !strings.HasPrefix(resolved.String(), h1.URL.String()) {
490 return fmt.Errorf("invalid path")
491 }
492 h1.URL = resolved
493 case "content-length":
494 contentLength, err := strconv.ParseInt(header.Value, 10, 64)
495 if err != nil {
496 return fmt.Errorf("unparseable content length")
497 }
498 h1.ContentLength = contentLength
499 default:
500 h1.Header.Add(http.CanonicalHeaderKey(header.Name), header.Value)
501 }
502 }
503 return nil
504}
505
506func H1ResponseToH2Response(h1 *http.Response) (h2 []h2mux.Header) {
507 h2 = []h2mux.Header{{Name: ":status", Value: fmt.Sprintf("%d", h1.StatusCode)}}
508 for headerName, headerValues := range h1.Header {
509 for _, headerValue := range headerValues {
510 h2 = append(h2, h2mux.Header{Name: strings.ToLower(headerName), Value: headerValue})
511 }
512 }
513 return
514}
515
516func FindCfRayHeader(h1 *http.Request) string {
517 return h1.Header.Get("Cf-Ray")
518}
519
520type TunnelHandler struct {
521 originUrl string
522 muxer *h2mux.Muxer
523 httpClient http.RoundTripper
524 tlsConfig *tls.Config
525 tags []tunnelpogs.Tag
526 metrics *TunnelMetrics
527 // connectionID is only used by metrics, and prometheus requires labels to be string
528 connectionID string
529 logger *log.Logger
530 noChunkedEncoding bool
531}
532
533var dialer = net.Dialer{DualStack: true}
534
535// NewTunnelHandler returns a TunnelHandler, origin LAN IP and error
536func NewTunnelHandler(ctx context.Context,
537 config *TunnelConfig,
538 addr string,
539 connectionID uint8,
540) (*TunnelHandler, string, error) {
541 originURL, err := validation.ValidateUrl(config.OriginUrl)
542 if err != nil {
543 return nil, "", fmt.Errorf("unable to parse origin URL %#v", originURL)
544 }
545 h := &TunnelHandler{
546 originUrl: originURL,
547 httpClient: config.HTTPTransport,
548 tlsConfig: config.ClientTlsConfig,
549 tags: config.Tags,
550 metrics: config.Metrics,
551 connectionID: uint8ToString(connectionID),
552 logger: config.Logger,
553 noChunkedEncoding: config.NoChunkedEncoding,
554 }
555 if h.httpClient == nil {
556 h.httpClient = http.DefaultTransport
557 }
558 // Inherit from parent context so we can cancel (Ctrl-C) while dialing
559 dialCtx, dialCancel := context.WithTimeout(ctx, dialTimeout)
560 // TUN-92: enforce a timeout on dial and handshake (as tls.Dial does not support one)
561 plaintextEdgeConn, err := dialer.DialContext(dialCtx, "tcp", addr)
562 dialCancel()
563 if err != nil {
564 return nil, "", dialError{cause: errors.Wrap(err, "DialContext error")}
565 }
566 edgeConn := tls.Client(plaintextEdgeConn, config.TlsConfig)
567 edgeConn.SetDeadline(time.Now().Add(dialTimeout))
568 err = edgeConn.Handshake()
569 if err != nil {
570 return nil, "", dialError{cause: errors.Wrap(err, "Handshake with edge error")}
571 }
572 // clear the deadline on the conn; h2mux has its own timeouts
573 edgeConn.SetDeadline(time.Time{})
574 // Establish a muxed connection with the edge
575 // Client mux handshake with agent server
576 h.muxer, err = h2mux.Handshake(edgeConn, edgeConn, h2mux.MuxerConfig{
577 Timeout: 5 * time.Second,
578 Handler: h,
579 IsClient: true,
580 HeartbeatInterval: config.HeartbeatInterval,
581 MaxHeartbeats: config.MaxHeartbeats,
582 Logger: config.TransportLogger.WithFields(log.Fields{}),
583 CompressionQuality: h2mux.CompressionSetting(config.CompressionQuality),
584 })
585 if err != nil {
586 return h, "", errors.New("TLS handshake error")
587 }
588 return h, edgeConn.LocalAddr().String(), err
589}
590
591func (h *TunnelHandler) AppendTagHeaders(r *http.Request) {
592 for _, tag := range h.tags {
593 r.Header.Add(TagHeaderNamePrefix+tag.Name, tag.Value)
594 }
595}
596
597func (h *TunnelHandler) ServeStream(stream *h2mux.MuxedStream) error {
598 h.metrics.incrementRequests(h.connectionID)
599 req, err := http.NewRequest("GET", h.originUrl, h2mux.MuxedStreamReader{MuxedStream: stream})
600 if err != nil {
601 h.logger.WithError(err).Panic("Unexpected error from http.NewRequest")
602 }
603 err = H2RequestHeadersToH1Request(stream.Headers, req)
604 if err != nil {
605 h.logger.WithError(err).Error("invalid request received")
606 }
607 h.AppendTagHeaders(req)
608 cfRay := FindCfRayHeader(req)
609 lbProbe := isLBProbeRequest(req)
610 h.logRequest(req, cfRay, lbProbe)
611 if websocket.IsWebSocketUpgrade(req) {
612 conn, response, err := websocket.ClientConnect(req, h.tlsConfig)
613 if err != nil {
614 h.logError(stream, err)
615 } else {
616 stream.WriteHeaders(H1ResponseToH2Response(response))
617 defer conn.Close()
618 // Copy to/from stream to the undelying connection. Use the underlying
619 // connection because cloudflared doesn't operate on the message themselves
620 websocket.Stream(conn.UnderlyingConn(), stream)
621 h.metrics.incrementResponses(h.connectionID, "200")
622 h.logResponse(response, cfRay, lbProbe)
623 }
624 } else {
625 // Support for WSGI Servers by switching transfer encoding from chunked to gzip/deflate
626 if h.noChunkedEncoding {
627 req.TransferEncoding = []string{"gzip", "deflate"}
628 cLength, err := strconv.Atoi(req.Header.Get("Content-Length"))
629 if err == nil {
630 req.ContentLength = int64(cLength)
631 }
632 }
633
634 // Request origin to keep connection alive to improve performance
635 req.Header.Set("Connection", "keep-alive")
636
637 response, err := h.httpClient.RoundTrip(req)
638
639 if err != nil {
640 h.logError(stream, err)
641 } else {
642 defer response.Body.Close()
643 stream.WriteHeaders(H1ResponseToH2Response(response))
644 if h.isEventStream(response) {
645 h.writeEventStream(stream, response.Body)
646 } else {
647 // Use CopyBuffer, because Copy only allocates a 32KiB buffer, and cross-stream
648 // compression generates dictionary on first write
649 io.CopyBuffer(stream, response.Body, make([]byte, 512*1024))
650 }
651
652 h.metrics.incrementResponses(h.connectionID, "200")
653 h.logResponse(response, cfRay, lbProbe)
654 }
655 }
656 h.metrics.decrementConcurrentRequests(h.connectionID)
657 return nil
658}
659
660func (h *TunnelHandler) writeEventStream(stream *h2mux.MuxedStream, responseBody io.ReadCloser) {
661 reader := bufio.NewReader(responseBody)
662 for {
663 line, err := reader.ReadBytes('\n')
664 if err != nil {
665 break
666 }
667 stream.Write(line)
668 }
669}
670
671func (h *TunnelHandler) isEventStream(response *http.Response) bool {
672 if response.Header.Get("content-type") == "text/event-stream" {
673 h.logger.Debug("Detected Server-Side Events from Origin")
674 return true
675 }
676 return false
677}
678
679func (h *TunnelHandler) logError(stream *h2mux.MuxedStream, err error) {
680 h.logger.WithError(err).Error("HTTP request error")
681 stream.WriteHeaders([]h2mux.Header{{Name: ":status", Value: "502"}})
682 stream.Write([]byte("502 Bad Gateway"))
683 h.metrics.incrementResponses(h.connectionID, "502")
684}
685
686func (h *TunnelHandler) logRequest(req *http.Request, cfRay string, lbProbe bool) {
687 logger := log.NewEntry(h.logger)
688 if cfRay != "" {
689 logger = logger.WithField("CF-RAY", cfRay)
690 logger.Debugf("%s %s %s", req.Method, req.URL, req.Proto)
691 } else if lbProbe {
692 logger.Debugf("Load Balancer health check %s %s %s", req.Method, req.URL, req.Proto)
693 } else {
694 logger.Warnf("All requests should have a CF-RAY header. Please open a support ticket with Cloudflare. %s %s %s ", req.Method, req.URL, req.Proto)
695 }
696 logger.Debugf("Request Headers %+v", req.Header)
697
698 if contentLen := req.ContentLength; contentLen == -1 {
699 logger.Debugf("Request Content length unknown")
700 } else {
701 logger.Debugf("Request content length %d", contentLen)
702 }
703}
704
705func (h *TunnelHandler) logResponse(r *http.Response, cfRay string, lbProbe bool) {
706 logger := log.NewEntry(h.logger)
707 if cfRay != "" {
708 logger = logger.WithField("CF-RAY", cfRay)
709 logger.Debugf("%s", r.Status)
710 } else if lbProbe {
711 logger.Debugf("Response to Load Balancer health check %s", r.Status)
712 } else {
713 logger.Infof("%s", r.Status)
714 }
715 logger.Debugf("Response Headers %+v", r.Header)
716
717 if contentLen := r.ContentLength; contentLen == -1 {
718 logger.Debugf("Response content length unknown")
719 } else {
720 logger.Debugf("Response content length %d", contentLen)
721 }
722}
723
724func (h *TunnelHandler) UpdateMetrics(connectionID string) {
725 h.metrics.updateMuxerMetrics(connectionID, h.muxer.Metrics())
726}
727
728func uint8ToString(input uint8) string {
729 return strconv.FormatUint(uint64(input), 10)
730}
731
732func isLBProbeRequest(req *http.Request) bool {
733 return strings.HasPrefix(req.UserAgent(), lbProbeUserAgentPrefix)
734}
735
736// Print out the given lines in a nice ASCII box.
737func asciiBox(lines []string, padding int) (box []string) {
738 maxLen := maxLen(lines)
739 spacer := strings.Repeat(" ", padding)
740
741 border := "+" + strings.Repeat("-", maxLen+(padding*2)) + "+"
742
743 box = append(box, border)
744 for _, line := range lines {
745 box = append(box, "|"+spacer+line+strings.Repeat(" ", maxLen-len(line))+spacer+"|")
746 }
747 box = append(box, border)
748 return
749}
750
751func maxLen(lines []string) int {
752 max := 0
753 for _, line := range lines {
754 if len(line) > max {
755 max = len(line)
756 }
757 }
758 return max
759}
760
761func trialZoneMsg(url string) []string {
762 return []string{
763 "Your free tunnel has started! Visit it:",
764 " " + url,
765 }
766}
767
768func activeIncidentsMsg(incidents []Incident) string {
769 preamble := "There is an active Cloudflare incident that may be related:"
770 if len(incidents) > 1 {
771 preamble = "There are active Cloudflare incidents that may be related:"
772 }
773 incidentStrings := []string{}
774 for _, incident := range incidents {
775 incidentString := fmt.Sprintf("%s (%s)", incident.Name, incident.URL())
776 incidentStrings = append(incidentStrings, incidentString)
777 }
778 return preamble + " " + strings.Join(incidentStrings, "; ")
779
780}
781