diff --git a/api/sftp_endpoints.go b/api/sftp_endpoints.go deleted file mode 100644 index e222749..0000000 --- a/api/sftp_endpoints.go +++ /dev/null @@ -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 -} diff --git a/remote/client.go b/remote/client.go index 7a15ed2..1e0a0d0 100644 --- a/remote/client.go +++ b/remote/client.go @@ -18,7 +18,7 @@ type Client interface { SetBackupStatus(ctx context.Context, backup string, data api.BackupRequest) error SetInstallationStatus(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 { diff --git a/remote/errors.go b/remote/errors.go index b805d76..f204928 100644 --- a/remote/errors.go +++ b/remote/errors.go @@ -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) } -type sftpInvalidCredentialsError struct { +type SftpInvalidCredentialsError struct { } -func (ice sftpInvalidCredentialsError) Error() string { +func (ice SftpInvalidCredentialsError) Error() string { return "the credentials provided were invalid" } - -func IsInvalidCredentialsError(err error) bool { - _, ok := err.(*sftpInvalidCredentialsError) - - return ok -} diff --git a/remote/sftp.go b/remote/sftp.go index 2decbc9..0284d67 100644 --- a/remote/sftp.go +++ b/remote/sftp.go @@ -3,30 +3,34 @@ package remote import ( "context" "errors" - "regexp" "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 -// 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})$`) +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"` +} -func (c *client) ValidateSftpCredentials(ctx context.Context, request api.SftpAuthRequest) (api.SftpAuthResponse, error) { - 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) if err != nil { - return api.SftpAuthResponse{}, err + return auth, err } e := res.Error() @@ -38,13 +42,12 @@ func (c *client) ValidateSftpCredentials(ctx context.Context, request api.SftpAu "ip": request.IP, }).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(&r) - return r, err + err = res.BindJSON(&auth) + return auth, err } diff --git a/server/manager.go b/server/manager.go index 1b59865..6f8d4fa 100644 --- a/server/manager.go +++ b/server/manager.go @@ -46,54 +46,10 @@ func NewEmptyManager(client remote.Client) *Manager { return &Manager{client: client} } -// 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 +// Client returns the HTTP client interface that allows interaction with the +// Panel API. +func (m *Manager) Client() remote.Client { + return m.client } // Put replaces all of the current values in the collection with the value that @@ -255,4 +211,54 @@ func (m *Manager) InitServer(data remote.ServerConfigurationResponse) (*Server, } return s, nil -} \ No newline at end of file +} + +// 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 +} diff --git a/sftp/server.go b/sftp/server.go index d2260f1..6e8b755 100644 --- a/sftp/server.go +++ b/sftp/server.go @@ -1,6 +1,7 @@ package sftp import ( + "context" "crypto/rand" "crypto/rsa" "crypto/x509" @@ -10,6 +11,7 @@ import ( "net" "os" "path" + "regexp" "strconv" "strings" @@ -18,10 +20,16 @@ import ( "github.com/pkg/sftp" "github.com/pterodactyl/wings/api" "github.com/pterodactyl/wings/config" + "github.com/pterodactyl/wings/remote" "github.com/pterodactyl/wings/server" "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 type SFTPServer struct { manager *server.Manager @@ -164,7 +172,7 @@ func (c *SFTPServer) generatePrivateKey() error { // A function capable of validating user credentials with the Panel API. func (c *SFTPServer) passwordCallback(conn ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { - request := api.SftpAuthRequest{ + request := remote.SftpAuthRequest{ User: conn.User(), Pass: string(pass), 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.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 api.IsInvalidCredentialsError(err) { logger.Warn("failed to validate user credentials (invalid username or password)")