/
githubmirror
/
client
Обзор
Документация
Войти
/
githubmirror
/
client
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
go/kbfs/libhttpserver/server.go
308 строк
8 KB
zoom-ua
modernize error handling with errors.As/Is (#29350)
25 июн 2026, 17:41
Не верифицирован
25 июн 2026, 17:41
e09e254
Код
Авторство
О чём код?
// Copyright 2018 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 libhttpserver import ( "context" "crypto/rand" "encoding/base64" "errors" "io" "net/http" "path" "strings" "sync" "time" lru "github.com/hashicorp/golang-lru" "github.com/keybase/client/go/kbfs/data" "github.com/keybase/client/go/kbfs/env" "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/tlf" "github.com/keybase/client/go/kbhttp" "github.com/keybase/client/go/libkb" "github.com/keybase/client/go/logger" "github.com/keybase/client/go/protocol/keybase1" ) const fsCacheSize = 64 // Server is a local HTTP server for serving KBFS content over HTTP. type Server struct { config libkbfs.Config logger logger.Logger vlog *libkb.VDebugLog appStateUpdater env.AppStateUpdater cancel func() tokenLock sync.RWMutex token string tokenExpireTime time.Time fs *lru.Cache serverLock sync.RWMutex server *kbhttp.Srv } const ( tokenByteSize = 32 tokenValidTime = 10 * time.Minute ) // CurrentToken returns the currently valid token that a HTTP client can use to // load content from the server. func (s *Server) CurrentToken() (token string, err error) { s.tokenLock.RLock() if s.config.Clock().Now().Before(s.tokenExpireTime) { defer s.tokenLock.RUnlock() return s.token, nil } s.tokenLock.RUnlock() buf := make([]byte, tokenByteSize) if _, err = rand.Read(buf); err != nil { return "", err } token = base64.URLEncoding.EncodeToString(buf) s.tokenLock.Lock() defer s.tokenLock.Unlock() if s.config.Clock().Now().Before(s.tokenExpireTime) { return s.token, nil } s.token = token s.tokenExpireTime = s.config.Clock().Now().Add(tokenValidTime) return token, nil } func (s *Server) handleInvalidToken(w http.ResponseWriter) { w.WriteHeader(http.StatusForbidden) _, _ = io.WriteString(w, ` <html> <head> <title>KBFS HTTP Token Invalid</title> </head> <body> token invalid </body> </html> `) } func (s *Server) handleBadRequest(w http.ResponseWriter) { w.WriteHeader(http.StatusBadRequest) } func (s *Server) handleInternalServerError(w http.ResponseWriter) { w.WriteHeader(http.StatusInternalServerError) } type obsoleteTrackingFS struct { fs *libfs.FS ch <-chan struct{} } func (e obsoleteTrackingFS) isObsolete() bool { select { case <-e.ch: return true default: return false } } func (s *Server) getHTTPFileSystem(ctx context.Context, requestPath string) ( toStrip string, fs http.FileSystem, err error, ) { fields := strings.Split(requestPath, "/") if len(fields) < 2 { return "", libfs.NewRootFS(s.config).ToHTTPFileSystem(ctx), nil } tlfType, err := tlf.ParseTlfTypeFromPath(fields[0]) if err != nil { return "", nil, err } toStrip = path.Join(fields[0], fields[1]) if fsCached, ok := s.fs.Get(toStrip); ok { if fsCachedTyped, ok := fsCached.(obsoleteTrackingFS); ok { if !fsCachedTyped.isObsolete() { return toStrip, fsCachedTyped.fs.ToHTTPFileSystem(ctx), nil } } } tlfHandle, err := libkbfs.GetHandleFromFolderNameAndType(ctx, s.config.KBPKI(), s.config.MDOps(), s.config, fields[1], tlfType) if err != nil { return "", nil, err } tlfFS, err := libfs.NewFS(ctx, s.config, tlfHandle, data.MasterBranch, "", "", keybase1.MDPriorityNormal) if err != nil { return "", nil, err } fsLifeCh, err := tlfFS.SubscribeToObsolete() if err != nil { return "", nil, err } s.fs.Add(toStrip, obsoleteTrackingFS{fs: tlfFS, ch: fsLifeCh}) return toStrip, tlfFS.ToHTTPFileSystem(ctx), nil } // serve accepts "/<fs path>?token=<token>" // For example: // // /team/keybase/file.txt?token=1234567890abcdef1234567890abcdef func (s *Server) serve(w http.ResponseWriter, req *http.Request) { s.vlog.Log(libkb.VLog1, "Incoming request from %q: %s", req.UserAgent(), req.URL) addr, err := s.server.Addr() if err != nil { s.logger.Error("serve: failed to get HTTP server address: %s", err) s.handleInternalServerError(w) return } if req.Host != addr { s.logger.Warning("Host %s didn't match addr %s, failing request to protect against DNS rebinding", req.Host, addr) s.handleBadRequest(w) return } token := req.URL.Query().Get("token") currentToken, err := s.CurrentToken() if err != nil { s.logger.Error("serve: failed to get current token: %s", err) s.handleInternalServerError(w) return } if len(token) == 0 || token != currentToken { s.vlog.Log(libkb.VLog1, "Invalid token %q", token) s.handleInvalidToken(w) return } toStrip, fs, err := s.getHTTPFileSystem(req.Context(), req.URL.Path) if err != nil { s.logger.Warning("Bad request; error=%v", err) s.handleBadRequest(w) return } viewTypeInvariance := req.URL.Query().Get("viewTypeInvariance") if len(viewTypeInvariance) == 0 { s.logger.Warning("Bad request; missing viewTypeInvariance") s.handleBadRequest(w) return } wrappedW := newContentTypeOverridingResponseWriter(w, viewTypeInvariance) http.StripPrefix(toStrip, http.FileServer(fs)).ServeHTTP(wrappedW, req) } const ( portStart = 16723 portEnd = 60000 requestPathRoot = "/files/" ) func (s *Server) restart() (err error) { s.serverLock.Lock() defer s.serverLock.Unlock() if s.server != nil { s.server.Stop() err = s.server.Start() } if s.server == nil || // If pinned port is in use, just pick a new one like we never had a // server before. errors.Is(err, kbhttp.ErrPinnedPortInUse) { s.server = kbhttp.NewSrv(s.logger, kbhttp.NewRandomPortRangeListenerSource(portStart, portEnd)) err = s.server.Start() } if err != nil { return err } // Have to start this first to populate the ServeMux object. s.server.Handle(requestPathRoot, http.StripPrefix(requestPathRoot, http.HandlerFunc(s.serve))) return nil } func (s *Server) monitorAppState(ctx context.Context) { state := keybase1.MobileAppState_FOREGROUND for { select { case <-ctx.Done(): return case state = <-s.appStateUpdater.NextAppStateUpdate(&state): // Due to the way NextUpdate is designed, it's possible we miss an // update if processing the last update takes too long. So it's // possible to get consecutive FOREGROUND updates even if there are // other states in-between. Since libkb/appstate.go already // deduplicates, it'll never actually send consecutive identical // states to us. In addition, apart from FOREGROUND/BACKGROUND, // there are other possible states too, and potentially more in the // future. So, we just restart the server under FOREGROUND instead // of trying to listen on all state updates. if state != keybase1.MobileAppState_FOREGROUND { continue } if err := s.restart(); err != nil { s.logger.Error("(Re)starting server failed: %v", err) } } } } // New creates and starts a new server. func New(appStateUpdater env.AppStateUpdater, config libkbfs.Config) ( s *Server, err error, ) { logger := config.MakeLogger("HTTP") s = &Server{ appStateUpdater: appStateUpdater, config: config, logger: logger, vlog: config.MakeVLogger(logger), } if s.fs, err = lru.New(fsCacheSize); err != nil { return nil, err } if err = s.restart(); err != nil { return nil, err } ctx, cancel := context.WithCancel(context.Background()) go s.monitorAppState(ctx) s.cancel = cancel libmime.Patch(additionalMimeTypes) return s, nil } // Address returns the address that the server is listening on. func (s *Server) Address() (string, error) { s.serverLock.RLock() defer s.serverLock.RUnlock() return s.server.Addr() } // Shutdown shuts down the server. func (s *Server) Shutdown() { s.serverLock.Lock() defer s.serverLock.Unlock() s.server.Stop() s.cancel() }