github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/libpages/root.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  	"fmt"
    10  	"os"
    11  	"path"
    12  	"strings"
    13  
    14  	"github.com/keybase/client/go/kbfs/data"
    15  	"github.com/keybase/client/go/kbfs/libcontext"
    16  	"github.com/keybase/client/go/kbfs/libfs"
    17  	"github.com/keybase/client/go/kbfs/libgit"
    18  	"github.com/keybase/client/go/kbfs/libkbfs"
    19  	"github.com/keybase/client/go/kbfs/tlf"
    20  	"github.com/keybase/client/go/protocol/keybase1"
    21  	"go.uber.org/zap"
    22  	"go.uber.org/zap/zapcore"
    23  )
    24  
    25  // ErrInvalidKeybasePagesRecord is returned when the kbp= DNS record for a
    26  // domain is invalid.
    27  type ErrInvalidKeybasePagesRecord struct{}
    28  
    29  // Error returns the error interface.
    30  func (ErrInvalidKeybasePagesRecord) Error() string {
    31  	return "invalid TXT record"
    32  }
    33  
    34  // RootType specifies the type of a root.
    35  type RootType int
    36  
    37  const (
    38  	_ RootType = iota
    39  	// KBFSRoot means the root is backed by a KBFS path.
    40  	KBFSRoot
    41  	// GitRoot means the root is backed by a git repo stored in KBFS.
    42  	GitRoot
    43  )
    44  
    45  // String implements the fmt.Stringer interface
    46  func (t RootType) String() string {
    47  	switch t {
    48  	case KBFSRoot:
    49  		return "kbfs"
    50  	case GitRoot:
    51  		return "git"
    52  	default:
    53  		return "unknown"
    54  	}
    55  }
    56  
    57  // Debug tag ID for an individual FS in keybase pages.
    58  const ctxOpID = "KBP"
    59  
    60  type ctxTagKey int
    61  
    62  const (
    63  	ctxIDKey ctxTagKey = iota
    64  )
    65  
    66  // Root defines the root of a static site hosted by Keybase Pages. It is
    67  // normally constructed from DNS records directly and is cheap to make.
    68  type Root struct {
    69  	Type            RootType
    70  	TlfType         tlf.Type
    71  	TlfNameUnparsed string
    72  	PathUnparsed    string
    73  }
    74  
    75  // CacheableFS is a wrapper around a *libfs.FS and a subdir. Use Use() to get a
    76  // *libfs.FS that roots at subdir. This essentially delays "cd"ing into subdir,
    77  // and is useful for caching a *libfs.FS object without the downside of caching
    78  // a libkbfs.Node that can be obsolete when it's renamed or removed.
    79  type CacheableFS struct {
    80  	obsoleteTrackingCh <-chan struct{}
    81  	tlfFS              *libfs.FS
    82  	subdir             string
    83  }
    84  
    85  // IsObsolete returns true if fs has reached the end of life, because of a
    86  // handle change for example. In this case user should not use this fs anymore,
    87  // but instead make a new one.
    88  func (fs CacheableFS) IsObsolete() bool {
    89  	select {
    90  	case <-fs.obsoleteTrackingCh:
    91  		return true
    92  	default:
    93  		return false
    94  	}
    95  }
    96  
    97  // EnsureNoSuchFileOutsideRoot walks from the sub dir that this FS is
    98  // configured to use back all the way to the TLF root, and try to find a file
    99  // named `name`. If the file is found, an error is returned.
   100  //
   101  // For example, if a subdir /dir1/dir2/dir3 is configured as root dir, calling
   102  // this function makes sure none of /a/b/{name}, /a/{name}, and /{name} exist.
   103  // Though /a/b/c/{name} can exist.
   104  func (fs CacheableFS) EnsureNoSuchFileOutsideRoot(name string) (err error) {
   105  	p := path.Clean(fs.subdir)
   106  	if !strings.HasPrefix(p, "/") {
   107  		p = "/" + p
   108  	}
   109  
   110  	for {
   111  		p, _ = path.Split(p)
   112  		if p == "/" {
   113  			return nil
   114  		}
   115  		p = strings.TrimSuffix(p, "/")
   116  		_, statErr := fs.tlfFS.Stat(path.Join(p, name))
   117  		switch statErr {
   118  		case os.ErrNotExist:
   119  		case nil:
   120  			return fmt.Errorf("%s exists in a parent dir", name)
   121  		default:
   122  			return statErr
   123  		}
   124  	}
   125  }
   126  
   127  // Use returns a *libfs.FS to use.
   128  func (fs CacheableFS) Use() (*libfs.FS, error) {
   129  	return fs.tlfFS.ChrootAsLibFS(fs.subdir)
   130  }
   131  
   132  // MakeFS makes a CacheableFS from *r, which can be adapted to a http.FileSystem
   133  // (through ToHTTPFileSystem) to be used by http package to serve through HTTP.
   134  // Caller must call Use() to get a usable FS.
   135  func (r *Root) MakeFS(
   136  	ctx context.Context, log *zap.Logger, kbfsConfig libkbfs.Config) (
   137  	fs CacheableFS, tlfID tlf.ID, shutdown func(), err error) {
   138  	fsCtx, cancel := context.WithCancel(context.Background())
   139  	defer func() {
   140  		zapFields := []zapcore.Field{
   141  			zap.String("root_type", r.Type.String()),
   142  			zap.String("tlf_type", r.TlfType.String()),
   143  			zap.String("tlf", r.TlfNameUnparsed),
   144  			zap.String("root_path", r.PathUnparsed),
   145  		}
   146  		if err == nil {
   147  			log.Info("root.MakeFS", zapFields...)
   148  		} else {
   149  			cancel()
   150  			log.Warn("root.MakeFS", append(zapFields, zap.Error(err))...)
   151  		}
   152  	}()
   153  	fsCtx, err = libcontext.NewContextWithCancellationDelayer(
   154  		libkbfs.CtxWithRandomIDReplayable(
   155  			fsCtx, ctxIDKey, ctxOpID, nil))
   156  	if err != nil {
   157  		return CacheableFS{}, tlf.ID{}, nil, err
   158  	}
   159  	fsCtx = libfs.EnableFastMode(fsCtx)
   160  	switch r.Type {
   161  	case KBFSRoot:
   162  		tlfHandle, err := libkbfs.GetHandleFromFolderNameAndType(
   163  			ctx, kbfsConfig.KBPKI(), kbfsConfig.MDOps(), kbfsConfig,
   164  			r.TlfNameUnparsed, r.TlfType)
   165  		if err != nil {
   166  			return CacheableFS{}, tlf.ID{}, nil, err
   167  		}
   168  		tlfFS, err := libfs.NewFS(
   169  			fsCtx, kbfsConfig, tlfHandle, data.MasterBranch, "", "",
   170  			keybase1.MDPriorityNormal)
   171  		if err != nil {
   172  			return CacheableFS{}, tlf.ID{}, nil, err
   173  		}
   174  		obsoleteCh, err := tlfFS.SubscribeToObsolete()
   175  		if err != nil {
   176  			return CacheableFS{}, tlf.ID{}, nil, err
   177  		}
   178  		cacheableFS := CacheableFS{
   179  			obsoleteTrackingCh: obsoleteCh,
   180  			tlfFS:              tlfFS,
   181  			subdir:             r.PathUnparsed,
   182  		}
   183  		if _, err = cacheableFS.Use(); err != nil {
   184  			return CacheableFS{}, tlf.ID{}, nil, err
   185  		}
   186  		return cacheableFS, tlfHandle.TlfID(), cancel, nil
   187  	case GitRoot:
   188  		tlfHandle, err := libkbfs.GetHandleFromFolderNameAndType(
   189  			ctx, kbfsConfig.KBPKI(), kbfsConfig.MDOps(), kbfsConfig,
   190  			r.TlfNameUnparsed, r.TlfType)
   191  		if err != nil {
   192  			return CacheableFS{}, tlf.ID{}, nil, err
   193  		}
   194  		autogitTLFFS, err := libfs.NewFS(
   195  			fsCtx, kbfsConfig, tlfHandle, data.MasterBranch,
   196  			libgit.AutogitRoot, "", keybase1.MDPriorityNormal)
   197  		if err != nil {
   198  			return CacheableFS{}, tlf.ID{}, nil, err
   199  		}
   200  		cacheableFS := CacheableFS{
   201  			tlfFS:  autogitTLFFS,
   202  			subdir: r.PathUnparsed,
   203  		}
   204  		if _, err = cacheableFS.Use(); err != nil {
   205  			return CacheableFS{}, tlf.ID{}, nil, err
   206  		}
   207  		return cacheableFS, tlfHandle.TlfID(), cancel, nil
   208  	default:
   209  		return CacheableFS{}, tlf.ID{}, nil, ErrInvalidKeybasePagesRecord{}
   210  	}
   211  }
   212  
   213  const gitPrefix = "git@keybase:"
   214  const kbfsPrefix = "/keybase/"
   215  const privatePrefix = "private/"
   216  const publicPrefix = "public/"
   217  const teamPrefix = "team/"
   218  
   219  func setRootTlfNameAndPath(root *Root, str string) {
   220  	parts := strings.SplitN(str, "/", 2)
   221  	root.TlfNameUnparsed = parts[0]
   222  	if len(parts) > 1 {
   223  		root.PathUnparsed = parts[1]
   224  	}
   225  }
   226  
   227  // str is everything after either gitPrefix or kbfsPrefix.
   228  func setRoot(root *Root, str string) error {
   229  	switch {
   230  	case strings.HasPrefix(str, privatePrefix):
   231  		root.TlfType = tlf.Private
   232  		setRootTlfNameAndPath(root, str[len(privatePrefix):])
   233  		return nil
   234  	case strings.HasPrefix(str, publicPrefix):
   235  		root.TlfType = tlf.Public
   236  		setRootTlfNameAndPath(root, str[len(publicPrefix):])
   237  		return nil
   238  	case strings.HasPrefix(str, teamPrefix):
   239  		root.TlfType = tlf.SingleTeam
   240  		setRootTlfNameAndPath(root, str[len(teamPrefix):])
   241  		return nil
   242  	default:
   243  		return ErrInvalidKeybasePagesRecord{}
   244  	}
   245  }
   246  
   247  // ParseRoot parses a kbp= TXT record from a domain into a Root object.
   248  func ParseRoot(str string) (*Root, error) {
   249  	str = strings.TrimSpace(str)
   250  	switch {
   251  	case strings.HasPrefix(str, gitPrefix):
   252  		root := &Root{Type: GitRoot}
   253  		if err := setRoot(root, str[len(gitPrefix):]); err != nil {
   254  			return nil, err
   255  		}
   256  		return root, nil
   257  	case strings.HasPrefix(str, kbfsPrefix):
   258  		root := &Root{Type: KBFSRoot}
   259  		if err := setRoot(root, str[len(kbfsPrefix):]); err != nil {
   260  			return nil, err
   261  		}
   262  		return root, nil
   263  
   264  	default:
   265  		return nil, ErrInvalidKeybasePagesRecord{}
   266  	}
   267  }