/
githubmirror
/
client
Обзор
Документация
Войти
/
githubmirror
/
client
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
go/kbfs/libpages/server.go
619 строк
18 KB
songgao-zoom
kbpages: add pprof (#29490)
30 июл 2026, 20:02
Не верифицирован
30 июл 2026, 20:02
3c2f02d
Код
Авторство
О чём код?
// Copyright 2017 Keybase Inc. All rights reserved. // Use of this source code is governed by a BSD // license that can be found in the LICENSE file. package libpages import ( "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "fmt" "io" "log" "net/http" "os" "path" "reflect" "strings" "sync" "time" lru "github.com/hashicorp/golang-lru" "github.com/keybase/client/go/kbfs/libfs" "github.com/keybase/client/go/kbfs/libkbfs" "github.com/keybase/client/go/kbfs/libmime" "github.com/keybase/client/go/kbfs/libpages/config" "github.com/keybase/client/go/kbfs/tlf" "go.uber.org/zap" "golang.org/x/crypto/acme" "golang.org/x/crypto/acme/autocert" ) // CertStoreType is a type for specifying if and what cert store should be used // for acme/autocert. type CertStoreType string // Possible cert store types. const ( NoCertStore CertStoreType = "" DiskCertStore CertStoreType = "disk" KVStoreCertStore CertStoreType = "kvstore" ) // ServerConfig holds configuration parameters for Server. type ServerConfig struct { // If DomainWhitelist is non-nil and non-empty, only domains in the // whitelist are served and others are blocked. DomainWhitelist []string // If DomainBlacklist is non-nil and non-empty, domains in the blacklist // and all subdomains under them are blocked. When a domain is present in // both blacklist and whitelist, the domain is blocked. DomainBlacklist []string UseStaging bool Logger *zap.Logger CertStore CertStoreType StatsReporter StatsReporter domainListsOnce sync.Once domainWhitelist map[string]bool domainBlacklist []string } // ErrDomainBlockedInBlacklist is returned when the server is configured // with a domain blacklist, and we receive a HTTP request that was sent to a // domain that's in the blacklist. type ErrDomainBlockedInBlacklist struct{} // Error implements the error interface. func (ErrDomainBlockedInBlacklist) Error() string { return "we cannot serve this site" } // ErrDomainNotAllowedInWhitelist is returned when the server is configured // with a domain whitelist, and we receive a HTTP request that was sent to a // domain that's not in the whitelist. type ErrDomainNotAllowedInWhitelist struct{} // Error implements the error interface. func (ErrDomainNotAllowedInWhitelist) Error() string { return "a whitelist is configured and the given domain is not in the list" } func (c *ServerConfig) checkDomainLists(domain string) error { c.domainListsOnce.Do(func() { if len(c.DomainWhitelist) > 0 { c.domainWhitelist = make(map[string]bool, len(c.DomainWhitelist)) for _, d := range c.DomainWhitelist { c.domainWhitelist[strings.ToLower(strings.TrimSpace(d))] = true } } if len(c.DomainBlacklist) > 0 { c.domainBlacklist = make([]string, len(c.DomainBlacklist)) for i, d := range c.DomainBlacklist { c.domainBlacklist[i] = strings.ToLower(strings.TrimSpace(d)) } } }) for _, blocked := range c.domainBlacklist { if strings.HasSuffix(domain, blocked) { return ErrDomainBlockedInBlacklist{} } } if len(c.domainWhitelist) > 0 && !c.domainWhitelist[domain] { return ErrDomainNotAllowedInWhitelist{} } // No domainWhitelist; allow everything! return nil } const fsCacheSize = 2 << 15 // Server handles incoming HTTP requests by creating a Root for each host and // serving content from it. type Server struct { config *ServerConfig kbfsConfig libkbfs.Config rootLoader RootLoader siteCache *lru.Cache } func (s *Server) getSite(ctx context.Context, root Root) (st *site, err error) { siteCached, ok := s.siteCache.Get(root) if ok { if st, ok := siteCached.(*site); ok { if !st.fs.IsObsolete() { return st, nil } s.config.Logger.Info("fs end of life", zap.String("root", fmt.Sprintf("%#+v", root))) } s.config.Logger.Error("nasty entry in s.siteCache", zap.String("reflect_type", reflect.TypeOf(siteCached).String())) } fs, tlfID, fsShutdown, err := root.MakeFS(ctx, s.config.Logger, s.kbfsConfig) if err != nil { return nil, err } var added bool defer func() { // This is in case there's a panic before we get to add st into // s.siteCache. if !added { fsShutdown() } }() st = makeSite(fs, tlfID, fsShutdown, root) s.siteCache.Add(root, st) added = true return st, nil } func (s *Server) siteCacheEvict(_ any, value any) { if s, ok := value.(*site); ok { // It's possible to have a race here where a site gets evicted by the // LRU cache while the server is still using it to serve a request. But // since the cache is LRU, this should almost never happen given a // sufficiently large cache, and under the assumption that serving a // request won't take super long. s.shutdown() return } s.config.Logger.Error("nasty entry in s.siteCache", zap.String("reflect_type", reflect.TypeOf(value).String())) } func (s *Server) handleError(w http.ResponseWriter, err error) { // TODO: have a nicer error page for configuration errors? switch err.(type) { case nil: case ErrKeybasePagesRecordNotFound, ErrDomainNotAllowedInWhitelist, ErrDomainBlockedInBlacklist: http.Error(w, err.Error(), http.StatusServiceUnavailable) return case ErrKeybasePagesRecordTooMany, ErrInvalidKeybasePagesRecord: http.Error(w, err.Error(), http.StatusPreconditionFailed) return case config.ErrDuplicatePerPathConfigPath, config.ErrInvalidPermissions, config.ErrInvalidVersion, config.ErrUndefinedUsername: http.Error(w, "invalid .kbp_config", http.StatusPreconditionFailed) return default: // Don't write unknown errors in case we leak data unintentionally. http.Error(w, "", http.StatusInternalServerError) return } } // CtxKBPTagKey is the type used for unique context tags within kbp and // libpages. type CtxKBPTagKey int const ( // CtxKBPKey is the tag key for unique operation IDs within kbp and // libpages. CtxKBPKey CtxKBPTagKey = iota ) // CtxKBPOpID is the display name for unique operations in kbp and libpages. const CtxKBPOpID = "KBP" type adaptedLogger struct { msg string logger *zap.Logger } func (a adaptedLogger) Warning(format string, args ...any) { a.logger.Warn(a.msg, zap.String("desc", fmt.Sprintf(format, args...))) } func (s *Server) handleUnauthorized(w http.ResponseWriter, realm string, authorizationPossible bool, ) { if authorizationPossible { w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=%s", realm)) w.WriteHeader(http.StatusUnauthorized) } else { w.WriteHeader(http.StatusForbidden) } } func (s *Server) isDirWithNoIndexHTML( realFS *libfs.FS, requestPath string, ) (bool, error) { fi, err := realFS.Stat(strings.Trim(path.Clean(requestPath), "/")) switch { case os.IsNotExist(err): // It doesn't exist! So just let the http package handle it. return false, nil case err != nil: // Some other error happened. To be safe, error here. return false, err default: // continue } if !fi.IsDir() { return false, nil } _, err = realFS.Stat(path.Join(requestPath, "index.html")) switch { case err == nil: return false, nil case os.IsNotExist(err): return true, nil default: // Some other error happened. To be safe, error here. return false, err } } // ServedRequestInfo holds information regarding to an incoming request // that might be useful for stats. type ServedRequestInfo struct { // Host is the `Host` field of http.Request. Host string // Proto is the `Proto` field of http.Request. Proto string // Authenticated means the client set WWW-Authenticate in this request and // authentication using the given credentials has succeeded. It doesn't // necessarily indicate that the authentication is required for this // particular request. Authenticated bool // TlfID is the TLF ID associated with the site. TlfID tlf.ID // TlfType is the TLF type of the root that's used to serve the request. TlfType tlf.Type // RootType is the type of the root that's used to serve the request. RootType RootType // HTTPStatus is the HTTP status code that we have written for the request // in the response header. HTTPStatus int // CloningShown is set to true if a "CLONING" page instead of the real site // was served to the request. CloningShown bool // InvalidConfig is set to true if user has a config for the site being // requested, but it's invalid. InvalidConfig bool } type statusCodePeekingResponseWriter struct { w http.ResponseWriter code *int } var _ http.ResponseWriter = statusCodePeekingResponseWriter{} func (w statusCodePeekingResponseWriter) Header() http.Header { return w.w.Header() } func (w statusCodePeekingResponseWriter) WriteHeader(status int) { if *w.code == 0 { *w.code = status } w.w.WriteHeader(status) } func (w statusCodePeekingResponseWriter) Write(data []byte) (int, error) { if *w.code == 0 { *w.code = http.StatusOK } return w.w.Write(data) } func (s *ServedRequestInfo) wrapResponseWriter( w http.ResponseWriter, ) http.ResponseWriter { return statusCodePeekingResponseWriter{w: w, code: &s.HTTPStatus} } func (s *Server) logRequest(sri *ServedRequestInfo, requestPath string, startTime time.Time, err *error) { s.config.Logger.Info("ReqProcessed", zap.String("host", sri.Host), zap.String("path", requestPath), zap.String("proto", sri.Proto), zap.String("tlf_id", sri.TlfID.String()), zap.Int("http_status", sri.HTTPStatus), zap.Bool("authenticated", sri.Authenticated), zap.Bool("cloning_shown", sri.CloningShown), zap.Bool("invalid_config", sri.InvalidConfig), zap.Duration("duration", time.Since(startTime)), zap.NamedError("pre_FileServer_error", *err), ) } func (s *Server) setCommonResponseHeaders(w http.ResponseWriter) { // Enforce XSS protection. References: // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection // https://blog.innerht.ml/the-misunderstood-x-xss-protection/ w.Header().Set("X-XSS-Protection", "1; mode=block") // Only allow HTTPS on this domain, and make this policy expire in a // week. This means if user decides to migrate off Keybase Pages, there's a // 1-week gap before they can use HTTP again. Note that we don't use the // 'preload' directive, for the same reason we use 302 instead of 301 for // HTTP->HTTPS redirection. Reference: https://hstspreload.org/#opt-in w.Header().Set("Strict-Transport-Security", "max-age=604800") // TODO: allow user to opt-in some directives of Content-Security-Policy? } func (s *Server) setAccessControlAllowOriginHeader(w http.ResponseWriter, accessControlAllowOrigin string) { w.Header().Set("Access-Control-Allow-Origin", accessControlAllowOrigin) } // ServeHTTP implements the http.Handler interface. func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { startTime := time.Now() sri := &ServedRequestInfo{ Proto: r.Proto, Host: r.Host, } w = sri.wrapResponseWriter(w) if s.config.StatsReporter != nil { defer s.config.StatsReporter.ReportServedRequest(sri) } var err error defer s.logRequest(sri, r.URL.Path, startTime, &err) if err = s.config.checkDomainLists(r.Host); err != nil { s.handleError(w, err) return } s.setCommonResponseHeaders(w) // Don't serve the config file itself. if path.Clean(strings.ToLower(r.URL.Path)) == config.DefaultConfigFilepath { // TODO: integrate this check into Config? http.Error(w, fmt.Sprintf("Reading %s directly is forbidden.", config.DefaultConfigFilepath), http.StatusForbidden) return } // Construct a *site from DNS record. root, err := s.rootLoader.LoadRoot(r.Host) if err != nil { s.handleError(w, err) return } sri.TlfType, sri.RootType = root.TlfType, root.Type ctx := libfs.EnableFastMode( libkbfs.CtxWithRandomIDReplayable(r.Context(), CtxKBPKey, CtxKBPOpID, adaptedLogger{ msg: "CtxWithRandomIDReplayable", logger: s.config.Logger, }), ) st, err := s.getSite(ctx, root) if err != nil { s.handleError(w, err) return } sri.TlfID = st.tlfID realFS, err := st.fs.Use() if err != nil { s.handleError(w, err) return } // Get a site config, which can be either a user-defined one, or the // default one if it's missing from the site root. cfg, err := st.getConfig(false) if err != nil { // User has a .kbp_config file but it's invalid. // TODO: error page to show the error message? sri.InvalidConfig = true s.handleError(w, err) return } var username *string user, pass, ok := r.BasicAuth() if ok && cfg.Authenticate(r.Context(), user, pass) { sri.Authenticated = true username = &user } canRead, canList, possibleRead, possibleList, realm, err := cfg.GetPermissions(r.URL.Path, username) if err != nil { s.handleError(w, err) return } // Check if it's a directory containing no index.html before letting // http.FileServer handle it. This permission check should ideally // happen inside the http package, but unfortunately there isn't a // way today. isListing, err := s.isDirWithNoIndexHTML(realFS, r.URL.Path) if err != nil { s.handleError(w, err) return } if isListing && !canList { s.handleUnauthorized(w, realm, possibleList) return } if !isListing && !canRead { s.handleUnauthorized(w, realm, possibleRead) return } accessControlAllowOrigin, err := cfg.GetAccessControlAllowOrigin(r.URL.Path) if err != nil { s.handleError(w, err) return } if len(accessControlAllowOrigin) > 0 { s.setAccessControlAllowOriginHeader(w, accessControlAllowOrigin) } http.FileServer(realFS.ToHTTPFileSystem(ctx)).ServeHTTP(w, r) } // allowDomain is used to determine whether a given domain should be // served. It's also used as a HostPolicy in autocert package. func (s *Server) allowDomain(ctx context.Context, host string) (err error) { host = strings.ToLower(strings.TrimSpace(host)) if err = s.config.checkDomainLists(host); err != nil { return err } // DoS protection: look up kbp TXT record before attempting ACME cert // issuance, and only allow those that have DNS records configured. This is // in case someone keeps sending us TLS handshakes with random SNIs, // causing us to be rate-limited by the ACME server. // // TODO: cache the parsed root somewhere so we don't end up doing it twice // for each connection. if _, err = s.rootLoader.LoadRoot(host); err != nil { return err } return nil } const ( gracefulShutdownTimeout = 16 * time.Second httpReadHeaderTimeout = 8 * time.Second httpIdleTimeout = 1 * time.Minute stagingDiskCacheName = "./kbp-cert-cache-staging" prodDiskCacheName = "./kbp-cert-cache" ) func makeACMEManager(kbfsConfig libkbfs.Config, useStaging bool, certStoreType CertStoreType, hostPolicy autocert.HostPolicy) ( *autocert.Manager, error, ) { manager := &autocert.Manager{ Prompt: autocert.AcceptTOS, HostPolicy: hostPolicy, } switch certStoreType { case DiskCertStore: if useStaging { manager.Cache = autocert.DirCache(stagingDiskCacheName) } else { manager.Cache = autocert.DirCache(prodDiskCacheName) } case KVStoreCertStore: manager.Cache = newCertStoreBackedByKVStore(kbfsConfig) default: } if useStaging { acmeKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return nil, err } manager.Client = &acme.Client{ DirectoryURL: "https://acme-staging.api.letsencrypt.org/directory", Key: acmeKey, } } return manager, nil } var additionalMimeTypes = map[string]string{ ".wasm": "application/wasm", } type logForwarder struct { msg string logFunc func(msg string, fields ...zap.Field) } var _ io.Writer = (*logForwarder)(nil) func (f *logForwarder) Write(p []byte) (n int, err error) { f.logFunc(f.msg, zap.String("content", string(p))) return len(p), nil } // ListenAndServe listens on 443 and 80 ports of all addresses, and serve // Keybase Pages based on config and kbfsConfig. HTTPs setup is handled with // ACME. func ListenAndServe(ctx context.Context, config *ServerConfig, kbfsConfig libkbfs.Config, ) (err error) { ctx, cancel := context.WithCancel(ctx) defer cancel() libmime.Patch(additionalMimeTypes) server := &Server{ config: config, kbfsConfig: kbfsConfig, rootLoader: NewDNSRootLoader(config.Logger), } server.siteCache, err = lru.NewWithEvict(fsCacheSize, server.siteCacheEvict) if err != nil { return err } manager, err := makeACMEManager( kbfsConfig, config.UseStaging, config.CertStore, server.allowDomain) if err != nil { return err } if manager.Client == nil || len(manager.Client.DirectoryURL) == 0 { config.Logger.Info("ListenAndServe", zap.String("acme directory url", autocert.DefaultACMEDirectory)) } else { config.Logger.Info("ListenAndServe", zap.String("acme directory url", manager.Client.DirectoryURL)) } httpsServer := http.Server{ Handler: server, ReadHeaderTimeout: httpReadHeaderTimeout, IdleTimeout: httpIdleTimeout, ErrorLog: log.New(&logForwarder{ msg: "http error log", logFunc: config.Logger.Error, }, "", 0), } httpServer := http.Server{ Addr: ":80", // Enable http-01 by calling the HTTPHandler method, and set the // fallback HTTP handler to nil. As described in the autocert doc // (https://github.com/golang/crypto/blob/13931e22f9e72ea58bb73048bc752b48c6d4d4ac/acme/autocert/autocert.go#L248-L251), // this means for requests not for ACME domain verification, a default // fallback handler is used, which redirects all HTTP traffic using GET // and HEAD to HTTPS using 302 Found, and responds with 400 Bad Request // for requests with other methods. Handler: manager.HTTPHandler(nil), ReadHeaderTimeout: httpReadHeaderTimeout, IdleTimeout: httpIdleTimeout, } go func() { <-ctx.Done() shutdownCtx, cancel := context.WithTimeout( context.Background(), gracefulShutdownTimeout) defer cancel() _ = httpsServer.Shutdown(shutdownCtx) _ = httpServer.Shutdown(shutdownCtx) }() go func() { err := httpServer.ListenAndServe() if err != nil { config.Logger.Error("http.ListenAndServe:80", zap.Error(err)) } }() return httpsServer.Serve(manager.Listener()) }