| 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 |
| |
| case "Readlink": |
| symlink, err := fs.readlink(r.Filepath) |
| if err != nil { |
| return nil, err |
| } |
| |
| // SFTP-v2: The server will respond with a SSH_FXP_NAME packet containing only |
| // one name and a dummy attributes value. |
| return listerat{ |
| &memFile{ |
| name: symlink, |
| err: os.ErrNotExist, // prevent accidental use as a reader/writer. |
| }, |
| }, 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(path string) (*memFile, error) { |
| file, err := fs.lfetch(path) |
| if err != nil { |
| return nil, err |
| } |
| |
| var count int |
| for file.symlink != "" { |
| if count++; count > maxSymlinkFollows { |
| return nil, errTooManySymlinks |
| } |
| |
| file, err = fs.lfetch(file.symlink) |
| 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 |
| } |