Merge branch 'dane/api-cleanup' into develop
This commit is contained in:
commit
2eb721bbe7
197
api/api.go
197
api/api.go
|
@ -1,197 +0,0 @@
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"emperror.dev/errors"
|
|
||||||
"github.com/apex/log"
|
|
||||||
"github.com/pterodactyl/wings/config"
|
|
||||||
"github.com/pterodactyl/wings/system"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Initializes the requester instance.
|
|
||||||
func New() *Request {
|
|
||||||
return &Request{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Builds the base request instance that can be used with the HTTP client.
|
|
||||||
func (r *Request) Client() *http.Client {
|
|
||||||
return &http.Client{Timeout: time.Second * time.Duration(config.Get().RemoteQuery.Timeout)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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, "/"),
|
|
||||||
strings.TrimPrefix(strings.TrimPrefix(endpoint, "/"), "api/remote/"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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, opts ...func(r *http.Request)) (*Response, error) {
|
|
||||||
req, err := http.NewRequest(method, url, body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 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))
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 *Request) debug(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")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 Q) (*Response, error) {
|
|
||||||
return r.Make(http.MethodGet, r.Endpoint(url), nil, func(r *http.Request) {
|
|
||||||
q := r.URL.Query()
|
|
||||||
for k, v := range data {
|
|
||||||
q.Set(k, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.URL.RawQuery = q.Encode()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 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. 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) Bind(v interface{}) error {
|
|
||||||
b, err := r.Read()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return 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 *Response) Error() error {
|
|
||||||
if !r.HasError() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var bag RequestErrorBag
|
|
||||||
_ = r.Bind(&bag)
|
|
||||||
|
|
||||||
e := &RequestError{}
|
|
||||||
if len(bag.Errors) > 0 {
|
|
||||||
e = &bag.Errors[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
e.response = r.Response
|
|
||||||
|
|
||||||
return e
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
type BackupRemoteUploadResponse struct {
|
|
||||||
Parts []string `json:"parts"`
|
|
||||||
PartSize int64 `json:"part_size"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Request) GetBackupRemoteUploadURLs(backup string, size int64) (*BackupRemoteUploadResponse, error) {
|
|
||||||
resp, err := r.Get(fmt.Sprintf("/backups/%s", backup), Q{"size": strconv.FormatInt(size, 10)})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.HasError() {
|
|
||||||
return nil, resp.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
var res BackupRemoteUploadResponse
|
|
||||||
if err := resp.Bind(&res); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type BackupRequest struct {
|
|
||||||
Checksum string `json:"checksum"`
|
|
||||||
ChecksumType string `json:"checksum_type"`
|
|
||||||
Size int64 `json:"size"`
|
|
||||||
Successful bool `json:"successful"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendBackupStatus notifies the panel that a specific backup has been completed
|
|
||||||
// and is now available for a user to view and download.
|
|
||||||
func (r *Request) SendBackupStatus(backup string, data BackupRequest) error {
|
|
||||||
resp, err := r.Post(fmt.Sprintf("/backups/%s", backup), data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
return resp.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendRestorationStatus triggers a request to the Panel to notify it that a
|
|
||||||
// restoration has been completed and the server should be marked as being
|
|
||||||
// activated again.
|
|
||||||
func (r *Request) SendRestorationStatus(backup string, successful bool) error {
|
|
||||||
resp, err := r.Post(fmt.Sprintf("/backups/%s/restore", backup), D{"successful": successful})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
return resp.Error()
|
|
||||||
}
|
|
33
api/error.go
33
api/error.go
|
@ -1,33 +0,0 @@
|
||||||
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 {
|
|
||||||
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)
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/apex/log"
|
|
||||||
"github.com/pterodactyl/wings/parser"
|
|
||||||
)
|
|
||||||
|
|
||||||
type OutputLineMatcher struct {
|
|
||||||
// The raw string to match against. This may or may not be prefixed with
|
|
||||||
// regex: which indicates we want to match against the regex expression.
|
|
||||||
raw string
|
|
||||||
reg *regexp.Regexp
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine if a given string "s" matches the given line.
|
|
||||||
func (olm *OutputLineMatcher) Matches(s string) bool {
|
|
||||||
if olm.reg == nil {
|
|
||||||
return strings.Contains(s, olm.raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
return olm.reg.MatchString(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the matcher's raw comparison string.
|
|
||||||
func (olm *OutputLineMatcher) String() string {
|
|
||||||
return olm.raw
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal the startup lines into individual structs for easier matching abilities.
|
|
||||||
func (olm *OutputLineMatcher) UnmarshalJSON(data []byte) error {
|
|
||||||
if err := json.Unmarshal(data, &olm.raw); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(olm.raw, "regex:") && len(olm.raw) > 6 {
|
|
||||||
r, err := regexp.Compile(strings.TrimPrefix(olm.raw, "regex:"))
|
|
||||||
if err != nil {
|
|
||||||
log.WithField("error", err).WithField("raw", olm.raw).Warn("failed to compile output line marked as being regex")
|
|
||||||
}
|
|
||||||
|
|
||||||
olm.reg = r
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProcessStopConfiguration struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defines the process configuration for a given server instance. This sets what the
|
|
||||||
// daemon is looking for to mark a server as done starting, what to do when stopping,
|
|
||||||
// and what changes to make to the configuration file for a server.
|
|
||||||
type ProcessConfiguration struct {
|
|
||||||
Startup struct {
|
|
||||||
Done []*OutputLineMatcher `json:"done"`
|
|
||||||
UserInteraction []string `json:"user_interaction"`
|
|
||||||
StripAnsi bool `json:"strip_ansi"`
|
|
||||||
} `json:"startup"`
|
|
||||||
|
|
||||||
Stop ProcessStopConfiguration `json:"stop"`
|
|
||||||
|
|
||||||
ConfigurationFiles []parser.ConfigurationFile `json:"configs"`
|
|
||||||
}
|
|
|
@ -1,118 +0,0 @@
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 *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 RawServerData struct {
|
|
||||||
Uuid string `json:"uuid"`
|
|
||||||
Settings json.RawMessage `json:"settings"`
|
|
||||||
ProcessConfiguration json.RawMessage `json:"process_configuration"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetches the server configuration and returns the struct for it.
|
|
||||||
func (r *Request) GetServerConfiguration(uuid string) (ServerConfigurationResponse, error) {
|
|
||||||
var cfg ServerConfigurationResponse
|
|
||||||
|
|
||||||
resp, err := r.Get(fmt.Sprintf("/servers/%s", uuid), nil)
|
|
||||||
if err != nil {
|
|
||||||
return cfg, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.HasError() {
|
|
||||||
return cfg, resp.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := resp.Bind(&cfg); err != nil {
|
|
||||||
return cfg, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetches installation information for the server process.
|
|
||||||
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 is, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.HasError() {
|
|
||||||
return is, resp.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := resp.Bind(&is); err != nil {
|
|
||||||
return is, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return is, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marks a server as being installed successfully or unsuccessfully on the panel.
|
|
||||||
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 err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.HasError() {
|
|
||||||
return resp.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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 err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
return resp.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Request) SendTransferStatus(uuid string, successful bool) error {
|
|
||||||
state := "failure"
|
|
||||||
if successful {
|
|
||||||
state = "success"
|
|
||||||
}
|
|
||||||
resp, err := r.Get(fmt.Sprintf("/servers/%s/transfer/%s", uuid, state), nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
return resp.Error()
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
20
cmd/root.go
20
cmd/root.go
|
@ -137,11 +137,12 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
|
||||||
"gid": config.Get().System.User.Gid,
|
"gid": config.Get().System.User.Gid,
|
||||||
}).Info("configured system user successfully")
|
}).Info("configured system user successfully")
|
||||||
|
|
||||||
pclient := remote.CreateClient(
|
pclient := remote.New(
|
||||||
config.Get().PanelLocation,
|
config.Get().PanelLocation,
|
||||||
config.Get().AuthenticationTokenId,
|
remote.WithCredentials(config.Get().AuthenticationTokenId, config.Get().AuthenticationToken),
|
||||||
config.Get().AuthenticationToken,
|
remote.WithHttpClient(&http.Client{
|
||||||
remote.WithTimeout(time.Second*time.Duration(config.Get().RemoteQuery.Timeout)),
|
Timeout: time.Second * time.Duration(config.Get().RemoteQuery.Timeout),
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
manager, err := server.NewManager(cmd.Context(), pclient)
|
manager, err := server.NewManager(cmd.Context(), pclient)
|
||||||
|
@ -264,6 +265,15 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
log.Info("updating server states on Panel: marking installing/restoring servers as normal")
|
||||||
|
// Update all of the servers on the Panel to be in a valid state if they're
|
||||||
|
// currently marked as installing/restoring now that Wings is restarted.
|
||||||
|
if err := pclient.ResetServersState(cmd.Context()); err != nil {
|
||||||
|
log.WithField("error", err).Error("failed to reset server states on Panel: some instances may be stuck in an installing/restoring state unexpectedly")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
sys := config.Get().System
|
sys := config.Get().System
|
||||||
// Ensure the archive directory exists.
|
// Ensure the archive directory exists.
|
||||||
if err := os.MkdirAll(sys.ArchiveDirectory, 0755); err != nil {
|
if err := os.MkdirAll(sys.ArchiveDirectory, 0755); err != nil {
|
||||||
|
@ -293,7 +303,7 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
|
||||||
// and external clients.
|
// and external clients.
|
||||||
s := &http.Server{
|
s := &http.Server{
|
||||||
Addr: api.Host + ":" + strconv.Itoa(api.Port),
|
Addr: api.Host + ":" + strconv.Itoa(api.Port),
|
||||||
Handler: router.Configure(manager),
|
Handler: router.Configure(manager, pclient),
|
||||||
TLSConfig: config.DefaultTLSConfig,
|
TLSConfig: config.DefaultTLSConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,15 +10,15 @@ import (
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/pterodactyl/wings/api"
|
|
||||||
"github.com/pterodactyl/wings/environment"
|
"github.com/pterodactyl/wings/environment"
|
||||||
"github.com/pterodactyl/wings/events"
|
"github.com/pterodactyl/wings/events"
|
||||||
|
"github.com/pterodactyl/wings/remote"
|
||||||
"github.com/pterodactyl/wings/system"
|
"github.com/pterodactyl/wings/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Metadata struct {
|
type Metadata struct {
|
||||||
Image string
|
Image string
|
||||||
Stop api.ProcessStopConfiguration
|
Stop remote.ProcessStopConfiguration
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure that the Docker environment is always implementing all of the methods
|
// Ensure that the Docker environment is always implementing all of the methods
|
||||||
|
@ -177,7 +177,7 @@ func (e *Environment) Config() *environment.Configuration {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets the stop configuration for the environment.
|
// Sets the stop configuration for the environment.
|
||||||
func (e *Environment) SetStopConfiguration(c api.ProcessStopConfiguration) {
|
func (e *Environment) SetStopConfiguration(c remote.ProcessStopConfiguration) {
|
||||||
e.mu.Lock()
|
e.mu.Lock()
|
||||||
defer e.mu.Unlock()
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,9 @@ import (
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/pterodactyl/wings/api"
|
|
||||||
"github.com/pterodactyl/wings/environment"
|
"github.com/pterodactyl/wings/environment"
|
||||||
|
"github.com/pterodactyl/wings/remote"
|
||||||
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
@ -133,7 +134,7 @@ func (e *Environment) Stop() error {
|
||||||
// A native "stop" as the Type field value will just skip over all of this
|
// A native "stop" as the Type field value will just skip over all of this
|
||||||
// logic and end up only executing the container stop command (which may or
|
// logic and end up only executing the container stop command (which may or
|
||||||
// may not work as expected).
|
// may not work as expected).
|
||||||
if s.Type == "" || s.Type == api.ProcessStopSignal {
|
if s.Type == "" || s.Type == remote.ProcessStopSignal {
|
||||||
if s.Type == "" {
|
if s.Type == "" {
|
||||||
log.WithField("container_id", e.Id).Warn("no stop configuration detected for environment, using termination procedure")
|
log.WithField("container_id", e.Id).Warn("no stop configuration detected for environment, using termination procedure")
|
||||||
}
|
}
|
||||||
|
@ -160,7 +161,7 @@ func (e *Environment) Stop() error {
|
||||||
|
|
||||||
// Only attempt to send the stop command to the instance if we are actually attached to
|
// Only attempt to send the stop command to the instance if we are actually attached to
|
||||||
// the instance. If we are not for some reason, just send the container stop event.
|
// the instance. If we are not for some reason, just send the container stop event.
|
||||||
if e.IsAttached() && s.Type == api.ProcessStopCommand {
|
if e.IsAttached() && s.Type == remote.ProcessStopCommand {
|
||||||
return e.SendCommand(s.Value)
|
return e.SendCommand(s.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
package installer
|
package installer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"emperror.dev/errors"
|
"emperror.dev/errors"
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
"github.com/buger/jsonparser"
|
"github.com/buger/jsonparser"
|
||||||
"github.com/pterodactyl/wings/api"
|
|
||||||
"github.com/pterodactyl/wings/environment"
|
"github.com/pterodactyl/wings/environment"
|
||||||
|
"github.com/pterodactyl/wings/remote"
|
||||||
"github.com/pterodactyl/wings/server"
|
"github.com/pterodactyl/wings/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,10 +16,10 @@ type Installer struct {
|
||||||
server *server.Server
|
server *server.Server
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validates the received data to ensure that all of the required fields
|
// New validates the received data to ensure that all of the required fields
|
||||||
// have been passed along in the request. This should be manually run before
|
// have been passed along in the request. This should be manually run before
|
||||||
// calling Execute().
|
// calling Execute().
|
||||||
func New(data []byte) (*Installer, error) {
|
func New(ctx context.Context, manager *server.Manager, data []byte) (*Installer, error) {
|
||||||
if !govalidator.IsUUIDv4(getString(data, "uuid")) {
|
if !govalidator.IsUUIDv4(getString(data, "uuid")) {
|
||||||
return nil, NewValidationError("uuid provided was not in a valid format")
|
return nil, NewValidationError("uuid provided was not in a valid format")
|
||||||
}
|
}
|
||||||
|
@ -64,30 +65,27 @@ func New(data []byte) (*Installer, error) {
|
||||||
|
|
||||||
cfg.Container.Image = getString(data, "container", "image")
|
cfg.Container.Image = getString(data, "container", "image")
|
||||||
|
|
||||||
c, err := api.New().GetServerConfiguration(cfg.Uuid)
|
c, err := manager.Client().GetServerConfiguration(ctx, cfg.Uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !api.IsRequestError(err) {
|
if !remote.IsRequestError(err) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.New(err.Error())
|
return nil, errors.New(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new server instance using the configuration we wrote to the disk
|
// Create a new server instance using the configuration we wrote to the disk
|
||||||
// so that everything gets instantiated correctly on the struct.
|
// so that everything gets instantiated correctly on the struct.
|
||||||
s, err := server.FromConfiguration(c)
|
s, err := manager.InitServer(c)
|
||||||
|
|
||||||
return &Installer{
|
return &Installer{server: s}, err
|
||||||
server: s,
|
|
||||||
}, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the UUID associated with this installer instance.
|
// Uuid returns the UUID associated with this installer instance.
|
||||||
func (i *Installer) Uuid() string {
|
func (i *Installer) Uuid() string {
|
||||||
return i.server.Id()
|
return i.server.Id()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the server instance.
|
// Server returns the server instance.
|
||||||
func (i *Installer) Server() *server.Server {
|
func (i *Installer) Server() *server.Server {
|
||||||
return i.server
|
return i.server
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
package remote
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
package remote
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"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,
|
|
||||||
}
|
|
||||||
base = strings.TrimSuffix(base, "/")
|
|
||||||
c := &client{
|
|
||||||
baseUrl: 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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
package remote
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
)
|
|
||||||
|
|
||||||
func createTestClient(h http.HandlerFunc) (*client, *httptest.Server) {
|
|
||||||
s := httptest.NewServer(h)
|
|
||||||
c := &client{
|
|
||||||
httpClient: s.Client(),
|
|
||||||
baseUrl: s.URL,
|
|
||||||
|
|
||||||
retries: 1,
|
|
||||||
tokenId: "testid",
|
|
||||||
token: "testtoken",
|
|
||||||
}
|
|
||||||
return c, s
|
|
||||||
}
|
|
|
@ -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)
|
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"
|
return "the credentials provided were invalid"
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsInvalidCredentialsError(err error) bool {
|
|
||||||
_, ok := err.(*sftpInvalidCredentialsError)
|
|
||||||
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
|
@ -9,26 +9,67 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/apex/log"
|
||||||
"github.com/pterodactyl/wings/system"
|
"github.com/pterodactyl/wings/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Response is a custom response type that allows for commonly used error
|
type Client interface {
|
||||||
// handling and response parsing from the Panel API. This just embeds the normal
|
GetBackupRemoteUploadURLs(ctx context.Context, backup string, size int64) (BackupRemoteUploadResponse, error)
|
||||||
// HTTP response from Go and we attach a few helper functions to it.
|
GetInstallationScript(ctx context.Context, uuid string) (InstallationScript, error)
|
||||||
type Response struct {
|
GetServerConfiguration(ctx context.Context, uuid string) (ServerConfigurationResponse, error)
|
||||||
*http.Response
|
GetServers(context context.Context, perPage int) ([]RawServerData, error)
|
||||||
|
ResetServersState(ctx context.Context) error
|
||||||
|
SetArchiveStatus(ctx context.Context, uuid string, successful bool) error
|
||||||
|
SetBackupStatus(ctx context.Context, backup string, data BackupRequest) error
|
||||||
|
SendRestorationStatus(ctx context.Context, backup string, successful bool) error
|
||||||
|
SetInstallationStatus(ctx context.Context, uuid string, successful bool) error
|
||||||
|
SetTransferStatus(ctx context.Context, uuid string, successful bool) error
|
||||||
|
ValidateSftpCredentials(ctx context.Context, request SftpAuthRequest) (SftpAuthResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// A generic type allowing for easy binding use when making requests to API
|
type client struct {
|
||||||
// endpoints that only expect a singular argument or something that would not
|
httpClient *http.Client
|
||||||
// benefit from being a typed struct.
|
baseUrl string
|
||||||
//
|
tokenId string
|
||||||
// Inspired by gin.H, same concept.
|
token string
|
||||||
type d map[string]interface{}
|
retries int
|
||||||
|
}
|
||||||
|
|
||||||
// Same concept as d, but a map of strings, used for querying GET requests.
|
// New returns a new HTTP request client that is used for making authenticated
|
||||||
type q map[string]string
|
// requests to the Panel that this instance is running under.
|
||||||
|
func New(base string, opts ...ClientOption) Client {
|
||||||
|
c := client{
|
||||||
|
baseUrl: strings.TrimSuffix(base, "/") + "/api/remote",
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Timeout: time.Second * 15,
|
||||||
|
},
|
||||||
|
retries: 3,
|
||||||
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&c)
|
||||||
|
}
|
||||||
|
return &c
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCredentials sets the credentials to use when making request to the remote
|
||||||
|
// API endpoint.
|
||||||
|
func WithCredentials(id, token string) ClientOption {
|
||||||
|
return func(c *client) {
|
||||||
|
c.tokenId = id
|
||||||
|
c.token = token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHttpClient sets the underlying HTTP client instance to use when making
|
||||||
|
// requests to the Panel API.
|
||||||
|
func WithHttpClient(httpClient *http.Client) ClientOption {
|
||||||
|
return func(c *client) {
|
||||||
|
c.httpClient = httpClient
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// requestOnce creates a http request and executes it once. Prefer request()
|
// 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
|
// over this method when possible. It appends the path to the endpoint of the
|
||||||
|
@ -94,6 +135,13 @@ func (c *client) post(ctx context.Context, path string, data interface{}) (*Resp
|
||||||
return c.request(ctx, http.MethodPost, path, bytes.NewBuffer(b))
|
return c.request(ctx, http.MethodPost, path, bytes.NewBuffer(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Response is 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
|
||||||
|
}
|
||||||
|
|
||||||
// HasError determines if the API call encountered an error. If no request has
|
// HasError 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
|
// been made the response will be false. This function will evaluate to true if
|
||||||
// the response code is anything 300 or higher.
|
// the response code is anything 300 or higher.
|
||||||
|
@ -156,3 +204,26 @@ func (r *Response) Error() error {
|
||||||
|
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 debugLogRequest(req *http.Request) {
|
||||||
|
if l, ok := log.Log.(*log.Logger); ok && l.Level != log.DebugLevel {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
headers := make(map[string][]string)
|
||||||
|
for k, v := range req.Header {
|
||||||
|
if k != "Authorization" || len(v) == 0 || len(v[0]) == 0 {
|
||||||
|
headers[k] = v
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
headers[k] = []string{"(redacted)"}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"method": req.Method,
|
||||||
|
"endpoint": req.URL.String(),
|
||||||
|
"headers": headers,
|
||||||
|
}).Debug("making request to external HTTP endpoint")
|
||||||
|
}
|
||||||
|
|
|
@ -3,11 +3,25 @@ package remote
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func createTestClient(h http.HandlerFunc) (*client, *httptest.Server) {
|
||||||
|
s := httptest.NewServer(h)
|
||||||
|
c := &client{
|
||||||
|
httpClient: s.Client(),
|
||||||
|
baseUrl: s.URL,
|
||||||
|
|
||||||
|
retries: 1,
|
||||||
|
tokenId: "testid",
|
||||||
|
token: "testtoken",
|
||||||
|
}
|
||||||
|
return c, s
|
||||||
|
}
|
||||||
|
|
||||||
func TestRequest(t *testing.T) {
|
func TestRequest(t *testing.T) {
|
||||||
c, _ := createTestClient(func(rw http.ResponseWriter, r *http.Request) {
|
c, _ := createTestClient(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
assert.Equal(t, "application/vnd.pterodactyl.v1+json", r.Header.Get("Accept"))
|
assert.Equal(t, "application/vnd.pterodactyl.v1+json", r.Header.Get("Accept"))
|
||||||
|
|
|
@ -2,12 +2,12 @@ package remote
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/pterodactyl/wings/api"
|
"emperror.dev/errors"
|
||||||
|
"github.com/apex/log"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,41 +17,10 @@ const (
|
||||||
ProcessStopNativeStop = "stop"
|
ProcessStopNativeStop = "stop"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServerConfigurationResponse 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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// InstallationScript 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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RawServerData is a raw response from the API for a server.
|
|
||||||
type RawServerData struct {
|
|
||||||
Uuid string `json:"uuid"`
|
|
||||||
Settings json.RawMessage `json:"settings"`
|
|
||||||
ProcessConfiguration json.RawMessage `json:"process_configuration"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetServers returns all of the servers that are present on the Panel making
|
// GetServers returns all of the servers that are present on the Panel making
|
||||||
// parallel API calls to the endpoint if more than one page of servers is
|
// parallel API calls to the endpoint if more than one page of servers is
|
||||||
// returned.
|
// returned.
|
||||||
func (c *client) GetServers(ctx context.Context, limit int) ([]api.RawServerData, error) {
|
func (c *client) GetServers(ctx context.Context, limit int) ([]RawServerData, error) {
|
||||||
servers, meta, err := c.getServersPaged(ctx, 0, limit)
|
servers, meta, err := c.getServersPaged(ctx, 0, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -81,34 +50,50 @@ func (c *client) GetServers(ctx context.Context, limit int) ([]api.RawServerData
|
||||||
return servers, nil
|
return servers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) GetServerConfiguration(ctx context.Context, uuid string) (api.ServerConfigurationResponse, error) {
|
// ResetServersState updates the state of all servers on the node that are
|
||||||
|
// currently marked as "installing" or "restoring from backup" to be marked as
|
||||||
|
// a normal successful install state.
|
||||||
|
//
|
||||||
|
// This handles Wings exiting during either of these processes which will leave
|
||||||
|
// things in a bad state within the Panel. This API call is executed once Wings
|
||||||
|
// has fully booted all of the servers.
|
||||||
|
func (c *client) ResetServersState(ctx context.Context) error {
|
||||||
|
res, err := c.post(ctx, "/servers/reset", nil)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WrapIf(err, "remote/servers: failed to reset server state on Panel")
|
||||||
|
}
|
||||||
|
res.Body.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) GetServerConfiguration(ctx context.Context, uuid string) (ServerConfigurationResponse, error) {
|
||||||
|
var config ServerConfigurationResponse
|
||||||
res, err := c.get(ctx, fmt.Sprintf("/servers/%s", uuid), nil)
|
res, err := c.get(ctx, fmt.Sprintf("/servers/%s", uuid), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return api.ServerConfigurationResponse{}, err
|
return config, err
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.HasError() {
|
if res.HasError() {
|
||||||
return api.ServerConfigurationResponse{}, err
|
return config, err
|
||||||
}
|
}
|
||||||
|
|
||||||
config := api.ServerConfigurationResponse{}
|
|
||||||
err = res.BindJSON(&config)
|
err = res.BindJSON(&config)
|
||||||
return config, err
|
return config, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) GetInstallationScript(ctx context.Context, uuid string) (api.InstallationScript, error) {
|
func (c *client) GetInstallationScript(ctx context.Context, uuid string) (InstallationScript, error) {
|
||||||
res, err := c.get(ctx, fmt.Sprintf("/servers/%s/install", uuid), nil)
|
res, err := c.get(ctx, fmt.Sprintf("/servers/%s/install", uuid), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return api.InstallationScript{}, err
|
return InstallationScript{}, err
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.HasError() {
|
if res.HasError() {
|
||||||
return api.InstallationScript{}, err
|
return InstallationScript{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
config := api.InstallationScript{}
|
var config InstallationScript
|
||||||
err = res.BindJSON(&config)
|
err = res.BindJSON(&config)
|
||||||
return config, err
|
return config, err
|
||||||
}
|
}
|
||||||
|
@ -144,29 +129,97 @@ func (c *client) SetTransferStatus(ctx context.Context, uuid string, successful
|
||||||
return resp.Error()
|
return resp.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 auth, 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 auth, &SftpInvalidCredentialsError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return auth, errors.New(e.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = res.BindJSON(&auth)
|
||||||
|
return auth, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) GetBackupRemoteUploadURLs(ctx context.Context, backup string, size int64) (BackupRemoteUploadResponse, error) {
|
||||||
|
var data BackupRemoteUploadResponse
|
||||||
|
res, err := c.get(ctx, fmt.Sprintf("/backups/%s", backup), q{"size": strconv.FormatInt(size, 10)})
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
if res.HasError() {
|
||||||
|
return data, res.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
err = res.BindJSON(&data)
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) SetBackupStatus(ctx context.Context, backup string, data 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// SendRestorationStatus triggers a request to the Panel to notify it that a
|
||||||
|
// restoration has been completed and the server should be marked as being
|
||||||
|
// activated again.
|
||||||
|
func (c *client) SendRestorationStatus(ctx context.Context, backup string, successful bool) error {
|
||||||
|
resp, err := c.post(ctx, fmt.Sprintf("/backups/%s/restore", backup), d{"successful": successful})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
return resp.Error()
|
||||||
|
}
|
||||||
|
|
||||||
// getServersPaged returns a subset of servers from the Panel API using the
|
// getServersPaged returns a subset of servers from the Panel API using the
|
||||||
// pagination query parameters.
|
// pagination query parameters.
|
||||||
func (c *client) getServersPaged(ctx context.Context, page, limit int) ([]api.RawServerData, api.Pagination, error) {
|
func (c *client) getServersPaged(ctx context.Context, page, limit int) ([]RawServerData, Pagination, error) {
|
||||||
|
var r struct {
|
||||||
|
Data []RawServerData `json:"data"`
|
||||||
|
Meta Pagination `json:"meta"`
|
||||||
|
}
|
||||||
|
|
||||||
res, err := c.get(ctx, "/servers", q{
|
res, err := c.get(ctx, "/servers", q{
|
||||||
"page": strconv.Itoa(page),
|
"page": strconv.Itoa(page),
|
||||||
"per_page": strconv.Itoa(limit),
|
"per_page": strconv.Itoa(limit),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, api.Pagination{}, err
|
return nil, r.Meta, err
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.HasError() {
|
if res.HasError() {
|
||||||
return nil, api.Pagination{}, res.Error()
|
return nil, r.Meta, res.Error()
|
||||||
}
|
|
||||||
|
|
||||||
var r struct {
|
|
||||||
Data []api.RawServerData `json:"data"`
|
|
||||||
Meta api.Pagination `json:"meta"`
|
|
||||||
}
|
}
|
||||||
if err := res.BindJSON(&r); err != nil {
|
if err := res.BindJSON(&r); err != nil {
|
||||||
return nil, api.Pagination{}, err
|
return nil, r.Meta, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return r.Data, r.Meta, nil
|
return r.Data, r.Meta, nil
|
||||||
}
|
}
|
|
@ -1,50 +0,0 @@
|
||||||
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})$`)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
154
remote/types.go
Normal file
154
remote/types.go
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
package remote
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/apex/log"
|
||||||
|
"github.com/pterodactyl/wings/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
type ClientOption func(c *client)
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerConfigurationResponse 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 *ProcessConfiguration `json:"process_configuration"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstallationScript 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RawServerData is a raw response from the API for a server.
|
||||||
|
type RawServerData struct {
|
||||||
|
Uuid string `json:"uuid"`
|
||||||
|
Settings json.RawMessage `json:"settings"`
|
||||||
|
ProcessConfiguration json.RawMessage `json:"process_configuration"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SftpAuthRequest defines the request details that are passed along to the Panel
|
||||||
|
// when determining if the credentials provided to Wings are valid.
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SftpAuthResponse is returned by the Panel when a pair of SFTP credentials
|
||||||
|
// is successfully validated. This will include the specific server that was
|
||||||
|
// matched as well as the permissions that are assigned to the authenticated
|
||||||
|
// user for the SFTP subsystem.
|
||||||
|
type SftpAuthResponse struct {
|
||||||
|
Server string `json:"server"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
Permissions []string `json:"permissions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutputLineMatcher struct {
|
||||||
|
// The raw string to match against. This may or may not be prefixed with
|
||||||
|
// regex: which indicates we want to match against the regex expression.
|
||||||
|
raw string
|
||||||
|
reg *regexp.Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches determines if a given string "s" matches the given line.
|
||||||
|
func (olm *OutputLineMatcher) Matches(s string) bool {
|
||||||
|
if olm.reg == nil {
|
||||||
|
return strings.Contains(s, olm.raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
return olm.reg.MatchString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the matcher's raw comparison string.
|
||||||
|
func (olm *OutputLineMatcher) String() string {
|
||||||
|
return olm.raw
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON unmarshals the startup lines into individual structs for easier
|
||||||
|
// matching abilities.
|
||||||
|
func (olm *OutputLineMatcher) UnmarshalJSON(data []byte) error {
|
||||||
|
if err := json.Unmarshal(data, &olm.raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(olm.raw, "regex:") && len(olm.raw) > 6 {
|
||||||
|
r, err := regexp.Compile(strings.TrimPrefix(olm.raw, "regex:"))
|
||||||
|
if err != nil {
|
||||||
|
log.WithField("error", err).WithField("raw", olm.raw).Warn("failed to compile output line marked as being regex")
|
||||||
|
}
|
||||||
|
|
||||||
|
olm.reg = r
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessStopConfiguration defines what is used when stopping an instance.
|
||||||
|
type ProcessStopConfiguration struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessConfiguration defines the process configuration for a given server
|
||||||
|
// instance. This sets what Wings is looking for to mark a server as done starting
|
||||||
|
// what to do when stopping, and what changes to make to the configuration file
|
||||||
|
// for a server.
|
||||||
|
type ProcessConfiguration struct {
|
||||||
|
Startup struct {
|
||||||
|
Done []*OutputLineMatcher `json:"done"`
|
||||||
|
UserInteraction []string `json:"user_interaction"`
|
||||||
|
StripAnsi bool `json:"strip_ansi"`
|
||||||
|
} `json:"startup"`
|
||||||
|
Stop ProcessStopConfiguration `json:"stop"`
|
||||||
|
ConfigurationFiles []parser.ConfigurationFile `json:"configs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BackupRemoteUploadResponse struct {
|
||||||
|
Parts []string `json:"parts"`
|
||||||
|
PartSize int64 `json:"part_size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BackupRequest struct {
|
||||||
|
Checksum string `json:"checksum"`
|
||||||
|
ChecksumType string `json:"checksum_type"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
Successful bool `json:"successful"`
|
||||||
|
}
|
|
@ -1,30 +0,0 @@
|
||||||
package remote
|
|
||||||
|
|
||||||
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.
|
|
||||||
func debugLogRequest(req *http.Request) {
|
|
||||||
if l, ok := log.Log.(*log.Logger); ok && l.Level != log.DebugLevel {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
headers := make(map[string][]string)
|
|
||||||
for k, v := range req.Header {
|
|
||||||
if k != "Authorization" || len(v) == 0 || len(v[0]) == 0 {
|
|
||||||
headers[k] = v
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
headers[k] = []string{"(redacted)"}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"method": req.Method,
|
|
||||||
"endpoint": req.URL.String(),
|
|
||||||
"headers": headers,
|
|
||||||
}).Debug("making request to external HTTP endpoint")
|
|
||||||
}
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"github.com/pterodactyl/wings/remote"
|
||||||
"github.com/pterodactyl/wings/server"
|
"github.com/pterodactyl/wings/server"
|
||||||
"github.com/pterodactyl/wings/server/filesystem"
|
"github.com/pterodactyl/wings/server/filesystem"
|
||||||
)
|
)
|
||||||
|
@ -168,6 +169,15 @@ func AttachServerManager(m *server.Manager) gin.HandlerFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AttachApiClient attaches the application API client which allows routes to
|
||||||
|
// access server resources from the Panel easily.
|
||||||
|
func AttachApiClient(client remote.Client) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
c.Set("api_client", client)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CaptureAndAbort aborts the request and attaches the provided error to the gin
|
// CaptureAndAbort aborts the request and attaches the provided error to the gin
|
||||||
// context so it can be reported properly. If the error is missing a stacktrace
|
// context so it can be reported properly. If the error is missing a stacktrace
|
||||||
// at the time it is called the stack will be attached.
|
// at the time it is called the stack will be attached.
|
||||||
|
@ -327,6 +337,14 @@ func ExtractServer(c *gin.Context) *server.Server {
|
||||||
return v.(*server.Server)
|
return v.(*server.Server)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExtractApiClient returns the API client defined for the routes.
|
||||||
|
func ExtractApiClient(c *gin.Context) remote.Client {
|
||||||
|
if v, ok := c.Get("api_client"); ok {
|
||||||
|
return v.(remote.Client)
|
||||||
|
}
|
||||||
|
panic("middleware/middlware: cannot extract api clinet: not present in context")
|
||||||
|
}
|
||||||
|
|
||||||
// ExtractManager returns the server manager instance set on the request context.
|
// ExtractManager returns the server manager instance set on the request context.
|
||||||
func ExtractManager(c *gin.Context) *server.Manager {
|
func ExtractManager(c *gin.Context) *server.Manager {
|
||||||
if v, ok := c.Get("manager"); ok {
|
if v, ok := c.Get("manager"); ok {
|
||||||
|
|
|
@ -3,18 +3,19 @@ package router
|
||||||
import (
|
import (
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/pterodactyl/wings/remote"
|
||||||
"github.com/pterodactyl/wings/router/middleware"
|
"github.com/pterodactyl/wings/router/middleware"
|
||||||
"github.com/pterodactyl/wings/server"
|
"github.com/pterodactyl/wings/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Configure configures the routing infrastructure for this daemon instance.
|
// Configure configures the routing infrastructure for this daemon instance.
|
||||||
func Configure(m *server.Manager) *gin.Engine {
|
func Configure(m *server.Manager, client remote.Client) *gin.Engine {
|
||||||
gin.SetMode("release")
|
gin.SetMode("release")
|
||||||
|
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
router.Use(gin.Recovery())
|
router.Use(gin.Recovery())
|
||||||
router.Use(middleware.AttachRequestID(), middleware.CaptureErrors(), middleware.SetAccessControlHeaders())
|
router.Use(middleware.AttachRequestID(), middleware.CaptureErrors(), middleware.SetAccessControlHeaders())
|
||||||
router.Use(middleware.AttachServerManager(m))
|
router.Use(middleware.AttachServerManager(m), middleware.AttachApiClient(client))
|
||||||
// @todo log this into a different file so you can setup IP blocking for abusive requests and such.
|
// @todo log this into a different file so you can setup IP blocking for abusive requests and such.
|
||||||
// This should still dump requests in debug mode since it does help with understanding the request
|
// This should still dump requests in debug mode since it does help with understanding the request
|
||||||
// lifecycle and quickly seeing what was called leading to the logs. However, it isn't feasible to mix
|
// lifecycle and quickly seeing what was called leading to the logs. However, it isn't feasible to mix
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
|
|
||||||
// Handle a download request for a server backup.
|
// Handle a download request for a server backup.
|
||||||
func getDownloadBackup(c *gin.Context) {
|
func getDownloadBackup(c *gin.Context) {
|
||||||
|
client := middleware.ExtractApiClient(c)
|
||||||
manager := middleware.ExtractManager(c)
|
manager := middleware.ExtractManager(c)
|
||||||
|
|
||||||
token := tokens.BackupPayload{}
|
token := tokens.BackupPayload{}
|
||||||
|
@ -31,7 +32,7 @@ func getDownloadBackup(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b, st, err := backup.LocateLocal(token.BackupUuid)
|
b, st, err := backup.LocateLocal(client, token.BackupUuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
// provided backup adapter.
|
// provided backup adapter.
|
||||||
func postServerBackup(c *gin.Context) {
|
func postServerBackup(c *gin.Context) {
|
||||||
s := middleware.ExtractServer(c)
|
s := middleware.ExtractServer(c)
|
||||||
|
client := middleware.ExtractApiClient(c)
|
||||||
logger := middleware.ExtractLogger(c)
|
logger := middleware.ExtractLogger(c)
|
||||||
var data struct {
|
var data struct {
|
||||||
Adapter backup.AdapterType `json:"adapter"`
|
Adapter backup.AdapterType `json:"adapter"`
|
||||||
|
@ -30,9 +31,9 @@ func postServerBackup(c *gin.Context) {
|
||||||
var adapter backup.BackupInterface
|
var adapter backup.BackupInterface
|
||||||
switch data.Adapter {
|
switch data.Adapter {
|
||||||
case backup.LocalBackupAdapter:
|
case backup.LocalBackupAdapter:
|
||||||
adapter = backup.NewLocal(data.Uuid, data.Ignore)
|
adapter = backup.NewLocal(client, data.Uuid, data.Ignore)
|
||||||
case backup.S3BackupAdapter:
|
case backup.S3BackupAdapter:
|
||||||
adapter = backup.NewS3(data.Uuid, data.Ignore)
|
adapter = backup.NewS3(client, data.Uuid, data.Ignore)
|
||||||
default:
|
default:
|
||||||
middleware.CaptureAndAbort(c, errors.New("router/backups: provided adapter is not valid: "+string(data.Adapter)))
|
middleware.CaptureAndAbort(c, errors.New("router/backups: provided adapter is not valid: "+string(data.Adapter)))
|
||||||
return
|
return
|
||||||
|
@ -65,6 +66,7 @@ func postServerBackup(c *gin.Context) {
|
||||||
// TODO: stop the server if it is running; internally mark it as suspended
|
// TODO: stop the server if it is running; internally mark it as suspended
|
||||||
func postServerRestoreBackup(c *gin.Context) {
|
func postServerRestoreBackup(c *gin.Context) {
|
||||||
s := middleware.ExtractServer(c)
|
s := middleware.ExtractServer(c)
|
||||||
|
client := middleware.ExtractApiClient(c)
|
||||||
logger := middleware.ExtractLogger(c)
|
logger := middleware.ExtractLogger(c)
|
||||||
|
|
||||||
var data struct {
|
var data struct {
|
||||||
|
@ -94,7 +96,7 @@ func postServerRestoreBackup(c *gin.Context) {
|
||||||
// Now that we've cleaned up the data directory if necessary, grab the backup file
|
// Now that we've cleaned up the data directory if necessary, grab the backup file
|
||||||
// and attempt to restore it into the server directory.
|
// and attempt to restore it into the server directory.
|
||||||
if data.Adapter == backup.LocalBackupAdapter {
|
if data.Adapter == backup.LocalBackupAdapter {
|
||||||
b, _, err := backup.LocateLocal(c.Param("backup"))
|
b, _, err := backup.LocateLocal(client, c.Param("backup"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
middleware.CaptureAndAbort(c, err)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
|
@ -114,7 +116,7 @@ func postServerRestoreBackup(c *gin.Context) {
|
||||||
|
|
||||||
// Since this is not a local backup we need to stream the archive and then
|
// Since this is not a local backup we need to stream the archive and then
|
||||||
// parse over the contents as we go in order to restore it to the server.
|
// parse over the contents as we go in order to restore it to the server.
|
||||||
client := http.Client{}
|
httpClient := http.Client{}
|
||||||
logger.Info("downloading backup from remote location...")
|
logger.Info("downloading backup from remote location...")
|
||||||
// TODO: this will hang if there is an issue. We can't use c.Request.Context() (or really any)
|
// TODO: this will hang if there is an issue. We can't use c.Request.Context() (or really any)
|
||||||
// since it will be canceled when the request is closed which happens quickly since we push
|
// since it will be canceled when the request is closed which happens quickly since we push
|
||||||
|
@ -127,7 +129,7 @@ func postServerRestoreBackup(c *gin.Context) {
|
||||||
middleware.CaptureAndAbort(c, err)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
res, err := client.Do(req)
|
res, err := httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
middleware.CaptureAndAbort(c, err)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
|
@ -143,7 +145,7 @@ func postServerRestoreBackup(c *gin.Context) {
|
||||||
|
|
||||||
go func(s *server.Server, uuid string, logger *log.Entry) {
|
go func(s *server.Server, uuid string, logger *log.Entry) {
|
||||||
logger.Info("starting restoration process for server backup using S3 driver")
|
logger.Info("starting restoration process for server backup using S3 driver")
|
||||||
if err := s.RestoreBackup(backup.NewS3(uuid, ""), res.Body); err != nil {
|
if err := s.RestoreBackup(backup.NewS3(client, uuid, ""), res.Body); err != nil {
|
||||||
logger.WithField("error", errors.WithStack(err)).Error("failed to restore remote S3 backup to server")
|
logger.WithField("error", errors.WithStack(err)).Error("failed to restore remote S3 backup to server")
|
||||||
}
|
}
|
||||||
s.Events().Publish(server.DaemonMessageEvent, "Completed server restoration from S3 backup.")
|
s.Events().Publish(server.DaemonMessageEvent, "Completed server restoration from S3 backup.")
|
||||||
|
@ -159,7 +161,7 @@ func postServerRestoreBackup(c *gin.Context) {
|
||||||
// endpoint can make its own decisions as to how it wants to handle that
|
// endpoint can make its own decisions as to how it wants to handle that
|
||||||
// response.
|
// response.
|
||||||
func deleteServerBackup(c *gin.Context) {
|
func deleteServerBackup(c *gin.Context) {
|
||||||
b, _, err := backup.LocateLocal(c.Param("backup"))
|
b, _, err := backup.LocateLocal(middleware.ExtractApiClient(c), c.Param("backup"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Just return from the function at this point if the backup was not located.
|
// Just return from the function at this point if the backup was not located.
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
|
|
@ -34,10 +34,11 @@ func getAllServers(c *gin.Context) {
|
||||||
// Creates a new server on the wings daemon and begins the installation process
|
// Creates a new server on the wings daemon and begins the installation process
|
||||||
// for it.
|
// for it.
|
||||||
func postCreateServer(c *gin.Context) {
|
func postCreateServer(c *gin.Context) {
|
||||||
|
manager := middleware.ExtractManager(c)
|
||||||
buf := bytes.Buffer{}
|
buf := bytes.Buffer{}
|
||||||
buf.ReadFrom(c.Request.Body)
|
buf.ReadFrom(c.Request.Body)
|
||||||
|
|
||||||
install, err := installer.New(buf.Bytes())
|
install, err := installer.New(c.Request.Context(), manager, buf.Bytes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if installer.IsValidationError(err) {
|
if installer.IsValidationError(err) {
|
||||||
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
|
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
|
||||||
|
@ -52,7 +53,6 @@ func postCreateServer(c *gin.Context) {
|
||||||
|
|
||||||
// Plop that server instance onto the request so that it can be referenced in
|
// Plop that server instance onto the request so that it can be referenced in
|
||||||
// requests from here-on out.
|
// requests from here-on out.
|
||||||
manager := middleware.ExtractManager(c)
|
|
||||||
manager.Add(install.Server())
|
manager.Add(install.Server())
|
||||||
|
|
||||||
// Begin the installation process in the background to not block the request
|
// Begin the installation process in the background to not block the request
|
||||||
|
|
|
@ -2,6 +2,7 @@ package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
@ -22,9 +23,9 @@ import (
|
||||||
"github.com/juju/ratelimit"
|
"github.com/juju/ratelimit"
|
||||||
"github.com/mholt/archiver/v3"
|
"github.com/mholt/archiver/v3"
|
||||||
"github.com/mitchellh/colorstring"
|
"github.com/mitchellh/colorstring"
|
||||||
"github.com/pterodactyl/wings/api"
|
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"github.com/pterodactyl/wings/installer"
|
"github.com/pterodactyl/wings/installer"
|
||||||
|
"github.com/pterodactyl/wings/remote"
|
||||||
"github.com/pterodactyl/wings/router/middleware"
|
"github.com/pterodactyl/wings/router/middleware"
|
||||||
"github.com/pterodactyl/wings/router/tokens"
|
"github.com/pterodactyl/wings/router/tokens"
|
||||||
"github.com/pterodactyl/wings/server"
|
"github.com/pterodactyl/wings/server"
|
||||||
|
@ -109,10 +110,10 @@ func getServerArchive(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func postServerArchive(c *gin.Context) {
|
func postServerArchive(c *gin.Context) {
|
||||||
s := ExtractServer(c)
|
s := middleware.ExtractServer(c)
|
||||||
|
manager := middleware.ExtractManager(c)
|
||||||
|
|
||||||
go func(s *server.Server) {
|
go func(s *server.Server) {
|
||||||
r := api.New()
|
|
||||||
l := log.WithField("server", s.Id())
|
l := log.WithField("server", s.Id())
|
||||||
|
|
||||||
// This function automatically adds the Source Node prefix and Timestamp to the log
|
// This function automatically adds the Source Node prefix and Timestamp to the log
|
||||||
|
@ -133,12 +134,11 @@ func postServerArchive(c *gin.Context) {
|
||||||
|
|
||||||
// Mark the server as not being transferred so it can actually be used.
|
// Mark the server as not being transferred so it can actually be used.
|
||||||
s.SetTransferring(false)
|
s.SetTransferring(false)
|
||||||
|
|
||||||
s.Events().Publish(server.TransferStatusEvent, "failure")
|
s.Events().Publish(server.TransferStatusEvent, "failure")
|
||||||
|
|
||||||
sendTransferLog("Attempting to notify panel of archive failure..")
|
sendTransferLog("Attempting to notify panel of archive failure..")
|
||||||
if err := r.SendArchiveStatus(s.Id(), false); err != nil {
|
if err := manager.Client().SetArchiveStatus(s.Context(), s.Id(), false); err != nil {
|
||||||
if !api.IsRequestError(err) {
|
if !remote.IsRequestError(err) {
|
||||||
sendTransferLog("Failed to notify panel of archive failure: " + err.Error())
|
sendTransferLog("Failed to notify panel of archive failure: " + err.Error())
|
||||||
l.WithField("error", err).Error("failed to notify panel of failed archive status")
|
l.WithField("error", err).Error("failed to notify panel of failed archive status")
|
||||||
return
|
return
|
||||||
|
@ -174,8 +174,8 @@ func postServerArchive(c *gin.Context) {
|
||||||
sendTransferLog("Successfully created archive, attempting to notify panel..")
|
sendTransferLog("Successfully created archive, attempting to notify panel..")
|
||||||
l.Info("successfully created server transfer archive, notifying panel..")
|
l.Info("successfully created server transfer archive, notifying panel..")
|
||||||
|
|
||||||
if err := r.SendArchiveStatus(s.Id(), true); err != nil {
|
if err := manager.Client().SetArchiveStatus(s.Context(), s.Id(), true); err != nil {
|
||||||
if !api.IsRequestError(err) {
|
if !remote.IsRequestError(err) {
|
||||||
sendTransferLog("Failed to notify panel of archive success: " + err.Error())
|
sendTransferLog("Failed to notify panel of archive success: " + err.Error())
|
||||||
l.WithField("error", err).Error("failed to notify panel of successful archive status")
|
l.WithField("error", err).Error("failed to notify panel of successful archive status")
|
||||||
return
|
return
|
||||||
|
@ -275,10 +275,10 @@ func (str serverTransferRequest) verifyChecksum(matches string) (bool, string, e
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sends a notification to the Panel letting it know what the status of this transfer is.
|
// Sends a notification to the Panel letting it know what the status of this transfer is.
|
||||||
func (str serverTransferRequest) sendTransferStatus(successful bool) error {
|
func (str serverTransferRequest) sendTransferStatus(client remote.Client, successful bool) error {
|
||||||
lg := str.log().WithField("transfer_successful", successful)
|
lg := str.log().WithField("transfer_successful", successful)
|
||||||
lg.Info("notifying Panel of server transfer state")
|
lg.Info("notifying Panel of server transfer state")
|
||||||
if err := api.New().SendTransferStatus(str.ServerID, successful); err != nil {
|
if err := client.SetTransferStatus(context.Background(), str.ServerID, successful); err != nil {
|
||||||
lg.WithField("error", err).Error("error notifying panel of transfer state")
|
lg.WithField("error", err).Error("error notifying panel of transfer state")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -294,6 +294,7 @@ func postTransfer(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
manager := middleware.ExtractManager(c)
|
||||||
u, err := uuid.Parse(data.ServerID)
|
u, err := uuid.Parse(data.ServerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
WithError(c, err)
|
WithError(c, err)
|
||||||
|
@ -310,9 +311,9 @@ func postTransfer(c *gin.Context) {
|
||||||
|
|
||||||
// Create a new server installer. This will only configure the environment and not
|
// Create a new server installer. This will only configure the environment and not
|
||||||
// run the installer scripts.
|
// run the installer scripts.
|
||||||
i, err := installer.New(data.Server)
|
i, err := installer.New(context.Background(), manager, data.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = data.sendTransferStatus(false)
|
_ = data.sendTransferStatus(manager.Client(), false)
|
||||||
data.log().WithField("error", err).Error("failed to validate received server data")
|
data.log().WithField("error", err).Error("failed to validate received server data")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -324,7 +325,6 @@ func postTransfer(c *gin.Context) {
|
||||||
i.Server().Events().Publish(server.TransferLogsEvent, output)
|
i.Server().Events().Publish(server.TransferLogsEvent, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
manager := middleware.ExtractManager(c)
|
|
||||||
// Mark the server as transferring to prevent problems later on during the process and
|
// Mark the server as transferring to prevent problems later on during the process and
|
||||||
// then push the server into the global server collection for this instance.
|
// then push the server into the global server collection for this instance.
|
||||||
i.Server().SetTransferring(true)
|
i.Server().SetTransferring(true)
|
||||||
|
@ -332,7 +332,7 @@ func postTransfer(c *gin.Context) {
|
||||||
defer func(s *server.Server) {
|
defer func(s *server.Server) {
|
||||||
// In the event that this transfer call fails, remove the server from the global
|
// In the event that this transfer call fails, remove the server from the global
|
||||||
// server tracking so that we don't have a dangling instance.
|
// server tracking so that we don't have a dangling instance.
|
||||||
if err := data.sendTransferStatus(!hasError); hasError || err != nil {
|
if err := data.sendTransferStatus(manager.Client(), !hasError); hasError || err != nil {
|
||||||
sendTransferLog("Server transfer failed, check Wings logs for additional information.")
|
sendTransferLog("Server transfer failed, check Wings logs for additional information.")
|
||||||
s.Events().Publish(server.TransferStatusEvent, "failure")
|
s.Events().Publish(server.TransferStatusEvent, "failure")
|
||||||
manager.Remove(func(match *server.Server) bool {
|
manager.Remove(func(match *server.Server) bool {
|
||||||
|
|
|
@ -8,16 +8,16 @@ import (
|
||||||
"emperror.dev/errors"
|
"emperror.dev/errors"
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/pterodactyl/wings/api"
|
|
||||||
"github.com/pterodactyl/wings/environment"
|
"github.com/pterodactyl/wings/environment"
|
||||||
|
"github.com/pterodactyl/wings/remote"
|
||||||
"github.com/pterodactyl/wings/server/backup"
|
"github.com/pterodactyl/wings/server/backup"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Notifies the panel of a backup's state and returns an error if one is encountered
|
// Notifies the panel of a backup's state and returns an error if one is encountered
|
||||||
// while performing this action.
|
// while performing this action.
|
||||||
func (s *Server) notifyPanelOfBackup(uuid string, ad *backup.ArchiveDetails, successful bool) error {
|
func (s *Server) notifyPanelOfBackup(uuid string, ad *backup.ArchiveDetails, successful bool) error {
|
||||||
if err := api.New().SendBackupStatus(uuid, ad.ToRequest(successful)); err != nil {
|
if err := s.client.SetBackupStatus(s.Context(), uuid, ad.ToRequest(successful)); err != nil {
|
||||||
if !api.IsRequestError(err) {
|
if !remote.IsRequestError(err) {
|
||||||
s.Log().WithFields(log.Fields{
|
s.Log().WithFields(log.Fields{
|
||||||
"backup": uuid,
|
"backup": uuid,
|
||||||
"error": err,
|
"error": err,
|
||||||
|
@ -131,7 +131,7 @@ func (s *Server) RestoreBackup(b backup.BackupInterface, reader io.ReadCloser) (
|
||||||
// Send an API call to the Panel as soon as this function is done running so that
|
// Send an API call to the Panel as soon as this function is done running so that
|
||||||
// the Panel is informed of the restoration status of this backup.
|
// the Panel is informed of the restoration status of this backup.
|
||||||
defer func() {
|
defer func() {
|
||||||
if rerr := api.New().SendRestorationStatus(b.Identifier(), err == nil); rerr != nil {
|
if rerr := s.client.SendRestorationStatus(s.Context(), b.Identifier(), err == nil); rerr != nil {
|
||||||
s.Log().WithField("error", rerr).WithField("backup", b.Identifier()).Error("failed to notify Panel of backup restoration status")
|
s.Log().WithField("error", rerr).WithField("backup", b.Identifier()).Error("failed to notify Panel of backup restoration status")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
|
@ -9,8 +9,8 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/pterodactyl/wings/api"
|
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"github.com/pterodactyl/wings/remote"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AdapterType string
|
type AdapterType string
|
||||||
|
@ -31,8 +31,8 @@ type ArchiveDetails struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToRequest returns a request object.
|
// ToRequest returns a request object.
|
||||||
func (ad *ArchiveDetails) ToRequest(successful bool) api.BackupRequest {
|
func (ad *ArchiveDetails) ToRequest(successful bool) remote.BackupRequest {
|
||||||
return api.BackupRequest{
|
return remote.BackupRequest{
|
||||||
Checksum: ad.Checksum,
|
Checksum: ad.Checksum,
|
||||||
ChecksumType: ad.ChecksumType,
|
ChecksumType: ad.ChecksumType,
|
||||||
Size: ad.Size,
|
Size: ad.Size,
|
||||||
|
@ -49,12 +49,15 @@ type Backup struct {
|
||||||
// compatible with a standard .gitignore structure.
|
// compatible with a standard .gitignore structure.
|
||||||
Ignore string `json:"ignore"`
|
Ignore string `json:"ignore"`
|
||||||
|
|
||||||
|
client remote.Client
|
||||||
adapter AdapterType
|
adapter AdapterType
|
||||||
logContext map[string]interface{}
|
logContext map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// noinspection GoNameStartsWithPackageName
|
// noinspection GoNameStartsWithPackageName
|
||||||
type BackupInterface interface {
|
type BackupInterface interface {
|
||||||
|
// SetClient sets the API request client on the backup interface.
|
||||||
|
SetClient(c remote.Client)
|
||||||
// Identifier returns the UUID of this backup as tracked by the panel
|
// Identifier returns the UUID of this backup as tracked by the panel
|
||||||
// instance.
|
// instance.
|
||||||
Identifier() string
|
Identifier() string
|
||||||
|
@ -84,6 +87,10 @@ type BackupInterface interface {
|
||||||
Restore(reader io.Reader, callback RestoreCallback) error
|
Restore(reader io.Reader, callback RestoreCallback) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Backup) SetClient(c remote.Client) {
|
||||||
|
b.client = c
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Backup) Identifier() string {
|
func (b *Backup) Identifier() string {
|
||||||
return b.Uuid
|
return b.Uuid
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/mholt/archiver/v3"
|
"github.com/mholt/archiver/v3"
|
||||||
|
"github.com/pterodactyl/wings/remote"
|
||||||
"github.com/pterodactyl/wings/system"
|
"github.com/pterodactyl/wings/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,9 +16,10 @@ type LocalBackup struct {
|
||||||
|
|
||||||
var _ BackupInterface = (*LocalBackup)(nil)
|
var _ BackupInterface = (*LocalBackup)(nil)
|
||||||
|
|
||||||
func NewLocal(uuid string, ignore string) *LocalBackup {
|
func NewLocal(client remote.Client, uuid string, ignore string) *LocalBackup {
|
||||||
return &LocalBackup{
|
return &LocalBackup{
|
||||||
Backup{
|
Backup{
|
||||||
|
client: client,
|
||||||
Uuid: uuid,
|
Uuid: uuid,
|
||||||
Ignore: ignore,
|
Ignore: ignore,
|
||||||
adapter: LocalBackupAdapter,
|
adapter: LocalBackupAdapter,
|
||||||
|
@ -27,14 +29,8 @@ func NewLocal(uuid string, ignore string) *LocalBackup {
|
||||||
|
|
||||||
// LocateLocal finds the backup for a server and returns the local path. This
|
// LocateLocal finds the backup for a server and returns the local path. This
|
||||||
// will obviously only work if the backup was created as a local backup.
|
// will obviously only work if the backup was created as a local backup.
|
||||||
func LocateLocal(uuid string) (*LocalBackup, os.FileInfo, error) {
|
func LocateLocal(client remote.Client, uuid string) (*LocalBackup, os.FileInfo, error) {
|
||||||
b := &LocalBackup{
|
b := NewLocal(client, uuid, "")
|
||||||
Backup{
|
|
||||||
Uuid: uuid,
|
|
||||||
Ignore: "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
st, err := os.Stat(b.Path())
|
st, err := os.Stat(b.Path())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
|
|
|
@ -3,6 +3,7 @@ package backup
|
||||||
import (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -10,8 +11,8 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/juju/ratelimit"
|
"github.com/juju/ratelimit"
|
||||||
"github.com/pterodactyl/wings/api"
|
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"github.com/pterodactyl/wings/remote"
|
||||||
)
|
)
|
||||||
|
|
||||||
type S3Backup struct {
|
type S3Backup struct {
|
||||||
|
@ -20,9 +21,10 @@ type S3Backup struct {
|
||||||
|
|
||||||
var _ BackupInterface = (*S3Backup)(nil)
|
var _ BackupInterface = (*S3Backup)(nil)
|
||||||
|
|
||||||
func NewS3(uuid string, ignore string) *S3Backup {
|
func NewS3(client remote.Client, uuid string, ignore string) *S3Backup {
|
||||||
return &S3Backup{
|
return &S3Backup{
|
||||||
Backup{
|
Backup{
|
||||||
|
client: client,
|
||||||
Uuid: uuid,
|
Uuid: uuid,
|
||||||
Ignore: ignore,
|
Ignore: ignore,
|
||||||
adapter: S3BackupAdapter,
|
adapter: S3BackupAdapter,
|
||||||
|
@ -91,7 +93,7 @@ func (s *S3Backup) generateRemoteRequest(rc io.ReadCloser) error {
|
||||||
s.log().WithField("size", size).Debug("got size of backup")
|
s.log().WithField("size", size).Debug("got size of backup")
|
||||||
|
|
||||||
s.log().Debug("attempting to get S3 upload urls from Panel...")
|
s.log().Debug("attempting to get S3 upload urls from Panel...")
|
||||||
urls, err := api.New().GetBackupRemoteUploadURLs(s.Backup.Uuid, size)
|
urls, err := s.client.GetBackupRemoteUploadURLs(context.Background(), s.Backup.Uuid, size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,9 @@ import (
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/mount"
|
"github.com/docker/docker/api/types/mount"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/pterodactyl/wings/api"
|
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"github.com/pterodactyl/wings/environment"
|
"github.com/pterodactyl/wings/environment"
|
||||||
|
"github.com/pterodactyl/wings/remote"
|
||||||
"github.com/pterodactyl/wings/system"
|
"github.com/pterodactyl/wings/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -88,9 +88,9 @@ func (s *Server) Reinstall() error {
|
||||||
|
|
||||||
// Internal installation function used to simplify reporting back to the Panel.
|
// Internal installation function used to simplify reporting back to the Panel.
|
||||||
func (s *Server) internalInstall() error {
|
func (s *Server) internalInstall() error {
|
||||||
script, err := api.New().GetInstallationScript(s.Id())
|
script, err := s.client.GetInstallationScript(s.Context(), s.Id())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !api.IsRequestError(err) {
|
if !remote.IsRequestError(err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,7 +113,7 @@ func (s *Server) internalInstall() error {
|
||||||
|
|
||||||
type InstallationProcess struct {
|
type InstallationProcess struct {
|
||||||
Server *Server
|
Server *Server
|
||||||
Script *api.InstallationScript
|
Script *remote.InstallationScript
|
||||||
|
|
||||||
client *client.Client
|
client *client.Client
|
||||||
context context.Context
|
context context.Context
|
||||||
|
@ -121,7 +121,7 @@ type InstallationProcess struct {
|
||||||
|
|
||||||
// Generates a new installation process struct that will be used to create containers,
|
// Generates a new installation process struct that will be used to create containers,
|
||||||
// and otherwise perform installation commands for a server.
|
// and otherwise perform installation commands for a server.
|
||||||
func NewInstallationProcess(s *Server, script *api.InstallationScript) (*InstallationProcess, error) {
|
func NewInstallationProcess(s *Server, script *remote.InstallationScript) (*InstallationProcess, error) {
|
||||||
proc := &InstallationProcess{
|
proc := &InstallationProcess{
|
||||||
Script: script,
|
Script: script,
|
||||||
Server: s,
|
Server: s,
|
||||||
|
@ -532,9 +532,9 @@ func (ip *InstallationProcess) StreamOutput(ctx context.Context, id string) erro
|
||||||
// value of "true" means everything was successful, "false" means something went
|
// value of "true" means everything was successful, "false" means something went
|
||||||
// wrong and the server must be deleted and re-created.
|
// wrong and the server must be deleted and re-created.
|
||||||
func (s *Server) SyncInstallState(successful bool) error {
|
func (s *Server) SyncInstallState(successful bool) error {
|
||||||
err := api.New().SendInstallationStatus(s.Id(), successful)
|
err := s.client.SetInstallationStatus(s.Context(), s.Id(), successful)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !api.IsRequestError(err) {
|
if !remote.IsRequestError(err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,10 +7,10 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/pterodactyl/wings/api"
|
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"github.com/pterodactyl/wings/environment"
|
"github.com/pterodactyl/wings/environment"
|
||||||
"github.com/pterodactyl/wings/events"
|
"github.com/pterodactyl/wings/events"
|
||||||
|
"github.com/pterodactyl/wings/remote"
|
||||||
)
|
)
|
||||||
|
|
||||||
var dockerEvents = []string{
|
var dockerEvents = []string{
|
||||||
|
@ -186,7 +186,7 @@ func (s *Server) onConsoleOutput(data string) {
|
||||||
if s.IsRunning() {
|
if s.IsRunning() {
|
||||||
stop := processConfiguration.Stop
|
stop := processConfiguration.Stop
|
||||||
|
|
||||||
if stop.Type == api.ProcessStopCommand && data == stop.Value {
|
if stop.Type == remote.ProcessStopCommand && data == stop.Value {
|
||||||
s.Environment.SetState(environment.ProcessOfflineState)
|
s.Environment.SetState(environment.ProcessOfflineState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
@ -14,13 +15,16 @@ import (
|
||||||
"emperror.dev/errors"
|
"emperror.dev/errors"
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/gammazero/workerpool"
|
"github.com/gammazero/workerpool"
|
||||||
"github.com/pterodactyl/wings/api"
|
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"github.com/pterodactyl/wings/environment"
|
||||||
|
"github.com/pterodactyl/wings/environment/docker"
|
||||||
"github.com/pterodactyl/wings/remote"
|
"github.com/pterodactyl/wings/remote"
|
||||||
|
"github.com/pterodactyl/wings/server/filesystem"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
|
client remote.Client
|
||||||
servers []*Server
|
servers []*Server
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,8 +32,8 @@ type Manager struct {
|
||||||
// the servers that are currently present on the filesystem and set them into
|
// the servers that are currently present on the filesystem and set them into
|
||||||
// the manager.
|
// the manager.
|
||||||
func NewManager(ctx context.Context, client remote.Client) (*Manager, error) {
|
func NewManager(ctx context.Context, client remote.Client) (*Manager, error) {
|
||||||
m := NewEmptyManager()
|
m := NewEmptyManager(client)
|
||||||
if err := m.init(ctx, client); err != nil {
|
if err := m.init(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
@ -38,58 +42,14 @@ func NewManager(ctx context.Context, client remote.Client) (*Manager, error) {
|
||||||
// NewEmptyManager returns a new empty manager collection without actually
|
// NewEmptyManager returns a new empty manager collection without actually
|
||||||
// loading any of the servers from the disk. This allows the caller to set their
|
// loading any of the servers from the disk. This allows the caller to set their
|
||||||
// own servers into the collection as needed.
|
// own servers into the collection as needed.
|
||||||
func NewEmptyManager() *Manager {
|
func NewEmptyManager(client remote.Client) *Manager {
|
||||||
return &Manager{}
|
return &Manager{client: client}
|
||||||
}
|
}
|
||||||
|
|
||||||
// initializeFromRemoteSource iterates over a given directory and loads all of
|
// Client returns the HTTP client interface that allows interaction with the
|
||||||
// the servers listed before returning them to the calling function.
|
// Panel API.
|
||||||
func (m *Manager) init(ctx context.Context, client remote.Client) error {
|
func (m *Manager) Client() remote.Client {
|
||||||
log.Info("fetching list of servers from API")
|
return m.client
|
||||||
servers, err := 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 := api.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 := FromConfiguration(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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put replaces all of the current values in the collection with the value that
|
// Put replaces all of the current values in the collection with the value that
|
||||||
|
@ -202,3 +162,103 @@ func (m *Manager) ReadStates() (map[string]string, error) {
|
||||||
}
|
}
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InitServer initializes a server using a data byte array. This will be
|
||||||
|
// marshaled into the given struct using a YAML marshaler. This will also
|
||||||
|
// configure the given environment for a server.
|
||||||
|
func (m *Manager) InitServer(data remote.ServerConfigurationResponse) (*Server, error) {
|
||||||
|
s, err := New(m.client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithMessage(err, "loader: failed to instantiate empty server struct")
|
||||||
|
}
|
||||||
|
if err := s.UpdateDataStructure(data.Settings); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Archiver = Archiver{Server: s}
|
||||||
|
s.fs = filesystem.New(filepath.Join(config.Get().System.Data, s.Id()), s.DiskSpace(), s.Config().Egg.FileDenylist)
|
||||||
|
|
||||||
|
// Right now we only support a Docker based environment, so I'm going to hard code
|
||||||
|
// this logic in. When we're ready to support other environment we'll need to make
|
||||||
|
// some modifications here obviously.
|
||||||
|
settings := environment.Settings{
|
||||||
|
Mounts: s.Mounts(),
|
||||||
|
Allocations: s.cfg.Allocations,
|
||||||
|
Limits: s.cfg.Build,
|
||||||
|
}
|
||||||
|
|
||||||
|
envCfg := environment.NewConfiguration(settings, s.GetEnvironmentVariables())
|
||||||
|
meta := docker.Metadata{
|
||||||
|
Image: s.Config().Container.Image,
|
||||||
|
}
|
||||||
|
|
||||||
|
if env, err := docker.New(s.Id(), &meta, envCfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
s.Environment = env
|
||||||
|
s.StartEventListeners()
|
||||||
|
s.Throttler().StartTimer(s.Context())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forces the configuration to be synced with the panel.
|
||||||
|
if err := s.SyncWithConfiguration(data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the server's data directory exists, force disk usage calculation.
|
||||||
|
if _, err := os.Stat(s.Filesystem().Path()); err == nil {
|
||||||
|
s.Filesystem().HasSpaceAvailable(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
|
@ -4,18 +4,17 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"emperror.dev/errors"
|
"emperror.dev/errors"
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/creasty/defaults"
|
"github.com/creasty/defaults"
|
||||||
"github.com/pterodactyl/wings/api"
|
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"github.com/pterodactyl/wings/environment"
|
"github.com/pterodactyl/wings/environment"
|
||||||
"github.com/pterodactyl/wings/environment/docker"
|
"github.com/pterodactyl/wings/environment/docker"
|
||||||
"github.com/pterodactyl/wings/events"
|
"github.com/pterodactyl/wings/events"
|
||||||
|
"github.com/pterodactyl/wings/remote"
|
||||||
"github.com/pterodactyl/wings/server/filesystem"
|
"github.com/pterodactyl/wings/server/filesystem"
|
||||||
"github.com/pterodactyl/wings/system"
|
"github.com/pterodactyl/wings/system"
|
||||||
"golang.org/x/sync/semaphore"
|
"golang.org/x/sync/semaphore"
|
||||||
|
@ -36,7 +35,8 @@ type Server struct {
|
||||||
|
|
||||||
// Maintains the configuration for the server. This is the data that gets returned by the Panel
|
// Maintains the configuration for the server. This is the data that gets returned by the Panel
|
||||||
// such as build settings and container images.
|
// such as build settings and container images.
|
||||||
cfg Configuration
|
cfg Configuration
|
||||||
|
client remote.Client
|
||||||
|
|
||||||
// The crash handler for this server instance.
|
// The crash handler for this server instance.
|
||||||
crasher CrashHandler
|
crasher CrashHandler
|
||||||
|
@ -53,7 +53,7 @@ type Server struct {
|
||||||
// Defines the process configuration for the server instance. This is dynamically
|
// Defines the process configuration for the server instance. This is dynamically
|
||||||
// fetched from the Pterodactyl Server instance each time the server process is
|
// fetched from the Pterodactyl Server instance each time the server process is
|
||||||
// started, and then cached here.
|
// started, and then cached here.
|
||||||
procConfig *api.ProcessConfiguration
|
procConfig *remote.ProcessConfiguration
|
||||||
|
|
||||||
// Tracks the installation process for this server and prevents a server from running
|
// Tracks the installation process for this server and prevents a server from running
|
||||||
// two installer processes at the same time. This also allows us to cancel a running
|
// two installer processes at the same time. This also allows us to cancel a running
|
||||||
|
@ -72,11 +72,12 @@ type Server struct {
|
||||||
|
|
||||||
// Returns a new server instance with a context and all of the default values set on
|
// Returns a new server instance with a context and all of the default values set on
|
||||||
// the instance.
|
// the instance.
|
||||||
func New() (*Server, error) {
|
func New(client remote.Client) (*Server, error) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
s := Server{
|
s := Server{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
ctxCancel: &cancel,
|
ctxCancel: &cancel,
|
||||||
|
client: client,
|
||||||
installing: system.NewAtomicBool(false),
|
installing: system.NewAtomicBool(false),
|
||||||
transferring: system.NewAtomicBool(false),
|
transferring: system.NewAtomicBool(false),
|
||||||
}
|
}
|
||||||
|
@ -148,13 +149,13 @@ func (s *Server) Log() *log.Entry {
|
||||||
// This also means mass actions can be performed against servers on the Panel and they
|
// This also means mass actions can be performed against servers on the Panel and they
|
||||||
// will automatically sync with Wings when the server is started.
|
// will automatically sync with Wings when the server is started.
|
||||||
func (s *Server) Sync() error {
|
func (s *Server) Sync() error {
|
||||||
cfg, err := api.New().GetServerConfiguration(s.Id())
|
cfg, err := s.client.GetServerConfiguration(s.Context(), s.Id())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !api.IsRequestError(err) {
|
if !remote.IsRequestError(err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err.(*api.RequestError).Status == "404" {
|
if err.(*remote.RequestError).Status == "404" {
|
||||||
return &serverDoesNotExist{}
|
return &serverDoesNotExist{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,7 +165,7 @@ func (s *Server) Sync() error {
|
||||||
return s.SyncWithConfiguration(cfg)
|
return s.SyncWithConfiguration(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) SyncWithConfiguration(cfg api.ServerConfigurationResponse) error {
|
func (s *Server) SyncWithConfiguration(cfg remote.ServerConfigurationResponse) error {
|
||||||
// Update the data structure and persist it to the disk.
|
// Update the data structure and persist it to the disk.
|
||||||
if err := s.UpdateDataStructure(cfg.Settings); err != nil {
|
if err := s.UpdateDataStructure(cfg.Settings); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -218,7 +219,7 @@ func (s *Server) IsSuspended() bool {
|
||||||
return s.Config().Suspended
|
return s.Config().Suspended
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) ProcessConfiguration() *api.ProcessConfiguration {
|
func (s *Server) ProcessConfiguration() *remote.ProcessConfiguration {
|
||||||
s.RLock()
|
s.RLock()
|
||||||
defer s.RUnlock()
|
defer s.RUnlock()
|
||||||
|
|
||||||
|
@ -295,61 +296,11 @@ func (s *Server) OnStateChange() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determines if the server state is running or not. This is different than the
|
// IsRunning determines if the server state is running or not. This is different
|
||||||
// environment state, it is simply the tracked state from this daemon instance, and
|
// than the environment state, it is simply the tracked state from this daemon
|
||||||
// not the response from Docker.
|
// instance, and not the response from Docker.
|
||||||
func (s *Server) IsRunning() bool {
|
func (s *Server) IsRunning() bool {
|
||||||
st := s.Environment.State()
|
st := s.Environment.State()
|
||||||
|
|
||||||
return st == environment.ProcessRunningState || st == environment.ProcessStartingState
|
return st == environment.ProcessRunningState || st == environment.ProcessStartingState
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromConfiguration initializes a server using a data byte array. This will be
|
|
||||||
// marshaled into the given struct using a YAML marshaler. This will also
|
|
||||||
// configure the given environment for a server.
|
|
||||||
func FromConfiguration(data api.ServerConfigurationResponse) (*Server, error) {
|
|
||||||
s, err := New()
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithMessage(err, "loader: failed to instantiate empty server struct")
|
|
||||||
}
|
|
||||||
if err := s.UpdateDataStructure(data.Settings); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Archiver = Archiver{Server: s}
|
|
||||||
s.fs = filesystem.New(filepath.Join(config.Get().System.Data, s.Id()), s.DiskSpace(), s.Config().Egg.FileDenylist)
|
|
||||||
|
|
||||||
// Right now we only support a Docker based environment, so I'm going to hard code
|
|
||||||
// this logic in. When we're ready to support other environment we'll need to make
|
|
||||||
// some modifications here obviously.
|
|
||||||
settings := environment.Settings{
|
|
||||||
Mounts: s.Mounts(),
|
|
||||||
Allocations: s.cfg.Allocations,
|
|
||||||
Limits: s.cfg.Build,
|
|
||||||
}
|
|
||||||
|
|
||||||
envCfg := environment.NewConfiguration(settings, s.GetEnvironmentVariables())
|
|
||||||
meta := docker.Metadata{
|
|
||||||
Image: s.Config().Container.Image,
|
|
||||||
}
|
|
||||||
|
|
||||||
if env, err := docker.New(s.Id(), &meta, envCfg); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
s.Environment = env
|
|
||||||
s.StartEventListeners()
|
|
||||||
s.Throttler().StartTimer(s.Context())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forces the configuration to be synced with the panel.
|
|
||||||
if err := s.SyncWithConfiguration(data); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the server's data directory exists, force disk usage calculation.
|
|
||||||
if _, err := os.Stat(s.Filesystem().Path()); err == nil {
|
|
||||||
s.Filesystem().HasSpaceAvailable(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package sftp
|
package sftp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
@ -10,18 +11,24 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"emperror.dev/errors"
|
"emperror.dev/errors"
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/pkg/sftp"
|
"github.com/pkg/sftp"
|
||||||
"github.com/pterodactyl/wings/api"
|
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"github.com/pterodactyl/wings/remote"
|
||||||
"github.com/pterodactyl/wings/server"
|
"github.com/pterodactyl/wings/server"
|
||||||
"golang.org/x/crypto/ssh"
|
"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
|
//goland:noinspection GoNameStartsWithPackageName
|
||||||
type SFTPServer struct {
|
type SFTPServer struct {
|
||||||
manager *server.Manager
|
manager *server.Manager
|
||||||
|
@ -164,7 +171,7 @@ func (c *SFTPServer) generatePrivateKey() error {
|
||||||
|
|
||||||
// A function capable of validating user credentials with the Panel API.
|
// A function capable of validating user credentials with the Panel API.
|
||||||
func (c *SFTPServer) passwordCallback(conn ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
|
func (c *SFTPServer) passwordCallback(conn ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
|
||||||
request := api.SftpAuthRequest{
|
request := remote.SftpAuthRequest{
|
||||||
User: conn.User(),
|
User: conn.User(),
|
||||||
Pass: string(pass),
|
Pass: string(pass),
|
||||||
IP: conn.RemoteAddr().String(),
|
IP: conn.RemoteAddr().String(),
|
||||||
|
@ -175,9 +182,14 @@ 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 := log.WithFields(log.Fields{"subsystem": "sftp", "username": conn.User(), "ip": conn.RemoteAddr().String()})
|
||||||
logger.Debug("validating credentials for SFTP connection")
|
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 err != nil {
|
||||||
if api.IsInvalidCredentialsError(err) {
|
if _, ok := err.(*remote.SftpInvalidCredentialsError); ok {
|
||||||
logger.Warn("failed to validate user credentials (invalid username or password)")
|
logger.Warn("failed to validate user credentials (invalid username or password)")
|
||||||
} else {
|
} else {
|
||||||
logger.WithField("error", err).Error("encountered an error while trying to validate user credentials")
|
logger.WithField("error", err).Error("encountered an error while trying to validate user credentials")
|
||||||
|
|
Loading…
Reference in New Issue
Block a user