Compare commits
100 Commits
v1.0.0-alp
...
v1.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0c2c48ef8 | ||
|
|
151b00de23 | ||
|
|
b2f6863399 | ||
|
|
46056dbce9 | ||
|
|
7321fe1421 | ||
|
|
b0fa5abe31 | ||
|
|
6395b8b56c | ||
|
|
df6d98bbda | ||
|
|
8eaf590f78 | ||
|
|
3bca54655b | ||
|
|
9dcf06d106 | ||
|
|
71d38ff62e | ||
|
|
0a815d72a5 | ||
|
|
a60e261a49 | ||
|
|
9e0cacc076 | ||
|
|
4279fa510e | ||
|
|
4ff7bd2777 | ||
|
|
dbe403ef6e | ||
|
|
11035b561a | ||
|
|
fd9487ea4d | ||
|
|
7d1ca0a695 | ||
|
|
12c97c41b0 | ||
|
|
2bf293823f | ||
|
|
b73f78a832 | ||
|
|
5c4064f87a | ||
|
|
70a647097e | ||
|
|
89bda64e47 | ||
|
|
621c5b3406 | ||
|
|
137b6cddae | ||
|
|
fd28db4a68 | ||
|
|
9de094f078 | ||
|
|
a4c9562e42 | ||
|
|
7d7766e4cb | ||
|
|
13fc464508 | ||
|
|
89e5b63c32 | ||
|
|
3bd48bbac1 | ||
|
|
d3a3d4dbf5 | ||
|
|
083bea5504 | ||
|
|
03045c94be | ||
|
|
17d204a631 | ||
|
|
af241f3de4 | ||
|
|
c57708d1e0 | ||
|
|
814914ca19 | ||
|
|
28214ef0ea | ||
|
|
e91dd84279 | ||
|
|
1f4aca8210 | ||
|
|
2220eb049c | ||
|
|
4ea1b90560 | ||
|
|
12648b43fa | ||
|
|
d3cbf96c57 | ||
|
|
acf425b705 | ||
|
|
1e12b7b37c | ||
|
|
ac9ab4c0b0 | ||
|
|
28c8f3f400 | ||
|
|
e719c67e0b | ||
|
|
2278347b4c | ||
|
|
9e30d63818 | ||
|
|
62ed90e621 | ||
|
|
1c5f30f5be | ||
|
|
59b11eb80c | ||
|
|
1c235025b7 | ||
|
|
33875105b6 | ||
|
|
222091b68c | ||
|
|
45d441ac32 | ||
|
|
3edcd5f9c3 | ||
|
|
cc54b99b55 | ||
|
|
3c49d6e947 | ||
|
|
f5a804210f | ||
|
|
0a13cfe236 | ||
|
|
718b126baf | ||
|
|
03311ecf03 | ||
|
|
5bb6dff277 | ||
|
|
c4474e22f6 | ||
|
|
ccbb119948 | ||
|
|
cf2ef1a173 | ||
|
|
223b9e05a1 | ||
|
|
4ad57af990 | ||
|
|
0ca9c8a114 | ||
|
|
8da9d45c9d | ||
|
|
d1127569f8 | ||
|
|
e53de71946 | ||
|
|
5693d0431e | ||
|
|
4ce2b73490 | ||
|
|
6af3cb2c9b | ||
|
|
8bd7708631 | ||
|
|
ef0ad45b71 | ||
|
|
c096d8802f | ||
|
|
3dba11ac6f | ||
|
|
5fd138e188 | ||
|
|
019d028011 | ||
|
|
ddca34f9e8 | ||
|
|
f0e37f1e98 | ||
|
|
679e21a6dd | ||
|
|
d0dabb927b | ||
|
|
c8d953380e | ||
|
|
8107532080 | ||
|
|
07b1876954 | ||
|
|
3f6b0ce44c | ||
|
|
5bde25d5b5 | ||
|
|
b37a4a4926 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -45,3 +45,4 @@ test_*/
|
|||||||
# Keep all gitkeep files (This needs to stay at the bottom)
|
# Keep all gitkeep files (This needs to stay at the bottom)
|
||||||
!.gitkeep
|
!.gitkeep
|
||||||
debug
|
debug
|
||||||
|
data/.states.json
|
||||||
|
|||||||
12
.travis.yml
12
.travis.yml
@@ -1,22 +1,24 @@
|
|||||||
|
os: linux
|
||||||
|
dist: xenial
|
||||||
language: go
|
language: go
|
||||||
|
|
||||||
go:
|
go:
|
||||||
- 1.9.x
|
- 1.13.x
|
||||||
|
|
||||||
|
go_import_path: "github.com/pterodactyl/wings"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
- docker
|
- docker
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- mkdir -p $GOPATH/bin
|
- mkdir -p $GOPATH/bin
|
||||||
|
|
||||||
# Install used tools
|
# Install used tools
|
||||||
- go get github.com/golang/dep/cmd/dep
|
|
||||||
- go get github.com/mitchellh/gox
|
- go get github.com/mitchellh/gox
|
||||||
- go get github.com/haya14busa/goverage
|
- go get github.com/haya14busa/goverage
|
||||||
- go get github.com/schrej/godacov
|
- go get github.com/schrej/godacov
|
||||||
|
|
||||||
# Install project dependencies with dep
|
- go mod download
|
||||||
- dep ensure
|
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- make cross-build
|
- make cross-build
|
||||||
|
|||||||
12
CHANGELOG.md
Normal file
12
CHANGELOG.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## v1.0.0-alpha.2
|
||||||
|
### Added
|
||||||
|
* Ability to run an installation process for a server and notify the panel when completed.
|
||||||
|
* Output from the installation process is now emitted over the server console for administrative users to view.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* Fixed bugs caused when unmarshaling data into a struct with default values.
|
||||||
|
* Timezone is properly set in containers by using the `TZ=` environment variable rather than mounting timezone data into the filesystem.
|
||||||
|
* Server data directly is now properly created when running the install process.
|
||||||
|
* Time drift caused in the socket JWT is now handled in a better manner and less likely to unexpectedly error out.
|
||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Initalizes the requester instance.
|
// Initializes the requester instance.
|
||||||
func NewRequester() *PanelRequest {
|
func NewRequester() *PanelRequest {
|
||||||
return &PanelRequest{
|
return &PanelRequest{
|
||||||
Response: nil,
|
Response: nil,
|
||||||
@@ -32,7 +32,7 @@ func (r *PanelRequest) GetClient() *http.Client {
|
|||||||
func (r *PanelRequest) SetHeaders(req *http.Request) *http.Request {
|
func (r *PanelRequest) SetHeaders(req *http.Request) *http.Request {
|
||||||
req.Header.Set("Accept", "application/vnd.pterodactyl.v1+json")
|
req.Header.Set("Accept", "application/vnd.pterodactyl.v1+json")
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
req.Header.Set("Authorization", "Bearer "+config.Get().AuthenticationToken)
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s.%s", config.Get().AuthenticationTokenId, config.Get().AuthenticationToken))
|
||||||
|
|
||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
|
|||||||
35
api/backup_endpoints.go
Normal file
35
api/backup_endpoints.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BackupRequest struct {
|
||||||
|
Checksum string `json:"checksum"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
Successful bool `json:"successful"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notifies the panel that a specific backup has been completed and is now
|
||||||
|
// available for a user to view and download.
|
||||||
|
func (r *PanelRequest) SendBackupStatus(backup string, data BackupRequest) (*RequestError, error) {
|
||||||
|
b, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := r.Post(fmt.Sprintf("/backups/%s", backup), b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
r.Response = resp
|
||||||
|
if r.HasError() {
|
||||||
|
return r.Error(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
@@ -49,6 +49,33 @@ type InstallationScript struct {
|
|||||||
Script string `json:"script"`
|
Script string `json:"script"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllServerConfigurations fetches configurations for all servers assigned to this node.
|
||||||
|
func (r *PanelRequest) GetAllServerConfigurations() (map[string]*ServerConfigurationResponse, *RequestError, error) {
|
||||||
|
resp, err := r.Get("/servers")
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
r.Response = resp
|
||||||
|
|
||||||
|
if r.HasError() {
|
||||||
|
return nil, r.Error(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b, _ := r.ReadBody()
|
||||||
|
res := map[string]*ServerConfigurationResponse{}
|
||||||
|
if len(b) == 2 {
|
||||||
|
return res, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(b, &res); err != nil {
|
||||||
|
return nil, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Fetches the server configuration and returns the struct for it.
|
// Fetches the server configuration and returns the struct for it.
|
||||||
func (r *PanelRequest) GetServerConfiguration(uuid string) (*ServerConfigurationResponse, *RequestError, error) {
|
func (r *PanelRequest) GetServerConfiguration(uuid string) (*ServerConfigurationResponse, *RequestError, error) {
|
||||||
resp, err := r.Get(fmt.Sprintf("/servers/%s", uuid))
|
resp, err := r.Get(fmt.Sprintf("/servers/%s", uuid))
|
||||||
@@ -122,3 +149,57 @@ func (r *PanelRequest) SendInstallationStatus(uuid string, successful bool) (*Re
|
|||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type archiveRequest struct {
|
||||||
|
Successful bool `json:"successful"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PanelRequest) SendArchiveStatus(uuid string, successful bool) (*RequestError, error) {
|
||||||
|
b, err := json.Marshal(archiveRequest{Successful: successful})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := r.Post(fmt.Sprintf("/servers/%s/archive", uuid), b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
r.Response = resp
|
||||||
|
if r.HasError() {
|
||||||
|
return r.Error(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PanelRequest) SendTransferFailure(uuid string) (*RequestError, error) {
|
||||||
|
resp, err := r.Get(fmt.Sprintf("/servers/%s/transfer/failure", uuid))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
r.Response = resp
|
||||||
|
if r.HasError() {
|
||||||
|
return r.Error(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PanelRequest) SendTransferSuccess(uuid string) (*RequestError, error) {
|
||||||
|
resp, err := r.Get(fmt.Sprintf("/servers/%s/transfer/success", uuid))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
r.Response = resp
|
||||||
|
if r.HasError() {
|
||||||
|
return r.Error(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/pterodactyl/sftp-server"
|
"github.com/pterodactyl/sftp-server"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *PanelRequest) ValidateSftpCredentials(request sftp_server.AuthenticationRequest) (*sftp_server.AuthenticationResponse, error) {
|
func (r *PanelRequest) ValidateSftpCredentials(request sftp_server.AuthenticationRequest) (*sftp_server.AuthenticationResponse, error) {
|
||||||
@@ -21,11 +22,16 @@ func (r *PanelRequest) ValidateSftpCredentials(request sftp_server.Authenticatio
|
|||||||
r.Response = resp
|
r.Response = resp
|
||||||
|
|
||||||
if r.HasError() {
|
if r.HasError() {
|
||||||
if r.HttpResponseCode() == 403 {
|
if r.HttpResponseCode() >= 400 && r.HttpResponseCode() < 500 {
|
||||||
return nil, sftp_server.InvalidCredentialsError{}
|
zap.S().Debugw("failed to validate server credentials for SFTP", zap.String("error", r.Error().String()))
|
||||||
|
|
||||||
|
return nil, new(sftp_server.InvalidCredentialsError)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.WithStack(errors.New(r.Error().String()))
|
rerr := errors.New(r.Error().String())
|
||||||
|
zap.S().Warnw("error validating SFTP credentials", zap.Error(rerr))
|
||||||
|
|
||||||
|
return nil, rerr
|
||||||
}
|
}
|
||||||
|
|
||||||
response := new(sftp_server.AuthenticationResponse)
|
response := new(sftp_server.AuthenticationResponse)
|
||||||
|
|||||||
184
cmd/configure.go
Normal file
184
cmd/configure.go
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/AlecAivazis/survey/v2"
|
||||||
|
"github.com/AlecAivazis/survey/v2/terminal"
|
||||||
|
"github.com/creasty/defaults"
|
||||||
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
configureArgs struct {
|
||||||
|
PanelURL string
|
||||||
|
Token string
|
||||||
|
ConfigPath string
|
||||||
|
Node string
|
||||||
|
Override bool
|
||||||
|
AllowInsecure bool
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var nodeIdRegex = regexp.MustCompile(`^(\d+)$`)
|
||||||
|
|
||||||
|
var configureCmd = &cobra.Command{
|
||||||
|
Use: "configure",
|
||||||
|
Short: "Use a token to configure wings automatically",
|
||||||
|
Run: configureCmdRun,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
configureCmd.PersistentFlags().StringVarP(&configureArgs.PanelURL, "panel-url", "p", "", "The base URL for this daemon's panel")
|
||||||
|
configureCmd.PersistentFlags().StringVarP(&configureArgs.Token, "token", "t", "", "The API key to use for fetching node information")
|
||||||
|
configureCmd.PersistentFlags().StringVarP(&configureArgs.Node, "node", "n", "", "The ID of the node which will be connected to this daemon")
|
||||||
|
configureCmd.PersistentFlags().StringVarP(&configureArgs.ConfigPath, "config-path", "c", config.DefaultLocation, "The path where the configuration file should be made")
|
||||||
|
configureCmd.PersistentFlags().BoolVar(&configureArgs.Override, "override", false, "Set to true to override an existing configuration for this node")
|
||||||
|
configureCmd.PersistentFlags().BoolVar(&configureArgs.AllowInsecure, "allow-insecure", false, "Set to true to disable certificate checking")
|
||||||
|
}
|
||||||
|
|
||||||
|
func configureCmdRun(cmd *cobra.Command, args []string) {
|
||||||
|
if configureArgs.AllowInsecure {
|
||||||
|
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(configureArgs.ConfigPath); err == nil && !configureArgs.Override {
|
||||||
|
survey.AskOne(&survey.Confirm{Message: "Override existing configuration file"}, &configureArgs.Override)
|
||||||
|
if !configureArgs.Override {
|
||||||
|
fmt.Println("Aborting process; a configuration file already exists for this node.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
} else if err != nil && !os.IsNotExist(err) {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var questions []*survey.Question
|
||||||
|
if configureArgs.PanelURL == "" {
|
||||||
|
questions = append(questions, &survey.Question{
|
||||||
|
Name: "PanelURL",
|
||||||
|
Prompt: &survey.Input{Message: "Panel URL: "},
|
||||||
|
Validate: func(ans interface{}) error {
|
||||||
|
if str, ok := ans.(string); ok {
|
||||||
|
_, err := url.ParseRequestURI(str)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if configureArgs.Token == "" {
|
||||||
|
questions = append(questions, &survey.Question{
|
||||||
|
Name: "Token",
|
||||||
|
Prompt: &survey.Input{Message: "API Token: "},
|
||||||
|
Validate: func(ans interface{}) error {
|
||||||
|
if str, ok := ans.(string); ok {
|
||||||
|
if len(str) == 0 {
|
||||||
|
return fmt.Errorf("please provide a valid authentication token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if configureArgs.Node == "" {
|
||||||
|
questions = append(questions, &survey.Question{
|
||||||
|
Name: "Node",
|
||||||
|
Prompt: &survey.Input{Message: "Node ID: "},
|
||||||
|
Validate: func(ans interface{}) error {
|
||||||
|
if str, ok := ans.(string); ok {
|
||||||
|
if !nodeIdRegex.Match([]byte(str)) {
|
||||||
|
return fmt.Errorf("please provide a valid authentication token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := survey.Ask(questions, &configureArgs); err != nil {
|
||||||
|
if err == terminal.InterruptErr {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &http.Client{
|
||||||
|
Timeout: time.Second * 30,
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := getRequest()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%+v", req.Header)
|
||||||
|
fmt.Printf(req.URL.String())
|
||||||
|
|
||||||
|
res, err := c.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Failed to fetch configuration from the panel.\n", err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
if res.StatusCode == http.StatusForbidden || res.StatusCode == http.StatusUnauthorized {
|
||||||
|
fmt.Println("The authentication credentials provided were not valid.")
|
||||||
|
os.Exit(1)
|
||||||
|
} else if res.StatusCode != http.StatusOK {
|
||||||
|
b, _ := ioutil.ReadAll(res.Body)
|
||||||
|
|
||||||
|
fmt.Println("An error occurred while processing this request.\n", string(b))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := ioutil.ReadAll(res.Body)
|
||||||
|
|
||||||
|
cfg := new(config.Configuration)
|
||||||
|
if err := defaults.Set(cfg); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(b, cfg); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = cfg.WriteToDisk(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Successfully configured wings.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRequest() (*http.Request, error) {
|
||||||
|
u, err := url.Parse(configureArgs.PanelURL)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Path = path.Join(u.Path, fmt.Sprintf("api/application/nodes/%s/configuration", configureArgs.Node))
|
||||||
|
|
||||||
|
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Header.Set("Accept", "application/vnd.pterodactyl.v1+json")
|
||||||
|
r.Header.Set("Content-Type", "application/json")
|
||||||
|
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", configureArgs.Token))
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
272
cmd/root.go
Normal file
272
cmd/root.go
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/pkg/profile"
|
||||||
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"github.com/pterodactyl/wings/environment"
|
||||||
|
"github.com/pterodactyl/wings/router"
|
||||||
|
"github.com/pterodactyl/wings/server"
|
||||||
|
"github.com/pterodactyl/wings/sftp"
|
||||||
|
"github.com/pterodactyl/wings/system"
|
||||||
|
"github.com/remeh/sizedwaitgroup"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
var configPath = config.DefaultLocation
|
||||||
|
var debug = false
|
||||||
|
var shouldRunProfiler = false
|
||||||
|
|
||||||
|
var root = &cobra.Command{
|
||||||
|
Use: "wings",
|
||||||
|
Short: "The wings of the pterodactyl game management panel",
|
||||||
|
Long: ``,
|
||||||
|
Run: rootCmdRun,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
root.PersistentFlags().StringVar(&configPath, "config", config.DefaultLocation, "set the location for the configuration file")
|
||||||
|
root.PersistentFlags().BoolVar(&debug, "debug", false, "pass in order to run wings in debug mode")
|
||||||
|
root.PersistentFlags().BoolVar(&shouldRunProfiler, "profile", false, "pass in order to profile wings")
|
||||||
|
|
||||||
|
root.AddCommand(configureCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the configuration path based on the arguments provided.
|
||||||
|
func readConfiguration() (*config.Configuration, error) {
|
||||||
|
var p = configPath
|
||||||
|
if !strings.HasPrefix(p, "/") {
|
||||||
|
d, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
p = path.Clean(path.Join(d, configPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
if s, err := os.Stat(p); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
} else if s.IsDir() {
|
||||||
|
return nil, errors.New("cannot use directory as configuration file path")
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.ReadConfiguration(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rootCmdRun(*cobra.Command, []string) {
|
||||||
|
// Profile wings in production!!!!
|
||||||
|
if shouldRunProfiler {
|
||||||
|
defer profile.Start().Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := readConfiguration()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
c.Debug = true
|
||||||
|
}
|
||||||
|
|
||||||
|
printLogo()
|
||||||
|
if err := configureLogging(c.Debug); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.S().Infof("using configuration from path: %s", c.GetPath())
|
||||||
|
if c.Debug {
|
||||||
|
zap.S().Debugw("running in debug mode")
|
||||||
|
zap.S().Infow("certificate checking is disabled")
|
||||||
|
|
||||||
|
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Set(c)
|
||||||
|
config.SetDebugViaFlag(debug)
|
||||||
|
|
||||||
|
if err := c.System.ConfigureDirectories(); err != nil {
|
||||||
|
zap.S().Panicw("failed to configure system directories for pterodactyl", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.S().Infof("checking for pterodactyl system user \"%s\"", c.System.Username)
|
||||||
|
if su, err := c.EnsurePterodactylUser(); err != nil {
|
||||||
|
zap.S().Panicw("failed to create pterodactyl system user", zap.Error(err))
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
zap.S().Infow("configured system user", zap.String("username", su.Username), zap.String("uid", su.Uid), zap.String("gid", su.Gid))
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.S().Infow("beginning file permission setting on server data directories")
|
||||||
|
if err := c.EnsureFilePermissions(); err != nil {
|
||||||
|
zap.S().Errorw("failed to properly chown data directories", zap.Error(err))
|
||||||
|
} else {
|
||||||
|
zap.S().Infow("finished ensuring file permissions")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := server.LoadDirectory(); err != nil {
|
||||||
|
zap.S().Fatalw("failed to load server configurations", zap.Error(errors.WithStack(err)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := environment.ConfigureDocker(&c.Docker); err != nil {
|
||||||
|
zap.S().Fatalw("failed to configure docker environment", zap.Error(errors.WithStack(err)))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.WriteToDisk(); err != nil {
|
||||||
|
zap.S().Errorw("failed to save configuration to disk", zap.Error(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just for some nice log output.
|
||||||
|
for _, s := range server.GetServers().All() {
|
||||||
|
zap.S().Infow("loaded configuration for server", zap.String("server", s.Uuid))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new WaitGroup that limits us to 4 servers being bootstrapped at a time
|
||||||
|
// on Wings. This allows us to ensure the environment exists, write configurations,
|
||||||
|
// and reboot processes without causing a slow-down due to sequential booting.
|
||||||
|
wg := sizedwaitgroup.New(4)
|
||||||
|
|
||||||
|
for _, serv := range server.GetServers().All() {
|
||||||
|
wg.Add()
|
||||||
|
|
||||||
|
go func(s *server.Server) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
// Create a server environment if none exists currently. This allows us to recover from Docker
|
||||||
|
// being reinstalled on the host system for example.
|
||||||
|
zap.S().Infow("ensuring environment exists", zap.String("server", s.Uuid))
|
||||||
|
if err := s.Environment.Create(); err != nil {
|
||||||
|
zap.S().Errorw("failed to create an environment for server", zap.String("server", s.Uuid), zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := s.Environment.IsRunning()
|
||||||
|
if err != nil {
|
||||||
|
zap.S().Errorw("error checking server environment status", zap.String("server", s.Uuid), zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the server is currently running on Docker, mark the process as being in that state.
|
||||||
|
// We never want to stop an instance that is currently running external from Wings since
|
||||||
|
// that is a good way of keeping things running even if Wings gets in a very corrupted state.
|
||||||
|
//
|
||||||
|
// This will also validate that a server process is running if the last tracked state we have
|
||||||
|
// is that it was running, but we see that the container process is not currently running.
|
||||||
|
if r || (!r && s.IsRunning()) {
|
||||||
|
zap.S().Infow("detected server is running, re-attaching to process", zap.String("server", s.Uuid))
|
||||||
|
if err := s.Environment.Start(); err != nil {
|
||||||
|
zap.S().Warnw(
|
||||||
|
"failed to properly start server detected as already running",
|
||||||
|
zap.String("server", s.Uuid),
|
||||||
|
zap.Error(errors.WithStack(err)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Addresses potentially invalid data in the stored file that can cause Wings to lose
|
||||||
|
// track of what the actual server state is.
|
||||||
|
s.SetState(server.ProcessOfflineState)
|
||||||
|
}(serv)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait until all of the servers are ready to go before we fire up the HTTP server.
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// If the SFTP subsystem should be started, do so now.
|
||||||
|
if c.System.Sftp.UseInternalSystem {
|
||||||
|
sftp.Initialize(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the archive directory exists.
|
||||||
|
if err := os.MkdirAll(c.System.ArchiveDirectory, 0755); err != nil {
|
||||||
|
zap.S().Errorw("failed to create archive directory", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the backup directory exists.
|
||||||
|
if err := os.MkdirAll(c.System.BackupDirectory, 0755); err != nil {
|
||||||
|
zap.S().Errorw("failed to create backup directory", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.S().Infow("configuring webserver", zap.Bool("ssl", c.Api.Ssl.Enabled), zap.String("host", c.Api.Host), zap.Int("port", c.Api.Port))
|
||||||
|
|
||||||
|
r := router.Configure()
|
||||||
|
addr := fmt.Sprintf("%s:%d", c.Api.Host, c.Api.Port)
|
||||||
|
|
||||||
|
if c.Api.Ssl.Enabled {
|
||||||
|
if err := r.RunTLS(addr, c.Api.Ssl.CertificateFile, c.Api.Ssl.KeyFile); err != nil {
|
||||||
|
zap.S().Fatalw("failed to configure HTTPS server", zap.Error(err))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := r.Run(addr); err != nil {
|
||||||
|
zap.S().Fatalw("failed to configure HTTP server", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// r := &Router{
|
||||||
|
// token: c.AuthenticationToken,
|
||||||
|
// upgrader: websocket.Upgrader{
|
||||||
|
// // Ensure that the websocket request is originating from the Panel itself,
|
||||||
|
// // and not some other location.
|
||||||
|
// CheckOrigin: func(r *http.Request) bool {
|
||||||
|
// return r.Header.Get("Origin") == c.PanelLocation
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute calls cobra to handle cli commands
|
||||||
|
func Execute() error {
|
||||||
|
return root.Execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configures the global logger for Zap so that we can call it from any location
|
||||||
|
// in the code without having to pass around a logger instance.
|
||||||
|
func configureLogging(debug bool) error {
|
||||||
|
cfg := zap.NewProductionConfig()
|
||||||
|
if debug {
|
||||||
|
cfg = zap.NewDevelopmentConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Encoding = "console"
|
||||||
|
cfg.OutputPaths = []string{
|
||||||
|
"stdout",
|
||||||
|
}
|
||||||
|
|
||||||
|
logger, err := cfg.Build()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.ReplaceGlobals(logger)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prints the wings logo, nothing special here!
|
||||||
|
func printLogo() {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println(` ____`)
|
||||||
|
fmt.Println(`__ Pterodactyl _____/___/_______ _______ ______`)
|
||||||
|
fmt.Println(`\_____\ \/\/ / / / __ / ___/`)
|
||||||
|
fmt.Println(` \___\ / / / / /_/ /___ /`)
|
||||||
|
fmt.Println(` \___/\___/___/___/___/___ /______/`)
|
||||||
|
fmt.Println(` /_______/ v` + system.Version)
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println(`Website: https://pterodactyl.io`)
|
||||||
|
fmt.Println(`Source: https://github.com/pterodactyl/wings`)
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println(`Copyright © 2018 - 2020 Dane Everitt & Contributors`)
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
214
config/config.go
214
config/config.go
@@ -1,8 +1,11 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/cobaugh/osrelease"
|
||||||
"github.com/creasty/defaults"
|
"github.com/creasty/defaults"
|
||||||
|
"github.com/gbrlsnchs/jwt/v3"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@@ -16,11 +19,33 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const DefaultLocation = "/var/lib/pterodactyl/config.yml"
|
||||||
|
|
||||||
type Configuration struct {
|
type Configuration struct {
|
||||||
|
sync.RWMutex `json:"-" yaml:"-"`
|
||||||
|
|
||||||
|
// The location from which this configuration instance was instantiated.
|
||||||
|
path string
|
||||||
|
|
||||||
|
// Locker specific to writing the configuration to the disk, this happens
|
||||||
|
// in areas that might already be locked so we don't want to crash the process.
|
||||||
|
writeLock sync.Mutex
|
||||||
|
|
||||||
// Determines if wings should be running in debug mode. This value is ignored
|
// Determines if wings should be running in debug mode. This value is ignored
|
||||||
// if the debug flag is passed through the command line arguments.
|
// if the debug flag is passed through the command line arguments.
|
||||||
Debug bool
|
Debug bool
|
||||||
|
|
||||||
|
// A unique identifier for this node in the Panel.
|
||||||
|
Uuid string
|
||||||
|
|
||||||
|
// An identifier for the token which must be included in any requests to the panel
|
||||||
|
// so that the token can be looked up correctly.
|
||||||
|
AuthenticationTokenId string `json:"token_id" yaml:"token_id"`
|
||||||
|
|
||||||
|
// The token used when performing operations. Requests to this instance must
|
||||||
|
// validate against it.
|
||||||
|
AuthenticationToken string `json:"token" yaml:"token"`
|
||||||
|
|
||||||
Api ApiConfiguration
|
Api ApiConfiguration
|
||||||
System SystemConfiguration
|
System SystemConfiguration
|
||||||
Docker DockerConfiguration
|
Docker DockerConfiguration
|
||||||
@@ -41,134 +66,38 @@ type Configuration struct {
|
|||||||
// The number of seconds that must elapse before the internal counter
|
// The number of seconds that must elapse before the internal counter
|
||||||
// begins decrementing warnings assigned to a process that is outputting
|
// begins decrementing warnings assigned to a process that is outputting
|
||||||
// too much data.
|
// too much data.
|
||||||
DecaySeconds int `default:"10" yaml:"decay"`
|
DecaySeconds int `default:"10" json:"decay" yaml:"decay"`
|
||||||
|
|
||||||
// The total number of bytes allowed to be output by a server process
|
// The total number of bytes allowed to be output by a server process
|
||||||
// per interval.
|
// per interval.
|
||||||
BytesPerInterval int `default:"4096" yaml:"bytes"`
|
BytesPerInterval int `default:"4096" json:"bytes" yaml:"bytes"`
|
||||||
|
|
||||||
// The amount of time that should lapse between data output throttle
|
// The amount of time that should lapse between data output throttle
|
||||||
// checks. This should be defined in milliseconds.
|
// checks. This should be defined in milliseconds.
|
||||||
CheckInterval int `defauly:"100" yaml:"check_interval"`
|
CheckInterval int `default:"100" yaml:"check_interval"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// The location where the panel is running that this daemon should connect to
|
// The location where the panel is running that this daemon should connect to
|
||||||
// to collect data and send events.
|
// to collect data and send events.
|
||||||
PanelLocation string `yaml:"remote"`
|
PanelLocation string `json:"remote" yaml:"remote"`
|
||||||
|
|
||||||
// The token used when performing operations. Requests to this instance must
|
|
||||||
// validate aganist it.
|
|
||||||
AuthenticationToken string `yaml:"token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defines basic system configuration settings.
|
|
||||||
type SystemConfiguration struct {
|
|
||||||
// Directory where the server data is stored at.
|
|
||||||
Data string `default:"/srv/daemon-data" yaml:"data"`
|
|
||||||
|
|
||||||
// The user that should own all of the server files, and be used for containers.
|
|
||||||
Username string `default:"pterodactyl" yaml:"username"`
|
|
||||||
|
|
||||||
// Definitions for the user that gets created to ensure that we can quickly access
|
|
||||||
// this information without constantly having to do a system lookup.
|
|
||||||
User struct {
|
|
||||||
Uid int
|
|
||||||
Gid int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determines wether or not server data should be synced when the Daemon is started.
|
|
||||||
// If set to false, data will only be synced when a server process is started, or
|
|
||||||
// detected as started when booting.
|
|
||||||
SyncServersOnBoot bool `default:"true" yaml:"sync_servers_on_boot"`
|
|
||||||
|
|
||||||
// The path to the system's timezone file that will be mounted into running Docker containers.
|
|
||||||
TimezonePath string `yaml:"timezone_path"`
|
|
||||||
|
|
||||||
// Determines if permissions for a server should be set automatically on
|
|
||||||
// daemon boot. This can take a long time on systems with many servers, or on
|
|
||||||
// systems with servers containing thousands of files.
|
|
||||||
//
|
|
||||||
// Setting this to true by default helps us avoid a lot of support requests
|
|
||||||
// from people that keep trying to move files around as a root user leading
|
|
||||||
// to server permission issues.
|
|
||||||
//
|
|
||||||
// In production and heavy use environments where boot speed is essential,
|
|
||||||
// this should be set to false as servers will self-correct permissions on
|
|
||||||
// boot anyways.
|
|
||||||
SetPermissionsOnBoot bool `default:"true" yaml:"set_permissions_on_boot"`
|
|
||||||
|
|
||||||
// Determines if Wings should detect a server that stops with a normal exit code of
|
|
||||||
// "0" as being crashed if the process stopped without any Wings interaction. E.g.
|
|
||||||
// the user did not press the stop button, but the process stopped cleanly.
|
|
||||||
DetectCleanExitAsCrash bool `default:"true" yaml:"detect_clean_exit_as_crash"`
|
|
||||||
|
|
||||||
Sftp *SftpConfiguration `yaml:"sftp"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Defines the configuration of the internal SFTP server.
|
// Defines the configuration of the internal SFTP server.
|
||||||
type SftpConfiguration struct {
|
type SftpConfiguration struct {
|
||||||
// If set to false, the internal SFTP server will not be booted and you will need
|
// If set to false, the internal SFTP server will not be booted and you will need
|
||||||
// to run the SFTP server independent of this program.
|
// to run the SFTP server independent of this program.
|
||||||
UseInternalSystem bool `default:"true" yaml:"use_internal"`
|
UseInternalSystem bool `default:"true" json:"use_internal" yaml:"use_internal"`
|
||||||
// If set to true disk checking will not be performed. This will prevent the SFTP
|
// If set to true disk checking will not be performed. This will prevent the SFTP
|
||||||
// server from checking the total size of a directory when uploading files.
|
// server from checking the total size of a directory when uploading files.
|
||||||
DisableDiskChecking bool `default:"false" yaml:"disable_disk_checking"`
|
DisableDiskChecking bool `default:"false" yaml:"disable_disk_checking"`
|
||||||
// The bind address of the SFTP server.
|
// The bind address of the SFTP server.
|
||||||
Address string `default:"0.0.0.0" yaml:"bind_address"`
|
Address string `default:"0.0.0.0" json:"bind_address" yaml:"bind_address"`
|
||||||
// The bind port of the SFTP server.
|
// The bind port of the SFTP server.
|
||||||
Port int `default:"2022" yaml:"bind_port"`
|
Port int `default:"2022" json:"bind_port" yaml:"bind_port"`
|
||||||
// If set to true, no write actions will be allowed on the SFTP server.
|
// If set to true, no write actions will be allowed on the SFTP server.
|
||||||
ReadOnly bool `default:"false" yaml:"read_only"`
|
ReadOnly bool `default:"false" yaml:"read_only"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type dockerNetworkInterfaces struct {
|
|
||||||
V4 struct {
|
|
||||||
Subnet string `default:"172.18.0.0/16"`
|
|
||||||
Gateway string `default:"172.18.0.1"`
|
|
||||||
}
|
|
||||||
|
|
||||||
V6 struct {
|
|
||||||
Subnet string `default:"fdba:17c8:6c94::/64"`
|
|
||||||
Gateway string `default:"fdba:17c8:6c94::1011"`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type DockerNetworkConfiguration struct {
|
|
||||||
// The interface that should be used to create the network. Must not conflict
|
|
||||||
// with any other interfaces in use by Docker or on the system.
|
|
||||||
Interface string `default:"172.18.0.1"`
|
|
||||||
|
|
||||||
// The name of the network to use. If this network already exists it will not
|
|
||||||
// be created. If it is not found, a new network will be created using the interface
|
|
||||||
// defined.
|
|
||||||
Name string `default:"pterodactyl_nw"`
|
|
||||||
ISPN bool `default:"false" yaml:"ispn"`
|
|
||||||
Driver string `default:"bridge"`
|
|
||||||
IsInternal bool `default:"false" yaml:"is_internal"`
|
|
||||||
EnableICC bool `default:"true" yaml:"enable_icc"`
|
|
||||||
Interfaces dockerNetworkInterfaces `yaml:"interfaces"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defines the docker configuration used by the daemon when interacting with
|
|
||||||
// containers and networks on the system.
|
|
||||||
type DockerConfiguration struct {
|
|
||||||
// Network configuration that should be used when creating a new network
|
|
||||||
// for containers run through the daemon.
|
|
||||||
Network DockerNetworkConfiguration `yaml:"network"`
|
|
||||||
|
|
||||||
// If true, container images will be updated when a server starts if there
|
|
||||||
// is an update available. If false the daemon will not attempt updates and will
|
|
||||||
// defer to the host system to manage image updates.
|
|
||||||
UpdateImages bool `default:"true" yaml:"update_images"`
|
|
||||||
|
|
||||||
// The location of the Docker socket.
|
|
||||||
Socket string `default:"/var/run/docker.sock"`
|
|
||||||
|
|
||||||
// Defines the location of the timezone file on the host system that should
|
|
||||||
// be mounted into the created containers so that they all use the same time.
|
|
||||||
TimezonePath string `default:"/etc/timezone" yaml:"timezone_path"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defines the configuration for the internal API that is exposed by the
|
// Defines the configuration for the internal API that is exposed by the
|
||||||
// daemon webserver.
|
// daemon webserver.
|
||||||
type ApiConfiguration struct {
|
type ApiConfiguration struct {
|
||||||
@@ -181,12 +110,12 @@ type ApiConfiguration struct {
|
|||||||
// SSL configuration for the daemon.
|
// SSL configuration for the daemon.
|
||||||
Ssl struct {
|
Ssl struct {
|
||||||
Enabled bool `default:"false"`
|
Enabled bool `default:"false"`
|
||||||
CertificateFile string `yaml:"cert"`
|
CertificateFile string `json:"cert" yaml:"cert"`
|
||||||
KeyFile string `yaml:"key"`
|
KeyFile string `json:"key" yaml:"key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// The maximum size for files uploaded through the Panel in bytes.
|
// The maximum size for files uploaded through the Panel in bytes.
|
||||||
UploadLimit int `default:"100" yaml:"upload_limit"`
|
UploadLimit int `default:"100" json:"upload_limit" yaml:"upload_limit"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reads the configuration from the provided file and returns the configuration
|
// Reads the configuration from the provided file and returns the configuration
|
||||||
@@ -199,12 +128,15 @@ func ReadConfiguration(path string) (*Configuration, error) {
|
|||||||
|
|
||||||
c := new(Configuration)
|
c := new(Configuration)
|
||||||
// Configures the default values for many of the configuration options present
|
// Configures the default values for many of the configuration options present
|
||||||
// in the structs. Valkues set in the configuration file take priority over the
|
// in the structs. Values set in the configuration file take priority over the
|
||||||
// default values.
|
// default values.
|
||||||
if err := defaults.Set(c); err != nil {
|
if err := defaults.Set(c); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track the location where we created this configuration.
|
||||||
|
c.path = path
|
||||||
|
|
||||||
// Replace environment variables within the configuration file with their
|
// Replace environment variables within the configuration file with their
|
||||||
// values from the host system.
|
// values from the host system.
|
||||||
b = []byte(os.ExpandEnv(string(b)))
|
b = []byte(os.ExpandEnv(string(b)))
|
||||||
@@ -216,23 +148,52 @@ func ReadConfiguration(path string) (*Configuration, error) {
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var Mutex sync.RWMutex
|
||||||
|
|
||||||
var _config *Configuration
|
var _config *Configuration
|
||||||
|
var _jwtAlgo *jwt.HMACSHA
|
||||||
var _debugViaFlag bool
|
var _debugViaFlag bool
|
||||||
|
|
||||||
// Set the global configuration instance.
|
// Set the global configuration instance. This is a blocking operation such that
|
||||||
|
// anything trying to set a different configuration value, or read the configuration
|
||||||
|
// will be paused until it is complete.
|
||||||
func Set(c *Configuration) {
|
func Set(c *Configuration) {
|
||||||
|
Mutex.Lock()
|
||||||
|
|
||||||
|
if _config == nil || _config.AuthenticationToken != c.AuthenticationToken {
|
||||||
|
_jwtAlgo = jwt.NewHS256([]byte(c.AuthenticationToken))
|
||||||
|
}
|
||||||
|
|
||||||
_config = c
|
_config = c
|
||||||
|
Mutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetDebugViaFlag(d bool) {
|
func SetDebugViaFlag(d bool) {
|
||||||
_debugViaFlag = d
|
_debugViaFlag = d
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the global configuration instance.
|
// Get the global configuration instance. This is a read-safe operation that will block
|
||||||
|
// if the configuration is presently being modified.
|
||||||
func Get() *Configuration {
|
func Get() *Configuration {
|
||||||
|
Mutex.RLock()
|
||||||
|
defer Mutex.RUnlock()
|
||||||
|
|
||||||
return _config
|
return _config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns the in-memory JWT algorithm.
|
||||||
|
func GetJwtAlgorithm() *jwt.HMACSHA {
|
||||||
|
Mutex.RLock()
|
||||||
|
defer Mutex.RUnlock()
|
||||||
|
|
||||||
|
return _jwtAlgo
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the path for this configuration file.
|
||||||
|
func (c *Configuration) GetPath() string {
|
||||||
|
return c.path
|
||||||
|
}
|
||||||
|
|
||||||
// Ensures that the Pterodactyl core user exists on the system. This user will be the
|
// Ensures that the Pterodactyl core user exists on the system. This user will be the
|
||||||
// owner of all data in the root data directory and is used as the user within containers.
|
// owner of all data in the root data directory and is used as the user within containers.
|
||||||
//
|
//
|
||||||
@@ -258,12 +219,12 @@ func (c *Configuration) EnsurePterodactylUser() (*user.User, error) {
|
|||||||
|
|
||||||
// Alpine Linux is the only OS we currently support that doesn't work with the useradd command, so
|
// Alpine Linux is the only OS we currently support that doesn't work with the useradd command, so
|
||||||
// in those cases we just modify the command a bit to work as expected.
|
// in those cases we just modify the command a bit to work as expected.
|
||||||
if strings.HasPrefix(sysName, "Alpine") {
|
if strings.HasPrefix(sysName, "alpine") {
|
||||||
command = fmt.Sprintf("adduser -S -D -H -G %[1]s -s /bin/false %[1]s", c.System.Username)
|
command = fmt.Sprintf("adduser -S -D -H -G %[1]s -s /bin/false %[1]s", c.System.Username)
|
||||||
|
|
||||||
// We have to create the group first on Alpine, so do that here before continuing on
|
// We have to create the group first on Alpine, so do that here before continuing on
|
||||||
// to the user creation process.
|
// to the user creation process.
|
||||||
if _, err := exec.Command("addgroup", "-s", c.System.Username).Output(); err != nil {
|
if _, err := exec.Command("addgroup", "-S", c.System.Username).Output(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -286,6 +247,9 @@ func (c *Configuration) setSystemUser(u *user.User) error {
|
|||||||
uid, _ := strconv.Atoi(u.Uid)
|
uid, _ := strconv.Atoi(u.Uid)
|
||||||
gid, _ := strconv.Atoi(u.Gid)
|
gid, _ := strconv.Atoi(u.Gid)
|
||||||
|
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
|
||||||
c.System.Username = u.Username
|
c.System.Username = u.Username
|
||||||
c.System.User.Uid = uid
|
c.System.User.Uid = uid
|
||||||
c.System.User.Gid = gid
|
c.System.User.Gid = gid
|
||||||
@@ -323,7 +287,7 @@ func (c *Configuration) EnsureFilePermissions() error {
|
|||||||
// the item is not a folder, or is not a folder that matches the expected UUIDv4 format
|
// the item is not a folder, or is not a folder that matches the expected UUIDv4 format
|
||||||
// skip over it.
|
// skip over it.
|
||||||
//
|
//
|
||||||
// If we do have a positive match, run a chown aganist the directory.
|
// If we do have a positive match, run a chown against the directory.
|
||||||
go func(f os.FileInfo) {
|
go func(f os.FileInfo) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
@@ -349,12 +313,6 @@ func (c *Configuration) EnsureFilePermissions() error {
|
|||||||
// lock on the file. This prevents something else from writing at the exact same time and
|
// lock on the file. This prevents something else from writing at the exact same time and
|
||||||
// leading to bad data conditions.
|
// leading to bad data conditions.
|
||||||
func (c *Configuration) WriteToDisk() error {
|
func (c *Configuration) WriteToDisk() error {
|
||||||
f, err := os.OpenFile("config.yml", os.O_WRONLY, os.ModeExclusive)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
ccopy := *c
|
ccopy := *c
|
||||||
// If debugging is set with the flag, don't save that to the configuration file, otherwise
|
// If debugging is set with the flag, don't save that to the configuration file, otherwise
|
||||||
// you'll always end up in debug mode.
|
// you'll always end up in debug mode.
|
||||||
@@ -362,12 +320,20 @@ func (c *Configuration) WriteToDisk() error {
|
|||||||
ccopy.Debug = false
|
ccopy.Debug = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.path == "" {
|
||||||
|
return errors.New("cannot write configuration, no path defined in struct")
|
||||||
|
}
|
||||||
|
|
||||||
b, err := yaml.Marshal(&ccopy)
|
b, err := yaml.Marshal(&ccopy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := f.Write(b); err != nil {
|
// Obtain an exclusive write against the configuration file.
|
||||||
|
c.writeLock.Lock()
|
||||||
|
defer c.writeLock.Unlock()
|
||||||
|
|
||||||
|
if err := ioutil.WriteFile(c.GetPath(), b, 0644); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,12 +342,10 @@ func (c *Configuration) WriteToDisk() error {
|
|||||||
|
|
||||||
// Gets the system release name.
|
// Gets the system release name.
|
||||||
func getSystemName() (string, error) {
|
func getSystemName() (string, error) {
|
||||||
cmd := exec.Command("lsb_release", "-is")
|
// use osrelease to get release version and ID
|
||||||
|
if release, err := osrelease.Read(); err != nil {
|
||||||
b, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
return "", err
|
||||||
|
} else {
|
||||||
|
return release["ID"], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(b), nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
52
config/config_docker.go
Normal file
52
config/config_docker.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type dockerNetworkInterfaces struct {
|
||||||
|
V4 struct {
|
||||||
|
Subnet string `default:"172.18.0.0/16"`
|
||||||
|
Gateway string `default:"172.18.0.1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
V6 struct {
|
||||||
|
Subnet string `default:"fdba:17c8:6c94::/64"`
|
||||||
|
Gateway string `default:"fdba:17c8:6c94::1011"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DockerNetworkConfiguration struct {
|
||||||
|
// The interface that should be used to create the network. Must not conflict
|
||||||
|
// with any other interfaces in use by Docker or on the system.
|
||||||
|
Interface string `default:"172.18.0.1"`
|
||||||
|
|
||||||
|
// The DNS settings for containers.
|
||||||
|
Dns []string `default:"[\"1.1.1.1\", \"1.0.0.1\"]"`
|
||||||
|
|
||||||
|
// The name of the network to use. If this network already exists it will not
|
||||||
|
// be created. If it is not found, a new network will be created using the interface
|
||||||
|
// defined.
|
||||||
|
Name string `default:"pterodactyl_nw"`
|
||||||
|
ISPN bool `default:"false" yaml:"ispn"`
|
||||||
|
Driver string `default:"bridge"`
|
||||||
|
IsInternal bool `default:"false" yaml:"is_internal"`
|
||||||
|
EnableICC bool `default:"true" yaml:"enable_icc"`
|
||||||
|
Interfaces dockerNetworkInterfaces `yaml:"interfaces"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defines the docker configuration used by the daemon when interacting with
|
||||||
|
// containers and networks on the system.
|
||||||
|
type DockerConfiguration struct {
|
||||||
|
// Network configuration that should be used when creating a new network
|
||||||
|
// for containers run through the daemon.
|
||||||
|
Network DockerNetworkConfiguration `json:"network" yaml:"network"`
|
||||||
|
|
||||||
|
// If true, container images will be updated when a server starts if there
|
||||||
|
// is an update available. If false the daemon will not attempt updates and will
|
||||||
|
// defer to the host system to manage image updates.
|
||||||
|
UpdateImages bool `default:"true" json:"update_images" yaml:"update_images"`
|
||||||
|
|
||||||
|
// The location of the Docker socket.
|
||||||
|
Socket string `default:"/var/run/docker.sock"`
|
||||||
|
|
||||||
|
// Defines the location of the timezone file on the host system that should
|
||||||
|
// be mounted into the created containers so that they all use the same time.
|
||||||
|
TimezonePath string `default:"/etc/timezone" json:"timezone_path" yaml:"timezone_path"`
|
||||||
|
}
|
||||||
96
config/config_system.go
Normal file
96
config/config_system.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Defines basic system configuration settings.
|
||||||
|
type SystemConfiguration struct {
|
||||||
|
// The root directory where all of the pterodactyl data is stored at.
|
||||||
|
RootDirectory string `default:"/var/lib/pterodactyl" yaml:"root_directory"`
|
||||||
|
|
||||||
|
// Directory where logs for server installations and other wings events are logged.
|
||||||
|
LogDirectory string `default:"/var/log/pterodactyl" yaml:"log_directory"`
|
||||||
|
|
||||||
|
// Directory where the server data is stored at.
|
||||||
|
Data string `default:"/var/lib/pterodactyl/volumes" yaml:"data"`
|
||||||
|
|
||||||
|
// Directory where server archives for transferring will be stored.
|
||||||
|
ArchiveDirectory string `default:"/var/lib/pterodactyl/archives" yaml:"archive_directory"`
|
||||||
|
|
||||||
|
// Directory where local backups will be stored on the machine.
|
||||||
|
BackupDirectory string `default:"/var/lib/pterodactyl/backups" yaml:"backup_directory"`
|
||||||
|
|
||||||
|
// The user that should own all of the server files, and be used for containers.
|
||||||
|
Username string `default:"pterodactyl" yaml:"username"`
|
||||||
|
|
||||||
|
// Definitions for the user that gets created to ensure that we can quickly access
|
||||||
|
// this information without constantly having to do a system lookup.
|
||||||
|
User struct {
|
||||||
|
Uid int
|
||||||
|
Gid int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determines if permissions for a server should be set automatically on
|
||||||
|
// daemon boot. This can take a long time on systems with many servers, or on
|
||||||
|
// systems with servers containing thousands of files.
|
||||||
|
//
|
||||||
|
// Setting this to true by default helps us avoid a lot of support requests
|
||||||
|
// from people that keep trying to move files around as a root user leading
|
||||||
|
// to server permission issues.
|
||||||
|
//
|
||||||
|
// In production and heavy use environments where boot speed is essential,
|
||||||
|
// this should be set to false as servers will self-correct permissions on
|
||||||
|
// boot anyways.
|
||||||
|
SetPermissionsOnBoot bool `default:"true" yaml:"set_permissions_on_boot"`
|
||||||
|
|
||||||
|
// Determines if Wings should detect a server that stops with a normal exit code of
|
||||||
|
// "0" as being crashed if the process stopped without any Wings interaction. E.g.
|
||||||
|
// the user did not press the stop button, but the process stopped cleanly.
|
||||||
|
DetectCleanExitAsCrash bool `default:"true" yaml:"detect_clean_exit_as_crash"`
|
||||||
|
|
||||||
|
Sftp *SftpConfiguration `yaml:"sftp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensures that all of the system directories exist on the system. These directories are
|
||||||
|
// created so that only the owner can read the data, and no other users.
|
||||||
|
func (sc *SystemConfiguration) ConfigureDirectories() error {
|
||||||
|
zap.S().Debugw("ensuring root data directory exists", zap.String("path", sc.RootDirectory))
|
||||||
|
if err := os.MkdirAll(sc.RootDirectory, 0700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.S().Debugw("ensuring log directory exists", zap.String("path", sc.LogDirectory))
|
||||||
|
if err := os.MkdirAll(path.Join(sc.LogDirectory, "/install"), 0700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.S().Debugw("ensuring server data directory exists", zap.String("path", sc.Data))
|
||||||
|
if err := os.MkdirAll(sc.Data, 0700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.S().Debugw("ensuring archive data directory exists", zap.String("path", sc.ArchiveDirectory))
|
||||||
|
if err := os.MkdirAll(sc.ArchiveDirectory, 0700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.S().Debugw("ensuring backup data directory exists", zap.String("path", sc.BackupDirectory))
|
||||||
|
if err := os.MkdirAll(sc.BackupDirectory, 0700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the location of the JSON file that tracks server states.
|
||||||
|
func (sc *SystemConfiguration) GetStatesPath() string {
|
||||||
|
return path.Join(sc.RootDirectory, "states.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the location of the JSON file that tracks server states.
|
||||||
|
func (sc *SystemConfiguration) GetInstallLogPath() string {
|
||||||
|
return path.Join(sc.LogDirectory, "install/")
|
||||||
|
}
|
||||||
3
data/.gitignore
vendored
3
data/.gitignore
vendored
@@ -1,3 +0,0 @@
|
|||||||
servers/*.yml
|
|
||||||
!install_logs/.gitkeep
|
|
||||||
install_logs/*
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
package main
|
package environment
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/network"
|
"github.com/docker/docker/api/types/network"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
@@ -10,7 +11,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Configures the required network for the docker environment.
|
// Configures the required network for the docker environment.
|
||||||
func ConfigureDockerEnvironment(c *config.DockerConfiguration) error {
|
func ConfigureDocker(c *config.DockerConfiguration) error {
|
||||||
// Ensure the required docker network exists on the system.
|
// Ensure the required docker network exists on the system.
|
||||||
cli, err := client.NewClientWithOpts(client.FromEnv)
|
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
41
go.mod
41
go.mod
@@ -11,6 +11,7 @@ go 1.12
|
|||||||
// replace github.com/pterodactyl/sftp-server => ../sftp-server
|
// replace github.com/pterodactyl/sftp-server => ../sftp-server
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/AlecAivazis/survey/v2 v2.0.7
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
|
||||||
github.com/Jeffail/gabs/v2 v2.2.0
|
github.com/Jeffail/gabs/v2 v2.2.0
|
||||||
github.com/Microsoft/go-winio v0.4.7 // indirect
|
github.com/Microsoft/go-winio v0.4.7 // indirect
|
||||||
@@ -18,6 +19,7 @@ require (
|
|||||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a
|
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a
|
||||||
github.com/beevik/etree v1.1.0
|
github.com/beevik/etree v1.1.0
|
||||||
github.com/buger/jsonparser v0.0.0-20191204142016-1a29609e0929
|
github.com/buger/jsonparser v0.0.0-20191204142016-1a29609e0929
|
||||||
|
github.com/cobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249
|
||||||
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448 // indirect
|
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448 // indirect
|
||||||
github.com/creasty/defaults v1.3.0
|
github.com/creasty/defaults v1.3.0
|
||||||
github.com/docker/distribution v2.7.1+incompatible // indirect
|
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||||
@@ -27,41 +29,44 @@ require (
|
|||||||
github.com/gabriel-vasile/mimetype v0.1.4
|
github.com/gabriel-vasile/mimetype v0.1.4
|
||||||
github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.0
|
github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.0
|
||||||
github.com/ghodss/yaml v1.0.0
|
github.com/ghodss/yaml v1.0.0
|
||||||
github.com/gogo/protobuf v1.2.1 // indirect
|
github.com/gin-gonic/gin v1.6.2
|
||||||
|
github.com/golang/protobuf v1.3.5 // indirect
|
||||||
github.com/google/uuid v1.1.1
|
github.com/google/uuid v1.1.1
|
||||||
github.com/gorilla/websocket v1.4.0
|
github.com/gorilla/websocket v1.4.0
|
||||||
github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect
|
github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect
|
||||||
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334
|
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334
|
||||||
github.com/imdario/mergo v0.3.8
|
github.com/imdario/mergo v0.3.8
|
||||||
github.com/julienschmidt/httprouter v1.2.0
|
github.com/klauspost/pgzip v1.2.3
|
||||||
github.com/magiconair/properties v1.8.1
|
github.com/magiconair/properties v1.8.1
|
||||||
|
github.com/mattn/go-shellwords v1.0.10 // indirect
|
||||||
|
github.com/mholt/archiver/v3 v3.3.0
|
||||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db
|
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db
|
||||||
github.com/olebedev/emitter v0.0.0-20190110104742-e8d1457e6aee
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/onsi/ginkgo v1.8.0 // indirect
|
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||||
github.com/onsi/gomega v1.5.0 // indirect
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
|
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
|
||||||
github.com/opencontainers/image-spec v1.0.1 // indirect
|
github.com/opencontainers/image-spec v1.0.1 // indirect
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/pkg/errors v0.8.1
|
github.com/pkg/errors v0.9.1
|
||||||
|
github.com/pkg/profile v1.4.0
|
||||||
github.com/pkg/sftp v1.10.1 // indirect
|
github.com/pkg/sftp v1.10.1 // indirect
|
||||||
github.com/pterodactyl/sftp-server v1.1.1
|
github.com/pterodactyl/sftp-server v1.1.1
|
||||||
github.com/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce
|
github.com/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce
|
||||||
github.com/sirupsen/logrus v1.0.5 // indirect
|
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94
|
||||||
|
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||||
|
github.com/spf13/cobra v0.0.7
|
||||||
|
github.com/stretchr/testify v1.5.1 // indirect
|
||||||
go.uber.org/atomic v1.5.1 // indirect
|
go.uber.org/atomic v1.5.1 // indirect
|
||||||
go.uber.org/multierr v1.4.0 // indirect
|
go.uber.org/multierr v1.4.0 // indirect
|
||||||
go.uber.org/zap v1.13.0
|
go.uber.org/zap v1.13.0
|
||||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413
|
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904 // indirect
|
||||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f // indirect
|
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f // indirect
|
||||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect
|
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
|
||||||
golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab // indirect
|
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 // indirect
|
||||||
golang.org/x/text v0.3.2 // indirect
|
golang.org/x/tools v0.0.0-20200417140056-c07e33ef3290 // indirect
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||||
golang.org/x/tools v0.0.0-20191206204035-259af5ff87bd // indirect
|
|
||||||
golang.org/x/tools/gopls v0.1.7 // indirect
|
|
||||||
gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect
|
|
||||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect
|
|
||||||
gopkg.in/ini.v1 v1.51.0
|
gopkg.in/ini.v1 v1.51.0
|
||||||
gopkg.in/yaml.v2 v2.2.2
|
gopkg.in/yaml.v2 v2.2.8
|
||||||
gotest.tools v2.2.0+incompatible // indirect
|
gotest.tools v2.2.0+incompatible // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
285
go.sum
285
go.sum
@@ -1,3 +1,6 @@
|
|||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
github.com/AlecAivazis/survey/v2 v2.0.7 h1:+f825XHLse/hWd2tE/V5df04WFGimk34Eyg/z35w/rc=
|
||||||
|
github.com/AlecAivazis/survey/v2 v2.0.7/go.mod h1:mlizQTaPjnR4jcpwRSaSlkbsRfYFEyKgLQvYTzxxiHA=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
|
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||||
@@ -6,23 +9,43 @@ github.com/Jeffail/gabs/v2 v2.2.0 h1:7touC+WzbQ7LO5+mwgxT44miyTqAVCOlIWLA6PiIB5w
|
|||||||
github.com/Jeffail/gabs/v2 v2.2.0/go.mod h1:xCn81vdHKxFUuWWAaD5jCTQDNPBMh5pPs9IJ+NcziBI=
|
github.com/Jeffail/gabs/v2 v2.2.0/go.mod h1:xCn81vdHKxFUuWWAaD5jCTQDNPBMh5pPs9IJ+NcziBI=
|
||||||
github.com/Microsoft/go-winio v0.4.7 h1:vOvDiY/F1avSWlCWiKJjdYKz2jVjTK3pWPHndeG4OAY=
|
github.com/Microsoft/go-winio v0.4.7 h1:vOvDiY/F1avSWlCWiKJjdYKz2jVjTK3pWPHndeG4OAY=
|
||||||
github.com/Microsoft/go-winio v0.4.7/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
github.com/Microsoft/go-winio v0.4.7/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||||
|
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
|
||||||
|
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
|
||||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
||||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/andybalholm/brotli v0.0.0-20190621154722-5f990b63d2d6 h1:bZ28Hqta7TFAK3Q08CMvv8y3/8ATaEqv2nGoc6yff6c=
|
||||||
|
github.com/andybalholm/brotli v0.0.0-20190621154722-5f990b63d2d6/go.mod h1:+lx6/Aqd1kLJ1GQfkvOnaZ1WGmLpMpbprPuIOOZX30U=
|
||||||
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
|
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||||
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
|
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
|
||||||
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
||||||
github.com/buger/jsonparser v0.0.0-20181023193515-52c6e1462ebd/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23 h1:D21IyuvjDCshj1/qq+pCNd3VZOAEI9jy6Bi131YlXgI=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
|
||||||
github.com/buger/jsonparser v0.0.0-20191204142016-1a29609e0929 h1:MW/JDk68Rny52yI0M0N+P8lySNgB+NhpI/uAmhgOhUM=
|
github.com/buger/jsonparser v0.0.0-20191204142016-1a29609e0929 h1:MW/JDk68Rny52yI0M0N+P8lySNgB+NhpI/uAmhgOhUM=
|
||||||
github.com/buger/jsonparser v0.0.0-20191204142016-1a29609e0929/go.mod h1:tgcrVJ81GPSF0mz+0nu1Xaz0fazGPrmmJfJtxjbHhUQ=
|
github.com/buger/jsonparser v0.0.0-20191204142016-1a29609e0929/go.mod h1:tgcrVJ81GPSF0mz+0nu1Xaz0fazGPrmmJfJtxjbHhUQ=
|
||||||
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/cobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249 h1:R0IDH8daQ3lODvu8YtxnIqqth5qMGCJyADoUQvmLx4o=
|
||||||
|
github.com/cobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249/go.mod h1:EHKW9yNEYSBpTKzuu7Y9oOrft/UlzH57rMIB03oev6M=
|
||||||
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448 h1:PUD50EuOMkXVcpBIA/R95d56duJR9VxhwncsFbNnxW4=
|
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448 h1:PUD50EuOMkXVcpBIA/R95d56duJR9VxhwncsFbNnxW4=
|
||||||
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
|
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
|
||||||
|
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||||
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
github.com/creasty/defaults v1.3.0 h1:uG+RAxYbJgOPCOdKEcec9ZJXeva7Y6mj/8egdzwmLtw=
|
github.com/creasty/defaults v1.3.0 h1:uG+RAxYbJgOPCOdKEcec9ZJXeva7Y6mj/8egdzwmLtw=
|
||||||
github.com/creasty/defaults v1.3.0/go.mod h1:CIEEvs7oIVZm30R8VxtFJs+4k201gReYyuYHJxZc68I=
|
github.com/creasty/defaults v1.3.0/go.mod h1:CIEEvs7oIVZm30R8VxtFJs+4k201gReYyuYHJxZc68I=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
|
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
|
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
|
||||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||||
github.com/docker/docker v0.0.0-20180422163414-57142e89befe h1:VW8TnWi0CZgg7oCv0wH6evNwkzcJg/emnw4HrVIWws4=
|
github.com/docker/docker v0.0.0-20180422163414-57142e89befe h1:VW8TnWi0CZgg7oCv0wH6evNwkzcJg/emnw4HrVIWws4=
|
||||||
@@ -31,6 +54,9 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh
|
|||||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||||
github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk=
|
github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk=
|
||||||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
|
||||||
|
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
|
||||||
|
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/gabriel-vasile/mimetype v0.1.4 h1:5mcsq3+DXypREUkW+1juhjeKmE/XnWgs+paHMJn7lf8=
|
github.com/gabriel-vasile/mimetype v0.1.4 h1:5mcsq3+DXypREUkW+1juhjeKmE/XnWgs+paHMJn7lf8=
|
||||||
@@ -39,82 +65,216 @@ github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.0 h1:7KeiSrO5puFH1+vdAdbpiie2TrNnkvFc/eOQz
|
|||||||
github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.0/go.mod h1:D1+3UtCYAJ1os1PI+zhTVEj6Tb+IHJvXjXKz83OstmM=
|
github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.0/go.mod h1:D1+3UtCYAJ1os1PI+zhTVEj6Tb+IHJvXjXKz83OstmM=
|
||||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.6.2 h1:88crIK23zO6TqlQBt+f9FrPJNKm9ZEr7qjp9vl/d5TM=
|
||||||
|
github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||||
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||||
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
|
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||||
|
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||||
|
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||||
|
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
|
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
|
||||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||||
|
github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721 h1:KRMr9A3qfbVM7iV/WcLY/rL5LICqwMHLhwRXKu99fXw=
|
||||||
|
github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
|
||||||
|
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||||
|
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||||
|
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
github.com/gotestyourself/gotestyourself v2.2.0+incompatible h1:AQwinXlbQR2HvPjQZOmDhRqsv5mZf+Jb1RnSLxcqZcI=
|
github.com/gotestyourself/gotestyourself v2.2.0+incompatible h1:AQwinXlbQR2HvPjQZOmDhRqsv5mZf+Jb1RnSLxcqZcI=
|
||||||
github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY=
|
github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY=
|
||||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||||
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
|
||||||
|
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
|
||||||
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 h1:VHgatEHNcBFEB7inlalqfNqw65aNkM1lGX2yt3NmbS8=
|
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 h1:VHgatEHNcBFEB7inlalqfNqw65aNkM1lGX2yt3NmbS8=
|
||||||
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
|
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
|
||||||
github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
|
github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
|
||||||
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||||
|
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||||
|
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g=
|
github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g=
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||||
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
|
github.com/klauspost/compress v1.9.2 h1:LfVyl+ZlLlLDeQ/d2AqfGIIH4qEDu0Ed2S5GyhCWIWY=
|
||||||
|
github.com/klauspost/compress v1.9.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
|
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||||
|
github.com/klauspost/pgzip v1.2.1 h1:oIPZROsWuPHpOdMVWLuJZXwgjhrW8r1yEX8UqMyeNHM=
|
||||||
|
github.com/klauspost/pgzip v1.2.1/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||||
|
github.com/klauspost/pgzip v1.2.3 h1:Ce2to9wvs/cuJ2b86/CKQoTYr9VHfpanYosZ0UBJqdw=
|
||||||
|
github.com/klauspost/pgzip v1.2.3/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ=
|
||||||
|
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||||
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
github.com/magefile/mage v1.9.0 h1:t3AU2wNwehMCW97vuqQLtw6puppWXHO+O2MHo5a50XE=
|
github.com/magefile/mage v1.9.0 h1:t3AU2wNwehMCW97vuqQLtw6puppWXHO+O2MHo5a50XE=
|
||||||
github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||||
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||||
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw=
|
||||||
|
github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
|
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
||||||
|
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||||
|
github.com/mholt/archiver/v3 v3.3.0 h1:vWjhY8SQp5yzM9P6OJ/eZEkmi3UAbRrxCq48MxjAzig=
|
||||||
|
github.com/mholt/archiver/v3 v3.3.0/go.mod h1:YnQtqsp+94Rwd0D/rk5cnLrxusUBUXg+08Ebtr1Mqao=
|
||||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
|
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
|
||||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
|
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
|
||||||
github.com/olebedev/emitter v0.0.0-20190110104742-e8d1457e6aee h1:IquUs3fIykn10zWDIyddanhpTqBvAHMaPnFhQuyYw5U=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/olebedev/emitter v0.0.0-20190110104742-e8d1457e6aee/go.mod h1:eT2/Pcsim3XBjbvldGiJBvvgiqZkAFyiOJJsDKXs/ts=
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||||
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
|
github.com/nwaples/rardecode v1.0.0 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs=
|
||||||
|
github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||||
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
|
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
|
||||||
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||||
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
|
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
|
||||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||||
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
|
github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
|
||||||
|
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/profile v1.4.0 h1:uCmaf4vVbWAOZz36k1hrQD7ijGRzLwaME8Am/7a4jZI=
|
||||||
|
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
|
||||||
github.com/pkg/sftp v1.8.3 h1:9jSe2SxTM8/3bXZjtqnkgTBW+lA8db0knZJyns7gpBA=
|
github.com/pkg/sftp v1.8.3 h1:9jSe2SxTM8/3bXZjtqnkgTBW+lA8db0knZJyns7gpBA=
|
||||||
github.com/pkg/sftp v1.8.3/go.mod h1:NxmoDg/QLVWluQDUYG7XBZTLUpKeFa8e3aMf1BfjyHk=
|
github.com/pkg/sftp v1.8.3/go.mod h1:NxmoDg/QLVWluQDUYG7XBZTLUpKeFa8e3aMf1BfjyHk=
|
||||||
github.com/pkg/sftp v1.10.1 h1:VasscCm72135zRysgrJDKsntdmPN+OuU3+nnHYA9wyc=
|
github.com/pkg/sftp v1.10.1 h1:VasscCm72135zRysgrJDKsntdmPN+OuU3+nnHYA9wyc=
|
||||||
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pterodactyl/sftp-server v1.0.4 h1:hPUaUQvA6U/R8/bybQFDMBDcZaqqj+kufGBQZ3esP5M=
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
github.com/pterodactyl/sftp-server v1.0.4/go.mod h1:0LKDl+f1qY2TH9+B5jxdROktW0+10UM1qJ472iWbyvQ=
|
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||||
github.com/pterodactyl/sftp-server v1.1.0 h1:NcYh+UqEH8pfvFsee6yt7eb08RLLidw6q+cNOCdh/V0=
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
github.com/pterodactyl/sftp-server v1.1.0/go.mod h1:b1VVWYv0RF9rxSZQqaD/rYXriiRMNPsbV//CKMXR4ag=
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||||
|
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
|
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||||
github.com/pterodactyl/sftp-server v1.1.1 h1:IjuOy21BNZxfejKnXG1RgLxXAYylDqBVpbKZ6+fG5FQ=
|
github.com/pterodactyl/sftp-server v1.1.1 h1:IjuOy21BNZxfejKnXG1RgLxXAYylDqBVpbKZ6+fG5FQ=
|
||||||
github.com/pterodactyl/sftp-server v1.1.1/go.mod h1:b1VVWYv0RF9rxSZQqaD/rYXriiRMNPsbV//CKMXR4ag=
|
github.com/pterodactyl/sftp-server v1.1.1/go.mod h1:b1VVWYv0RF9rxSZQqaD/rYXriiRMNPsbV//CKMXR4ag=
|
||||||
github.com/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce h1:aP+C+YbHZfOQlutA4p4soHi7rVUqHQdWEVMSkHfDTqY=
|
github.com/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce h1:aP+C+YbHZfOQlutA4p4soHi7rVUqHQdWEVMSkHfDTqY=
|
||||||
github.com/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo=
|
github.com/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo=
|
||||||
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/sirupsen/logrus v1.0.5 h1:8c8b5uO0zS4X6RPl/sd1ENwSkIc0/H2PaHxE3udaE8I=
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94 h1:G04eS0JkAIVZfaJLjla9dNxkJCPiKIGZlw9AfOhzOD0=
|
||||||
|
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94/go.mod h1:b18R55ulyQ/h3RaWyloPyER7fWQVZvimKKhnI5OfrJQ=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
|
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
|
||||||
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
|
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||||
|
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
|
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
|
github.com/spf13/cobra v0.0.7 h1:FfTH+vuMXOas8jmfb5/M7dzEYx7LpcLb7a0LPe34uOU=
|
||||||
|
github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||||
|
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
|
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||||
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||||
github.com/uber-go/zap v1.9.1/go.mod h1:GY+83l3yxBcBw2kmHu/sAWwItnTn+ynxHCRo+WiIQOY=
|
github.com/uber-go/zap v1.9.1/go.mod h1:GY+83l3yxBcBw2kmHu/sAWwItnTn+ynxHCRo+WiIQOY=
|
||||||
|
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||||
|
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||||
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
|
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||||
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
|
github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
|
||||||
|
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||||
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||||
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||||
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||||
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
|
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
|
||||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||||
go.uber.org/atomic v1.5.1 h1:rsqfU5vBkVknbhUGbAUwQKR2H4ItV8tjJ+6kJX4cxHM=
|
go.uber.org/atomic v1.5.1 h1:rsqfU5vBkVknbhUGbAUwQKR2H4ItV8tjJ+6kJX4cxHM=
|
||||||
go.uber.org/atomic v1.5.1/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
go.uber.org/atomic v1.5.1/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||||
@@ -127,88 +287,109 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEa
|
|||||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||||
go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o=
|
go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o=
|
||||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU=
|
go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU=
|
||||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480 h1:O5YqonU5IWby+w98jVUG9h7zlCWCcH4RHyPVReBmhzk=
|
|
||||||
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904 h1:bXoxMPcSLOq08zI3/c5dEBT6lE4eh+jOh886GHrn6V8=
|
||||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
|
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE=
|
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE=
|
||||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
|
golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g=
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
|
||||||
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs=
|
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e h1:nFYrTHrdrAOpShe27kaFHjsqYSEQ0KWqdWLu3xuZJts=
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M=
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab h1:FvshnhkKW+LO3HWHodML8kuVX8rnJTxKm9dFPuI68UM=
|
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY=
|
||||||
golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
golang.org/x/tools v0.0.0-20190710153321-831012c29e42/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
|
golang.org/x/tools v0.0.0-20190710153321-831012c29e42/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
|
||||||
golang.org/x/tools v0.0.0-20190918214516-5a1a30219888/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20190925020647-22afafe3322a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs=
|
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs=
|
||||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20191206204035-259af5ff87bd h1:Zc7EU2PqpsNeIfOoVA7hvQX4cS3YDJEs5KlfatT3hLo=
|
golang.org/x/tools v0.0.0-20200417140056-c07e33ef3290 h1:NXNmtp0ToD36cui5IqWy95LC4Y6vT/4y3RnPxlQPinU=
|
||||||
golang.org/x/tools v0.0.0-20191206204035-259af5ff87bd/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20200417140056-c07e33ef3290/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
golang.org/x/tools/gopls v0.1.3/go.mod h1:vrCQzOKxvuiZLjCKSmbbov04oeBQQOb4VQqwYK2PWIY=
|
golang.org/x/tools/gopls v0.1.3/go.mod h1:vrCQzOKxvuiZLjCKSmbbov04oeBQQOb4VQqwYK2PWIY=
|
||||||
golang.org/x/tools/gopls v0.1.7/go.mod h1:PE3vTwT0ejw3a2L2fFgSJkxlEbA8Slbk+Lsy9hTmbG8=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
|
||||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0=
|
|
||||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
|
||||||
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
|
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
|
||||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
|
|||||||
580
http.go
580
http.go
@@ -1,580 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/buger/jsonparser"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/julienschmidt/httprouter"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/pterodactyl/wings/config"
|
|
||||||
"github.com/pterodactyl/wings/installer"
|
|
||||||
"github.com/pterodactyl/wings/server"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Retrieves a server out of the collection by UUID.
|
|
||||||
func (rt *Router) GetServer(uuid string) *server.Server {
|
|
||||||
return server.GetServers().Find(func(i *server.Server) bool {
|
|
||||||
return i.Uuid == uuid
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type Router struct {
|
|
||||||
upgrader websocket.Upgrader
|
|
||||||
|
|
||||||
// The authentication token defined in the config.yml file that allows
|
|
||||||
// a request to perform any action aganist the daemon.
|
|
||||||
token string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rt *Router) AuthenticateRequest(h httprouter.Handle) httprouter.Handle {
|
|
||||||
return rt.AuthenticateToken(rt.AuthenticateServer(h))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Middleware to protect server specific routes. This will ensure that the server exists and
|
|
||||||
// is in a state that allows it to be exposed to the API.
|
|
||||||
func (rt *Router) AuthenticateServer(h httprouter.Handle) httprouter.Handle {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
|
||||||
if rt.GetServer(ps.ByName("server")) != nil {
|
|
||||||
h(w, r, ps)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
http.NotFound(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attaches required access control headers to all of the requests.
|
|
||||||
func (rt *Router) AttachAccessControlHeaders(w http.ResponseWriter, r *http.Request, ps httprouter.Params) (http.ResponseWriter, *http.Request, httprouter.Params) {
|
|
||||||
w.Header().Set("Access-Control-Allow-Origin", config.Get().PanelLocation)
|
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
|
|
||||||
|
|
||||||
return w, r, ps
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authenticates the request token aganist the given permission string, ensuring that
|
|
||||||
// if it is a server permission, the token has control over that server. If it is a global
|
|
||||||
// token, this will ensure that the request is using a properly signed global token.
|
|
||||||
func (rt *Router) AuthenticateToken(h httprouter.Handle) httprouter.Handle {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
|
||||||
// Adds support for using this middleware on the websocket routes for servers. Those
|
|
||||||
// routes don't support Authorization headers, per the spec, so we abuse the socket
|
|
||||||
// protocol header and use that to pass the authorization token along to Wings without
|
|
||||||
// exposing the token in the URL directly. Neat. 📸
|
|
||||||
auth := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
|
|
||||||
|
|
||||||
if len(auth) != 2 || auth[0] != "Bearer" {
|
|
||||||
w.Header().Set("WWW-Authenticate", "Bearer")
|
|
||||||
http.Error(w, "authorization failed", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to match the request aganist the global token for the Daemon, regardless
|
|
||||||
// of the permission type. If nothing is matched we will fall through to the Panel
|
|
||||||
// API to try and validate permissions for a server.
|
|
||||||
if auth[1] == rt.token {
|
|
||||||
h(rt.AttachAccessControlHeaders(w, r, ps))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Happens because we don't have any of the server handling code here.
|
|
||||||
http.Error(w, "not implemented", http.StatusNotImplemented)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the basic Wings index page without anything else.
|
|
||||||
func (rt *Router) routeIndex(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
|
|
||||||
fmt.Fprint(w, "Welcome!\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns all of the servers that exist on the Daemon. This route is only accessible to
|
|
||||||
// requests that include an administrative control key, otherwise a 404 is returned. This
|
|
||||||
// authentication is handled by a middleware.
|
|
||||||
func (rt *Router) routeAllServers(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
|
|
||||||
json.NewEncoder(w).Encode(server.GetServers().All())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns basic information about a single server found on the Daemon.
|
|
||||||
func (rt *Router) routeServer(w http.ResponseWriter, _ *http.Request, ps httprouter.Params) {
|
|
||||||
s := rt.GetServer(ps.ByName("server"))
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
type PowerActionRequest struct {
|
|
||||||
Action string `json:"action"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateDirectoryRequest struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pr *PowerActionRequest) IsValid() bool {
|
|
||||||
return pr.Action == "start" || pr.Action == "stop" || pr.Action == "kill" || pr.Action == "restart"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handles a request to control the power state of a server. If the action being passed
|
|
||||||
// through is invalid a 404 is returned. Otherwise, a HTTP/202 Accepted response is returned
|
|
||||||
// and the actual power action is run asynchronously so that we don't have to block the
|
|
||||||
// request until a potentially slow operation completes.
|
|
||||||
//
|
|
||||||
// This is done because for the most part the Panel is using websockets to determine when
|
|
||||||
// things are happening, so theres no reason to sit and wait for a request to finish. We'll
|
|
||||||
// just see over the socket if something isn't working correctly.
|
|
||||||
func (rt *Router) routeServerPower(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
|
||||||
s := rt.GetServer(ps.ByName("server"))
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
dec := json.NewDecoder(r.Body)
|
|
||||||
var action PowerActionRequest
|
|
||||||
|
|
||||||
if err := dec.Decode(&action); err != nil {
|
|
||||||
// Don't flood the logs with error messages if someone sends through bad
|
|
||||||
// JSON data. We don't really care.
|
|
||||||
if err != io.EOF && err != io.ErrUnexpectedEOF {
|
|
||||||
zap.S().Errorw("failed to decode power action", zap.Error(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Error(w, "could not parse power action from request", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !action.IsValid() {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Because we route all of the actual bootup process to a seperate thread we need to
|
|
||||||
// check the suspension status here, otherwise the user will hit the endpoint and then
|
|
||||||
// just sit there wondering why it returns a success but nothing actually happens.
|
|
||||||
//
|
|
||||||
// We don't really care about any of the other actions at this point, they'll all result
|
|
||||||
// in the process being stopped, which should have happened anyways if the server is suspended.
|
|
||||||
if action.Action == "start" && s.Suspended {
|
|
||||||
http.Error(w, "server is suspended", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass the actual heavy processing off to a seperate thread to handle so that
|
|
||||||
// we can immediately return a response from the server.
|
|
||||||
go func(a string, s *server.Server) {
|
|
||||||
switch a {
|
|
||||||
case "start":
|
|
||||||
if err := s.Environment.Start(); err != nil {
|
|
||||||
zap.S().Errorw(
|
|
||||||
"encountered unexpected error starting server process",
|
|
||||||
zap.Error(err),
|
|
||||||
zap.String("server", s.Uuid),
|
|
||||||
zap.String("action", "start"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case "stop":
|
|
||||||
if err := s.Environment.Stop(); err != nil {
|
|
||||||
zap.S().Errorw(
|
|
||||||
"encountered unexpected error stopping server process",
|
|
||||||
zap.Error(err),
|
|
||||||
zap.String("server", s.Uuid),
|
|
||||||
zap.String("action", "stop"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case "restart":
|
|
||||||
break
|
|
||||||
case "kill":
|
|
||||||
if err := s.Environment.Terminate(os.Kill); err != nil {
|
|
||||||
zap.S().Errorw(
|
|
||||||
"encountered unexpected error killing server process",
|
|
||||||
zap.Error(err),
|
|
||||||
zap.String("server", s.Uuid),
|
|
||||||
zap.String("action", "kill"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}(action.Action, s)
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusAccepted)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the last 1Kb of the server log file.
|
|
||||||
func (rt *Router) routeServerLogs(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
|
||||||
s := rt.GetServer(ps.ByName("server"))
|
|
||||||
|
|
||||||
l, _ := strconv.ParseInt(r.URL.Query().Get("size"), 10, 64)
|
|
||||||
if l <= 0 {
|
|
||||||
l = 2048
|
|
||||||
}
|
|
||||||
|
|
||||||
out, err := s.ReadLogfile(l)
|
|
||||||
if err != nil {
|
|
||||||
zap.S().Errorw("failed to read server log file", zap.Error(err))
|
|
||||||
http.Error(w, "failed to read log", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(struct{ Data []string `json:"data"` }{Data: out})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle a request to get the contents of a file on the server.
|
|
||||||
func (rt *Router) routeServerFileRead(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
|
||||||
s := rt.GetServer(ps.ByName("server"))
|
|
||||||
|
|
||||||
cleaned, err := s.Filesystem.SafePath(r.URL.Query().Get("file"))
|
|
||||||
if err != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
st, err := s.Filesystem.Stat(cleaned)
|
|
||||||
if err != nil {
|
|
||||||
if !os.IsNotExist(err) {
|
|
||||||
zap.S().Errorw("failed to stat file for reading", zap.String("path", ps.ByName("path")), zap.String("server", s.Uuid), zap.Error(err))
|
|
||||||
|
|
||||||
http.Error(w, "failed to stat file", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.OpenFile(cleaned, os.O_RDONLY, 0)
|
|
||||||
if err != nil {
|
|
||||||
if !os.IsNotExist(err) {
|
|
||||||
zap.S().Errorw("failed to open file for reading", zap.String("path", ps.ByName("path")), zap.String("server", s.Uuid), zap.Error(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Error(w, "failed to open file", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
w.Header().Set("X-Mime-Type", st.Mimetype)
|
|
||||||
w.Header().Set("Content-Length", strconv.Itoa(int(st.Info.Size())))
|
|
||||||
|
|
||||||
// If a download parameter is included in the URL go ahead and attach the necessary headers
|
|
||||||
// so that the file can be downloaded.
|
|
||||||
if r.URL.Query().Get("download") != "" {
|
|
||||||
w.Header().Set("Content-Disposition", "attachment; filename="+st.Info.Name())
|
|
||||||
w.Header().Set("Content-Type", "application/octet-stream")
|
|
||||||
}
|
|
||||||
|
|
||||||
bufio.NewReader(f).WriteTo(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lists the contents of a directory.
|
|
||||||
func (rt *Router) routeServerListDirectory(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
|
||||||
s := rt.GetServer(ps.ByName("server"))
|
|
||||||
|
|
||||||
stats, err := s.Filesystem.ListDirectory(r.URL.Query().Get("directory"))
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
} else if err != nil {
|
|
||||||
zap.S().Errorw("failed to list contents of directory", zap.String("server", s.Uuid), zap.String("path", ps.ByName("path")), zap.Error(err))
|
|
||||||
|
|
||||||
http.Error(w, "failed to list directory", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(stats)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Writes a file to the system for the server.
|
|
||||||
func (rt *Router) routeServerWriteFile(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
|
||||||
s := rt.GetServer(ps.ByName("server"))
|
|
||||||
|
|
||||||
p := r.URL.Query().Get("file")
|
|
||||||
defer r.Body.Close()
|
|
||||||
err := s.Filesystem.Writefile(p, r.Body)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
zap.S().Errorw("failed to write file to directory", zap.String("server", s.Uuid), zap.String("path", p), zap.Error(err))
|
|
||||||
|
|
||||||
http.Error(w, "failed to write file to directory", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a new directory for the server.
|
|
||||||
func (rt *Router) routeServerCreateDirectory(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
|
||||||
s := rt.GetServer(ps.ByName("server"))
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
dec := json.NewDecoder(r.Body)
|
|
||||||
var data CreateDirectoryRequest
|
|
||||||
|
|
||||||
if err := dec.Decode(&data); err != nil {
|
|
||||||
// Don't flood the logs with error messages if someone sends through bad
|
|
||||||
// JSON data. We don't really care.
|
|
||||||
if err != io.EOF && err != io.ErrUnexpectedEOF {
|
|
||||||
zap.S().Errorw("failed to decode directory creation data", zap.Error(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Error(w, "could not parse data in request", http.StatusUnprocessableEntity)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Filesystem.CreateDirectory(data.Name, data.Path); err != nil {
|
|
||||||
zap.S().Errorw("failed to create directory for server", zap.String("server", s.Uuid), zap.Error(err))
|
|
||||||
|
|
||||||
http.Error(w, "an error was encountered while creating the directory", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rt *Router) routeServerRenameFile(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
|
||||||
s := rt.GetServer(ps.ByName("server"))
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
data := rt.ReaderToBytes(r.Body)
|
|
||||||
oldPath, _ := jsonparser.GetString(data, "rename_from")
|
|
||||||
newPath, _ := jsonparser.GetString(data, "rename_to")
|
|
||||||
|
|
||||||
if oldPath == "" || newPath == "" {
|
|
||||||
http.Error(w, "invalid paths provided; did you forget to provide an old path and new path?", http.StatusUnprocessableEntity)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Filesystem.Rename(oldPath, newPath); err != nil {
|
|
||||||
zap.S().Errorw("failed to rename file on server", zap.String("server", s.Uuid), zap.Error(err))
|
|
||||||
|
|
||||||
http.Error(w, "an error occurred while renaming the file", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rt *Router) routeServerCopyFile(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
|
||||||
s := rt.GetServer(ps.ByName("server"))
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
data := rt.ReaderToBytes(r.Body)
|
|
||||||
loc, _ := jsonparser.GetString(data, "location")
|
|
||||||
|
|
||||||
if err := s.Filesystem.Copy(loc); err != nil {
|
|
||||||
zap.S().Errorw("error copying file for server", zap.String("server", s.Uuid), zap.Error(err))
|
|
||||||
|
|
||||||
http.Error(w, "an error occurred while copying the file", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rt *Router) routeServerDeleteFile(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
|
||||||
s := rt.GetServer(ps.ByName("server"))
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
data := rt.ReaderToBytes(r.Body)
|
|
||||||
loc, _ := jsonparser.GetString(data, "location")
|
|
||||||
|
|
||||||
if err := s.Filesystem.Delete(loc); err != nil {
|
|
||||||
zap.S().Errorw("failed to delete a file or directory for server", zap.String("server", s.Uuid), zap.Error(err))
|
|
||||||
|
|
||||||
http.Error(w, "an error occurred while trying to delete a file or directory", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rt *Router) routeServerSendCommand(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
|
||||||
s := rt.GetServer(ps.ByName("server"))
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
if running, err := s.Environment.IsRunning(); !running || err != nil {
|
|
||||||
http.Error(w, "cannot send commands to a stopped instance", http.StatusBadGateway)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
data := rt.ReaderToBytes(r.Body)
|
|
||||||
commands, dt, _, _ := jsonparser.Get(data, "commands")
|
|
||||||
if dt != jsonparser.Array {
|
|
||||||
http.Error(w, "commands must be an array of strings", http.StatusUnprocessableEntity)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, command := range commands {
|
|
||||||
if err := s.Environment.SendCommand(string(command)); err != nil {
|
|
||||||
zap.S().Warnw("failed to send command to server", zap.Any("command", command), zap.Error(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rt *Router) routeServerInstall(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
|
||||||
s := rt.GetServer(ps.ByName("server"))
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
go func (serv *server.Server) {
|
|
||||||
if err := serv.Install(); err != nil {
|
|
||||||
zap.S().Errorw("failed to execute server installation process", zap.String("server", s.Uuid), zap.Error(err))
|
|
||||||
}
|
|
||||||
}(s)
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusAccepted)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rt *Router) routeServerUpdate(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
|
||||||
s := rt.GetServer(ps.ByName("server"))
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
data := rt.ReaderToBytes(r.Body)
|
|
||||||
if err := s.UpdateDataStructure(data, true); err != nil {
|
|
||||||
zap.S().Errorw("failed to update a server's data structure", zap.String("server", s.Uuid), zap.Error(err))
|
|
||||||
|
|
||||||
http.Error(w, "failed to update data structure", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rt *Router) routeCreateServer(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
inst, err := installer.New(rt.ReaderToBytes(r.Body))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
zap.S().Warnw("failed to validate the received data", zap.Error(err))
|
|
||||||
|
|
||||||
http.Error(w, "failed to validate data", http.StatusUnprocessableEntity)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Plop that server instance onto the request so that it can be referenced in
|
|
||||||
// requests from here-on out.
|
|
||||||
server.GetServers().Add(inst.Server())
|
|
||||||
|
|
||||||
zap.S().Infow("beginning installation process for server", zap.String("server", inst.Uuid()))
|
|
||||||
// Begin the installation process in the background to not block the request
|
|
||||||
// cycle. If there are any errors they will be logged and communicated back
|
|
||||||
// to the Panel where a reinstall may take place.
|
|
||||||
go func(i *installer.Installer) {
|
|
||||||
i.Execute()
|
|
||||||
|
|
||||||
if err := i.Server().Install(); err != nil {
|
|
||||||
zap.S().Errorw("failed to run install process for server", zap.String("server", i.Uuid()), zap.Error(err))
|
|
||||||
}
|
|
||||||
}(inst)
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusAccepted)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rt *Router) routeSystemInformation(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
s, err := GetSystemInformation()
|
|
||||||
if err != nil {
|
|
||||||
zap.S().Errorw("failed to retrieve system information", zap.Error(errors.WithStack(err)))
|
|
||||||
|
|
||||||
http.Error(w, "failed to retrieve information", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rt *Router) routeServerDelete(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
|
||||||
s := rt.GetServer(ps.ByName("server"))
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
// Immediately suspend the server to prevent a user from attempting
|
|
||||||
// to start it while this process is running.
|
|
||||||
s.Suspended = true
|
|
||||||
|
|
||||||
zap.S().Infow("processing server deletion request", zap.String("server", s.Uuid))
|
|
||||||
// Destroy the environment; in Docker this will handle a running container and
|
|
||||||
// forcibly terminate it before removing the container, so we do not need to handle
|
|
||||||
// that here.
|
|
||||||
if err := s.Environment.Destroy(); err != nil {
|
|
||||||
zap.S().Errorw("failed to destroy server environment", zap.Error(errors.WithStack(err)))
|
|
||||||
|
|
||||||
http.Error(w, "failed to destroy server environment", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Once the environment is terminated, remove the server files from the system. This is
|
|
||||||
// done in a seperate process since failure is not the end of the world and can be
|
|
||||||
// manually cleaned up after the fact.
|
|
||||||
//
|
|
||||||
// In addition, servers with large amounts of files can take some time to finish deleting
|
|
||||||
// so we don't want to block the HTTP call while waiting on this.
|
|
||||||
go func(p string) {
|
|
||||||
if err := os.RemoveAll(p); err != nil {
|
|
||||||
zap.S().Warnw("failed to remove server files on deletion", zap.String("path", p), zap.Error(errors.WithStack(err)))
|
|
||||||
}
|
|
||||||
}(s.Filesystem.Path())
|
|
||||||
|
|
||||||
var uuid = s.Uuid
|
|
||||||
server.GetServers().Remove(func(s2 *server.Server) bool {
|
|
||||||
return s2.Uuid == uuid
|
|
||||||
})
|
|
||||||
|
|
||||||
s = nil
|
|
||||||
|
|
||||||
// Remove the configuration file stored on the Daemon for this server.
|
|
||||||
go func(u string) {
|
|
||||||
if err := os.Remove("data/servers/" + u + ".yml"); err != nil {
|
|
||||||
zap.S().Warnw("failed to delete server configuration file on deletion", zap.String("server", u), zap.Error(errors.WithStack(err)))
|
|
||||||
}
|
|
||||||
}(uuid)
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusAccepted)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rt *Router) ReaderToBytes(r io.Reader) []byte {
|
|
||||||
buf := bytes.Buffer{}
|
|
||||||
buf.ReadFrom(r)
|
|
||||||
|
|
||||||
return buf.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configures the router and all of the associated routes.
|
|
||||||
func (rt *Router) ConfigureRouter() *httprouter.Router {
|
|
||||||
router := httprouter.New()
|
|
||||||
|
|
||||||
router.OPTIONS("/api/system", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
|
||||||
rt.AttachAccessControlHeaders(w, r, ps)
|
|
||||||
})
|
|
||||||
|
|
||||||
router.GET("/", rt.routeIndex)
|
|
||||||
router.GET("/api/system", rt.AuthenticateToken(rt.routeSystemInformation))
|
|
||||||
router.GET("/api/servers", rt.AuthenticateToken(rt.routeAllServers))
|
|
||||||
router.GET("/api/servers/:server", rt.AuthenticateRequest(rt.routeServer))
|
|
||||||
router.GET("/api/servers/:server/ws", rt.AuthenticateServer(rt.routeWebsocket))
|
|
||||||
router.GET("/api/servers/:server/logs", rt.AuthenticateRequest(rt.routeServerLogs))
|
|
||||||
router.GET("/api/servers/:server/files/contents", rt.AuthenticateRequest(rt.routeServerFileRead))
|
|
||||||
router.GET("/api/servers/:server/files/list-directory", rt.AuthenticateRequest(rt.routeServerListDirectory))
|
|
||||||
router.PUT("/api/servers/:server/files/rename", rt.AuthenticateRequest(rt.routeServerRenameFile))
|
|
||||||
router.POST("/api/servers", rt.AuthenticateToken(rt.routeCreateServer))
|
|
||||||
router.POST("/api/servers/:server/install", rt.AuthenticateRequest(rt.routeServerInstall))
|
|
||||||
router.POST("/api/servers/:server/files/copy", rt.AuthenticateRequest(rt.routeServerCopyFile))
|
|
||||||
router.POST("/api/servers/:server/files/write", rt.AuthenticateRequest(rt.routeServerWriteFile))
|
|
||||||
router.POST("/api/servers/:server/files/create-directory", rt.AuthenticateRequest(rt.routeServerCreateDirectory))
|
|
||||||
router.POST("/api/servers/:server/files/delete", rt.AuthenticateRequest(rt.routeServerDeleteFile))
|
|
||||||
router.POST("/api/servers/:server/power", rt.AuthenticateRequest(rt.routeServerPower))
|
|
||||||
router.POST("/api/servers/:server/commands", rt.AuthenticateRequest(rt.routeServerSendCommand))
|
|
||||||
router.PATCH("/api/servers/:server", rt.AuthenticateRequest(rt.routeServerUpdate))
|
|
||||||
router.DELETE("/api/servers/:server", rt.AuthenticateRequest(rt.routeServerDelete))
|
|
||||||
|
|
||||||
return router
|
|
||||||
}
|
|
||||||
19
installer/errors.go
Normal file
19
installer/errors.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package installer
|
||||||
|
|
||||||
|
type validationError struct {
|
||||||
|
msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *validationError) Error() string {
|
||||||
|
return e.msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsValidationError(err error) bool {
|
||||||
|
_, ok := err.(*validationError)
|
||||||
|
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewValidationError(msg string) error {
|
||||||
|
return &validationError{msg: msg}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
"github.com/buger/jsonparser"
|
"github.com/buger/jsonparser"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/pterodactyl/wings/api"
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"github.com/pterodactyl/wings/server"
|
"github.com/pterodactyl/wings/server"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@@ -21,11 +22,11 @@ type Installer struct {
|
|||||||
// calling Execute().
|
// calling Execute().
|
||||||
func New(data []byte) (*Installer, error) {
|
func New(data []byte) (*Installer, error) {
|
||||||
if !govalidator.IsUUIDv4(getString(data, "uuid")) {
|
if !govalidator.IsUUIDv4(getString(data, "uuid")) {
|
||||||
return nil, errors.New("uuid provided was not in a valid format")
|
return nil, NewValidationError("uuid provided was not in a valid format")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !govalidator.IsUUIDv4(getString(data, "service", "egg")) {
|
if !govalidator.IsUUIDv4(getString(data, "service", "egg")) {
|
||||||
return nil, errors.New("service egg provided was not in a valid format")
|
return nil, NewValidationError("service egg provided was not in a valid format")
|
||||||
}
|
}
|
||||||
|
|
||||||
s := &server.Server{
|
s := &server.Server{
|
||||||
@@ -40,14 +41,13 @@ func New(data []byte) (*Installer, error) {
|
|||||||
IoWeight: uint16(getInt(data, "build", "io")),
|
IoWeight: uint16(getInt(data, "build", "io")),
|
||||||
CpuLimit: getInt(data, "build", "cpu"),
|
CpuLimit: getInt(data, "build", "cpu"),
|
||||||
DiskSpace: getInt(data, "build", "disk"),
|
DiskSpace: getInt(data, "build", "disk"),
|
||||||
|
Threads: getString(data, "build", "threads"),
|
||||||
},
|
},
|
||||||
Allocations: server.Allocations{
|
Allocations: server.Allocations{
|
||||||
Mappings: make(map[string][]int),
|
Mappings: make(map[string][]int),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Init()
|
|
||||||
|
|
||||||
s.Allocations.DefaultMapping.Ip = getString(data, "allocations", "default", "ip")
|
s.Allocations.DefaultMapping.Ip = getString(data, "allocations", "default", "ip")
|
||||||
s.Allocations.DefaultMapping.Port = int(getInt(data, "allocations", "default", "port"))
|
s.Allocations.DefaultMapping.Port = int(getInt(data, "allocations", "default", "port"))
|
||||||
|
|
||||||
@@ -73,9 +73,13 @@ func New(data []byte) (*Installer, error) {
|
|||||||
|
|
||||||
s.Container.Image = getString(data, "container", "image")
|
s.Container.Image = getString(data, "container", "image")
|
||||||
|
|
||||||
b, err := s.WriteConfigurationToDisk()
|
c, rerr, err := api.NewRequester().GetServerConfiguration(s.Uuid)
|
||||||
if err != nil {
|
if err != nil || rerr != nil {
|
||||||
return nil, err
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New(rerr.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Destroy the temporary server instance.
|
// Destroy the temporary server instance.
|
||||||
@@ -83,7 +87,7 @@ func New(data []byte) (*Installer, 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.
|
||||||
s2, err := server.FromConfiguration(b, &config.Get().System)
|
s2, err := server.FromConfiguration(c)
|
||||||
|
|
||||||
return &Installer{
|
return &Installer{
|
||||||
server: s2,
|
server: s2,
|
||||||
@@ -115,7 +119,6 @@ func (i *Installer) Execute() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
zap.S().Debugw("creating required environment for server instance", zap.String("server", i.Uuid()))
|
zap.S().Debugw("creating required environment for server instance", zap.String("server", i.Uuid()))
|
||||||
if err := i.server.Environment.Create(); err != nil {
|
if err := i.server.Environment.Create(); err != nil {
|
||||||
zap.S().Errorw("failed to create environment for server", zap.String("server", i.Uuid()), zap.Error(err))
|
zap.S().Errorw("failed to create environment for server", zap.String("server", i.Uuid()), zap.Error(err))
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package parser
|
package parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"github.com/Jeffail/gabs/v2"
|
"github.com/Jeffail/gabs/v2"
|
||||||
"github.com/buger/jsonparser"
|
"github.com/buger/jsonparser"
|
||||||
"github.com/iancoleman/strcase"
|
"github.com/iancoleman/strcase"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"go.uber.org/zap"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -44,32 +47,19 @@ func readFileBytes(path string) ([]byte, error) {
|
|||||||
return ioutil.ReadAll(file)
|
return ioutil.ReadAll(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to set the value of the JSON key item based on the jsonparser value
|
|
||||||
// type returned.
|
|
||||||
func setPathway(c *gabs.Container, path string, value []byte, vt jsonparser.ValueType) error {
|
|
||||||
v := getKeyValue(value, vt)
|
|
||||||
|
|
||||||
_, err := c.SetP(v, path)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets the value of a key based on the value type defined.
|
// Gets the value of a key based on the value type defined.
|
||||||
func getKeyValue(value []byte, vt jsonparser.ValueType) interface{} {
|
func getKeyValue(value []byte) interface{} {
|
||||||
switch vt {
|
if reflect.ValueOf(value).Kind() == reflect.Bool {
|
||||||
case jsonparser.Number:
|
v, _ := strconv.ParseBool(string(value))
|
||||||
{
|
return v
|
||||||
v, _ := strconv.Atoi(string(value))
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
case jsonparser.Boolean:
|
|
||||||
{
|
|
||||||
v, _ := strconv.ParseBool(string(value))
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return string(value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to parse into an int, if this fails just ignore the error and
|
||||||
|
if v, err := strconv.Atoi(string(value)); err == nil {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterate over an unstructured JSON/YAML/etc. interface and set all of the required
|
// Iterate over an unstructured JSON/YAML/etc. interface and set all of the required
|
||||||
@@ -88,7 +78,7 @@ func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range f.Replace {
|
for _, v := range f.Replace {
|
||||||
value, dt, err := f.LookupConfigurationValue(v)
|
value, err := f.LookupConfigurationValue(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -104,12 +94,12 @@ func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error
|
|||||||
// If the child is a null value, nothing will happen. Seems reasonable as of the
|
// If the child is a null value, nothing will happen. Seems reasonable as of the
|
||||||
// time this code is being written.
|
// time this code is being written.
|
||||||
for _, child := range parsed.Path(strings.Trim(parts[0], ".")).Children() {
|
for _, child := range parsed.Path(strings.Trim(parts[0], ".")).Children() {
|
||||||
if err := setPathway(child, strings.Trim(parts[1], "."), value, dt); err != nil {
|
if err := v.SetAtPathway(child, strings.Trim(parts[1], "."), value); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err = setPathway(parsed, v.Match, value, dt); err != nil {
|
if err = v.SetAtPathway(parsed, v.Match, value); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,17 +108,60 @@ func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error
|
|||||||
return parsed, nil
|
return parsed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sets the value at a specific pathway, but checks if we were looking for a specific
|
||||||
|
// value or not before doing it.
|
||||||
|
func (cfr *ConfigurationFileReplacement) SetAtPathway(c *gabs.Container, path string, value []byte) error {
|
||||||
|
if cfr.IfValue != "" {
|
||||||
|
// If this is a regex based matching, we need to get a little more creative since
|
||||||
|
// we're only going to replacing part of the string, and not the whole thing.
|
||||||
|
if c.Exists(path) && strings.HasPrefix(cfr.IfValue, "regex:") {
|
||||||
|
// We're doing some regex here.
|
||||||
|
r, err := regexp.Compile(strings.TrimPrefix(cfr.IfValue, "regex:"))
|
||||||
|
if err != nil {
|
||||||
|
zap.S().Warnw(
|
||||||
|
"configuration if_value using invalid regexp, cannot do replacement",
|
||||||
|
zap.String("if_value", strings.TrimPrefix(cfr.IfValue, "regex:")),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the path exists and there is a regex match, go ahead and attempt the replacement
|
||||||
|
// using the value we got from the key. This will only replace the one match.
|
||||||
|
v := strings.Trim(string(c.Path(path).Bytes()), "\"")
|
||||||
|
if r.Match([]byte(v)) {
|
||||||
|
_, err := c.SetP(r.ReplaceAllString(v, string(value)), path)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
if !c.Exists(path) || (c.Exists(path) && !bytes.Equal(c.Bytes(), []byte(cfr.IfValue))) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := c.SetP(getKeyValue(value), path)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Looks up a configuration value on the Daemon given a dot-notated syntax.
|
// Looks up a configuration value on the Daemon given a dot-notated syntax.
|
||||||
func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplacement) ([]byte, jsonparser.ValueType, error) {
|
func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplacement) ([]byte, error) {
|
||||||
if !configMatchRegex.Match([]byte(cfr.Value)) {
|
// If this is not something that we can do a regex lookup on then just continue
|
||||||
return []byte(cfr.Value), cfr.ValueType, nil
|
// on our merry way. If the value isn't a string, we're not going to be doing anything
|
||||||
|
// with it anyways.
|
||||||
|
if cfr.ReplaceWith.Type() != jsonparser.String || !configMatchRegex.Match(cfr.ReplaceWith.Value()) {
|
||||||
|
return cfr.ReplaceWith.Value(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is a match, lookup the value in the configuration for the Daemon. If no key
|
// If there is a match, lookup the value in the configuration for the Daemon. If no key
|
||||||
// is found, just return the string representation, otherwise use the value from the
|
// is found, just return the string representation, otherwise use the value from the
|
||||||
// daemon configuration here.
|
// daemon configuration here.
|
||||||
huntPath := configMatchRegex.ReplaceAllString(
|
huntPath := configMatchRegex.ReplaceAllString(
|
||||||
configMatchRegex.FindString(cfr.Value), "$1",
|
configMatchRegex.FindString(cfr.ReplaceWith.String()), "$1",
|
||||||
)
|
)
|
||||||
|
|
||||||
var path []string
|
var path []string
|
||||||
@@ -141,18 +174,24 @@ func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplac
|
|||||||
|
|
||||||
// Look for the key in the configuration file, and if found return that value to the
|
// Look for the key in the configuration file, and if found return that value to the
|
||||||
// calling function.
|
// calling function.
|
||||||
match, dt, _, err := jsonparser.Get(f.configuration, path...)
|
match, _, _, err := jsonparser.Get(f.configuration, path...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != jsonparser.KeyPathNotFoundError {
|
if err != jsonparser.KeyPathNotFoundError {
|
||||||
return match, dt, errors.WithStack(err)
|
return match, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
zap.S().Debugw(
|
||||||
|
"attempted to load a configuration value that does not exist",
|
||||||
|
zap.Strings("path", path),
|
||||||
|
zap.String("filename", f.FileName),
|
||||||
|
)
|
||||||
|
|
||||||
// If there is no key, keep the original value intact, that way it is obvious there
|
// If there is no key, keep the original value intact, that way it is obvious there
|
||||||
// is a replace issue at play.
|
// is a replace issue at play.
|
||||||
return []byte(cfr.Value), cfr.ValueType, nil
|
return match, nil
|
||||||
} else {
|
} else {
|
||||||
replaced := []byte(configMatchRegex.ReplaceAllString(cfr.Value, string(match)))
|
replaced := []byte(configMatchRegex.ReplaceAllString(cfr.ReplaceWith.String(), string(match)))
|
||||||
|
|
||||||
return replaced, cfr.ValueType, nil
|
return replaced, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package parser
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"github.com/beevik/etree"
|
"github.com/beevik/etree"
|
||||||
"github.com/buger/jsonparser"
|
"github.com/buger/jsonparser"
|
||||||
"github.com/ghodss/yaml"
|
"github.com/ghodss/yaml"
|
||||||
@@ -43,29 +42,35 @@ type ConfigurationFile struct {
|
|||||||
|
|
||||||
// Defines a single find/replace instance for a given server configuration file.
|
// Defines a single find/replace instance for a given server configuration file.
|
||||||
type ConfigurationFileReplacement struct {
|
type ConfigurationFileReplacement struct {
|
||||||
Match string `json:"match"`
|
Match string `json:"match"`
|
||||||
Value string `json:"value"`
|
IfValue string `json:"if_value"`
|
||||||
ValueType jsonparser.ValueType `json:"-"`
|
ReplaceWith ReplaceValue `json:"replace_with"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handles unmarshaling the JSON representation into a struct that provides more useful
|
||||||
|
// data to this functionality.
|
||||||
func (cfr *ConfigurationFileReplacement) UnmarshalJSON(data []byte) error {
|
func (cfr *ConfigurationFileReplacement) UnmarshalJSON(data []byte) error {
|
||||||
if m, err := jsonparser.GetString(data, "match"); err != nil {
|
m, err := jsonparser.GetString(data, "match")
|
||||||
return err
|
if err != nil {
|
||||||
} else {
|
return errors.WithStack(err)
|
||||||
cfr.Match = m
|
|
||||||
}
|
}
|
||||||
|
cfr.Match = m
|
||||||
|
|
||||||
if v, dt, _, err := jsonparser.Get(data, "value"); err != nil {
|
iv, err := jsonparser.GetString(data, "if_value")
|
||||||
return err
|
// We only check keypath here since match & replace_with should be present on all of
|
||||||
} else {
|
// them, however if_value is optional.
|
||||||
if dt != jsonparser.String && dt != jsonparser.Number && dt != jsonparser.Boolean {
|
if err != nil && err != jsonparser.KeyPathNotFoundError {
|
||||||
return errors.New(
|
return errors.WithStack(err)
|
||||||
fmt.Sprintf("cannot parse JSON: received unexpected replacement value type: %d", dt),
|
}
|
||||||
)
|
cfr.IfValue = iv
|
||||||
}
|
|
||||||
|
|
||||||
cfr.Value = string(v)
|
rw, dt, _, err := jsonparser.Get(data, "replace_with")
|
||||||
cfr.ValueType = dt
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
cfr.ReplaceWith = ReplaceValue{
|
||||||
|
value: rw,
|
||||||
|
valueType: dt,
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -139,7 +144,7 @@ func (f *ConfigurationFile) parseXmlFile(path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, replacement := range f.Replace {
|
for i, replacement := range f.Replace {
|
||||||
value, _, err := f.LookupConfigurationValue(replacement)
|
value, err := f.LookupConfigurationValue(replacement)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -199,6 +204,11 @@ func (f *ConfigurationFile) parseXmlFile(path string) error {
|
|||||||
// Ensure the XML is indented properly.
|
// Ensure the XML is indented properly.
|
||||||
doc.Indent(2)
|
doc.Indent(2)
|
||||||
|
|
||||||
|
// Truncate the file before attempting to write the changes.
|
||||||
|
if err := os.Truncate(path, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Write the XML to the file.
|
// Write the XML to the file.
|
||||||
_, err = doc.WriteTo(file)
|
_, err = doc.WriteTo(file)
|
||||||
|
|
||||||
@@ -209,7 +219,7 @@ func (f *ConfigurationFile) parseXmlFile(path string) error {
|
|||||||
func (f *ConfigurationFile) parseIniFile(path string) error {
|
func (f *ConfigurationFile) parseIniFile(path string) error {
|
||||||
// Ini package can't handle a non-existent file, so handle that automatically here
|
// Ini package can't handle a non-existent file, so handle that automatically here
|
||||||
// by creating it if not exists.
|
// by creating it if not exists.
|
||||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0644);
|
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -223,7 +233,7 @@ func (f *ConfigurationFile) parseIniFile(path string) error {
|
|||||||
for _, replacement := range f.Replace {
|
for _, replacement := range f.Replace {
|
||||||
path := strings.SplitN(replacement.Match, ".", 2)
|
path := strings.SplitN(replacement.Match, ".", 2)
|
||||||
|
|
||||||
value, _, err := f.LookupConfigurationValue(replacement)
|
value, err := f.LookupConfigurationValue(replacement)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -256,6 +266,11 @@ func (f *ConfigurationFile) parseIniFile(path string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Truncate the file before attempting to write the changes.
|
||||||
|
if err := os.Truncate(path, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := cfg.WriteTo(file); err != nil {
|
if _, err := cfg.WriteTo(file); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -336,7 +351,7 @@ func (f *ConfigurationFile) parseTextFile(path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hasReplaced = true
|
hasReplaced = true
|
||||||
t = strings.Replace(t, replace.Match, replace.Value, 1)
|
t = strings.Replace(t, replace.Match, replace.ReplaceWith.String(), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there was a replacement that occurred on this specific line, do a write to the file
|
// If there was a replacement that occurred on this specific line, do a write to the file
|
||||||
@@ -364,17 +379,25 @@ func (f *ConfigurationFile) parsePropertiesFile(path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, replace := range f.Replace {
|
for _, replace := range f.Replace {
|
||||||
data, _, err := f.LookupConfigurationValue(replace)
|
data, err := f.LookupConfigurationValue(replace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
v, ok := p.Get(replace.Match)
|
||||||
|
// Don't attempt to replace the value if we're looking for a specific value and
|
||||||
|
// it does not match. If there was no match at all in the file for this key but
|
||||||
|
// we're doing an IfValue match, do nothing.
|
||||||
|
if replace.IfValue != "" && (!ok || (ok && v != replace.IfValue)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if _, _, err := p.Set(replace.Match, string(data)); err != nil {
|
if _, _, err := p.Set(replace.Match, string(data)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
w, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644)
|
w, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
22
parser/value.go
Normal file
22
parser/value.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/buger/jsonparser"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReplaceValue struct {
|
||||||
|
value []byte
|
||||||
|
valueType jsonparser.ValueType `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cv *ReplaceValue) Value() []byte {
|
||||||
|
return cv.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cv *ReplaceValue) String() string {
|
||||||
|
return string(cv.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cv *ReplaceValue) Type() jsonparser.ValueType {
|
||||||
|
return cv.valueType
|
||||||
|
}
|
||||||
99
router/error.go
Normal file
99
router/error.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/pterodactyl/wings/server"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RequestError struct {
|
||||||
|
Err error
|
||||||
|
Uuid string
|
||||||
|
Message string
|
||||||
|
server *server.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates a new tracked error, which simply tracks the specific error that
|
||||||
|
// is being passed in, and also assigned a UUID to the error so that it can be
|
||||||
|
// cross referenced in the logs.
|
||||||
|
func TrackedError(err error) *RequestError {
|
||||||
|
return &RequestError{
|
||||||
|
Err: err,
|
||||||
|
Uuid: uuid.Must(uuid.NewRandom()).String(),
|
||||||
|
Message: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same as TrackedError, except this will also attach the server instance that
|
||||||
|
// generated this server for the purposes of logging.
|
||||||
|
func TrackedServerError(err error, s *server.Server) *RequestError {
|
||||||
|
return &RequestError{
|
||||||
|
Err: err,
|
||||||
|
Uuid: uuid.Must(uuid.NewRandom()).String(),
|
||||||
|
Message: "",
|
||||||
|
server: s,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets the output message to display to the user in the error.
|
||||||
|
func (e *RequestError) SetMessage(msg string) *RequestError {
|
||||||
|
e.Message = msg
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aborts the request with the given status code, and responds with the error. This
|
||||||
|
// will also include the error UUID in the output so that the user can report that
|
||||||
|
// and link the response to a specific error in the logs.
|
||||||
|
func (e *RequestError) AbortWithStatus(status int, c *gin.Context) {
|
||||||
|
// If this error is because the resource does not exist, we likely do not need to log
|
||||||
|
// the error anywhere, just return a 404 and move on with our lives.
|
||||||
|
if os.IsNotExist(e.Err) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
||||||
|
"error": "The requested resource was not found on the system.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, log the error to zap, and then report the error back to the user.
|
||||||
|
if status >= 500 {
|
||||||
|
if e.server != nil {
|
||||||
|
zap.S().Errorw("encountered error while handling HTTP request", zap.String("server", e.server.Uuid), zap.String("error_id", e.Uuid), zap.Error(e.Err))
|
||||||
|
} else {
|
||||||
|
zap.S().Errorw("encountered error while handling HTTP request", zap.String("error_id", e.Uuid), zap.Error(e.Err))
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Error(errors.WithStack(e))
|
||||||
|
} else {
|
||||||
|
if e.server != nil {
|
||||||
|
zap.S().Debugw("encountered error while handling HTTP request", zap.String("server", e.server.Uuid), zap.String("error_id", e.Uuid), zap.Error(e.Err))
|
||||||
|
} else {
|
||||||
|
zap.S().Debugw("encountered error while handling HTTP request", zap.String("error_id", e.Uuid), zap.Error(e.Err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := "An unexpected error was encountered while processing this request."
|
||||||
|
if e.Message != "" {
|
||||||
|
msg = e.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
c.AbortWithStatusJSON(status, gin.H{
|
||||||
|
"error": msg,
|
||||||
|
"error_id": e.Uuid,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to just abort with an internal server error. This is generally the response
|
||||||
|
// from most errors encountered by the API.
|
||||||
|
func (e *RequestError) AbortWithServerError(c *gin.Context) {
|
||||||
|
e.AbortWithStatus(http.StatusInternalServerError, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format the error to a string and include the UUID.
|
||||||
|
func (e *RequestError) Error() string {
|
||||||
|
return fmt.Sprintf("%v (uuid: %s)", e.Err, e.Uuid)
|
||||||
|
}
|
||||||
67
router/middleware.go
Normal file
67
router/middleware.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"github.com/pterodactyl/wings/server"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Set the access request control headers on all of the requests.
|
||||||
|
func SetAccessControlHeaders(c *gin.Context) {
|
||||||
|
c.Header("Access-Control-Allow-Origin", config.Get().PanelLocation)
|
||||||
|
c.Header("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticates the request token against the given permission string, ensuring that
|
||||||
|
// if it is a server permission, the token has control over that server. If it is a global
|
||||||
|
// token, this will ensure that the request is using a properly signed global token.
|
||||||
|
func AuthorizationMiddleware(c *gin.Context) {
|
||||||
|
auth := strings.SplitN(c.GetHeader("Authorization"), " ", 2)
|
||||||
|
|
||||||
|
if len(auth) != 2 || auth[0] != "Bearer" {
|
||||||
|
c.Header("WWW-Authenticate", "Bearer")
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||||
|
"error": "The required authorization heads were not present in the request.",
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to match the request against the global token for the Daemon, regardless
|
||||||
|
// of the permission type. If nothing is matched we will fall through to the Panel
|
||||||
|
// API to try and validate permissions for a server.
|
||||||
|
if auth[1] == config.Get().AuthenticationToken {
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
|
||||||
|
"error": "You are not authorized to access this endpoint.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to fetch a server out of the servers collection stored in memory.
|
||||||
|
func GetServer(uuid string) *server.Server {
|
||||||
|
return server.GetServers().Find(func(s *server.Server) bool {
|
||||||
|
return uuid == s.Uuid
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that the requested server exists in this setup. Returns a 404 if we cannot
|
||||||
|
// locate it.
|
||||||
|
func ServerExists(c *gin.Context) {
|
||||||
|
u, err := uuid.Parse(c.Param("server"))
|
||||||
|
if err != nil || GetServer(u.String()) == nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
||||||
|
"error": "The requested server does not exist.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
77
router/router.go
Normal file
77
router/router.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Configures the routing infrastructure for this daemon instance.
|
||||||
|
func Configure() *gin.Engine {
|
||||||
|
router := gin.Default()
|
||||||
|
router.Use(SetAccessControlHeaders)
|
||||||
|
|
||||||
|
router.OPTIONS("/api/system", func(c *gin.Context) {
|
||||||
|
c.Status(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
// These routes use signed URLs to validate access to the resource being requested.
|
||||||
|
router.GET("/download/backup", getDownloadBackup)
|
||||||
|
router.GET("/download/file", getDownloadFile)
|
||||||
|
|
||||||
|
// This route is special it sits above all of the other requests because we are
|
||||||
|
// using a JWT to authorize access to it, therefore it needs to be publicly
|
||||||
|
// accessible.
|
||||||
|
router.GET("/api/servers/:server/ws", getServerWebsocket)
|
||||||
|
|
||||||
|
// This request is called by another daemon when a server is going to be transferred out.
|
||||||
|
// This request does not need the AuthorizationMiddleware as the panel should never call it
|
||||||
|
// and requests are authenticated through a JWT the panel issues to the other daemon.
|
||||||
|
router.GET("/api/servers/:server/archive", getServerArchive)
|
||||||
|
|
||||||
|
// All of the routes beyond this mount will use an authorization middleware
|
||||||
|
// and will not be accessible without the correct Authorization header provided.
|
||||||
|
protected := router.Use(AuthorizationMiddleware)
|
||||||
|
protected.POST("/api/update", postUpdateConfiguration)
|
||||||
|
protected.GET("/api/system", getSystemInformation)
|
||||||
|
protected.GET("/api/servers", getAllServers)
|
||||||
|
protected.POST("/api/servers", postCreateServer)
|
||||||
|
protected.POST("/api/transfer", postTransfer)
|
||||||
|
|
||||||
|
// These are server specific routes, and require that the request be authorized, and
|
||||||
|
// that the server exist on the Daemon.
|
||||||
|
server := router.Group("/api/servers/:server")
|
||||||
|
server.Use(AuthorizationMiddleware, ServerExists)
|
||||||
|
{
|
||||||
|
server.GET("", getServer)
|
||||||
|
server.PATCH("", patchServer)
|
||||||
|
server.DELETE("", deleteServer)
|
||||||
|
|
||||||
|
server.GET("/logs", getServerLogs)
|
||||||
|
server.POST("/power", postServerPower)
|
||||||
|
server.POST("/commands", postServerCommands)
|
||||||
|
server.POST("/install", postServerInstall)
|
||||||
|
server.POST("/reinstall", postServerReinstall)
|
||||||
|
|
||||||
|
// This archive request causes the archive to start being created
|
||||||
|
// this should only be triggered by the panel.
|
||||||
|
server.POST("/archive", postServerArchive)
|
||||||
|
|
||||||
|
files := server.Group("/files")
|
||||||
|
{
|
||||||
|
files.GET("/contents", getServerFileContents)
|
||||||
|
files.GET("/list-directory", getServerListDirectory)
|
||||||
|
files.PUT("/rename", putServerRenameFile)
|
||||||
|
files.POST("/copy", postServerCopyFile)
|
||||||
|
files.POST("/write", postServerWriteFile)
|
||||||
|
files.POST("/create-directory", postServerCreateDirectory)
|
||||||
|
files.POST("/delete", postServerDeleteFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
backup := server.Group("/backup")
|
||||||
|
{
|
||||||
|
backup.POST("", postServerBackup)
|
||||||
|
backup.DELETE("/:backup", deleteServerBackup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
||||||
97
router/router_download.go
Normal file
97
router/router_download.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/pterodactyl/wings/router/tokens"
|
||||||
|
"github.com/pterodactyl/wings/server/backup"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handle a download request for a server backup.
|
||||||
|
func getDownloadBackup(c *gin.Context) {
|
||||||
|
token := tokens.BackupPayload{}
|
||||||
|
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
|
||||||
|
TrackedError(err).AbortWithServerError(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s := GetServer(token.ServerUuid)
|
||||||
|
if s == nil || !token.IsUniqueRequest() {
|
||||||
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
||||||
|
"error": "The requested resource was not found on this server.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b, st, err := backup.LocateLocal(token.BackupUuid)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
||||||
|
"error": "The requested backup was not found on this server.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackedServerError(err, s).AbortWithServerError(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(b.Path())
|
||||||
|
if err != nil {
|
||||||
|
TrackedServerError(err, s).AbortWithServerError(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
c.Header("Content-Length", strconv.Itoa(int(st.Size())))
|
||||||
|
c.Header("Content-Disposition", "attachment; filename="+st.Name())
|
||||||
|
c.Header("Content-Type", "application/octet-stream")
|
||||||
|
|
||||||
|
bufio.NewReader(f).WriteTo(c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles downloading a specific file for a server.
|
||||||
|
func getDownloadFile(c *gin.Context) {
|
||||||
|
token := tokens.FilePayload{}
|
||||||
|
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
|
||||||
|
TrackedError(err).AbortWithServerError(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s := GetServer(token.ServerUuid)
|
||||||
|
if s == nil || !token.IsUniqueRequest() {
|
||||||
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
||||||
|
"error": "The requested resource was not found on this server.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p, _ := s.Filesystem.SafePath(token.FilePath)
|
||||||
|
st, err := os.Stat(p)
|
||||||
|
// If there is an error or we're somehow trying to download a directory, just
|
||||||
|
// respond with the appropriate error.
|
||||||
|
if err != nil {
|
||||||
|
TrackedServerError(err, s).AbortWithServerError(c)
|
||||||
|
return
|
||||||
|
} else if st.IsDir() {
|
||||||
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
||||||
|
"error": "The requested resource was not found on this server.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(p)
|
||||||
|
if err != nil {
|
||||||
|
TrackedServerError(err, s).AbortWithServerError(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header("Content-Length", strconv.Itoa(int(st.Size())))
|
||||||
|
c.Header("Content-Disposition", "attachment; filename="+st.Name())
|
||||||
|
c.Header("Content-Type", "application/octet-stream")
|
||||||
|
|
||||||
|
bufio.NewReader(f).WriteTo(c.Writer)
|
||||||
|
}
|
||||||
209
router/router_server.go
Normal file
209
router/router_server.go
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/pterodactyl/wings/server"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Returns a single server from the collection of servers.
|
||||||
|
func getServer(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, GetServer(c.Param("server")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the logs for a given server instance.
|
||||||
|
func getServerLogs(c *gin.Context) {
|
||||||
|
s := GetServer(c.Param("server"))
|
||||||
|
|
||||||
|
l, _ := strconv.ParseInt(c.DefaultQuery("size", "8192"), 10, 64)
|
||||||
|
if l <= 0 {
|
||||||
|
l = 2048
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := s.ReadLogfile(l)
|
||||||
|
if err != nil {
|
||||||
|
TrackedServerError(err, s).AbortWithServerError(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"data": out})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles a request to control the power state of a server. If the action being passed
|
||||||
|
// through is invalid a 404 is returned. Otherwise, a HTTP/202 Accepted response is returned
|
||||||
|
// and the actual power action is run asynchronously so that we don't have to block the
|
||||||
|
// request until a potentially slow operation completes.
|
||||||
|
//
|
||||||
|
// This is done because for the most part the Panel is using websockets to determine when
|
||||||
|
// things are happening, so theres no reason to sit and wait for a request to finish. We'll
|
||||||
|
// just see over the socket if something isn't working correctly.
|
||||||
|
func postServerPower(c *gin.Context) {
|
||||||
|
s := GetServer(c.Param("server"))
|
||||||
|
|
||||||
|
var data server.PowerAction
|
||||||
|
c.BindJSON(&data)
|
||||||
|
|
||||||
|
if !data.IsValid() {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
|
||||||
|
"error": "The power action provided was not valid, should be one of \"stop\", \"start\", \"restart\", \"kill\"",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Because we route all of the actual bootup process to a separate thread we need to
|
||||||
|
// check the suspension status here, otherwise the user will hit the endpoint and then
|
||||||
|
// just sit there wondering why it returns a success but nothing actually happens.
|
||||||
|
//
|
||||||
|
// We don't really care about any of the other actions at this point, they'll all result
|
||||||
|
// in the process being stopped, which should have happened anyways if the server is suspended.
|
||||||
|
if (data.Action == "start" || data.Action == "restart") && s.Suspended {
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": "Cannot start or restart a server that is suspended.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass the actual heavy processing off to a seperate thread to handle so that
|
||||||
|
// we can immediately return a response from the server. Some of these actions
|
||||||
|
// can take quite some time, especially stopping or restarting.
|
||||||
|
go func() {
|
||||||
|
if err := s.HandlePowerAction(data); err != nil {
|
||||||
|
zap.S().Errorw(
|
||||||
|
"encountered an error processing a server power action",
|
||||||
|
zap.String("server", s.Uuid),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
c.Status(http.StatusAccepted)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends an array of commands to a running server instance.
|
||||||
|
func postServerCommands(c *gin.Context) {
|
||||||
|
s := GetServer(c.Param("server"))
|
||||||
|
|
||||||
|
if running, err := s.Environment.IsRunning(); err != nil {
|
||||||
|
TrackedServerError(err, s).AbortWithServerError(c)
|
||||||
|
return
|
||||||
|
} else if !running {
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadGateway, gin.H{
|
||||||
|
"error": "Cannot send commands to a stopped server instance.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data struct{ Commands []string `json:"commands"` }
|
||||||
|
c.BindJSON(&data)
|
||||||
|
|
||||||
|
for _, command := range data.Commands {
|
||||||
|
if err := s.Environment.SendCommand(command); err != nil {
|
||||||
|
zap.S().Warnw(
|
||||||
|
"failed to send command to server",
|
||||||
|
zap.String("server", s.Uuid),
|
||||||
|
zap.String("command", command),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates information about a server internally.
|
||||||
|
func patchServer(c *gin.Context) {
|
||||||
|
s := GetServer(c.Param("server"))
|
||||||
|
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
buf.ReadFrom(c.Request.Body)
|
||||||
|
|
||||||
|
if err := s.UpdateDataStructure(buf.Bytes(), true); err != nil {
|
||||||
|
TrackedServerError(err, s).AbortWithServerError(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performs a server installation in a background thread.
|
||||||
|
func postServerInstall(c *gin.Context) {
|
||||||
|
s := GetServer(c.Param("server"))
|
||||||
|
|
||||||
|
go func(serv *server.Server) {
|
||||||
|
if err := serv.Install(); err != nil {
|
||||||
|
zap.S().Errorw(
|
||||||
|
"failed to execute server installation process",
|
||||||
|
zap.String("server", serv.Uuid),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}(s)
|
||||||
|
|
||||||
|
c.Status(http.StatusAccepted)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reinstalls a server.
|
||||||
|
func postServerReinstall(c *gin.Context) {
|
||||||
|
s := GetServer(c.Param("server"))
|
||||||
|
|
||||||
|
go func(serv *server.Server) {
|
||||||
|
if err := serv.Reinstall(); err != nil {
|
||||||
|
zap.S().Errorw(
|
||||||
|
"failed to complete server reinstall process",
|
||||||
|
zap.String("server", serv.Uuid),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}(s)
|
||||||
|
|
||||||
|
c.Status(http.StatusAccepted)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deletes a server from the wings daemon and deassociates its objects.
|
||||||
|
func deleteServer(c *gin.Context) {
|
||||||
|
s := GetServer(c.Param("server"))
|
||||||
|
|
||||||
|
// Immediately suspend the server to prevent a user from attempting
|
||||||
|
// to start it while this process is running.
|
||||||
|
s.Suspended = true
|
||||||
|
|
||||||
|
// Delete the server's archive if it exists. We intentionally don't return
|
||||||
|
// here, if the archive fails to delete, the server can still be removed.
|
||||||
|
if err := s.Archiver.DeleteIfExists(); err != nil {
|
||||||
|
zap.S().Warnw("failed to delete server archive during deletion process", zap.String("server", s.Uuid), zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy the environment; in Docker this will handle a running container and
|
||||||
|
// forcibly terminate it before removing the container, so we do not need to handle
|
||||||
|
// that here.
|
||||||
|
if err := s.Environment.Destroy(); err != nil {
|
||||||
|
TrackedServerError(err, s).AbortWithServerError(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once the environment is terminated, remove the server files from the system. This is
|
||||||
|
// done in a separate process since failure is not the end of the world and can be
|
||||||
|
// manually cleaned up after the fact.
|
||||||
|
//
|
||||||
|
// In addition, servers with large amounts of files can take some time to finish deleting
|
||||||
|
// so we don't want to block the HTTP call while waiting on this.
|
||||||
|
go func(p string) {
|
||||||
|
if err := os.RemoveAll(p); err != nil {
|
||||||
|
zap.S().Warnw("failed to remove server files during deletion process", zap.String("path", p), zap.Error(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
}(s.Filesystem.Path())
|
||||||
|
|
||||||
|
var uuid = s.Uuid
|
||||||
|
server.GetServers().Remove(func(s2 *server.Server) bool {
|
||||||
|
return s2.Uuid == uuid
|
||||||
|
})
|
||||||
|
|
||||||
|
// Deallocate the reference to this server.
|
||||||
|
s = nil
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
43
router/router_server_backup.go
Normal file
43
router/router_server_backup.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/pterodactyl/wings/server"
|
||||||
|
"github.com/pterodactyl/wings/server/backup"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Backs up a server.
|
||||||
|
func postServerBackup(c *gin.Context) {
|
||||||
|
s := GetServer(c.Param("server"))
|
||||||
|
|
||||||
|
data := &backup.LocalBackup{}
|
||||||
|
c.BindJSON(&data)
|
||||||
|
|
||||||
|
go func(b *backup.LocalBackup, serv *server.Server) {
|
||||||
|
if err := serv.BackupLocal(b); err != nil {
|
||||||
|
zap.S().Errorw("failed to generate backup for server", zap.Error(err))
|
||||||
|
}
|
||||||
|
}(data, s)
|
||||||
|
|
||||||
|
c.Status(http.StatusAccepted)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deletes a local backup of a server.
|
||||||
|
func deleteServerBackup(c *gin.Context) {
|
||||||
|
s := GetServer(c.Param("server"))
|
||||||
|
|
||||||
|
b, _, err := backup.LocateLocal(c.Param("backup"))
|
||||||
|
if err != nil {
|
||||||
|
TrackedServerError(err, s).AbortWithServerError(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.Remove(); err != nil {
|
||||||
|
TrackedServerError(err, s).AbortWithServerError(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
155
router/router_server_files.go
Normal file
155
router/router_server_files.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Returns the contents of a file on the server.
|
||||||
|
func getServerFileContents(c *gin.Context) {
|
||||||
|
s := GetServer(c.Param("server"))
|
||||||
|
cleaned, err := s.Filesystem.SafePath(c.Query("file"))
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
||||||
|
"error": "The file requested could not be found.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
st, err := s.Filesystem.Stat(cleaned)
|
||||||
|
if err != nil {
|
||||||
|
TrackedServerError(err, s).AbortWithServerError(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if st.Info.IsDir() {
|
||||||
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
||||||
|
"error": "The requested resource was not found on the system.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(cleaned)
|
||||||
|
if err != nil {
|
||||||
|
TrackedServerError(err, s).AbortWithServerError(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
c.Header("X-Mime-Type", st.Mimetype)
|
||||||
|
c.Header("Content-Length", strconv.Itoa(int(st.Info.Size())))
|
||||||
|
|
||||||
|
// If a download parameter is included in the URL go ahead and attach the necessary headers
|
||||||
|
// so that the file can be downloaded.
|
||||||
|
if c.Query("download") != "" {
|
||||||
|
c.Header("Content-Disposition", "attachment; filename="+st.Info.Name())
|
||||||
|
c.Header("Content-Type", "application/octet-stream")
|
||||||
|
}
|
||||||
|
|
||||||
|
bufio.NewReader(f).WriteTo(c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the contents of a directory for a server.
|
||||||
|
func getServerListDirectory(c *gin.Context) {
|
||||||
|
s := GetServer(c.Param("server"))
|
||||||
|
|
||||||
|
stats, err := s.Filesystem.ListDirectory(c.Query("directory"))
|
||||||
|
if err != nil {
|
||||||
|
TrackedServerError(err, s).AbortWithServerError(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renames (or moves) a file for a server.
|
||||||
|
func putServerRenameFile(c *gin.Context) {
|
||||||
|
s := GetServer(c.Param("server"))
|
||||||
|
|
||||||
|
var data struct{
|
||||||
|
RenameFrom string `json:"rename_from"`
|
||||||
|
RenameTo string `json:"rename_to"`
|
||||||
|
}
|
||||||
|
c.BindJSON(&data)
|
||||||
|
|
||||||
|
if data.RenameFrom == "" || data.RenameTo == "" {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
|
||||||
|
"error": "Invalid paths were provided, did you forget to provide both a new and old path?",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Filesystem.Rename(data.RenameFrom, data.RenameTo); err != nil {
|
||||||
|
TrackedServerError(err, s).AbortWithServerError(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copies a server file.
|
||||||
|
func postServerCopyFile(c *gin.Context) {
|
||||||
|
s := GetServer(c.Param("server"))
|
||||||
|
|
||||||
|
var data struct {
|
||||||
|
Location string `json:"location"`
|
||||||
|
}
|
||||||
|
c.BindJSON(&data)
|
||||||
|
|
||||||
|
if err := s.Filesystem.Copy(data.Location); err != nil {
|
||||||
|
TrackedServerError(err, s).AbortWithServerError(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deletes a server file.
|
||||||
|
func postServerDeleteFile(c *gin.Context) {
|
||||||
|
s := GetServer(c.Param("server"))
|
||||||
|
|
||||||
|
var data struct {
|
||||||
|
Location string `json:"location"`
|
||||||
|
}
|
||||||
|
c.BindJSON(&data)
|
||||||
|
|
||||||
|
if err := s.Filesystem.Delete(data.Location); err != nil {
|
||||||
|
TrackedServerError(err, s).AbortWithServerError(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writes the contents of the request to a file on a server.
|
||||||
|
func postServerWriteFile(c *gin.Context) {
|
||||||
|
s := GetServer(c.Param("server"))
|
||||||
|
|
||||||
|
if err := s.Filesystem.Writefile(c.Query("file"), c.Request.Body); err != nil {
|
||||||
|
TrackedServerError(err, s).AbortWithServerError(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a directory on a server.
|
||||||
|
func postServerCreateDirectory(c *gin.Context) {
|
||||||
|
s := GetServer(c.Param("server"))
|
||||||
|
|
||||||
|
var data struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
}
|
||||||
|
c.BindJSON(&data)
|
||||||
|
|
||||||
|
if err := s.Filesystem.CreateDirectory(data.Name, data.Path); err != nil {
|
||||||
|
TrackedServerError(err, s).AbortWithServerError(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
60
router/router_server_ws.go
Normal file
60
router/router_server_ws.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
ws "github.com/gorilla/websocket"
|
||||||
|
"github.com/pterodactyl/wings/router/websocket"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Upgrades a connection to a websocket and passes events along between.
|
||||||
|
func getServerWebsocket(c *gin.Context) {
|
||||||
|
s := GetServer(c.Param("server"))
|
||||||
|
handler, err := websocket.GetHandler(s, c.Writer, c.Request)
|
||||||
|
if err != nil {
|
||||||
|
TrackedServerError(err, s).AbortWithServerError(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer handler.Connection.Close()
|
||||||
|
|
||||||
|
// Create a context that can be canceled when the user disconnects from this
|
||||||
|
// socket that will also cancel listeners running in separate threads.
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
go handler.ListenForServerEvents(ctx)
|
||||||
|
go handler.ListenForExpiration(ctx)
|
||||||
|
|
||||||
|
for {
|
||||||
|
j := websocket.Message{}
|
||||||
|
|
||||||
|
_, p, err := handler.Connection.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
if !ws.IsCloseError(
|
||||||
|
err,
|
||||||
|
ws.CloseNormalClosure,
|
||||||
|
ws.CloseGoingAway,
|
||||||
|
ws.CloseNoStatusReceived,
|
||||||
|
ws.CloseServiceRestart,
|
||||||
|
ws.CloseAbnormalClosure,
|
||||||
|
) {
|
||||||
|
zap.S().Warnw("error handling websocket message", zap.Error(err))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discard and JSON parse errors into the void and don't continue processing this
|
||||||
|
// specific socket request. If we did a break here the client would get disconnected
|
||||||
|
// from the socket, which is NOT what we want to do.
|
||||||
|
if err := json.Unmarshal(p, &j); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := handler.HandleInbound(j); err != nil {
|
||||||
|
handler.SendErrorJson(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
93
router/router_system.go
Normal file
93
router/router_system.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"github.com/pterodactyl/wings/installer"
|
||||||
|
"github.com/pterodactyl/wings/server"
|
||||||
|
"github.com/pterodactyl/wings/system"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Returns information about the system that wings is running on.
|
||||||
|
func getSystemInformation(c *gin.Context) {
|
||||||
|
i, err := system.GetSystemInformation()
|
||||||
|
if err != nil {
|
||||||
|
TrackedError(err).AbortWithServerError(c)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns all of the servers that are registered and configured correctly on
|
||||||
|
// this wings instance.
|
||||||
|
func getAllServers(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, server.GetServers().All())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new server on the wings daemon and begins the installation process
|
||||||
|
// for it.
|
||||||
|
func postCreateServer(c *gin.Context) {
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
buf.ReadFrom(c.Request.Body)
|
||||||
|
|
||||||
|
install, err := installer.New(buf.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
if installer.IsValidationError(err) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
|
||||||
|
"error": "The data provided in the request could not be validated.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackedError(err).AbortWithServerError(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plop that server instance onto the request so that it can be referenced in
|
||||||
|
// requests from here-on out.
|
||||||
|
server.GetServers().Add(install.Server())
|
||||||
|
|
||||||
|
// Begin the installation process in the background to not block the request
|
||||||
|
// cycle. If there are any errors they will be logged and communicated back
|
||||||
|
// to the Panel where a reinstall may take place.
|
||||||
|
go func(i *installer.Installer) {
|
||||||
|
i.Execute()
|
||||||
|
|
||||||
|
if err := i.Server().Install(); err != nil {
|
||||||
|
zap.S().Errorw(
|
||||||
|
"failed to run install process for server",
|
||||||
|
zap.String("server", i.Uuid()),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}(install)
|
||||||
|
|
||||||
|
c.Status(http.StatusAccepted)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates the running configuration for this daemon instance.
|
||||||
|
func postUpdateConfiguration(c *gin.Context) {
|
||||||
|
// A backup of the configuration for error purposes.
|
||||||
|
ccopy := *config.Get()
|
||||||
|
// A copy of the configuration we're using to bind the data recevied into.
|
||||||
|
cfg := *config.Get()
|
||||||
|
|
||||||
|
c.BindJSON(&cfg)
|
||||||
|
|
||||||
|
config.Set(&cfg)
|
||||||
|
if err := config.Get().WriteToDisk(); err != nil {
|
||||||
|
// If there was an error writing to the disk, revert back to the configuration we had
|
||||||
|
// before this code was run.
|
||||||
|
config.Set(&ccopy)
|
||||||
|
|
||||||
|
TrackedError(err).AbortWithServerError(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
313
router/router_transfer.go
Normal file
313
router/router_transfer.go
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"github.com/buger/jsonparser"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/mholt/archiver/v3"
|
||||||
|
"github.com/pterodactyl/wings/api"
|
||||||
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"github.com/pterodactyl/wings/installer"
|
||||||
|
"github.com/pterodactyl/wings/router/tokens"
|
||||||
|
"github.com/pterodactyl/wings/server"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getServerArchive(c *gin.Context) {
|
||||||
|
auth := strings.SplitN(c.GetHeader("Authorization"), " ", 2)
|
||||||
|
|
||||||
|
if len(auth) != 2 || auth[0] != "Bearer" {
|
||||||
|
c.Header("WWW-Authenticate", "Bearer")
|
||||||
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||||
|
"error": "The required authorization heads were not present in the request.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token := tokens.TransferPayload{}
|
||||||
|
if err := tokens.ParseToken([]byte(auth[1]), &token); err != nil {
|
||||||
|
TrackedError(err).AbortWithServerError(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if token.Subject != c.Param("server") {
|
||||||
|
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
|
||||||
|
"error": "( .. •˘___˘• .. )",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s := GetServer(c.Param("server"))
|
||||||
|
|
||||||
|
st, err := s.Archiver.Stat()
|
||||||
|
if err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
TrackedServerError(err, s).SetMessage("failed to stat archive").AbortWithServerError(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.AbortWithStatus(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
checksum, err := s.Archiver.Checksum()
|
||||||
|
if err != nil {
|
||||||
|
TrackedServerError(err, s).SetMessage("failed to calculate checksum").AbortWithServerError(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(s.Archiver.ArchivePath())
|
||||||
|
if err != nil {
|
||||||
|
tserr := TrackedServerError(err, s)
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
tserr.SetMessage("failed to open archive for reading")
|
||||||
|
} else {
|
||||||
|
tserr.SetMessage("failed to open archive")
|
||||||
|
}
|
||||||
|
|
||||||
|
tserr.AbortWithServerError(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
c.Header("X-Checksum", checksum)
|
||||||
|
c.Header("X-Mime-Type", st.Mimetype)
|
||||||
|
c.Header("Content-Length", strconv.Itoa(int(st.Info.Size())))
|
||||||
|
c.Header("Content-Disposition", "attachment; filename="+s.Archiver.ArchiveName())
|
||||||
|
c.Header("Content-Type", "application/octet-stream")
|
||||||
|
|
||||||
|
bufio.NewReader(file).WriteTo(c.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func postServerArchive(c *gin.Context) {
|
||||||
|
s := GetServer(c.Param("server"))
|
||||||
|
|
||||||
|
go func(server *server.Server) {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
if err := server.Archiver.Archive(); err != nil {
|
||||||
|
zap.S().Errorw("failed to get archive for server", zap.String("server", s.Uuid), zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.S().Debugw(
|
||||||
|
"successfully created archive for server",
|
||||||
|
zap.String("server", server.Uuid),
|
||||||
|
zap.Duration("time", time.Now().Sub(start).Round(time.Microsecond)),
|
||||||
|
)
|
||||||
|
|
||||||
|
r := api.NewRequester()
|
||||||
|
rerr, err := r.SendArchiveStatus(server.Uuid, true)
|
||||||
|
if rerr != nil || err != nil {
|
||||||
|
if err != nil {
|
||||||
|
zap.S().Errorw("failed to notify panel with archive status", zap.String("server", server.Uuid), zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.S().Errorw(
|
||||||
|
"panel returned an error when sending the archive status",
|
||||||
|
zap.String("server", server.Uuid),
|
||||||
|
zap.Error(errors.New(rerr.String())),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.S().Debugw("successfully notified panel about archive status", zap.String("server", server.Uuid))
|
||||||
|
}(s)
|
||||||
|
|
||||||
|
c.Status(http.StatusAccepted)
|
||||||
|
}
|
||||||
|
|
||||||
|
func postTransfer(c *gin.Context) {
|
||||||
|
zap.S().Debug("incoming transfer from panel")
|
||||||
|
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
buf.ReadFrom(c.Request.Body)
|
||||||
|
|
||||||
|
go func(data []byte) {
|
||||||
|
serverID, _ := jsonparser.GetString(data, "server_id")
|
||||||
|
url, _ := jsonparser.GetString(data, "url")
|
||||||
|
token, _ := jsonparser.GetString(data, "token")
|
||||||
|
|
||||||
|
// Create an http client with no timeout.
|
||||||
|
client := &http.Client{Timeout: 0}
|
||||||
|
|
||||||
|
hasError := true
|
||||||
|
defer func() {
|
||||||
|
if !hasError {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.S().Errorw("server transfer has failed", zap.String("server", serverID))
|
||||||
|
rerr, err := api.NewRequester().SendTransferFailure(serverID)
|
||||||
|
if rerr != nil || err != nil {
|
||||||
|
if err != nil {
|
||||||
|
zap.S().Errorw("failed to notify panel with transfer failure", zap.String("server", serverID), zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.S().Errorw("panel returned an error when notifying of a transfer failure", zap.String("server", serverID), zap.Error(errors.New(rerr.String())))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.S().Debugw("successfully notified panel about transfer failure", zap.String("server", serverID))
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Make a new GET request to the URL the panel gave us.
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
zap.S().Errorw("failed to create http request", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the authorization header.
|
||||||
|
req.Header.Set("Authorization", token)
|
||||||
|
|
||||||
|
// Execute the http request.
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
zap.S().Errorw("failed to send http request", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
// Handle non-200 status codes.
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
body, err := ioutil.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
zap.S().Errorw("failed to read response body", zap.Int("status", res.StatusCode), zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.S().Errorw("failed to request server archive", zap.Int("status", res.StatusCode), zap.String("body", string(body)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the path to the archive.
|
||||||
|
archivePath := filepath.Join(config.Get().System.ArchiveDirectory, serverID + ".tar.gz")
|
||||||
|
|
||||||
|
// Check if the archive already exists and delete it if it does.
|
||||||
|
_, err = os.Stat(archivePath)
|
||||||
|
if err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
zap.S().Errorw("failed to stat file", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := os.Remove(archivePath); err != nil {
|
||||||
|
zap.S().Errorw("failed to delete old file", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the file.
|
||||||
|
file, err := os.Create(archivePath)
|
||||||
|
if err != nil {
|
||||||
|
zap.S().Errorw("failed to open file on disk", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the file.
|
||||||
|
_, err = io.Copy(file, res.Body)
|
||||||
|
if err != nil {
|
||||||
|
zap.S().Errorw("failed to copy file to disk", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the file so it can be opened to verify the checksum.
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
zap.S().Errorw("failed to close archive file", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
zap.S().Debug("server archive has been downloaded, computing checksum..", zap.String("server", serverID))
|
||||||
|
|
||||||
|
// Open the archive file for computing a checksum.
|
||||||
|
file, err = os.Open(archivePath)
|
||||||
|
if err != nil {
|
||||||
|
zap.S().Errorw("failed to open file on disk", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the sha256 checksum of the file.
|
||||||
|
hash := sha256.New()
|
||||||
|
if _, err := io.Copy(hash, file); err != nil {
|
||||||
|
zap.S().Errorw("failed to copy file for checksum verification", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the two checksums.
|
||||||
|
if hex.EncodeToString(hash.Sum(nil)) != res.Header.Get("X-Checksum") {
|
||||||
|
zap.S().Errorw("checksum failed verification")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the file.
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
zap.S().Errorw("failed to close archive file", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.S().Infow("server archive transfer was successful", zap.String("server", serverID))
|
||||||
|
|
||||||
|
// Get the server data from the request.
|
||||||
|
serverData, t, _, _ := jsonparser.Get(data, "server")
|
||||||
|
if t != jsonparser.Object {
|
||||||
|
zap.S().Errorw("invalid server data passed in request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new server installer (note this does not execute the install script)
|
||||||
|
i, err := installer.New(serverData)
|
||||||
|
if err != nil {
|
||||||
|
zap.S().Warnw("failed to validate the received server data", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the server to the collection.
|
||||||
|
server.GetServers().Add(i.Server())
|
||||||
|
|
||||||
|
// Create the server's environment (note this does not execute the install script)
|
||||||
|
i.Execute()
|
||||||
|
|
||||||
|
// Un-archive the archive. That sounds weird..
|
||||||
|
if err := archiver.NewTarGz().Unarchive(archivePath, i.Server().Filesystem.Path()); err != nil {
|
||||||
|
zap.S().Errorw("failed to extract archive", zap.String("server", serverID), zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We mark the process as being successful here as if we fail to send a transfer success,
|
||||||
|
// then a transfer failure won't probably be successful either.
|
||||||
|
//
|
||||||
|
// It may be useful to retry sending the transfer success every so often just in case of a small
|
||||||
|
// hiccup or the fix of whatever error causing the success request to fail.
|
||||||
|
hasError = false
|
||||||
|
|
||||||
|
// Notify the panel that the transfer succeeded.
|
||||||
|
rerr, err := api.NewRequester().SendTransferSuccess(serverID)
|
||||||
|
if rerr != nil || err != nil {
|
||||||
|
if err != nil {
|
||||||
|
zap.S().Errorw("failed to notify panel with transfer success", zap.String("server", serverID), zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.S().Errorw("panel returned an error when notifying of a transfer success", zap.String("server", serverID), zap.Error(errors.New(rerr.String())))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.S().Debugw("successfully notified panel about transfer success", zap.String("server", serverID))
|
||||||
|
}(buf.Bytes())
|
||||||
|
|
||||||
|
c.Status(http.StatusAccepted)
|
||||||
|
}
|
||||||
25
router/tokens/backup.go
Normal file
25
router/tokens/backup.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package tokens
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gbrlsnchs/jwt/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BackupPayload struct {
|
||||||
|
jwt.Payload
|
||||||
|
ServerUuid string `json:"server_uuid"`
|
||||||
|
BackupUuid string `json:"backup_uuid"`
|
||||||
|
UniqueId string `json:"unique_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the JWT payload.
|
||||||
|
func (p *BackupPayload) GetPayload() *jwt.Payload {
|
||||||
|
return &p.Payload
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determines if this JWT is valid for the given request cycle. If the
|
||||||
|
// unique ID passed in the token has already been seen before this will
|
||||||
|
// return false. This allows us to use this JWT as a one-time token that
|
||||||
|
// validates all of the request.
|
||||||
|
func (p *BackupPayload) IsUniqueRequest() bool {
|
||||||
|
return getTokenStore().IsValidToken(p.UniqueId)
|
||||||
|
}
|
||||||
25
router/tokens/file.go
Normal file
25
router/tokens/file.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package tokens
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gbrlsnchs/jwt/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FilePayload struct {
|
||||||
|
jwt.Payload
|
||||||
|
FilePath string `json:"file_path"`
|
||||||
|
ServerUuid string `json:"server_uuid"`
|
||||||
|
UniqueId string `json:"unique_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the JWT payload.
|
||||||
|
func (p *FilePayload) GetPayload() *jwt.Payload {
|
||||||
|
return &p.Payload
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determines if this JWT is valid for the given request cycle. If the
|
||||||
|
// unique ID passed in the token has already been seen before this will
|
||||||
|
// return false. This allows us to use this JWT as a one-time token that
|
||||||
|
// validates all of the request.
|
||||||
|
func (p *FilePayload) IsUniqueRequest() bool {
|
||||||
|
return getTokenStore().IsValidToken(p.UniqueId)
|
||||||
|
}
|
||||||
27
router/tokens/parser.go
Normal file
27
router/tokens/parser.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package tokens
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gbrlsnchs/jwt/v3"
|
||||||
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TokenData interface {
|
||||||
|
GetPayload() *jwt.Payload
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validates the provided JWT against the known secret for the Daemon and returns the
|
||||||
|
// parsed data. This function DOES NOT validate that the token is valid for the connected
|
||||||
|
// server, nor does it ensure that the user providing the token is able to actually do things.
|
||||||
|
//
|
||||||
|
// This simply returns a parsed token.
|
||||||
|
func ParseToken(token []byte, data TokenData) error {
|
||||||
|
verifyOptions := jwt.ValidatePayload(
|
||||||
|
data.GetPayload(),
|
||||||
|
jwt.ExpirationTimeValidator(time.Now()),
|
||||||
|
)
|
||||||
|
|
||||||
|
_, err := jwt.Verify(token, config.GetJwtAlgorithm(), &data, verifyOptions)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
41
router/tokens/token_store.go
Normal file
41
router/tokens/token_store.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package tokens
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/patrickmn/go-cache"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TokenStore struct {
|
||||||
|
sync.Mutex
|
||||||
|
cache *cache.Cache
|
||||||
|
}
|
||||||
|
|
||||||
|
var _tokens *TokenStore
|
||||||
|
|
||||||
|
// Returns the global unique token store cache. This is used to validate
|
||||||
|
// one time token usage by storing any received tokens in a local memory
|
||||||
|
// cache until they are ready to expire.
|
||||||
|
func getTokenStore() *TokenStore {
|
||||||
|
if _tokens == nil {
|
||||||
|
_tokens = &TokenStore{
|
||||||
|
cache: cache.New(time.Minute*60, time.Minute*5),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if a token is valid or not.
|
||||||
|
func (t *TokenStore) IsValidToken(token string) bool {
|
||||||
|
t.Lock()
|
||||||
|
defer t.Unlock()
|
||||||
|
|
||||||
|
_, exists := t.cache.Get(token)
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
t.cache.Add(token, "", time.Minute*60)
|
||||||
|
}
|
||||||
|
|
||||||
|
return !exists
|
||||||
|
}
|
||||||
14
router/tokens/transfer.go
Normal file
14
router/tokens/transfer.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package tokens
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gbrlsnchs/jwt/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TransferPayload struct {
|
||||||
|
jwt.Payload
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPayload returns the JWT payload.
|
||||||
|
func (p *TransferPayload) GetPayload() *jwt.Payload {
|
||||||
|
return &p.Payload
|
||||||
|
}
|
||||||
30
router/tokens/websocket.go
Normal file
30
router/tokens/websocket.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package tokens
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/gbrlsnchs/jwt/v3"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebsocketPayload struct {
|
||||||
|
jwt.Payload
|
||||||
|
UserID json.Number `json:"user_id"`
|
||||||
|
ServerUUID string `json:"server_uuid"`
|
||||||
|
Permissions []string `json:"permissions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the JWT payload.
|
||||||
|
func (p *WebsocketPayload) GetPayload() *jwt.Payload {
|
||||||
|
return &p.Payload
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if the given token payload has a permission string.
|
||||||
|
func (p *WebsocketPayload) HasPermission(permission string) bool {
|
||||||
|
for _, k := range p.Permissions {
|
||||||
|
if k == permission || (!strings.HasPrefix(permission, "admin") && k == "*") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
71
router/websocket/listeners.go
Normal file
71
router/websocket/listeners.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package websocket
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/pterodactyl/wings/server"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Checks the time to expiration on the JWT every 30 seconds until the token has
|
||||||
|
// expired. If we are within 3 minutes of the token expiring, send a notice over
|
||||||
|
// the socket that it is expiring soon. If it has expired, send that notice as well.
|
||||||
|
func (h *Handler) ListenForExpiration(ctx context.Context) {
|
||||||
|
// Make a ticker and completion channel that is used to continuously poll the
|
||||||
|
// JWT stored in the session to send events to the socket when it is expiring.
|
||||||
|
ticker := time.NewTicker(time.Second * 30)
|
||||||
|
|
||||||
|
// Whenever this function is complete, end the ticker, close out the channel,
|
||||||
|
// and then close the websocket connection.
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
jwt := h.GetJwt()
|
||||||
|
if jwt != nil {
|
||||||
|
if jwt.ExpirationTime.Unix()-time.Now().Unix() <= 0 {
|
||||||
|
h.SendJson(&Message{Event: TokenExpiredEvent})
|
||||||
|
} else if jwt.ExpirationTime.Unix()-time.Now().Unix() <= 180 {
|
||||||
|
h.SendJson(&Message{Event: TokenExpiringEvent})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listens for different events happening on a server and sends them along
|
||||||
|
// to the connected websocket.
|
||||||
|
func (h *Handler) ListenForServerEvents(ctx context.Context) {
|
||||||
|
events := []string{
|
||||||
|
server.StatsEvent,
|
||||||
|
server.StatusEvent,
|
||||||
|
server.ConsoleOutputEvent,
|
||||||
|
server.InstallOutputEvent,
|
||||||
|
server.DaemonMessageEvent,
|
||||||
|
server.BackupCompletedEvent,
|
||||||
|
}
|
||||||
|
|
||||||
|
eventChannel := make(chan server.Event)
|
||||||
|
for _, event := range events {
|
||||||
|
h.server.Events().Subscribe(event, eventChannel)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
for _, event := range events {
|
||||||
|
h.server.Events().Unsubscribe(event, eventChannel)
|
||||||
|
}
|
||||||
|
|
||||||
|
close(eventChannel)
|
||||||
|
default:
|
||||||
|
// Listen for different events emitted by the server and respond to them appropriately.
|
||||||
|
for d := range eventChannel {
|
||||||
|
h.SendJson(&Message{
|
||||||
|
Event: d.Topic,
|
||||||
|
Args: []string{d.Data},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
router/websocket/message.go
Normal file
26
router/websocket/message.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package websocket
|
||||||
|
|
||||||
|
const (
|
||||||
|
AuthenticationSuccessEvent = "auth success"
|
||||||
|
TokenExpiringEvent = "token expiring"
|
||||||
|
TokenExpiredEvent = "token expired"
|
||||||
|
AuthenticationEvent = "auth"
|
||||||
|
SetStateEvent = "set state"
|
||||||
|
SendServerLogsEvent = "send logs"
|
||||||
|
SendCommandEvent = "send command"
|
||||||
|
ErrorEvent = "daemon error"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
// The event to perform. Should be one of the following that are supported:
|
||||||
|
//
|
||||||
|
// - status : Returns the server's power state.
|
||||||
|
// - logs : Returns the server log data at the time of the request.
|
||||||
|
// - power : Performs a power action aganist the server based the data.
|
||||||
|
// - command : Performs a command on a server using the data field.
|
||||||
|
Event string `json:"event"`
|
||||||
|
|
||||||
|
// The data to pass along, only used by power/command currently. Other requests
|
||||||
|
// should either omit the field or pass an empty value as it is ignored.
|
||||||
|
Args []string `json:"args,omitempty"`
|
||||||
|
}
|
||||||
303
router/websocket/websocket.go
Normal file
303
router/websocket/websocket.go
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
package websocket
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/gbrlsnchs/jwt/v3"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"github.com/pterodactyl/wings/router/tokens"
|
||||||
|
"github.com/pterodactyl/wings/server"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PermissionConnect = "websocket.connect"
|
||||||
|
PermissionSendCommand = "control.console"
|
||||||
|
PermissionSendPowerStart = "control.start"
|
||||||
|
PermissionSendPowerStop = "control.stop"
|
||||||
|
PermissionSendPowerRestart = "control.restart"
|
||||||
|
PermissionReceiveErrors = "admin.websocket.errors"
|
||||||
|
PermissionReceiveInstall = "admin.websocket.install"
|
||||||
|
PermissionReceiveBackups = "backup.read"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
sync.RWMutex
|
||||||
|
Connection *websocket.Conn
|
||||||
|
jwt *tokens.WebsocketPayload `json:"-"`
|
||||||
|
server *server.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parses a JWT into a websocket token payload.
|
||||||
|
func NewTokenPayload(token []byte) (*tokens.WebsocketPayload, error) {
|
||||||
|
payload := tokens.WebsocketPayload{}
|
||||||
|
err := tokens.ParseToken(token, &payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !payload.HasPermission(PermissionConnect) {
|
||||||
|
return nil, errors.New("not authorized to connect to this socket")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a new websocket handler using the context provided.
|
||||||
|
func GetHandler(s *server.Server, w http.ResponseWriter, r *http.Request) (*Handler, error) {
|
||||||
|
upgrader := websocket.Upgrader{
|
||||||
|
// Ensure that the websocket request is originating from the Panel itself,
|
||||||
|
// and not some other location.
|
||||||
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
|
return r.Header.Get("Origin") == config.Get().PanelLocation
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Handler{
|
||||||
|
Connection: conn,
|
||||||
|
jwt: nil,
|
||||||
|
server: s,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) SendJson(v *Message) error {
|
||||||
|
// Do not send JSON down the line if the JWT on the connection is not
|
||||||
|
// valid!
|
||||||
|
if err := h.TokenValid(); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
j := h.GetJwt()
|
||||||
|
if j != nil {
|
||||||
|
// If we're sending installation output but the user does not have the required
|
||||||
|
// permissions to see the output, don't send it down the line.
|
||||||
|
if v.Event == server.InstallOutputEvent {
|
||||||
|
zap.S().Debugf("%+v", v.Args)
|
||||||
|
if !j.HasPermission(PermissionReceiveInstall) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user does not have permission to see backup events, do not emit
|
||||||
|
// them over the socket.
|
||||||
|
if strings.HasPrefix(v.Event, server.BackupCompletedEvent) {
|
||||||
|
if !j.HasPermission(PermissionReceiveBackups) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.unsafeSendJson(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends JSON over the websocket connection, ignoring the authentication state of the
|
||||||
|
// socket user. Do not call this directly unless you are positive a response should be
|
||||||
|
// sent back to the client!
|
||||||
|
func (h *Handler) unsafeSendJson(v interface{}) error {
|
||||||
|
h.Lock()
|
||||||
|
defer h.Unlock()
|
||||||
|
|
||||||
|
return h.Connection.WriteJSON(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if the JWT is still valid.
|
||||||
|
func (h *Handler) TokenValid() error {
|
||||||
|
j := h.GetJwt()
|
||||||
|
if j == nil {
|
||||||
|
return errors.New("no jwt present")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := jwt.ExpirationTimeValidator(time.Now())(&j.Payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !j.HasPermission(PermissionConnect) {
|
||||||
|
return errors.New("jwt does not have connect permission")
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.server.Uuid != j.ServerUUID {
|
||||||
|
return errors.New("jwt server uuid mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends an error back to the connected websocket instance by checking the permissions
|
||||||
|
// of the token. If the user has the "receive-errors" grant we will send back the actual
|
||||||
|
// error message, otherwise we just send back a standard error message.
|
||||||
|
func (h *Handler) SendErrorJson(err error) error {
|
||||||
|
h.Lock()
|
||||||
|
defer h.Unlock()
|
||||||
|
|
||||||
|
j := h.GetJwt()
|
||||||
|
|
||||||
|
message := "an unexpected error was encountered while handling this request"
|
||||||
|
if server.IsSuspendedError(err) || (j != nil && j.HasPermission(PermissionReceiveErrors)) {
|
||||||
|
message = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
m, u := h.GetErrorMessage(message)
|
||||||
|
|
||||||
|
wsm := Message{Event: ErrorEvent}
|
||||||
|
wsm.Args = []string{m}
|
||||||
|
|
||||||
|
if !server.IsSuspendedError(err) {
|
||||||
|
zap.S().Errorw(
|
||||||
|
"an error was encountered in the websocket process",
|
||||||
|
zap.String("server", h.server.Uuid),
|
||||||
|
zap.String("error_identifier", u.String()),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Connection.WriteJSON(wsm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts an error message into a more readable representation and returns a UUID
|
||||||
|
// that can be cross-referenced to find the specific error that triggered.
|
||||||
|
func (h *Handler) GetErrorMessage(msg string) (string, uuid.UUID) {
|
||||||
|
u := uuid.Must(uuid.NewRandom())
|
||||||
|
|
||||||
|
m := fmt.Sprintf("Error Event [%s]: %s", u.String(), msg)
|
||||||
|
|
||||||
|
return m, u
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets the JWT for the websocket in a race-safe manner.
|
||||||
|
func (h *Handler) setJwt(token *tokens.WebsocketPayload) {
|
||||||
|
h.Lock()
|
||||||
|
h.jwt = token
|
||||||
|
h.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) GetJwt() *tokens.WebsocketPayload {
|
||||||
|
h.RLock()
|
||||||
|
defer h.RUnlock()
|
||||||
|
|
||||||
|
return h.jwt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the inbound socket request and route it to the proper server action.
|
||||||
|
func (h *Handler) HandleInbound(m Message) error {
|
||||||
|
if m.Event != AuthenticationEvent {
|
||||||
|
if err := h.TokenValid(); err != nil {
|
||||||
|
zap.S().Debugw("jwt token is no longer valid", zap.String("message", err.Error()))
|
||||||
|
|
||||||
|
h.unsafeSendJson(Message{
|
||||||
|
Event: ErrorEvent,
|
||||||
|
Args: []string{"could not authenticate client: " + err.Error()},
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch m.Event {
|
||||||
|
case AuthenticationEvent:
|
||||||
|
{
|
||||||
|
token, err := NewTokenPayload([]byte(strings.Join(m.Args, "")))
|
||||||
|
if err != nil {
|
||||||
|
// If the error says the JWT expired, send a token expired
|
||||||
|
// event and hopefully the client renews the token.
|
||||||
|
if err == jwt.ErrExpValidation {
|
||||||
|
h.SendJson(&Message{Event: TokenExpiredEvent})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if token.HasPermission(PermissionConnect) {
|
||||||
|
h.setJwt(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// On every authentication event, send the current server status back
|
||||||
|
// to the client. :)
|
||||||
|
h.server.Events().Publish(server.StatusEvent, h.server.GetState())
|
||||||
|
|
||||||
|
h.unsafeSendJson(Message{
|
||||||
|
Event: AuthenticationSuccessEvent,
|
||||||
|
Args: []string{},
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
case SetStateEvent:
|
||||||
|
{
|
||||||
|
switch strings.Join(m.Args, "") {
|
||||||
|
case "start":
|
||||||
|
if h.GetJwt().HasPermission(PermissionSendPowerStart) {
|
||||||
|
return h.server.Environment.Start()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "stop":
|
||||||
|
if h.GetJwt().HasPermission(PermissionSendPowerStop) {
|
||||||
|
return h.server.Environment.Stop()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "restart":
|
||||||
|
if h.GetJwt().HasPermission(PermissionSendPowerRestart) {
|
||||||
|
if err := h.server.Environment.WaitForStop(60, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.server.Environment.Start()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "kill":
|
||||||
|
if h.GetJwt().HasPermission(PermissionSendPowerStop) {
|
||||||
|
return h.server.Environment.Terminate(os.Kill)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
case SendServerLogsEvent:
|
||||||
|
{
|
||||||
|
if running, _ := h.server.Environment.IsRunning(); !running {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, err := h.server.Environment.Readlog(1024 * 16)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range logs {
|
||||||
|
h.SendJson(&Message{
|
||||||
|
Event: server.ConsoleOutputEvent,
|
||||||
|
Args: []string{line},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
case SendCommandEvent:
|
||||||
|
{
|
||||||
|
if !h.GetJwt().HasPermission(PermissionSendCommand) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.server.GetState() == server.ProcessOfflineState {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.server.Environment.SendCommand(strings.Join(m.Args, ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
104
server/archiver.go
Normal file
104
server/archiver.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"github.com/mholt/archiver/v3"
|
||||||
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Archiver represents a Server Archiver.
|
||||||
|
type Archiver struct {
|
||||||
|
Server *Server
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArchivePath returns the path to the server's archive.
|
||||||
|
func (a *Archiver) ArchivePath() string {
|
||||||
|
return filepath.Join(config.Get().System.ArchiveDirectory, a.ArchiveName())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArchiveName returns the name of the server's archive.
|
||||||
|
func (a *Archiver) ArchiveName() string {
|
||||||
|
return a.Server.Uuid + ".tar.gz"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exists returns a boolean based off if the archive exists.
|
||||||
|
func (a *Archiver) Exists() bool {
|
||||||
|
if _, err := os.Stat(a.ArchivePath()); os.IsNotExist(err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stat stats the archive file.
|
||||||
|
func (a *Archiver) Stat() (*Stat, error) {
|
||||||
|
return a.Server.Filesystem.unsafeStat(a.ArchivePath())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Archive creates an archive of the server and deletes the previous one.
|
||||||
|
func (a *Archiver) Archive() error {
|
||||||
|
path := a.Server.Filesystem.Path()
|
||||||
|
|
||||||
|
// Get the list of root files and directories to archive.
|
||||||
|
var files []string
|
||||||
|
fileInfo, err := ioutil.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range fileInfo {
|
||||||
|
files = append(files, filepath.Join(path, file.Name()))
|
||||||
|
}
|
||||||
|
|
||||||
|
stat, err := a.Stat()
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the file exists.
|
||||||
|
if stat != nil {
|
||||||
|
if err := os.Remove(a.ArchivePath()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return archiver.NewTarGz().Archive(files, a.ArchivePath())
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteIfExists deletes the archive if it exists.
|
||||||
|
func (a *Archiver) DeleteIfExists() error {
|
||||||
|
stat, err := a.Stat()
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the file exists.
|
||||||
|
if stat != nil {
|
||||||
|
if err := os.Remove(a.ArchivePath()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checksum computes a SHA256 checksum of the server's archive.
|
||||||
|
func (a *Archiver) Checksum() (string, error) {
|
||||||
|
file, err := os.Open(a.ArchivePath())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
hash := sha256.New()
|
||||||
|
if _, err := io.Copy(hash, file); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex.EncodeToString(hash.Sum(nil)), nil
|
||||||
|
}
|
||||||
96
server/backup.go
Normal file
96
server/backup.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/pterodactyl/wings/api"
|
||||||
|
"github.com/pterodactyl/wings/server/backup"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
r := api.NewRequester()
|
||||||
|
rerr, err := r.SendBackupStatus(uuid, ad.ToRequest(successful))
|
||||||
|
if rerr != nil || err != nil {
|
||||||
|
if err != nil {
|
||||||
|
zap.S().Errorw(
|
||||||
|
"failed to notify panel of backup status due to internal code error",
|
||||||
|
zap.String("backup", s.Uuid),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.S().Warnw(rerr.String(), zap.String("backup", uuid))
|
||||||
|
|
||||||
|
return errors.New(rerr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performs a server backup and then emits the event over the server websocket. We
|
||||||
|
// let the actual backup system handle notifying the panel of the status, but that
|
||||||
|
// won't emit a websocket event.
|
||||||
|
func (s *Server) BackupLocal(b *backup.LocalBackup) error {
|
||||||
|
// If no ignored files are present in the request, check for a .pteroignore file in the root
|
||||||
|
// of the server files directory, and use that to generate the backup.
|
||||||
|
if len(b.IgnoredFiles) == 0 {
|
||||||
|
f, err := os.Open(path.Join(s.Filesystem.Path(), ".pteroignore"))
|
||||||
|
if err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
zap.S().Warnw("failed to open .pteroignore file in server directory", zap.String("server", s.Uuid), zap.Error(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
for scanner.Scan() {
|
||||||
|
// Only include non-empty lines, for the sake of clarity...
|
||||||
|
if t := scanner.Text(); t != "" {
|
||||||
|
b.IgnoredFiles = append(b.IgnoredFiles, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
zap.S().Warnw("failed to scan .pteroignore file for lines", zap.String("server", s.Uuid), zap.Error(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the included files based on the root path and the ignored files provided.
|
||||||
|
inc, err := s.Filesystem.GetIncludedFiles(s.Filesystem.Path(), b.IgnoredFiles)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.Backup(inc, s.Filesystem.Path()); err != nil {
|
||||||
|
if notifyError := s.notifyPanelOfBackup(b.Identifier(), &backup.ArchiveDetails{}, false); notifyError != nil {
|
||||||
|
zap.S().Warnw("failed to notify panel of failed backup state", zap.String("backup", b.Uuid), zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to notify the panel about the status of this backup. If for some reason this request
|
||||||
|
// fails, delete the archive from the daemon and return that error up the chain to the caller.
|
||||||
|
ad := b.Details()
|
||||||
|
if notifyError := s.notifyPanelOfBackup(b.Identifier(), ad, true); notifyError != nil {
|
||||||
|
b.Remove()
|
||||||
|
|
||||||
|
return notifyError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit an event over the socket so we can update the backup in realtime on
|
||||||
|
// the frontend for the server.
|
||||||
|
s.Events().PublishJson(BackupCompletedEvent+":"+b.Uuid, map[string]interface{}{
|
||||||
|
"uuid": b.Uuid,
|
||||||
|
"sha256_hash": ad.Checksum,
|
||||||
|
"file_size": ad.Size,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
111
server/backup/archiver.go
Normal file
111
server/backup/archiver.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package backup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"context"
|
||||||
|
gzip "github.com/klauspost/pgzip"
|
||||||
|
"github.com/remeh/sizedwaitgroup"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Archive struct {
|
||||||
|
sync.Mutex
|
||||||
|
|
||||||
|
TrimPrefix string
|
||||||
|
Files *IncludedFiles
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates an archive at dest with all of the files definied in the included files struct.
|
||||||
|
func (a *Archive) Create(dest string, ctx context.Context) error {
|
||||||
|
f, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
gzw := gzip.NewWriter(f)
|
||||||
|
defer gzw.Close()
|
||||||
|
|
||||||
|
tw := tar.NewWriter(gzw)
|
||||||
|
defer tw.Close()
|
||||||
|
|
||||||
|
wg := sizedwaitgroup.New(10)
|
||||||
|
g, ctx := errgroup.WithContext(ctx)
|
||||||
|
// Iterate over all of the files to be included and put them into the archive. This is
|
||||||
|
// done as a concurrent goroutine to speed things along. If an error is encountered at
|
||||||
|
// any step, the entire process is aborted.
|
||||||
|
for p, s := range a.Files.All() {
|
||||||
|
if (*s).IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pa := p
|
||||||
|
st := s
|
||||||
|
|
||||||
|
g.Go(func() error {
|
||||||
|
wg.Add()
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
return a.addToArchive(pa, st, tw)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block until the entire routine is completed.
|
||||||
|
if err := g.Wait(); err != nil {
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
// Attempt to remove the archive if there is an error, report that error to
|
||||||
|
// the logger if it fails.
|
||||||
|
if rerr := os.Remove(dest); rerr != nil && !os.IsNotExist(rerr) {
|
||||||
|
zap.S().Warnw("failed to delete corrupted backup archive", zap.String("location", dest))
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds a single file to the existing tar archive writer.
|
||||||
|
func (a *Archive) addToArchive(p string, s *os.FileInfo, w *tar.Writer) error {
|
||||||
|
f, err := os.Open(p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
st := *s
|
||||||
|
header := &tar.Header{
|
||||||
|
// Trim the long server path from the name of the file so that the resulting
|
||||||
|
// archive is exactly how the user would see it in the panel file manager.
|
||||||
|
Name: strings.TrimPrefix(p, a.TrimPrefix),
|
||||||
|
Size: st.Size(),
|
||||||
|
Mode: int64(st.Mode()),
|
||||||
|
ModTime: st.ModTime(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// These actions must occur sequentially, even if this function is called multiple
|
||||||
|
// in parallel. You'll get some nasty panic's otherwise.
|
||||||
|
a.Lock()
|
||||||
|
defer a.Unlock()
|
||||||
|
|
||||||
|
if err = w.WriteHeader(header); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.Copy(w, f); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
42
server/backup/backup.go
Normal file
42
server/backup/backup.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package backup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pterodactyl/wings/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Backup interface {
|
||||||
|
// Returns the UUID of this backup as tracked by the panel instance.
|
||||||
|
Identifier() string
|
||||||
|
|
||||||
|
// Generates a backup in whatever the configured source for the specific
|
||||||
|
// implementation is.
|
||||||
|
Backup(*IncludedFiles, string) error
|
||||||
|
|
||||||
|
// Returns a SHA256 checksum for the generated backup.
|
||||||
|
Checksum() ([]byte, error)
|
||||||
|
|
||||||
|
// Returns the size of the generated backup.
|
||||||
|
Size() (int64, error)
|
||||||
|
|
||||||
|
// Returns the path to the backup on the machine. This is not always the final
|
||||||
|
// storage location of the backup, simply the location we're using to store
|
||||||
|
// it until it is moved to the final spot.
|
||||||
|
Path() string
|
||||||
|
|
||||||
|
// Returns details about the archive.
|
||||||
|
Details() *ArchiveDetails
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArchiveDetails struct {
|
||||||
|
Checksum string `json:"checksum"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a request object.
|
||||||
|
func (ad *ArchiveDetails) ToRequest(successful bool) api.BackupRequest {
|
||||||
|
return api.BackupRequest{
|
||||||
|
Checksum: ad.Checksum,
|
||||||
|
Size: ad.Size,
|
||||||
|
Successful: successful,
|
||||||
|
}
|
||||||
|
}
|
||||||
154
server/backup/backup_local.go
Normal file
154
server/backup/backup_local.go
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
package backup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LocalBackup struct {
|
||||||
|
// The UUID of this backup object. This must line up with a backup from
|
||||||
|
// the panel instance.
|
||||||
|
Uuid string `json:"uuid"`
|
||||||
|
|
||||||
|
// An array of files to ignore when generating this backup. This should be
|
||||||
|
// compatible with a standard .gitignore structure.
|
||||||
|
IgnoredFiles []string `json:"ignored_files"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Backup = (*LocalBackup)(nil)
|
||||||
|
|
||||||
|
// Locates 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{
|
||||||
|
Uuid: uuid,
|
||||||
|
IgnoredFiles: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
st, err := os.Stat(b.Path())
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if st.IsDir() {
|
||||||
|
return nil, nil, errors.New("invalid archive found; is directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, st, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LocalBackup) Identifier() string {
|
||||||
|
return b.Uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the path for this specific backup.
|
||||||
|
func (b *LocalBackup) Path() string {
|
||||||
|
return path.Join(config.Get().System.BackupDirectory, b.Uuid+".tar.gz")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the SHA256 checksum of a backup.
|
||||||
|
func (b *LocalBackup) Checksum() ([]byte, error) {
|
||||||
|
h := sha256.New()
|
||||||
|
|
||||||
|
f, err := os.Open(b.Path())
|
||||||
|
if err != nil {
|
||||||
|
return []byte{}, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if _, err := io.Copy(h, f); err != nil {
|
||||||
|
return []byte{}, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Sum(nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removes a backup from the system.
|
||||||
|
func (b *LocalBackup) Remove() error {
|
||||||
|
return os.Remove(b.Path())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates a backup of the selected files and pushes it to the defined location
|
||||||
|
// for this instance.
|
||||||
|
func (b *LocalBackup) Backup(included *IncludedFiles, prefix string) error {
|
||||||
|
a := &Archive{
|
||||||
|
TrimPrefix: prefix,
|
||||||
|
Files: included,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := a.Create(b.Path(), context.Background())
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the size of the generated backup.
|
||||||
|
func (b *LocalBackup) Size() (int64, error) {
|
||||||
|
st, err := os.Stat(b.Path())
|
||||||
|
if err != nil {
|
||||||
|
return 0, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return st.Size(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns details of the archive by utilizing two go-routines to get the checksum and
|
||||||
|
// the size of the archive.
|
||||||
|
func (b *LocalBackup) Details() *ArchiveDetails {
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(2)
|
||||||
|
|
||||||
|
var checksum string
|
||||||
|
// Calculate the checksum for the file.
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
resp, err := b.Checksum()
|
||||||
|
if err != nil {
|
||||||
|
zap.S().Errorw("failed to calculate checksum for backup", zap.String("backup", b.Uuid), zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
checksum = hex.EncodeToString(resp)
|
||||||
|
}()
|
||||||
|
|
||||||
|
var sz int64
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
st, err := os.Stat(b.Path())
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sz = st.Size()
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
return &ArchiveDetails{
|
||||||
|
Checksum: checksum,
|
||||||
|
Size: sz,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensures that the local backup destination for files exists.
|
||||||
|
func (b *LocalBackup) ensureLocalBackupLocation() error {
|
||||||
|
d := config.Get().System.BackupDirectory
|
||||||
|
|
||||||
|
if _, err := os.Stat(d); err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.MkdirAll(d, 0700)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
37
server/backup/backup_s3.go
Normal file
37
server/backup/backup_s3.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package backup
|
||||||
|
|
||||||
|
type S3Backup struct {
|
||||||
|
// The UUID of this backup object. This must line up with a backup from
|
||||||
|
// the panel instance.
|
||||||
|
Uuid string
|
||||||
|
|
||||||
|
// An array of files to ignore when generating this backup. This should be
|
||||||
|
// compatible with a standard .gitignore structure.
|
||||||
|
IgnoredFiles []string
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Backup = (*S3Backup)(nil)
|
||||||
|
|
||||||
|
func (s *S3Backup) Identifier() string {
|
||||||
|
return s.Uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *S3Backup) Backup(included *IncludedFiles, prefix string) error {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *S3Backup) Checksum() ([]byte, error) {
|
||||||
|
return []byte(""), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *S3Backup) Size() (int64, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *S3Backup) Path() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *S3Backup) Details() *ArchiveDetails {
|
||||||
|
return &ArchiveDetails{}
|
||||||
|
}
|
||||||
31
server/backup/included.go
Normal file
31
server/backup/included.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package backup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IncludedFiles struct {
|
||||||
|
sync.RWMutex
|
||||||
|
files map[string]*os.FileInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pushes an additional file or folder onto the struct.
|
||||||
|
func (i *IncludedFiles) Push(info *os.FileInfo, p string) {
|
||||||
|
i.Lock()
|
||||||
|
defer i.Unlock()
|
||||||
|
|
||||||
|
if i.files == nil {
|
||||||
|
i.files = make(map[string]*os.FileInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
i.files[p] = info
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns all of the files that were marked as being included.
|
||||||
|
func (i *IncludedFiles) All() map[string]*os.FileInfo {
|
||||||
|
i.RLock()
|
||||||
|
defer i.RUnlock()
|
||||||
|
|
||||||
|
return i.files
|
||||||
|
}
|
||||||
@@ -4,34 +4,35 @@ import "sync"
|
|||||||
|
|
||||||
type Collection struct {
|
type Collection struct {
|
||||||
items []*Server
|
items []*Server
|
||||||
mutex *sync.Mutex
|
sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new collection from a slice of servers.
|
// Create a new collection from a slice of servers.
|
||||||
func NewCollection(servers []*Server) *Collection {
|
func NewCollection(servers []*Server) *Collection {
|
||||||
return &Collection{
|
return &Collection{
|
||||||
items: servers,
|
items: servers,
|
||||||
mutex: &sync.Mutex{},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return all of the items in the collection.
|
// Return all of the items in the collection.
|
||||||
func (c *Collection) All() []*Server {
|
func (c *Collection) All() []*Server {
|
||||||
|
c.RLock()
|
||||||
|
defer c.RUnlock()
|
||||||
|
|
||||||
return c.items
|
return c.items
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds an item to the collection store.
|
// Adds an item to the collection store.
|
||||||
func (c *Collection) Add(s *Server) {
|
func (c *Collection) Add(s *Server) {
|
||||||
c.mutex.Lock()
|
c.Lock()
|
||||||
defer c.mutex.Unlock()
|
|
||||||
|
|
||||||
c.items = append(c.items, s)
|
c.items = append(c.items, s)
|
||||||
|
c.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns only those items matching the filter criteria.
|
// Returns only those items matching the filter criteria.
|
||||||
func (c *Collection) Filter(filter func(*Server) bool) []*Server {
|
func (c *Collection) Filter(filter func(*Server) bool) []*Server {
|
||||||
c.mutex.Lock()
|
c.RLock()
|
||||||
defer c.mutex.Unlock()
|
defer c.RUnlock()
|
||||||
|
|
||||||
r := make([]*Server, 0)
|
r := make([]*Server, 0)
|
||||||
for _, v := range c.items {
|
for _, v := range c.items {
|
||||||
@@ -46,6 +47,9 @@ func (c *Collection) Filter(filter func(*Server) bool) []*Server {
|
|||||||
// Returns a single element from the collection matching the filter. If nothing is
|
// Returns a single element from the collection matching the filter. If nothing is
|
||||||
// found a nil result is returned.
|
// found a nil result is returned.
|
||||||
func (c *Collection) Find(filter func(*Server) bool) *Server {
|
func (c *Collection) Find(filter func(*Server) bool) *Server {
|
||||||
|
c.RLock()
|
||||||
|
defer c.RUnlock()
|
||||||
|
|
||||||
for _, v := range c.items {
|
for _, v := range c.items {
|
||||||
if filter(v) {
|
if filter(v) {
|
||||||
return v
|
return v
|
||||||
@@ -57,8 +61,8 @@ func (c *Collection) Find(filter func(*Server) bool) *Server {
|
|||||||
|
|
||||||
// Removes all items from the collection that match the filter function.
|
// Removes all items from the collection that match the filter function.
|
||||||
func (c *Collection) Remove(filter func(*Server) bool) {
|
func (c *Collection) Remove(filter func(*Server) bool) {
|
||||||
c.mutex.Lock()
|
c.Lock()
|
||||||
defer c.mutex.Unlock()
|
defer c.Unlock()
|
||||||
|
|
||||||
r := make([]*Server, 0)
|
r := make([]*Server, 0)
|
||||||
for _, v := range c.items {
|
for _, v := range c.items {
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Writes the server configuration to the disk. The saved configuration will be returned
|
|
||||||
// back to the calling function to use if desired.
|
|
||||||
func (s *Server) WriteConfigurationToDisk() ([]byte, error) {
|
|
||||||
s.mutex.Lock()
|
|
||||||
defer s.mutex.Unlock()
|
|
||||||
|
|
||||||
f, err := os.Create("data/servers/" + s.Uuid + ".yml")
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
b, err := yaml.Marshal(&s)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := f.Write(b); err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
@@ -33,7 +33,7 @@ func (s *Server) handleServerCrash() error {
|
|||||||
// No point in doing anything here if the server isn't currently offline, there
|
// No point in doing anything here if the server isn't currently offline, there
|
||||||
// is no reason to do a crash detection event. If the server crash detection is
|
// is no reason to do a crash detection event. If the server crash detection is
|
||||||
// disabled we want to skip anything after this as well.
|
// disabled we want to skip anything after this as well.
|
||||||
if s.State != ProcessOfflineState || !s.CrashDetection.Enabled {
|
if s.GetState() != ProcessOfflineState || !s.CrashDetection.Enabled {
|
||||||
if !s.CrashDetection.Enabled {
|
if !s.CrashDetection.Enabled {
|
||||||
zap.S().Debugw("server triggered crash detection but handler is disabled for server process", zap.String("server", s.Uuid))
|
zap.S().Debugw("server triggered crash detection but handler is disabled for server process", zap.String("server", s.Uuid))
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ type Environment interface {
|
|||||||
// not be returned.
|
// not be returned.
|
||||||
Stop() error
|
Stop() error
|
||||||
|
|
||||||
|
// Waits for a server instance to stop gracefully. If the server is still detected
|
||||||
|
// as running after seconds, an error will be returned, or the server will be terminated
|
||||||
|
// depending on the value of the second argument.
|
||||||
|
WaitForStop(seconds int, terminate bool) error
|
||||||
|
|
||||||
// Determines if the server instance exists. For example, in a docker environment
|
// Determines if the server instance exists. For example, in a docker environment
|
||||||
// this should confirm that the container is created and in a bootable state. In
|
// this should confirm that the container is created and in a bootable state. In
|
||||||
// a basic CLI environment this can probably just return true right away.
|
// a basic CLI environment this can probably just return true right away.
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"io"
|
"io"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -146,6 +147,10 @@ func (d *DockerEnvironment) OnBeforeStart() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !d.Server.Filesystem.HasSpaceAvailable() {
|
||||||
|
return errors.New("cannot start server, not enough disk space available")
|
||||||
|
}
|
||||||
|
|
||||||
// Always destroy and re-create the server container to ensure that synced data from
|
// Always destroy and re-create the server container to ensure that synced data from
|
||||||
// the Panel is used.
|
// the Panel is used.
|
||||||
if err := d.Client.ContainerRemove(context.Background(), d.Server.Uuid, types.ContainerRemoveOptions{RemoveVolumes: true}); err != nil {
|
if err := d.Client.ContainerRemove(context.Background(), d.Server.Uuid, types.ContainerRemoveOptions{RemoveVolumes: true}); err != nil {
|
||||||
@@ -267,6 +272,44 @@ func (d *DockerEnvironment) Stop() error {
|
|||||||
return d.Client.ContainerStop(context.Background(), d.Server.Uuid, &t)
|
return d.Client.ContainerStop(context.Background(), d.Server.Uuid, &t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attempts to gracefully stop a server using the defined stop command. If the server
|
||||||
|
// does not stop after seconds have passed, an error will be returned, or the instance
|
||||||
|
// will be terminated forcefully depending on the value of the second argument.
|
||||||
|
func (d *DockerEnvironment) WaitForStop(seconds int, terminate bool) error {
|
||||||
|
if d.Server.GetState() == ProcessOfflineState {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.Stop(); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(seconds)*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Block the return of this function until the container as been marked as no
|
||||||
|
// longer running. If this wait does not end by the time seconds have passed,
|
||||||
|
// attempt to terminate the container, or return an error.
|
||||||
|
ok, errChan := d.Client.ContainerWait(ctx, d.Server.Uuid, container.WaitConditionNotRunning)
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||||
|
if terminate {
|
||||||
|
return d.Terminate(os.Kill)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.WithStack(ctxErr)
|
||||||
|
}
|
||||||
|
case err := <-errChan:
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
case <-ok:
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Forcefully terminates the container using the signal passed through.
|
// Forcefully terminates the container using the signal passed through.
|
||||||
func (d *DockerEnvironment) Terminate(signal os.Signal) error {
|
func (d *DockerEnvironment) Terminate(signal os.Signal) error {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -406,7 +449,7 @@ func (d *DockerEnvironment) FollowConsoleOutput() error {
|
|||||||
// information, instead just sit there with an async process that lets Docker stream all of this data
|
// information, instead just sit there with an async process that lets Docker stream all of this data
|
||||||
// to us automatically.
|
// to us automatically.
|
||||||
func (d *DockerEnvironment) EnableResourcePolling() error {
|
func (d *DockerEnvironment) EnableResourcePolling() error {
|
||||||
if d.Server.State == ProcessOfflineState {
|
if d.Server.GetState() == ProcessOfflineState {
|
||||||
return errors.New("cannot enable resource polling on a server that is not running")
|
return errors.New("cannot enable resource polling on a server that is not running")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,7 +477,7 @@ func (d *DockerEnvironment) EnableResourcePolling() error {
|
|||||||
|
|
||||||
// Disable collection if the server is in an offline state and this process is
|
// Disable collection if the server is in an offline state and this process is
|
||||||
// still running.
|
// still running.
|
||||||
if s.State == ProcessOfflineState {
|
if s.GetState() == ProcessOfflineState {
|
||||||
d.DisableResourcePolling()
|
d.DisableResourcePolling()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -576,8 +619,7 @@ func (d *DockerEnvironment) Create() error {
|
|||||||
// from the Panel.
|
// from the Panel.
|
||||||
Resources: d.getResourcesForServer(),
|
Resources: d.getResourcesForServer(),
|
||||||
|
|
||||||
// @todo make this configurable again
|
DNS: config.Get().Docker.Network.Dns,
|
||||||
DNS: []string{"1.1.1.1", "8.8.8.8"},
|
|
||||||
|
|
||||||
// Configure logging for the container to make it easier on the Daemon to grab
|
// Configure logging for the container to make it easier on the Daemon to grab
|
||||||
// the server output. Ensure that we don't use too much space on the host machine
|
// the server output. Ensure that we don't use too much space on the host machine
|
||||||
@@ -618,30 +660,6 @@ func (d *DockerEnvironment) Create() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Given a host configuration mount, also mount the timezone data into it.
|
|
||||||
func mountTimezoneData(c *container.HostConfig) error {
|
|
||||||
p := config.Get().System.TimezonePath
|
|
||||||
|
|
||||||
// Check for the timezone file, if it exists use it assuming it isn't also a directory,
|
|
||||||
// otherwise bubble the error back up the stack.
|
|
||||||
if s, err := os.Stat(p); err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
if s.IsDir() {
|
|
||||||
return errors.New("attempting to mount directory as timezone file")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Mounts = append(c.Mounts, mount.Mount{
|
|
||||||
Target: p,
|
|
||||||
Source: p,
|
|
||||||
Type: mount.TypeBind,
|
|
||||||
ReadOnly: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sends the specified command to the stdin of the running container instance. There is no
|
// Sends the specified command to the stdin of the running container instance. There is no
|
||||||
// confirmation that this data is sent successfully, only that it gets pushed into the stdin.
|
// confirmation that this data is sent successfully, only that it gets pushed into the stdin.
|
||||||
func (d *DockerEnvironment) SendCommand(c string) error {
|
func (d *DockerEnvironment) SendCommand(c string) error {
|
||||||
@@ -807,9 +825,18 @@ func (d *DockerEnvironment) exposedPorts() nat.PortSet {
|
|||||||
// Formats the resources available to a server instance in such as way that Docker will
|
// Formats the resources available to a server instance in such as way that Docker will
|
||||||
// generate a matching environment in the container.
|
// generate a matching environment in the container.
|
||||||
func (d *DockerEnvironment) getResourcesForServer() container.Resources {
|
func (d *DockerEnvironment) getResourcesForServer() container.Resources {
|
||||||
|
overhead := 1.05
|
||||||
|
// Set the hard limit for memory usage to be 5% more than the amount of memory assigned to
|
||||||
|
// the server. If the memory limit for the server is < 4G, use 10%, if less than 2G use
|
||||||
|
// 15%. This avoids unexpected crashes from processes like Java which run over the limit.
|
||||||
|
if d.Server.Build.MemoryLimit <= 2048 {
|
||||||
|
overhead = 1.15
|
||||||
|
} else if d.Server.Build.MemoryLimit <= 4096 {
|
||||||
|
overhead = 1.10;
|
||||||
|
}
|
||||||
|
|
||||||
return container.Resources{
|
return container.Resources{
|
||||||
// @todo memory limit should be slightly higher than the reservation
|
Memory: int64(math.Round(float64(d.Server.Build.MemoryLimit) * 1000000.0 * overhead)),
|
||||||
Memory: d.Server.Build.MemoryLimit * 1000000,
|
|
||||||
MemoryReservation: d.Server.Build.MemoryLimit * 1000000,
|
MemoryReservation: d.Server.Build.MemoryLimit * 1000000,
|
||||||
MemorySwap: d.Server.Build.ConvertedSwap(),
|
MemorySwap: d.Server.Build.ConvertedSwap(),
|
||||||
CPUQuota: d.Server.Build.ConvertedCpuLimit(),
|
CPUQuota: d.Server.Build.ConvertedCpuLimit(),
|
||||||
@@ -817,5 +844,6 @@ func (d *DockerEnvironment) getResourcesForServer() container.Resources {
|
|||||||
CPUShares: 1024,
|
CPUShares: 1024,
|
||||||
BlkioWeight: d.Server.Build.IoWeight,
|
BlkioWeight: d.Server.Build.IoWeight,
|
||||||
OomKillDisable: &d.Server.Container.OomDisabled,
|
OomKillDisable: &d.Server.Container.OomDisabled,
|
||||||
|
CpusetCpus: d.Server.Build.Threads,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Defines all of the possible output events for a server.
|
// Defines all of the possible output events for a server.
|
||||||
// noinspection GoNameStartsWithPackageName
|
// noinspection GoNameStartsWithPackageName
|
||||||
const (
|
const (
|
||||||
DaemonMessageEvent = "daemon message"
|
DaemonMessageEvent = "daemon message"
|
||||||
InstallOutputEvent = "install output"
|
InstallOutputEvent = "install output"
|
||||||
ConsoleOutputEvent = "console output"
|
ConsoleOutputEvent = "console output"
|
||||||
StatusEvent = "status"
|
StatusEvent = "status"
|
||||||
StatsEvent = "stats"
|
StatsEvent = "stats"
|
||||||
|
BackupCompletedEvent = "backup completed"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Event struct {
|
type Event struct {
|
||||||
@@ -40,7 +43,21 @@ func (e *EventBus) Publish(topic string, data string) {
|
|||||||
e.mu.Lock()
|
e.mu.Lock()
|
||||||
defer e.mu.Unlock()
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
if ch, ok := e.subscribers[topic]; ok {
|
t := topic
|
||||||
|
// Some of our topics for the socket support passing a more specific namespace,
|
||||||
|
// such as "backup completed:1234" to indicate which specific backup was completed.
|
||||||
|
//
|
||||||
|
// In these cases, we still need to the send the event using the standard listener
|
||||||
|
// name of "backup completed".
|
||||||
|
if strings.Contains(topic, ":") {
|
||||||
|
parts := strings.SplitN(topic, ":", 2)
|
||||||
|
|
||||||
|
if len(parts) == 2 {
|
||||||
|
t = parts[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ch, ok := e.subscribers[t]; ok {
|
||||||
go func(data Event, cs []chan Event) {
|
go func(data Event, cs []chan Event) {
|
||||||
for _, channel := range cs {
|
for _, channel := range cs {
|
||||||
channel <- data
|
channel <- data
|
||||||
@@ -49,6 +66,17 @@ func (e *EventBus) Publish(topic string, data string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *EventBus) PublishJson(topic string, data interface{}) error {
|
||||||
|
b, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Publish(topic, string(b))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Subscribe to an emitter topic using a channel.
|
// Subscribe to an emitter topic using a channel.
|
||||||
func (e *EventBus) Subscribe(topic string, ch chan Event) {
|
func (e *EventBus) Subscribe(topic string, ch chan Event) {
|
||||||
e.mu.Lock()
|
e.mu.Lock()
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ package server
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gabriel-vasile/mimetype"
|
"github.com/gabriel-vasile/mimetype"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"github.com/pterodactyl/wings/server/backup"
|
||||||
|
ignore "github.com/sabhiram/go-gitignore"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@@ -18,6 +21,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,7 +70,7 @@ func (fs *Filesystem) SafePath(p string) (string, error) {
|
|||||||
// Range over all of the path parts and form directory pathings from the end
|
// Range over all of the path parts and form directory pathings from the end
|
||||||
// moving up until we have a valid resolution or we run out of paths to try.
|
// moving up until we have a valid resolution or we run out of paths to try.
|
||||||
for k := range parts {
|
for k := range parts {
|
||||||
try = strings.Join(parts[:(len(parts) - k)], "/")
|
try = strings.Join(parts[:(len(parts)-k)], "/")
|
||||||
|
|
||||||
if !strings.HasPrefix(try, fs.Path()) {
|
if !strings.HasPrefix(try, fs.Path()) {
|
||||||
break
|
break
|
||||||
@@ -87,7 +91,7 @@ func (fs *Filesystem) SafePath(p string) (string, error) {
|
|||||||
return "", InvalidPathResolution
|
return "", InvalidPathResolution
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the nonExistentPathResoltion variable is not empty then the initial path requested
|
// If the nonExistentPathResolution variable is not empty then the initial path requested
|
||||||
// did not exist and we looped through the pathway until we found a match. At this point
|
// did not exist and we looped through the pathway until we found a match. At this point
|
||||||
// we've confirmed the first matched pathway exists in the root server directory, so we
|
// we've confirmed the first matched pathway exists in the root server directory, so we
|
||||||
// can go ahead and just return the path that was requested initially.
|
// can go ahead and just return the path that was requested initially.
|
||||||
@@ -117,23 +121,27 @@ func (fs *Filesystem) HasSpaceAvailable() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
var size int64
|
// If we have a match in the cache, use that value in the return. No need to perform an expensive
|
||||||
|
// disk operation, even if this is an empty value.
|
||||||
if x, exists := fs.Server.Cache.Get("disk_used"); exists {
|
if x, exists := fs.Server.Cache.Get("disk_used"); exists {
|
||||||
size = x.(int64)
|
fs.Server.Resources.Disk = x.(int64)
|
||||||
|
return (x.(int64) / 1000.0 / 1000.0) <= space
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is no size its either because there is no data (in which case running this function
|
// If there is no size its either because there is no data (in which case running this function
|
||||||
// will have effectively no impact), or there is nothing in the cache, in which case we need to
|
// will have effectively no impact), or there is nothing in the cache, in which case we need to
|
||||||
// grab the size of their data directory. This is a taxing operation, so we want to store it in
|
// grab the size of their data directory. This is a taxing operation, so we want to store it in
|
||||||
// the cache once we've gotten it.
|
// the cache once we've gotten it.
|
||||||
if size == 0 {
|
size, err := fs.DirectorySize("/")
|
||||||
if size, err := fs.DirectorySize("/"); err != nil {
|
if err != nil {
|
||||||
zap.S().Warnw("failed to determine directory size", zap.String("server", fs.Server.Uuid), zap.Error(err))
|
zap.S().Warnw("failed to determine directory size", zap.String("server", fs.Server.Uuid), zap.Error(err))
|
||||||
} else {
|
|
||||||
fs.Server.Cache.Set("disk_used", size, time.Second * 60)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always cache the size, even if there is an error. We want to always return that value
|
||||||
|
// so that we don't cause an endless loop of determining the disk size if there is a temporary
|
||||||
|
// error encountered.
|
||||||
|
fs.Server.Cache.Set("disk_used", size, time.Second*60)
|
||||||
|
|
||||||
// Determine if their folder size, in bytes, is smaller than the amount of space they've
|
// Determine if their folder size, in bytes, is smaller than the amount of space they've
|
||||||
// been allocated.
|
// been allocated.
|
||||||
fs.Server.Resources.Disk = size
|
fs.Server.Resources.Disk = size
|
||||||
@@ -145,41 +153,20 @@ func (fs *Filesystem) HasSpaceAvailable() bool {
|
|||||||
// through all of the folders. Returns the size in bytes. This can be a fairly taxing operation
|
// through all of the folders. Returns the size in bytes. This can be a fairly taxing operation
|
||||||
// on locations with tons of files, so it is recommended that you cache the output.
|
// on locations with tons of files, so it is recommended that you cache the output.
|
||||||
func (fs *Filesystem) DirectorySize(dir string) (int64, error) {
|
func (fs *Filesystem) DirectorySize(dir string) (int64, error) {
|
||||||
|
w := fs.NewWalker()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
var size int64
|
var size int64
|
||||||
var wg sync.WaitGroup
|
err := w.Walk(dir, ctx, func(f os.FileInfo, _ string) bool {
|
||||||
|
// Only increment the size when we're dealing with a file specifically, otherwise
|
||||||
cleaned, err := fs.SafePath(dir)
|
// just continue digging deeper until there are no more directories to iterate over.
|
||||||
if err != nil {
|
if !f.IsDir() {
|
||||||
return 0, err
|
atomic.AddInt64(&size, f.Size())
|
||||||
}
|
|
||||||
|
|
||||||
files, err := ioutil.ReadDir(cleaned)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterate over all of the files and directories. If it is a file, immediately add its size
|
|
||||||
// to the total size being returned. If we're dealing with a directory, call this function
|
|
||||||
// on a seperate thread until we have gotten the size of everything nested within the given
|
|
||||||
// directory.
|
|
||||||
for _, f := range files {
|
|
||||||
if f.IsDir() {
|
|
||||||
wg.Add(1)
|
|
||||||
|
|
||||||
go func(p string) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
s, _ := fs.DirectorySize(p)
|
|
||||||
size += s
|
|
||||||
}(filepath.Join(cleaned, f.Name()))
|
|
||||||
} else {
|
|
||||||
size += f.Size()
|
|
||||||
}
|
}
|
||||||
}
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
wg.Wait()
|
return size, err
|
||||||
|
|
||||||
return size, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reads a file on the system and returns it as a byte representation in a file
|
// Reads a file on the system and returns it as a byte representation in a file
|
||||||
@@ -299,14 +286,18 @@ func (fs *Filesystem) Stat(p string) (*Stat, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err := os.Stat(cleaned)
|
return fs.unsafeStat(cleaned)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *Filesystem) unsafeStat(p string) (*Stat, error) {
|
||||||
|
s, err := os.Stat(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var m = "inode/directory"
|
var m = "inode/directory"
|
||||||
if !s.IsDir() {
|
if !s.IsDir() {
|
||||||
m, _, err = mimetype.DetectFile(cleaned)
|
m, _, err = mimetype.DetectFile(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -373,6 +364,9 @@ func (fs *Filesystem) chownDirectory(path string) error {
|
|||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chown the directory itself.
|
||||||
|
os.Chown(cleaned, config.Get().System.User.Uid, config.Get().System.User.Gid)
|
||||||
|
|
||||||
files, err := ioutil.ReadDir(cleaned)
|
files, err := ioutil.ReadDir(cleaned)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
@@ -387,6 +381,7 @@ func (fs *Filesystem) chownDirectory(path string) error {
|
|||||||
fs.chownDirectory(p)
|
fs.chownDirectory(p)
|
||||||
}(filepath.Join(cleaned, f.Name()))
|
}(filepath.Join(cleaned, f.Name()))
|
||||||
} else {
|
} else {
|
||||||
|
// Chown the file.
|
||||||
os.Chown(filepath.Join(cleaned, f.Name()), fs.Configuration.User.Uid, fs.Configuration.User.Gid)
|
os.Chown(filepath.Join(cleaned, f.Name()), fs.Configuration.User.Uid, fs.Configuration.User.Gid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -511,14 +506,14 @@ func (fs *Filesystem) ListDirectory(p string) ([]*Stat, error) {
|
|||||||
// You must initialize the output of this directory as a non-nil value otherwise
|
// You must initialize the output of this directory as a non-nil value otherwise
|
||||||
// when it is marshaled into a JSON object you'll just get 'null' back, which will
|
// when it is marshaled into a JSON object you'll just get 'null' back, which will
|
||||||
// break the panel badly.
|
// break the panel badly.
|
||||||
out := make([]*Stat, 0)
|
out := make([]*Stat, len(files))
|
||||||
|
|
||||||
// Iterate over all of the files and directories returned and perform an async process
|
// Iterate over all of the files and directories returned and perform an async process
|
||||||
// to get the mime-type for them all.
|
// to get the mime-type for them all.
|
||||||
for _, file := range files {
|
for i, file := range files {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
|
|
||||||
go func(f os.FileInfo) {
|
go func(idx int, f os.FileInfo) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
var m = "inode/directory"
|
var m = "inode/directory"
|
||||||
@@ -526,11 +521,11 @@ func (fs *Filesystem) ListDirectory(p string) ([]*Stat, error) {
|
|||||||
m, _, _ = mimetype.DetectFile(filepath.Join(cleaned, f.Name()))
|
m, _, _ = mimetype.DetectFile(filepath.Join(cleaned, f.Name()))
|
||||||
}
|
}
|
||||||
|
|
||||||
out = append(out, &Stat{
|
out[idx] = &Stat{
|
||||||
Info: f,
|
Info: f,
|
||||||
Mimetype: m,
|
Mimetype: m,
|
||||||
})
|
}
|
||||||
}(file)
|
}(i, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@@ -568,3 +563,43 @@ func (fs *Filesystem) EnsureDataDirectory() error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Given a directory, iterate through all of the files and folders within it and determine
|
||||||
|
// if they should be included in the output based on an array of ignored matches. This uses
|
||||||
|
// standard .gitignore formatting to make that determination.
|
||||||
|
//
|
||||||
|
// If no ignored files are passed through you'll get the entire directory listing.
|
||||||
|
func (fs *Filesystem) GetIncludedFiles(dir string, ignored []string) (*backup.IncludedFiles, error) {
|
||||||
|
cleaned, err := fs.SafePath(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
w := fs.NewWalker()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
i, err := ignore.CompileIgnoreLines(ignored...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk through all of the files and directories on a server. This callback only returns
|
||||||
|
// files found, and will keep walking deeper and deeper into directories.
|
||||||
|
inc := new(backup.IncludedFiles)
|
||||||
|
if err := w.Walk(cleaned, ctx, func(f os.FileInfo, p string) bool {
|
||||||
|
// Avoid unnecessary parsing if there are no ignored files, nothing will match anyways
|
||||||
|
// so no reason to call the function.
|
||||||
|
if len(ignored) == 0 || !i.MatchesPath(strings.TrimPrefix(p, fs.Path() + "/")) {
|
||||||
|
inc.Push(&f, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can't just abort if the path is technically ignored. It is possible there is a nested
|
||||||
|
// file or folder that should not be excluded, so in this case we need to just keep going
|
||||||
|
// until we get to a final state.
|
||||||
|
return true
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return inc, nil
|
||||||
|
}
|
||||||
|
|||||||
70
server/filesystem_walker.go
Normal file
70
server/filesystem_walker.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileWalker struct {
|
||||||
|
*Filesystem
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a new walker instance.
|
||||||
|
func (fs *Filesystem) NewWalker() *FileWalker {
|
||||||
|
return &FileWalker{fs}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over all of the files and directories within a given directory. When a file is
|
||||||
|
// found the callback will be called with the file information. If a directory is encountered
|
||||||
|
// it will be recursively passed back through to this function.
|
||||||
|
func (fw *FileWalker) Walk(dir string, ctx context.Context, callback func (os.FileInfo, string) bool) error {
|
||||||
|
cleaned, err := fw.SafePath(dir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all of the files from this directory.
|
||||||
|
files, err := ioutil.ReadDir(cleaned)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an error group that we can use to run processes in parallel while retaining
|
||||||
|
// the ability to cancel the entire process immediately should any of it fail.
|
||||||
|
g, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
|
for _, f := range files {
|
||||||
|
if f.IsDir() {
|
||||||
|
fi := f
|
||||||
|
p := filepath.Join(cleaned, f.Name())
|
||||||
|
// Recursively call this function to continue digging through the directory tree within
|
||||||
|
// a seperate goroutine. If the context is canceled abort this process.
|
||||||
|
g.Go(func() error {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
// If the callback returns true, go ahead and keep walking deeper. This allows
|
||||||
|
// us to programatically continue deeper into directories, or stop digging
|
||||||
|
// if that pathway knows it needs nothing else.
|
||||||
|
if callback(fi, p) {
|
||||||
|
return fw.Walk(p, ctx, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// If this isn't a directory, go ahead and pass the file information into the
|
||||||
|
// callback. We don't care about the response since we won't be stepping into
|
||||||
|
// anything from here.
|
||||||
|
callback(f, filepath.Join(cleaned, f.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block until all of the routines finish and have returned a value.
|
||||||
|
return g.Wait()
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/pterodactyl/wings/api"
|
"github.com/pterodactyl/wings/api"
|
||||||
|
"github.com/pterodactyl/wings/config"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@@ -36,6 +37,19 @@ func (s *Server) Install() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reinstalls a server's software by utilizing the install script for the server egg. This
|
||||||
|
// does not touch any existing files for the server, other than what the script modifies.
|
||||||
|
func (s *Server) Reinstall() error {
|
||||||
|
if s.GetState() != ProcessOfflineState {
|
||||||
|
zap.S().Debugw("waiting for server instance to enter a stopped state", zap.String("server", s.Uuid))
|
||||||
|
if err := s.Environment.WaitForStop(10, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Install()
|
||||||
|
}
|
||||||
|
|
||||||
// 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, rerr, err := api.NewRequester().GetInstallationScript(s.Uuid)
|
script, rerr, err := api.NewRequester().GetInstallationScript(s.Uuid)
|
||||||
@@ -117,7 +131,7 @@ func (ip *InstallationProcess) Run() error {
|
|||||||
// Writes the installation script to a temporary file on the host machine so that it
|
// Writes the installation script to a temporary file on the host machine so that it
|
||||||
// can be properly mounted into the installation container and then executed.
|
// can be properly mounted into the installation container and then executed.
|
||||||
func (ip *InstallationProcess) writeScriptToDisk() (string, error) {
|
func (ip *InstallationProcess) writeScriptToDisk() (string, error) {
|
||||||
d, err := ioutil.TempDir("", "pterodactyl")
|
d, err := ioutil.TempDir("", "pterodactyl/")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.WithStack(err)
|
return "", errors.WithStack(err)
|
||||||
}
|
}
|
||||||
@@ -218,6 +232,11 @@ func (ip *InstallationProcess) BeforeExecute() (string, error) {
|
|||||||
return fileName, nil
|
return fileName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns the log path for the installation process.
|
||||||
|
func (ip *InstallationProcess) GetLogPath() string {
|
||||||
|
return filepath.Join(config.Get().System.GetInstallLogPath(), ip.Server.Uuid+".log")
|
||||||
|
}
|
||||||
|
|
||||||
// Cleans up after the execution of the installation process. This grabs the logs from the
|
// Cleans up after the execution of the installation process. This grabs the logs from the
|
||||||
// process to store in the server configuration directory, and then destroys the associated
|
// process to store in the server configuration directory, and then destroys the associated
|
||||||
// installation container.
|
// installation container.
|
||||||
@@ -235,7 +254,7 @@ func (ip *InstallationProcess) AfterExecute(containerId string) error {
|
|||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.OpenFile(filepath.Join("data/install_logs/", ip.Server.Uuid+".log"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
f, err := os.OpenFile(ip.GetLogPath(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func (s *Server) onConsoleOutput(data string) {
|
|||||||
// If the specific line of output is one that would mark the server as started,
|
// If the specific line of output is one that would mark the server as started,
|
||||||
// set the server to that state. Only do this if the server is not currently stopped
|
// set the server to that state. Only do this if the server is not currently stopped
|
||||||
// or stopping.
|
// or stopping.
|
||||||
if s.State == ProcessStartingState && strings.Contains(data, s.processConfiguration.Startup.Done) {
|
if s.GetState() == ProcessStartingState && strings.Contains(data, s.processConfiguration.Startup.Done) {
|
||||||
zap.S().Debugw(
|
zap.S().Debugw(
|
||||||
"detected server in running state based on line output", zap.String("match", s.processConfiguration.Startup.Done), zap.String("against", data),
|
"detected server in running state based on line output", zap.String("match", s.processConfiguration.Startup.Done), zap.String("against", data),
|
||||||
)
|
)
|
||||||
@@ -38,7 +38,7 @@ func (s *Server) onConsoleOutput(data string) {
|
|||||||
// If the command sent to the server is one that should stop the server we will need to
|
// If the command sent to the server is one that should stop the server we will need to
|
||||||
// set the server to be in a stopping state, otherwise crash detection will kick in and
|
// set the server to be in a stopping state, otherwise crash detection will kick in and
|
||||||
// cause the server to unexpectedly restart on the user.
|
// cause the server to unexpectedly restart on the user.
|
||||||
if s.State == ProcessStartingState || s.State == ProcessRunningState {
|
if s.IsRunning() {
|
||||||
if s.processConfiguration.Stop.Type == api.ProcessStopCommand && data == s.processConfiguration.Stop.Value {
|
if s.processConfiguration.Stop.Type == api.ProcessStopCommand && data == s.processConfiguration.Stop.Value {
|
||||||
s.SetState(ProcessStoppingState)
|
s.SetState(ProcessStoppingState)
|
||||||
}
|
}
|
||||||
|
|||||||
12
server/power.go
Normal file
12
server/power.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
type PowerAction struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pr *PowerAction) IsValid() bool {
|
||||||
|
return pr.Action == "start" ||
|
||||||
|
pr.Action == "stop" ||
|
||||||
|
pr.Action == "kill" ||
|
||||||
|
pr.Action == "restart"
|
||||||
|
}
|
||||||
174
server/server.go
174
server/server.go
@@ -9,10 +9,7 @@ import (
|
|||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"github.com/remeh/sizedwaitgroup"
|
"github.com/remeh/sizedwaitgroup"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -27,11 +24,11 @@ func GetServers() *Collection {
|
|||||||
// High level definition for a server instance being controlled by Wings.
|
// High level definition for a server instance being controlled by Wings.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
// The unique identifier for the server that should be used when referencing
|
// The unique identifier for the server that should be used when referencing
|
||||||
// it aganist the Panel API (and internally). This will be used when naming
|
// it against the Panel API (and internally). This will be used when naming
|
||||||
// docker containers as well as in log output.
|
// docker containers as well as in log output.
|
||||||
Uuid string `json:"uuid"`
|
Uuid string `json:"uuid"`
|
||||||
|
|
||||||
// Wether or not the server is in a suspended state. Suspended servers cannot
|
// Whether or not the server is in a suspended state. Suspended servers cannot
|
||||||
// be started or modified except in certain scenarios by an admin user.
|
// be started or modified except in certain scenarios by an admin user.
|
||||||
Suspended bool `json:"suspended"`
|
Suspended bool `json:"suspended"`
|
||||||
|
|
||||||
@@ -45,6 +42,7 @@ type Server struct {
|
|||||||
// server process.
|
// server process.
|
||||||
EnvVars map[string]string `json:"environment" yaml:"environment"`
|
EnvVars map[string]string `json:"environment" yaml:"environment"`
|
||||||
|
|
||||||
|
Archiver Archiver `json:"-" yaml:"-"`
|
||||||
CrashDetection CrashDetection `json:"crash_detection" yaml:"crash_detection"`
|
CrashDetection CrashDetection `json:"crash_detection" yaml:"crash_detection"`
|
||||||
Build BuildSettings `json:"build"`
|
Build BuildSettings `json:"build"`
|
||||||
Allocations Allocations `json:"allocations"`
|
Allocations Allocations `json:"allocations"`
|
||||||
@@ -74,7 +72,7 @@ type Server struct {
|
|||||||
|
|
||||||
// Internal mutex used to block actions that need to occur sequentially, such as
|
// Internal mutex used to block actions that need to occur sequentially, such as
|
||||||
// writing the configuration to the disk.
|
// writing the configuration to the disk.
|
||||||
mutex *sync.Mutex
|
sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// The build settings for a given server that impact docker container creation and
|
// The build settings for a given server that impact docker container creation and
|
||||||
@@ -98,6 +96,9 @@ type BuildSettings struct {
|
|||||||
|
|
||||||
// The amount of disk space in megabytes that a server is allowed to use.
|
// The amount of disk space in megabytes that a server is allowed to use.
|
||||||
DiskSpace int64 `json:"disk_space" yaml:"disk"`
|
DiskSpace int64 `json:"disk_space" yaml:"disk"`
|
||||||
|
|
||||||
|
// Sets which CPU threads can be used by the docker instance.
|
||||||
|
Threads string `json:"threads" yaml:"threads"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converts the CPU limit for a server build into a number that can be better understood
|
// Converts the CPU limit for a server build into a number that can be better understood
|
||||||
@@ -140,7 +141,7 @@ type Allocations struct {
|
|||||||
|
|
||||||
// Iterates over a given directory and loads all of the servers listed before returning
|
// Iterates over a given directory and loads all of the servers listed before returning
|
||||||
// them to the calling function.
|
// them to the calling function.
|
||||||
func LoadDirectory(dir string, cfg *config.SystemConfiguration) error {
|
func LoadDirectory() error {
|
||||||
// We could theoretically use a standard wait group here, however doing
|
// We could theoretically use a standard wait group here, however doing
|
||||||
// that introduces the potential to crash the program due to too many
|
// that introduces the potential to crash the program due to too many
|
||||||
// open files. This wouldn't happen on a small setup, but once the daemon is
|
// open files. This wouldn't happen on a small setup, but once the daemon is
|
||||||
@@ -151,43 +152,41 @@ func LoadDirectory(dir string, cfg *config.SystemConfiguration) error {
|
|||||||
// the road to help big instances scale better.
|
// the road to help big instances scale better.
|
||||||
wg := sizedwaitgroup.New(10)
|
wg := sizedwaitgroup.New(10)
|
||||||
|
|
||||||
f, err := ioutil.ReadDir(dir)
|
configs, rerr, err := api.NewRequester().GetAllServerConfigurations()
|
||||||
|
if err != nil || rerr != nil {
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New(rerr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
states, err := getServerStates()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
servers = NewCollection(nil)
|
servers = NewCollection(nil)
|
||||||
|
|
||||||
for _, file := range f {
|
for uuid, data := range configs {
|
||||||
if !strings.HasSuffix(file.Name(), ".yml") || file.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Add()
|
wg.Add()
|
||||||
// For each of the YAML files we find, parse it and create a new server
|
|
||||||
// configuration object that can then be returned to the caller.
|
go func(uuid string, data *api.ServerConfigurationResponse) {
|
||||||
go func(file os.FileInfo) {
|
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
b, err := ioutil.ReadFile(path.Join(dir, file.Name()))
|
s, err := FromConfiguration(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.S().Errorw("failed to read server configuration file, skipping...", zap.String("server", file.Name()), zap.Error(err))
|
zap.S().Errorw("failed to load server, skipping...", zap.String("server", uuid), zap.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err := FromConfiguration(b, cfg)
|
if state, exists := states[s.Uuid]; exists {
|
||||||
if err != nil {
|
s.SetState(state)
|
||||||
if IsServerDoesNotExistError(err) {
|
zap.S().Debugw("loaded server state from cache", zap.String("server", s.Uuid), zap.String("state", s.GetState()))
|
||||||
zap.S().Infow("server does not exist on remote system", zap.String("server", file.Name()))
|
|
||||||
} else {
|
|
||||||
zap.S().Errorw("failed to parse server configuration, skipping...", zap.String("server", file.Name()), zap.Error(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
servers.Add(s)
|
servers.Add(s)
|
||||||
}(file)
|
}(uuid, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait until we've processed all of the configuration files in the directory
|
// Wait until we've processed all of the configuration files in the directory
|
||||||
@@ -197,24 +196,17 @@ func LoadDirectory(dir string, cfg *config.SystemConfiguration) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initializes the default required internal struct components for a Server.
|
// Initializes a server using a data byte array. This will be marshaled into the
|
||||||
func (s *Server) Init() {
|
|
||||||
s.mutex = &sync.Mutex{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initalizes 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
|
// given struct using a YAML marshaler. This will also configure the given environment
|
||||||
// for a server.
|
// for a server.
|
||||||
func FromConfiguration(data []byte, cfg *config.SystemConfiguration) (*Server, error) {
|
func FromConfiguration(data *api.ServerConfigurationResponse) (*Server, error) {
|
||||||
s := new(Server)
|
s := new(Server)
|
||||||
|
|
||||||
if err := defaults.Set(s); err != nil {
|
if err := defaults.Set(s); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Init()
|
if err := s.UpdateDataStructure(data.Settings, false); err != nil {
|
||||||
|
|
||||||
if err := yaml.Unmarshal(data, s); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,19 +220,18 @@ func FromConfiguration(data []byte, cfg *config.SystemConfiguration) (*Server, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.Cache = cache.New(time.Minute*10, time.Minute*15)
|
s.Cache = cache.New(time.Minute*10, time.Minute*15)
|
||||||
|
s.Archiver = Archiver{
|
||||||
|
Server: s,
|
||||||
|
}
|
||||||
s.Filesystem = Filesystem{
|
s.Filesystem = Filesystem{
|
||||||
Configuration: cfg,
|
Configuration: &config.Get().System,
|
||||||
Server: s,
|
Server: s,
|
||||||
}
|
}
|
||||||
s.Resources = ResourceUsage{}
|
s.Resources = ResourceUsage{}
|
||||||
|
|
||||||
// This is also done when the server is booted, however we need to account for instances
|
// Forces the configuration to be synced with the panel.
|
||||||
// where the server is already running and the Daemon reboots. In those cases this will
|
if err := s.SyncWithConfiguration(data); err != nil {
|
||||||
// allow us to you know, stop servers.
|
return nil, err
|
||||||
if cfg.SyncServersOnBoot {
|
|
||||||
if err := s.Sync(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
@@ -293,13 +284,16 @@ func (s *Server) Sync() error {
|
|||||||
return errors.New(rerr.String())
|
return errors.New(rerr.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return s.SyncWithConfiguration(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) SyncWithConfiguration(cfg *api.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, false); err != nil {
|
if err := s.UpdateDataStructure(cfg.Settings, false); err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.processConfiguration = cfg.ProcessConfiguration
|
s.processConfiguration = cfg.ProcessConfiguration
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,68 +316,28 @@ func (s *Server) CreateEnvironment() error {
|
|||||||
return s.Environment.Create()
|
return s.Environment.Create()
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
ProcessOfflineState = "offline"
|
|
||||||
ProcessStartingState = "starting"
|
|
||||||
ProcessRunningState = "running"
|
|
||||||
ProcessStoppingState = "stopping"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Sets the state of the server internally. This function handles crash detection as
|
|
||||||
// well as reporting to event listeners for the server.
|
|
||||||
func (s *Server) SetState(state string) error {
|
|
||||||
if state != ProcessOfflineState && state != ProcessStartingState && state != ProcessRunningState && state != ProcessStoppingState {
|
|
||||||
return errors.New(fmt.Sprintf("invalid server state received: %s", state))
|
|
||||||
}
|
|
||||||
|
|
||||||
prevState := s.State
|
|
||||||
s.State = state
|
|
||||||
|
|
||||||
// Persist this change to the disk immediately so that should the Daemon be stopped or
|
|
||||||
// crash we can immediately restore the server state.
|
|
||||||
//
|
|
||||||
// This really only makes a difference if all of the Docker containers are also stopped,
|
|
||||||
// but this was a highly requested feature and isn't hard to work with, so lets do it.
|
|
||||||
//
|
|
||||||
// We also get the benefit of server status changes always propagating corrected configurations
|
|
||||||
// to the disk should we forget to do it elsewhere.
|
|
||||||
go func(server *Server) {
|
|
||||||
if _, err := server.WriteConfigurationToDisk(); err != nil {
|
|
||||||
zap.S().Warnw("failed to write server state change to disk", zap.String("server", server.Uuid), zap.Error(err))
|
|
||||||
}
|
|
||||||
}(s)
|
|
||||||
|
|
||||||
zap.S().Debugw("saw server status change event", zap.String("server", s.Uuid), zap.String("status", s.State))
|
|
||||||
|
|
||||||
// Emit the event to any listeners that are currently registered.
|
|
||||||
s.Events().Publish(StatusEvent, s.State)
|
|
||||||
|
|
||||||
// If server was in an online state, and is now in an offline state we should handle
|
|
||||||
// that as a crash event. In that scenario, check the last crash time, and the crash
|
|
||||||
// counter.
|
|
||||||
//
|
|
||||||
// In the event that we have passed the thresholds, don't do anything, otherwise
|
|
||||||
// automatically attempt to start the process back up for the user. This is done in a
|
|
||||||
// seperate thread as to not block any actions currently taking place in the flow
|
|
||||||
// that called this function.
|
|
||||||
if (prevState == ProcessStartingState || prevState == ProcessRunningState) && s.State == ProcessOfflineState {
|
|
||||||
zap.S().Infow("detected server as entering a potentially crashed state; running handler", zap.String("server", s.Uuid))
|
|
||||||
|
|
||||||
go func(server *Server) {
|
|
||||||
if err := server.handleServerCrash(); err != nil {
|
|
||||||
if IsTooFrequentCrashError(err) {
|
|
||||||
zap.S().Infow("did not restart server after crash; occurred too soon after last", zap.String("server", server.Uuid))
|
|
||||||
} else {
|
|
||||||
zap.S().Errorw("failed to handle server crash state", zap.String("server", server.Uuid), zap.Error(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets the process configuration data for the server.
|
// Gets the process configuration data for the server.
|
||||||
func (s *Server) GetProcessConfiguration() (*api.ServerConfigurationResponse, *api.RequestError, error) {
|
func (s *Server) GetProcessConfiguration() (*api.ServerConfigurationResponse, *api.RequestError, error) {
|
||||||
return api.NewRequester().GetServerConfiguration(s.Uuid)
|
return api.NewRequester().GetServerConfiguration(s.Uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function that can receieve a power action and then process the
|
||||||
|
// actions that need to occur for it.
|
||||||
|
func (s *Server) HandlePowerAction(action PowerAction) error {
|
||||||
|
switch action.Action {
|
||||||
|
case "start":
|
||||||
|
return s.Environment.Start()
|
||||||
|
case "restart":
|
||||||
|
if err := s.Environment.WaitForStop(60, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Environment.Start()
|
||||||
|
case "stop":
|
||||||
|
return s.Environment.Stop()
|
||||||
|
case "kill":
|
||||||
|
return s.Environment.Terminate(os.Kill)
|
||||||
|
default:
|
||||||
|
return errors.New("an invalid power action was provided")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
143
server/state.go
Normal file
143
server/state.go
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var stateMutex sync.Mutex
|
||||||
|
|
||||||
|
// Returns the state of the servers.
|
||||||
|
func getServerStates() (map[string]string, error) {
|
||||||
|
// Request a lock after we check if the file exists.
|
||||||
|
stateMutex.Lock()
|
||||||
|
defer stateMutex.Unlock()
|
||||||
|
|
||||||
|
// Open the states file.
|
||||||
|
f, err := os.OpenFile(config.Get().System.GetStatesPath(), os.O_RDONLY|os.O_CREATE, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// Convert the json object to a map.
|
||||||
|
states := map[string]string{}
|
||||||
|
if err := json.NewDecoder(f).Decode(&states); err != nil && err != io.EOF {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return states, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveServerStates .
|
||||||
|
func saveServerStates() error {
|
||||||
|
// Get the states of all servers on the daemon.
|
||||||
|
states := map[string]string{}
|
||||||
|
for _, s := range GetServers().All() {
|
||||||
|
states[s.Uuid] = s.GetState()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the map to a json object.
|
||||||
|
data, err := json.Marshal(states)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stateMutex.Lock()
|
||||||
|
defer stateMutex.Unlock()
|
||||||
|
|
||||||
|
// Write the data to the file
|
||||||
|
if err := ioutil.WriteFile(config.Get().System.GetStatesPath(), data, 0644); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProcessOfflineState = "offline"
|
||||||
|
ProcessStartingState = "starting"
|
||||||
|
ProcessRunningState = "running"
|
||||||
|
ProcessStoppingState = "stopping"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sets the state of the server internally. This function handles crash detection as
|
||||||
|
// well as reporting to event listeners for the server.
|
||||||
|
func (s *Server) SetState(state string) error {
|
||||||
|
if state != ProcessOfflineState && state != ProcessStartingState && state != ProcessRunningState && state != ProcessStoppingState {
|
||||||
|
return errors.New(fmt.Sprintf("invalid server state received: %s", state))
|
||||||
|
}
|
||||||
|
|
||||||
|
prevState := s.GetState()
|
||||||
|
|
||||||
|
// Obtain a mutex lock and update the current state of the server.
|
||||||
|
s.Lock()
|
||||||
|
s.State = state
|
||||||
|
|
||||||
|
// Emit the event to any listeners that are currently registered.
|
||||||
|
zap.S().Debugw("saw server status change event", zap.String("server", s.Uuid), zap.String("status", s.State))
|
||||||
|
s.Events().Publish(StatusEvent, s.State)
|
||||||
|
|
||||||
|
// Release the lock as it is no longer needed for the following actions.
|
||||||
|
s.Unlock()
|
||||||
|
|
||||||
|
// Persist this change to the disk immediately so that should the Daemon be stopped or
|
||||||
|
// crash we can immediately restore the server state.
|
||||||
|
//
|
||||||
|
// This really only makes a difference if all of the Docker containers are also stopped,
|
||||||
|
// but this was a highly requested feature and isn't hard to work with, so lets do it.
|
||||||
|
//
|
||||||
|
// We also get the benefit of server status changes always propagating corrected configurations
|
||||||
|
// to the disk should we forget to do it elsewhere.
|
||||||
|
go func() {
|
||||||
|
if err := saveServerStates(); err != nil {
|
||||||
|
zap.S().Warnw("failed to write server states to disk", zap.Error(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// If server was in an online state, and is now in an offline state we should handle
|
||||||
|
// that as a crash event. In that scenario, check the last crash time, and the crash
|
||||||
|
// counter.
|
||||||
|
//
|
||||||
|
// In the event that we have passed the thresholds, don't do anything, otherwise
|
||||||
|
// automatically attempt to start the process back up for the user. This is done in a
|
||||||
|
// separate thread as to not block any actions currently taking place in the flow
|
||||||
|
// that called this function.
|
||||||
|
if (prevState == ProcessStartingState || prevState == ProcessRunningState) && s.GetState() == ProcessOfflineState {
|
||||||
|
zap.S().Infow("detected server as entering a potentially crashed state; running handler", zap.String("server", s.Uuid))
|
||||||
|
|
||||||
|
go func(server *Server) {
|
||||||
|
if err := server.handleServerCrash(); err != nil {
|
||||||
|
if IsTooFrequentCrashError(err) {
|
||||||
|
zap.S().Infow("did not restart server after crash; occurred too soon after last", zap.String("server", server.Uuid))
|
||||||
|
} else {
|
||||||
|
zap.S().Errorw("failed to handle server crash state", zap.String("server", server.Uuid), zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the current state of the server in a race-safe manner.
|
||||||
|
func (s *Server) GetState() string {
|
||||||
|
s.RLock()
|
||||||
|
defer s.RUnlock()
|
||||||
|
|
||||||
|
return s.State
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
return s.GetState() == ProcessRunningState || s.GetState() == ProcessStartingState
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"github.com/imdario/mergo"
|
"github.com/imdario/mergo"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"os"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Merges data passed through in JSON form into the existing server object.
|
// Merges data passed through in JSON form into the existing server object.
|
||||||
@@ -25,7 +24,7 @@ func (s *Server) UpdateDataStructure(data []byte, background bool) error {
|
|||||||
// Don't allow obviously corrupted data to pass through into this function. If the UUID
|
// Don't allow obviously corrupted data to pass through into this function. If the UUID
|
||||||
// doesn't match something has gone wrong and the API is attempting to meld this server
|
// doesn't match something has gone wrong and the API is attempting to meld this server
|
||||||
// instance into a totally different one, which would be bad.
|
// instance into a totally different one, which would be bad.
|
||||||
if src.Uuid != "" && src.Uuid != s.Uuid {
|
if src.Uuid != "" && s.Uuid != "" && src.Uuid != s.Uuid {
|
||||||
return errors.New("attempting to merge a data stack with an invalid UUID")
|
return errors.New("attempting to merge a data stack with an invalid UUID")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,10 +64,6 @@ func (s *Server) UpdateDataStructure(data []byte, background bool) error {
|
|||||||
s.Allocations.Mappings = src.Allocations.Mappings
|
s.Allocations.Mappings = src.Allocations.Mappings
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := s.WriteConfigurationToDisk(); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if background {
|
if background {
|
||||||
s.runBackgroundActions()
|
s.runBackgroundActions()
|
||||||
}
|
}
|
||||||
@@ -98,12 +93,12 @@ func (s *Server) runBackgroundActions() {
|
|||||||
// Check if the server is now suspended, and if so and the process is not terminated
|
// Check if the server is now suspended, and if so and the process is not terminated
|
||||||
// yet, do it immediately.
|
// yet, do it immediately.
|
||||||
go func(server *Server) {
|
go func(server *Server) {
|
||||||
if server.Suspended && server.State != ProcessOfflineState {
|
if server.Suspended && server.GetState() != ProcessOfflineState {
|
||||||
zap.S().Infow("server suspended with running process state, terminating now", zap.String("server", server.Uuid))
|
zap.S().Infow("server suspended with running process state, terminating now", zap.String("server", server.Uuid))
|
||||||
|
|
||||||
if err := server.Environment.Terminate(os.Kill); err != nil {
|
if err := server.Environment.WaitForStop(10, true); err != nil {
|
||||||
zap.S().Warnw(
|
zap.S().Warnw(
|
||||||
"failed to terminate server environment after seeing suspension",
|
"failed to stop server environment after seeing suspension",
|
||||||
zap.String("server", server.Uuid),
|
zap.String("server", server.Uuid),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package main
|
package system
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// The current version of this software.
|
// The current version of this software.
|
||||||
Version = "1.0.0-alpha.2"
|
Version = "1.0.0-beta.2"
|
||||||
)
|
)
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package main
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/docker/docker/pkg/parsers/kernel"
|
"github.com/docker/docker/pkg/parsers/kernel"
|
||||||
"runtime"
|
"runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SystemInformation struct {
|
type Information struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
KernelVersion string `json:"kernel_version"`
|
KernelVersion string `json:"kernel_version"`
|
||||||
Architecture string `json:"architecture"`
|
Architecture string `json:"architecture"`
|
||||||
@@ -13,13 +13,13 @@ type SystemInformation struct {
|
|||||||
CpuCount int `json:"cpu_count"`
|
CpuCount int `json:"cpu_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetSystemInformation() (*SystemInformation, error) {
|
func GetSystemInformation() (*Information, error) {
|
||||||
k, err := kernel.GetKernelVersion()
|
k, err := kernel.GetKernelVersion()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s := &SystemInformation{
|
s := &Information{
|
||||||
Version: Version,
|
Version: Version,
|
||||||
KernelVersion: k.String(),
|
KernelVersion: k.String(),
|
||||||
Architecture: runtime.GOARCH,
|
Architecture: runtime.GOARCH,
|
||||||
424
websocket.go
424
websocket.go
@@ -1,424 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/gbrlsnchs/jwt/v3"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/julienschmidt/httprouter"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/pterodactyl/wings/config"
|
|
||||||
"github.com/pterodactyl/wings/server"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
AuthenticationSuccessEvent = "auth success"
|
|
||||||
TokenExpiringEvent = "token expiring"
|
|
||||||
TokenExpiredEvent = "token expired"
|
|
||||||
AuthenticationEvent = "auth"
|
|
||||||
SetStateEvent = "set state"
|
|
||||||
SendServerLogsEvent = "send logs"
|
|
||||||
SendCommandEvent = "send command"
|
|
||||||
ErrorEvent = "daemon error"
|
|
||||||
)
|
|
||||||
|
|
||||||
type WebsocketMessage struct {
|
|
||||||
// The event to perform. Should be one of the following that are supported:
|
|
||||||
//
|
|
||||||
// - status : Returns the server's power state.
|
|
||||||
// - logs : Returns the server log data at the time of the request.
|
|
||||||
// - power : Performs a power action aganist the server based the data.
|
|
||||||
// - command : Performs a command on a server using the data field.
|
|
||||||
Event string `json:"event"`
|
|
||||||
|
|
||||||
// The data to pass along, only used by power/command currently. Other requests
|
|
||||||
// should either omit the field or pass an empty value as it is ignored.
|
|
||||||
Args []string `json:"args,omitempty"`
|
|
||||||
|
|
||||||
// Is set to true when the request is originating from outside of the Daemon,
|
|
||||||
// otherwise set to false for outbound.
|
|
||||||
inbound bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type WebsocketHandler struct {
|
|
||||||
Server *server.Server
|
|
||||||
Mutex sync.Mutex
|
|
||||||
Connection *websocket.Conn
|
|
||||||
JWT *WebsocketTokenPayload
|
|
||||||
}
|
|
||||||
|
|
||||||
type WebsocketTokenPayload struct {
|
|
||||||
jwt.Payload
|
|
||||||
UserID json.Number `json:"user_id"`
|
|
||||||
ServerUUID string `json:"server_uuid"`
|
|
||||||
Permissions []string `json:"permissions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
PermissionConnect = "connect"
|
|
||||||
PermissionSendCommand = "send-command"
|
|
||||||
PermissionSendPower = "send-power"
|
|
||||||
PermissionReceiveErrors = "receive-errors"
|
|
||||||
PermissionReceiveInstall = "receive-install"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Checks if the given token payload has a permission string.
|
|
||||||
func (wtp *WebsocketTokenPayload) HasPermission(permission string) bool {
|
|
||||||
for _, k := range wtp.Permissions {
|
|
||||||
if k == permission {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var alg *jwt.HMACSHA
|
|
||||||
|
|
||||||
// Validates the provided JWT against the known secret for the Daemon and returns the
|
|
||||||
// parsed data.
|
|
||||||
//
|
|
||||||
// This function DOES NOT validate that the token is valid for the connected server, nor
|
|
||||||
// does it ensure that the user providing the token is able to actually do things.
|
|
||||||
func ParseJWT(token []byte) (*WebsocketTokenPayload, error) {
|
|
||||||
var payload WebsocketTokenPayload
|
|
||||||
if alg == nil {
|
|
||||||
alg = jwt.NewHS256([]byte(config.Get().AuthenticationToken))
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
verifyOptions := jwt.ValidatePayload(
|
|
||||||
&payload.Payload,
|
|
||||||
jwt.ExpirationTimeValidator(now),
|
|
||||||
)
|
|
||||||
|
|
||||||
_, err := jwt.Verify(token, alg, &payload, verifyOptions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !payload.HasPermission(PermissionConnect) {
|
|
||||||
return nil, errors.New("not authorized to connect to this socket")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &payload, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if the JWT is still valid.
|
|
||||||
func (wsh *WebsocketHandler) TokenValid() error {
|
|
||||||
if wsh.JWT == nil {
|
|
||||||
return errors.New("no jwt present")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := jwt.ExpirationTimeValidator(time.Now())(&wsh.JWT.Payload); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !wsh.JWT.HasPermission(PermissionConnect) {
|
|
||||||
return errors.New("jwt does not have connect permission")
|
|
||||||
}
|
|
||||||
|
|
||||||
if wsh.Server.Uuid != wsh.JWT.ServerUUID {
|
|
||||||
return errors.New("jwt server uuid mismatch")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle a request for a specific server websocket. This will handle inbound requests as well
|
|
||||||
// as ensure that any console output is also passed down the wire on the socket.
|
|
||||||
func (rt *Router) routeWebsocket(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
|
||||||
c, err := rt.upgrader.Upgrade(w, r, nil)
|
|
||||||
if err != nil {
|
|
||||||
zap.S().Errorw("error upgrading websocket", zap.Error(errors.WithStack(err)))
|
|
||||||
http.Error(w, "failed to upgrade websocket", http.StatusInternalServerError)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make a ticker and completion channel that is used to continuously poll the
|
|
||||||
// JWT stored in the session to send events to the socket when it is expiring.
|
|
||||||
ticker := time.NewTicker(time.Second * 30)
|
|
||||||
done := make(chan bool)
|
|
||||||
|
|
||||||
// Whenever this function is complete, end the ticker, close out the channel,
|
|
||||||
// and then close the websocket connection.
|
|
||||||
defer func() {
|
|
||||||
ticker.Stop()
|
|
||||||
done <- true
|
|
||||||
c.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
s := rt.GetServer(ps.ByName("server"))
|
|
||||||
handler := WebsocketHandler{
|
|
||||||
Server: s,
|
|
||||||
Mutex: sync.Mutex{},
|
|
||||||
Connection: c,
|
|
||||||
JWT: nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
events := []string{
|
|
||||||
server.StatsEvent,
|
|
||||||
server.StatusEvent,
|
|
||||||
server.ConsoleOutputEvent,
|
|
||||||
server.InstallOutputEvent,
|
|
||||||
server.DaemonMessageEvent,
|
|
||||||
}
|
|
||||||
|
|
||||||
eventChannel := make(chan server.Event)
|
|
||||||
for _, event := range events {
|
|
||||||
s.Events().Subscribe(event, eventChannel)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
for _, event := range events {
|
|
||||||
s.Events().Unsubscribe(event, eventChannel)
|
|
||||||
}
|
|
||||||
|
|
||||||
close(eventChannel)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Listen for different events emitted by the server and respond to them appropriately.
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case d := <-eventChannel:
|
|
||||||
handler.SendJson(&WebsocketMessage{
|
|
||||||
Event: d.Topic,
|
|
||||||
Args: []string{d.Data},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Sit here and check the time to expiration on the JWT every 30 seconds until
|
|
||||||
// the token has expired. If we are within 3 minutes of the token expiring, send
|
|
||||||
// a notice over the socket that it is expiring soon. If it has expired, send that
|
|
||||||
// notice as well.
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
case <-ticker.C:
|
|
||||||
{
|
|
||||||
if handler.JWT != nil {
|
|
||||||
if handler.JWT.ExpirationTime.Unix()-time.Now().Unix() <= 0 {
|
|
||||||
handler.SendJson(&WebsocketMessage{Event: TokenExpiredEvent})
|
|
||||||
} else if handler.JWT.ExpirationTime.Unix()-time.Now().Unix() <= 180 {
|
|
||||||
handler.SendJson(&WebsocketMessage{Event: TokenExpiringEvent})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
j := WebsocketMessage{inbound: true}
|
|
||||||
|
|
||||||
_, p, err := c.ReadMessage()
|
|
||||||
if err != nil {
|
|
||||||
if !websocket.IsCloseError(
|
|
||||||
err,
|
|
||||||
websocket.CloseNormalClosure,
|
|
||||||
websocket.CloseGoingAway,
|
|
||||||
websocket.CloseNoStatusReceived,
|
|
||||||
websocket.CloseServiceRestart,
|
|
||||||
websocket.CloseAbnormalClosure,
|
|
||||||
) {
|
|
||||||
zap.S().Errorw("error handling websocket message", zap.Error(err))
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discard and JSON parse errors into the void and don't continue processing this
|
|
||||||
// specific socket request. If we did a break here the client would get disconnected
|
|
||||||
// from the socket, which is NOT what we want to do.
|
|
||||||
if err := json.Unmarshal(p, &j); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := handler.HandleInbound(j); err != nil {
|
|
||||||
handler.SendErrorJson(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform a blocking send operation on the websocket since we want to avoid any
|
|
||||||
// concurrent writes to the connection, which would cause a runtime panic and cause
|
|
||||||
// the program to crash out.
|
|
||||||
func (wsh *WebsocketHandler) SendJson(v *WebsocketMessage) error {
|
|
||||||
// Do not send JSON down the line if the JWT on the connection is not
|
|
||||||
// valid!
|
|
||||||
if err := wsh.TokenValid(); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're sending installation output but the user does not have the required
|
|
||||||
// permissions to see the output, don't send it down the line.
|
|
||||||
if v.Event == server.InstallOutputEvent {
|
|
||||||
zap.S().Debugf("%+v", v.Args)
|
|
||||||
if wsh.JWT != nil && !wsh.JWT.HasPermission(PermissionReceiveInstall) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return wsh.unsafeSendJson(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sends JSON over the websocket connection, ignoring the authentication state of the
|
|
||||||
// socket user. Do not call this directly unless you are positive a response should be
|
|
||||||
// sent back to the client!
|
|
||||||
func (wsh *WebsocketHandler) unsafeSendJson(v interface{}) error {
|
|
||||||
wsh.Mutex.Lock()
|
|
||||||
defer wsh.Mutex.Unlock()
|
|
||||||
|
|
||||||
return wsh.Connection.WriteJSON(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sends an error back to the connected websocket instance by checking the permissions
|
|
||||||
// of the token. If the user has the "receive-errors" grant we will send back the actual
|
|
||||||
// error message, otherwise we just send back a standard error message.
|
|
||||||
func (wsh *WebsocketHandler) SendErrorJson(err error) error {
|
|
||||||
wsh.Mutex.Lock()
|
|
||||||
defer wsh.Mutex.Unlock()
|
|
||||||
|
|
||||||
message := "an unexpected error was encountered while handling this request"
|
|
||||||
if wsh.JWT != nil {
|
|
||||||
if server.IsSuspendedError(err) || wsh.JWT.HasPermission(PermissionReceiveErrors) {
|
|
||||||
message = err.Error()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m, u := wsh.GetErrorMessage(message)
|
|
||||||
|
|
||||||
wsm := WebsocketMessage{Event: ErrorEvent}
|
|
||||||
wsm.Args = []string{m}
|
|
||||||
|
|
||||||
if !server.IsSuspendedError(err) {
|
|
||||||
zap.S().Errorw(
|
|
||||||
"an error was encountered in the websocket process",
|
|
||||||
zap.String("server", wsh.Server.Uuid),
|
|
||||||
zap.String("error_identifier", u.String()),
|
|
||||||
zap.Error(err),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return wsh.Connection.WriteJSON(wsm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Converts an error message into a more readable representation and returns a UUID
|
|
||||||
// that can be cross-referenced to find the specific error that triggered.
|
|
||||||
func (wsh *WebsocketHandler) GetErrorMessage(msg string) (string, uuid.UUID) {
|
|
||||||
u, _ := uuid.NewRandom()
|
|
||||||
|
|
||||||
m := fmt.Sprintf("Error Event [%s]: %s", u.String(), msg)
|
|
||||||
|
|
||||||
return m, u
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle the inbound socket request and route it to the proper server action.
|
|
||||||
func (wsh *WebsocketHandler) HandleInbound(m WebsocketMessage) error {
|
|
||||||
if !m.inbound {
|
|
||||||
return errors.New("cannot handle websocket message, not an inbound connection")
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.Event != AuthenticationEvent {
|
|
||||||
if err := wsh.TokenValid(); err != nil {
|
|
||||||
zap.S().Debugw("jwt token is no longer valid", zap.String("message", err.Error()))
|
|
||||||
|
|
||||||
wsh.unsafeSendJson(WebsocketMessage{
|
|
||||||
Event: ErrorEvent,
|
|
||||||
Args: []string{"could not authenticate client: " + err.Error()},
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch m.Event {
|
|
||||||
case AuthenticationEvent:
|
|
||||||
{
|
|
||||||
token, err := ParseJWT([]byte(strings.Join(m.Args, "")))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if token.HasPermission(PermissionConnect) {
|
|
||||||
wsh.JWT = token
|
|
||||||
}
|
|
||||||
|
|
||||||
// On every authentication event, send the current server status back
|
|
||||||
// to the client. :)
|
|
||||||
wsh.Server.Events().Publish(server.StatusEvent, wsh.Server.State)
|
|
||||||
|
|
||||||
wsh.unsafeSendJson(WebsocketMessage{
|
|
||||||
Event: AuthenticationSuccessEvent,
|
|
||||||
Args: []string{},
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
case SetStateEvent:
|
|
||||||
{
|
|
||||||
if !wsh.JWT.HasPermission(PermissionSendPower) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch strings.Join(m.Args, "") {
|
|
||||||
case "start":
|
|
||||||
return wsh.Server.Environment.Start()
|
|
||||||
case "stop":
|
|
||||||
return wsh.Server.Environment.Stop()
|
|
||||||
case "restart":
|
|
||||||
return nil
|
|
||||||
case "kill":
|
|
||||||
return wsh.Server.Environment.Terminate(os.Kill)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
case SendServerLogsEvent:
|
|
||||||
{
|
|
||||||
if running, _ := wsh.Server.Environment.IsRunning(); !running {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
logs, err := wsh.Server.Environment.Readlog(1024 * 16)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, line := range logs {
|
|
||||||
wsh.SendJson(&WebsocketMessage{
|
|
||||||
Event: server.ConsoleOutputEvent,
|
|
||||||
Args: []string{line},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
case SendCommandEvent:
|
|
||||||
{
|
|
||||||
if !wsh.JWT.HasPermission(PermissionSendCommand) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if wsh.Server.State == server.ProcessOfflineState {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return wsh.Server.Environment.SendCommand(strings.Join(m.Args, ""))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
201
wings.go
201
wings.go
@@ -1,206 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"github.com/pterodactyl/wings/cmd"
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/pterodactyl/wings/config"
|
|
||||||
"github.com/pterodactyl/wings/server"
|
|
||||||
"github.com/pterodactyl/wings/sftp"
|
|
||||||
"github.com/remeh/sizedwaitgroup"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var configPath = "config.yml"
|
|
||||||
var debug = false
|
|
||||||
|
|
||||||
// Entrypoint for the Wings application. Configures the logger and checks any
|
|
||||||
// flags that were passed through in the boot arguments.
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.StringVar(&configPath, "config", "config.yml", "set the location for the configuration file")
|
cmd.Execute()
|
||||||
flag.BoolVar(&debug, "debug", false, "pass in order to run wings in debug mode")
|
|
||||||
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
c, err := config.ReadConfiguration(configPath)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if debug {
|
|
||||||
c.Debug = true
|
|
||||||
}
|
|
||||||
|
|
||||||
printLogo()
|
|
||||||
if err := configureLogging(c.Debug); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
zap.S().Infof("using configuration from path: %s", configPath)
|
|
||||||
if c.Debug {
|
|
||||||
zap.S().Debugw("running in debug mode")
|
|
||||||
zap.S().Infow("certificate checking is disabled")
|
|
||||||
|
|
||||||
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.Set(c)
|
|
||||||
config.SetDebugViaFlag(debug)
|
|
||||||
|
|
||||||
zap.S().Infof("checking for pterodactyl system user \"%s\"", c.System.User)
|
|
||||||
if su, err := c.EnsurePterodactylUser(); err != nil {
|
|
||||||
zap.S().Panicw("failed to create pterodactyl system user", zap.Error(err))
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
zap.S().Infow("configured system user", zap.String("username", su.Username), zap.String("uid", su.Uid), zap.String("gid", su.Gid))
|
|
||||||
}
|
|
||||||
|
|
||||||
zap.S().Infow("beginnning file permission setting on server data directories")
|
|
||||||
if err := c.EnsureFilePermissions(); err != nil {
|
|
||||||
zap.S().Errorw("failed to properly chown data directories", zap.Error(err))
|
|
||||||
} else {
|
|
||||||
zap.S().Infow("finished ensuring file permissions")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := server.LoadDirectory("data/servers", &c.System); err != nil {
|
|
||||||
zap.S().Fatalw("failed to load server configurations", zap.Error(errors.WithStack(err)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ConfigureDockerEnvironment(&c.Docker); err != nil {
|
|
||||||
zap.S().Fatalw("failed to configure docker environment", zap.Error(errors.WithStack(err)))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.WriteToDisk(); err != nil {
|
|
||||||
zap.S().Errorw("failed to save configuration to disk", zap.Error(errors.WithStack(err)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Just for some nice log output.
|
|
||||||
for _, s := range server.GetServers().All() {
|
|
||||||
zap.S().Infow("loaded configuration for server", zap.String("server", s.Uuid))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new WaitGroup that limits us to 4 servers being bootstrapped at a time
|
|
||||||
// on Wings. This allows us to ensure the environment exists, write configurations,
|
|
||||||
// and reboot processes without causing a slow-down due to sequential booting.
|
|
||||||
wg := sizedwaitgroup.New(4)
|
|
||||||
|
|
||||||
for _, serv := range server.GetServers().All() {
|
|
||||||
wg.Add()
|
|
||||||
|
|
||||||
go func(s *server.Server) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
// Create a server environment if none exists currently. This allows us to recover from Docker
|
|
||||||
// being reinstalled on the host system for example.
|
|
||||||
zap.S().Infow("ensuring envrionment exists", zap.String("server", s.Uuid))
|
|
||||||
if err := s.Environment.Create(); err != nil {
|
|
||||||
zap.S().Errorw("failed to create an environment for server", zap.String("server", s.Uuid), zap.Error(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := s.Environment.IsRunning()
|
|
||||||
if err != nil {
|
|
||||||
zap.S().Errorw("error checking server environment status", zap.String("server", s.Uuid), zap.Error(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the server is currently running on Docker, mark the process as being in that state.
|
|
||||||
// We never want to stop an instance that is currently running external from Wings since
|
|
||||||
// that is a good way of keeping things running even if Wings gets in a very corrupted state.
|
|
||||||
//
|
|
||||||
// This will also validate that a server process is running if the last tracked state we have
|
|
||||||
// is that it was running, but we see that the container process is not currently running.
|
|
||||||
if r || (!r && (s.State == server.ProcessRunningState || s.State == server.ProcessStartingState)) {
|
|
||||||
zap.S().Infow("detected server is running, re-attaching to process", zap.String("server", s.Uuid))
|
|
||||||
if err := s.Environment.Start(); err != nil {
|
|
||||||
zap.S().Warnw(
|
|
||||||
"failed to properly start server detected as already running",
|
|
||||||
zap.String("server", s.Uuid),
|
|
||||||
zap.Error(errors.WithStack(err)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Addresses potentially invalid data in the stored file that can cause Wings to lose
|
|
||||||
// track of what the actual server state is.
|
|
||||||
s.SetState(server.ProcessOfflineState)
|
|
||||||
}(serv)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait until all of the servers are ready to go before we fire up the HTTP server.
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// If the SFTP subsystem should be started, do so now.
|
|
||||||
if c.System.Sftp.UseInternalSystem {
|
|
||||||
sftp.Initialize(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
r := &Router{
|
|
||||||
token: c.AuthenticationToken,
|
|
||||||
upgrader: websocket.Upgrader{
|
|
||||||
// Ensure that the websocket request is originating from the Panel itself,
|
|
||||||
// and not some other location.
|
|
||||||
CheckOrigin: func(r *http.Request) bool {
|
|
||||||
return r.Header.Get("Origin") == c.PanelLocation
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
router := r.ConfigureRouter()
|
|
||||||
zap.S().Infow("configuring webserver", zap.Bool("ssl", c.Api.Ssl.Enabled), zap.String("host", c.Api.Host), zap.Int("port", c.Api.Port))
|
|
||||||
|
|
||||||
addr := fmt.Sprintf("%s:%d", c.Api.Host, c.Api.Port)
|
|
||||||
if c.Api.Ssl.Enabled {
|
|
||||||
if err := http.ListenAndServeTLS(addr, c.Api.Ssl.CertificateFile, c.Api.Ssl.KeyFile, router); err != nil {
|
|
||||||
zap.S().Fatalw("failed to configure HTTPS server", zap.Error(err))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := http.ListenAndServe(addr, router); err != nil {
|
|
||||||
zap.S().Fatalw("failed to configure HTTP server", zap.Error(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configures the global logger for Zap so that we can call it from any location
|
|
||||||
// in the code without having to pass around a logger instance.
|
|
||||||
func configureLogging(debug bool) error {
|
|
||||||
cfg := zap.NewProductionConfig()
|
|
||||||
if debug {
|
|
||||||
cfg = zap.NewDevelopmentConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.Encoding = "console"
|
|
||||||
cfg.OutputPaths = []string{
|
|
||||||
"stdout",
|
|
||||||
}
|
|
||||||
|
|
||||||
logger, err := cfg.Build()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
zap.ReplaceGlobals(logger)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prints the wings logo, nothing special here!
|
|
||||||
func printLogo() {
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println(` ____`)
|
|
||||||
fmt.Println(`__ Pterodactyl _____/___/_______ _______ ______`)
|
|
||||||
fmt.Println(`\_____\ \/\/ / / / __ / ___/`)
|
|
||||||
fmt.Println(` \___\ / / / / /_/ /___ /`)
|
|
||||||
fmt.Println(` \___/\___/___/___/___/___ /______/`)
|
|
||||||
fmt.Println(` /_______/ v` + Version)
|
|
||||||
fmt.Println()
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user