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
 | 
			
		||||
      matrix:
 | 
			
		||||
        os: [ ubuntu-20.04 ]
 | 
			
		||||
        go: [ '^1.16' ]
 | 
			
		||||
        go: [ '^1.17' ]
 | 
			
		||||
        goos: [ linux ]
 | 
			
		||||
        goarch: [ amd64, arm64 ]
 | 
			
		||||
    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/setup-go@v2
 | 
			
		||||
        with:
 | 
			
		||||
          go-version: '^1.16'
 | 
			
		||||
          go-version: '^1.17'
 | 
			
		||||
      - name: Build
 | 
			
		||||
        env:
 | 
			
		||||
          REF: ${{ github.ref }}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										40
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -1,5 +1,45 @@
 | 
			
		|||
# 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
 | 
			
		||||
### 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.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
# Stage 1 (Build)
 | 
			
		||||
FROM --platform=$BUILDPLATFORM golang:1.16-alpine AS builder
 | 
			
		||||
FROM --platform=$BUILDPLATFORM golang:1.17-alpine AS builder
 | 
			
		||||
 | 
			
		||||
ARG VERSION
 | 
			
		||||
RUN apk add --update --no-cache git make upx
 | 
			
		||||
| 
						 | 
				
			
			@ -19,5 +19,8 @@ RUN echo "ID=\"distroless\"" > /etc/os-release
 | 
			
		|||
# Stage 2 (Final)
 | 
			
		||||
FROM gcr.io/distroless/static:latest
 | 
			
		||||
COPY --from=builder /etc/os-release /etc/os-release
 | 
			
		||||
 | 
			
		||||
COPY --from=builder /app/wings /usr/bin/
 | 
			
		||||
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. |
 | 
			
		||||
