2020-09-01 03:14:04 +00:00
|
|
|
package sftp
|
|
|
|
|
|
|
|
import (
|
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2021-01-10 22:25:39 +00:00
|
|
|
"strings"
|
2020-09-01 03:14:04 +00:00
|
|
|
"sync"
|
2021-01-10 01:22:39 +00:00
|
|
|
|
2021-01-10 22:25:39 +00:00
|
|
|
"emperror.dev/errors"
|
2021-01-10 01:22:39 +00:00
|
|
|
"github.com/apex/log"
|
|
|
|
"github.com/pkg/sftp"
|
2021-08-02 21:07:00 +00:00
|
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
|
2021-01-10 23:06:06 +00:00
|
|
|
"github.com/pterodactyl/wings/config"
|
2021-08-04 02:56:02 +00:00
|
|
|
"github.com/pterodactyl/wings/server"
|
2021-01-10 22:25:39 +00:00
|
|
|
"github.com/pterodactyl/wings/server/filesystem"
|
2020-09-01 03:14:04 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
PermissionFileRead = "file.read"
|
|
|
|
PermissionFileReadContent = "file.read-content"
|
|
|
|
PermissionFileCreate = "file.create"
|
|
|
|
PermissionFileUpdate = "file.update"
|
|
|
|
PermissionFileDelete = "file.delete"
|
|
|
|
)
|
|
|
|
|
2022-07-09 23:37:39 +00:00
|
|
|
type Handler struct {
|
|
|
|
mu sync.Mutex
|
2022-07-10 18:30:32 +00:00
|
|
|
server *server.Server
|
2021-01-10 22:25:39 +00:00
|
|
|
fs *filesystem.Filesystem
|
2022-07-10 18:30:32 +00:00
|
|
|
events *eventHandler
|
|
|
|
permissions []string
|
2021-01-10 22:25:39 +00:00
|
|
|
logger *log.Entry
|
|
|
|
ro bool
|
|
|
|
}
|
|
|
|
|
2022-07-09 23:37:39 +00:00
|
|
|
// NewHandler returns a new connection handler for the SFTP server. This allows a given user
|
2021-01-10 23:06:06 +00:00
|
|
|
// to access the underlying filesystem.
|
2022-07-09 23:37:39 +00:00
|
|
|
func NewHandler(sc *ssh.ServerConn, srv *server.Server) (*Handler, error) {
|
|
|
|
uuid, ok := sc.Permissions.Extensions["user"]
|
|
|
|
if !ok {
|
|
|
|
return nil, errors.New("sftp: mismatched Wings and Panel versions — Panel 1.10 is required for this version of Wings.")
|
|
|
|
}
|
|
|
|
|
2022-07-10 18:30:32 +00:00
|
|
|
events := eventHandler{
|
|
|
|
ip: sc.RemoteAddr().String(),
|
|
|
|
user: uuid,
|
|
|
|
server: srv.ID(),
|
|
|
|
}
|
|
|
|
|
2021-01-10 23:06:06 +00:00
|
|
|
return &Handler{
|
|
|
|
permissions: strings.Split(sc.Permissions.Extensions["permissions"], ","),
|
2022-07-10 18:30:32 +00:00
|
|
|
server: srv,
|
|
|
|
fs: srv.Filesystem(),
|
|
|
|
events: &events,
|
|
|
|
ro: config.Get().System.Sftp.ReadOnly,
|
|
|
|
logger: log.WithFields(log.Fields{"subsystem": "sftp", "user": uuid, "ip": sc.RemoteAddr()}),
|
2022-07-09 23:37:39 +00:00
|
|
|
}, nil
|
2021-01-10 23:06:06 +00:00
|
|
|
}
|
|
|
|
|
2022-07-10 18:30:32 +00:00
|
|
|
// Handlers returns the sftp.Handlers for this struct.
|
2021-01-10 23:06:06 +00:00
|
|
|
func (h *Handler) Handlers() sftp.Handlers {
|
|
|
|
return sftp.Handlers{
|
|
|
|
FileGet: h,
|
|
|
|
FilePut: h,
|
|
|
|
FileCmd: h,
|
|
|
|
FileList: h,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-01 03:14:04 +00:00
|
|
|
// Fileread creates a reader for a file on the system and returns the reader back.
|
2021-01-10 22:25:39 +00:00
|
|
|
func (h *Handler) Fileread(request *sftp.Request) (io.ReaderAt, error) {
|
2020-09-01 03:14:04 +00:00
|
|
|
// 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.
|
2021-01-10 22:25:39 +00:00
|
|
|
if !h.can(PermissionFileReadContent) {
|
|
|
|
return nil, sftp.ErrSSHFxPermissionDenied
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
2021-01-10 22:25:39 +00:00
|
|
|
h.mu.Lock()
|
|
|
|
defer h.mu.Unlock()
|
|
|
|
f, _, err := h.fs.File(request.Filepath)
|
2020-09-01 03:14:04 +00:00
|
|
|
if err != nil {
|
2021-01-10 22:25:39 +00:00
|
|
|
if !errors.Is(err, os.ErrNotExist) {
|
|
|
|
h.logger.WithField("error", err).Error("error processing readfile request")
|
|
|
|
return nil, sftp.ErrSSHFxFailure
|
|
|
|
}
|
|
|
|
return nil, sftp.ErrSSHFxNoSuchFile
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
2021-01-10 22:25:39 +00:00
|
|
|
return f, nil
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Filewrite handles the write actions for a file on the system.
|
2021-01-10 22:25:39 +00:00
|
|
|
func (h *Handler) Filewrite(request *sftp.Request) (io.WriterAt, error) {
|
|
|
|
if h.ro {
|
|
|
|
return nil, sftp.ErrSSHFxOpUnsupported
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
2021-01-10 22:25:39 +00:00
|
|
|
l := h.logger.WithField("source", request.Filepath)
|
2020-09-01 03:14:04 +00:00
|
|
|
// 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.
|
2021-01-10 22:25:39 +00:00
|
|
|
if !h.fs.HasSpaceAvailable(true) {
|
|
|
|
return nil, ErrSSHQuotaExceeded
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
|
|
|
|
2021-01-10 22:25:39 +00:00
|
|
|
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
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
2021-01-10 22:25:39 +00:00
|
|
|
permission = PermissionFileCreate
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
2021-01-10 22:25:39 +00:00
|
|
|
// 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
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
2024-03-13 03:44:55 +00:00
|
|
|
f, err := h.fs.Touch(request.Filepath, os.O_RDWR|os.O_TRUNC)
|
2020-09-01 03:14:04 +00:00
|
|
|
if err != nil {
|
2020-11-28 23:57:10 +00:00
|
|
|
l.WithField("flags", request.Flags).WithField("error", err).Error("failed to open existing file on system")
|
2021-01-10 22:25:39 +00:00
|
|
|
return nil, sftp.ErrSSHFxFailure
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
2022-01-23 18:14:02 +00:00
|
|
|
// Chown may or may not have been called in the touch function, so always do
|
|
|
|
// it at this point to avoid the file being improperly owned.
|
2022-07-09 23:37:39 +00:00
|
|
|
_ = h.fs.Chown(request.Filepath)
|
2022-07-10 20:51:11 +00:00
|
|
|
event := server.ActivitySftpWrite
|
2022-07-10 18:30:32 +00:00
|
|
|
if permission == PermissionFileCreate {
|
2022-07-10 20:51:11 +00:00
|
|
|
event = server.ActivitySftpCreate
|
2022-07-10 18:30:32 +00:00
|
|
|
}
|
|
|
|
h.events.MustLog(event, FileAction{Entity: request.Filepath})
|
2021-01-10 22:25:39 +00:00
|
|
|
return f, nil
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Filecmd hander for basic SFTP system calls related to files, but not anything to do with reading
|
|
|
|
// or writing to those files.
|
2021-01-10 22:25:39 +00:00
|
|
|
func (h *Handler) Filecmd(request *sftp.Request) error {
|
|
|
|
if h.ro {
|
|
|
|
return sftp.ErrSSHFxOpUnsupported
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
2021-01-10 22:25:39 +00:00
|
|
|
l := h.logger.WithField("source", request.Filepath)
|
2020-09-01 03:14:04 +00:00
|
|
|
if request.Target != "" {
|
2021-01-10 22:25:39 +00:00
|
|
|
l = l.WithField("target", request.Target)
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
switch request.Method {
|
2021-01-10 22:25:39 +00:00
|
|
|
// Allows a user to make changes to the permissions of a given file or directory
|
|
|
|
// on their server using their SFTP client.
|
2020-09-01 03:14:04 +00:00
|
|
|
case "Setstat":
|
2021-01-10 22:25:39 +00:00
|
|
|
if !h.can(PermissionFileUpdate) {
|
|
|
|
return sftp.ErrSSHFxPermissionDenied
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
2021-01-10 22:25:39 +00:00
|
|
|
mode := request.Attributes().FileMode().Perm()
|
|
|
|
// If the client passes an invalid FileMode just use the default 0644.
|
2021-11-15 17:24:52 +00:00
|
|
|
if mode == 0o000 {
|
|
|
|
mode = os.FileMode(0o644)
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
2021-01-10 22:25:39 +00:00
|
|
|
// Force directories to be 0755.
|
2020-09-01 03:14:04 +00:00
|
|
|
if request.Attributes().FileMode().IsDir() {
|
2021-11-15 17:24:52 +00:00
|
|
|
mode = 0o755
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
2021-01-10 22:25:39 +00:00
|
|
|
if err := h.fs.Chmod(request.Filepath, mode); err != nil {
|
|
|
|
if errors.Is(err, os.ErrNotExist) {
|
2020-09-01 03:14:04 +00:00
|
|
|
return sftp.ErrSSHFxNoSuchFile
|
|
|
|
}
|
2020-11-28 23:57:10 +00:00
|
|
|
l.WithField("error", err).Error("failed to perform setstat on item")
|
2020-09-01 03:14:04 +00:00
|
|
|
return sftp.ErrSSHFxFailure
|
|
|
|
}
|
2021-01-10 22:25:39 +00:00
|
|
|
break
|
|
|
|
// Support renaming a file (aka Move).
|
2020-09-01 03:14:04 +00:00
|
|
|
case "Rename":
|
2021-01-10 22:25:39 +00:00
|
|
|
if !h.can(PermissionFileUpdate) {
|
2020-09-01 03:14:04 +00:00
|
|
|
return sftp.ErrSSHFxPermissionDenied
|
|
|
|
}
|
2022-07-10 18:30:32 +00:00
|
|
|
if err := h.fs.Rename(request.Filepath, request.Target); err != nil {
|
2021-01-10 22:25:39 +00:00
|
|
|
if errors.Is(err, os.ErrNotExist) {
|
2020-09-01 03:14:04 +00:00
|
|
|
return sftp.ErrSSHFxNoSuchFile
|
|
|
|
}
|
2021-01-10 22:25:39 +00:00
|
|
|
l.WithField("error", err).Error("failed to rename file")
|
|
|
|
return sftp.ErrSSHFxFailure
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
2022-07-10 20:51:11 +00:00
|
|
|
h.events.MustLog(server.ActivitySftpRename, FileAction{Entity: request.Filepath, Target: request.Target})
|
2020-09-01 03:14:04 +00:00
|
|
|
break
|
2021-01-10 22:25:39 +00:00
|
|
|
// 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).
|
2020-09-01 03:14:04 +00:00
|
|
|
case "Rmdir":
|
2021-01-10 22:25:39 +00:00
|
|
|
if !h.can(PermissionFileDelete) {
|
|
|
|
return sftp.ErrSSHFxPermissionDenied
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
2022-07-09 23:37:39 +00:00
|
|
|
p := filepath.Clean(request.Filepath)
|
|
|
|
if err := h.fs.Delete(p); err != nil {
|
2020-11-28 23:57:10 +00:00
|
|
|
l.WithField("error", err).Error("failed to remove directory")
|
2021-01-10 22:25:39 +00:00
|
|
|
return sftp.ErrSSHFxFailure
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
2022-07-10 20:51:11 +00:00
|
|
|
h.events.MustLog(server.ActivitySftpDelete, FileAction{Entity: request.Filepath})
|
2021-01-10 22:25:39 +00:00
|
|
|
return sftp.ErrSSHFxOk
|
|
|
|
// Handle requests to create a new Directory.
|
2020-09-01 03:14:04 +00:00
|
|
|
case "Mkdir":
|
2021-01-10 22:25:39 +00:00
|
|
|
if !h.can(PermissionFileCreate) {
|
|
|
|
return sftp.ErrSSHFxPermissionDenied
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
2021-01-10 22:25:39 +00:00
|
|
|
name := strings.Split(filepath.Clean(request.Filepath), "/")
|
2022-07-09 23:37:39 +00:00
|
|
|
p := strings.Join(name[0:len(name)-1], "/")
|
|
|
|
if err := h.fs.CreateDirectory(name[len(name)-1], p); err != nil {
|
2020-11-28 23:57:10 +00:00
|
|
|
l.WithField("error", err).Error("failed to create directory")
|
2021-01-10 22:25:39 +00:00
|
|
|
return sftp.ErrSSHFxFailure
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
2022-07-10 20:51:11 +00:00
|
|
|
h.events.MustLog(server.ActivitySftpCreateDirectory, FileAction{Entity: request.Filepath})
|
2020-09-01 03:14:04 +00:00
|
|
|
break
|
2021-01-10 22:25:39 +00:00
|
|
|
// Support creating symlinks between files. The source and target must resolve within
|
|
|
|
// the server home directory.
|
2020-09-01 03:14:04 +00:00
|
|
|
case "Symlink":
|
2021-01-10 22:25:39 +00:00
|
|
|
if !h.can(PermissionFileCreate) {
|
|
|
|
return sftp.ErrSSHFxPermissionDenied
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
2024-03-13 03:44:55 +00:00
|
|
|
if err := h.fs.Symlink(request.Filepath, request.Target); err != nil {
|
|
|
|
l.WithField("target", request.Target).WithField("error", err).Error("failed to create symlink")
|
2021-01-10 22:25:39 +00:00
|
|
|
return sftp.ErrSSHFxFailure
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
|
|
|
break
|
2021-01-10 22:25:39 +00:00
|
|
|
// Called when deleting a file.
|
2020-09-01 03:14:04 +00:00
|
|
|
case "Remove":
|
2021-01-10 22:25:39 +00:00
|
|
|
if !h.can(PermissionFileDelete) {
|
|
|
|
return sftp.ErrSSHFxPermissionDenied
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
2022-07-10 18:30:32 +00:00
|
|
|
if err := h.fs.Delete(request.Filepath); err != nil {
|
2021-01-10 22:25:39 +00:00
|
|
|
if errors.Is(err, os.ErrNotExist) {
|
2020-09-01 03:14:04 +00:00
|
|
|
return sftp.ErrSSHFxNoSuchFile
|
|
|
|
}
|
2020-11-28 23:57:10 +00:00
|
|
|
l.WithField("error", err).Error("failed to remove a file")
|
2021-01-10 22:25:39 +00:00
|
|
|
return sftp.ErrSSHFxFailure
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
2022-07-10 20:51:11 +00:00
|
|
|
h.events.MustLog(server.ActivitySftpDelete, FileAction{Entity: request.Filepath})
|
2021-01-10 22:25:39 +00:00
|
|
|
return sftp.ErrSSHFxOk
|
2020-09-01 03:14:04 +00:00
|
|
|
default:
|
2021-01-10 22:25:39 +00:00
|
|
|
return sftp.ErrSSHFxOpUnsupported
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
|
|
|
|
2021-01-10 22:25:39 +00:00
|
|
|
target := request.Filepath
|
|
|
|
if request.Target != "" {
|
|
|
|
target = request.Target
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
|
|
|
// 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.
|
2021-01-10 22:25:39 +00:00
|
|
|
if err := h.fs.Chown(target); err != nil {
|
2020-11-28 23:57:10 +00:00
|
|
|
l.WithField("error", err).Warn("error chowning file")
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
|
|
|
|
2021-01-10 22:25:39 +00:00
|
|
|
return sftp.ErrSSHFxOk
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
2021-01-10 22:25:39 +00:00
|
|
|
func (h *Handler) Filelist(request *sftp.Request) (sftp.ListerAt, error) {
|
|
|
|
if !h.can(PermissionFileRead) {
|
|
|
|
return nil, sftp.ErrSSHFxPermissionDenied
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
switch request.Method {
|
|
|
|
case "List":
|
2024-03-13 03:44:55 +00:00
|
|
|
entries, err := h.fs.ReadDirStat(request.Filepath)
|
2020-09-01 03:14:04 +00:00
|
|
|
if err != nil {
|
2021-01-10 22:25:39 +00:00
|
|
|
h.logger.WithField("source", request.Filepath).WithField("error", err).Error("error while listing directory")
|
|
|
|
return nil, sftp.ErrSSHFxFailure
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
2024-03-13 03:44:55 +00:00
|
|
|
return ListerAt(entries), nil
|
2020-09-01 03:14:04 +00:00
|
|
|
case "Stat":
|
2021-01-10 22:25:39 +00:00
|
|
|
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
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
2021-01-16 20:03:55 +00:00
|
|
|
return ListerAt([]os.FileInfo{st.FileInfo}), nil
|
2020-09-01 03:14:04 +00:00
|
|
|
default:
|
2021-01-10 22:25:39 +00:00
|
|
|
return nil, sftp.ErrSSHFxOpUnsupported
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
2021-01-10 22:25:39 +00:00
|
|
|
func (h *Handler) can(permission string) bool {
|
2022-07-10 18:30:32 +00:00
|
|
|
if h.server.IsSuspended() {
|
2021-08-04 02:56:02 +00:00
|
|
|
return false
|
|
|
|
}
|
2021-01-10 22:25:39 +00:00
|
|
|
for _, p := range h.permissions {
|
2022-05-30 01:48:49 +00:00
|
|
|
// If we match the permission specifically, or the user has been granted the "*"
|
|
|
|
// permission because they're an admin, let them through.
|
|
|
|
if p == permission || p == "*" {
|
2020-09-01 03:14:04 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|