github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/libpages/server.go (about)

     1  // Copyright 2017 Keybase Inc. All rights reserved.
     2  // Use of this source code is governed by a BSD
     3  // license that can be found in the LICENSE file.
     4  
     5  package libpages
     6  
     7  import (
     8  	"context"
     9  	"crypto/ecdsa"
    10  	"crypto/elliptic"
    11  	"crypto/rand"
    12  	"fmt"
    13  	"io"
    14  	"log"
    15  	"net/http"
    16  	"os"
    17  	"path"
    18  	"reflect"
    19  	"strings"
    20  	"sync"
    21  	"time"
    22  
    23  	lru "github.com/hashicorp/golang-lru"
    24  	"github.com/keybase/client/go/kbfs/libfs"
    25  	"github.com/keybase/client/go/kbfs/libkbfs"
    26  	"github.com/keybase/client/go/kbfs/libmime"
    27  	"github.com/keybase/client/go/kbfs/libpages/config"
    28  	"github.com/keybase/client/go/kbfs/tlf"
    29  	"go.uber.org/zap"
    30  	"golang.org/x/crypto/acme"
    31  	"golang.org/x/crypto/acme/autocert"
    32  )
    33  
    34  // CertStoreType is a type for specifying if and what cert store should be used
    35  // for acme/autocert.
    36  type CertStoreType string
    37  
    38  // Possible cert store types.
    39  const (
    40  	NoCertStore      CertStoreType = ""
    41  	DiskCertStore    CertStoreType = "disk"
    42  	KVStoreCertStore CertStoreType = "kvstore"
    43  )
    44  
    45  // ServerConfig holds configuration parameters for Server.
    46  type ServerConfig struct {
    47  	// If DomainWhitelist is non-nil and non-empty, only domains in the
    48  	// whitelist are served and others are blocked.
    49  	DomainWhitelist []string
    50  	// If DomainBlacklist is non-nil and non-empty, domains in the blacklist
    51  	// and all subdomains under them are blocked. When a domain is present in
    52  	// both blacklist and whitelist, the domain is blocked.
    53  	DomainBlacklist []string
    54  	UseStaging      bool
    55  	Logger          *zap.Logger
    56  	CertStore       CertStoreType
    57  	StatsReporter   StatsReporter
    58  
    59  	domainListsOnce sync.Once
    60  	domainWhitelist map[string]bool
    61  	domainBlacklist []string
    62  }
    63  
    64  // ErrDomainBlockedInBlacklist is returned when the server is configured
    65  // with a domain blacklist, and we receive a HTTP request that was sent to a
    66  // domain that's in the blacklist.
    67  type ErrDomainBlockedInBlacklist struct{}
    68  
    69  // Error implements the error interface.
    70  func (ErrDomainBlockedInBlacklist) Error() string {
    71  	return "a blacklist is configured and the given domain is in the list"
    72  }
    73  
    74  // ErrDomainNotAllowedInWhitelist is returned when the server is configured
    75  // with a domain whitelist, and we receive a HTTP request that was sent to a
    76  // domain that's not in the whitelist.
    77  type ErrDomainNotAllowedInWhitelist struct{}
    78  
    79  // Error implements the error interface.
    80  func (ErrDomainNotAllowedInWhitelist) Error() string {
    81  	return "a whitelist is configured and the given domain is not in the list"
    82  }
    83  
    84  func (c *ServerConfig) checkDomainLists(domain string) error {
    85  	c.domainListsOnce.Do(func() {
    86  		if len(c.DomainWhitelist) > 0 {
    87  			c.domainWhitelist = make(map[string]bool, len(c.DomainWhitelist))
    88  			for _, d := range c.DomainWhitelist {
    89  				c.domainWhitelist[strings.ToLower(strings.TrimSpace(d))] = true
    90  			}
    91  		}
    92  		if len(c.DomainBlacklist) > 0 {
    93  			c.domainBlacklist = make([]string, len(c.DomainBlacklist))
    94  			for i, d := range c.DomainBlacklist {
    95  				c.domainBlacklist[i] = strings.ToLower(strings.TrimSpace(d))
    96  			}
    97  		}
    98  	})
    99  
   100  	for _, blocked := range c.domainBlacklist {
   101  		if strings.HasSuffix(domain, blocked) {
   102  			return ErrDomainBlockedInBlacklist{}
   103  		}
   104  	}
   105  	if len(c.domainWhitelist) > 0 && !c.domainWhitelist[domain] {
   106  		return ErrDomainNotAllowedInWhitelist{}
   107  	}
   108  
   109  	// No domainWhitelist; allow everything!
   110  	return nil
   111  }
   112  
   113  const fsCacheSize = 2 << 15
   114  
   115  // Server handles incoming HTTP requests by creating a Root for each host and
   116  // serving content from it.
   117  type Server struct {
   118  	config     *ServerConfig
   119  	kbfsConfig libkbfs.Config
   120  
   121  	rootLoader RootLoader
   122  	siteCache  *lru.Cache
   123  }
   124  
   125  func (s *Server) getSite(ctx context.Context, root Root) (st *site, err error) {
   126  	siteCached, ok := s.siteCache.Get(root)
   127  	if ok {
   128  		if st, ok := siteCached.(*site); ok {
   129  			if !st.fs.IsObsolete() {
   130  				return st, nil
   131  			}
   132  			s.config.Logger.Info("fs end of life",
   133  				zap.String("root", fmt.Sprintf("%#+v", root)))
   134  		}
   135  		s.config.Logger.Error("nasty entry in s.siteCache",
   136  			zap.String("reflect_type", reflect.TypeOf(siteCached).String()))
   137  	}
   138  	fs, tlfID, fsShutdown, err := root.MakeFS(ctx, s.config.Logger, s.kbfsConfig)
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  	var added bool
   143  	defer func() {
   144  		// This is in case there's a panic before we get to add st into
   145  		// s.siteCache.
   146  		if !added {
   147  			fsShutdown()
   148  		}
   149  	}()
   150  	st = makeSite(fs, tlfID, fsShutdown, root)
   151  	s.siteCache.Add(root, st)
   152  	added = true
   153  	return st, nil
   154  }
   155  
   156  func (s *Server) siteCacheEvict(_ interface{}, value interface{}) {
   157  	if s, ok := value.(*site); ok {
   158  		// It's possible to have a race here where a site gets evicted by the
   159  		// LRU cache while the server is still using it to serve a request. But
   160  		// since the cache is LRU, this should almost never happen given a
   161  		// sufficiently large cache, and under the assumption that serving a
   162  		// request won't take super long.
   163  		s.shutdown()
   164  		return
   165  	}
   166  	s.config.Logger.Error("nasty entry in s.siteCache",
   167  		zap.String("reflect_type", reflect.TypeOf(value).String()))
   168  }
   169  
   170  func (s *Server) handleError(w http.ResponseWriter, err error) {
   171  	// TODO: have a nicer error page for configuration errors?
   172  	switch err.(type) {
   173  	case nil:
   174  	case ErrKeybasePagesRecordNotFound,
   175  		ErrDomainNotAllowedInWhitelist, ErrDomainBlockedInBlacklist:
   176  		http.Error(w, err.Error(), http.StatusServiceUnavailable)
   177  		return
   178  	case ErrKeybasePagesRecordTooMany, ErrInvalidKeybasePagesRecord:
   179  		http.Error(w, err.Error(), http.StatusPreconditionFailed)
   180  		return
   181  	case config.ErrDuplicatePerPathConfigPath, config.ErrInvalidPermissions,
   182  		config.ErrInvalidVersion, config.ErrUndefinedUsername:
   183  		http.Error(w, "invalid .kbp_config", http.StatusPreconditionFailed)
   184  		return
   185  	default:
   186  		// Don't write unknown errors in case we leak data unintentionally.
   187  		http.Error(w, "", http.StatusInternalServerError)
   188  		return
   189  	}
   190  }
   191  
   192  // CtxKBPTagKey is the type used for unique context tags within kbp and
   193  // libpages.
   194  type CtxKBPTagKey int
   195  
   196  const (
   197  	// CtxKBPKey is the tag key for unique operation IDs within kbp and
   198  	// libpages.
   199  	CtxKBPKey CtxKBPTagKey = iota
   200  )
   201  
   202  // CtxKBPOpID is the display name for unique operations in kbp and libpages.
   203  const CtxKBPOpID = "KBP"
   204  
   205  type adaptedLogger struct {
   206  	msg    string
   207  	logger *zap.Logger
   208  }
   209  
   210  func (a adaptedLogger) Warning(format string, args ...interface{}) {
   211  	a.logger.Warn(a.msg, zap.String("desc", fmt.Sprintf(format, args...)))
   212  }
   213  
   214  func (s *Server) handleUnauthorized(w http.ResponseWriter,
   215  	r *http.Request, realm string, authorizationPossible bool) {
   216  	if authorizationPossible {
   217  		w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=%s", realm))
   218  		w.WriteHeader(http.StatusUnauthorized)
   219  	} else {
   220  		w.WriteHeader(http.StatusForbidden)
   221  	}
   222  }
   223  
   224  func (s *Server) isDirWithNoIndexHTML(
   225  	realFS *libfs.FS, requestPath string) (bool, error) {
   226  	fi, err := realFS.Stat(strings.Trim(path.Clean(requestPath), "/"))
   227  	switch {
   228  	case os.IsNotExist(err):
   229  		// It doesn't exist! So just let the http package handle it.
   230  		return false, nil
   231  	case err != nil:
   232  		// Some other error happened. To be safe, error here.
   233  		return false, err
   234  	default:
   235  		// continue
   236  	}
   237  
   238  	if !fi.IsDir() {
   239  		return false, nil
   240  	}
   241  
   242  	_, err = realFS.Stat(path.Join(requestPath, "index.html"))
   243  	switch {
   244  	case err == nil:
   245  		return false, nil
   246  	case os.IsNotExist(err):
   247  		return true, nil
   248  	default:
   249  		// Some other error happened. To be safe, error here.
   250  		return false, err
   251  	}
   252  }
   253  
   254  // ServedRequestInfo holds information regarding to an incoming request
   255  // that might be useful for stats.
   256  type ServedRequestInfo struct {
   257  	// Host is the `Host` field of http.Request.
   258  	Host string
   259  	// Proto is the `Proto` field of http.Request.
   260  	Proto string
   261  	// Authenticated means the client set WWW-Authenticate in this request and
   262  	// authentication using the given credentials has succeeded. It doesn't
   263  	// necessarily indicate that the authentication is required for this
   264  	// particular request.
   265  	Authenticated bool
   266  	// TlfID is the TLF ID associated with the site.
   267  	TlfID tlf.ID
   268  	// TlfType is the TLF type of the root that's used to serve the request.
   269  	TlfType tlf.Type
   270  	// RootType is the type of the root that's used to serve the request.
   271  	RootType RootType
   272  	// HTTPStatus is the HTTP status code that we have written for the request
   273  	// in the response header.
   274  	HTTPStatus int
   275  	// CloningShown is set to true if a "CLONING" page instead of the real site
   276  	// was served to the request.
   277  	CloningShown bool
   278  	// InvalidConfig is set to true if user has a config for the site being
   279  	// requested, but it's invalid.
   280  	InvalidConfig bool
   281  }
   282  
   283  type statusCodePeekingResponseWriter struct {
   284  	w    http.ResponseWriter
   285  	code *int
   286  }
   287  
   288  var _ http.ResponseWriter = statusCodePeekingResponseWriter{}
   289  
   290  func (w statusCodePeekingResponseWriter) Header() http.Header {
   291  	return w.w.Header()
   292  }
   293  
   294  func (w statusCodePeekingResponseWriter) WriteHeader(status int) {
   295  	if *w.code == 0 {
   296  		*w.code = status
   297  	}
   298  	w.w.WriteHeader(status)
   299  }
   300  
   301  func (w statusCodePeekingResponseWriter) Write(data []byte) (int, error) {
   302  	if *w.code == 0 {
   303  		*w.code = http.StatusOK
   304  	}
   305  	return w.w.Write(data)
   306  }
   307  
   308  func (s *ServedRequestInfo) wrapResponseWriter(
   309  	w http.ResponseWriter) http.ResponseWriter {
   310  	return statusCodePeekingResponseWriter{w: w, code: &s.HTTPStatus}
   311  }
   312  
   313  func (s *Server) logRequest(sri *ServedRequestInfo, requestPath string, startTime time.Time, err *error) {
   314  	s.config.Logger.Info("ReqProcessed",
   315  		zap.String("host", sri.Host),
   316  		zap.String("path", requestPath),
   317  		zap.String("proto", sri.Proto),
   318  		zap.String("tlf_id", sri.TlfID.String()),
   319  		zap.Int("http_status", sri.HTTPStatus),
   320  		zap.Bool("authenticated", sri.Authenticated),
   321  		zap.Bool("cloning_shown", sri.CloningShown),
   322  		zap.Bool("invalid_config", sri.InvalidConfig),
   323  		zap.Duration("duration", time.Since(startTime)),
   324  		zap.NamedError("pre_FileServer_error", *err),
   325  	)
   326  }
   327  
   328  func (s *Server) setCommonResponseHeaders(w http.ResponseWriter) {
   329  	// Enforce XSS protection. References:
   330  	// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection
   331  	// https://blog.innerht.ml/the-misunderstood-x-xss-protection/
   332  	w.Header().Set("X-XSS-Protection", "1; mode=block")
   333  	// Only allow HTTPS on this domain, and make this policy expire in a
   334  	// week. This means if user decides to migrate off Keybase Pages, there's a
   335  	// 1-week gap before they can use HTTP again. Note that we don't use the
   336  	// 'preload' directive, for the same reason we use 302 instead of 301 for
   337  	// HTTP->HTTPS redirection. Reference: https://hstspreload.org/#opt-in
   338  	w.Header().Set("Strict-Transport-Security", "max-age=604800")
   339  	// TODO: allow user to opt-in some directives of Content-Security-Policy?
   340  }
   341  
   342  func (s *Server) setAccessControlAllowOriginHeader(w http.ResponseWriter, accessControlAllowOrigin string) {
   343  	w.Header().Set("Access-Control-Allow-Origin", accessControlAllowOrigin)
   344  }
   345  
   346  // ServeHTTP implements the http.Handler interface.
   347  func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   348  	startTime := time.Now()
   349  	sri := &ServedRequestInfo{
   350  		Proto: r.Proto,
   351  		Host:  r.Host,
   352  	}
   353  	w = sri.wrapResponseWriter(w)
   354  	if s.config.StatsReporter != nil {
   355  		defer s.config.StatsReporter.ReportServedRequest(sri)
   356  	}
   357  
   358  	var err error
   359  	defer s.logRequest(sri, r.URL.Path, startTime, &err)
   360  
   361  	if err = s.config.checkDomainLists(r.Host); err != nil {
   362  		s.handleError(w, err)
   363  		return
   364  	}
   365  
   366  	s.setCommonResponseHeaders(w)
   367  
   368  	// Don't serve the config file itself.
   369  	if path.Clean(strings.ToLower(r.URL.Path)) == config.DefaultConfigFilepath {
   370  		// TODO: integrate this check into Config?
   371  		http.Error(w, fmt.Sprintf("Reading %s directly is forbidden.",
   372  			config.DefaultConfigFilepath), http.StatusForbidden)
   373  		return
   374  	}
   375  
   376  	// Construct a *site from DNS record.
   377  	root, err := s.rootLoader.LoadRoot(r.Host)
   378  	if err != nil {
   379  		s.handleError(w, err)
   380  		return
   381  	}
   382  	sri.TlfType, sri.RootType = root.TlfType, root.Type
   383  	ctx := libfs.EnableFastMode(
   384  		libkbfs.CtxWithRandomIDReplayable(r.Context(),
   385  			CtxKBPKey, CtxKBPOpID, adaptedLogger{
   386  				msg:    "CtxWithRandomIDReplayable",
   387  				logger: s.config.Logger,
   388  			}),
   389  	)
   390  	st, err := s.getSite(ctx, root)
   391  	if err != nil {
   392  		s.handleError(w, err)
   393  		return
   394  	}
   395  	sri.TlfID = st.tlfID
   396  
   397  	realFS, err := st.fs.Use()
   398  	if err != nil {
   399  		s.handleError(w, err)
   400  		return
   401  	}
   402  
   403  	// Get a site config, which can be either a user-defined one, or the
   404  	// default one if it's missing from the site root.
   405  	cfg, err := st.getConfig(false)
   406  	if err != nil {
   407  		// User has a .kbp_config file but it's invalid.
   408  		// TODO: error page to show the error message?
   409  		sri.InvalidConfig = true
   410  		s.handleError(w, err)
   411  		return
   412  	}
   413  
   414  	var username *string
   415  	user, pass, ok := r.BasicAuth()
   416  	if ok && cfg.Authenticate(r.Context(), user, pass) {
   417  		sri.Authenticated = true
   418  		username = &user
   419  	}
   420  	canRead, canList, possibleRead, possibleList,
   421  		realm, err := cfg.GetPermissions(r.URL.Path, username)
   422  	if err != nil {
   423  		s.handleError(w, err)
   424  		return
   425  	}
   426  
   427  	// Check if it's a directory containing no index.html before letting
   428  	// http.FileServer handle it.  This permission check should ideally
   429  	// happen inside the http package, but unfortunately there isn't a
   430  	// way today.
   431  	isListing, err := s.isDirWithNoIndexHTML(realFS, r.URL.Path)
   432  	if err != nil {
   433  		s.handleError(w, err)
   434  		return
   435  	}
   436  
   437  	if isListing && !canList {
   438  		s.handleUnauthorized(w, r, realm, possibleList)
   439  		return
   440  	}
   441  
   442  	if !isListing && !canRead {
   443  		s.handleUnauthorized(w, r, realm, possibleRead)
   444  		return
   445  	}
   446  
   447  	accessControlAllowOrigin, err := cfg.GetAccessControlAllowOrigin(r.URL.Path)
   448  	if err != nil {
   449  		s.handleError(w, err)
   450  		return
   451  	}
   452  	if len(accessControlAllowOrigin) > 0 {
   453  		s.setAccessControlAllowOriginHeader(w, accessControlAllowOrigin)
   454  	}
   455  
   456  	http.FileServer(realFS.ToHTTPFileSystem(ctx)).ServeHTTP(w, r)
   457  }
   458  
   459  // allowDomain is used to determine whether a given domain should be
   460  // served. It's also used as a HostPolicy in autocert package.
   461  func (s *Server) allowDomain(ctx context.Context, host string) (err error) {
   462  	host = strings.ToLower(strings.TrimSpace(host))
   463  	if err = s.config.checkDomainLists(host); err != nil {
   464  		return err
   465  	}
   466  
   467  	// DoS protection: look up kbp TXT record before attempting ACME cert
   468  	// issuance, and only allow those that have DNS records configured. This is
   469  	// in case someone keeps sending us TLS handshakes with random SNIs,
   470  	// causing us to be rate-limited by the ACME server.
   471  	//
   472  	// TODO: cache the parsed root somewhere so we don't end up doing it twice
   473  	// for each connection.
   474  	if _, err = s.rootLoader.LoadRoot(host); err != nil {
   475  		return err
   476  	}
   477  
   478  	return nil
   479  }
   480  
   481  const (
   482  	gracefulShutdownTimeout = 16 * time.Second
   483  	httpReadHeaderTimeout   = 8 * time.Second
   484  	httpIdleTimeout         = 1 * time.Minute
   485  	stagingDiskCacheName    = "./kbp-cert-cache-staging"
   486  	prodDiskCacheName       = "./kbp-cert-cache"
   487  )
   488  
   489  func makeACMEManager(kbfsConfig libkbfs.Config, useStaging bool,
   490  	certStoreType CertStoreType, hostPolicy autocert.HostPolicy) (
   491  	*autocert.Manager, error) {
   492  	manager := &autocert.Manager{
   493  		Prompt:     autocert.AcceptTOS,
   494  		HostPolicy: hostPolicy,
   495  	}
   496  
   497  	switch certStoreType {
   498  	case DiskCertStore:
   499  		if useStaging {
   500  			manager.Cache = autocert.DirCache(stagingDiskCacheName)
   501  		} else {
   502  			manager.Cache = autocert.DirCache(prodDiskCacheName)
   503  		}
   504  	case KVStoreCertStore:
   505  		manager.Cache = newCertStoreBackedByKVStore(kbfsConfig)
   506  	default:
   507  	}
   508  
   509  	if useStaging {
   510  		acmeKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   511  		if err != nil {
   512  			return nil, err
   513  		}
   514  		manager.Client = &acme.Client{
   515  			DirectoryURL: "https://acme-staging.api.letsencrypt.org/directory",
   516  			Key:          acmeKey,
   517  		}
   518  	}
   519  
   520  	return manager, nil
   521  }
   522  
   523  var additionalMimeTypes = map[string]string{
   524  	".wasm": "application/wasm",
   525  }
   526  
   527  type logForwarder struct {
   528  	msg     string
   529  	logFunc func(msg string, fields ...zap.Field)
   530  }
   531  
   532  var _ io.Writer = (*logForwarder)(nil)
   533  
   534  func (f *logForwarder) Write(p []byte) (n int, err error) {
   535  	f.logFunc(f.msg, zap.String("content", string(p)))
   536  	return len(p), nil
   537  }
   538  
   539  // ListenAndServe listens on 443 and 80 ports of all addresses, and serve
   540  // Keybase Pages based on config and kbfsConfig. HTTPs setup is handled with
   541  // ACME.
   542  func ListenAndServe(ctx context.Context,
   543  	config *ServerConfig, kbfsConfig libkbfs.Config) (err error) {
   544  	ctx, cancel := context.WithCancel(ctx)
   545  	defer cancel()
   546  
   547  	libmime.Patch(additionalMimeTypes)
   548  
   549  	server := &Server{
   550  		config:     config,
   551  		kbfsConfig: kbfsConfig,
   552  		rootLoader: NewDNSRootLoader(config.Logger),
   553  	}
   554  	server.siteCache, err = lru.NewWithEvict(fsCacheSize, server.siteCacheEvict)
   555  	if err != nil {
   556  		return err
   557  	}
   558  
   559  	manager, err := makeACMEManager(
   560  		kbfsConfig, config.UseStaging, config.CertStore, server.allowDomain)
   561  	if err != nil {
   562  		return err
   563  	}
   564  
   565  	if manager.Client == nil || len(manager.Client.DirectoryURL) == 0 {
   566  		config.Logger.Info("ListenAndServe",
   567  			zap.String("acme directory url", autocert.DefaultACMEDirectory))
   568  	} else {
   569  		config.Logger.Info("ListenAndServe",
   570  			zap.String("acme directory url", manager.Client.DirectoryURL))
   571  	}
   572  
   573  	httpsServer := http.Server{
   574  		Handler:           server,
   575  		ReadHeaderTimeout: httpReadHeaderTimeout,
   576  		IdleTimeout:       httpIdleTimeout,
   577  		ErrorLog: log.New(&logForwarder{
   578  			msg:     "http error log",
   579  			logFunc: config.Logger.Error,
   580  		}, "", 0),
   581  	}
   582  
   583  	httpServer := http.Server{
   584  		Addr: ":80",
   585  		// Enable http-01 by calling the HTTPHandler method, and set the
   586  		// fallback HTTP handler to nil. As described in the autocert doc
   587  		// (https://github.com/golang/crypto/blob/13931e22f9e72ea58bb73048bc752b48c6d4d4ac/acme/autocert/autocert.go#L248-L251),
   588  		// this means for requests not for ACME domain verification, a default
   589  		// fallback handler is used, which redirects all HTTP traffic using GET
   590  		// and HEAD to HTTPS using 302 Found, and responds with 400 Bad Request
   591  		// for requests with other methods.
   592  		Handler:           manager.HTTPHandler(nil),
   593  		ReadHeaderTimeout: httpReadHeaderTimeout,
   594  		IdleTimeout:       httpIdleTimeout,
   595  	}
   596  
   597  	go func() {
   598  		<-ctx.Done()
   599  		shutdownCtx, cancel := context.WithTimeout(
   600  			context.Background(), gracefulShutdownTimeout)
   601  		defer cancel()
   602  		_ = httpsServer.Shutdown(shutdownCtx)
   603  		_ = httpServer.Shutdown(shutdownCtx)
   604  	}()
   605  
   606  	go func() {
   607  		err := httpServer.ListenAndServe()
   608  		if err != nil {
   609  			config.Logger.Error("http.ListenAndServe:80", zap.Error(err))
   610  		}
   611  	}()
   612  
   613  	return httpsServer.Serve(manager.Listener())
   614  }