github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/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  }