From 865c1b3baddc70500068b920382bbc79dd1b95af Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 7 Dec 2019 15:53:07 -0800 Subject: [PATCH] Begin implementing SFTP server code --- api/api.go | 42 +++++++++++++++++++++++++++++++++++------ api/server_endpoints.go | 6 +++--- api/sftp_endpoints.go | 39 ++++++++++++++++++++++++++++++++++++++ go.mod | 4 +++- go.sum | 1 + sftp/server.go | 40 ++++++++++++++++++++++++++++++--------- wings.go | 6 +----- 7 files changed, 114 insertions(+), 24 deletions(-) create mode 100644 api/sftp_endpoints.go diff --git a/api/api.go b/api/api.go index b82c546..1843f2b 100644 --- a/api/api.go +++ b/api/api.go @@ -24,6 +24,12 @@ type PanelRequest struct { Response *http.Response } +func IsRequestError (err error) bool { + _, ok := err.(*PanelRequest) + + return ok +} + // Builds the base request instance that can be used with the HTTP client. func (r *PanelRequest) GetClient() *http.Client { return &http.Client{Timeout: time.Second * 30} @@ -60,6 +66,21 @@ func (r *PanelRequest) Get(url string) (*http.Response, error) { return c.Do(req) } +func (r *PanelRequest) Post(url string, data []byte) (*http.Response, error) { + c := r.GetClient() + + req, err := http.NewRequest(http.MethodPost, r.GetEndpoint(url), bytes.NewBuffer(data)) + req = r.SetHeaders(req) + + if err != nil { + return nil, err + } + + zap.S().Debugw("POST request to endpoint", zap.String("endpoint", r.GetEndpoint(url)), zap.Any("headers", req.Header)) + + return c.Do(req) +} + // Determines if the API call encountered an error. If no request has been made // the response will be false. func (r *PanelRequest) HasError() bool { @@ -67,7 +88,7 @@ func (r *PanelRequest) HasError() bool { return false } - return r.Response.StatusCode >= 300 || r.Response.StatusCode < 200; + return r.Response.StatusCode >= 300 || r.Response.StatusCode < 200 } // Reads the body from the response and returns it, then replaces it on the response @@ -86,29 +107,38 @@ func (r *PanelRequest) ReadBody() ([]byte, error) { return b, nil } + +func (r *PanelRequest) HttpResponseCode() int { + if r.Response == nil { + return 0 + } + + return r.Response.StatusCode +} + // Returns the error message from the API call as a string. The error message will be formatted // similar to the below example: // // HttpNotFoundException: The requested resource does not exist. (HTTP/404) -func (r *PanelRequest) Error() (string, error) { +func (r *PanelRequest) Error() string { body, err := r.ReadBody() if err != nil { - return "", err + return err.Error() } zap.S().Debugw("got body", zap.ByteString("b", body)) _, valueType, _, err := jsonparser.Get(body, "errors") if err != nil { - return "", err + return err.Error() } if valueType != jsonparser.Object { - return "no error object present on response", nil + return "no error object present on response" } code, _ := jsonparser.GetString(body, "errors.0.code") status, _ := jsonparser.GetString(body, "errors.0.status") detail, _ := jsonparser.GetString(body, "errors.0.detail") - return fmt.Sprintf("%s: %s (HTTP/%s)", code, detail, status), nil + return fmt.Sprintf("%s: %s (HTTP/%s)", code, detail, status) } \ No newline at end of file diff --git a/api/server_endpoints.go b/api/server_endpoints.go index 02c8a58..886ef70 100644 --- a/api/server_endpoints.go +++ b/api/server_endpoints.go @@ -3,6 +3,7 @@ package api import ( "encoding/json" "fmt" + "github.com/pkg/errors" "github.com/pterodactyl/wings/parser" "go.uber.org/zap" ) @@ -39,10 +40,9 @@ func (r *PanelRequest) GetServerConfiguration(uuid string) (*ServerConfiguration r.Response = resp if r.HasError() { - e, err := r.Error() - zap.S().Warnw("got error", zap.String("message", e), zap.Error(err)) + zap.S().Warnw("got error", zap.String("message", r.Error())) - return nil, err + return nil, errors.WithStack(errors.New(r.Error())) } res := &ServerConfiguration{} diff --git a/api/sftp_endpoints.go b/api/sftp_endpoints.go new file mode 100644 index 0000000..2304601 --- /dev/null +++ b/api/sftp_endpoints.go @@ -0,0 +1,39 @@ +package api + +import ( + "encoding/json" + "github.com/pkg/errors" + "github.com/pterodactyl/sftp-server" +) + +func (r *PanelRequest) ValidateSftpCredentials(request sftp_server.AuthenticationRequest) (*sftp_server.AuthenticationResponse, error) { + b, err := json.Marshal(request) + if err != nil { + return nil, err + } + + resp, err := r.Post("/sftp/auth/login", b) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + r.Response = resp + + if r.HasError() { + if r.HttpResponseCode() == 403 { + return nil, sftp_server.InvalidCredentialsError{} + } + + return nil, errors.WithStack(errors.New(r.Error())) + } + + response := new(sftp_server.AuthenticationResponse) + body, _ := r.ReadBody() + + if err := json.Unmarshal(body, response); err != nil { + return nil, err + } + + return response, nil +} \ No newline at end of file diff --git a/go.mod b/go.mod index 00f74c7..44db38a 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module github.com/pterodactyl/wings go 1.12 +replace github.com/pterodactyl/sftp-server => ../sftp-server + require ( github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect github.com/Jeffail/gabs/v2 v2.2.0 @@ -41,7 +43,7 @@ require ( github.com/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce github.com/sirupsen/logrus v1.0.5 // indirect go.uber.org/zap v1.9.1 - golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 // indirect + golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac // indirect golang.org/x/net v0.0.0-20190923162816-aa69164e4478 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect diff --git a/go.sum b/go.sum index f07dc29..8c2ecf1 100644 --- a/go.sum +++ b/go.sum @@ -65,6 +65,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magefile/mage v1.9.0 h1:t3AU2wNwehMCW97vuqQLtw6puppWXHO+O2MHo5a50XE= github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= diff --git a/sftp/server.go b/sftp/server.go index b51a707..3f842c1 100644 --- a/sftp/server.go +++ b/sftp/server.go @@ -1,22 +1,21 @@ package sftp import ( - "github.com/patrickmn/go-cache" - sftpserver "github.com/pterodactyl/sftp-server/src/server" + "github.com/pkg/errors" + "github.com/pterodactyl/sftp-server" + "github.com/pterodactyl/wings/api" "github.com/pterodactyl/wings/config" + "go.uber.org/zap" "path" - "time" ) func Initialize(config *config.Configuration) error { - c := sftpserver.Configuration{ - Data: []byte("{}"), - Cache: cache.New(5*time.Minute, 10*time.Minute), - User: sftpserver.SftpUser{ + c := &sftp_server.Server{ + User: sftp_server.SftpUser{ Uid: config.System.User.Uid, Gid: config.System.User.Gid, }, - Settings: sftpserver.Settings{ + Settings: sftp_server.Settings{ BasePath: config.System.Data, ReadOnly: config.System.Sftp.ReadOnly, BindAddress: config.System.Sftp.Address, @@ -24,7 +23,30 @@ func Initialize(config *config.Configuration) error { ServerDataFolder: path.Join(config.System.Data, "/servers"), DisableDiskCheck: config.System.Sftp.DisableDiskChecking, }, + CredentialValidator: validateCredentials, } - return c.Initalize() + if err := sftp_server.New(c); err != nil { + return err + } + + c.ConfigureLogger(func() *zap.SugaredLogger { + return zap.S().Named("sftp") + }) + + // Initialize the SFTP server in a background thread since this is + // a long running operation. + go func(instance *sftp_server.Server) { + if err := c.Initalize(); err != nil { + zap.S().Named("sftp").Errorw("failed to initialize SFTP subsystem", zap.Error(errors.WithStack(err))) + } + }(c) + + return nil +} + +// Validates a set of credentials for a SFTP login aganist Pterodactyl Panel and returns +// the server's UUID if the credentials were valid. +func validateCredentials(c sftp_server.AuthenticationRequest) (*sftp_server.AuthenticationResponse, error) { + return api.NewRequester().ValidateSftpCredentials(c) } diff --git a/wings.go b/wings.go index eaa5594..afcda6b 100644 --- a/wings.go +++ b/wings.go @@ -12,7 +12,6 @@ import ( "github.com/remeh/sizedwaitgroup" "go.uber.org/zap" "net/http" - "os" ) // Entrypoint for the Wings application. Configures the logger and checks any @@ -141,10 +140,7 @@ func main() { // If the SFTP subsystem should be started, do so now. if c.System.Sftp.UseInternalSystem { - if err := sftp.Initialize(c); err != nil { - zap.S().Fatalw("failed to initialize SFTP subsystem", zap.Error(errors.WithStack(err))) - os.Exit(1) - } + sftp.Initialize(c) } r := &Router{