github.com/pkg/sftp@v1.13.6/request-example.go (about) 1 package sftp 2 3 // This serves as an example of how to implement the request server handler as 4 // well as a dummy backend for testing. It implements an in-memory backend that 5 // works as a very simple filesystem with simple flat key-value lookup system. 6 7 import ( 8 "errors" 9 "io" 10 "os" 11 "path" 12 "sort" 13 "strings" 14 "sync" 15 "syscall" 16 "time" 17 ) 18 19 const maxSymlinkFollows = 5 20 21 var errTooManySymlinks = errors.New("too many symbolic links") 22 23 // InMemHandler returns a Hanlders object with the test handlers. 24 func InMemHandler() Handlers { 25 root := &root{ 26 rootFile: &memFile{name: "/", modtime: time.Now(), isdir: true}, 27 files: make(map[string]*memFile), 28 } 29 return Handlers{root, root, root, root} 30 } 31 32 // Example Handlers 33 func (fs *root) Fileread(r *Request) (io.ReaderAt, error) { 34 flags := r.Pflags() 35 if !flags.Read { 36 // sanity check 37 return nil, os.ErrInvalid 38 } 39 40 return fs.OpenFile(r) 41 } 42 43 func (fs *root) Filewrite(r *Request) (io.WriterAt, error) { 44 flags := r.Pflags() 45 if !flags.Write { 46 // sanity check 47 return nil, os.ErrInvalid 48 } 49 50 return fs.OpenFile(r) 51 } 52 53 func (fs *root) OpenFile(r *Request) (WriterAtReaderAt, error) { 54 if fs.mockErr != nil { 55 return nil, fs.mockErr 56 } 57 _ = r.WithContext(r.Context()) // initialize context for deadlock testing 58 59 fs.mu.Lock() 60 defer fs.mu.Unlock() 61 62 return fs.openfile(r.Filepath, r.Flags) 63 } 64 65 func (fs *root) putfile(pathname string, file *memFile) error { 66 pathname, err := fs.canonName(pathname) 67 if err != nil { 68 return err 69 } 70 71 if !strings.HasPrefix(pathname, "/") { 72 return os.ErrInvalid 73 } 74 75 if _, err := fs.lfetch(pathname); err != os.ErrNotExist { 76 return os.ErrExist 77 } 78 79 file.name = pathname 80 fs.files[pathname] = file 81 82 return nil 83 } 84 85 func (fs *root) openfile(pathname string, flags uint32) (*memFile, error) { 86 pflags := newFileOpenFlags(flags) 87 88 file, err := fs.fetch(pathname) 89 if err == os.ErrNotExist { 90 if !pflags.Creat { 91 return nil, os.ErrNotExist 92 } 93 94 var count int 95 // You can create files through dangling symlinks. 96 link, err := fs.lfetch(pathname) 97 for err == nil && link.symlink != "" { 98 if pflags.Excl { 99 // unless you also passed in O_EXCL 100 return nil, os.ErrInvalid 101 } 102 103 if count++; count > maxSymlinkFollows { 104 return nil, errTooManySymlinks 105 } 106 107 pathname = link.symlink 108 link, err = fs.lfetch(pathname) 109 } 110 111 file := &memFile{ 112 modtime: time.Now(), 113 } 114 115 if err := fs.putfile(pathname, file); err != nil { 116 return nil, err 117 } 118 119 return file, nil 120 } 121 122 if err != nil { 123 return nil, err 124 } 125 126 if pflags.Creat && pflags.Excl { 127 return nil, os.ErrExist 128 } 129 130 if file.IsDir() { 131 return nil, os.ErrInvalid 132 } 133 134 if pflags.Trunc { 135 if err := file.Truncate(0); err != nil { 136 return nil, err 137 } 138 } 139 140 return file, nil 141 } 142 143 func (fs *root) Filecmd(r *Request) error { 144 if fs.mockErr != nil { 145 return fs.mockErr 146 } 147 _ = r.WithContext(r.Context()) // initialize context for deadlock testing 148 149 fs.mu.Lock() 150 defer fs.mu.Unlock() 151 152 switch r.Method { 153 case "Setstat": 154 file, err := fs.openfile(r.Filepath, sshFxfWrite) 155 if err != nil { 156 return err 157 } 158 159 if r.AttrFlags().Size { 160 return file.Truncate(int64(r.Attributes().Size)) 161 } 162 163 return nil 164 165 case "Rename": 166 // SFTP-v2: "It is an error if there already exists a file with the name specified by newpath." 167 // This varies from the POSIX specification, which allows limited replacement of target files. 168 if fs.exists(r.Target) { 169 return os.ErrExist 170 } 171 172 return fs.rename(r.Filepath, r.Target) 173 174 case "Rmdir": 175 return fs.rmdir(r.Filepath) 176 177 case "Remove": 178 // IEEE 1003.1 remove explicitly can unlink files and remove empty directories. 179 // We use instead here the semantics of unlink, which is allowed to be restricted against directories. 180 return fs.unlink(r.Filepath) 181 182 case "Mkdir": 183 return fs.mkdir(r.Filepath) 184 185 case "Link": 186 return fs.link(r.Filepath, r.Target) 187 188 case "Symlink": 189 // NOTE: r.Filepath is the target, and r.Target is the linkpath. 190 return fs.symlink(r.Filepath, r.Target) 191 } 192 193 return errors.New("unsupported") 194 } 195 196 func (fs *root) rename(oldpath, newpath string) error { 197 file, err := fs.lfetch(oldpath) 198 if err != nil { 199 return err 200 } 201 202 newpath, err = fs.canonName(newpath) 203 if err != nil { 204 return err 205 } 206 207 if !strings.HasPrefix(newpath, "/") { 208 return os.ErrInvalid 209 } 210 211 target, err := fs.lfetch(newpath) 212 if err != os.ErrNotExist { 213 if target == file { 214 // IEEE 1003.1: if oldpath and newpath are the same directory entry, 215 // then return no error, and perform no further action. 216 return nil 217 } 218 219 switch { 220 case file.IsDir(): 221 // IEEE 1003.1: if oldpath is a directory, and newpath exists, 222 // then newpath must be a directory, and empty. 223 // It is to be removed prior to rename. 224 if err := fs.rmdir(newpath); err != nil { 225 return err 226 } 227 228 case target.IsDir(): 229 // IEEE 1003.1: if oldpath is not a directory, and newpath exists, 230 // then newpath may not be a directory. 231 return syscall.EISDIR 232 } 233 } 234 235 fs.files[newpath] = file 236 237 if file.IsDir() { 238 dirprefix := file.name + "/" 239 240 for name, file := range fs.files { 241 if strings.HasPrefix(name, dirprefix) { 242 newname := path.Join(newpath, strings.TrimPrefix(name, dirprefix)) 243 244 fs.files[newname] = file 245 file.name = newname 246 delete(fs.files, name) 247 } 248 } 249 } 250 251 file.name = newpath 252 delete(fs.files, oldpath) 253 254 return nil 255 } 256 257 func (fs *root) PosixRename(r *Request) error { 258 if fs.mockErr != nil { 259 return fs.mockErr 260 } 261 _ = r.WithContext(r.Context()) // initialize context for deadlock testing 262 263 fs.mu.Lock() 264 defer fs.mu.Unlock() 265 266 return fs.rename(r.Filepath, r.Target) 267 } 268 269 func (fs *root) StatVFS(r *Request) (*StatVFS, error) { 270 if fs.mockErr != nil { 271 return nil, fs.mockErr 272 } 273 274 return getStatVFSForPath(r.Filepath) 275 } 276 277 func (fs *root) mkdir(pathname string) error { 278 dir := &memFile{ 279 modtime: time.Now(), 280 isdir: true, 281 } 282 283 return fs.putfile(pathname, dir) 284 } 285 286 func (fs *root) rmdir(pathname string) error { 287 // IEEE 1003.1: If pathname is a symlink, then rmdir should fail with ENOTDIR. 288 dir, err := fs.lfetch(pathname) 289 if err != nil { 290 return err 291 } 292 293 if !dir.IsDir() { 294 return syscall.ENOTDIR 295 } 296 297 // use the dir‘s internal name not the pathname we passed in. 298 // the dir.name is always the canonical name of a directory. 299 pathname = dir.name 300 301 for name := range fs.files { 302 if path.Dir(name) == pathname { 303 return errors.New("directory not empty") 304 } 305 } 306 307 delete(fs.files, pathname) 308 309 return nil 310 } 311 312 func (fs *root) link(oldpath, newpath string) error { 313 file, err := fs.lfetch(oldpath) 314 if err != nil { 315 return err 316 } 317 318 if file.IsDir() { 319 return errors.New("hard link not allowed for directory") 320 } 321 322 return fs.putfile(newpath, file) 323 } 324 325 // symlink() creates a symbolic link named `linkpath` which contains the string `target`. 326 // NOTE! This would be called with `symlink(req.Filepath, req.Target)` due to different semantics. 327 func (fs *root) symlink(target, linkpath string) error { 328 link := &memFile{ 329 modtime: time.Now(), 330 symlink: target, 331 } 332 333 return fs.putfile(linkpath, link) 334 } 335 336 func (fs *root) unlink(pathname string) error { 337 // does not follow symlinks! 338 file, err := fs.lfetch(pathname) 339 if err != nil { 340 return err 341 } 342 343 if file.IsDir() { 344 // IEEE 1003.1: implementations may opt out of allowing the unlinking of directories. 345 // SFTP-v2: SSH_FXP_REMOVE may not remove directories. 346 return os.ErrInvalid 347 } 348 349 // DO NOT use the file’s internal name. 350 // because of hard-links files cannot have a single canonical name. 351 delete(fs.files, pathname) 352 353 return nil 354 } 355 356 type listerat []os.FileInfo 357 358 // Modeled after strings.Reader's ReadAt() implementation 359 func (f listerat) ListAt(ls []os.FileInfo, offset int64) (int, error) { 360 var n int 361 if offset >= int64(len(f)) { 362 return 0, io.EOF 363 } 364 n = copy(ls, f[offset:]) 365 if n < len(ls) { 366 return n, io.EOF 367 } 368 return n, nil 369 } 370 371 func (fs *root) Filelist(r *Request) (ListerAt, error) { 372 if fs.mockErr != nil { 373 return nil, fs.mockErr 374 } 375 _ = r.WithContext(r.Context()) // initialize context for deadlock testing 376 377 fs.mu.Lock() 378 defer fs.mu.Unlock() 379 380 switch r.Method { 381 case "List": 382 files, err := fs.readdir(r.Filepath) 383 if err != nil { 384 return nil, err 385 } 386 return listerat(files), nil 387 388 case "Stat": 389 file, err := fs.fetch(r.Filepath) 390 if err != nil { 391 return nil, err 392 } 393 return listerat{file}, nil 394 } 395 396 return nil, errors.New("unsupported") 397 } 398 399 func (fs *root) readdir(pathname string) ([]os.FileInfo, error) { 400 dir, err := fs.fetch(pathname) 401 if err != nil { 402 return nil, err 403 } 404 405 if !dir.IsDir() { 406 return nil, syscall.ENOTDIR 407 } 408 409 var files []os.FileInfo 410 411 for name, file := range fs.files { 412 if path.Dir(name) == dir.name { 413 files = append(files, file) 414 } 415 } 416 417 sort.Slice(files, func(i, j int) bool { return files[i].Name() < files[j].Name() }) 418 419 return files, nil 420 } 421 422 func (fs *root) Readlink(pathname string) (string, error) { 423 file, err := fs.lfetch(pathname) 424 if err != nil { 425 return "", err 426 } 427 428 if file.symlink == "" { 429 return "", os.ErrInvalid 430 } 431 432 return file.symlink, nil 433 } 434 435 // implements LstatFileLister interface 436 func (fs *root) Lstat(r *Request) (ListerAt, error) { 437 if fs.mockErr != nil { 438 return nil, fs.mockErr 439 } 440 _ = r.WithContext(r.Context()) // initialize context for deadlock testing 441 442 fs.mu.Lock() 443 defer fs.mu.Unlock() 444 445 file, err := fs.lfetch(r.Filepath) 446 if err != nil { 447 return nil, err 448 } 449 return listerat{file}, nil 450 } 451 452 // In memory file-system-y thing that the Hanlders live on 453 type root struct { 454 rootFile *memFile 455 mockErr error 456 457 mu sync.Mutex 458 files map[string]*memFile 459 } 460 461 // Set a mocked error that the next handler call will return. 462 // Set to nil to reset for no error. 463 func (fs *root) returnErr(err error) { 464 fs.mockErr = err 465 } 466 467 func (fs *root) lfetch(path string) (*memFile, error) { 468 if path == "/" { 469 return fs.rootFile, nil 470 } 471 472 file, ok := fs.files[path] 473 if file == nil { 474 if ok { 475 delete(fs.files, path) 476 } 477 478 return nil, os.ErrNotExist 479 } 480 481 return file, nil 482 } 483 484 // canonName returns the “canonical” name of a file, that is: 485 // if the directory of the pathname is a symlink, it follows that symlink to the valid directory name. 486 // this is relatively easy, since `dir.name` will be the only valid canonical path for a directory. 487 func (fs *root) canonName(pathname string) (string, error) { 488 dirname, filename := path.Dir(pathname), path.Base(pathname) 489 490 dir, err := fs.fetch(dirname) 491 if err != nil { 492 return "", err 493 } 494 495 if !dir.IsDir() { 496 return "", syscall.ENOTDIR 497 } 498 499 return path.Join(dir.name, filename), nil 500 } 501 502 func (fs *root) exists(path string) bool { 503 path, err := fs.canonName(path) 504 if err != nil { 505 return false 506 } 507 508 _, err = fs.lfetch(path) 509 510 return err != os.ErrNotExist 511 } 512 513 func (fs *root) fetch(pathname string) (*memFile, error) { 514 file, err := fs.lfetch(pathname) 515 if err != nil { 516 return nil, err 517 } 518 519 var count int 520 for file.symlink != "" { 521 if count++; count > maxSymlinkFollows { 522 return nil, errTooManySymlinks 523 } 524 525 linkTarget := file.symlink 526 if !path.IsAbs(linkTarget) { 527 linkTarget = path.Join(path.Dir(file.name), linkTarget) 528 } 529 530 file, err = fs.lfetch(linkTarget) 531 if err != nil { 532 return nil, err 533 } 534 } 535 536 return file, nil 537 } 538 539 // Implements os.FileInfo, io.ReaderAt and io.WriterAt interfaces. 540 // These are the 3 interfaces necessary for the Handlers. 541 // Implements the optional interface TransferError. 542 type memFile struct { 543 name string 544 modtime time.Time 545 symlink string 546 isdir bool 547 548 mu sync.RWMutex 549 content []byte 550 err error 551 } 552 553 // These are helper functions, they must be called while holding the memFile.mu mutex 554 func (f *memFile) size() int64 { return int64(len(f.content)) } 555 func (f *memFile) grow(n int64) { f.content = append(f.content, make([]byte, n)...) } 556 557 // Have memFile fulfill os.FileInfo interface 558 func (f *memFile) Name() string { return path.Base(f.name) } 559 func (f *memFile) Size() int64 { 560 f.mu.Lock() 561 defer f.mu.Unlock() 562 563 return f.size() 564 } 565 func (f *memFile) Mode() os.FileMode { 566 if f.isdir { 567 return os.FileMode(0755) | os.ModeDir 568 } 569 if f.symlink != "" { 570 return os.FileMode(0777) | os.ModeSymlink 571 } 572 return os.FileMode(0644) 573 } 574 func (f *memFile) ModTime() time.Time { return f.modtime } 575 func (f *memFile) IsDir() bool { return f.isdir } 576 func (f *memFile) Sys() interface{} { 577 return fakeFileInfoSys() 578 } 579 580 func (f *memFile) ReadAt(b []byte, off int64) (int, error) { 581 f.mu.Lock() 582 defer f.mu.Unlock() 583 584 if f.err != nil { 585 return 0, f.err 586 } 587 588 if off < 0 { 589 return 0, errors.New("memFile.ReadAt: negative offset") 590 } 591 592 if off >= f.size() { 593 return 0, io.EOF 594 } 595 596 n := copy(b, f.content[off:]) 597 if n < len(b) { 598 return n, io.EOF 599 } 600 601 return n, nil 602 } 603 604 func (f *memFile) WriteAt(b []byte, off int64) (int, error) { 605 // fmt.Println(string(p), off) 606 // mimic write delays, should be optional 607 time.Sleep(time.Microsecond * time.Duration(len(b))) 608 609 f.mu.Lock() 610 defer f.mu.Unlock() 611 612 if f.err != nil { 613 return 0, f.err 614 } 615 616 grow := int64(len(b)) + off - f.size() 617 if grow > 0 { 618 f.grow(grow) 619 } 620 621 return copy(f.content[off:], b), nil 622 } 623 624 func (f *memFile) Truncate(size int64) error { 625 f.mu.Lock() 626 defer f.mu.Unlock() 627 628 if f.err != nil { 629 return f.err 630 } 631 632 grow := size - f.size() 633 if grow <= 0 { 634 f.content = f.content[:size] 635 } else { 636 f.grow(grow) 637 } 638 639 return nil 640 } 641 642 func (f *memFile) TransferError(err error) { 643 f.mu.Lock() 644 defer f.mu.Unlock() 645 646 f.err = err 647 }