summaryrefslogtreecommitdiff
path: root/vendor/github.com/pkg/sftp/request-example.go
diff options
context:
space:
mode:
authorCharlie Doern <cdoern@redhat.com>2022-07-15 15:42:14 -0400
committerCharlie Doern <cdoern@redhat.com>2022-08-09 14:00:58 -0400
commit280f5d8cb01d115618d5ef131c718496a3b4900e (patch)
tree17e506cde2a18252da41096dbcc634ef485eff5e /vendor/github.com/pkg/sftp/request-example.go
parentc33dc90ace724f920c14e41769ce237f5c5d14ec (diff)
downloadpodman-280f5d8cb01d115618d5ef131c718496a3b4900e.tar.gz
podman-280f5d8cb01d115618d5ef131c718496a3b4900e.tar.bz2
podman-280f5d8cb01d115618d5ef131c718496a3b4900e.zip
podman ssh work, using new c/common interface
implement new ssh interface into podman this completely redesigns the entire functionality of podman image scp, podman system connection add, and podman --remote. All references to golang.org/x/crypto/ssh have been moved to common as have native ssh/scp execs and the new usage of the sftp package. this PR adds a global flag, --ssh to podman which has two valid inputs `golang` and `native` where golang is the default. Users should not notice any difference in their everyday workflows if they continue using the golang option. UNLESS they have been using an improperly verified ssh key, this will now fail. This is because podman was incorrectly using the ssh callback method to IGNORE the ssh known hosts file which is very insecure and golang tells you not yo use this in production. The native paths allows for immense flexibility, with a new containers.conf field `SSH_CONFIG` that specifies a specific ssh config file to be used in all operations. Else the users ~/.ssh/config file will be used. podman --remote currently only uses the golang path, given its deep interconnection with dialing multiple clients and urls. My goal after this PR is to go back and abstract the idea of podman --remote from golang's dialed clients, as it should not be so intrinsically connected. Overall, this is a v1 of a long process of offering native ssh, and one that covers some good ground with podman system connection add and podman image scp. Signed-off-by: Charlie Doern <cdoern@redhat.com>
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
+}