cloudflare/cloudflared

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
2018.12.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

origin/tunnel.go

698lines · modecode

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