| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647 |
- package sftp
- // This serves as an example of how to implement the request server handler as
- // well as a dummy backend for testing. It implements an in-memory backend that
- // works as a very simple filesystem with simple flat key-value lookup system.
- import (
- "errors"
- "io"
- "os"
- "path"
- "sort"
- "strings"
- "sync"
- "syscall"
- "time"
- )
- const maxSymlinkFollows = 5
- var errTooManySymlinks = errors.New("too many symbolic links")
- // InMemHandler returns a Hanlders object with the test handlers.
- func InMemHandler() Handlers {
- root := &root{
- rootFile: &memFile{name: "/", modtime: time.Now(), isdir: true},
- files: make(map[string]*memFile),
- }
- return Handlers{root, root, root, root}
- }
- // Example Handlers
- func (fs *root) Fileread(r *Request) (io.ReaderAt, error) {
- flags := r.Pflags()
- if !flags.Read {
- // sanity check
- return nil, os.ErrInvalid
- }
- return fs.OpenFile(r)
- }
- func (fs *root) Filewrite(r *Request) (io.WriterAt, error) {
- flags := r.Pflags()
- if !flags.Write {
- // sanity check
- return nil, os.ErrInvalid
- }
- return fs.OpenFile(r)
- }
- func (fs *root) OpenFile(r *Request) (WriterAtReaderAt, error) {
- if fs.mockErr != nil {
- return nil, fs.mockErr
- }
- _ = r.WithContext(r.Context()) // initialize context for deadlock testing
- fs.mu.Lock()
- defer fs.mu.Unlock()
- return fs.openfile(r.Filepath, r.Flags)
- }
- func (fs *root) putfile(pathname string, file *memFile) error {
- pathname, err := fs.canonName(pathname)
- if err != nil {
- return err
- }
- if !strings.HasPrefix(pathname, "/") {
- return os.ErrInvalid
- }
- if _, err := fs.lfetch(pathname); err != os.ErrNotExist {
- return os.ErrExist
- }
- file.name = pathname
- fs.files[pathname] = file
- return nil
- }
- func (fs *root) openfile(pathname string, flags uint32) (*memFile, error) {
- pflags := newFileOpenFlags(flags)
- file, err := fs.fetch(pathname)
- if err == os.ErrNotExist {
- if !pflags.Creat {
- return nil, os.ErrNotExist
- }
- var count int
- // You can create files through dangling symlinks.
- link, err := fs.lfetch(pathname)
- for err == nil && link.symlink != "" {
- if pflags.Excl {
- // unless you also passed in O_EXCL
- return nil, os.ErrInvalid
- }
- if count++; count > maxSymlinkFollows {
- return nil, errTooManySymlinks
- }
- pathname = link.symlink
- link, err = fs.lfetch(pathname)
- }
- file := &memFile{
- modtime: time.Now(),
- }
- if err := fs.putfile(pathname, file); err != nil {
- return nil, err
- }
- return file, nil
- }
- if err != nil {
- return nil, err
- }
- if pflags.Creat && pflags.Excl {
- return nil, os.ErrExist
- }
- if file.IsDir() {
- return nil, os.ErrInvalid
- }
- if pflags.Trunc {
- if err := file.Truncate(0); err != nil {
- return nil, err
- }
- }
- return file, nil
- }
- func (fs *root) Filecmd(r *Request) error {
- if fs.mockErr != nil {
- return fs.mockErr
- }
- _ = r.WithContext(r.Context()) // initialize context for deadlock testing
- fs.mu.Lock()
- defer fs.mu.Unlock()
- switch r.Method {
- case "Setstat":
- file, err := fs.openfile(r.Filepath, sshFxfWrite)
- if err != nil {
- return err
- }
- if r.AttrFlags().Size {
- return file.Truncate(int64(r.Attributes().Size))
- }
- return nil
- case "Rename":
- // SFTP-v2: "It is an error if there already exists a file with the name specified by newpath."
- // This varies from the POSIX specification, which allows limited replacement of target files.
- if fs.exists(r.Target) {
- return os.ErrExist
- }
- return fs.rename(r.Filepath, r.Target)
- case "Rmdir":
- return fs.rmdir(r.Filepath)
- case "Remove":
- // IEEE 1003.1 remove explicitly can unlink files and remove empty directories.
- // We use instead here the semantics of unlink, which is allowed to be restricted against directories.
- return fs.unlink(r.Filepath)
- case "Mkdir":
- return fs.mkdir(r.Filepath)
- case "Link":
- return fs.link(r.Filepath, r.Target)
- case "Symlink":
- // NOTE: r.Filepath is the target, and r.Target is the linkpath.
- return fs.symlink(r.Filepath, r.Target)
- }
- return errors.New("unsupported")
- }
- func (fs *root) rename(oldpath, newpath string) error {
- file, err := fs.lfetch(oldpath)
- if err != nil {
- return err
- }
- newpath, err = fs.canonName(newpath)
- if err != nil {
- return err
- }
- if !strings.HasPrefix(newpath, "/") {
- return os.ErrInvalid
- }
- target, err := fs.lfetch(newpath)
- if err != os.ErrNotExist {
- if target == file {
- // IEEE 1003.1: if oldpath and newpath are the same directory entry,
- // then return no error, and perform no further action.
- return nil
- }
- switch {
- case file.IsDir():
- // IEEE 1003.1: if oldpath is a directory, and newpath exists,
- // then newpath must be a directory, and empty.
- // It is to be removed prior to rename.
- if err := fs.rmdir(newpath); err != nil {
- return err
- }
- case target.IsDir():
- // IEEE 1003.1: if oldpath is not a directory, and newpath exists,
- // then newpath may not be a directory.
- return syscall.EISDIR
- }
- }
- fs.files[newpath] = file
- if file.IsDir() {
- dirprefix := file.name + "/"
- for name, file := range fs.files {
- if strings.HasPrefix(name, dirprefix) {
- newname := path.Join(newpath, strings.TrimPrefix(name, dirprefix))
- fs.files[newname] = file
- file.name = newname
- delete(fs.files, name)
- }
- }
- }
- file.name = newpath
- delete(fs.files, oldpath)
- return nil
- }
- func (fs *root) PosixRename(r *Request) error {
- if fs.mockErr != nil {
- return fs.mockErr
- }
- _ = r.WithContext(r.Context()) // initialize context for deadlock testing
- fs.mu.Lock()
- defer fs.mu.Unlock()
- return fs.rename(r.Filepath, r.Target)
- }
- func (fs *root) StatVFS(r *Request) (*StatVFS, error) {
- if fs.mockErr != nil {
- return nil, fs.mockErr
- }
- return getStatVFSForPath(r.Filepath)
- }
- func (fs *root) mkdir(pathname string) error {
- dir := &memFile{
- modtime: time.Now(),
- isdir: true,
- }
- return fs.putfile(pathname, dir)
- }
- func (fs *root) rmdir(pathname string) error {
- // IEEE 1003.1: If pathname is a symlink, then rmdir should fail with ENOTDIR.
- dir, err := fs.lfetch(pathname)
- if err != nil {
- return err
- }
- if !dir.IsDir() {
- return syscall.ENOTDIR
- }
- // use the dir‘s internal name not the pathname we passed in.
- // the dir.name is always the canonical name of a directory.
- pathname = dir.name
- for name := range fs.files {
- if path.Dir(name) == pathname {
- return errors.New("directory not empty")
- }
- }
- delete(fs.files, pathname)
- return nil
- }
- func (fs *root) link(oldpath, newpath string) error {
- file, err := fs.lfetch(oldpath)
- if err != nil {
- return err
- }
- if file.IsDir() {
- return errors.New("hard link not allowed for directory")
- }
- return fs.putfile(newpath, file)
- }
- // symlink() creates a symbolic link named `linkpath` which contains the string `target`.
- // NOTE! This would be called with `symlink(req.Filepath, req.Target)` due to different semantics.
- func (fs *root) symlink(target, linkpath string) error {
- link := &memFile{
- modtime: time.Now(),
- symlink: target,
- }
- return fs.putfile(linkpath, link)
- }
- func (fs *root) unlink(pathname string) error {
- // does not follow symlinks!
- file, err := fs.lfetch(pathname)
- if err != nil {
- return err
- }
- if file.IsDir() {
- // IEEE 1003.1: implementations may opt out of allowing the unlinking of directories.
- // SFTP-v2: SSH_FXP_REMOVE may not remove directories.
- return os.ErrInvalid
- }
- // DO NOT use the file’s internal name.
- // because of hard-links files cannot have a single canonical name.
- delete(fs.files, pathname)
- return nil
- }
- type listerat []os.FileInfo
- // Modeled after strings.Reader's ReadAt() implementation
- func (f listerat) ListAt(ls []os.FileInfo, offset int64) (int, error) {
- var n int
- if offset >= int64(len(f)) {
- return 0, io.EOF
- }
- n = copy(ls, f[offset:])
- if n < len(ls) {
- return n, io.EOF
- }
- return n, nil
- }
- func (fs *root) Filelist(r *Request) (ListerAt, error) {
- if fs.mockErr != nil {
- return nil, fs.mockErr
- }
- _ = r.WithContext(r.Context()) // initialize context for deadlock testing
- fs.mu.Lock()
- defer fs.mu.Unlock()
- switch r.Method {
- case "List":
- files, err := fs.readdir(r.Filepath)
- if err != nil {
- return nil, err
- }
- return listerat(files), nil
- case "Stat":
- file, err := fs.fetch(r.Filepath)
- if err != nil {
- return nil, err
- }
- return listerat{file}, nil
- }
- return nil, errors.New("unsupported")
- }
- func (fs *root) readdir(pathname string) ([]os.FileInfo, error) {
- dir, err := fs.fetch(pathname)
- if err != nil {
- return nil, err
- }
- if !dir.IsDir() {
- return nil, syscall.ENOTDIR
- }
- var files []os.FileInfo
- for name, file := range fs.files {
- if path.Dir(name) == dir.name {
- files = append(files, file)
- }
- }
- sort.Slice(files, func(i, j int) bool { return files[i].Name() < files[j].Name() })
- return files, nil
- }
- func (fs *root) Readlink(pathname string) (string, error) {
- file, err := fs.lfetch(pathname)
- if err != nil {
- return "", err
- }
- if file.symlink == "" {
- return "", os.ErrInvalid
- }
- return file.symlink, nil
- }
- // implements LstatFileLister interface
- func (fs *root) Lstat(r *Request) (ListerAt, error) {
- if fs.mockErr != nil {
- return nil, fs.mockErr
- }
- _ = r.WithContext(r.Context()) // initialize context for deadlock testing
- fs.mu.Lock()
- defer fs.mu.Unlock()
- file, err := fs.lfetch(r.Filepath)
- if err != nil {
- return nil, err
- }
- return listerat{file}, nil
- }
- // In memory file-system-y thing that the Hanlders live on
- type root struct {
- rootFile *memFile
- mockErr error
- mu sync.Mutex
- files map[string]*memFile
- }
- // Set a mocked error that the next handler call will return.
- // Set to nil to reset for no error.
- func (fs *root) returnErr(err error) {
- fs.mockErr = err
- }
- func (fs *root) lfetch(path string) (*memFile, error) {
- if path == "/" {
- return fs.rootFile, nil
- }
- file, ok := fs.files[path]
- if file == nil {
- if ok {
- delete(fs.files, path)
- }
- return nil, os.ErrNotExist
- }
- return file, nil
- }
- // canonName returns the “canonical” name of a file, that is:
- // if the directory of the pathname is a symlink, it follows that symlink to the valid directory name.
- // this is relatively easy, since `dir.name` will be the only valid canonical path for a directory.
- func (fs *root) canonName(pathname string) (string, error) {
- dirname, filename := path.Dir(pathname), path.Base(pathname)
- dir, err := fs.fetch(dirname)
- if err != nil {
- return "", err
- }
- if !dir.IsDir() {
- return "", syscall.ENOTDIR
- }
- return path.Join(dir.name, filename), nil
- }
- func (fs *root) exists(path string) bool {
- path, err := fs.canonName(path)
- if err != nil {
- return false
- }
- _, err = fs.lfetch(path)
- return err != os.ErrNotExist
- }
- func (fs *root) fetch(pathname string) (*memFile, error) {
- file, err := fs.lfetch(pathname)
- if err != nil {
- return nil, err
- }
- var count int
- for file.symlink != "" {
- if count++; count > maxSymlinkFollows {
- return nil, errTooManySymlinks
- }
- linkTarget := file.symlink
- if !path.IsAbs(linkTarget) {
- linkTarget = path.Join(path.Dir(file.name), linkTarget)
- }
- file, err = fs.lfetch(linkTarget)
- if err != nil {
- return nil, err
- }
- }
- return file, nil
- }
- // Implements os.FileInfo, io.ReaderAt and io.WriterAt interfaces.
- // These are the 3 interfaces necessary for the Handlers.
- // Implements the optional interface TransferError.
- type memFile struct {
- name string
- modtime time.Time
- symlink string
- isdir bool
- mu sync.RWMutex
- content []byte
- err error
- }
- // These are helper functions, they must be called while holding the memFile.mu mutex
- func (f *memFile) size() int64 { return int64(len(f.content)) }
- func (f *memFile) grow(n int64) { f.content = append(f.content, make([]byte, n)...) }
- // Have memFile fulfill os.FileInfo interface
- func (f *memFile) Name() string { return path.Base(f.name) }
- func (f *memFile) Size() int64 {
- f.mu.Lock()
- defer f.mu.Unlock()
- return f.size()
- }
- func (f *memFile) Mode() os.FileMode {
- if f.isdir {
- return os.FileMode(0755) | os.ModeDir
- }
- if f.symlink != "" {
- return os.FileMode(0777) | os.ModeSymlink
- }
- return os.FileMode(0644)
- }
- func (f *memFile) ModTime() time.Time { return f.modtime }
- func (f *memFile) IsDir() bool { return f.isdir }
- func (f *memFile) Sys() interface{} {
- return fakeFileInfoSys()
- }
- func (f *memFile) ReadAt(b []byte, off int64) (int, error) {
- f.mu.Lock()
- defer f.mu.Unlock()
- if f.err != nil {
- return 0, f.err
- }
- if off < 0 {
- return 0, errors.New("memFile.ReadAt: negative offset")
- }
- if off >= f.size() {
- return 0, io.EOF
- }
- n := copy(b, f.content[off:])
- if n < len(b) {
- return n, io.EOF
- }
- return n, nil
- }
- func (f *memFile) WriteAt(b []byte, off int64) (int, error) {
- // fmt.Println(string(p), off)
- // mimic write delays, should be optional
- time.Sleep(time.Microsecond * time.Duration(len(b)))
- f.mu.Lock()
- defer f.mu.Unlock()
- if f.err != nil {
- return 0, f.err
- }
- grow := int64(len(b)) + off - f.size()
- if grow > 0 {
- f.grow(grow)
- }
- return copy(f.content[off:], b), nil
- }
- func (f *memFile) Truncate(size int64) error {
- f.mu.Lock()
- defer f.mu.Unlock()
- if f.err != nil {
- return f.err
- }
- grow := size - f.size()
- if grow <= 0 {
- f.content = f.content[:size]
- } else {
- f.grow(grow)
- }
- return nil
- }
- func (f *memFile) TransferError(err error) {
- f.mu.Lock()
- defer f.mu.Unlock()
- f.err = err
- }
|