2019-12-07 22:01:40 +00:00
|
|
|
package sftp
|
|
|
|
|
|
|
|
import (
|
2021-02-02 04:59:17 +00:00
|
|
|
"context"
|
2020-09-01 03:14:04 +00:00
|
|
|
"crypto/rand"
|
|
|
|
"crypto/rsa"
|
|
|
|
"crypto/x509"
|
|
|
|
"encoding/pem"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"net"
|
|
|
|
"os"
|
|
|
|
"path"
|
2021-02-02 04:59:17 +00:00
|
|
|
"regexp"
|
2021-01-10 23:06:06 +00:00
|
|
|
"strconv"
|
2020-09-01 03:14:04 +00:00
|
|
|
"strings"
|
2021-01-10 01:22:39 +00:00
|
|
|
|
2021-01-10 22:43:27 +00:00
|
|
|
"emperror.dev/errors"
|
2021-01-10 01:22:39 +00:00
|
|
|
"github.com/apex/log"
|
|
|
|
"github.com/pkg/sftp"
|
2021-01-10 22:25:39 +00:00
|
|
|
"github.com/pterodactyl/wings/config"
|
2021-02-02 04:59:17 +00:00
|
|
|
"github.com/pterodactyl/wings/remote"
|
2021-01-10 22:25:39 +00:00
|
|
|
"github.com/pterodactyl/wings/server"
|
2021-01-10 01:22:39 +00:00
|
|
|
"golang.org/x/crypto/ssh"
|
2019-12-07 22:01:40 +00:00
|
|
|
)
|
|
|
|
|
2021-02-02 04:59:17 +00:00
|
|
|
// Usernames all follow the same format, so don't even bother hitting the API if the username is not
|
|
|
|
// at least in the expected format. This is very basic protection against random bots finding the SFTP
|
|
|
|
// server and sending a flood of usernames.
|
|
|
|
var validUsernameRegexp = regexp.MustCompile(`^(?i)(.+)\.([a-z0-9]{8})$`)
|
|
|
|
|
2021-01-10 22:43:27 +00:00
|
|
|
//goland:noinspection GoNameStartsWithPackageName
|
|
|
|
type SFTPServer struct {
|
2021-01-26 04:28:24 +00:00
|
|
|
manager *server.Manager
|
2021-01-10 23:06:06 +00:00
|
|
|
BasePath string
|
|
|
|
ReadOnly bool
|
|
|
|
Listen string
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
|
|
|
|
2021-01-26 04:28:24 +00:00
|
|
|
func New(m *server.Manager) *SFTPServer {
|
2021-01-10 22:43:27 +00:00
|
|
|
cfg := config.Get().System
|
|
|
|
return &SFTPServer{
|
2021-01-26 04:28:24 +00:00
|
|
|
manager: m,
|
2021-01-10 23:06:06 +00:00
|
|
|
BasePath: cfg.Data,
|
|
|
|
ReadOnly: cfg.Sftp.ReadOnly,
|
|
|
|
Listen: cfg.Sftp.Address + ":" + strconv.Itoa(cfg.Sftp.Port),
|
2021-01-10 22:43:27 +00:00
|
|
|
}
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
|
|
|
|
2021-01-10 22:43:27 +00:00
|
|
|
// Starts the SFTP server and add a persistent listener to handle inbound SFTP connections.
|
|
|
|
func (c *SFTPServer) Run() error {
|
|
|
|
if _, err := os.Stat(path.Join(c.BasePath, ".sftp/id_rsa")); os.IsNotExist(err) {
|
2020-09-01 03:14:04 +00:00
|
|
|
if err := c.generatePrivateKey(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
} else if err != nil {
|
2021-01-10 23:06:06 +00:00
|
|
|
return errors.Wrap(err, "sftp/server: could not stat private key file")
|
2019-12-07 23:53:07 +00:00
|
|
|
}
|
2021-01-10 23:06:06 +00:00
|
|
|
pb, err := ioutil.ReadFile(path.Join(c.BasePath, ".sftp/id_rsa"))
|
2020-09-01 03:14:04 +00:00
|
|
|
if err != nil {
|
2021-01-10 23:06:06 +00:00
|
|
|
return errors.Wrap(err, "sftp/server: could not read private key file")
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
2021-01-10 23:06:06 +00:00
|
|
|
private, err := ssh.ParsePrivateKey(pb)
|
2020-09-01 03:14:04 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-12-07 23:53:07 +00:00
|
|
|
|
2021-01-10 23:06:06 +00:00
|
|
|
conf := &ssh.ServerConfig{
|
|
|
|
NoClientAuth: false,
|
|
|
|
MaxAuthTries: 6,
|
|
|
|
PasswordCallback: c.passwordCallback,
|
|
|
|
}
|
|
|
|
conf.AddHostKey(private)
|
2019-12-08 01:35:45 +00:00
|
|
|
|
2021-01-10 23:06:06 +00:00
|
|
|
listener, err := net.Listen("tcp", c.Listen)
|
2020-09-01 03:14:04 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
2019-12-08 01:35:45 +00:00
|
|
|
}
|
|
|
|
|
2021-01-10 23:06:06 +00:00
|
|
|
log.WithField("listen", c.Listen).Info("sftp server listening for connections")
|
2020-09-01 03:14:04 +00:00
|
|
|
for {
|
2021-01-10 23:06:06 +00:00
|
|
|
if conn, _ := listener.Accept(); conn != nil {
|
|
|
|
go func(conn net.Conn) {
|
|
|
|
defer conn.Close()
|
|
|
|
c.AcceptInbound(conn, conf)
|
|
|
|
}(conn)
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
|
|
|
}
|
2019-12-08 00:43:00 +00:00
|
|
|
}
|
|
|
|
|
2021-01-10 23:06:06 +00:00
|
|
|
// Handles an inbound connection to the instance and determines if we should serve the
|
|
|
|
// request or not.
|
2021-01-26 04:28:24 +00:00
|
|
|
func (c *SFTPServer) AcceptInbound(conn net.Conn, config *ssh.ServerConfig) {
|
2020-09-01 03:14:04 +00:00
|
|
|
// Before beginning a handshake must be performed on the incoming net.Conn
|
|
|
|
sconn, chans, reqs, err := ssh.NewServerConn(conn, config)
|
|
|
|
if err != nil {
|
|
|
|
return
|
2019-12-08 01:35:45 +00:00
|
|
|
}
|
2020-09-01 03:14:04 +00:00
|
|
|
defer sconn.Close()
|
|
|
|
go ssh.DiscardRequests(reqs)
|
2019-12-08 00:43:00 +00:00
|
|
|
|
2021-01-10 23:06:06 +00:00
|
|
|
for ch := range chans {
|
2020-09-01 03:14:04 +00:00
|
|
|
// If its not a session channel we just move on because its not something we
|
|
|
|
// know how to handle at this point.
|
2021-01-10 23:06:06 +00:00
|
|
|
if ch.ChannelType() != "session" {
|
|
|
|
ch.Reject(ssh.UnknownChannelType, "unknown channel type")
|
2020-09-01 03:14:04 +00:00
|
|
|
continue
|
|
|
|
}
|
2020-07-03 04:03:11 +00:00
|
|
|
|
2021-01-10 23:06:06 +00:00
|
|
|
channel, requests, err := ch.Accept()
|
2020-09-01 03:14:04 +00:00
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
go func(in <-chan *ssh.Request) {
|
|
|
|
for req := range in {
|
2021-01-10 23:06:06 +00:00
|
|
|
// Channels have a type that is dependent on the protocol. For SFTP
|
|
|
|
// this is "subsystem" with a payload that (should) be "sftp". Discard
|
|
|
|
// anything else we receive ("pty", "shell", etc)
|
|
|
|
req.Reply(req.Type == "subsystem" && string(req.Payload[4:]) == "sftp", nil)
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
|
|
|
}(requests)
|
|
|
|
|
2021-01-10 23:06:06 +00:00
|
|
|
// If no UUID has been set on this inbound request then we can assume we
|
|
|
|
// have screwed up something in the authentication code. This is a sanity
|
|
|
|
// check, but should never be encountered (ideally...).
|
|
|
|
//
|
|
|
|
// This will also attempt to match a specific server out of the global server
|
|
|
|
// store and return nil if there is no match.
|
|
|
|
uuid := sconn.Permissions.Extensions["uuid"]
|
2021-01-26 04:28:24 +00:00
|
|
|
srv := c.manager.Find(func(s *server.Server) bool {
|
2021-01-10 23:06:06 +00:00
|
|
|
if uuid == "" {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return s.Id() == uuid
|
|
|
|
})
|
|
|
|
if srv == nil {
|
2020-09-01 03:14:04 +00:00
|
|
|
continue
|
|
|
|
}
|
2020-07-03 04:03:11 +00:00
|
|
|
|
2021-01-10 23:06:06 +00:00
|
|
|
// Spin up a SFTP server instance for the authenticated user's server allowing
|
|
|
|
// them access to the underlying filesystem.
|
|
|
|
handler := sftp.NewRequestServer(channel, NewHandler(sconn, srv.Filesystem()).Handlers())
|
2021-01-10 22:25:39 +00:00
|
|
|
if err := handler.Serve(); err == io.EOF {
|
|
|
|
handler.Close()
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
2020-07-03 04:03:11 +00:00
|
|
|
}
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
2020-07-03 04:03:11 +00:00
|
|
|
|
2020-09-01 03:14:04 +00:00
|
|
|
// Generates a private key that will be used by the SFTP server.
|
2021-01-10 22:43:27 +00:00
|
|
|
func (c *SFTPServer) generatePrivateKey() error {
|
2020-09-01 03:14:04 +00:00
|
|
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
2019-12-08 01:35:45 +00:00
|
|
|
if err != nil {
|
2021-01-10 23:06:06 +00:00
|
|
|
return errors.WithStack(err)
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
2021-01-10 22:43:27 +00:00
|
|
|
if err := os.MkdirAll(path.Join(c.BasePath, ".sftp"), 0755); err != nil {
|
2021-01-10 23:06:06 +00:00
|
|
|
return errors.Wrap(err, "sftp/server: could not create .sftp directory")
|
2019-12-08 01:35:45 +00:00
|
|
|
}
|
2021-01-10 22:43:27 +00:00
|
|
|
o, err := os.OpenFile(path.Join(c.BasePath, ".sftp/id_rsa"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
2020-09-01 03:14:04 +00:00
|
|
|
if err != nil {
|
2021-01-10 23:06:06 +00:00
|
|
|
return errors.WithStack(err)
|
2020-09-01 03:14:04 +00:00
|
|
|
}
|
|
|
|
defer o.Close()
|
2019-12-08 01:35:45 +00:00
|
|
|
|
2021-01-10 23:06:06 +00:00
|
|
|
err = pem.Encode(o, &pem.Block{
|
2020-09-01 03:14:04 +00:00
|
|
|
Type: "RSA PRIVATE KEY",
|
|
|
|
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
2021-01-10 23:06:06 +00:00
|
|
|
})
|
|
|
|
return errors.WithStack(err)
|
2019-12-07 22:01:40 +00:00
|
|
|
}
|
2021-01-10 22:43:27 +00:00
|
|
|
|
|
|
|
// A function capable of validating user credentials with the Panel API.
|
|
|
|
func (c *SFTPServer) passwordCallback(conn ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
|
2021-02-02 04:59:17 +00:00
|
|
|
request := remote.SftpAuthRequest{
|
2021-01-10 22:43:27 +00:00
|
|
|
User: conn.User(),
|
|
|
|
Pass: string(pass),
|
|
|
|
IP: conn.RemoteAddr().String(),
|
|
|
|
SessionID: conn.SessionID(),
|
|
|
|
ClientVersion: conn.ClientVersion(),
|
|
|
|
}
|
|
|
|
|
|
|
|
logger := log.WithFields(log.Fields{"subsystem": "sftp", "username": conn.User(), "ip": conn.RemoteAddr().String()})
|
|
|
|
logger.Debug("validating credentials for SFTP connection")
|
|
|
|
|
2021-02-02 04:59:17 +00:00
|
|
|
if !validUsernameRegexp.MatchString(request.User) {
|
|
|
|
logger.Warn("failed to validate user credentials (invalid format)")
|
|
|
|
return nil, &remote.SftpInvalidCredentialsError{}
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := c.manager.Client().ValidateSftpCredentials(context.Background(), request)
|
2021-01-10 22:43:27 +00:00
|
|
|
if err != nil {
|
2021-02-02 05:32:34 +00:00
|
|
|
if _, ok := err.(*remote.SftpInvalidCredentialsError); ok {
|
2021-01-10 22:43:27 +00:00
|
|
|
logger.Warn("failed to validate user credentials (invalid username or password)")
|
|
|
|
} else {
|
2021-01-10 23:12:13 +00:00
|
|
|
logger.WithField("error", err).Error("encountered an error while trying to validate user credentials")
|
2021-01-10 22:43:27 +00:00
|
|
|
}
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.WithField("server", resp.Server).Debug("credentials validated and matched to server instance")
|
|
|
|
sshPerm := &ssh.Permissions{
|
|
|
|
Extensions: map[string]string{
|
|
|
|
"uuid": resp.Server,
|
|
|
|
"user": conn.User(),
|
|
|
|
"permissions": strings.Join(resp.Permissions, ","),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
return sshPerm, nil
|
|
|
|
}
|