Merge branch 'develop' into v2
This commit is contained in:
commit
6653466ca8
2
.github/workflows/build-test.yml
vendored
2
.github/workflows/build-test.yml
vendored
|
@ -12,7 +12,7 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-20.04 ]
|
os: [ ubuntu-20.04 ]
|
||||||
go: [ '^1.16' ]
|
go: [ '^1.17' ]
|
||||||
goos: [ linux ]
|
goos: [ linux ]
|
||||||
goarch: [ amd64, arm64 ]
|
goarch: [ amd64, arm64 ]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
@ -11,7 +11,7 @@ jobs:
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: '^1.16'
|
go-version: '^1.17'
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
REF: ${{ github.ref }}
|
REF: ${{ github.ref }}
|
||||||
|
|
40
CHANGELOG.md
40
CHANGELOG.md
|
@ -1,5 +1,45 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.5.0
|
||||||
|
### Fixed
|
||||||
|
* Fixes a race condition when setting the application name in the console output for a server.
|
||||||
|
* Fixes a server being reinstalled causing the `file_denylist` parameter for an Egg to be ignored until Wings is restarted.
|
||||||
|
* Fixes YAML file parser not correctly setting boolean values.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* Exposes `8080` in the default Docker setup to better support proxy tools.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* Releases are now built using `Go 1.17` — the minimum version required to build Wings remains `Go 1.16`.
|
||||||
|
* Simplifed the logic powering server updates to only pull information from the Panel rather than trying to accept updated values. All parts of Wings needing the most up-to-date server details should call `Server#Sync()` to fetch the latest stored build information.
|
||||||
|
* `Installer#New()` no longer requires passing all of the server data as a byte slice, rather a new `Installer#ServerDetails` struct is exposed which can be passed and accepts a UUID and if the server should be started after the installer finishes.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* Removes complicated (and unused) logic during the server installation process that was a hold-over from legacy Wings architectures.
|
||||||
|
* Removes the `PATCH /api/servers/:server` endpoint — if you were previously using this API call it should be replaced with `POST /api/servers/:server/sync`.
|
||||||
|
|
||||||
|
## v1.4.7
|
||||||
|
### Fixed
|
||||||
|
* SFTP access is now properly denied if a server is suspended.
|
||||||
|
* Correctly uses `start_on_completion` and `crash_detection_enabled` for servers.
|
||||||
|
|
||||||
|
## v1.4.6
|
||||||
|
### Fixed
|
||||||
|
* Environment variable starting with the same prefix no longer get merged into a single environment variable value (skipping all but the first).
|
||||||
|
* The `start_on_completion` flag for server installs will now properly start the server.
|
||||||
|
* Fixes socket files unintentionally causing backups to be aborted.
|
||||||
|
* Files extracted from a backup now have their preior mode properly set on the restored files, rather than defaulting to 0644.
|
||||||
|
* Fixes logrotate issues due to a bad user configuration on some systems.
|
||||||
|
|
||||||
|
### Updated
|
||||||
|
* The minimum Go version required to compile Wings is now `go1.16`.
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
> Both of these deprecations will be removed in `Wings@2.0.0`.
|
||||||
|
|
||||||
|
* The `Server.Id()` method has been deprecated in favor of `Server.ID()`.
|
||||||
|
* The `directory` field on the `/api/servers/:server/files/pull` endpoint is deprecated and should be updated to use `root` instead for consistency with other endpoints.
|
||||||
|
|
||||||
## v1.4.5
|
## v1.4.5
|
||||||
### Changed
|
### Changed
|
||||||
* Upped the process limit for a container from `256` to `512` in order to address edge-cases for some games that spawn a lot of processes.
|
* Upped the process limit for a container from `256` to `512` in order to address edge-cases for some games that spawn a lot of processes.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# Stage 1 (Build)
|
# Stage 1 (Build)
|
||||||
FROM --platform=$BUILDPLATFORM golang:1.16-alpine AS builder
|
FROM --platform=$BUILDPLATFORM golang:1.17-alpine AS builder
|
||||||
|
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
RUN apk add --update --no-cache git make upx
|
RUN apk add --update --no-cache git make upx
|
||||||
|
@ -19,5 +19,8 @@ RUN echo "ID=\"distroless\"" > /etc/os-release
|
||||||
# Stage 2 (Final)
|
# Stage 2 (Final)
|
||||||
FROM gcr.io/distroless/static:latest
|
FROM gcr.io/distroless/static:latest
|
||||||
COPY --from=builder /etc/os-release /etc/os-release
|
COPY --from=builder /etc/os-release /etc/os-release
|
||||||
|
|
||||||
COPY --from=builder /app/wings /usr/bin/
|
COPY --from=builder /app/wings /usr/bin/
|
||||||
CMD [ "/usr/bin/wings", "--config", "/etc/pterodactyl/config.yml" ]
|
CMD [ "/usr/bin/wings", "--config", "/etc/pterodactyl/config.yml" ]
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
|
@ -32,6 +32,9 @@ I would like to extend my sincere thanks to the following sponsors for helping f
|
||||||
| [**HostBend**](https://hostbend.com/) | HostBend offers a variety of solutions for developers, students, and others who have a tight budget but don't want to compromise quality and support. |
|
| [**HostBend**](https://hostbend.com/) | HostBend offers a variety of solutions for developers, students, and others who have a tight budget but don't want to compromise quality and support. |
|
||||||
| [**Capitol Hosting Solutions**](https://capitolsolutions.cloud/) | CHS is *the* budget friendly hosting company for Australian and American gamers, offering a variety of plans from Web Hosting to Game Servers; Custom Solutions too! |
|
| [**Capitol Hosting Solutions**](https://capitolsolutions.cloud/) | CHS is *the* budget friendly hosting company for Australian and American gamers, offering a variety of plans from Web Hosting to Game Servers; Custom Solutions too! |
|
||||||
| [**ByteAnia**](https://byteania.com/?utm_source=pterodactyl) | ByteAnia offers the best performing and most affordable **Ryzen 5000 Series hosting** on the market for *unbeatable prices*! |
|
| [**ByteAnia**](https://byteania.com/?utm_source=pterodactyl) | ByteAnia offers the best performing and most affordable **Ryzen 5000 Series hosting** on the market for *unbeatable prices*! |
|
||||||
|
| [**Aussie Server Hosts**](https://aussieserverhosts.com/) | No frills Australian Owned and operated High Performance Server hosting for some of the most demanding games serving Australia and New Zealand. |
|
||||||
|
| [**VibeGAMES**](https://vibegames.net/) | VibeGAMES is a game server provider that specializes in DDOS protection for the games we offer. We have multiple locations in the US, Brazil, France, Germany, Singapore, Australia and South Africa.|
|
||||||
|
| [**RocketNode**](https://rocketnode.net) | RocketNode is a VPS and Game Server provider that offers the best performing VPS and Game hosting Solutions at affordable prices! |
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
* [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html)
|
* [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html)
|
||||||
|
|
|
@ -2,72 +2,32 @@ package installer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
"emperror.dev/errors"
|
"emperror.dev/errors"
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
"github.com/buger/jsonparser"
|
|
||||||
|
|
||||||
"github.com/pterodactyl/wings/environment"
|
|
||||||
"github.com/pterodactyl/wings/remote"
|
"github.com/pterodactyl/wings/remote"
|
||||||
"github.com/pterodactyl/wings/server"
|
"github.com/pterodactyl/wings/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Installer struct {
|
type Installer struct {
|
||||||
server *server.Server
|
server *server.Server
|
||||||
|
StartOnCompletion bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerDetails struct {
|
||||||
|
UUID string `json:"uuid"`
|
||||||
|
StartOnCompletion bool `json:"start_on_completion"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// New validates the received data to ensure that all the required fields
|
// New validates the received data to ensure that all the required fields
|
||||||
// have been passed along in the request. This should be manually run before
|
// have been passed along in the request. This should be manually run before
|
||||||
// calling Execute().
|
// calling Execute().
|
||||||
func New(ctx context.Context, manager *server.Manager, data []byte) (*Installer, error) {
|
func New(ctx context.Context, manager *server.Manager, details ServerDetails) (*Installer, error) {
|
||||||
if !govalidator.IsUUIDv4(getString(data, "uuid")) {
|
if !govalidator.IsUUIDv4(details.UUID) {
|
||||||
return nil, NewValidationError("uuid provided was not in a valid format")
|
return nil, NewValidationError("uuid provided was not in a valid format")
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := &server.Configuration{
|
c, err := manager.Client().GetServerConfiguration(ctx, details.UUID)
|
||||||
Uuid: getString(data, "uuid"),
|
|
||||||
Suspended: false,
|
|
||||||
Invocation: getString(data, "invocation"),
|
|
||||||
SkipEggScripts: getBoolean(data, "skip_egg_scripts"),
|
|
||||||
StartOnCompletion: getBoolean(data, "start_on_completion"),
|
|
||||||
Build: environment.Limits{
|
|
||||||
MemoryLimit: getInt(data, "build", "memory"),
|
|
||||||
Swap: getInt(data, "build", "swap"),
|
|
||||||
IoWeight: uint16(getInt(data, "build", "io")),
|
|
||||||
CpuLimit: getInt(data, "build", "cpu"),
|
|
||||||
DiskSpace: getInt(data, "build", "disk"),
|
|
||||||
Threads: getString(data, "build", "threads"),
|
|
||||||
},
|
|
||||||
CrashDetectionEnabled: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.Allocations.DefaultMapping.Ip = getString(data, "allocations", "default", "ip")
|
|
||||||
cfg.Allocations.DefaultMapping.Port = int(getInt(data, "allocations", "default", "port"))
|
|
||||||
|
|
||||||
// Unmarshal the environment variables from the request into the server struct.
|
|
||||||
if b, _, _, err := jsonparser.Get(data, "environment"); err != nil {
|
|
||||||
return nil, errors.WithStackIf(err)
|
|
||||||
} else {
|
|
||||||
cfg.EnvVars = make(environment.Variables)
|
|
||||||
if err := json.Unmarshal(b, &cfg.EnvVars); err != nil {
|
|
||||||
return nil, errors.WrapIf(err, "installer: could not unmarshal environment variables for server")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal the allocation mappings from the request into the server struct.
|
|
||||||
if b, _, _, err := jsonparser.Get(data, "allocations", "mappings"); err != nil {
|
|
||||||
return nil, errors.WithStackIf(err)
|
|
||||||
} else {
|
|
||||||
cfg.Allocations.Mappings = make(map[string][]int)
|
|
||||||
if err := json.Unmarshal(b, &cfg.Allocations.Mappings); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "installer: could not unmarshal allocation mappings")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.Container.Image = getString(data, "container", "image")
|
|
||||||
|
|
||||||
c, err := manager.Client().GetServerConfiguration(ctx, cfg.Uuid)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !remote.IsRequestError(err) {
|
if !remote.IsRequestError(err) {
|
||||||
return nil, errors.WithStackIf(err)
|
return nil, errors.WithStackIf(err)
|
||||||
|
@ -81,35 +41,11 @@ func New(ctx context.Context, manager *server.Manager, data []byte) (*Installer,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WrapIf(err, "installer: could not init server instance")
|
return nil, errors.WrapIf(err, "installer: could not init server instance")
|
||||||
}
|
}
|
||||||
return &Installer{server: s}, nil
|
i := Installer{server: s, StartOnCompletion: details.StartOnCompletion}
|
||||||
}
|
return &i, nil
|
||||||
|
|
||||||
// Uuid returns the UUID associated with this installer instance.
|
|
||||||
func (i *Installer) Uuid() string {
|
|
||||||
return i.server.ID()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server returns the server instance.
|
// Server returns the server instance.
|
||||||
func (i *Installer) Server() *server.Server {
|
func (i *Installer) Server() *server.Server {
|
||||||
return i.server
|
return i.server
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a string value from the JSON data provided.
|
|
||||||
func getString(data []byte, key ...string) string {
|
|
||||||
value, _ := jsonparser.GetString(data, key...)
|
|
||||||
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns an int value from the JSON data provided.
|
|
||||||
func getInt(data []byte, key ...string) int64 {
|
|
||||||
value, _ := jsonparser.GetInt(data, key...)
|
|
||||||
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
func getBoolean(data []byte, key ...string) bool {
|
|
||||||
value, _ := jsonparser.GetBoolean(data, key...)
|
|
||||||
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
|
@ -48,19 +48,19 @@ func readFileBytes(path string) ([]byte, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 (cfr *ConfigurationFileReplacement) getKeyValue(value []byte) interface{} {
|
func (cfr *ConfigurationFileReplacement) getKeyValue(value string) interface{} {
|
||||||
if cfr.ReplaceWith.Type() == jsonparser.Boolean {
|
if cfr.ReplaceWith.Type() == jsonparser.Boolean {
|
||||||
v, _ := strconv.ParseBool(string(value))
|
v, _ := strconv.ParseBool(value)
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to parse into an int, if this fails just ignore the error and continue
|
// Try to parse into an int, if this fails just ignore the error and continue
|
||||||
// through, returning the string.
|
// through, returning the string.
|
||||||
if v, err := strconv.Atoi(string(value)); err == nil {
|
if v, err := strconv.Atoi(value); err == nil {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(value)
|
return 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
|
||||||
|
@ -97,22 +97,21 @@ 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 := v.SetAtPathway(child, strings.Trim(parts[1], "."), []byte(value)); err != nil {
|
if err := v.SetAtPathway(child, strings.Trim(parts[1], "."), value); err != nil {
|
||||||
if errors.Is(err, gabs.ErrNotFound) {
|
if errors.Is(err, gabs.ErrNotFound) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.WithMessage(err, "failed to set config value of array child")
|
return nil, errors.WithMessage(err, "failed to set config value of array child")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
continue
|
||||||
if err = v.SetAtPathway(parsed, v.Match, []byte(value)); err != nil {
|
}
|
||||||
if errors.Is(err, gabs.ErrNotFound) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, errors.WithMessage(err, "unable to set config value at pathway: "+v.Match)
|
if err := v.SetAtPathway(parsed, v.Match, value); err != nil {
|
||||||
|
if errors.Is(err, gabs.ErrNotFound) {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
return nil, errors.WithMessage(err, "unable to set config value at pathway: "+v.Match)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,13 +131,10 @@ func setValueAtPath(c *gabs.Container, path string, value interface{}) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
matches := checkForArrayElement.FindStringSubmatch(path)
|
matches := checkForArrayElement.FindStringSubmatch(path)
|
||||||
if len(matches) < 3 {
|
|
||||||
// Only update the value if the pathway actually exists in the configuration, otherwise
|
|
||||||
// do nothing.
|
|
||||||
if c.ExistsP(path) {
|
|
||||||
_, err = c.SetP(value, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Check if we are **NOT** updating an array element.
|
||||||
|
if len(matches) < 3 {
|
||||||
|
_, err = c.SetP(value, path)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,32 +192,34 @@ func setValueAtPath(c *gabs.Container, path string, value interface{}) error {
|
||||||
|
|
||||||
// Sets the value at a specific pathway, but checks if we were looking for a specific
|
// Sets the value at a specific pathway, but checks if we were looking for a specific
|
||||||
// value or not before doing it.
|
// value or not before doing it.
|
||||||
func (cfr *ConfigurationFileReplacement) SetAtPathway(c *gabs.Container, path string, value []byte) error {
|
func (cfr *ConfigurationFileReplacement) SetAtPathway(c *gabs.Container, path string, value string) error {
|
||||||
if cfr.IfValue == "" {
|
if cfr.IfValue == "" {
|
||||||
return setValueAtPath(c, path, cfr.getKeyValue(value))
|
return setValueAtPath(c, path, cfr.getKeyValue(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this is a regex based matching, we need to get a little more creative since
|
// Check if we are replacing instead of overwriting.
|
||||||
// we're only going to replacing part of the string, and not the whole thing.
|
if strings.HasPrefix(cfr.IfValue, "regex:") {
|
||||||
if c.ExistsP(path) && strings.HasPrefix(cfr.IfValue, "regex:") {
|
// Doing a regex replacement requires an existing value.
|
||||||
// We're doing some regex here.
|
// TODO: Do we try passing an empty string to the regex?
|
||||||
|
if c.ExistsP(path) {
|
||||||
|
return gabs.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
r, err := regexp.Compile(strings.TrimPrefix(cfr.IfValue, "regex:"))
|
r, err := regexp.Compile(strings.TrimPrefix(cfr.IfValue, "regex:"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields(log.Fields{"if_value": strings.TrimPrefix(cfr.IfValue, "regex:"), "error": err}).
|
log.WithFields(log.Fields{"if_value": strings.TrimPrefix(cfr.IfValue, "regex:"), "error": err}).
|
||||||
Warn("configuration if_value using invalid regexp, cannot perform replacement")
|
Warn("configuration if_value using invalid regexp, cannot perform replacement")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the path exists and there is a regex match, go ahead and attempt the replacement
|
v := strings.Trim(c.Path(path).String(), "\"")
|
||||||
// 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)) {
|
if r.Match([]byte(v)) {
|
||||||
return setValueAtPath(c, path, r.ReplaceAllString(v, string(value)))
|
return setValueAtPath(c, path, r.ReplaceAllString(v, value))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
} else if !c.ExistsP(path) || (c.ExistsP(path) && !bytes.Equal(c.Bytes(), []byte(cfr.IfValue))) {
|
}
|
||||||
|
|
||||||
|
if c.ExistsP(path) && !bytes.Equal(c.Bytes(), []byte(cfr.IfValue)) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,17 +57,22 @@ func (cv *ReplaceValue) Type() jsonparser.ValueType {
|
||||||
// handle casting the UTF-8 sequence into the expected value, switching something
|
// handle casting the UTF-8 sequence into the expected value, switching something
|
||||||
// like "\u00a7Foo" into "§Foo".
|
// like "\u00a7Foo" into "§Foo".
|
||||||
func (cv *ReplaceValue) String() string {
|
func (cv *ReplaceValue) String() string {
|
||||||
if cv.Type() != jsonparser.String {
|
switch cv.Type() {
|
||||||
if cv.Type() == jsonparser.Null {
|
case jsonparser.String:
|
||||||
return "<nil>"
|
str, err := jsonparser.ParseString(cv.value)
|
||||||
|
if err != nil {
|
||||||
|
panic(errors.Wrap(err, "parser: could not parse value"))
|
||||||
}
|
}
|
||||||
|
return str
|
||||||
|
case jsonparser.Null:
|
||||||
|
return "<nil>"
|
||||||
|
case jsonparser.Boolean:
|
||||||
|
return string(cv.value)
|
||||||
|
case jsonparser.Number:
|
||||||
|
return string(cv.value)
|
||||||
|
default:
|
||||||
return "<invalid>"
|
return "<invalid>"
|
||||||
}
|
}
|
||||||
str, err := jsonparser.ParseString(cv.value)
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.Wrap(err, "parser: could not parse value"))
|
|
||||||
}
|
|
||||||
return str
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigurationParser string
|
type ConfigurationParser string
|
||||||
|
|
|
@ -6,11 +6,11 @@ import (
|
||||||
|
|
||||||
"github.com/pterodactyl/wings/remote"
|
"github.com/pterodactyl/wings/remote"
|
||||||
"github.com/pterodactyl/wings/router/middleware"
|
"github.com/pterodactyl/wings/router/middleware"
|
||||||
"github.com/pterodactyl/wings/server"
|
wserver "github.com/pterodactyl/wings/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Configure configures the routing infrastructure for this daemon instance.
|
// Configure configures the routing infrastructure for this daemon instance.
|
||||||
func Configure(m *server.Manager, client remote.Client) *gin.Engine {
|
func Configure(m *wserver.Manager, client remote.Client) *gin.Engine {
|
||||||
gin.SetMode("release")
|
gin.SetMode("release")
|
||||||
|
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
|
@ -63,7 +63,6 @@ func Configure(m *server.Manager, client remote.Client) *gin.Engine {
|
||||||
server.Use(middleware.RequireAuthorization(), middleware.ServerExists())
|
server.Use(middleware.RequireAuthorization(), middleware.ServerExists())
|
||||||
{
|
{
|
||||||
server.GET("", getServer)
|
server.GET("", getServer)
|
||||||
server.PATCH("", patchServer)
|
|
||||||
server.DELETE("", deleteServer)
|
server.DELETE("", deleteServer)
|
||||||
|
|
||||||
server.GET("/logs", getServerLogs)
|
server.GET("/logs", getServerLogs)
|
||||||
|
@ -71,6 +70,7 @@ func Configure(m *server.Manager, client remote.Client) *gin.Engine {
|
||||||
server.POST("/commands", postServerCommands)
|
server.POST("/commands", postServerCommands)
|
||||||
server.POST("/install", postServerInstall)
|
server.POST("/install", postServerInstall)
|
||||||
server.POST("/reinstall", postServerReinstall)
|
server.POST("/reinstall", postServerReinstall)
|
||||||
|
server.POST("/sync", postServerSync)
|
||||||
server.POST("/ws/deny", postServerDenyWSTokens)
|
server.POST("/ws/deny", postServerDenyWSTokens)
|
||||||
|
|
||||||
// This archive request causes the archive to start being created
|
// This archive request causes the archive to start being created
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package router
|
package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
@ -10,7 +9,6 @@ import (
|
||||||
"emperror.dev/errors"
|
"emperror.dev/errors"
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/pterodactyl/wings/router/downloader"
|
"github.com/pterodactyl/wings/router/downloader"
|
||||||
"github.com/pterodactyl/wings/router/middleware"
|
"github.com/pterodactyl/wings/router/middleware"
|
||||||
"github.com/pterodactyl/wings/router/tokens"
|
"github.com/pterodactyl/wings/router/tokens"
|
||||||
|
@ -130,21 +128,18 @@ func postServerCommands(c *gin.Context) {
|
||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates information about a server internally.
|
// postServerSync will accept a POST request and trigger a re-sync of the given
|
||||||
func patchServer(c *gin.Context) {
|
// server against the Panel. This can be manually triggered when needed by an
|
||||||
|
// external system, or triggered by the Panel itself when modifications are made
|
||||||
|
// to the build of a server internally.
|
||||||
|
func postServerSync(c *gin.Context) {
|
||||||
s := ExtractServer(c)
|
s := ExtractServer(c)
|
||||||
|
|
||||||
buf := bytes.Buffer{}
|
if err := s.Sync(); err != nil {
|
||||||
buf.ReadFrom(c.Request.Body)
|
WithError(c, err)
|
||||||
|
} else {
|
||||||
if err := s.UpdateDataStructure(buf.Bytes()); err != nil {
|
c.Status(http.StatusNoContent)
|
||||||
NewServerError(err, s).Abort(c)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.SyncWithEnvironment()
|
|
||||||
|
|
||||||
c.Status(http.StatusNoContent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Performs a server installation in a background thread.
|
// Performs a server installation in a background thread.
|
||||||
|
|
|
@ -254,13 +254,20 @@ func getServerPullingFiles(c *gin.Context) {
|
||||||
func postServerPullRemoteFile(c *gin.Context) {
|
func postServerPullRemoteFile(c *gin.Context) {
|
||||||
s := ExtractServer(c)
|
s := ExtractServer(c)
|
||||||
var data struct {
|
var data struct {
|
||||||
RootPath string `binding:"required,omitempty" json:"root"`
|
// Deprecated
|
||||||
URL string `binding:"required" json:"url"`
|
Directory string `binding:"required_without=RootPath,omitempty" json:"directory"`
|
||||||
|
RootPath string `binding:"required_without=Directory,omitempty" json:"root"`
|
||||||
|
URL string `binding:"required" json:"url"`
|
||||||
}
|
}
|
||||||
if err := c.BindJSON(&data); err != nil {
|
if err := c.BindJSON(&data); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle the deprecated Directory field in the struct until it is removed.
|
||||||
|
if data.Directory != "" && data.RootPath == "" {
|
||||||
|
data.RootPath = data.Directory
|
||||||
|
}
|
||||||
|
|
||||||
u, err := url.Parse(data.URL)
|
u, err := url.Parse(data.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if e, ok := err.(*url.Error); ok {
|
if e, ok := err.(*url.Error); ok {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package router
|
package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -44,10 +43,13 @@ func getAllServers(c *gin.Context) {
|
||||||
// for it.
|
// for it.
|
||||||
func postCreateServer(c *gin.Context) {
|
func postCreateServer(c *gin.Context) {
|
||||||
manager := middleware.ExtractManager(c)
|
manager := middleware.ExtractManager(c)
|
||||||
buf := bytes.Buffer{}
|
|
||||||
buf.ReadFrom(c.Request.Body)
|
|
||||||
|
|
||||||
install, err := installer.New(c.Request.Context(), manager, buf.Bytes())
|
details := installer.ServerDetails{}
|
||||||
|
if err := c.BindJSON(&details); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
install, err := installer.New(c.Request.Context(), manager, details)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if installer.IsValidationError(err) {
|
if installer.IsValidationError(err) {
|
||||||
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
|
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
|
||||||
|
@ -74,24 +76,21 @@ func postCreateServer(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := i.Server().Install(false); err != nil {
|
if err := i.Server().Install(false); err != nil {
|
||||||
log.WithFields(log.Fields{"server": i.Uuid(), "error": err}).Error("failed to run install process for server")
|
log.WithFields(log.Fields{"server": i.Server().ID(), "error": err}).Error("failed to run install process for server")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if i.Server().Config().StartOnCompletion {
|
if i.StartOnCompletion {
|
||||||
log.WithField("server_id", i.Server().ID()).Debug("starting server after successful installation")
|
log.WithField("server_id", i.Server().ID()).Debug("starting server after successful installation")
|
||||||
if err := i.Server().HandlePowerAction(server.PowerActionStart, 30); err != nil {
|
if err := i.Server().HandlePowerAction(server.PowerActionStart, 30); err != nil {
|
||||||
if errors.Is(err, context.DeadlineExceeded) {
|
if errors.Is(err, context.DeadlineExceeded) {
|
||||||
log.WithFields(log.Fields{"server_id": i.Server().ID(), "action": "start"}).
|
log.WithFields(log.Fields{"server_id": i.Server().ID(), "action": "start"}).Warn("could not acquire a lock while attempting to perform a power action")
|
||||||
Warn("could not acquire a lock while attempting to perform a power action")
|
|
||||||
} else {
|
} else {
|
||||||
log.WithFields(log.Fields{"server_id": i.Server().ID(), "action": "start", "error": err}).
|
log.WithFields(log.Fields{"server_id": i.Server().ID(), "action": "start", "error": err}).Error("encountered error processing a server power action in the background")
|
||||||
Error("encountered error processing a server power action in the background")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.WithField("server_id", i.Server().ID()).
|
log.WithField("server_id", i.Server().ID()).Debug("skipping automatic start after successful server installation")
|
||||||
Debug("skipping automatic start after successful server installation")
|
|
||||||
}
|
}
|
||||||
}(install)
|
}(install)
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -47,10 +46,10 @@ type downloadProgress struct {
|
||||||
|
|
||||||
// Data passed over to initiate a server transfer.
|
// Data passed over to initiate a server transfer.
|
||||||
type serverTransferRequest struct {
|
type serverTransferRequest struct {
|
||||||
ServerID string `binding:"required" json:"server_id"`
|
ServerID string `binding:"required" json:"server_id"`
|
||||||
URL string `binding:"required" json:"url"`
|
URL string `binding:"required" json:"url"`
|
||||||
Token string `binding:"required" json:"token"`
|
Token string `binding:"required" json:"token"`
|
||||||
Server json.RawMessage `json:"server"`
|
Server installer.ServerDetails `json:"server"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func getArchivePath(sID string) string {
|
func getArchivePath(sID string) string {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"emperror.dev/errors"
|
"emperror.dev/errors"
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
|
@ -152,12 +153,15 @@ func (s *Server) RestoreBackup(b backup.BackupInterface, reader io.ReadCloser) (
|
||||||
// Attempt to restore the backup to the server by running through each entry
|
// Attempt to restore the backup to the server by running through each entry
|
||||||
// in the file one at a time and writing them to the disk.
|
// in the file one at a time and writing them to the disk.
|
||||||
s.Log().Debug("starting file writing process for backup restoration")
|
s.Log().Debug("starting file writing process for backup restoration")
|
||||||
err = b.Restore(s.Context(), reader, func(file string, r io.Reader, mode fs.FileMode) error {
|
err = b.Restore(s.Context(), reader, func(file string, r io.Reader, mode fs.FileMode, atime, mtime time.Time) error {
|
||||||
s.Events().Publish(DaemonMessageEvent, "(restoring): "+file)
|
s.Events().Publish(DaemonMessageEvent, "(restoring): "+file)
|
||||||
if err := s.Filesystem().Writefile(file, r); err != nil {
|
if err := s.Filesystem().Writefile(file, r); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return s.Filesystem().Chmod(file, mode)
|
if err := s.Filesystem().Chmod(file, mode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.Filesystem().Chtimes(file, atime, mtime)
|
||||||
})
|
})
|
||||||
|
|
||||||
return errors.WithStackIf(err)
|
return errors.WithStackIf(err)
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"time"
|
||||||
|
|
||||||
"emperror.dev/errors"
|
"emperror.dev/errors"
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
|
@ -26,7 +27,7 @@ const (
|
||||||
|
|
||||||
// RestoreCallback is a generic restoration callback that exists for both local
|
// RestoreCallback is a generic restoration callback that exists for both local
|
||||||
// and remote backups allowing the files to be restored.
|
// and remote backups allowing the files to be restored.
|
||||||
type RestoreCallback func(file string, r io.Reader, mode fs.FileMode) error
|
type RestoreCallback func(file string, r io.Reader, mode fs.FileMode, atime, mtime time.Time) error
|
||||||
|
|
||||||
// noinspection GoNameStartsWithPackageName
|
// noinspection GoNameStartsWithPackageName
|
||||||
type BackupInterface interface {
|
type BackupInterface interface {
|
||||||
|
|
|
@ -88,7 +88,7 @@ func (b *LocalBackup) Restore(ctx context.Context, _ io.Reader, callback Restore
|
||||||
if f.IsDir() {
|
if f.IsDir() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return callback(filesystem.ExtractNameFromArchive(f), f, f.Mode())
|
return callback(filesystem.ExtractNameFromArchive(f), f, f.Mode(), f.ModTime(), f.ModTime())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,7 +116,7 @@ func (s *S3Backup) Restore(ctx context.Context, r io.Reader, callback RestoreCal
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if header.Typeflag == tar.TypeReg {
|
if header.Typeflag == tar.TypeReg {
|
||||||
if err := callback(header.Name, tr, header.FileInfo().Mode()); err != nil {
|
if err := callback(header.Name, tr, header.FileInfo().Mode(), header.AccessTime, header.ModTime); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,8 +35,6 @@ type Configuration struct {
|
||||||
// server, specific installation scripts will be skipped for the server process.
|
// server, specific installation scripts will be skipped for the server process.
|
||||||
SkipEggScripts bool `json:"skip_egg_scripts"`
|
SkipEggScripts bool `json:"skip_egg_scripts"`
|
||||||
|
|
||||||
StartOnCompletion bool `json:"start_on_completion"`
|
|
||||||
|
|
||||||
// An array of environment variables that should be passed along to the running
|
// An array of environment variables that should be passed along to the running
|
||||||
// server process.
|
// server process.
|
||||||
EnvVars environment.Variables `json:"environment"`
|
EnvVars environment.Variables `json:"environment"`
|
||||||
|
|
|
@ -19,6 +19,8 @@ import (
|
||||||
// a server.
|
// a server.
|
||||||
var appName string
|
var appName string
|
||||||
|
|
||||||
|
var appNameSync sync.Once
|
||||||
|
|
||||||
var ErrTooMuchConsoleData = errors.New("console is outputting too much data")
|
var ErrTooMuchConsoleData = errors.New("console is outputting too much data")
|
||||||
|
|
||||||
type ConsoleThrottler struct {
|
type ConsoleThrottler struct {
|
||||||
|
@ -131,9 +133,9 @@ func (s *Server) Throttler() *ConsoleThrottler {
|
||||||
// PublishConsoleOutputFromDaemon sends output to the server console formatted
|
// PublishConsoleOutputFromDaemon sends output to the server console formatted
|
||||||
// to appear correctly as being sent from Wings.
|
// to appear correctly as being sent from Wings.
|
||||||
func (s *Server) PublishConsoleOutputFromDaemon(data string) {
|
func (s *Server) PublishConsoleOutputFromDaemon(data string) {
|
||||||
if appName == "" {
|
appNameSync.Do(func() {
|
||||||
appName = config.Get().AppName
|
appName = config.Get().AppName
|
||||||
}
|
})
|
||||||
s.Events().Publish(
|
s.Events().Publish(
|
||||||
ConsoleOutputEvent,
|
ConsoleOutputEvent,
|
||||||
colorstring.Color(fmt.Sprintf("[yellow][bold][%s Daemon]:[default] %s", appName, data)),
|
colorstring.Color(fmt.Sprintf("[yellow][bold][%s Daemon]:[default] %s", appName, data)),
|
||||||
|
|
|
@ -136,6 +136,10 @@ func (fs *Filesystem) DecompressFile(dir string, file string) error {
|
||||||
if err := fs.Chmod(p, f.Mode()); err != nil {
|
if err := fs.Chmod(p, f.Mode()); err != nil {
|
||||||
return wrapError(err, source)
|
return wrapError(err, source)
|
||||||
}
|
}
|
||||||
|
// Update the file modification time to the one set in the archive.
|
||||||
|
if err := fs.Chtimes(p, f.ModTime(), f.ModTime()); err != nil {
|
||||||
|
return wrapError(err, source)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -528,3 +528,20 @@ func (fs *Filesystem) ListDirectory(p string) ([]Stat, error) {
|
||||||
|
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fs *Filesystem) Chtimes(path string, atime, mtime time.Time) error {
|
||||||
|
cleaned, err := fs.SafePath(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if fs.isTest {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Chtimes(cleaned, atime, mtime); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -172,8 +172,11 @@ func (m *Manager) InitServer(data remote.ServerConfigurationResponse) (*Server,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := s.UpdateDataStructure(data.Settings); err != nil {
|
|
||||||
return nil, err
|
// Setup the base server configuration data which will be used for all of the
|
||||||
|
// remaining functionality in this call.
|
||||||
|
if err := s.SyncWithConfiguration(data); err != nil {
|
||||||
|
return nil, errors.WithStackIf(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.fs = filesystem.New(filepath.Join(config.Get().System.Data, s.ID()), s.DiskSpace(), s.Config().Egg.FileDenylist)
|
s.fs = filesystem.New(filepath.Join(config.Get().System.Data, s.ID()), s.DiskSpace(), s.Config().Egg.FileDenylist)
|
||||||
|
@ -200,11 +203,6 @@ func (m *Manager) InitServer(data remote.ServerConfigurationResponse) (*Server,
|
||||||
s.Throttler().StartTimer(s.Context())
|
s.Throttler().StartTimer(s.Context())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forces the configuration to be synced with the panel.
|
|
||||||
if err := s.SyncWithConfiguration(data); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the server's data directory exists, force disk usage calculation.
|
// If the server's data directory exists, force disk usage calculation.
|
||||||
if _, err := os.Stat(s.Filesystem().Path()); err == nil {
|
if _, err := os.Stat(s.Filesystem().Path()); err == nil {
|
||||||
s.Filesystem().HasSpaceAvailable(true)
|
s.Filesystem().HasSpaceAvailable(true)
|
||||||
|
|
|
@ -2,6 +2,7 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
@ -15,7 +16,6 @@ import (
|
||||||
|
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"github.com/pterodactyl/wings/environment"
|
"github.com/pterodactyl/wings/environment"
|
||||||
"github.com/pterodactyl/wings/environment/docker"
|
|
||||||
"github.com/pterodactyl/wings/events"
|
"github.com/pterodactyl/wings/events"
|
||||||
"github.com/pterodactyl/wings/remote"
|
"github.com/pterodactyl/wings/remote"
|
||||||
"github.com/pterodactyl/wings/server/filesystem"
|
"github.com/pterodactyl/wings/server/filesystem"
|
||||||
|
@ -99,6 +99,14 @@ func (s *Server) ID() string {
|
||||||
return s.Config().GetUuid()
|
return s.Config().GetUuid()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Id returns the UUID for the server instance. This function is deprecated
|
||||||
|
// in favor of Server.ID().
|
||||||
|
//
|
||||||
|
// Deprecated
|
||||||
|
func (s *Server) Id() string {
|
||||||
|
return s.ID()
|
||||||
|
}
|
||||||
|
|
||||||
// Cancels the context assigned to this server instance. Assuming background tasks
|
// Cancels the context assigned to this server instance. Assuming background tasks
|
||||||
// are using this server's context for things, all of the background tasks will be
|
// are using this server's context for things, all of the background tasks will be
|
||||||
// stopped as a result.
|
// stopped as a result.
|
||||||
|
@ -159,31 +167,48 @@ func (s *Server) Sync() error {
|
||||||
}
|
}
|
||||||
return errors.WithStackIf(err)
|
return errors.WithStackIf(err)
|
||||||
}
|
}
|
||||||
return s.SyncWithConfiguration(cfg)
|
|
||||||
|
if err := s.SyncWithConfiguration(cfg); err != nil {
|
||||||
|
return errors.WithStackIf(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the disk space limits for the server whenever the configuration for
|
||||||
|
// it changes.
|
||||||
|
s.fs.SetDiskLimit(s.DiskSpace())
|
||||||
|
|
||||||
|
s.SyncWithEnvironment()
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SyncWithConfiguration accepts a configuration object for a server and will
|
||||||
|
// sync all of the values with the existing server state. This only replaces the
|
||||||
|
// existing configuration and process configuration for the server. The
|
||||||
|
// underlying environment will not be affected. This is because this function
|
||||||
|
// can be called from scoped where the server may not be fully initialized,
|
||||||
|
// therefore other things like the filesystem and environment may not exist yet.
|
||||||
func (s *Server) SyncWithConfiguration(cfg remote.ServerConfigurationResponse) error {
|
func (s *Server) SyncWithConfiguration(cfg remote.ServerConfigurationResponse) error {
|
||||||
// Update the data structure and persist it to the disk.
|
c := Configuration{}
|
||||||
if err := s.UpdateDataStructure(cfg.Settings); err != nil {
|
if err := json.Unmarshal(cfg.Settings, &c); err != nil {
|
||||||
return err
|
return errors.WithStackIf(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.cfg.mu.Lock()
|
||||||
|
defer s.cfg.mu.Unlock()
|
||||||
|
|
||||||
|
// Lock the new configuration. Since we have the defered Unlock above we need
|
||||||
|
// to make sure that the NEW configuration object is already locked since that
|
||||||
|
// defer is running on the memory address for "s.cfg.mu" which we're explcitly
|
||||||
|
// changing on the next line.
|
||||||
|
c.mu.Lock()
|
||||||
|
|
||||||
|
//goland:noinspection GoVetCopyLock
|
||||||
|
s.cfg = c
|
||||||
|
|
||||||
s.Lock()
|
s.Lock()
|
||||||
s.procConfig = cfg.ProcessConfiguration
|
s.procConfig = cfg.ProcessConfiguration
|
||||||
s.Unlock()
|
s.Unlock()
|
||||||
|
|
||||||
// Update the disk space limits for the server whenever the configuration
|
|
||||||
// for it changes.
|
|
||||||
s.fs.SetDiskLimit(s.DiskSpace())
|
|
||||||
|
|
||||||
// If this is a Docker environment we need to sync the stop configuration with it so that
|
|
||||||
// the process isn't just terminated when a user requests it be stopped.
|
|
||||||
if e, ok := s.Environment.(*docker.Environment); ok {
|
|
||||||
s.Log().Debug("syncing stop configuration with configured docker environment")
|
|
||||||
e.SetImage(s.Config().Container.Image)
|
|
||||||
e.SetStopConfiguration(cfg.ProcessConfiguration.Stop)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
131
server/update.go
131
server/update.go
|
@ -1,128 +1,41 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"github.com/pterodactyl/wings/environment/docker"
|
||||||
|
|
||||||
"emperror.dev/errors"
|
|
||||||
"github.com/buger/jsonparser"
|
|
||||||
"github.com/imdario/mergo"
|
|
||||||
|
|
||||||
"github.com/pterodactyl/wings/environment"
|
"github.com/pterodactyl/wings/environment"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UpdateDataStructure merges data passed through in JSON form into the existing
|
// SyncWithEnvironment updates the environment for the server to match any of
|
||||||
// server object. Any changes to the build settings will apply immediately in
|
// the changed data. This pushes new settings and environment variables to the
|
||||||
// the environment if the environment supports it.
|
// environment. In addition, the in-situ update method is called on the
|
||||||
|
// environment which will allow environments that make use of it (such as Docker)
|
||||||
|
// to immediately apply some settings without having to wait on a server to
|
||||||
|
// restart.
|
||||||
//
|
//
|
||||||
// The server will be marked as requiring a rebuild on the next boot sequence,
|
// This functionality allows a server's resources limits to be modified on the
|
||||||
// it is up to the specific environment to determine what needs to happen when
|
// fly and have them apply right away allowing for dynamic resource allocation
|
||||||
// that is the case.
|
// and responses to abusive server processes.
|
||||||
func (s *Server) UpdateDataStructure(data []byte) error {
|
|
||||||
src := new(Configuration)
|
|
||||||
if err := json.Unmarshal(data, src); err != nil {
|
|
||||||
return errors.Wrap(err, "server/update: could not unmarshal source data into Configuration struct")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// instance into a totally different one, which would be bad.
|
|
||||||
if src.Uuid != "" && s.ID() != "" && src.Uuid != s.ID() {
|
|
||||||
return errors.New("server/update: attempting to merge a data stack with an invalid UUID")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grab a copy of the configuration to work on.
|
|
||||||
c := *s.Config()
|
|
||||||
|
|
||||||
// Lock our copy of the configuration since the deferred unlock will end up acting upon this
|
|
||||||
// new memory address rather than the old one. If we don't lock this, the deferred unlock will
|
|
||||||
// cause a panic when it goes to run. However, since we only update s.cfg at the end, if there
|
|
||||||
// is an error before that point we'll still properly unlock the original configuration for the
|
|
||||||
// server.
|
|
||||||
c.mu.Lock()
|
|
||||||
|
|
||||||
// Lock the server configuration while we're doing this merge to avoid anything
|
|
||||||
// trying to overwrite it or make modifications while we're sorting out what we
|
|
||||||
// need to do.
|
|
||||||
s.cfg.mu.Lock()
|
|
||||||
defer s.cfg.mu.Unlock()
|
|
||||||
|
|
||||||
// Merge the new data object that we have received with the existing server data object
|
|
||||||
// and then save it to the disk so it is persistent.
|
|
||||||
if err := mergo.Merge(&c, src, mergo.WithOverride); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't explode if we're setting CPU limits to 0. Mergo sees that as an empty value
|
|
||||||
// so it won't override the value we've passed through in the API call. However, we can
|
|
||||||
// safely assume that we're passing through valid data structures here. I foresee this
|
|
||||||
// backfiring at some point, but until then...
|
|
||||||
c.Build = src.Build
|
|
||||||
|
|
||||||
// Mergo can't quite handle this boolean value correctly, so for now we'll just
|
|
||||||
// handle this edge case manually since none of the other data passed through in this
|
|
||||||
// request is going to be boolean. Allegedly.
|
|
||||||
if v, err := jsonparser.GetBoolean(data, "container", "oom_disabled"); err != nil {
|
|
||||||
if err != jsonparser.KeyPathNotFoundError {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
c.Build.OOMDisabled = v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mergo also cannot handle this boolean value.
|
|
||||||
if v, err := jsonparser.GetBoolean(data, "suspended"); err != nil {
|
|
||||||
if err != jsonparser.KeyPathNotFoundError {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
c.Suspended = v
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, err := jsonparser.GetBoolean(data, "skip_egg_scripts"); err != nil {
|
|
||||||
if err != jsonparser.KeyPathNotFoundError {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
c.SkipEggScripts = v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Environment and Mappings should be treated as a full update at all times, never a
|
|
||||||
// true patch, otherwise we can't know what we're passing along.
|
|
||||||
if src.EnvVars != nil && len(src.EnvVars) > 0 {
|
|
||||||
c.EnvVars = src.EnvVars
|
|
||||||
}
|
|
||||||
|
|
||||||
if src.Allocations.Mappings != nil && len(src.Allocations.Mappings) > 0 {
|
|
||||||
c.Allocations.Mappings = src.Allocations.Mappings
|
|
||||||
}
|
|
||||||
|
|
||||||
if src.Mounts != nil && len(src.Mounts) > 0 {
|
|
||||||
c.Mounts = src.Mounts
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the configuration once we have a lock on the configuration object.
|
|
||||||
s.cfg = c
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Updates the environment for the server to match any of the changed data. This pushes new settings and
|
|
||||||
// environment variables to the environment. In addition, the in-situ update method is called on the
|
|
||||||
// environment which will allow environments that make use of it (such as Docker) to immediately apply
|
|
||||||
// some settings without having to wait on a server to restart.
|
|
||||||
//
|
|
||||||
// This functionality allows a server's resources limits to be modified on the fly and have them apply
|
|
||||||
// right away allowing for dynamic resource allocation and responses to abusive server processes.
|
|
||||||
func (s *Server) SyncWithEnvironment() {
|
func (s *Server) SyncWithEnvironment() {
|
||||||
s.Log().Debug("syncing server settings with environment")
|
s.Log().Debug("syncing server settings with environment")
|
||||||
|
|
||||||
|
cfg := s.Config()
|
||||||
|
|
||||||
// Update the environment settings using the new information from this server.
|
// Update the environment settings using the new information from this server.
|
||||||
s.Environment.Config().SetSettings(environment.Settings{
|
s.Environment.Config().SetSettings(environment.Settings{
|
||||||
Mounts: s.Mounts(),
|
Mounts: s.Mounts(),
|
||||||
Allocations: s.Config().Allocations,
|
Allocations: cfg.Allocations,
|
||||||
Limits: s.Config().Build,
|
Limits: cfg.Build,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// For Docker specific environments we also want to update the configured image
|
||||||
|
// and stop configuration.
|
||||||
|
if e, ok := s.Environment.(*docker.Environment); ok {
|
||||||
|
s.Log().Debug("syncing stop configuration with configured docker environment")
|
||||||
|
e.SetImage(cfg.Container.Image)
|
||||||
|
e.SetStopConfiguration(s.ProcessConfiguration().Stop)
|
||||||
|
}
|
||||||
|
|
||||||
// If build limits are changed, environment variables also change. Plus, any modifications to
|
// If build limits are changed, environment variables also change. Plus, any modifications to
|
||||||
// the startup command also need to be properly propagated to this environment.
|
// the startup command also need to be properly propagated to this environment.
|
||||||
//
|
//
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
|
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"github.com/pterodactyl/wings/server"
|
||||||
"github.com/pterodactyl/wings/server/filesystem"
|
"github.com/pterodactyl/wings/server/filesystem"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -26,8 +27,10 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
|
||||||
permissions []string
|
permissions []string
|
||||||
mu sync.Mutex
|
server *server.Server
|
||||||
fs *filesystem.Filesystem
|
fs *filesystem.Filesystem
|
||||||
logger *log.Entry
|
logger *log.Entry
|
||||||
ro bool
|
ro bool
|
||||||
|
@ -35,11 +38,12 @@ type Handler struct {
|
||||||
|
|
||||||
// Returns a new connection handler for the SFTP server. This allows a given user
|
// Returns a new connection handler for the SFTP server. This allows a given user
|
||||||
// to access the underlying filesystem.
|
// to access the underlying filesystem.
|
||||||
func NewHandler(sc *ssh.ServerConn, fs *filesystem.Filesystem) *Handler {
|
func NewHandler(sc *ssh.ServerConn, srv *server.Server) *Handler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
fs: fs,
|
|
||||||
ro: config.Get().System.Sftp.ReadOnly,
|
|
||||||
permissions: strings.Split(sc.Permissions.Extensions["permissions"], ","),
|
permissions: strings.Split(sc.Permissions.Extensions["permissions"], ","),
|
||||||
|
server: srv,
|
||||||
|
fs: srv.Filesystem(),
|
||||||
|
ro: config.Get().System.Sftp.ReadOnly,
|
||||||
logger: log.WithFields(log.Fields{
|
logger: log.WithFields(log.Fields{
|
||||||
"subsystem": "sftp",
|
"subsystem": "sftp",
|
||||||
"username": sc.User(),
|
"username": sc.User(),
|
||||||
|
@ -278,6 +282,10 @@ func (h *Handler) Filelist(request *sftp.Request) (sftp.ListerAt, error) {
|
||||||
// Determines if a user has permission to perform a specific action on the SFTP server. These
|
// Determines if a user has permission to perform a specific action on the SFTP server. These
|
||||||
// permissions are defined and returned by the Panel API.
|
// permissions are defined and returned by the Panel API.
|
||||||
func (h *Handler) can(permission string) bool {
|
func (h *Handler) can(permission string) bool {
|
||||||
|
if h.server.IsSuspended() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// SFTPServer owners and super admins have their permissions returned as '[*]' via the Panel
|
// SFTPServer owners and super admins have their permissions returned as '[*]' via the Panel
|
||||||
// API, so for the sake of speed do an initial check for that before iterating over the
|
// API, so for the sake of speed do an initial check for that before iterating over the
|
||||||
// entire array of permissions.
|
// entire array of permissions.
|
||||||
|
|
|
@ -139,7 +139,7 @@ func (c *SFTPServer) AcceptInbound(conn net.Conn, config *ssh.ServerConfig) {
|
||||||
|
|
||||||
// Spin up a SFTP server instance for the authenticated user's server allowing
|
// Spin up a SFTP server instance for the authenticated user's server allowing
|
||||||
// them access to the underlying filesystem.
|
// them access to the underlying filesystem.
|
||||||
handler := sftp.NewRequestServer(channel, NewHandler(sconn, srv.Filesystem()).Handlers())
|
handler := sftp.NewRequestServer(channel, NewHandler(sconn, srv).Handlers())
|
||||||
if err := handler.Serve(); err == io.EOF {
|
if err := handler.Serve(); err == io.EOF {
|
||||||
_ = handler.Close()
|
_ = handler.Close()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user