From c228acaafc474a9b53a66de3ee5907b851dcd243 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 10 Jan 2021 14:25:39 -0800 Subject: [PATCH] Massive refactor of SFTP system now that it is deeply integrated with Wings --- server/filesystem/errors.go | 6 +- server/filesystem/filesystem.go | 66 ++++-- sftp/errors.go | 19 -- sftp/handler.go | 383 +++++++++++--------------------- sftp/server.go | 66 +++--- sftp/sftp.go | 36 +-- sftp/{lister.go => utils.go} | 17 ++ 7 files changed, 226 insertions(+), 367 deletions(-) delete mode 100644 sftp/errors.go rename sftp/{lister.go => utils.go} (57%) diff --git a/server/filesystem/errors.go b/server/filesystem/errors.go index 52a2c20..a5890bd 100644 --- a/server/filesystem/errors.go +++ b/server/filesystem/errors.go @@ -49,10 +49,8 @@ func (e *Error) Code() ErrorCode { // Checks if the given error is one of the Filesystem errors. func IsFilesystemError(err error) (*Error, bool) { - if e := errors.Unwrap(err); e != nil { - err = e - } - if fserr, ok := err.(*Error); ok { + var fserr *Error + if errors.As(err, &fserr) { return fserr, true } return nil, false diff --git a/server/filesystem/filesystem.go b/server/filesystem/filesystem.go index 2e0bd88..a4a49de 100644 --- a/server/filesystem/filesystem.go +++ b/server/filesystem/filesystem.go @@ -2,11 +2,6 @@ package filesystem import ( "bufio" - "emperror.dev/errors" - "github.com/gabriel-vasile/mimetype" - "github.com/karrick/godirwalk" - "github.com/pterodactyl/wings/config" - "github.com/pterodactyl/wings/system" "io" "io/ioutil" "os" @@ -17,6 +12,12 @@ import ( "strings" "sync" "time" + + "emperror.dev/errors" + "github.com/gabriel-vasile/mimetype" + "github.com/karrick/godirwalk" + "github.com/pterodactyl/wings/config" + "github.com/pterodactyl/wings/system" ) type Filesystem struct { @@ -71,6 +72,41 @@ func (fs *Filesystem) File(p string) (*os.File, os.FileInfo, error) { return f, st, nil } +// Acts by creating the given file and path on the disk if it is not present already. If +// it is present, the file is opened using the defaults which will truncate the contents. +// The opened file is then returned to the caller. +func (fs *Filesystem) Touch(p string, flag int) (*os.File, error) { + cleaned, err := fs.SafePath(p) + if err != nil { + return nil, err + } + f, err := os.OpenFile(cleaned, flag, 0644) + if err == nil { + return f, nil + } + // If the error is not because it doesn't exist then we just need to bail at this point. + if !errors.Is(err, os.ErrNotExist) { + return nil, err + } + // Create the path leading up to the file we're trying to create, setting the final perms + // on it as we go. + if err := os.MkdirAll(filepath.Dir(cleaned), 0755); err != nil { + return nil, err + } + if err := fs.Chown(filepath.Dir(cleaned)); err != nil { + return nil, err + } + o := &fileOpener{} + // Try to open the file now that we have created the pathing necessary for it, and then + // Chown that file so that the permissions don't mess with things. + f, err = o.open(cleaned, flag, 0644) + if err != nil { + return nil, err + } + _ = fs.Chown(cleaned) + return f, nil +} + // Reads a file on the system and returns it as a byte representation in a file // reader. This is not the most memory efficient usage since it will be reading the // entirety of the file into memory. @@ -112,22 +148,9 @@ func (fs *Filesystem) Writefile(p string, r io.Reader) error { return err } - // If we were unable to stat the location because it did not exist, go ahead and create - // it now. We do this after checking the disk space so that we do not just create empty - // directories at random. - if err != nil { - if err := os.MkdirAll(filepath.Dir(cleaned), 0755); err != nil { - return err - } - if err := fs.Chown(filepath.Dir(cleaned)); err != nil { - return err - } - } - - o := &fileOpener{} - // This will either create the file if it does not already exist, or open and - // truncate the existing file. - file, err := o.open(cleaned, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + // Touch the file and return the handle to it at this point. This will create the file + // and any necessary directories as needed. + file, err := fs.Touch(cleaned, os.O_RDWR|os.O_CREATE|os.O_TRUNC) if err != nil { return err } @@ -150,7 +173,6 @@ func (fs *Filesystem) CreateDirectory(name string, p string) error { if err != nil { return err } - return os.MkdirAll(cleaned, 0755) } diff --git a/sftp/errors.go b/sftp/errors.go deleted file mode 100644 index 122aa15..0000000 --- a/sftp/errors.go +++ /dev/null @@ -1,19 +0,0 @@ -package sftp - -type fxerr uint32 - -const ( - // Extends the default SFTP server to return a quota exceeded error to the client. - // - // @see https://tools.ietf.org/id/draft-ietf-secsh-filexfer-13.txt - ErrSshQuotaExceeded = fxerr(15) -) - -func (e fxerr) Error() string { - switch e { - case ErrSshQuotaExceeded: - return "Quota Exceeded" - default: - return "Failure" - } -} diff --git a/sftp/handler.go b/sftp/handler.go index 68eed8c..8957583 100644 --- a/sftp/handler.go +++ b/sftp/handler.go @@ -5,31 +5,15 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" "sync" + "emperror.dev/errors" "github.com/apex/log" - "github.com/patrickmn/go-cache" "github.com/pkg/sftp" + "github.com/pterodactyl/wings/server/filesystem" ) -type FileSystem struct { - UUID string - Permissions []string - ReadOnly bool - User User - Cache *cache.Cache - - PathValidator func(fs FileSystem, p string) (string, error) - HasDiskSpace func(fs FileSystem) bool - - logger *log.Entry - lock sync.Mutex -} - -func (fs FileSystem) buildPath(p string) (string, error) { - return fs.PathValidator(fs, p) -} - const ( PermissionFileRead = "file.read" PermissionFileReadContent = "file.read-content" @@ -38,343 +22,244 @@ const ( PermissionFileDelete = "file.delete" ) +type Handler struct { + permissions []string + mu sync.Mutex + fs *filesystem.Filesystem + logger *log.Entry + ro bool +} + // Fileread creates a reader for a file on the system and returns the reader back. -func (fs FileSystem) Fileread(request *sftp.Request) (io.ReaderAt, error) { +func (h *Handler) Fileread(request *sftp.Request) (io.ReaderAt, error) { // Check first if the user can actually open and view a file. This permission is named // really poorly, but it is checking if they can read. There is an addition permission, // "save-files" which determines if they can write that file. - if !fs.can(PermissionFileReadContent) { - return nil, sftp.ErrSshFxPermissionDenied + if !h.can(PermissionFileReadContent) { + return nil, sftp.ErrSSHFxPermissionDenied } - - p, err := fs.buildPath(request.Filepath) + h.mu.Lock() + defer h.mu.Unlock() + f, _, err := h.fs.File(request.Filepath) if err != nil { - return nil, sftp.ErrSshFxNoSuchFile + if !errors.Is(err, os.ErrNotExist) { + h.logger.WithField("error", err).Error("error processing readfile request") + return nil, sftp.ErrSSHFxFailure + } + return nil, sftp.ErrSSHFxNoSuchFile } - - fs.lock.Lock() - defer fs.lock.Unlock() - - if _, err := os.Stat(p); os.IsNotExist(err) { - return nil, sftp.ErrSshFxNoSuchFile - } else if err != nil { - fs.logger.WithField("error", err).Error("error while processing file stat") - - return nil, sftp.ErrSshFxFailure - } - - file, err := os.Open(p) - if err != nil { - fs.logger.WithField("source", p).WithField("error", err).Error("could not open file for reading") - return nil, sftp.ErrSshFxFailure - } - - return file, nil + return f, nil } // Filewrite handles the write actions for a file on the system. -func (fs FileSystem) Filewrite(request *sftp.Request) (io.WriterAt, error) { - if fs.ReadOnly { - return nil, sftp.ErrSshFxOpUnsupported +func (h *Handler) Filewrite(request *sftp.Request) (io.WriterAt, error) { + if h.ro { + return nil, sftp.ErrSSHFxOpUnsupported } - - p, err := fs.buildPath(request.Filepath) - if err != nil { - return nil, sftp.ErrSshFxNoSuchFile - } - - l := fs.logger.WithField("source", p) - + l := h.logger.WithField("source", request.Filepath) // If the user doesn't have enough space left on the server it should respond with an // error since we won't be letting them write this file to the disk. - if !fs.HasDiskSpace(fs) { - return nil, ErrSshQuotaExceeded + if !h.fs.HasSpaceAvailable(true) { + return nil, ErrSSHQuotaExceeded } - fs.lock.Lock() - defer fs.lock.Unlock() - - stat, statErr := os.Stat(p) - // If the file doesn't exist we need to create it, as well as the directory pathway - // leading up to where that file will be created. - if os.IsNotExist(statErr) { - // This is a different pathway than just editing an existing file. If it doesn't exist already - // we need to determine if this user has permission to create files. - if !fs.can(PermissionFileCreate) { - return nil, sftp.ErrSshFxPermissionDenied + h.mu.Lock() + defer h.mu.Unlock() + // The specific permission required to perform this action. If the file exists on the + // system already it only needs to be an update, otherwise we'll check for a create. + permission := PermissionFileUpdate + _, sterr := h.fs.Stat(request.Filepath) + if sterr != nil { + if !errors.Is(sterr, os.ErrNotExist) { + l.WithField("error", sterr).Error("error while getting file reader") + return nil, sftp.ErrSSHFxFailure } - - // Create all of the directories leading up to the location where this file is being created. - if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil { - l.WithFields(log.Fields{ - "path": filepath.Dir(p), - "error": err, - }).Error("error making path for file") - - return nil, sftp.ErrSshFxFailure - } - - file, err := os.Create(p) - if err != nil { - l.WithField("error", err).Error("failed to create file") - - return nil, sftp.ErrSshFxFailure - } - - // Not failing here is intentional. We still made the file, it is just owned incorrectly - // and will likely cause some issues. - if err := os.Chown(p, fs.User.Uid, fs.User.Gid); err != nil { - l.WithField("error", err).Warn("failed to set permissions on file") - } - - return file, nil + permission = PermissionFileCreate } - - // If the stat error isn't about the file not existing, there is some other issue - // at play and we need to go ahead and bail out of the process. - if statErr != nil { - l.WithField("error", statErr).Error("encountered error performing file stat") - - return nil, sftp.ErrSshFxFailure + // Confirm the user has permission to perform this action BEFORE calling Touch, otherwise + // you'll potentially create a file on the system and then fail out because of user + // permission checking after the fact. + if !h.can(permission) { + return nil, sftp.ErrSSHFxPermissionDenied } - - // If we've made it here it means the file already exists and we don't need to do anything - // fancy to handle it. Just pass over the request flags so the system knows what the end - // goal with the file is going to be. - // - // But first, check that the user has permission to save modified files. - if !fs.can(PermissionFileUpdate) { - return nil, sftp.ErrSshFxPermissionDenied - } - - // Not sure this would ever happen, but lets not find out. - if stat.IsDir() { - return nil, sftp.ErrSshFxOpUnsupported - } - - file, err := os.Create(p) + f, err := h.fs.Touch(request.Filepath, os.O_RDWR|os.O_CREATE|os.O_TRUNC) if err != nil { - // Prevent errors if the file is deleted between the stat and this call. - if os.IsNotExist(err) { - return nil, sftp.ErrSSHFxNoSuchFile - } - l.WithField("flags", request.Flags).WithField("error", err).Error("failed to open existing file on system") - return nil, sftp.ErrSshFxFailure + return nil, sftp.ErrSSHFxFailure } - - // Not failing here is intentional. We still made the file, it is just owned incorrectly - // and will likely cause some issues. - if err := os.Chown(p, fs.User.Uid, fs.User.Gid); err != nil { - l.WithField("error", err).Warn("error chowning file") - } - - return file, nil + return f, nil } // Filecmd hander for basic SFTP system calls related to files, but not anything to do with reading // or writing to those files. -func (fs FileSystem) Filecmd(request *sftp.Request) error { - if fs.ReadOnly { - return sftp.ErrSshFxOpUnsupported +func (h *Handler) Filecmd(request *sftp.Request) error { + if h.ro { + return sftp.ErrSSHFxOpUnsupported } - - p, err := fs.buildPath(request.Filepath) - if err != nil { - return sftp.ErrSshFxNoSuchFile - } - - l := fs.logger.WithField("source", p) - - var target string - // If a target is provided in this request validate that it is going to the correct - // location for the server. If it is not, return an operation unsupported error. This - // is maybe not the best error response, but its not wrong either. + l := h.logger.WithField("source", request.Filepath) if request.Target != "" { - target, err = fs.buildPath(request.Target) - if err != nil { - return sftp.ErrSshFxOpUnsupported - } + l = l.WithField("target", request.Target) } switch request.Method { + // Allows a user to make changes to the permissions of a given file or directory + // on their server using their SFTP client. case "Setstat": - if !fs.can(PermissionFileUpdate) { - return sftp.ErrSshFxPermissionDenied + if !h.can(PermissionFileUpdate) { + return sftp.ErrSSHFxPermissionDenied } - - mode := os.FileMode(0644) - // If the client passed a valid file permission use that, otherwise use the - // default of 0644 set above. - if request.Attributes().FileMode().Perm() != 0000 { - mode = request.Attributes().FileMode().Perm() + mode := request.Attributes().FileMode().Perm() + // If the client passes an invalid FileMode just use the default 0644. + if mode == 0000 { + mode = os.FileMode(0644) } - - // Force directories to be 0755 + // Force directories to be 0755. if request.Attributes().FileMode().IsDir() { mode = 0755 } - - if err := os.Chmod(p, mode); err != nil { - if os.IsNotExist(err) { + if err := h.fs.Chmod(request.Filepath, mode); err != nil { + if errors.Is(err, os.ErrNotExist) { return sftp.ErrSSHFxNoSuchFile } - l.WithField("error", err).Error("failed to perform setstat on item") return sftp.ErrSSHFxFailure } - return nil + break + // Support renaming a file (aka Move). case "Rename": - if !fs.can(PermissionFileUpdate) { + if !h.can(PermissionFileUpdate) { return sftp.ErrSSHFxPermissionDenied } - - if err := os.Rename(p, target); err != nil { - if os.IsNotExist(err) { + if err := h.fs.Rename(request.Filepath, request.Target); err != nil { + if errors.Is(err, os.ErrNotExist) { return sftp.ErrSSHFxNoSuchFile } - - l.WithField("target", target).WithField("error", err).Error("failed to rename file") - - return sftp.ErrSshFxFailure + l.WithField("error", err).Error("failed to rename file") + return sftp.ErrSSHFxFailure } - break + // Handle deletion of a directory. This will properly delete all of the files and + // folders within that directory if it is not already empty (unlike a lot of SFTP + // clients that must delete each file individually). case "Rmdir": - if !fs.can(PermissionFileDelete) { - return sftp.ErrSshFxPermissionDenied + if !h.can(PermissionFileDelete) { + return sftp.ErrSSHFxPermissionDenied } - - if err := os.RemoveAll(p); err != nil { + if err := h.fs.Delete(request.Filepath); err != nil { l.WithField("error", err).Error("failed to remove directory") - - return sftp.ErrSshFxFailure + return sftp.ErrSSHFxFailure } - - return sftp.ErrSshFxOk + return sftp.ErrSSHFxOk + // Handle requests to create a new Directory. case "Mkdir": - if !fs.can(PermissionFileCreate) { - return sftp.ErrSshFxPermissionDenied + if !h.can(PermissionFileCreate) { + return sftp.ErrSSHFxPermissionDenied } - - if err := os.MkdirAll(p, 0755); err != nil { + name := strings.Split(filepath.Clean(request.Filepath), "/") + err := h.fs.CreateDirectory(name[len(name)-1], strings.Join(name[0:len(name)-1], "/")) + if err != nil { l.WithField("error", err).Error("failed to create directory") - - return sftp.ErrSshFxFailure + return sftp.ErrSSHFxFailure } - break + // Support creating symlinks between files. The source and target must resolve within + // the server home directory. case "Symlink": - if !fs.can(PermissionFileCreate) { - return sftp.ErrSshFxPermissionDenied + if !h.can(PermissionFileCreate) { + return sftp.ErrSSHFxPermissionDenied } - - if err := os.Symlink(p, target); err != nil { + source, err := h.fs.SafePath(request.Filepath) + if err != nil { + return sftp.ErrSSHFxNoSuchFile + } + target, err := h.fs.SafePath(request.Target) + if err != nil { + return sftp.ErrSSHFxNoSuchFile + } + if err := os.Symlink(source, target); err != nil { l.WithField("target", target).WithField("error", err).Error("failed to create symlink") - - return sftp.ErrSshFxFailure + return sftp.ErrSSHFxFailure } - break + // Called when deleting a file. case "Remove": - if !fs.can(PermissionFileDelete) { - return sftp.ErrSshFxPermissionDenied + if !h.can(PermissionFileDelete) { + return sftp.ErrSSHFxPermissionDenied } - - if err := os.Remove(p); err != nil { - if os.IsNotExist(err) { + if err := h.fs.Delete(request.Filepath); err != nil { + if errors.Is(err, os.ErrNotExist) { return sftp.ErrSSHFxNoSuchFile } - l.WithField("error", err).Error("failed to remove a file") - - return sftp.ErrSshFxFailure + return sftp.ErrSSHFxFailure } - - return sftp.ErrSshFxOk + return sftp.ErrSSHFxOk default: - return sftp.ErrSshFxOpUnsupported + return sftp.ErrSSHFxOpUnsupported } - var fileLocation = p - if target != "" { - fileLocation = target + target := request.Filepath + if request.Target != "" { + target = request.Target } - // Not failing here is intentional. We still made the file, it is just owned incorrectly // and will likely cause some issues. There is no logical check for if the file was removed // because both of those cases (Rmdir, Remove) have an explicit return rather than break. - if err := os.Chown(fileLocation, fs.User.Uid, fs.User.Gid); err != nil { + if err := h.fs.Chown(target); err != nil { l.WithField("error", err).Warn("error chowning file") } - return sftp.ErrSshFxOk + return sftp.ErrSSHFxOk } // Filelist is the handler for SFTP filesystem list calls. This will handle calls to list the contents of // a directory as well as perform file/folder stat calls. -func (fs FileSystem) Filelist(request *sftp.Request) (sftp.ListerAt, error) { - p, err := fs.buildPath(request.Filepath) - if err != nil { - return nil, sftp.ErrSshFxNoSuchFile +func (h *Handler) Filelist(request *sftp.Request) (sftp.ListerAt, error) { + if !h.can(PermissionFileRead) { + return nil, sftp.ErrSSHFxPermissionDenied } switch request.Method { case "List": - if !fs.can(PermissionFileRead) { - return nil, sftp.ErrSshFxPermissionDenied + p, err := h.fs.SafePath(request.Filepath) + if err != nil { + return nil, sftp.ErrSSHFxNoSuchFile } - files, err := ioutil.ReadDir(p) if err != nil { - fs.logger.WithField("error", err).Error("error while listing directory") + h.logger.WithField("source", request.Filepath).WithField("error", err).Error("error while listing directory") - return nil, sftp.ErrSshFxFailure + return nil, sftp.ErrSSHFxFailure } - return ListerAt(files), nil case "Stat": - if !fs.can(PermissionFileRead) { - return nil, sftp.ErrSshFxPermissionDenied + st, err := h.fs.Stat(request.Filepath) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, sftp.ErrSSHFxNoSuchFile + } + h.logger.WithField("source", request.Filepath).WithField("error", err).Error("error performing stat on file") + return nil, sftp.ErrSSHFxFailure } - - s, err := os.Stat(p) - if os.IsNotExist(err) { - return nil, sftp.ErrSshFxNoSuchFile - } else if err != nil { - fs.logger.WithField("source", p).WithField("error", err).Error("error performing stat on file") - - return nil, sftp.ErrSshFxFailure - } - - return ListerAt([]os.FileInfo{s}), nil + return ListerAt([]os.FileInfo{st.Info}), nil default: - // Before adding readlink support we need to evaluate any potential security risks - // as a result of navigating around to a location that is outside the home directory - // for the logged in user. I don't foresee it being much of a problem, but I do want to - // check it out before slapping some code here. Until then, we'll just return an - // unsupported response code. - return nil, sftp.ErrSshFxOpUnsupported + return nil, sftp.ErrSSHFxOpUnsupported } } // Determines if a user has permission to perform a specific action on the SFTP server. These // permissions are defined and returned by the Panel API. -func (fs FileSystem) can(permission string) bool { - // Server owners and super admins have their permissions returned as '[*]' via the Panel +func (h *Handler) can(permission string) bool { + // SFTPServer owners and super admins have their permissions returned as '[*]' via the Panel // API, so for the sake of speed do an initial check for that before iterating over the // entire array of permissions. - if len(fs.Permissions) == 1 && fs.Permissions[0] == "*" { + if len(h.permissions) == 1 && h.permissions[0] == "*" { return true } - - // Not the owner or an admin, loop over the permissions that were returned to determine - // if they have the passed permission. - for _, p := range fs.Permissions { + for _, p := range h.permissions { if p == permission { return true } } - return false } diff --git a/sftp/server.go b/sftp/server.go index a0ae24b..294122a 100644 --- a/sftp/server.go +++ b/sftp/server.go @@ -12,12 +12,12 @@ import ( "os" "path" "strings" - "time" "github.com/apex/log" - "github.com/patrickmn/go-cache" "github.com/pkg/sftp" "github.com/pterodactyl/wings/api" + "github.com/pterodactyl/wings/config" + "github.com/pterodactyl/wings/server" "golang.org/x/crypto/ssh" ) @@ -33,35 +33,23 @@ type User struct { Gid int } -type Server struct { - cache *cache.Cache - +//goland:noinspection GoNameStartsWithPackageName +type SFTPServer struct { Settings Settings User User - - PathValidator func(fs FileSystem, p string) (string, error) - DiskSpaceValidator func(fs FileSystem) bool - // Validator function that is called when a user connects to the server. This should // check against whatever system is desired to confirm if the given username and password // combination is valid. If so, should return an authentication response. - CredentialValidator func(r api.SftpAuthRequest) (*api.SftpAuthResponse, error) -} - -// Create a new server configuration instance. -func New(c *Server) error { - c.cache = cache.New(5*time.Minute, 10*time.Minute) - - return nil + credentialValidator func(r api.SftpAuthRequest) (*api.SftpAuthResponse, error) } // Initialize the SFTP server and add a persistent listener to handle inbound SFTP connections. -func (c *Server) Initialize() error { +func (c *SFTPServer) Initialize() error { serverConfig := &ssh.ServerConfig{ NoClientAuth: false, MaxAuthTries: 6, PasswordCallback: func(conn ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { - resp, err := c.CredentialValidator(api.SftpAuthRequest{ + resp, err := c.credentialValidator(api.SftpAuthRequest{ User: conn.User(), Pass: string(pass), IP: conn.RemoteAddr().String(), @@ -123,7 +111,7 @@ func (c *Server) Initialize() error { // Handles an inbound connection to the instance and determines if we should serve the request // or not. -func (c Server) AcceptInboundConnection(conn net.Conn, config *ssh.ServerConfig) { +func (c SFTPServer) AcceptInboundConnection(conn net.Conn, config *ssh.ServerConfig) { defer conn.Close() // Before beginning a handshake must be performed on the incoming net.Conn @@ -165,19 +153,17 @@ func (c Server) AcceptInboundConnection(conn net.Conn, config *ssh.ServerConfig) } }(requests) - // Configure the user's home folder for the rest of the request cycle. if sconn.Permissions.Extensions["uuid"] == "" { continue } // Create a new handler for the currently logged in user's server. - fs := c.createHandler(sconn) + fs := c.newHandler(sconn) // Create the server instance for the channel using the filesystem we created above. - server := sftp.NewRequestServer(channel, fs) - - if err := server.Serve(); err == io.EOF { - server.Close() + handler := sftp.NewRequestServer(channel, fs) + if err := handler.Serve(); err == io.EOF { + handler.Close() } } } @@ -185,15 +171,15 @@ func (c Server) AcceptInboundConnection(conn net.Conn, config *ssh.ServerConfig) // Creates a new SFTP handler for a given server. The directory argument should // be the base directory for a server. All actions done on the server will be // relative to that directory, and the user will not be able to escape out of it. -func (c Server) createHandler(sc *ssh.ServerConn) sftp.Handlers { - p := FileSystem{ - UUID: sc.Permissions.Extensions["uuid"], - Permissions: strings.Split(sc.Permissions.Extensions["permissions"], ","), - ReadOnly: c.Settings.ReadOnly, - Cache: c.cache, - User: c.User, - HasDiskSpace: c.DiskSpaceValidator, - PathValidator: c.PathValidator, +func (c SFTPServer) newHandler(sc *ssh.ServerConn) sftp.Handlers { + s := server.GetServers().Find(func(s *server.Server) bool { + return s.Id() == sc.Permissions.Extensions["uuid"] + }) + + p := Handler{ + fs: s.Filesystem(), + permissions: strings.Split(sc.Permissions.Extensions["permissions"], ","), + ro: config.Get().System.Sftp.ReadOnly, logger: log.WithFields(log.Fields{ "subsystem": "sftp", "username": sc.User(), @@ -202,15 +188,15 @@ func (c Server) createHandler(sc *ssh.ServerConn) sftp.Handlers { } return sftp.Handlers{ - FileGet: p, - FilePut: p, - FileCmd: p, - FileList: p, + FileGet: &p, + FilePut: &p, + FileCmd: &p, + FileList: &p, } } // Generates a private key that will be used by the SFTP server. -func (c Server) generatePrivateKey() error { +func (c SFTPServer) generatePrivateKey() error { key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return err diff --git a/sftp/sftp.go b/sftp/sftp.go index 995db58..edab0a8 100644 --- a/sftp/sftp.go +++ b/sftp/sftp.go @@ -11,7 +11,7 @@ import ( var noMatchingServerError = errors.New("no matching server with that UUID was found") func Initialize(config config.SystemConfiguration) error { - s := &Server{ + s := &SFTPServer{ User: User{ Uid: config.User.Uid, Gid: config.User.Gid, @@ -22,18 +22,12 @@ func Initialize(config config.SystemConfiguration) error { BindAddress: config.Sftp.Address, BindPort: config.Sftp.Port, }, - CredentialValidator: validateCredentials, - PathValidator: validatePath, - DiskSpaceValidator: validateDiskSpace, - } - - if err := New(s); err != nil { - return err + credentialValidator: validateCredentials, } // Initialize the SFTP server in a background thread since this is // a long running operation. - go func(s *Server) { + go func(s *SFTPServer) { if err := s.Initialize(); err != nil { log.WithField("subsystem", "sftp").WithField("error", err).Error("failed to initialize SFTP subsystem") } @@ -42,30 +36,6 @@ func Initialize(config config.SystemConfiguration) error { return nil } -func validatePath(fs FileSystem, p string) (string, error) { - s := server.GetServers().Find(func(server *server.Server) bool { - return server.Id() == fs.UUID - }) - - if s == nil { - return "", noMatchingServerError - } - - return s.Filesystem().SafePath(p) -} - -func validateDiskSpace(fs FileSystem) bool { - s := server.GetServers().Find(func(server *server.Server) bool { - return server.Id() == fs.UUID - }) - - if s == nil { - return false - } - - return s.Filesystem().HasSpaceAvailable(true) -} - // Validates a set of credentials for a SFTP login against Pterodactyl Panel and returns // the server's UUID if the credentials were valid. func validateCredentials(c api.SftpAuthRequest) (*api.SftpAuthResponse, error) { diff --git a/sftp/lister.go b/sftp/utils.go similarity index 57% rename from sftp/lister.go rename to sftp/utils.go index 129020a..3ef6c5a 100644 --- a/sftp/lister.go +++ b/sftp/utils.go @@ -6,6 +6,14 @@ import ( ) type ListerAt []os.FileInfo +type fxerr uint32 + +const ( + // Extends the default SFTP server to return a quota exceeded error to the client. + // + // @see https://tools.ietf.org/id/draft-ietf-secsh-filexfer-13.txt + ErrSSHQuotaExceeded = fxerr(15) +) // Returns the number of entries copied and an io.EOF error if we made it to the end of the file list. // Take a look at the pkg/sftp godoc for more information about how this function should work. @@ -20,3 +28,12 @@ func (l ListerAt) ListAt(f []os.FileInfo, offset int64) (int, error) { return n, nil } } + +func (e fxerr) Error() string { + switch e { + case ErrSSHQuotaExceeded: + return "Quota Exceeded" + default: + return "Failure" + } +}