| [**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*! |
 | 
			
		||||
| [**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
 | 
			
		||||
* [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,72 +2,32 @@ package installer
 | 
			
		|||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
 | 
			
		||||
	"emperror.dev/errors"
 | 
			
		||||
	"github.com/asaskevich/govalidator"
 | 
			
		||||
	"github.com/buger/jsonparser"
 | 
			
		||||
 | 
			
		||||
	"github.com/pterodactyl/wings/environment"
 | 
			
		||||
	"github.com/pterodactyl/wings/remote"
 | 
			
		||||
	"github.com/pterodactyl/wings/server"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Installer struct {
 | 
			
		||||
	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
 | 
			
		||||
// have been passed along in the request. This should be manually run before
 | 
			
		||||
// calling Execute().
 | 
			
		||||
func New(ctx context.Context, manager *server.Manager, data []byte) (*Installer, error) {
 | 
			
		||||
	if !govalidator.IsUUIDv4(getString(data, "uuid")) {
 | 
			
		||||
func New(ctx context.Context, manager *server.Manager, details ServerDetails) (*Installer, error) {
 | 
			
		||||
	if !govalidator.IsUUIDv4(details.UUID) {
 | 
			
		||||
		return nil, NewValidationError("uuid provided was not in a valid format")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cfg := &server.Configuration{
 | 
			
		||||
		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)
 | 
			
		||||
	c, err := manager.Client().GetServerConfiguration(ctx, details.UUID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if !remote.IsRequestError(err) {
 | 
			
		||||
			return nil, errors.WithStackIf(err)
 | 
			
		||||
| 
						 | 
				
			
			@ -81,35 +41,11 @@ func New(ctx context.Context, manager *server.Manager, data []byte) (*Installer,
 | 
			
		|||
	if err != nil {
 | 
			
		||||
		return nil, errors.WrapIf(err, "installer: could not init server instance")
 | 
			
		||||
	}
 | 
			
		||||
	return &Installer{server: s}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Uuid returns the UUID associated with this installer instance.
 | 
			
		||||
func (i *Installer) Uuid() string {
 | 
			
		||||
	return i.server.ID()
 | 
			
		||||
	i := Installer{server: s, StartOnCompletion: details.StartOnCompletion}
 | 
			
		||||
	return &i, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Server returns the server instance.
 | 
			
		||||
func (i *Installer) Server() *server.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.
 | 
			
		||||
func (cfr *ConfigurationFileReplacement) getKeyValue(value []byte) interface{} {
 | 
			
		||||
func (cfr *ConfigurationFileReplacement) getKeyValue(value string) interface{} {
 | 
			
		||||
	if cfr.ReplaceWith.Type() == jsonparser.Boolean {
 | 
			
		||||
		v, _ := strconv.ParseBool(string(value))
 | 
			
		||||
		v, _ := strconv.ParseBool(value)
 | 
			
		||||
		return v
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Try to parse into an int, if this fails just ignore the error and continue
 | 
			
		||||
	// through, returning the string.
 | 
			
		||||
	if v, err := strconv.Atoi(string(value)); err == nil {
 | 
			
		||||
	if v, err := strconv.Atoi(value); err == nil {
 | 
			
		||||
		return v
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return string(value)
 | 
			
		||||
	return value
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
			// time this code is being written.
 | 
			
		||||
			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) {
 | 
			
		||||
						continue
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					return nil, errors.WithMessage(err, "failed to set config value of array child")
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			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
 | 
			
		||||
 | 
			
		||||
	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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
// 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 == "" {
 | 
			
		||||
		return setValueAtPath(c, path, cfr.getKeyValue(value))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 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.ExistsP(path) && strings.HasPrefix(cfr.IfValue, "regex:") {
 | 
			
		||||
		// We're doing some regex here.
 | 
			
		||||
	// Check if we are replacing instead of overwriting.
 | 
			
		||||
	if strings.HasPrefix(cfr.IfValue, "regex:") {
 | 
			
		||||
		// Doing a regex replacement requires an existing value.
 | 
			
		||||
		// 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:"))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.WithFields(log.Fields{"if_value": strings.TrimPrefix(cfr.IfValue, "regex:"), "error": err}).
 | 
			
		||||
				Warn("configuration if_value using invalid regexp, cannot perform replacement")
 | 
			
		||||
 | 
			
		||||
			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()), "\"")
 | 
			
		||||
		v := strings.Trim(c.Path(path).String(), "\"")
 | 
			
		||||
		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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -57,17 +57,22 @@ func (cv *ReplaceValue) Type() jsonparser.ValueType {
 | 
			
		|||
// handle casting the UTF-8 sequence into the expected value, switching something
 | 
			
		||||
// like "\u00a7Foo" into "§Foo".
 | 
			
		||||
func (cv *ReplaceValue) String() string {
 | 
			
		||||
	if cv.Type() != jsonparser.String {
 | 
			
		||||
		if cv.Type() == jsonparser.Null {
 | 
			
		||||
			return "<nil>"
 | 
			
		||||
		}
 | 
			
		||||
		return "<invalid>"
 | 
			
		||||
	}
 | 
			
		||||
	switch cv.Type() {
 | 
			
		||||
	case jsonparser.String:
 | 
			
		||||
		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>"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ConfigurationParser string
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,11 +6,11 @@ import (
 | 
			
		|||
 | 
			
		||||
	"github.com/pterodactyl/wings/remote"
 | 
			
		||||
	"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.
 | 
			
		||||
func Configure(m *server.Manager, client remote.Client) *gin.Engine {
 | 
			
		||||
func Configure(m *wserver.Manager, client remote.Client) *gin.Engine {
 | 
			
		||||
	gin.SetMode("release")
 | 
			
		||||
 | 
			
		||||
	router := gin.New()
 | 
			
		||||
| 
						 | 
				
			
			@ -63,7 +63,6 @@ func Configure(m *server.Manager, client remote.Client) *gin.Engine {
 | 
			
		|||
	server.Use(middleware.RequireAuthorization(), middleware.ServerExists())
 | 
			
		||||
	{
 | 
			
		||||
		server.GET("", getServer)
 | 
			
		||||
		server.PATCH("", patchServer)
 | 
			
		||||
		server.DELETE("", deleteServer)
 | 
			
		||||
 | 
			
		||||
		server.GET("/logs", getServerLogs)
 | 
			
		||||
| 
						 | 
				
			
			@ -71,6 +70,7 @@ func Configure(m *server.Manager, client remote.Client) *gin.Engine {
 | 
			
		|||
		server.POST("/commands", postServerCommands)
 | 
			
		||||
		server.POST("/install", postServerInstall)
 | 
			
		||||
		server.POST("/reinstall", postServerReinstall)
 | 
			
		||||
		server.POST("/sync", postServerSync)
 | 
			
		||||
		server.POST("/ws/deny", postServerDenyWSTokens)
 | 
			
		||||
 | 
			
		||||
		// This archive request causes the archive to start being created
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,6 @@
 | 
			
		|||
package router
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
| 
						 | 
				
			
			@ -10,7 +9,6 @@ import (
 | 
			
		|||
	"emperror.dev/errors"
 | 
			
		||||
	"github.com/apex/log"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
 | 
			
		||||
	"github.com/pterodactyl/wings/router/downloader"
 | 
			
		||||
	"github.com/pterodactyl/wings/router/middleware"
 | 
			
		||||
	"github.com/pterodactyl/wings/router/tokens"
 | 
			
		||||
| 
						 | 
				
			
			@ -130,21 +128,18 @@ func postServerCommands(c *gin.Context) {
 | 
			
		|||
	c.Status(http.StatusNoContent)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Updates information about a server internally.
 | 
			
		||||
func patchServer(c *gin.Context) {
 | 
			
		||||
// postServerSync will accept a POST request and trigger a re-sync of the given
 | 
			
		||||
// 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)
 | 
			
		||||
 | 
			
		||||
	buf := bytes.Buffer{}
 | 
			
		||||
	buf.ReadFrom(c.Request.Body)
 | 
			
		||||
 | 
			
		||||
	if err := s.UpdateDataStructure(buf.Bytes()); err != nil {
 | 
			
		||||
		NewServerError(err, s).Abort(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s.SyncWithEnvironment()
 | 
			
		||||
 | 
			
		||||
	if err := s.Sync(); err != nil {
 | 
			
		||||
		WithError(c, err)
 | 
			
		||||
	} else {
 | 
			
		||||
		c.Status(http.StatusNoContent)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Performs a server installation in a background thread.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -254,13 +254,20 @@ func getServerPullingFiles(c *gin.Context) {
 | 
			
		|||
func postServerPullRemoteFile(c *gin.Context) {
 | 
			
		||||
	s := ExtractServer(c)
 | 
			
		||||
	var data struct {
 | 
			
		||||
		RootPath string `binding:"required,omitempty" json:"root"`
 | 
			
		||||
		// Deprecated
 | 
			
		||||
		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 {
 | 
			
		||||
		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)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if e, ok := err.(*url.Error); ok {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,6 @@
 | 
			
		|||
package router
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"net/http"
 | 
			
		||||
| 
						 | 
				
			
			@ -44,10 +43,13 @@ func getAllServers(c *gin.Context) {
 | 
			
		|||
// for it.
 | 
			
		||||
func postCreateServer(c *gin.Context) {
 | 
			
		||||
	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 installer.IsValidationError(err) {
 | 
			
		||||
			c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
 | 
			
		||||
| 
						 | 
				
			
			@ -74,24 +76,21 @@ func postCreateServer(c *gin.Context) {
 | 
			
		|||
		}
 | 
			
		||||
 | 
			
		||||
		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
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if i.Server().Config().StartOnCompletion {
 | 
			
		||||
		if i.StartOnCompletion {
 | 
			
		||||
			log.WithField("server_id", i.Server().ID()).Debug("starting server after successful installation")
 | 
			
		||||
			if err := i.Server().HandlePowerAction(server.PowerActionStart, 30); err != nil {
 | 
			
		||||
				if errors.Is(err, context.DeadlineExceeded) {
 | 
			
		||||
					log.WithFields(log.Fields{"server_id": i.Server().ID(), "action": "start"}).
 | 
			
		||||
						Warn("could not acquire a lock while attempting to perform a power action")
 | 
			
		||||
					log.WithFields(log.Fields{"server_id": i.Server().ID(), "action": "start"}).Warn("could not acquire a lock while attempting to perform a power action")
 | 
			
		||||
				} else {
 | 
			
		||||
					log.WithFields(log.Fields{"server_id": i.Server().ID(), "action": "start", "error": err}).
 | 
			
		||||
						Error("encountered error processing a server power action in the background")
 | 
			
		||||
					log.WithFields(log.Fields{"server_id": i.Server().ID(), "action": "start", "error": err}).Error("encountered error processing a server power action in the background")
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			log.WithField("server_id", i.Server().ID()).
 | 
			
		||||
				Debug("skipping automatic start after successful server installation")
 | 
			
		||||
			log.WithField("server_id", i.Server().ID()).Debug("skipping automatic start after successful server installation")
 | 
			
		||||
		}
 | 
			
		||||
	}(install)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,6 @@ import (
 | 
			
		|||
	"context"
 | 
			
		||||
	"crypto/sha256"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
| 
						 | 
				
			
			@ -50,7 +49,7 @@ type serverTransferRequest struct {
 | 
			
		|||
	ServerID string                  `binding:"required" json:"server_id"`
 | 
			
		||||
	URL      string                  `binding:"required" json:"url"`
 | 
			
		||||
	Token    string                  `binding:"required" json:"token"`
 | 
			
		||||
	Server   json.RawMessage `json:"server"`
 | 
			
		||||
	Server   installer.ServerDetails `json:"server"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getArchivePath(sID string) string {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@ import (
 | 
			
		|||
	"io/fs"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"os"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"emperror.dev/errors"
 | 
			
		||||
	"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
 | 
			
		||||
	// in the file one at a time and writing them to the disk.
 | 
			
		||||
	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)
 | 
			
		||||
		if err := s.Filesystem().Writefile(file, r); err != nil {
 | 
			
		||||
			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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,7 @@ import (
 | 
			
		|||
	"io/fs"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"emperror.dev/errors"
 | 
			
		||||
	"github.com/apex/log"
 | 
			
		||||
| 
						 | 
				
			
			@ -26,7 +27,7 @@ const (
 | 
			
		|||
 | 
			
		||||
// RestoreCallback is a generic restoration callback that exists for both local
 | 
			
		||||
// 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
 | 
			
		||||
type BackupInterface interface {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -88,7 +88,7 @@ func (b *LocalBackup) Restore(ctx context.Context, _ io.Reader, callback Restore
 | 
			
		|||
			if f.IsDir() {
 | 
			
		||||
				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
 | 
			
		||||
		}
 | 
			
		||||
		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
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,8 +35,6 @@ type Configuration struct {
 | 
			
		|||
	// server, specific installation scripts will be skipped for the server process.
 | 
			
		||||
	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
 | 
			
		||||
	// server process.
 | 
			
		||||
	EnvVars environment.Variables `json:"environment"`
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,6 +19,8 @@ import (
 | 
			
		|||
// a server.
 | 
			
		||||
var appName string
 | 
			
		||||
 | 
			
		||||
var appNameSync sync.Once
 | 
			
		||||
 | 
			
		||||
var ErrTooMuchConsoleData = errors.New("console is outputting too much data")
 | 
			
		||||
 | 
			
		||||
type ConsoleThrottler struct {
 | 
			
		||||
| 
						 | 
				
			
			@ -131,9 +133,9 @@ func (s *Server) Throttler() *ConsoleThrottler {
 | 
			
		|||
// PublishConsoleOutputFromDaemon sends output to the server console formatted
 | 
			
		||||
// to appear correctly as being sent from Wings.
 | 
			
		||||
func (s *Server) PublishConsoleOutputFromDaemon(data string) {
 | 
			
		||||
	if appName == "" {
 | 
			
		||||
	appNameSync.Do(func() {
 | 
			
		||||
		appName = config.Get().AppName
 | 
			
		||||
	}
 | 
			
		||||
	})
 | 
			
		||||
	s.Events().Publish(
 | 
			
		||||
		ConsoleOutputEvent,
 | 
			
		||||
		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 {
 | 
			
		||||
			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
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -528,3 +528,20 @@ func (fs *Filesystem) ListDirectory(p string) ([]Stat, error) {
 | 
			
		|||
 | 
			
		||||
	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 {
 | 
			
		||||
		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)
 | 
			
		||||
| 
						 | 
				
			
			@ -200,11 +203,6 @@ func (m *Manager) InitServer(data remote.ServerConfigurationResponse) (*Server,
 | 
			
		|||
		s.Throttler().StartTimer(s.Context())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Forces the configuration to be synced with the panel.
 | 
			
		||||
	if err := s.SyncWithConfiguration(data); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If the server's data directory exists, force disk usage calculation.
 | 
			
		||||
	if _, err := os.Stat(s.Filesystem().Path()); err == nil {
 | 
			
		||||
		s.Filesystem().HasSpaceAvailable(true)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@ package server
 | 
			
		|||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
| 
						 | 
				
			
			@ -15,7 +16,6 @@ import (
 | 
			
		|||
 | 
			
		||||
	"github.com/pterodactyl/wings/config"
 | 
			
		||||
	"github.com/pterodactyl/wings/environment"
 | 
			
		||||
	"github.com/pterodactyl/wings/environment/docker"
 | 
			
		||||
	"github.com/pterodactyl/wings/events"
 | 
			
		||||
	"github.com/pterodactyl/wings/remote"
 | 
			
		||||
	"github.com/pterodactyl/wings/server/filesystem"
 | 
			
		||||
| 
						 | 
				
			
			@ -99,6 +99,14 @@ func (s *Server) ID() string {
 | 
			
		|||
	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
 | 
			
		||||
// are using this server's context for things, all of the background tasks will be
 | 
			
		||||
// stopped as a result.
 | 
			
		||||
| 
						 | 
				
			
			@ -159,31 +167,48 @@ func (s *Server) Sync() error {
 | 
			
		|||
		}
 | 
			
		||||
		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 {
 | 
			
		||||
	// Update the data structure and persist it to the disk.
 | 
			
		||||
	if err := s.UpdateDataStructure(cfg.Settings); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	c := Configuration{}
 | 
			
		||||
	if err := json.Unmarshal(cfg.Settings, &c); err != nil {
 | 
			
		||||
		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.procConfig = cfg.ProcessConfiguration
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										131
									
								
								server/update.go
									
									
									
									
									
								
							
							
						
						
									
										131
									
								
								server/update.go
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -1,128 +1,41 @@
 | 
			
		|||
package server
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
 | 
			
		||||
	"emperror.dev/errors"
 | 
			
		||||
	"github.com/buger/jsonparser"
 | 
			
		||||
	"github.com/imdario/mergo"
 | 
			
		||||
	"github.com/pterodactyl/wings/environment/docker"
 | 
			
		||||
 | 
			
		||||
	"github.com/pterodactyl/wings/environment"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// UpdateDataStructure merges data passed through in JSON form into the existing
 | 
			
		||||
// server object. Any changes to the build settings will apply immediately in
 | 
			
		||||
// the environment if the environment supports it.
 | 
			
		||||
// SyncWithEnvironment 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.
 | 
			
		||||
//
 | 
			
		||||
// The server will be marked as requiring a rebuild on the next boot sequence,
 | 
			
		||||
// it is up to the specific environment to determine what needs to happen when
 | 
			
		||||
// that is the case.
 | 
			
		||||
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.
 | 
			
		||||
// 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() {
 | 
			
		||||
	s.Log().Debug("syncing server settings with environment")
 | 
			
		||||
 | 
			
		||||
	cfg := s.Config()
 | 
			
		||||
 | 
			
		||||
	// Update the environment settings using the new information from this server.
 | 
			
		||||
	s.Environment.Config().SetSettings(environment.Settings{
 | 
			
		||||
		Mounts:      s.Mounts(),
 | 
			
		||||
		Allocations: s.Config().Allocations,
 | 
			
		||||
		Limits:      s.Config().Build,
 | 
			
		||||
		Allocations: cfg.Allocations,
 | 
			
		||||
		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
 | 
			
		||||
	// the startup command also need to be properly propagated to this environment.
 | 
			
		||||
	//
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,6 +14,7 @@ import (
 | 
			
		|||
	"golang.org/x/crypto/ssh"
 | 
			
		||||
 | 
			
		||||
	"github.com/pterodactyl/wings/config"
 | 
			
		||||
	"github.com/pterodactyl/wings/server"
 | 
			
		||||
	"github.com/pterodactyl/wings/server/filesystem"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -26,8 +27,10 @@ const (
 | 
			
		|||
)
 | 
			
		||||
 | 
			
		||||
type Handler struct {
 | 
			
		||||
	permissions []string
 | 
			
		||||
	mu sync.Mutex
 | 
			
		||||
 | 
			
		||||
	permissions []string
 | 
			
		||||
	server      *server.Server
 | 
			
		||||
	fs          *filesystem.Filesystem
 | 
			
		||||
	logger      *log.Entry
 | 
			
		||||
	ro          bool
 | 
			
		||||
| 
						 | 
				
			
			@ -35,11 +38,12 @@ type Handler struct {
 | 
			
		|||
 | 
			
		||||
// Returns a new connection handler for the SFTP server. This allows a given user
 | 
			
		||||
// 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{
 | 
			
		||||
		fs:          fs,
 | 
			
		||||
		ro:          config.Get().System.Sftp.ReadOnly,
 | 
			
		||||
		permissions: strings.Split(sc.Permissions.Extensions["permissions"], ","),
 | 
			
		||||
		server:      srv,
 | 
			
		||||
		fs:          srv.Filesystem(),
 | 
			
		||||
		ro:          config.Get().System.Sftp.ReadOnly,
 | 
			
		||||
		logger: log.WithFields(log.Fields{
 | 
			
		||||
			"subsystem": "sftp",
 | 
			
		||||
			"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
 | 
			
		||||
// permissions are defined and returned by the Panel API.
 | 
			
		||||
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
 | 
			
		||||
	// API, so for the sake of speed do an initial check for that before iterating over the
 | 
			
		||||
	// 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
 | 
			
		||||
		// 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 {
 | 
			
		||||
			_ = handler.Close()
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue
	
	Block a user