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,
|
||||
}).Info("configured system user successfully")
|
||||
|
||||
pclient := remote.CreateClient(
|
||||
pclient := remote.New(
|
||||
config.Get().PanelLocation,
|
||||
config.Get().AuthenticationTokenId,
|
||||
config.Get().AuthenticationToken,
|
||||
remote.WithTimeout(time.Second*time.Duration(config.Get().RemoteQuery.Timeout)),
|
||||
remote.WithCredentials(config.Get().AuthenticationTokenId, config.Get().AuthenticationToken),
|
||||
remote.WithHttpClient(&http.Client{
|
||||
Timeout: time.Second * time.Duration(config.Get().RemoteQuery.Timeout),
|
||||
}),
|
||||
)
|
||||
|
||||
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
|
||||
// Ensure the archive directory exists.
|
||||
if err := os.MkdirAll(sys.ArchiveDirectory, 0755); err != nil {
|
||||
|
@ -293,7 +303,7 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
|
|||
// and external clients.
|
||||
s := &http.Server{
|
||||
Addr: api.Host + ":" + strconv.Itoa(api.Port),
|
||||
Handler: router.Configure(manager),
|
||||
Handler: router.Configure(manager, pclient),
|
||||
TLSConfig: config.DefaultTLSConfig,
|
||||
}
|
||||
|
||||
|
|
|
@ -10,15 +10,15 @@ import (
|
|||
"github.com/apex/log"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pterodactyl/wings/api"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
"github.com/pterodactyl/wings/events"
|
||||
"github.com/pterodactyl/wings/remote"
|
||||
"github.com/pterodactyl/wings/system"
|
||||
)
|
||||
|
||||
type Metadata struct {
|
||||
Image string
|
||||
Stop api.ProcessStopConfiguration
|
||||
Stop remote.ProcessStopConfiguration
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (e *Environment) SetStopConfiguration(c api.ProcessStopConfiguration) {
|
||||
func (e *Environment) SetStopConfiguration(c remote.ProcessStopConfiguration) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
|
|
|
@ -7,8 +7,9 @@ import (
|
|||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pterodactyl/wings/api"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
"github.com/pterodactyl/wings/remote"
|
||||
|
||||
"os"
|
||||
"strings"
|
||||
"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
|
||||
// logic and end up only executing the container stop command (which may or
|
||||
// may not work as expected).
|
||||
if s.Type == "" || s.Type == api.ProcessStopSignal {
|
||||
if s.Type == "" || s.Type == remote.ProcessStopSignal {
|
||||
if s.Type == "" {
|
||||
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
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
package installer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"emperror.dev/errors"
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/buger/jsonparser"
|
||||
"github.com/pterodactyl/wings/api"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
"github.com/pterodactyl/wings/remote"
|
||||
"github.com/pterodactyl/wings/server"
|
||||
)
|
||||
|
||||
|
@ -15,10 +16,10 @@ type Installer struct {
|
|||
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
|
||||
// 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")) {
|
||||
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")
|
||||
|
||||
c, err := api.New().GetServerConfiguration(cfg.Uuid)
|
||||
c, err := manager.Client().GetServerConfiguration(ctx, cfg.Uuid)
|
||||
if err != nil {
|
||||
if !api.IsRequestError(err) {
|
||||
if !remote.IsRequestError(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, errors.New(err.Error())
|
||||
}
|
||||
|
||||
// Create a new server instance using the configuration we wrote to the disk
|
||||
// so that everything gets instantiated correctly on the struct.
|
||||
s, err := server.FromConfiguration(c)
|
||||
s, err := manager.InitServer(c)
|
||||
|
||||
return &Installer{
|
||||
server: s,
|
||||
}, err
|
||||
return &Installer{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 {
|
||||
return i.server.Id()
|
||||
}
|
||||
|
||||
// Return the server instance.
|
||||
// Server returns the server instance.
|
||||
func (i *Installer) Server() *server.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)
|
||||
}
|
||||
|
||||
type sftpInvalidCredentialsError struct {
|
||||
type SftpInvalidCredentialsError struct {
|
||||
}
|
||||
|
||||
func (ice sftpInvalidCredentialsError) Error() string {
|
||||
func (ice SftpInvalidCredentialsError) Error() string {
|
||||
return "the credentials provided were invalid"
|
||||
}
|
||||
|
||||
func IsInvalidCredentialsError(err error) bool {
|
||||
_, ok := err.(*sftpInvalidCredentialsError)
|
||||
|
||||
return ok
|
||||
}
|
||||
|
|
|
@ -9,26 +9,67 @@ import (
|
|||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/pterodactyl/wings/system"
|
||||
)
|
||||
|
||||
// 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
|
||||
type Client interface {
|
||||
GetBackupRemoteUploadURLs(ctx context.Context, backup string, size int64) (BackupRemoteUploadResponse, error)
|
||||
GetInstallationScript(ctx context.Context, uuid string) (InstallationScript, error)
|
||||
GetServerConfiguration(ctx context.Context, uuid string) (ServerConfigurationResponse, error)
|
||||
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
|
||||
// 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{}
|
||||
type client struct {
|
||||
httpClient *http.Client
|
||||
baseUrl string
|
||||
tokenId string
|
||||
token string
|
||||
retries int
|
||||
}
|
||||
|
||||
// Same concept as d, but a map of strings, used for querying GET requests.
|
||||
type q map[string]string
|
||||
// New returns a new HTTP request client that is used for making authenticated
|
||||
// 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()
|
||||
// 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))
|
||||
}
|
||||
|
||||
// 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
|
||||
// been made the response will be false. This function will evaluate to true if
|
||||
// the response code is anything 300 or higher.
|
||||
|
@ -156,3 +204,26 @@ func (r *Response) Error() error {
|
|||
|
||||
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 (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"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) {
|
||||
c, _ := createTestClient(func(rw http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "application/vnd.pterodactyl.v1+json", r.Header.Get("Accept"))
|
||||
|
|
|
@ -2,12 +2,12 @@ package remote
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/pterodactyl/wings/api"
|
||||
"emperror.dev/errors"
|
||||
"github.com/apex/log"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
|
@ -17,41 +17,10 @@ const (
|
|||
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
|
||||
// parallel API calls to the endpoint if more than one page of servers is
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -81,34 +50,50 @@ func (c *client) GetServers(ctx context.Context, limit int) ([]api.RawServerData
|
|||
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)
|
||||
if err != nil {
|
||||
return api.ServerConfigurationResponse{}, err
|
||||
return config, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.HasError() {
|
||||
return api.ServerConfigurationResponse{}, err
|
||||
return config, err
|
||||
}
|
||||
|
||||
config := api.ServerConfigurationResponse{}
|
||||
err = res.BindJSON(&config)
|
||||
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)
|
||||
if err != nil {
|
||||
return api.InstallationScript{}, err
|
||||
return InstallationScript{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.HasError() {
|
||||
return api.InstallationScript{}, err
|
||||
return InstallationScript{}, err
|
||||
}
|
||||
|
||||
config := api.InstallationScript{}
|
||||
var config InstallationScript
|
||||
err = res.BindJSON(&config)
|
||||
return config, err
|
||||
}
|
||||
|
@ -144,29 +129,97 @@ func (c *client) SetTransferStatus(ctx context.Context, uuid string, successful
|
|||
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
|
||||
// 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{
|
||||
"page": strconv.Itoa(page),
|
||||
"per_page": strconv.Itoa(limit),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, api.Pagination{}, err
|
||||
return nil, r.Meta, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.HasError() {
|
||||
return nil, api.Pagination{}, res.Error()
|
||||
}
|
||||
|
||||
var r struct {
|
||||
Data []api.RawServerData `json:"data"`
|
||||
Meta api.Pagination `json:"meta"`
|
||||
return nil, r.Meta, res.Error()
|
||||
}
|
||||
if err := res.BindJSON(&r); err != nil {
|
||||
return nil, api.Pagination{}, err
|
||||
return nil, r.Meta, err
|
||||
}
|
||||
|
||||
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/google/uuid"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/remote"
|
||||
"github.com/pterodactyl/wings/server"
|
||||
"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
|
||||
// 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.
|
||||
|
@ -327,6 +337,14 @@ func ExtractServer(c *gin.Context) *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.
|
||||
func ExtractManager(c *gin.Context) *server.Manager {
|
||||
if v, ok := c.Get("manager"); ok {
|
||||
|
|
|
@ -3,18 +3,19 @@ package router
|
|||
import (
|
||||
"github.com/apex/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pterodactyl/wings/remote"
|
||||
"github.com/pterodactyl/wings/router/middleware"
|
||||
"github.com/pterodactyl/wings/server"
|
||||
)
|
||||
|
||||
// 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")
|
||||
|
||||
router := gin.New()
|
||||
router.Use(gin.Recovery())
|
||||
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.
|
||||
// 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
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
|
||||
// Handle a download request for a server backup.
|
||||
func getDownloadBackup(c *gin.Context) {
|
||||
client := middleware.ExtractApiClient(c)
|
||||
manager := middleware.ExtractManager(c)
|
||||
|
||||
token := tokens.BackupPayload{}
|
||||
|
@ -31,7 +32,7 @@ func getDownloadBackup(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
b, st, err := backup.LocateLocal(token.BackupUuid)
|
||||
b, st, err := backup.LocateLocal(client, token.BackupUuid)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
||||
|
|
|
@ -17,6 +17,7 @@ import (
|
|||
// provided backup adapter.
|
||||
func postServerBackup(c *gin.Context) {
|
||||
s := middleware.ExtractServer(c)
|
||||
client := middleware.ExtractApiClient(c)
|
||||
logger := middleware.ExtractLogger(c)
|
||||
var data struct {
|
||||
Adapter backup.AdapterType `json:"adapter"`
|
||||
|
@ -30,9 +31,9 @@ func postServerBackup(c *gin.Context) {
|
|||
var adapter backup.BackupInterface
|
||||
switch data.Adapter {
|
||||
case backup.LocalBackupAdapter:
|
||||
adapter = backup.NewLocal(data.Uuid, data.Ignore)
|
||||
adapter = backup.NewLocal(client, data.Uuid, data.Ignore)
|
||||
case backup.S3BackupAdapter:
|
||||
adapter = backup.NewS3(data.Uuid, data.Ignore)
|
||||
adapter = backup.NewS3(client, data.Uuid, data.Ignore)
|
||||
default:
|
||||
middleware.CaptureAndAbort(c, errors.New("router/backups: provided adapter is not valid: "+string(data.Adapter)))
|
||||
return
|
||||
|
@ -65,6 +66,7 @@ func postServerBackup(c *gin.Context) {
|
|||
// TODO: stop the server if it is running; internally mark it as suspended
|
||||
func postServerRestoreBackup(c *gin.Context) {
|
||||
s := middleware.ExtractServer(c)
|
||||
client := middleware.ExtractApiClient(c)
|
||||
logger := middleware.ExtractLogger(c)
|
||||
|
||||
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
|
||||
// and attempt to restore it into the server directory.
|
||||
if data.Adapter == backup.LocalBackupAdapter {
|
||||
b, _, err := backup.LocateLocal(c.Param("backup"))
|
||||
b, _, err := backup.LocateLocal(client, c.Param("backup"))
|
||||
if err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
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
|
||||
// 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...")
|
||||
// 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
|
||||
|
@ -127,7 +129,7 @@ func postServerRestoreBackup(c *gin.Context) {
|
|||
middleware.CaptureAndAbort(c, err)
|
||||
return
|
||||
}
|
||||
res, err := client.Do(req)
|
||||
res, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
return
|
||||
|
@ -143,7 +145,7 @@ func postServerRestoreBackup(c *gin.Context) {
|
|||
|
||||
go func(s *server.Server, uuid string, logger *log.Entry) {
|
||||
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")
|
||||
}
|
||||
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
|
||||
// response.
|
||||
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 {
|
||||
// Just return from the function at this point if the backup was not located.
|
||||
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
|
||||
// for it.
|
||||
func postCreateServer(c *gin.Context) {
|
||||
manager := middleware.ExtractManager(c)
|
||||
buf := bytes.Buffer{}
|
||||
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 installer.IsValidationError(err) {
|
||||
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
|
||||
// requests from here-on out.
|
||||
manager := middleware.ExtractManager(c)
|
||||
manager.Add(install.Server())
|
||||
|
||||
// Begin the installation process in the background to not block the request
|
||||
|
|
|
@ -2,6 +2,7 @@ package router
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
|
@ -22,9 +23,9 @@ import (
|
|||
"github.com/juju/ratelimit"
|
||||
"github.com/mholt/archiver/v3"
|
||||
"github.com/mitchellh/colorstring"
|
||||
"github.com/pterodactyl/wings/api"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/installer"
|
||||
"github.com/pterodactyl/wings/remote"
|
||||
"github.com/pterodactyl/wings/router/middleware"
|
||||
"github.com/pterodactyl/wings/router/tokens"
|
||||
"github.com/pterodactyl/wings/server"
|
||||
|
@ -109,10 +110,10 @@ func getServerArchive(c *gin.Context) {
|
|||
}
|
||||
|
||||
func postServerArchive(c *gin.Context) {
|
||||
s := ExtractServer(c)
|
||||
s := middleware.ExtractServer(c)
|
||||
manager := middleware.ExtractManager(c)
|
||||
|
||||
go func(s *server.Server) {
|
||||
r := api.New()
|
||||
l := log.WithField("server", s.Id())
|
||||
|
||||
// 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.
|
||||
s.SetTransferring(false)
|
||||
|
||||
s.Events().Publish(server.TransferStatusEvent, "failure")
|
||||
|
||||
sendTransferLog("Attempting to notify panel of archive failure..")
|
||||
if err := r.SendArchiveStatus(s.Id(), false); err != nil {
|
||||
if !api.IsRequestError(err) {
|
||||
if err := manager.Client().SetArchiveStatus(s.Context(), s.Id(), false); err != nil {
|
||||
if !remote.IsRequestError(err) {
|
||||
sendTransferLog("Failed to notify panel of archive failure: " + err.Error())
|
||||
l.WithField("error", err).Error("failed to notify panel of failed archive status")
|
||||
return
|
||||
|
@ -174,8 +174,8 @@ func postServerArchive(c *gin.Context) {
|
|||
sendTransferLog("Successfully created archive, attempting to notify panel..")
|
||||
l.Info("successfully created server transfer archive, notifying panel..")
|
||||
|
||||
if err := r.SendArchiveStatus(s.Id(), true); err != nil {
|
||||
if !api.IsRequestError(err) {
|
||||
if err := manager.Client().SetArchiveStatus(s.Context(), s.Id(), true); err != nil {
|
||||
if !remote.IsRequestError(err) {
|
||||
sendTransferLog("Failed to notify panel of archive success: " + err.Error())
|
||||
l.WithField("error", err).Error("failed to notify panel of successful archive status")
|
||||
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.
|
||||
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.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")
|
||||
return err
|
||||
}
|
||||
|
@ -294,6 +294,7 @@ func postTransfer(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
manager := middleware.ExtractManager(c)
|
||||
u, err := uuid.Parse(data.ServerID)
|
||||
if err != nil {
|
||||
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
|
||||
// run the installer scripts.
|
||||
i, err := installer.New(data.Server)
|
||||
i, err := installer.New(context.Background(), manager, data.Server)
|
||||
if err != nil {
|
||||
_ = data.sendTransferStatus(false)
|
||||
_ = data.sendTransferStatus(manager.Client(), false)
|
||||
data.log().WithField("error", err).Error("failed to validate received server data")
|
||||
return
|
||||
}
|
||||
|
@ -324,7 +325,6 @@ func postTransfer(c *gin.Context) {
|
|||
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
|
||||
// then push the server into the global server collection for this instance.
|
||||
i.Server().SetTransferring(true)
|
||||
|
@ -332,7 +332,7 @@ func postTransfer(c *gin.Context) {
|
|||
defer func(s *server.Server) {
|
||||
// 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.
|
||||
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.")
|
||||
s.Events().Publish(server.TransferStatusEvent, "failure")
|
||||
manager.Remove(func(match *server.Server) bool {
|
||||
|
|
|
@ -8,16 +8,16 @@ import (
|
|||
"emperror.dev/errors"
|
||||
"github.com/apex/log"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pterodactyl/wings/api"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
"github.com/pterodactyl/wings/remote"
|
||||
"github.com/pterodactyl/wings/server/backup"
|
||||
)
|
||||
|
||||
// Notifies the panel of a backup's state and returns an error if one is encountered
|
||||
// while performing this action.
|
||||
func (s *Server) notifyPanelOfBackup(uuid string, ad *backup.ArchiveDetails, successful bool) error {
|
||||
if err := api.New().SendBackupStatus(uuid, ad.ToRequest(successful)); err != nil {
|
||||
if !api.IsRequestError(err) {
|
||||
if err := s.client.SetBackupStatus(s.Context(), uuid, ad.ToRequest(successful)); err != nil {
|
||||
if !remote.IsRequestError(err) {
|
||||
s.Log().WithFields(log.Fields{
|
||||
"backup": uuid,
|
||||
"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
|
||||
// the Panel is informed of the restoration status of this backup.
|
||||
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")
|
||||
}
|
||||
}()
|
||||
|
|
|
@ -9,8 +9,8 @@ import (
|
|||
"sync"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/pterodactyl/wings/api"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/remote"
|
||||
)
|
||||
|
||||
type AdapterType string
|
||||
|
@ -31,8 +31,8 @@ type ArchiveDetails struct {
|
|||
}
|
||||
|
||||
// ToRequest returns a request object.
|
||||
func (ad *ArchiveDetails) ToRequest(successful bool) api.BackupRequest {
|
||||
return api.BackupRequest{
|
||||
func (ad *ArchiveDetails) ToRequest(successful bool) remote.BackupRequest {
|
||||
return remote.BackupRequest{
|
||||
Checksum: ad.Checksum,
|
||||
ChecksumType: ad.ChecksumType,
|
||||
Size: ad.Size,
|
||||
|
@ -49,12 +49,15 @@ type Backup struct {
|
|||
// compatible with a standard .gitignore structure.
|
||||
Ignore string `json:"ignore"`
|
||||
|
||||
client remote.Client
|
||||
adapter AdapterType
|
||||
logContext map[string]interface{}
|
||||
}
|
||||
|
||||
// noinspection GoNameStartsWithPackageName
|
||||
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
|
||||
// instance.
|
||||
Identifier() string
|
||||
|
@ -84,6 +87,10 @@ type BackupInterface interface {
|
|||
Restore(reader io.Reader, callback RestoreCallback) error
|
||||
}
|
||||
|
||||
func (b *Backup) SetClient(c remote.Client) {
|
||||
b.client = c
|
||||
}
|
||||
|
||||
func (b *Backup) Identifier() string {
|
||||
return b.Uuid
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"os"
|
||||
|
||||
"github.com/mholt/archiver/v3"
|
||||
"github.com/pterodactyl/wings/remote"
|
||||
"github.com/pterodactyl/wings/system"
|
||||
)
|
||||
|
||||
|
@ -15,9 +16,10 @@ type LocalBackup struct {
|
|||
|
||||
var _ BackupInterface = (*LocalBackup)(nil)
|
||||
|
||||
func NewLocal(uuid string, ignore string) *LocalBackup {
|
||||
func NewLocal(client remote.Client, uuid string, ignore string) *LocalBackup {
|
||||
return &LocalBackup{
|
||||
Backup{
|
||||
client: client,
|
||||
Uuid: uuid,
|
||||
Ignore: ignore,
|
||||
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
|
||||
// will obviously only work if the backup was created as a local backup.
|
||||
func LocateLocal(uuid string) (*LocalBackup, os.FileInfo, error) {
|
||||
b := &LocalBackup{
|
||||
Backup{
|
||||
Uuid: uuid,
|
||||
Ignore: "",
|
||||
},
|
||||
}
|
||||
|
||||
func LocateLocal(client remote.Client, uuid string) (*LocalBackup, os.FileInfo, error) {
|
||||
b := NewLocal(client, uuid, "")
|
||||
st, err := os.Stat(b.Path())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
|
|
@ -3,6 +3,7 @@ package backup
|
|||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
@ -10,8 +11,8 @@ import (
|
|||
"strconv"
|
||||
|
||||
"github.com/juju/ratelimit"
|
||||
"github.com/pterodactyl/wings/api"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/remote"
|
||||
)
|
||||
|
||||
type S3Backup struct {
|
||||
|
@ -20,9 +21,10 @@ type S3Backup struct {
|
|||
|
||||
var _ BackupInterface = (*S3Backup)(nil)
|
||||
|
||||
func NewS3(uuid string, ignore string) *S3Backup {
|
||||
func NewS3(client remote.Client, uuid string, ignore string) *S3Backup {
|
||||
return &S3Backup{
|
||||
Backup{
|
||||
client: client,
|
||||
Uuid: uuid,
|
||||
Ignore: ignore,
|
||||
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().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 {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -17,9 +17,9 @@ import (
|
|||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pterodactyl/wings/api"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
"github.com/pterodactyl/wings/remote"
|
||||
"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.
|
||||
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 !api.IsRequestError(err) {
|
||||
if !remote.IsRequestError(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -113,7 +113,7 @@ func (s *Server) internalInstall() error {
|
|||
|
||||
type InstallationProcess struct {
|
||||
Server *Server
|
||||
Script *api.InstallationScript
|
||||
Script *remote.InstallationScript
|
||||
|
||||
client *client.Client
|
||||
context context.Context
|
||||
|
@ -121,7 +121,7 @@ type InstallationProcess struct {
|
|||
|
||||
// Generates a new installation process struct that will be used to create containers,
|
||||
// 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{
|
||||
Script: script,
|
||||
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
|
||||
// wrong and the server must be deleted and re-created.
|
||||
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 !api.IsRequestError(err) {
|
||||
if !remote.IsRequestError(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -7,10 +7,10 @@ import (
|
|||
"sync"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/pterodactyl/wings/api"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
"github.com/pterodactyl/wings/events"
|
||||
"github.com/pterodactyl/wings/remote"
|
||||
)
|
||||
|
||||
var dockerEvents = []string{
|
||||
|
@ -186,7 +186,7 @@ func (s *Server) onConsoleOutput(data string) {
|
|||
if s.IsRunning() {
|
||||
stop := processConfiguration.Stop
|
||||
|
||||
if stop.Type == api.ProcessStopCommand && data == stop.Value {
|
||||
if stop.Type == remote.ProcessStopCommand && data == stop.Value {
|
||||
s.Environment.SetState(environment.ProcessOfflineState)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
@ -14,13 +15,16 @@ import (
|
|||
"emperror.dev/errors"
|
||||
"github.com/apex/log"
|
||||
"github.com/gammazero/workerpool"
|
||||
"github.com/pterodactyl/wings/api"
|
||||
"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/server/filesystem"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
mu sync.RWMutex
|
||||
client remote.Client
|
||||
servers []*Server
|
||||
}
|
||||
|
||||
|
@ -28,8 +32,8 @@ type Manager struct {
|
|||
// the servers that are currently present on the filesystem and set them into
|
||||
// the manager.
|
||||
func NewManager(ctx context.Context, client remote.Client) (*Manager, error) {
|
||||
m := NewEmptyManager()
|
||||
if err := m.init(ctx, client); err != nil {
|
||||
m := NewEmptyManager(client)
|
||||
if err := m.init(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
// loading any of the servers from the disk. This allows the caller to set their
|
||||
// own servers into the collection as needed.
|
||||
func NewEmptyManager() *Manager {
|
||||
return &Manager{}
|
||||
func NewEmptyManager(client remote.Client) *Manager {
|
||||
return &Manager{client: client}
|
||||
}
|
||||
|
||||
// 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, client remote.Client) error {
|
||||
log.Info("fetching list of servers from API")
|
||||
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
|
||||
// Client returns the HTTP client interface that allows interaction with the
|
||||
// Panel API.
|
||||
func (m *Manager) Client() remote.Client {
|
||||
return m.client
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"emperror.dev/errors"
|
||||
"github.com/apex/log"
|
||||
"github.com/creasty/defaults"
|
||||
"github.com/pterodactyl/wings/api"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
"github.com/pterodactyl/wings/environment/docker"
|
||||
"github.com/pterodactyl/wings/events"
|
||||
"github.com/pterodactyl/wings/remote"
|
||||
"github.com/pterodactyl/wings/server/filesystem"
|
||||
"github.com/pterodactyl/wings/system"
|
||||
"golang.org/x/sync/semaphore"
|
||||
|
@ -37,6 +36,7 @@ type Server struct {
|
|||
// Maintains the configuration for the server. This is the data that gets returned by the Panel
|
||||
// such as build settings and container images.
|
||||
cfg Configuration
|
||||
client remote.Client
|
||||
|
||||
// The crash handler for this server instance.
|
||||
crasher CrashHandler
|
||||
|
@ -53,7 +53,7 @@ type Server struct {
|
|||
// Defines the process configuration for the server instance. This is dynamically
|
||||
// fetched from the Pterodactyl Server instance each time the server process is
|
||||
// started, and then cached here.
|
||||
procConfig *api.ProcessConfiguration
|
||||
procConfig *remote.ProcessConfiguration
|
||||
|
||||
// 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
|
||||
|
@ -72,11 +72,12 @@ type Server struct {
|
|||
|
||||
// Returns a new server instance with a context and all of the default values set on
|
||||
// the instance.
|
||||
func New() (*Server, error) {
|
||||
func New(client remote.Client) (*Server, error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s := Server{
|
||||
ctx: ctx,
|
||||
ctxCancel: &cancel,
|
||||
client: client,
|
||||
installing: 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
|
||||
// will automatically sync with Wings when the server is started.
|
||||
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 !api.IsRequestError(err) {
|
||||
if !remote.IsRequestError(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if err.(*api.RequestError).Status == "404" {
|
||||
if err.(*remote.RequestError).Status == "404" {
|
||||
return &serverDoesNotExist{}
|
||||
}
|
||||
|
||||
|
@ -164,7 +165,7 @@ func (s *Server) Sync() error {
|
|||
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.
|
||||
if err := s.UpdateDataStructure(cfg.Settings); err != nil {
|
||||
return err
|
||||
|
@ -218,7 +219,7 @@ func (s *Server) IsSuspended() bool {
|
|||
return s.Config().Suspended
|
||||
}
|
||||
|
||||
func (s *Server) ProcessConfiguration() *api.ProcessConfiguration {
|
||||
func (s *Server) ProcessConfiguration() *remote.ProcessConfiguration {
|
||||
s.RLock()
|
||||
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
|
||||
// environment state, it is simply the tracked state from this daemon instance, and
|
||||
// not the response from Docker.
|
||||
// IsRunning determines if the server state is running or not. This is different
|
||||
// than the environment state, it is simply the tracked state from this daemon
|
||||
// instance, and not the response from Docker.
|
||||
func (s *Server) IsRunning() bool {
|
||||
st := s.Environment.State()
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
|
@ -10,18 +11,24 @@ import (
|
|||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"emperror.dev/errors"
|
||||
"github.com/apex/log"
|
||||
"github.com/pkg/sftp"
|
||||
"github.com/pterodactyl/wings/api"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/remote"
|
||||
"github.com/pterodactyl/wings/server"
|
||||
"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
|
||||
type SFTPServer struct {
|
||||
manager *server.Manager
|
||||
|
@ -164,7 +171,7 @@ func (c *SFTPServer) generatePrivateKey() error {
|
|||
|
||||
// A function capable of validating user credentials with the Panel API.
|
||||
func (c *SFTPServer) passwordCallback(conn ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
|
||||
request := api.SftpAuthRequest{
|
||||
request := remote.SftpAuthRequest{
|
||||
User: conn.User(),
|
||||
Pass: string(pass),
|
||||
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.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 api.IsInvalidCredentialsError(err) {
|
||||
if _, ok := err.(*remote.SftpInvalidCredentialsError); ok {
|
||||
logger.Warn("failed to validate user credentials (invalid username or password)")
|
||||
} else {
|
||||
logger.WithField("error", err).Error("encountered an error while trying to validate user credentials")
|
||||
|
|
Loading…
Reference in New Issue
Block a user