aboutsummaryrefslogtreecommitdiff
path: root/vendor/github.com/pkg/sftp/request-example.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/pkg/sftp/request-example.go')
-rw-r--r--vendor/github.com/pkg/sftp/request-example.go666
1 files changed, 666 insertions, 0 deletions
diff --git a/vendor/github.com/pkg/sftp/request-example.go b/vendor/github.com/pkg/sftp/request-example.go
new file mode 100644
index 000000000..ba22bcd0f
--- /dev/null
+++ b/vendor/github.com/pkg/sftp/request-example.go
@@ -0,0 +1,666 @@
+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
+}
+
+// implements RealpathFileLister interface
+func (fs *root) Realpath(p string) string {
+ if fs.startDirectory == "" || fs.startDirectory == "/" {
+ return cleanPath(p)
+ }
+ return cleanPathWithBase(fs.startDirectory, p)
+}
+
+// In memory file-system-y thing that the Hanlders live on
+type root struct {
+ rootFile *memFile
+ mockErr error
+ startDirectory string
+
+ 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
+}