2019-09-23 03:47:38 +00:00
|
|
|
package api
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2019-12-17 04:34:58 +00:00
|
|
|
"encoding/json"
|
2019-09-23 03:47:38 +00:00
|
|
|
"fmt"
|
2020-05-29 05:07:53 +00:00
|
|
|
"github.com/apex/log"
|
2019-11-17 01:01:38 +00:00
|
|
|
"github.com/pkg/errors"
|
2019-09-23 03:47:38 +00:00
|
|
|
"github.com/pterodactyl/wings/config"
|
2020-10-31 17:04:20 +00:00
|
|
|
"github.com/pterodactyl/wings/system"
|
|
|
|
"io"
|
2019-09-23 03:47:38 +00:00
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
2020-04-10 21:39:07 +00:00
|
|
|
// Initializes the requester instance.
|
2020-10-31 17:04:20 +00:00
|
|
|
func New() *Request {
|
|
|
|
return &Request{}
|
2019-09-23 03:47:38 +00:00
|
|
|
}
|
|
|
|
|
2020-10-31 17:04:20 +00:00
|
|
|
// 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{}
|
|
|
|
|
2020-11-01 22:04:57 +00:00
|
|
|
// Same concept as D, but a map of strings, used for querying GET requests.
|
|
|
|
type Q map[string]string
|
|
|
|
|
2020-10-31 17:04:20 +00:00
|
|
|
// 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
|
2019-09-23 03:47:38 +00:00
|
|
|
}
|
|
|
|
|
2020-10-31 18:05:53 +00:00
|
|
|
// A pagination struct matching the expected pagination response from the Panel API.
|
|
|
|
type Pagination struct {
|
|
|
|
CurrentPage uint `json:"current_page"`
|
|
|
|
From uint `json:"from"`
|
|
|
|
LastPage uint `json:"last_page"`
|
|
|
|
PerPage uint `json:"per_page"`
|
|
|
|
To uint `json:"to"`
|
|
|
|
Total uint `json:"total"`
|
|
|
|
}
|
|
|
|
|
2019-09-23 03:47:38 +00:00
|
|
|
// Builds the base request instance that can be used with the HTTP client.
|
2020-10-31 17:04:20 +00:00
|
|
|
func (r *Request) Client() *http.Client {
|
2020-10-31 18:05:53 +00:00
|
|
|
return &http.Client{Timeout: time.Second * time.Duration(config.Get().RemoteQuery.Timeout)}
|
2019-09-23 03:47:38 +00:00
|
|
|
}
|
|
|
|
|
2020-10-31 17:04:20 +00:00
|
|
|
// Returns the given endpoint formatted as a URL to the Panel API.
|
|
|
|
func (r *Request) Endpoint(endpoint string) string {
|
2019-09-23 03:47:38 +00:00
|
|
|
return fmt.Sprintf(
|
|
|
|
"%s/api/remote/%s",
|
|
|
|
strings.TrimSuffix(config.Get().PanelLocation, "/"),
|
|
|
|
strings.TrimPrefix(strings.TrimPrefix(endpoint, "/"), "api/remote/"),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2020-10-31 17:04:20 +00:00
|
|
|
// 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.
|
2020-11-01 22:04:57 +00:00
|
|
|
func (r *Request) Make(method, url string, body io.Reader, opts ...func(r *http.Request)) (*Response, error) {
|
2020-10-31 17:04:20 +00:00
|
|
|
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))
|
|
|
|
|
2020-11-01 22:04:57 +00:00
|
|
|
// Make any options calls that will allow us to make modifications to the request
|
|
|
|
// before it is sent off.
|
|
|
|
for _, cb := range opts {
|
|
|
|
cb(req)
|
|
|
|
}
|
|
|
|
|
2020-10-31 17:04:20 +00:00
|
|
|
r.debug(req)
|
|
|
|
|
|
|
|
res, err := r.Client().Do(req)
|
|
|
|
|
|
|
|
return &Response{Response: res}, err
|
|
|
|
}
|
|
|
|
|
2020-05-17 22:28:04 +00:00
|
|
|
// Logs the request into the debug log with all of the important request bits.
|
|
|
|
// The authorization key will be cleaned up before being output.
|
2020-10-31 17:04:20 +00:00
|
|
|
func (r *Request) debug(req *http.Request) {
|
2020-05-17 22:28:04 +00:00
|
|
|
headers := make(map[string][]string)
|
|
|
|
for k, v := range req.Header {
|
|
|
|
if k != "Authorization" || len(v) == 0 {
|
|
|
|
headers[k] = v
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
headers[k] = []string{v[0][0:15] + "(redacted)"}
|
|
|
|
}
|
|
|
|
|
2020-05-29 05:07:53 +00:00
|
|
|
log.WithFields(log.Fields{
|
2020-09-05 19:08:40 +00:00
|
|
|
"method": req.Method,
|
2020-05-29 05:07:53 +00:00
|
|
|
"endpoint": req.URL.String(),
|
2020-09-05 19:08:40 +00:00
|
|
|
"headers": headers,
|
2020-05-29 05:07:53 +00:00
|
|
|
}).Debug("making request to external HTTP endpoint")
|
2020-05-17 22:28:04 +00:00
|
|
|
}
|
|
|
|
|
2020-10-31 17:04:20 +00:00
|
|
|
// 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.
|
2020-11-01 22:04:57 +00:00
|
|
|
func (r *Request) Get(url string, data Q) (*Response, error) {
|
|
|
|
return r.Make(http.MethodGet, r.Endpoint(url), nil, func(r *http.Request) {
|
|
|
|
for k, v := range data {
|
|
|
|
r.URL.Query().Set(k, v)
|
|
|
|
}
|
|
|
|
})
|
2019-09-23 03:47:38 +00:00
|
|
|
}
|
|
|
|
|
2020-10-31 17:04:20 +00:00
|
|
|
// 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)
|
2019-12-07 23:53:07 +00:00
|
|
|
if err != nil {
|
2020-10-31 17:04:20 +00:00
|
|
|
return nil, errors.WithStack(err)
|
2019-12-07 23:53:07 +00:00
|
|
|
}
|
|
|
|
|
2020-10-31 17:04:20 +00:00
|
|
|
return r.Make(http.MethodPost, r.Endpoint(url), bytes.NewBuffer(b))
|
2019-12-07 23:53:07 +00:00
|
|
|
}
|
|
|
|
|
2019-09-23 03:47:38 +00:00
|
|
|
// Determines if the API call encountered an error. If no request has been made
|
2020-10-31 17:04:20 +00:00
|
|
|
// 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 {
|
2019-09-23 03:47:38 +00:00
|
|
|
if r.Response == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2020-10-31 17:04:20 +00:00
|
|
|
return r.StatusCode >= 300 || r.StatusCode < 200
|
2019-09-23 03:47:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Reads the body from the response and returns it, then replaces it on the response
|
2020-10-31 17:04:20 +00:00
|
|
|
// 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) {
|
2019-09-23 03:47:38 +00:00
|
|
|
var b []byte
|
|
|
|
if r.Response == nil {
|
|
|
|
return nil, errors.New("no response exists on interface")
|
|
|
|
}
|
|
|
|
|
|
|
|
if r.Response.Body != nil {
|
|
|
|
b, _ = ioutil.ReadAll(r.Response.Body)
|
|
|
|
}
|
|
|
|
|
|
|
|
r.Response.Body = ioutil.NopCloser(bytes.NewBuffer(b))
|
|
|
|
|
|
|
|
return b, nil
|
|
|
|
}
|
2019-12-07 23:53:07 +00:00
|
|
|
|
2020-10-31 17:04:20 +00:00
|
|
|
// 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)
|
2019-12-07 23:53:07 +00:00
|
|
|
}
|
|
|
|
|
2020-10-31 17:04:20 +00:00
|
|
|
return errors.WithStack(json.Unmarshal(b, &v))
|
2019-12-17 04:34:58 +00:00
|
|
|
}
|
|
|
|
|
2019-09-23 03:47:38 +00:00
|
|
|
// 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)
|
2020-11-01 22:04:57 +00:00
|
|
|
func (r *Response) Error() error {
|
2020-10-31 17:04:20 +00:00
|
|
|
if !r.HasError() {
|
|
|
|
return nil
|
|
|
|
}
|
2019-09-23 03:47:38 +00:00
|
|
|
|
2020-10-31 17:04:20 +00:00
|
|
|
var bag RequestErrorBag
|
|
|
|
_ = r.Bind(&bag)
|
2019-09-23 03:47:38 +00:00
|
|
|
|
2020-09-13 20:55:40 +00:00
|
|
|
e := new(RequestError)
|
|
|
|
if len(bag.Errors) > 0 {
|
|
|
|
e = &bag.Errors[0]
|
2019-09-23 03:47:38 +00:00
|
|
|
}
|
|
|
|
|
2020-09-13 20:55:40 +00:00
|
|
|
e.response = r.Response
|
|
|
|
|
|
|
|
return e
|
2019-12-17 04:34:58 +00:00
|
|
|
}
|