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 }