package sftpserver import ( "errors" "fmt" "io" "os" "path" "path/filepath" "sort" "strings" "syscall" "time" "github.com/pkg/sftp" "imuslab.com/arozos/mod/filesystem" "imuslab.com/arozos/mod/filesystem/arozfs" ) //Root of the serving tree type root struct { username string rootFile *rootFolder startDirectory string fshs []*filesystem.FileSystemHandler } type rootFolder struct { name string modtime time.Time isdir bool content []byte } type sftpFileInterface interface { Name() string Size() int64 Mode() os.FileMode ModTime() time.Time IsDir() bool Sys() interface{} ReadAt([]byte, int64) (int, error) WriteAt([]byte, int64) (int, error) } //Wrapper for the arozfs File to provide missing functions type wrappedArozFile struct { file arozfs.File } func newArozFileWrapper(arozfile arozfs.File) *wrappedArozFile { return &wrappedArozFile{file: arozfile} } func (f *wrappedArozFile) Name() string { return f.file.Name() } func (f *wrappedArozFile) Size() int64 { stat, err := f.file.Stat() if err != nil { return 0 } return stat.Size() } func (f *wrappedArozFile) Mode() os.FileMode { stat, err := f.file.Stat() if err != nil { return 0 } return stat.Mode() } func (f *wrappedArozFile) ModTime() time.Time { stat, err := f.file.Stat() if err != nil { return time.Time{} } return stat.ModTime() } func (f *wrappedArozFile) IsDir() bool { stat, err := f.file.Stat() if err != nil { return false } return stat.IsDir() } func (f *wrappedArozFile) Sys() interface{} { return nil } func (f *wrappedArozFile) ReadAt(b []byte, off int64) (int, error) { return f.file.ReadAt(b, off) } func (f *wrappedArozFile) WriteAt(b []byte, off int64) (int, error) { return f.file.WriteAt(b, off) } func GetNewSFTPRoot(username string, accessibleFileSystemHandlers []*filesystem.FileSystemHandler) sftp.Handlers { root := &root{ username: username, rootFile: &rootFolder{name: "/", modtime: time.Now(), isdir: true}, startDirectory: "/", fshs: accessibleFileSystemHandlers, } return sftp.Handlers{root, root, root, root} } func (fs *root) getFshFromID(fshID string) *filesystem.FileSystemHandler { for _, thisFsh := range fs.fshs { if thisFsh.UUID == fshID && !thisFsh.Closed { return thisFsh } } return nil } // Example Handlers func (fs *root) Fileread(r *sftp.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 *sftp.Request) (io.WriterAt, error) { return nil, errors.New("wip") } func (fs *root) OpenFile(r *sftp.Request) (sftp.WriterAtReaderAt, error) { return nil, errors.New("wip") } func (fs *root) putfile(pathname string, file *arozfs.File) error { return nil } func (fs *root) openfile(pathname string, flags uint32) (*arozfs.File, error) { return nil, errors.New("wip") } func (fs *root) Filecmd(r *sftp.Request) error { switch r.Method { case "Setstat": 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 { return errors.New("wip") } func (fs *root) PosixRename(r *sftp.Request) error { return fs.rename(r.Filepath, r.Target) } func (fs *root) StatVFS(r *sftp.Request) (*sftp.StatVFS, error) { return nil, errors.New("wip") } func (fs *root) mkdir(pathname string) error { return errors.New("wip") } func (fs *root) rmdir(pathname string) error { return errors.New("wip") } func (fs *root) link(oldpath, newpath string) error { return errors.New("unsupported") } // 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 { return errors.New("unsupported") } func (fs *root) unlink(pathname string) error { /* 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 } */ return errors.New("wip") } 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 *sftp.Request) (sftp.ListerAt, error) { 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": return nil, errors.New("unsupported") } 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 } //Get the content of the dir using fsh infrastructure fmt.Println("READDIR", pathname, dir.Name()) targetFsh, _, rpath, err := fs.getFshAndSubpathFromSFTPPathname(pathname) if err != nil { return nil, err } entries, err := targetFsh.FileSystemAbstraction.ReadDir(rpath) if err != nil { return nil, err } files := []os.FileInfo{} for _, entry := range entries { i, err := entry.Info() if err != nil { continue } files = append(files, i) } 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) { return "", errors.New("unsupported") } // implements LstatFileLister interface func (fs *root) Lstat(r *sftp.Request) (sftp.ListerAt, error) { 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) } //Convert sftp raw path into fsh, subpath and realpath. return err if any func (fs *root) getFshAndSubpathFromSFTPPathname(pathname string) (*filesystem.FileSystemHandler, string, string, error) { pathname = strings.TrimSpace(pathname) if pathname[0:1] != "/" { pathname = "/" + pathname } pathChunks := strings.Split(pathname, "/") vrootID := pathChunks[1] subpath := "" if len(pathChunks) >= 2 { //Something like /user/Music subpath = strings.Join(pathChunks[2:], "/") } //Get target fsh fsh := fs.getFshFromID(vrootID) if fsh == nil { //Target fsh not found return nil, "", "", os.ErrExist } //Combined virtual path vpath := vrootID + ":/" + subpath //Translate it realpath and get from fsh fshAbs := fsh.FileSystemAbstraction rpath, err := fshAbs.VirtualPathToRealPath(vpath, fs.username) if err != nil { return nil, "", "", err } return fsh, subpath, rpath, nil } func (fs *root) lfetch(path string) (sftpFileInterface, error) { path = strings.TrimSpace(path) fmt.Println("lfetch", path) if path == "/" { fmt.Println("Requesting Root") return fs.rootFile, nil } //Fetching path other than root. Extract the vroot id from the path fsh, _, rpath, err := fs.getFshAndSubpathFromSFTPPathname(path) fshAbs := fsh.FileSystemAbstraction if !fshAbs.FileExists(rpath) { //Target file not exists return nil, os.ErrExist } //Open the file and return f, err := fshAbs.Open(rpath) if err != nil { return nil, err } f2 := newArozFileWrapper(f) return f2, nil } func (fs *root) exists(path string) bool { _, err := fs.lfetch(path) return err != nil } func (fs *root) fetch(path string) (sftpFileInterface, error) { file, err := fs.lfetch(path) if err != nil { return nil, err } return file, nil } // Have memFile fulfill os.FileInfo interface func (f *rootFolder) Name() string { return path.Base(f.name) } func (f *rootFolder) Size() int64 { return int64(len(f.content)) } func (f *rootFolder) Mode() os.FileMode { return os.FileMode(0755) | os.ModeDir } func (f *rootFolder) ModTime() time.Time { return f.modtime } func (f *rootFolder) IsDir() bool { return f.isdir } func (f *rootFolder) Sys() interface{} { return nil } func (f *rootFolder) ReadAt(b []byte, off int64) (int, error) { return 0, errors.New("root folder not support writeAt") } func (f *rootFolder) 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))) return 0, errors.New("root folder not support writeAt") } /* Utilities */ // Makes sure we have a clean POSIX (/) absolute path to work with func cleanPath(p string) string { return cleanPathWithBase("/", p) } func cleanPathWithBase(base, p string) string { p = filepath.ToSlash(filepath.Clean(p)) if !path.IsAbs(p) { return path.Join(base, p) } return p }