Refactor HTTP endpoints to be less complicated and follow better standards

This commit is contained in:
Dane Everitt
2020-10-31 10:04:20 -07:00
parent c4703f5541
commit 334b3e8d10
12 changed files with 203 additions and 245 deletions

View File

@@ -7,6 +7,8 @@ import (
"github.com/apex/log"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/system"
"io"
"io/ioutil"
"net/http"
"strings"
@@ -14,30 +16,34 @@ import (
)
// Initializes the requester instance.
func NewRequester() *PanelRequest {
return &PanelRequest{
Response: nil,
}
func New() *Request {
return &Request{}
}
type PanelRequest struct {
Response *http.Response
// A generic type allowing for easy binding use when making requests to API endpoints
// that only expect a singular argument or something that would not benefit from being
// a typed struct.
//
// Inspired by gin.H, same concept.
type D map[string]interface{}
// A custom API requester struct for Wings.
type Request struct{}
// A custom response type that allows for commonly used error handling and response
// parsing from the Panel API. This just embeds the normal HTTP response from Go and
// we attach a few helper functions to it.
type Response struct {
*http.Response
}
// Builds the base request instance that can be used with the HTTP client.
func (r *PanelRequest) GetClient() *http.Client {
func (r *Request) Client() *http.Client {
return &http.Client{Timeout: time.Second * 30}
}
func (r *PanelRequest) SetHeaders(req *http.Request) *http.Request {
req.Header.Set("Accept", "application/vnd.pterodactyl.v1+json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s.%s", config.Get().AuthenticationTokenId, config.Get().AuthenticationToken))
return req
}
func (r *PanelRequest) GetEndpoint(endpoint string) string {
// Returns the given endpoint formatted as a URL to the Panel API.
func (r *Request) Endpoint(endpoint string) string {
return fmt.Sprintf(
"%s/api/remote/%s",
strings.TrimSuffix(config.Get().PanelLocation, "/"),
@@ -45,9 +51,29 @@ func (r *PanelRequest) GetEndpoint(endpoint string) string {
)
}
// Makes a HTTP request to the given endpoint, attaching the necessary request headers from
// Wings to ensure that the request is properly handled by the Panel.
func (r *Request) Make(method, url string, body io.Reader) (*Response, error) {
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, errors.WithStack(err)
}
req.Header.Set("User-Agent", fmt.Sprintf("Pterodactyl Wings/v%s (id:%s)", system.Version, config.Get().AuthenticationTokenId))
req.Header.Set("Accept", "application/vnd.pterodactyl.v1+json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s.%s", config.Get().AuthenticationTokenId, config.Get().AuthenticationToken))
r.debug(req)
res, err := r.Client().Do(req)
return &Response{Response: res}, err
}
// Logs the request into the debug log with all of the important request bits.
// The authorization key will be cleaned up before being output.
func (r *PanelRequest) logDebug(req *http.Request) {
func (r *Request) debug(req *http.Request) {
headers := make(map[string][]string)
for k, v := range req.Header {
if k != "Authorization" || len(v) == 0 {
@@ -65,49 +91,42 @@ func (r *PanelRequest) logDebug(req *http.Request) {
}).Debug("making request to external HTTP endpoint")
}
func (r *PanelRequest) Get(url string) (*http.Response, error) {
c := r.GetClient()
req, err := http.NewRequest(http.MethodGet, r.GetEndpoint(url), nil)
req = r.SetHeaders(req)
// Makes a GET request to the given Panel API endpoint. If any data is passed as the
// second argument it will be passed through on the request as URL parameters.
func (r *Request) Get(url string, data interface{}) (*Response, error) {
b, err := json.Marshal(data)
if err != nil {
return nil, err
return nil, errors.WithStack(err)
}
r.logDebug(req)
return c.Do(req)
return r.Make(http.MethodGet, r.Endpoint(url), bytes.NewBuffer(b))
}
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)
// Makes a POST request to the given Panel API endpoint.
func (r *Request) Post(url string, data interface{}) (*Response, error) {
b, err := json.Marshal(data)
if err != nil {
return nil, err
return nil, errors.WithStack(err)
}
r.logDebug(req)
return c.Do(req)
return r.Make(http.MethodPost, r.Endpoint(url), bytes.NewBuffer(b))
}
// Determines if the API call encountered an error. If no request has been made
// the response will be false.
func (r *PanelRequest) HasError() bool {
// the response will be false. This function will evaluate to true if the response
// code is anything 300 or higher.
func (r *Response) HasError() bool {
if r.Response == nil {
return false
}
return r.Response.StatusCode >= 300 || r.Response.StatusCode < 200
return r.StatusCode >= 300 || r.StatusCode < 200
}
// Reads the body from the response and returns it, then replaces it on the response
// so that it can be read again later.
func (r *PanelRequest) ReadBody() ([]byte, error) {
// so that it can be read again later. This does not close the response body, so any
// functions calling this should be sure to manually defer a Body.Close() call.
func (r *Response) Read() ([]byte, error) {
var b []byte
if r.Response == nil {
return nil, errors.New("no response exists on interface")
@@ -122,49 +141,28 @@ func (r *PanelRequest) ReadBody() ([]byte, error) {
return b, nil
}
func (r *PanelRequest) HttpResponseCode() int {
if r.Response == nil {
return 0
// Binds a given interface with the data returned in the response. This is a shortcut
// for calling Read and then manually calling json.Unmarshal on the raw bytes.
func (r *Response) Bind(v interface{}) error {
b, err := r.Read()
if err != nil {
return errors.WithStack(err)
}
return r.Response.StatusCode
}
func IsRequestError(err error) bool {
_, ok := err.(*RequestError)
return ok
}
type RequestError struct {
response *http.Response
Code string `json:"code"`
Status string `json:"status"`
Detail string `json:"detail"`
}
// Returns the error response in a string form that can be more easily consumed.
func (re *RequestError) Error() string {
return fmt.Sprintf("Error response from Panel: %s: %s (HTTP/%d)", re.Code, re.Detail, re.response.StatusCode)
}
func (re *RequestError) String() string {
return re.Error()
}
type RequestErrorBag struct {
Errors []RequestError `json:"errors"`
return errors.WithStack(json.Unmarshal(b, &v))
}
// 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() *RequestError {
body, _ := r.ReadBody()
func (r *Response) Error() *RequestError {
if !r.HasError() {
return nil
}
bag := RequestErrorBag{}
json.Unmarshal(body, &bag)
var bag RequestErrorBag
_ = r.Bind(&bag)
e := new(RequestError)
if len(bag.Errors) > 0 {

View File

@@ -15,22 +15,17 @@ type BackupRequest struct {
// Notifies the panel that a specific backup has been completed and is now
// available for a user to view and download.
func (r *PanelRequest) SendBackupStatus(backup string, data BackupRequest) (*RequestError, error) {
func (r *Request) SendBackupStatus(backup string, data BackupRequest) error {
b, err := json.Marshal(data)
if err != nil {
return nil, errors.WithStack(err)
return errors.WithStack(err)
}
resp, err := r.Post(fmt.Sprintf("/backups/%s", backup), b)
if err != nil {
return nil, errors.WithStack(err)
return errors.WithStack(err)
}
defer resp.Body.Close()
r.Response = resp
if r.HasError() {
return r.Error(), nil
}
return nil, nil
return resp.Error()
}

28
api/error.go Normal file
View File

@@ -0,0 +1,28 @@
package api
import (
"fmt"
"net/http"
)
type RequestErrorBag struct {
Errors []RequestError `json:"errors"`
}
type RequestError struct {
response *http.Response
Code string `json:"code"`
Status string `json:"status"`
Detail string `json:"detail"`
}
func IsRequestError(err error) bool {
_, ok := err.(*RequestError)
return ok
}
// Returns the error response in a string form that can be more easily consumed.
func (re *RequestError) Error() string {
return fmt.Sprintf("Error response from Panel: %s: %s (HTTP/%d)", re.Code, re.Detail, re.response.StatusCode)
}

View File

@@ -34,155 +34,108 @@ type InstallationScript struct {
}
// GetAllServerConfigurations fetches configurations for all servers assigned to this node.
func (r *PanelRequest) GetAllServerConfigurations() (map[string]json.RawMessage, *RequestError, error) {
resp, err := r.Get("/servers")
func (r *Request) GetAllServerConfigurations() (map[string]json.RawMessage, error) {
resp, err := r.Get("/servers", nil)
if err != nil {
return nil, nil, errors.WithStack(err)
return nil, errors.WithStack(err)
}
defer resp.Body.Close()
r.Response = resp
if r.HasError() {
return nil, r.Error(), nil
if resp.HasError() {
return nil, resp.Error()
}
b, _ := r.ReadBody()
res := map[string]json.RawMessage{}
if len(b) == 2 {
return res, nil, nil
var res map[string]json.RawMessage
if err := resp.Bind(&res); err != nil {
return nil, errors.WithStack(err)
}
if err := json.Unmarshal(b, &res); err != nil {
return nil, nil, errors.WithStack(err)
}
return res, nil, nil
return res, nil
}
// Fetches the server configuration and returns the struct for it.
func (r *PanelRequest) GetServerConfiguration(uuid string) (ServerConfigurationResponse, *RequestError, error) {
res := ServerConfigurationResponse{}
func (r *Request) GetServerConfiguration(uuid string) (ServerConfigurationResponse, error) {
var cfg ServerConfigurationResponse
resp, err := r.Get(fmt.Sprintf("/servers/%s", uuid))
resp, err := r.Get(fmt.Sprintf("/servers/%s", uuid), nil)
if err != nil {
return res, nil, errors.WithStack(err)
return cfg, errors.WithStack(err)
}
defer resp.Body.Close()
r.Response = resp
if r.HasError() {
return res, r.Error(), nil
if resp.HasError() {
return cfg, resp.Error()
}
b, _ := r.ReadBody()
if err := json.Unmarshal(b, &res); err != nil {
return res, nil, errors.WithStack(err)
if err := resp.Bind(&cfg); err != nil {
return cfg, errors.WithStack(err)
}
return res, nil, nil
return cfg, nil
}
// Fetches installation information for the server process.
func (r *PanelRequest) GetInstallationScript(uuid string) (InstallationScript, *RequestError, error) {
res := InstallationScript{}
resp, err := r.Get(fmt.Sprintf("/servers/%s/install", uuid))
func (r *Request) GetInstallationScript(uuid string) (InstallationScript, error) {
var is InstallationScript
resp, err := r.Get(fmt.Sprintf("/servers/%s/install", uuid), nil)
if err != nil {
return res, nil, errors.WithStack(err)
return is, errors.WithStack(err)
}
defer resp.Body.Close()
r.Response = resp
if r.HasError() {
return res, r.Error(), nil
if resp.HasError() {
return is, resp.Error()
}
b, _ := r.ReadBody()
if err := json.Unmarshal(b, &res); err != nil {
return res, nil, errors.WithStack(err)
if err := resp.Bind(&is); err != nil {
return is, errors.WithStack(err)
}
return res, nil, nil
}
type installRequest struct {
Successful bool `json:"successful"`
return is, nil
}
// Marks a server as being installed successfully or unsuccessfully on the panel.
func (r *PanelRequest) SendInstallationStatus(uuid string, successful bool) (*RequestError, error) {
b, err := json.Marshal(installRequest{Successful: successful})
func (r *Request) SendInstallationStatus(uuid string, successful bool) error {
resp, err := r.Post(fmt.Sprintf("/servers/%s/install", uuid), D{"successful": successful})
if err != nil {
return nil, errors.WithStack(err)
}
resp, err := r.Post(fmt.Sprintf("/servers/%s/install", uuid), b)
if err != nil {
return nil, errors.WithStack(err)
return errors.WithStack(err)
}
defer resp.Body.Close()
r.Response = resp
if r.HasError() {
return r.Error(), nil
if resp.HasError() {
return resp.Error()
}
return nil, nil
return nil
}
type archiveRequest struct {
Successful bool `json:"successful"`
}
func (r *PanelRequest) SendArchiveStatus(uuid string, successful bool) (*RequestError, error) {
b, err := json.Marshal(archiveRequest{Successful: successful})
func (r *Request) SendArchiveStatus(uuid string, successful bool) error {
resp, err := r.Post(fmt.Sprintf("/servers/%s/archive", uuid), D{"successful": successful})
if err != nil {
return nil, errors.WithStack(err)
}
resp, err := r.Post(fmt.Sprintf("/servers/%s/archive", uuid), b)
if err != nil {
return nil, errors.WithStack(err)
return errors.WithStack(err)
}
defer resp.Body.Close()
r.Response = resp
if r.HasError() {
return r.Error(), nil
}
return nil, nil
return resp.Error()
}
func (r *PanelRequest) SendTransferFailure(uuid string) (*RequestError, error) {
resp, err := r.Get(fmt.Sprintf("/servers/%s/transfer/failure", uuid))
func (r *Request) SendTransferFailure(uuid string) error {
resp, err := r.Get(fmt.Sprintf("/servers/%s/transfer/failure", uuid), nil)
if err != nil {
return nil, errors.WithStack(err)
return errors.WithStack(err)
}
defer resp.Body.Close()
r.Response = resp
if r.HasError() {
return r.Error(), nil
}
return nil, nil
return resp.Error()
}
func (r *PanelRequest) SendTransferSuccess(uuid string) (*RequestError, error) {
resp, err := r.Get(fmt.Sprintf("/servers/%s/transfer/success", uuid))
func (r *Request) SendTransferSuccess(uuid string) error {
resp, err := r.Get(fmt.Sprintf("/servers/%s/transfer/success", uuid), nil)
if err != nil {
return nil, errors.WithStack(err)
return errors.WithStack(err)
}
defer resp.Body.Close()
r.Response = resp
if r.HasError() {
return r.Error(), nil
}
return nil, nil
return resp.Error()
}

View File

@@ -1,7 +1,6 @@
package api
import (
"encoding/json"
"github.com/apex/log"
"github.com/pkg/errors"
"regexp"
@@ -39,7 +38,7 @@ func IsInvalidCredentialsError(err error) bool {
// server and sending a flood of usernames.
var validUsernameRegexp = regexp.MustCompile(`^(?i)(.+)\.([a-z0-9]{8})$`)
func (r *PanelRequest) ValidateSftpCredentials(request SftpAuthRequest) (*SftpAuthResponse, error) {
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.
@@ -53,41 +52,33 @@ func (r *PanelRequest) ValidateSftpCredentials(request SftpAuthRequest) (*SftpAu
return nil, new(sftpInvalidCredentialsError)
}
b, err := json.Marshal(request)
if err != nil {
return nil, err
}
resp, err := r.Post("/sftp/auth", b)
resp, err := r.Post("/sftp/auth", request)
if err != nil {
return nil, err
}
defer resp.Body.Close()
r.Response = resp
if r.HasError() {
if r.HttpResponseCode() >= 400 && r.HttpResponseCode() < 500 {
e := resp.Error()
if e != nil {
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
log.WithFields(log.Fields{
"subsystem": "sftp",
"username": request.User,
"ip": request.IP,
}).Warn(r.Error().String())
}).Warn(e.Error())
return nil, new(sftpInvalidCredentialsError)
return nil, &sftpInvalidCredentialsError{}
}
rerr := errors.New(r.Error().String())
rerr := errors.New(e.Error())
return nil, rerr
}
response := new(SftpAuthResponse)
body, _ := r.ReadBody()
if err := json.Unmarshal(body, response); err != nil {
var response SftpAuthResponse
if err := resp.Bind(&response); err != nil {
return nil, err
}
return response, nil
return &response, nil
}