Migrate SFTP endpoints
This commit is contained in:
parent
6775c17324
commit
62cbe5e135
|
@ -1,79 +0,0 @@
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"regexp"
|
|
||||||
|
|
||||||
"emperror.dev/errors"
|
|
||||||
"github.com/apex/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SftpAuthRequest struct {
|
|
||||||
User string `json:"username"`
|
|
||||||
Pass string `json:"password"`
|
|
||||||
IP string `json:"ip"`
|
|
||||||
SessionID []byte `json:"session_id"`
|
|
||||||
ClientVersion []byte `json:"client_version"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SftpAuthResponse struct {
|
|
||||||
Server string `json:"server"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
Permissions []string `json:"permissions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type sftpInvalidCredentialsError struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ice sftpInvalidCredentialsError) Error() string {
|
|
||||||
return "the credentials provided were invalid"
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsInvalidCredentialsError(err error) bool {
|
|
||||||
_, ok := err.(*sftpInvalidCredentialsError)
|
|
||||||
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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})$`)
|
|
||||||
|
|
||||||
func (r *Request) ValidateSftpCredentials(request SftpAuthRequest) (*SftpAuthResponse, error) {
|
|
||||||
// If the username doesn't meet the expected format that the Panel would even recognize just go ahead
|
|
||||||
// and bail out of the process here to avoid accidentally brute forcing the panel if a bot decides
|
|
||||||
// to connect to spam username attempts.
|
|
||||||
if !validUsernameRegexp.MatchString(request.User) {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"subsystem": "sftp",
|
|
||||||
"username": request.User,
|
|
||||||
"ip": request.IP,
|
|
||||||
}).Warn("failed to validate user credentials (invalid format)")
|
|
||||||
|
|
||||||
return nil, new(sftpInvalidCredentialsError)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := r.Post("/sftp/auth", request)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
e := resp.Error()
|
|
||||||
if e != nil {
|
|
||||||
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
|
|
||||||
return nil, &sftpInvalidCredentialsError{}
|
|
||||||
}
|
|
||||||
|
|
||||||
rerr := errors.New(e.Error())
|
|
||||||
|
|
||||||
return nil, rerr
|
|
||||||
}
|
|
||||||
|
|
||||||
var response SftpAuthResponse
|
|
||||||
if err := resp.Bind(&response); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &response, nil
|
|
||||||
}
|
|
|
@ -18,7 +18,7 @@ type Client interface {
|
||||||
SetBackupStatus(ctx context.Context, backup string, data api.BackupRequest) error
|
SetBackupStatus(ctx context.Context, backup string, data api.BackupRequest) error
|
||||||
SetInstallationStatus(ctx context.Context, uuid string, successful bool) error
|
SetInstallationStatus(ctx context.Context, uuid string, successful bool) error
|
||||||
SetTransferStatus(ctx context.Context, uuid string, successful bool) error
|
SetTransferStatus(ctx context.Context, uuid string, successful bool) error
|
||||||
ValidateSftpCredentials(ctx context.Context, request api.SftpAuthRequest) (api.SftpAuthResponse, error)
|
ValidateSftpCredentials(ctx context.Context, request SftpAuthRequest) (SftpAuthResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type client struct {
|
type client struct {
|
||||||
|
|
|
@ -32,15 +32,9 @@ func (re *RequestError) Error() string {
|
||||||
return fmt.Sprintf("Error response from Panel: %s: %s (HTTP/%d)", re.Code, re.Detail, c)
|
return fmt.Sprintf("Error response from Panel: %s: %s (HTTP/%d)", re.Code, re.Detail, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
type sftpInvalidCredentialsError struct {
|
type SftpInvalidCredentialsError struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ice sftpInvalidCredentialsError) Error() string {
|
func (ice SftpInvalidCredentialsError) Error() string {
|
||||||
return "the credentials provided were invalid"
|
return "the credentials provided were invalid"
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsInvalidCredentialsError(err error) bool {
|
|
||||||
_, ok := err.(*sftpInvalidCredentialsError)
|
|
||||||
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,30 +3,34 @@ package remote
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"regexp"
|
|
||||||
|
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/pterodactyl/wings/api"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Usernames all follow the same format, so don't even bother hitting the API if the username is not
|
type SftpAuthRequest struct {
|
||||||
// at least in the expected format. This is very basic protection against random bots finding the SFTP
|
User string `json:"username"`
|
||||||
// server and sending a flood of usernames.
|
Pass string `json:"password"`
|
||||||
var validUsernameRegexp = regexp.MustCompile(`^(?i)(.+)\.([a-z0-9]{8})$`)
|
IP string `json:"ip"`
|
||||||
|
SessionID []byte `json:"session_id"`
|
||||||
func (c *client) ValidateSftpCredentials(ctx context.Context, request api.SftpAuthRequest) (api.SftpAuthResponse, error) {
|
ClientVersion []byte `json:"client_version"`
|
||||||
if !validUsernameRegexp.MatchString(request.User) {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"subsystem": "sftp",
|
|
||||||
"username": request.User,
|
|
||||||
"ip": request.IP,
|
|
||||||
}).Warn("failed to validate user credentials (invalid format)")
|
|
||||||
return api.SftpAuthResponse{}, new(sftpInvalidCredentialsError)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SftpAuthResponse struct {
|
||||||
|
Server string `json:"server"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
Permissions []string `json:"permissions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateSftpCredentials makes a request to determine if the username and
|
||||||
|
// password combination provided is associated with a valid server on the instance
|
||||||
|
// using the Panel's authentication control mechanisms. This will get itself
|
||||||
|
// throttled if too many requests are made, allowing us to completely offload
|
||||||
|
// all of the authorization security logic to the Panel.
|
||||||
|
func (c *client) ValidateSftpCredentials(ctx context.Context, request SftpAuthRequest) (SftpAuthResponse, error) {
|
||||||
|
var auth SftpAuthResponse
|
||||||
res, err := c.post(ctx, "/sftp/auth", request)
|
res, err := c.post(ctx, "/sftp/auth", request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return api.SftpAuthResponse{}, err
|
return auth, err
|
||||||
}
|
}
|
||||||
|
|
||||||
e := res.Error()
|
e := res.Error()
|
||||||
|
@ -38,13 +42,12 @@ func (c *client) ValidateSftpCredentials(ctx context.Context, request api.SftpAu
|
||||||
"ip": request.IP,
|
"ip": request.IP,
|
||||||
}).Warn(e.Error())
|
}).Warn(e.Error())
|
||||||
|
|
||||||
return api.SftpAuthResponse{}, &sftpInvalidCredentialsError{}
|
return auth, &SftpInvalidCredentialsError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return api.SftpAuthResponse{}, errors.New(e.Error())
|
return auth, errors.New(e.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
r := api.SftpAuthResponse{}
|
err = res.BindJSON(&auth)
|
||||||
err = res.BindJSON(&r)
|
return auth, err
|
||||||
return r, err
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,54 +46,10 @@ func NewEmptyManager(client remote.Client) *Manager {
|
||||||
return &Manager{client: client}
|
return &Manager{client: client}
|
||||||
}
|
}
|
||||||
|
|
||||||
// initializeFromRemoteSource iterates over a given directory and loads all of
|
// Client returns the HTTP client interface that allows interaction with the
|
||||||
// the servers listed before returning them to the calling function.
|
// Panel API.
|
||||||
func (m *Manager) init(ctx context.Context) error {
|
func (m *Manager) Client() remote.Client {
|
||||||
log.Info("fetching list of servers from API")
|
return m.client
|
||||||
servers, err := m.client.GetServers(ctx, config.Get().RemoteQuery.BootServersPerPage)
|
|
||||||
if err != nil {
|
|
||||||
if !remote.IsRequestError(err) {
|
|
||||||
return errors.WithStackIf(err)
|
|
||||||
}
|
|
||||||
return errors.New(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
start := time.Now()
|
|
||||||
log.WithField("total_configs", len(servers)).Info("processing servers returned by the API")
|
|
||||||
|
|
||||||
pool := workerpool.New(runtime.NumCPU())
|
|
||||||
log.Debugf("using %d workerpools to instantiate server instances", runtime.NumCPU())
|
|
||||||
for _, data := range servers {
|
|
||||||
data := data
|
|
||||||
pool.Submit(func() {
|
|
||||||
// Parse the json.RawMessage into an expected struct value. We do this here so that a single broken
|
|
||||||
// server does not cause the entire boot process to hang, and allows us to show more useful error
|
|
||||||
// messaging in the output.
|
|
||||||
d := remote.ServerConfigurationResponse{
|
|
||||||
Settings: data.Settings,
|
|
||||||
}
|
|
||||||
log.WithField("server", data.Uuid).Info("creating new server object from API response")
|
|
||||||
if err := json.Unmarshal(data.ProcessConfiguration, &d.ProcessConfiguration); err != nil {
|
|
||||||
log.WithField("server", data.Uuid).WithField("error", err).Error("failed to parse server configuration from API response, skipping...")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s, err := m.InitServer(d)
|
|
||||||
if err != nil {
|
|
||||||
log.WithField("server", data.Uuid).WithField("error", err).Error("failed to load server, skipping...")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m.Add(s)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait until we've processed all of the configuration files in the directory
|
|
||||||
// before continuing.
|
|
||||||
pool.StopWait()
|
|
||||||
|
|
||||||
diff := time.Now().Sub(start)
|
|
||||||
log.WithField("duration", fmt.Sprintf("%s", diff)).Info("finished processing server configurations")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put replaces all of the current values in the collection with the value that
|
// Put replaces all of the current values in the collection with the value that
|
||||||
|
@ -256,3 +212,53 @@ func (m *Manager) InitServer(data remote.ServerConfigurationResponse) (*Server,
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initializeFromRemoteSource iterates over a given directory and loads all of
|
||||||
|
// the servers listed before returning them to the calling function.
|
||||||
|
func (m *Manager) init(ctx context.Context) error {
|
||||||
|
log.Info("fetching list of servers from API")
|
||||||
|
servers, err := m.client.GetServers(ctx, config.Get().RemoteQuery.BootServersPerPage)
|
||||||
|
if err != nil {
|
||||||
|
if !remote.IsRequestError(err) {
|
||||||
|
return errors.WithStackIf(err)
|
||||||
|
}
|
||||||
|
return errors.New(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
log.WithField("total_configs", len(servers)).Info("processing servers returned by the API")
|
||||||
|
|
||||||
|
pool := workerpool.New(runtime.NumCPU())
|
||||||
|
log.Debugf("using %d workerpools to instantiate server instances", runtime.NumCPU())
|
||||||
|
for _, data := range servers {
|
||||||
|
data := data
|
||||||
|
pool.Submit(func() {
|
||||||
|
// Parse the json.RawMessage into an expected struct value. We do this here so that a single broken
|
||||||
|
// server does not cause the entire boot process to hang, and allows us to show more useful error
|
||||||
|
// messaging in the output.
|
||||||
|
d := remote.ServerConfigurationResponse{
|
||||||
|
Settings: data.Settings,
|
||||||
|
}
|
||||||
|
log.WithField("server", data.Uuid).Info("creating new server object from API response")
|
||||||
|
if err := json.Unmarshal(data.ProcessConfiguration, &d.ProcessConfiguration); err != nil {
|
||||||
|
log.WithField("server", data.Uuid).WithField("error", err).Error("failed to parse server configuration from API response, skipping...")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s, err := m.InitServer(d)
|
||||||
|
if err != nil {
|
||||||
|
log.WithField("server", data.Uuid).WithField("error", err).Error("failed to load server, skipping...")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.Add(s)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait until we've processed all of the configuration files in the directory
|
||||||
|
// before continuing.
|
||||||
|
pool.StopWait()
|
||||||
|
|
||||||
|
diff := time.Now().Sub(start)
|
||||||
|
log.WithField("duration", fmt.Sprintf("%s", diff)).Info("finished processing server configurations")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package sftp
|
package sftp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
@ -10,6 +11,7 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -18,10 +20,16 @@ import (
|
||||||
"github.com/pkg/sftp"
|
"github.com/pkg/sftp"
|
||||||
"github.com/pterodactyl/wings/api"
|
"github.com/pterodactyl/wings/api"
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"github.com/pterodactyl/wings/remote"
|
||||||
"github.com/pterodactyl/wings/server"
|
"github.com/pterodactyl/wings/server"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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})$`)
|
||||||
|
|
||||||
//goland:noinspection GoNameStartsWithPackageName
|
//goland:noinspection GoNameStartsWithPackageName
|
||||||
type SFTPServer struct {
|
type SFTPServer struct {
|
||||||
manager *server.Manager
|
manager *server.Manager
|
||||||
|
@ -164,7 +172,7 @@ func (c *SFTPServer) generatePrivateKey() error {
|
||||||
|
|
||||||
// A function capable of validating user credentials with the Panel API.
|
// A function capable of validating user credentials with the Panel API.
|
||||||
func (c *SFTPServer) passwordCallback(conn ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
|
func (c *SFTPServer) passwordCallback(conn ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
|
||||||
request := api.SftpAuthRequest{
|
request := remote.SftpAuthRequest{
|
||||||
User: conn.User(),
|
User: conn.User(),
|
||||||
Pass: string(pass),
|
Pass: string(pass),
|
||||||
IP: conn.RemoteAddr().String(),
|
IP: conn.RemoteAddr().String(),
|
||||||
|
@ -175,7 +183,12 @@ func (c *SFTPServer) passwordCallback(conn ssh.ConnMetadata, pass []byte) (*ssh.
|
||||||
logger := log.WithFields(log.Fields{"subsystem": "sftp", "username": conn.User(), "ip": conn.RemoteAddr().String()})
|
logger := log.WithFields(log.Fields{"subsystem": "sftp", "username": conn.User(), "ip": conn.RemoteAddr().String()})
|
||||||
logger.Debug("validating credentials for SFTP connection")
|
logger.Debug("validating credentials for SFTP connection")
|
||||||
|
|
||||||
resp, err := api.New().ValidateSftpCredentials(request)
|
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)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if api.IsInvalidCredentialsError(err) {
|
if api.IsInvalidCredentialsError(err) {
|
||||||
logger.Warn("failed to validate user credentials (invalid username or password)")
|
logger.Warn("failed to validate user credentials (invalid username or password)")
|
||||||
|
|
Loading…
Reference in New Issue
Block a user