github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/libpages/site.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 "os" 9 "sync" 10 "time" 11 12 "github.com/keybase/client/go/kbfs/libpages/config" 13 "github.com/keybase/client/go/kbfs/tlf" 14 ) 15 16 const configCacheTime = 2 * time.Minute 17 18 type site struct { 19 // fs should never be changed once it's constructed. 20 fs CacheableFS 21 tlfID tlf.ID 22 fsShutdown func() 23 root Root 24 25 // TODO: replace this with a notification mechanism from the FBO. 26 cachedConfigLock sync.RWMutex 27 cachedConfig config.Config 28 cachedConfigExpiresAt time.Time 29 } 30 31 func makeSite(fs CacheableFS, tlfID tlf.ID, fsShutdown func(), root Root) *site { 32 return &site{ 33 fs: fs, 34 tlfID: tlfID, 35 fsShutdown: fsShutdown, 36 root: root, 37 } 38 } 39 40 func (s *site) shutdown() { 41 s.fsShutdown() 42 } 43 44 func (s *site) getCachedConfig() (cfg config.Config, expiresAt time.Time) { 45 s.cachedConfigLock.RLock() 46 defer s.cachedConfigLock.RUnlock() 47 return s.cachedConfig, s.cachedConfigExpiresAt 48 } 49 50 func (s *site) fetchConfigAndRefreshCache() (cfg config.Config, err error) { 51 // Take the lock early to block other reads, since otherwise they would 52 // also reach here, causing unnecessary multiple fetches. 53 s.cachedConfigLock.Lock() 54 defer s.cachedConfigLock.Unlock() 55 56 if s.cachedConfigExpiresAt.After(time.Now()) { 57 // Some other goroutine beat us! The cached config is up-to-date now so 58 // just return it. 59 return s.cachedConfig, nil 60 } 61 62 // Makes sure we don't serve a subdir of a site that's already configured 63 // with a .kbp_config, which can potentially limit permissions. This is to 64 // prevent an attack where an evil user can override subdir config of 65 // another site if they know the subdir name. It's pretty bad when a site 66 // has limited permissions configured (e.g. disallow listing, http auth) 67 // where the attacker can just configure a "site" using a different domain 68 // but pointed to a subdir, and causes the kbp bot serve content that the 69 // site owner doesn't intend to share. 70 // 71 // Example: 72 // 1. Alice configures a site alice.example.com 73 // (kbp=/keybase/private/alice,kbpbot) with /.kbp_config that disallows 74 // list at "/", i.e. no file listing site wide. 75 // 2. Eve knows Alice has a secret folder /secrets where she puts files 76 // with random names to share with other people through URL. Eve 77 // configures eve.example.com rooted at /secrets 78 // (kbp=/keybase/private/alice,kbpbot/secrets). Since /secrets doesn't 79 // have a restrictive .kbp_config, Eve can now list the content even 80 // though they don't have access to /keybase/private/alice,kbpbot. 81 if err = s.fs.EnsureNoSuchFileOutsideRoot(config.DefaultConfigFilename); err != nil { 82 return nil, err 83 } 84 85 realFS, err := s.fs.Use() 86 if err != nil { 87 return nil, err 88 } 89 90 f, err := realFS.Open(config.DefaultConfigFilepath) 91 switch { 92 case os.IsNotExist(err): 93 cfg = config.DefaultV1() 94 case err == nil: 95 cfg, err = config.ParseConfig(f) 96 if err != nil { 97 return nil, err 98 } 99 default: 100 return nil, err 101 } 102 103 s.cachedConfig = cfg 104 s.cachedConfigExpiresAt = time.Now().Add(configCacheTime) 105 106 return cfg, nil 107 } 108 109 func (s *site) getConfig(forceRefresh bool) (cfg config.Config, err error) { 110 cachedConfig, cacheExpiresAt := s.getCachedConfig() 111 if !forceRefresh && cacheExpiresAt.After(time.Now()) { 112 return cachedConfig, nil 113 } 114 return s.fetchConfigAndRefreshCache() 115 }