ソースを参照

Added wip sftp server implementation

Toby Chui 2 年 前
コミット
ba9d1f2837
4 ファイル変更824 行追加360 行削除
  1. 277 282
      mod/storage/ftp/aofs.go
  2. 95 72
      mod/storage/sftpserver/sftpserver.go
  3. 446 0
      mod/storage/sftpserver/vroot.go
  4. 6 6
      startup.go

+ 277 - 282
mod/storage/ftp/aofs.go

@@ -1,282 +1,277 @@
-package ftp
-
-//arozos virtual path translation handler
-//author: tobychui
-
-import (
-	"errors"
-	"io/ioutil"
-	"os"
-	"path/filepath"
-	"strings"
-	"time"
-
-	"github.com/spf13/afero"
-	"imuslab.com/arozos/mod/filesystem"
-	"imuslab.com/arozos/mod/user"
-)
-
-var (
-	aofsCanRead  = 1
-	aofsCanWrite = 2
-)
-
-type aofs struct {
-	userinfo  *user.User
-	tmpFolder string
-}
-
-func (a aofs) Create(name string) (afero.File, error) {
-	fsh, rewritePath, err := a.pathRewrite(name)
-	if err != nil {
-		return nil, err
-	}
-	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
-		return nil, errors.New("Permission denied")
-	}
-	return fsh.FileSystemAbstraction.Create(rewritePath)
-}
-
-func (a aofs) Chown(name string, uid, gid int) error {
-	fsh, rewritePath, err := a.pathRewrite(name)
-	if err != nil {
-		return err
-	}
-	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
-		return errors.New("Permission denied")
-	}
-	return fsh.FileSystemAbstraction.Chown(rewritePath, uid, gid)
-}
-
-func (a aofs) Mkdir(name string, perm os.FileMode) error {
-	fsh, rewritePath, err := a.pathRewrite(name)
-	if err != nil {
-		return err
-	}
-	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
-		return errors.New("Permission denied")
-	}
-	return fsh.FileSystemAbstraction.Mkdir(rewritePath, perm)
-}
-
-func (a aofs) MkdirAll(path string, perm os.FileMode) error {
-	fsh, rewritePath, err := a.pathRewrite(path)
-	if err != nil {
-		return err
-	}
-	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
-		return errors.New("Permission denied")
-	}
-	return fsh.FileSystemAbstraction.MkdirAll(rewritePath, perm)
-}
-
-func (a aofs) Open(name string) (afero.File, error) {
-	//fmt.Println("FTP OPEN")
-	fsh, rewritePath, err := a.pathRewrite(name)
-	if err != nil {
-		return nil, err
-	}
-	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
-		return nil, errors.New("Permission denied")
-	}
-	return fsh.FileSystemAbstraction.Open(rewritePath)
-}
-
-func (a aofs) Stat(name string) (os.FileInfo, error) {
-	//fmt.Println("FTP STAT")
-	fsh, rewritePath, err := a.pathRewrite(name)
-	if err != nil {
-		return nil, err
-	}
-	if !a.checkAllowAccess(fsh, rewritePath, aofsCanRead) {
-		return nil, errors.New("Permission denied")
-	}
-	return fsh.FileSystemAbstraction.Stat(rewritePath)
-}
-
-func (a aofs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) {
-	//fmt.Println("FTP OPEN FILE")
-	fsh, rewritePath, err := a.pathRewrite(name)
-	if err != nil {
-		return nil, err
-	}
-	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
-		return nil, errors.New("Permission denied")
-	}
-	return fsh.FileSystemAbstraction.OpenFile(rewritePath, flag, perm)
-}
-
-func (a aofs) AllocateSpace(size int) error {
-	if a.userinfo.StorageQuota.HaveSpace(int64(size)) {
-		return nil
-	}
-	return errors.New("Storage Quota Fulled")
-}
-
-func (a aofs) Remove(name string) error {
-	fsh, rewritePath, err := a.pathRewrite(name)
-	if err != nil {
-		return err
-	}
-	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
-		return errors.New("Permission denied")
-	}
-
-	return fsh.FileSystemAbstraction.Remove(rewritePath)
-}
-
-func (a aofs) RemoveAll(path string) error {
-	fsh, rewritePath, err := a.pathRewrite(path)
-	if err != nil {
-		return err
-	}
-	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
-		return errors.New("Permission denied")
-	}
-	return fsh.FileSystemAbstraction.RemoveAll(rewritePath)
-}
-
-func (a aofs) Rename(oldname, newname string) error {
-	fshsrc, rewritePathsrc, err := a.pathRewrite(oldname)
-	if err != nil {
-		return err
-	}
-
-	fshdest, rewritePathdest, err := a.pathRewrite(newname)
-	if err != nil {
-		return err
-	}
-	if !a.checkAllowAccess(fshsrc, rewritePathsrc, aofsCanWrite) {
-		return errors.New("Permission denied")
-	}
-	if !a.checkAllowAccess(fshdest, rewritePathdest, aofsCanWrite) {
-		return errors.New("Permission denied")
-	}
-
-	if !fshdest.FileSystemAbstraction.FileExists(filepath.Dir(rewritePathdest)) {
-		fshdest.FileSystemAbstraction.MkdirAll(filepath.Dir(rewritePathdest), 0775)
-	}
-
-	if fshsrc.UUID == fshdest.UUID {
-		//Renaming in same fsh
-		return fshsrc.FileSystemAbstraction.Rename(rewritePathsrc, rewritePathdest)
-	} else {
-		//Cross fsh read write.
-		f, err := fshsrc.FileSystemAbstraction.ReadStream(rewritePathsrc)
-		if err != nil {
-			return err
-		}
-		defer f.Close()
-
-		err = fshdest.FileSystemAbstraction.WriteStream(rewritePathdest, f, 0775)
-		if err != nil {
-			return err
-		}
-
-		err = fshsrc.FileSystemAbstraction.RemoveAll(rewritePathsrc)
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func (a aofs) Name() string {
-	return "arozos virtualFS"
-}
-
-func (a aofs) Chmod(name string, mode os.FileMode) error {
-	fsh, rewritePath, err := a.pathRewrite(name)
-	if err != nil {
-		return err
-	}
-	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
-		return errors.New("Permission denied")
-	}
-	return fsh.FileSystemAbstraction.Chmod(rewritePath, mode)
-}
-
-func (a aofs) Chtimes(name string, atime time.Time, mtime time.Time) error {
-	fsh, rewritePath, err := a.pathRewrite(name)
-	if err != nil {
-		return err
-	}
-	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
-		return errors.New("Permission denied")
-	}
-	return fsh.FileSystemAbstraction.Chtimes(rewritePath, atime, mtime)
-}
-
-//arozos adaptive functions
-//This function rewrite the path from ftp representation to real filepath on disk
-func (a aofs) pathRewrite(path string) (*filesystem.FileSystemHandler, string, error) {
-	path = filepath.ToSlash(filepath.Clean(path))
-	//log.Println("Original path: ", path)
-	if path == "/" {
-		//Roots. Show ftpbuf root
-		fshs := a.userinfo.GetAllFileSystemHandler()
-		for _, fsh := range fshs {
-			//Create a folder representation for this virtual directory
-			if !fsh.RequireBuffer {
-				fsh.FileSystemAbstraction.Mkdir(filepath.Join(a.tmpFolder, fsh.UUID), 0755)
-			}
-		}
-
-		readmeContent, err := ioutil.ReadFile("./system/ftp/README.txt")
-		if err != nil {
-			readmeContent = []byte("DO NOT UPLOAD FILES INTO THE ROOT DIRECTORY")
-		}
-		ioutil.WriteFile(filepath.Join(a.tmpFolder, "README.txt"), readmeContent, 0755)
-
-		//Return the tmpFolder root
-		tmpfs, _ := a.userinfo.GetFileSystemHandlerFromVirtualPath("tmp:/")
-		return tmpfs, a.tmpFolder, nil
-	} else if path == "/README.txt" {
-		tmpfs, _ := a.userinfo.GetFileSystemHandlerFromVirtualPath("tmp:/")
-		return tmpfs, a.tmpFolder + "README.txt", nil
-	} else if len(path) > 0 {
-		//Rewrite the path for any alternative filepath
-		//Get the uuid of the filepath
-		path := path[1:]
-		subpaths := strings.Split(path, "/")
-		fsHandlerUUID := subpaths[0]
-		remainingPaths := subpaths[1:]
-
-		fsh, err := a.userinfo.GetFileSystemHandlerFromVirtualPath(fsHandlerUUID + ":")
-		if err != nil {
-			return nil, "", errors.New("File System Abstraction not found")
-		}
-
-		/*
-			if fsh.RequireBuffer {
-				//Not supported
-				return nil, "", errors.New("Buffered file system not supported by FTP driver")
-			}
-		*/
-
-		rpath, err := fsh.FileSystemAbstraction.VirtualPathToRealPath(fsh.UUID+":/"+strings.Join(remainingPaths, "/"), a.userinfo.Username)
-		if err != nil {
-			return nil, "", errors.New("File System Handler Hierarchy not supported by FTP driver")
-		}
-		return fsh, rpath, nil
-	} else {
-		//fsh not found.
-		return nil, "", errors.New("Invalid path")
-	}
-}
-
-//Check if user has access to the given path, mode can be string {read / write}
-func (a aofs) checkAllowAccess(fsh *filesystem.FileSystemHandler, path string, mode int) bool {
-	vpath, err := fsh.FileSystemAbstraction.RealPathToVirtualPath(path, a.userinfo.Username)
-	if err != nil {
-		return false
-	}
-
-	if mode == aofsCanRead {
-		return a.userinfo.CanRead(vpath)
-	} else if mode == aofsCanWrite {
-		return a.userinfo.CanWrite(vpath)
-	} else {
-		return false
-	}
-}
+package ftp
+
+//arozos virtual path translation handler
+//author: tobychui
+
+import (
+	"errors"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"github.com/spf13/afero"
+	"imuslab.com/arozos/mod/filesystem"
+	"imuslab.com/arozos/mod/user"
+)
+
+var (
+	aofsCanRead  = 1
+	aofsCanWrite = 2
+)
+
+type aofs struct {
+	userinfo  *user.User
+	tmpFolder string
+}
+
+func (a aofs) Create(name string) (afero.File, error) {
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return nil, err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return nil, errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.Create(rewritePath)
+}
+
+func (a aofs) Chown(name string, uid, gid int) error {
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.Chown(rewritePath, uid, gid)
+}
+
+func (a aofs) Mkdir(name string, perm os.FileMode) error {
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.Mkdir(rewritePath, perm)
+}
+
+func (a aofs) MkdirAll(path string, perm os.FileMode) error {
+	fsh, rewritePath, err := a.pathRewrite(path)
+	if err != nil {
+		return err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.MkdirAll(rewritePath, perm)
+}
+
+func (a aofs) Open(name string) (afero.File, error) {
+	//fmt.Println("FTP OPEN")
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return nil, err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return nil, errors.New("Permission denied")
+	}
+
+	return fsh.FileSystemAbstraction.Open(rewritePath)
+}
+
+func (a aofs) Stat(name string) (os.FileInfo, error) {
+	//fmt.Println("FTP STAT")
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return nil, err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanRead) {
+		return nil, errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.Stat(rewritePath)
+}
+
+func (a aofs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) {
+	//fmt.Println("FTP OPEN FILE")
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return nil, err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return nil, errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.OpenFile(rewritePath, flag, perm)
+}
+
+func (a aofs) AllocateSpace(size int) error {
+	if a.userinfo.StorageQuota.HaveSpace(int64(size)) {
+		return nil
+	}
+	return errors.New("Storage Quota Fulled")
+}
+
+func (a aofs) Remove(name string) error {
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
+
+	return fsh.FileSystemAbstraction.Remove(rewritePath)
+}
+
+func (a aofs) RemoveAll(path string) error {
+	fsh, rewritePath, err := a.pathRewrite(path)
+	if err != nil {
+		return err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.RemoveAll(rewritePath)
+}
+
+func (a aofs) Rename(oldname, newname string) error {
+	fshsrc, rewritePathsrc, err := a.pathRewrite(oldname)
+	if err != nil {
+		return err
+	}
+
+	fshdest, rewritePathdest, err := a.pathRewrite(newname)
+	if err != nil {
+		return err
+	}
+	if !a.checkAllowAccess(fshsrc, rewritePathsrc, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
+	if !a.checkAllowAccess(fshdest, rewritePathdest, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
+
+	if !fshdest.FileSystemAbstraction.FileExists(filepath.Dir(rewritePathdest)) {
+		fshdest.FileSystemAbstraction.MkdirAll(filepath.Dir(rewritePathdest), 0775)
+	}
+
+	if fshsrc.UUID == fshdest.UUID {
+		//Renaming in same fsh
+		return fshsrc.FileSystemAbstraction.Rename(rewritePathsrc, rewritePathdest)
+	} else {
+		//Cross fsh read write.
+		f, err := fshsrc.FileSystemAbstraction.ReadStream(rewritePathsrc)
+		if err != nil {
+			return err
+		}
+		defer f.Close()
+
+		err = fshdest.FileSystemAbstraction.WriteStream(rewritePathdest, f, 0775)
+		if err != nil {
+			return err
+		}
+
+		err = fshsrc.FileSystemAbstraction.RemoveAll(rewritePathsrc)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (a aofs) Name() string {
+	return "arozos virtualFS"
+}
+
+func (a aofs) Chmod(name string, mode os.FileMode) error {
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.Chmod(rewritePath, mode)
+}
+
+func (a aofs) Chtimes(name string, atime time.Time, mtime time.Time) error {
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.Chtimes(rewritePath, atime, mtime)
+}
+
+//arozos adaptive functions
+//This function rewrite the path from ftp representation to real filepath on disk
+func (a aofs) pathRewrite(path string) (*filesystem.FileSystemHandler, string, error) {
+	path = filepath.ToSlash(filepath.Clean(path))
+	//log.Println("Original path: ", path)
+	if path == "/" {
+		//Roots. Show ftpbuf root
+		fshs := a.userinfo.GetAllFileSystemHandler()
+		for _, fsh := range fshs {
+			//Create a folder representation for this virtual directory
+
+			if !fsh.RequireBuffer {
+				os.MkdirAll(filepath.Join(a.tmpFolder, fsh.UUID), 0755)
+			}
+		}
+
+		readmeContent, err := ioutil.ReadFile("./system/ftp/README.txt")
+		if err != nil {
+			readmeContent = []byte("DO NOT UPLOAD FILES INTO THE ROOT DIRECTORY")
+		}
+		ioutil.WriteFile(filepath.Join(a.tmpFolder, "README.txt"), readmeContent, 0755)
+
+		//Return the tmpFolder root
+		tmpfs, _ := a.userinfo.GetFileSystemHandlerFromVirtualPath("tmp:/")
+		return tmpfs, a.tmpFolder, nil
+	} else if path == "/README.txt" {
+		tmpfs, _ := a.userinfo.GetFileSystemHandlerFromVirtualPath("tmp:/")
+		return tmpfs, a.tmpFolder + "README.txt", nil
+	} else if len(path) > 0 {
+		//Rewrite the path for any alternative filepath
+		//Get the uuid of the filepath
+		path := path[1:]
+		subpaths := strings.Split(path, "/")
+		fsHandlerUUID := subpaths[0]
+		remainingPaths := subpaths[1:]
+
+		fsh, err := a.userinfo.GetFileSystemHandlerFromVirtualPath(fsHandlerUUID + ":")
+		if err != nil {
+			return nil, "", errors.New("File System Abstraction not found")
+		}
+
+		rpath, err := fsh.FileSystemAbstraction.VirtualPathToRealPath(fsh.UUID+":/"+strings.Join(remainingPaths, "/"), a.userinfo.Username)
+		if err != nil {
+			return nil, "", errors.New("File System Handler Hierarchy not supported by FTP driver")
+		}
+		return fsh, rpath, nil
+	} else {
+		//fsh not found.
+		return nil, "", errors.New("Invalid path")
+	}
+}
+
+//Check if user has access to the given path, mode can be string {read / write}
+func (a aofs) checkAllowAccess(fsh *filesystem.FileSystemHandler, path string, mode int) bool {
+	vpath, err := fsh.FileSystemAbstraction.RealPathToVirtualPath(path, a.userinfo.Username)
+	if err != nil {
+		return false
+	}
+
+	if mode == aofsCanRead {
+		return a.userinfo.CanRead(vpath)
+	} else if mode == aofsCanWrite {
+		return a.userinfo.CanWrite(vpath)
+	} else {
+		return false
+	}
+}

+ 95 - 72
mod/storage/sftpserver/sftpserver.go

@@ -10,14 +10,22 @@ import (
 
 	"github.com/pkg/sftp"
 	"golang.org/x/crypto/ssh"
+	user "imuslab.com/arozos/mod/user"
 )
 
+type SFTPConfig struct {
+	ListeningIP string
+	KeyFile     string
+	ReadOnly    bool
+	UserManager *user.UserHandler
+}
+
 type Instance struct {
 }
 
 //Create a new SFTP Server
 //listeningIP in the format of 0.0.0.0:2022
-func NewSFTPServer(listeningIp string, keyfile string, readOnly bool, passwordCheckFunc func(string, string) bool) (*Instance, error) {
+func NewSFTPServer(sftpConfig *SFTPConfig) (*Instance, error) {
 	// An SSH server is represented by a ServerConfig, which holds
 	// certificate details and handles authentication of ServerConns.
 	config := &ssh.ServerConfig{
@@ -26,14 +34,15 @@ func NewSFTPServer(listeningIp string, keyfile string, readOnly bool, passwordCh
 			// a production setting.
 			fmt.Printf("Login: %s\n", c.User())
 
-			if passwordCheckFunc(c.User(), string(pass)) {
-				return nil, nil
+			ok := sftpConfig.UserManager.GetAuthAgent().ValidateUsernameAndPassword(c.User(), string(pass))
+			if !ok {
+				return nil, errors.New("password rejected for " + c.User())
 			}
-			return nil, errors.New("password rejected for " + c.User())
+			return nil, nil
 		},
 	}
 
-	privateBytes, err := ioutil.ReadFile(keyfile)
+	privateBytes, err := ioutil.ReadFile(sftpConfig.KeyFile)
 	if err != nil {
 		return nil, err
 	}
@@ -45,99 +54,113 @@ func NewSFTPServer(listeningIp string, keyfile string, readOnly bool, passwordCh
 
 	config.AddHostKey(private)
 
-	// Once a ServerConfig has been configured, connections can be
-	// accepted.
-	listener, err := net.Listen("tcp", "0.0.0.0:2022")
+	// Once a ServerConfig has been configured, connections can be accepted.
+	listener, err := net.Listen("tcp", sftpConfig.ListeningIP)
 	if err != nil {
 		return nil, err
 	}
 	fmt.Printf("Listening on %v\n", listener.Addr())
 
-	for {
-		nConn, err := listener.Accept()
-		if err != nil {
-			return nil, err
-		}
-
-		go func(nConn net.Conn) error {
-			// Before use, a handshake must be performed on the incoming
-			// net.Conn.
-			_, chans, reqs, err := ssh.NewServerConn(nConn, config)
+	//Start the ssh server listener in go routine
+	go func() error {
+		for {
+			nConn, err := listener.Accept()
 			if err != nil {
 				return err
 			}
-			fmt.Println("SSH server established\n")
-
-			// The incoming Request channel must be serviced.
-			go ssh.DiscardRequests(reqs)
-
-			// Service the incoming Channel channel.
-			for newChannel := range chans {
-				// Channels have a type, depending on the application level
-				// protocol intended. In the case of an SFTP session, this is "subsystem"
-				// with a payload string of "<length=4>sftp"
-				fmt.Println("Incoming channel: %s\n", newChannel.ChannelType())
-				if newChannel.ChannelType() != "session" {
-					newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
-					fmt.Println("Unknown channel type: %s\n", newChannel.ChannelType())
-					continue
+
+			go func(nConn net.Conn) error {
+				// Before use, a handshake must be performed on the incoming
+				// net.Conn.
+				cx, chans, reqs, err := ssh.NewServerConn(nConn, config)
+				if err != nil {
+					return err
 				}
-				channel, requests, err := newChannel.Accept()
+				fmt.Println("SSH server established\n")
+
+				fmt.Println("Connected username", cx.User())
+
+				userinfo, err := sftpConfig.UserManager.GetUserInfoFromUsername(cx.User())
 				if err != nil {
 					return err
 				}
-				fmt.Println("Channel accepted\n")
-
-				// Sessions have out-of-band requests such as "shell",
-				// "pty-req" and "env".  Here we handle only the
-				// "subsystem" request.
-				go func(in <-chan *ssh.Request) {
-					for req := range in {
-						fmt.Println("Request: %v\n", req.Type)
-						ok := false
-						switch req.Type {
-						case "subsystem":
-							fmt.Println("Subsystem: %s\n", req.Payload[4:])
-							if string(req.Payload[4:]) == "sftp" {
-								ok = true
+
+				// The incoming Request channel must be serviced.
+				go ssh.DiscardRequests(reqs)
+
+				// Service the incoming Channel channel.
+				for newChannel := range chans {
+					// Channels have a type, depending on the application level
+					// protocol intended. In the case of an SFTP session, this is "subsystem"
+					// with a payload string of "<length=4>sftp"
+					fmt.Println("Incoming channel: %s\n", newChannel.ChannelType())
+					if newChannel.ChannelType() != "session" {
+						newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
+						fmt.Println("Unknown channel type: %s\n", newChannel.ChannelType())
+						continue
+					}
+					channel, requests, err := newChannel.Accept()
+					if err != nil {
+						return err
+					}
+					fmt.Println("Channel accepted\n")
+
+					// Sessions have out-of-band requests such as "shell",
+					// "pty-req" and "env".  Here we handle only the
+					// "subsystem" request.
+					go func(in <-chan *ssh.Request) {
+						for req := range in {
+							fmt.Println("Request: %v\n", req.Type)
+							ok := false
+							switch req.Type {
+							case "subsystem":
+								fmt.Println("Subsystem: %s\n", req.Payload[4:])
+								if string(req.Payload[4:]) == "sftp" {
+									ok = true
+								}
 							}
+							fmt.Println(" - accepted: %v\n", ok)
+							req.Reply(ok, nil)
 						}
-						fmt.Println(" - accepted: %v\n", ok)
-						req.Reply(ok, nil)
+					}(requests)
+
+					serverOptions := []sftp.ServerOption{}
+
+					if sftpConfig.ReadOnly {
+						serverOptions = append(serverOptions, sftp.ReadOnly())
+						fmt.Println("Read-only server\n")
+					} else {
+						fmt.Println("Read write server\n")
 					}
-				}(requests)
 
-				serverOptions := []sftp.ServerOption{}
+					/*
+						server, err := sftp.NewServer(
+							channel,
+							serverOptions...,
+						)
+						if err != nil {
+							return err
+						}
+					*/
 
-				if readOnly {
-					serverOptions = append(serverOptions, sftp.ReadOnly())
-					fmt.Println("Read-only server\n")
-				} else {
-					fmt.Println("Read write server\n")
-				}
+					//Create a virtual SSH Server that contains all this user's fsh
+					root := GetNewSFTPRoot(userinfo.Username, userinfo.GetAllFileSystemHandler())
+					server := sftp.NewRequestServer(channel, root)
 
-				server, err := sftp.NewServer(
-					channel,
-					serverOptions...,
-				)
-				if err != nil {
-					return err
-				}
-				go func() {
 					if err := server.Serve(); err == io.EOF {
 						server.Close()
 						log.Print("sftp client exited session.")
 					} else if err != nil {
 						log.Fatal("sftp server completed with error:", err)
 					}
-				}()
 
-			}
-			log.Println("Connection cycle ended")
-			return nil
-		}(nConn)
+				}
 
-	}
+				return nil
+			}(nConn)
+
+		}
+	}()
 
 	return &Instance{}, nil
 }

+ 446 - 0
mod/storage/sftpserver/vroot.go

@@ -0,0 +1,446 @@
+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
+}

+ 6 - 6
startup.go

@@ -128,12 +128,12 @@ func RunStartup() {
 	//Finally
 	moduleHandler.ModuleSortList() //Sort the system module list
 
-	_, err = sftpserver.NewSFTPServer("0.0.0.0:2022", "legacy/id_rsa", false, func(username string, password string) bool {
-		if username == "admin" && password == "password" {
-			return true
-		}
-
-		return false
+	_, err = sftpserver.NewSFTPServer(&sftpserver.SFTPConfig{
+		ListeningIP: "0.0.0.0:2022",
+		KeyFile:     "legacy/id_rsa",
+		ReadOnly:    false,
+		UserManager: userHandler,
 	})
 	fmt.Println(err)
+
 }