add new panelapi package
should eventually replace the api package
This commit is contained in:
parent
217ca72eb3
commit
94f4207d60
34
panelapi/backup.go
Normal file
34
panelapi/backup.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package panelapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/pterodactyl/wings/api"
|
||||
)
|
||||
|
||||
func (c *client) GetBackupRemoteUploadURLs(ctx context.Context, backup string, size int64) (api.BackupRemoteUploadResponse, error) {
|
||||
res, err := c.get(ctx, fmt.Sprintf("/backups/%s", backup), q{"size": strconv.FormatInt(size, 10)})
|
||||
if err != nil {
|
||||
return api.BackupRemoteUploadResponse{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.HasError() {
|
||||
return api.BackupRemoteUploadResponse{}, res.Error()
|
||||
}
|
||||
|
||||
r := api.BackupRemoteUploadResponse{}
|
||||
err = res.BindJSON(&r)
|
||||
return r, err
|
||||
}
|
||||
|
||||
func (c *client) SetBackupStatus(ctx context.Context, backup string, data api.BackupRequest) error {
|
||||
resp, err := c.post(ctx, fmt.Sprintf("/backups/%s", backup), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return resp.Error()
|
||||
}
|
55
panelapi/client.go
Normal file
55
panelapi/client.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package panelapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/pterodactyl/wings/api"
|
||||
)
|
||||
|
||||
type Client interface {
|
||||
GetBackupRemoteUploadURLs(ctx context.Context, backup string, size int64) (api.BackupRemoteUploadResponse, error)
|
||||
GetInstallationScript(ctx context.Context, uuid string) (api.InstallationScript, error)
|
||||
GetServerConfiguration(ctx context.Context, uuid string) (api.ServerConfigurationResponse, error)
|
||||
GetServers(context context.Context, perPage int) ([]api.RawServerData, error)
|
||||
SetArchiveStatus(ctx context.Context, uuid string, successful bool) error
|
||||
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)
|
||||
}
|
||||
|
||||
type client struct {
|
||||
httpClient *http.Client
|
||||
baseUrl string
|
||||
tokenId string
|
||||
token string
|
||||
retries int
|
||||
}
|
||||
|
||||
type ClientOption func(c *client)
|
||||
|
||||
func CreateClient(base, tokenId, token string, opts ...ClientOption) Client {
|
||||
httpClient := &http.Client{
|
||||
Timeout: time.Second * 15,
|
||||
}
|
||||
c := &client{
|
||||
baseUrl: filepath.Join(base, "api/remote"),
|
||||
tokenId: tokenId,
|
||||
token: token,
|
||||
httpClient: httpClient,
|
||||
retries: 3,
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func WithTimeout(timeout time.Duration) ClientOption {
|
||||
return func(c *client) {
|
||||
c.httpClient.Timeout = timeout
|
||||
}
|
||||
}
|
46
panelapi/errors.go
Normal file
46
panelapi/errors.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
package panelapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type RequestErrors 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 {
|
||||
c := 0
|
||||
if re.response != nil {
|
||||
c = re.response.StatusCode
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Error response from Panel: %s: %s (HTTP/%d)", re.Code, re.Detail, c)
|
||||
}
|
||||
|
||||
type sftpInvalidCredentialsError struct {
|
||||
}
|
||||
|
||||
func (ice sftpInvalidCredentialsError) Error() string {
|
||||
return "the credentials provided were invalid"
|
||||
}
|
||||
|
||||
func IsInvalidCredentialsError(err error) bool {
|
||||
_, ok := err.(*sftpInvalidCredentialsError)
|
||||
|
||||
return ok
|
||||
}
|
158
panelapi/http.go
Normal file
158
panelapi/http.go
Normal file
|
@ -0,0 +1,158 @@
|
|||
package panelapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pterodactyl/wings/system"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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{}
|
||||
|
||||
// Same concept as d, but a map of strings, used for querying GET requests.
|
||||
type q map[string]string
|
||||
|
||||
// requestOnce creates a http request and executes it once.
|
||||
// Prefer request() over this method when possible.
|
||||
// It appends the path to the endpoint of the client and adds the authentication token to the request.
|
||||
func (c *client) requestOnce(ctx context.Context, method, path string, body io.Reader, opts ...func(r *http.Request)) (*Response, error) {
|
||||
req, err := http.NewRequest(method, filepath.Join(c.baseUrl, path), body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", fmt.Sprintf("Pterodactyl Wings/v%s (id:%s)", system.Version, c.tokenId))
|
||||
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", c.tokenId, c.token))
|
||||
|
||||
// Call all opts functions to allow modifying the request
|
||||
for _, o := range opts {
|
||||
o(req)
|
||||
}
|
||||
|
||||
debugLogRequest(req)
|
||||
|
||||
res, err := c.httpClient.Do(req.WithContext(ctx))
|
||||
return &Response{res}, err
|
||||
}
|
||||
|
||||
// request executes a http request and retries when errors occur.
|
||||
// It appends the path to the endpoint of the client and adds the authentication token to the request.
|
||||
func (c *client) request(ctx context.Context, method, path string, body io.Reader, opts ...func(r *http.Request)) (*Response, error) {
|
||||
var doErr error
|
||||
var res *Response
|
||||
|
||||
for i := 0; i < c.retries; i++ {
|
||||
res, doErr = c.requestOnce(ctx, method, path, body, opts...)
|
||||
|
||||
if doErr == nil &&
|
||||
res.StatusCode < http.StatusInternalServerError &&
|
||||
res.StatusCode != http.StatusTooManyRequests {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return res, doErr
|
||||
}
|
||||
|
||||
// get executes a http get request.
|
||||
func (c *client) get(ctx context.Context, path string, query q) (*Response, error) {
|
||||
return c.request(ctx, http.MethodGet, path, nil, func(r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
for k, v := range query {
|
||||
q.Set(k, v)
|
||||
}
|
||||
r.URL.RawQuery = q.Encode()
|
||||
})
|
||||
}
|
||||
|
||||
// post executes a http post request.
|
||||
func (c *client) post(ctx context.Context, path string, data interface{}) (*Response, error) {
|
||||
b, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.request(ctx, http.MethodPost, path, bytes.NewBuffer(b))
|
||||
}
|
||||
|
||||
// Determines if the API call encountered an error. If no request has been made
|
||||
// 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.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. 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")
|
||||
}
|
||||
|
||||
if r.Response.Body != nil {
|
||||
b, _ = ioutil.ReadAll(r.Response.Body)
|
||||
}
|
||||
|
||||
r.Response.Body = ioutil.NopCloser(bytes.NewBuffer(b))
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// 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) BindJSON(v interface{}) error {
|
||||
b, err := r.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return json.Unmarshal(b, &v)
|
||||
}
|
||||
|
||||
// Returns the first 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 *Response) Error() error {
|
||||
if !r.HasError() {
|
||||
return nil
|
||||
}
|
||||
|
||||
var errs RequestErrors
|
||||
_ = r.BindJSON(&errs)
|
||||
|
||||
e := &RequestError{}
|
||||
if len(errs.Errors) > 0 {
|
||||
e = &errs.Errors[0]
|
||||
}
|
||||
|
||||
e.response = r.Response
|
||||
|
||||
return e
|
||||
}
|
167
panelapi/servers.go
Normal file
167
panelapi/servers.go
Normal file
|
@ -0,0 +1,167 @@
|
|||
package panelapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/pterodactyl/wings/api"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const (
|
||||
ProcessStopCommand = "command"
|
||||
ProcessStopSignal = "signal"
|
||||
ProcessStopNativeStop = "stop"
|
||||
)
|
||||
|
||||
// Holds the server configuration data returned from the Panel. When a server process
|
||||
// is started, Wings communicates with the Panel to fetch the latest build information
|
||||
// as well as get all of the details needed to parse the given Egg.
|
||||
//
|
||||
// This means we do not need to hit Wings each time part of the server is updated, and
|
||||
// the Panel serves as the source of truth at all times. This also means if a configuration
|
||||
// is accidentally wiped on Wings we can self-recover without too much hassle, so long
|
||||
// as Wings is aware of what servers should exist on it.
|
||||
type ServerConfigurationResponse struct {
|
||||
Settings json.RawMessage `json:"settings"`
|
||||
ProcessConfiguration *api.ProcessConfiguration `json:"process_configuration"`
|
||||
}
|
||||
|
||||
// Defines installation script information for a server process. This is used when
|
||||
// a server is installed for the first time, and when a server is marked for re-installation.
|
||||
type InstallationScript struct {
|
||||
ContainerImage string `json:"container_image"`
|
||||
Entrypoint string `json:"entrypoint"`
|
||||
Script string `json:"script"`
|
||||
}
|
||||
|
||||
type allServerResponse struct {
|
||||
Data []api.RawServerData `json:"data"`
|
||||
Meta api.Pagination `json:"meta"`
|
||||
}
|
||||
|
||||
type RawServerData struct {
|
||||
Uuid string `json:"uuid"`
|
||||
Settings json.RawMessage `json:"settings"`
|
||||
ProcessConfiguration json.RawMessage `json:"process_configuration"`
|
||||
}
|
||||
|
||||
func (c *client) GetServersPaged(ctx context.Context, page, limit int) ([]api.RawServerData, api.Pagination, error) {
|
||||
res, err := c.get(ctx, "/servers", q{
|
||||
"page": strconv.Itoa(page),
|
||||
"per_page": strconv.Itoa(limit),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, api.Pagination{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.HasError() {
|
||||
return nil, api.Pagination{}, res.Error()
|
||||
}
|
||||
|
||||
var r allServerResponse
|
||||
if err := res.BindJSON(&r); err != nil {
|
||||
return nil, api.Pagination{}, err
|
||||
}
|
||||
|
||||
return r.Data, r.Meta, nil
|
||||
}
|
||||
|
||||
func (c *client) GetServers(ctx context.Context, perPage int) ([]api.RawServerData, error) {
|
||||
servers, pageMeta, err := c.GetServersPaged(ctx, 0, perPage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if the amount of servers exceeds the page limit, get the remaining pages in parallel
|
||||
if pageMeta.LastPage > 1 {
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
serversMu := sync.Mutex{}
|
||||
|
||||
for page := pageMeta.CurrentPage + 1; page <= pageMeta.LastPage; page++ {
|
||||
eg.Go(func() error {
|
||||
ps, _, err := c.GetServersPaged(ctx, perPage, int(page))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serversMu.Lock()
|
||||
servers = append(servers, ps...)
|
||||
serversMu.Unlock()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := eg.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
func (c *client) GetServerConfiguration(ctx context.Context, uuid string) (api.ServerConfigurationResponse, error) {
|
||||
res, err := c.get(ctx, fmt.Sprintf("/servers/%s", uuid), nil)
|
||||
if err != nil {
|
||||
return api.ServerConfigurationResponse{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.HasError() {
|
||||
return api.ServerConfigurationResponse{}, err
|
||||
}
|
||||
|
||||
config := api.ServerConfigurationResponse{}
|
||||
err = res.BindJSON(&config)
|
||||
return config, err
|
||||
}
|
||||
|
||||
func (c *client) GetInstallationScript(ctx context.Context, uuid string) (api.InstallationScript, error) {
|
||||
res, err := c.get(ctx, fmt.Sprintf("/servers/%s/install", uuid), nil)
|
||||
if err != nil {
|
||||
return api.InstallationScript{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.HasError() {
|
||||
return api.InstallationScript{}, err
|
||||
}
|
||||
|
||||
config := api.InstallationScript{}
|
||||
err = res.BindJSON(&config)
|
||||
return config, err
|
||||
}
|
||||
|
||||
func (c *client) SetInstallationStatus(ctx context.Context, uuid string, successful bool) error {
|
||||
resp, err := c.post(ctx, fmt.Sprintf("/servers/%s/install", uuid), d{"successful": successful})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return resp.Error()
|
||||
}
|
||||
|
||||
func (c *client) SetArchiveStatus(ctx context.Context, uuid string, successful bool) error {
|
||||
resp, err := c.post(ctx, fmt.Sprintf("/servers/%s/archive", uuid), d{"successful": successful})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return resp.Error()
|
||||
}
|
||||
|
||||
func (c *client) SetTransferStatus(ctx context.Context, uuid string, successful bool) error {
|
||||
state := "failure"
|
||||
if successful {
|
||||
state = "success"
|
||||
}
|
||||
resp, err := c.post(ctx, fmt.Sprintf("/servers/%s/transfer/%s", uuid), state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return resp.Error()
|
||||
}
|
50
panelapi/sftp.go
Normal file
50
panelapi/sftp.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package panelapi
|
||||
|
||||
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})$`)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
res, err := c.post(ctx, "/sftp/auth", request)
|
||||
if err != nil {
|
||||
return api.SftpAuthResponse{}, err
|
||||
}
|
||||
|
||||
e := res.Error()
|
||||
if e != nil {
|
||||
if res.StatusCode >= 400 && res.StatusCode < 500 {
|
||||
log.WithFields(log.Fields{
|
||||
"subsystem": "sftp",
|
||||
"username": request.User,
|
||||
"ip": request.IP,
|
||||
}).Warn(e.Error())
|
||||
|
||||
return api.SftpAuthResponse{}, &sftpInvalidCredentialsError{}
|
||||
}
|
||||
|
||||
return api.SftpAuthResponse{}, errors.New(e.Error())
|
||||
}
|
||||
|
||||
r := api.SftpAuthResponse{}
|
||||
err = res.BindJSON(&r)
|
||||
return r, err
|
||||
}
|
29
panelapi/util.go
Normal file
29
panelapi/util.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package panelapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/apex/log"
|
||||
)
|
||||
|
||||
// Logs the request into the debug log with all of the important request bits.
|
||||
// The authorization key will be cleaned up before being output.
|
||||
//
|
||||
// TODO(schrej): Somehow only execute the logic when log level is debug.
|
||||
func debugLogRequest(req *http.Request) {
|
||||
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)"}
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"method": req.Method,
|
||||
"endpoint": req.URL.String(),
|
||||
"headers": headers,
|
||||
}).Debug("making request to external HTTP endpoint")
|
||||
}
|
Loading…
Reference in New Issue
Block a user