Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02734292a0 | ||
|
|
7899a7abdf | ||
|
|
6f9783f164 | ||
|
|
d9ebf693e0 | ||
|
|
0cfd72e1d1 | ||
|
|
6b5b42ec58 | ||
|
|
e13b6d3cb0 | ||
|
|
e79694d6d2 | ||
|
|
12b6b64086 | ||
|
|
9861286f96 | ||
|
|
09e1ba6f34 | ||
|
|
ee91224eb6 | ||
|
|
5cd43dd4c9 | ||
|
|
3b5e042ccc | ||
|
|
7321c6aa45 | ||
|
|
354e69b976 | ||
|
|
d2cfa6cd51 | ||
|
|
5764894a5e | ||
|
|
d4a8f25cc6 | ||
|
|
a0a54749d7 | ||
|
|
88caafa3f5 | ||
|
|
4ee7f367e7 | ||
|
|
c279d28c5d | ||
|
|
f7c8571f46 |
4
.github/workflows/build-test.yml
vendored
4
.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 }}
|
||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
run: go test ./...
|
run: go test ./...
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
if: ${{ matrix.go == '^1.16' && (github.ref == 'refs/heads/develop' || github.event_name == 'pull_request') }}
|
if: ${{ github.ref == 'refs/heads/develop' || github.event_name == 'pull_request' }}
|
||||||
with:
|
with:
|
||||||
name: wings_${{ matrix.goos }}_${{ matrix.goarch }}
|
name: wings_${{ matrix.goos }}_${{ matrix.goarch }}
|
||||||
path: build/wings_${{ matrix.goos }}_${{ matrix.goarch }}
|
path: build/wings_${{ matrix.goos }}_${{ matrix.goarch }}
|
||||||
|
|||||||
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 }}
|
||||||
|
|||||||
36
CHANGELOG.md
36
CHANGELOG.md
@@ -1,5 +1,41 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.5.2
|
||||||
|
### Fixed
|
||||||
|
* Fixes servers not properly re-syncing with the Panel if they are already running causing them to be hard-stopped when terminated, rather than stopped with the configured action.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* Changes SFTP server implementation to use ED25519 server keys rather than deprecated SHA1 RSA keys.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* Adds server uptime output in the stats event emitted to the websocket.
|
||||||
|
|
||||||
|
## v1.5.1
|
||||||
|
### Added
|
||||||
|
* Global configuration option for toggling server crash detection (`system.crash_detection.enabled`)
|
||||||
|
* RPM specfile
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
* Fixes potential issue where the underlying websocket connection is closed but the parent request context is not yet canceled causing a write over a closed connection.
|
||||||
|
* Fixes race condition when closing all active websocket connections when a server is deleted.
|
||||||
|
* Fixes logic to determine if a server's context is closed out and send a websocket close message to connected clients. Previously this fired off whenever the request itself was closed, and not when the server context was closed.
|
||||||
|
|
||||||
|
### 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
|
## v1.4.7
|
||||||
### Fixed
|
### Fixed
|
||||||
* SFTP access is now properly denied if a server is suspended.
|
* SFTP access is now properly denied if a server is suspended.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -30,8 +30,11 @@ I would like to extend my sincere thanks to the following sponsors for helping f
|
|||||||
| [**Spill Hosting**](https://spillhosting.no/) | Spill Hosting is a Norwegian hosting service, which aims for inexpensive services on quality servers. Premium i9-9900K processors will run your game like a dream. |
|
| [**Spill Hosting**](https://spillhosting.no/) | Spill Hosting is a Norwegian hosting service, which aims for inexpensive services on quality servers. Premium i9-9900K processors will run your game like a dream. |
|
||||||
| [**DeinServerHost**](https://deinserverhost.de/) | DeinServerHost offers Dedicated, vps and Gameservers for many popular Games like Minecraft and Rust in Germany since 2013. |
|
| [**DeinServerHost**](https://deinserverhost.de/) | DeinServerHost offers Dedicated, vps and Gameservers for many popular Games like Minecraft and Rust in Germany since 2013. |
|
||||||
| [**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://chs.gg/) | 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)
|
||||||
|
|||||||
28
cmd/root.go
28
cmd/root.go
@@ -1,6 +1,7 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -206,7 +207,17 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
|
|||||||
st = state
|
st = state
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := s.Environment.IsRunning()
|
// Use a timed context here to avoid booting issues where Docker hangs for a
|
||||||
|
// specific container that would cause Wings to be un-bootable until the entire
|
||||||
|
// machine is rebooted. It is much better for us to just have a single failed
|
||||||
|
// server instance than an entire offline node.
|
||||||
|
//
|
||||||
|
// @see https://github.com/pterodactyl/panel/issues/2475
|
||||||
|
// @see https://github.com/pterodactyl/panel/issues/3358
|
||||||
|
ctx, cancel := context.WithTimeout(cmd.Context(), time.Second*30)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
r, err := s.Environment.IsRunning(ctx)
|
||||||
// We ignore missing containers because we don't want to actually block booting of wings at this
|
// We ignore missing containers because we don't want to actually block booting of wings at this
|
||||||
// point. If we didn't do this, and you pruned all the images and then started wings you could
|
// point. If we didn't do this, and you pruned all the images and then started wings you could
|
||||||
// end up waiting a long period of time for all the images to be re-pulled on Wings boot rather
|
// end up waiting a long period of time for all the images to be re-pulled on Wings boot rather
|
||||||
@@ -235,7 +246,7 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
|
|||||||
s.Log().Info("detected server is running, re-attaching to process...")
|
s.Log().Info("detected server is running, re-attaching to process...")
|
||||||
|
|
||||||
s.Environment.SetState(environment.ProcessRunningState)
|
s.Environment.SetState(environment.ProcessRunningState)
|
||||||
if err := s.Environment.Attach(); err != nil {
|
if err := s.Environment.Attach(ctx); err != nil {
|
||||||
s.Log().WithField("error", err).Warn("failed to attach to running server environment")
|
s.Log().WithField("error", err).Warn("failed to attach to running server environment")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -244,6 +255,13 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
|
|||||||
// state being tracked.
|
// state being tracked.
|
||||||
s.Environment.SetState(environment.ProcessOfflineState)
|
s.Environment.SetState(environment.ProcessOfflineState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if state := s.Environment.State(); state == environment.ProcessStartingState || state == environment.ProcessRunningState {
|
||||||
|
s.Log().Debug("re-syncing server configuration for already running server")
|
||||||
|
if err := s.Sync(); err != nil {
|
||||||
|
s.Log().WithError(err).Error("failed to re-sync server configuration")
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,12 +294,12 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
|
|||||||
|
|
||||||
sys := config.Get().System
|
sys := config.Get().System
|
||||||
// Ensure the archive directory exists.
|
// Ensure the archive directory exists.
|
||||||
if err := os.MkdirAll(sys.ArchiveDirectory, 0755); err != nil {
|
if err := os.MkdirAll(sys.ArchiveDirectory, 0o755); err != nil {
|
||||||
log.WithField("error", err).Error("failed to create archive directory")
|
log.WithField("error", err).Error("failed to create archive directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the backup directory exists.
|
// Ensure the backup directory exists.
|
||||||
if err := os.MkdirAll(sys.BackupDirectory, 0755); err != nil {
|
if err := os.MkdirAll(sys.BackupDirectory, 0o755); err != nil {
|
||||||
log.WithField("error", err).Error("failed to create backup directory")
|
log.WithField("error", err).Error("failed to create backup directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +392,7 @@ func initConfig() {
|
|||||||
// in the code without having to pass around a logger instance.
|
// in the code without having to pass around a logger instance.
|
||||||
func initLogging() {
|
func initLogging() {
|
||||||
dir := config.Get().System.LogDirectory
|
dir := config.Get().System.LogDirectory
|
||||||
if err := os.MkdirAll(path.Join(dir, "/install"), 0700); err != nil {
|
if err := os.MkdirAll(path.Join(dir, "/install"), 0o700); err != nil {
|
||||||
log2.Fatalf("cmd/root: failed to create install directory path: %s", err)
|
log2.Fatalf("cmd/root: failed to create install directory path: %s", err)
|
||||||
}
|
}
|
||||||
p := filepath.Join(dir, "/wings.log")
|
p := filepath.Join(dir, "/wings.log")
|
||||||
|
|||||||
@@ -48,10 +48,12 @@ var DefaultTLSConfig = &tls.Config{
|
|||||||
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
|
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
|
||||||
}
|
}
|
||||||
|
|
||||||
var mu sync.RWMutex
|
var (
|
||||||
var _config *Configuration
|
mu sync.RWMutex
|
||||||
var _jwtAlgo *jwt.HMACSHA
|
_config *Configuration
|
||||||
var _debugViaFlag bool
|
_jwtAlgo *jwt.HMACSHA
|
||||||
|
_debugViaFlag bool
|
||||||
|
)
|
||||||
|
|
||||||
// Locker specific to writing the configuration to the disk, this happens
|
// Locker specific to writing the configuration to the disk, this happens
|
||||||
// in areas that might already be locked, so we don't want to crash the process.
|
// in areas that might already be locked, so we don't want to crash the process.
|
||||||
@@ -181,6 +183,9 @@ type SystemConfiguration struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CrashDetection struct {
|
type CrashDetection struct {
|
||||||
|
// CrashDetectionEnabled sets if crash detection is enabled globally for all servers on this node.
|
||||||
|
CrashDetectionEnabled bool `default:"true" yaml:"enabled"`
|
||||||
|
|
||||||
// Determines if Wings should detect a server that stops with a normal exit code of
|
// Determines if Wings should detect a server that stops with a normal exit code of
|
||||||
// "0" as being crashed if the process stopped without any Wings interaction. E.g.
|
// "0" as being crashed if the process stopped without any Wings interaction. E.g.
|
||||||
// the user did not press the stop button, but the process stopped cleanly.
|
// the user did not press the stop button, but the process stopped cleanly.
|
||||||
@@ -375,7 +380,7 @@ func WriteToDisk(c *Configuration) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := ioutil.WriteFile(c.path, b, 0600); err != nil {
|
if err := ioutil.WriteFile(c.path, b, 0o600); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -470,7 +475,7 @@ func FromFile(path string) error {
|
|||||||
func ConfigureDirectories() error {
|
func ConfigureDirectories() error {
|
||||||
root := _config.System.RootDirectory
|
root := _config.System.RootDirectory
|
||||||
log.WithField("path", root).Debug("ensuring root data directory exists")
|
log.WithField("path", root).Debug("ensuring root data directory exists")
|
||||||
if err := os.MkdirAll(root, 0700); err != nil {
|
if err := os.MkdirAll(root, 0o700); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,17 +496,17 @@ func ConfigureDirectories() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.WithField("path", _config.System.Data).Debug("ensuring server data directory exists")
|
log.WithField("path", _config.System.Data).Debug("ensuring server data directory exists")
|
||||||
if err := os.MkdirAll(_config.System.Data, 0700); err != nil {
|
if err := os.MkdirAll(_config.System.Data, 0o700); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.WithField("path", _config.System.ArchiveDirectory).Debug("ensuring archive data directory exists")
|
log.WithField("path", _config.System.ArchiveDirectory).Debug("ensuring archive data directory exists")
|
||||||
if err := os.MkdirAll(_config.System.ArchiveDirectory, 0700); err != nil {
|
if err := os.MkdirAll(_config.System.ArchiveDirectory, 0o700); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.WithField("path", _config.System.BackupDirectory).Debug("ensuring backup data directory exists")
|
log.WithField("path", _config.System.BackupDirectory).Debug("ensuring backup data directory exists")
|
||||||
if err := os.MkdirAll(_config.System.BackupDirectory, 0700); err != nil {
|
if err := os.MkdirAll(_config.System.BackupDirectory, 0o700); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func (nw noopWriter) Write(b []byte) (int, error) {
|
|||||||
// Calling this function will poll resources for the container in the background
|
// Calling this function will poll resources for the container in the background
|
||||||
// until the provided context is canceled by the caller. Failure to cancel said
|
// until the provided context is canceled by the caller. Failure to cancel said
|
||||||
// context will cause background memory leaks as the goroutine will not exit.
|
// context will cause background memory leaks as the goroutine will not exit.
|
||||||
func (e *Environment) Attach() error {
|
func (e *Environment) Attach(ctx context.Context) error {
|
||||||
if e.IsAttached() {
|
if e.IsAttached() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -62,14 +62,17 @@ func (e *Environment) Attach() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set the stream again with the container.
|
// Set the stream again with the container.
|
||||||
if st, err := e.client.ContainerAttach(context.Background(), e.Id, opts); err != nil {
|
if st, err := e.client.ContainerAttach(ctx, e.Id, opts); err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
e.SetStream(&st)
|
e.SetStream(&st)
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
// Don't use the context provided to the function, that'll cause the polling to
|
||||||
|
// exit unexpectedly. We want a custom context for this, the one passed to the
|
||||||
|
// function is to avoid a hang situation when trying to attach to a container.
|
||||||
|
pollCtx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
defer e.stream.Close()
|
defer e.stream.Close()
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -78,7 +81,7 @@ func (e *Environment) Attach() error {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := e.pollResources(ctx); err != nil {
|
if err := e.pollResources(pollCtx); err != nil {
|
||||||
if !errors.Is(err, context.Canceled) {
|
if !errors.Is(err, context.Canceled) {
|
||||||
e.log().WithField("error", err).Error("error during environment resource polling")
|
e.log().WithField("error", err).Error("error during environment resource polling")
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -128,20 +128,20 @@ func (e *Environment) Exists() (bool, error) {
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determines if the server's docker container is currently running. If there is no container
|
// IsRunning determines if the server's docker container is currently running.
|
||||||
// present, an error will be raised (since this shouldn't be a case that ever happens under
|
// If there is no container present, an error will be raised (since this
|
||||||
// correctly developed circumstances).
|
// shouldn't be a case that ever happens under correctly developed
|
||||||
|
// circumstances).
|
||||||
//
|
//
|
||||||
// You can confirm if the instance wasn't found by using client.IsErrNotFound from the Docker
|
// You can confirm if the instance wasn't found by using client.IsErrNotFound
|
||||||
// API.
|
// from the Docker API.
|
||||||
//
|
//
|
||||||
// @see docker/client/errors.go
|
// @see docker/client/errors.go
|
||||||
func (e *Environment) IsRunning() (bool, error) {
|
func (e *Environment) IsRunning(ctx context.Context) (bool, error) {
|
||||||
c, err := e.client.ContainerInspect(context.Background(), e.Id)
|
c, err := e.client.ContainerInspect(ctx, e.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.State.Running, nil
|
return c.State.Running, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,16 +17,17 @@ import (
|
|||||||
"github.com/pterodactyl/wings/remote"
|
"github.com/pterodactyl/wings/remote"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Run before the container starts and get the process configuration from the Panel.
|
// OnBeforeStart run before the container starts and get the process
|
||||||
// This is important since we use this to check configuration files as well as ensure
|
// configuration from the Panel. This is important since we use this to check
|
||||||
// we always have the latest version of an egg available for server processes.
|
// configuration files as well as ensure we always have the latest version of
|
||||||
|
// an egg available for server processes.
|
||||||
//
|
//
|
||||||
// This process will also confirm that the server environment exists and is in a bootable
|
// This process will also confirm that the server environment exists and is in
|
||||||
// state. This ensures that unexpected container deletion while Wings is running does
|
// a bootable state. This ensures that unexpected container deletion while Wings
|
||||||
// not result in the server becoming un-bootable.
|
// is running does not result in the server becoming un-bootable.
|
||||||
func (e *Environment) OnBeforeStart() error {
|
func (e *Environment) OnBeforeStart(ctx context.Context) error {
|
||||||
// Always destroy and re-create the server container to ensure that synced data from the Panel is used.
|
// Always destroy and re-create the server container to ensure that synced data from the Panel is used.
|
||||||
if err := e.client.ContainerRemove(context.Background(), e.Id, types.ContainerRemoveOptions{RemoveVolumes: true}); err != nil {
|
if err := e.client.ContainerRemove(ctx, e.Id, types.ContainerRemoveOptions{RemoveVolumes: true}); err != nil {
|
||||||
if !client.IsErrNotFound(err) {
|
if !client.IsErrNotFound(err) {
|
||||||
return errors.WrapIf(err, "environment/docker: failed to remove container during pre-boot")
|
return errors.WrapIf(err, "environment/docker: failed to remove container during pre-boot")
|
||||||
}
|
}
|
||||||
@@ -46,10 +47,10 @@ func (e *Environment) OnBeforeStart() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Starts the server environment and begins piping output to the event listeners for the
|
// Start will start the server environment and begins piping output to the event
|
||||||
// console. If a container does not exist, or needs to be rebuilt that will happen in the
|
// listeners for the console. If a container does not exist, or needs to be
|
||||||
// call to OnBeforeStart().
|
// rebuilt that will happen in the call to OnBeforeStart().
|
||||||
func (e *Environment) Start() error {
|
func (e *Environment) Start(ctx context.Context) error {
|
||||||
sawError := false
|
sawError := false
|
||||||
|
|
||||||
// If sawError is set to true there was an error somewhere in the pipeline that
|
// If sawError is set to true there was an error somewhere in the pipeline that
|
||||||
@@ -65,7 +66,7 @@ func (e *Environment) Start() error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if c, err := e.client.ContainerInspect(context.Background(), e.Id); err != nil {
|
if c, err := e.client.ContainerInspect(ctx, e.Id); err != nil {
|
||||||
// Do nothing if the container is not found, we just don't want to continue
|
// Do nothing if the container is not found, we just don't want to continue
|
||||||
// to the next block of code here. This check was inlined here to guard against
|
// to the next block of code here. This check was inlined here to guard against
|
||||||
// a nil-pointer when checking c.State below.
|
// a nil-pointer when checking c.State below.
|
||||||
@@ -79,7 +80,7 @@ func (e *Environment) Start() error {
|
|||||||
if c.State.Running {
|
if c.State.Running {
|
||||||
e.SetState(environment.ProcessRunningState)
|
e.SetState(environment.ProcessRunningState)
|
||||||
|
|
||||||
return e.Attach()
|
return e.Attach(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Truncate the log file, so we don't end up outputting a bunch of useless log information
|
// Truncate the log file, so we don't end up outputting a bunch of useless log information
|
||||||
@@ -101,21 +102,23 @@ func (e *Environment) Start() error {
|
|||||||
// Run the before start function and wait for it to finish. This will validate that the container
|
// Run the before start function and wait for it to finish. This will validate that the container
|
||||||
// exists on the system, and rebuild the container if that is required for server booting to
|
// exists on the system, and rebuild the container if that is required for server booting to
|
||||||
// occur.
|
// occur.
|
||||||
if err := e.OnBeforeStart(); err != nil {
|
if err := e.OnBeforeStart(ctx); err != nil {
|
||||||
return errors.WithStackIf(err)
|
return errors.WithStackIf(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
// If we cannot start & attach to the container in 30 seconds something has gone
|
||||||
|
// quite sideways and we should stop trying to avoid a hanging situation.
|
||||||
|
actx, cancel := context.WithTimeout(ctx, time.Second*30)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
if err := e.client.ContainerStart(ctx, e.Id, types.ContainerStartOptions{}); err != nil {
|
if err := e.client.ContainerStart(actx, e.Id, types.ContainerStartOptions{}); err != nil {
|
||||||
return errors.WrapIf(err, "environment/docker: failed to start container")
|
return errors.WrapIf(err, "environment/docker: failed to start container")
|
||||||
}
|
}
|
||||||
|
|
||||||
// No errors, good to continue through.
|
// No errors, good to continue through.
|
||||||
sawError = false
|
sawError = false
|
||||||
|
|
||||||
return e.Attach()
|
return e.Attach(actx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop stops the container that the server is running in. This will allow up to
|
// Stop stops the container that the server is running in. This will allow up to
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
"emperror.dev/errors"
|
"emperror.dev/errors"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
@@ -12,6 +13,23 @@ import (
|
|||||||
"github.com/pterodactyl/wings/environment"
|
"github.com/pterodactyl/wings/environment"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Uptime returns the current uptime of the container in milliseconds. If the
|
||||||
|
// container is not currently running this will return 0.
|
||||||
|
func (e *Environment) Uptime(ctx context.Context) (int64, error) {
|
||||||
|
ins, err := e.client.ContainerInspect(ctx, e.Id)
|
||||||
|
if err != nil {
|
||||||
|
return 0, errors.Wrap(err, "environment: could not inspect container")
|
||||||
|
}
|
||||||
|
if !ins.State.Running {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
started, err := time.Parse(time.RFC3339, ins.State.StartedAt)
|
||||||
|
if err != nil {
|
||||||
|
return 0, errors.Wrap(err, "environment: failed to parse container start time")
|
||||||
|
}
|
||||||
|
return time.Since(started).Milliseconds(), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Attach to the instance and then automatically emit an event whenever the resource usage for the
|
// Attach to the instance and then automatically emit an event whenever the resource usage for the
|
||||||
// server process changes.
|
// server process changes.
|
||||||
func (e *Environment) pollResources(ctx context.Context) error {
|
func (e *Environment) pollResources(ctx context.Context) error {
|
||||||
@@ -28,6 +46,11 @@ func (e *Environment) pollResources(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
defer stats.Body.Close()
|
defer stats.Body.Close()
|
||||||
|
|
||||||
|
uptime, err := e.Uptime(ctx)
|
||||||
|
if err != nil {
|
||||||
|
e.log().WithField("error", err).Warn("failed to calculate container uptime")
|
||||||
|
}
|
||||||
|
|
||||||
dec := json.NewDecoder(stats.Body)
|
dec := json.NewDecoder(stats.Body)
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -50,7 +73,12 @@ func (e *Environment) pollResources(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !v.PreRead.IsZero() {
|
||||||
|
uptime = uptime + v.Read.Sub(v.PreRead).Milliseconds()
|
||||||
|
}
|
||||||
|
|
||||||
st := environment.Stats{
|
st := environment.Stats{
|
||||||
|
Uptime: uptime,
|
||||||
Memory: calculateDockerMemory(v.MemoryStats),
|
Memory: calculateDockerMemory(v.MemoryStats),
|
||||||
MemoryLimit: v.MemoryStats.Limit,
|
MemoryLimit: v.MemoryStats.Limit,
|
||||||
CpuAbsolute: calculateDockerAbsoluteCpu(v.PreCPUStats, v.CPUStats),
|
CpuAbsolute: calculateDockerAbsoluteCpu(v.PreCPUStats, v.CPUStats),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package environment
|
package environment
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/pterodactyl/wings/events"
|
"github.com/pterodactyl/wings/events"
|
||||||
@@ -41,9 +42,9 @@ type ProcessEnvironment interface {
|
|||||||
// a basic CLI environment this can probably just return true right away.
|
// a basic CLI environment this can probably just return true right away.
|
||||||
Exists() (bool, error)
|
Exists() (bool, error)
|
||||||
|
|
||||||
// Determines if the environment is currently active and running a server process
|
// IsRunning determines if the environment is currently active and running
|
||||||
// for this specific server instance.
|
// a server process for this specific server instance.
|
||||||
IsRunning() (bool, error)
|
IsRunning(ctx context.Context) (bool, error)
|
||||||
|
|
||||||
// Performs an update of server resource limits without actually stopping the server
|
// Performs an update of server resource limits without actually stopping the server
|
||||||
// process. This only executes if the environment supports it, otherwise it is
|
// process. This only executes if the environment supports it, otherwise it is
|
||||||
@@ -52,11 +53,11 @@ type ProcessEnvironment interface {
|
|||||||
|
|
||||||
// Runs before the environment is started. If an error is returned starting will
|
// Runs before the environment is started. If an error is returned starting will
|
||||||
// not occur, otherwise proceeds as normal.
|
// not occur, otherwise proceeds as normal.
|
||||||
OnBeforeStart() error
|
OnBeforeStart(ctx context.Context) error
|
||||||
|
|
||||||
// Starts a server instance. If the server instance is not in a state where it
|
// Starts a server instance. If the server instance is not in a state where it
|
||||||
// can be started an error should be returned.
|
// can be started an error should be returned.
|
||||||
Start() error
|
Start(ctx context.Context) error
|
||||||
|
|
||||||
// Stops a server instance. If the server is already stopped an error should
|
// Stops a server instance. If the server is already stopped an error should
|
||||||
// not be returned.
|
// not be returned.
|
||||||
@@ -84,10 +85,10 @@ type ProcessEnvironment interface {
|
|||||||
// server.
|
// server.
|
||||||
Create() error
|
Create() error
|
||||||
|
|
||||||
// Attaches to the server console environment and allows piping the output to a
|
// Attach attaches to the server console environment and allows piping the output
|
||||||
// websocket or other internal tool to monitor output. Also allows you to later
|
// to a websocket or other internal tool to monitor output. Also allows you to later
|
||||||
// send data into the environment's stdin.
|
// send data into the environment's stdin.
|
||||||
Attach() error
|
Attach(ctx context.Context) error
|
||||||
|
|
||||||
// Sends the provided command to the running server instance.
|
// Sends the provided command to the running server instance.
|
||||||
SendCommand(string) error
|
SendCommand(string) error
|
||||||
@@ -103,4 +104,8 @@ type ProcessEnvironment interface {
|
|||||||
// handle this itself, but there are some scenarios where it is helpful for the server
|
// handle this itself, but there are some scenarios where it is helpful for the server
|
||||||
// to update the state externally (e.g. starting -> started).
|
// to update the state externally (e.g. starting -> started).
|
||||||
SetState(string)
|
SetState(string)
|
||||||
|
|
||||||
|
// Uptime returns the current environment uptime in milliseconds. This is
|
||||||
|
// the time that has passed since it was last started.
|
||||||
|
Uptime(ctx context.Context) (int64, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package environment
|
package environment
|
||||||
|
|
||||||
// Defines the current resource usage for a given server instance. If a server is offline you
|
// Stats defines the current resource usage for a given server instance.
|
||||||
// should obviously expect memory and CPU usage to be 0. However, disk will always be returned
|
|
||||||
// since that is not dependent on the server being running to collect that data.
|
|
||||||
type Stats struct {
|
type Stats struct {
|
||||||
// The total amount of memory, in bytes, that this server instance is consuming. This is
|
// The total amount of memory, in bytes, that this server instance is consuming. This is
|
||||||
// calculated slightly differently than just using the raw Memory field that the stats
|
// calculated slightly differently than just using the raw Memory field that the stats
|
||||||
@@ -19,12 +17,11 @@ type Stats struct {
|
|||||||
// does not take into account any limits on the server process itself.
|
// does not take into account any limits on the server process itself.
|
||||||
CpuAbsolute float64 `json:"cpu_absolute"`
|
CpuAbsolute float64 `json:"cpu_absolute"`
|
||||||
|
|
||||||
// The current disk space being used by the server. This is cached to prevent slow lookup
|
|
||||||
// issues on frequent refreshes.
|
|
||||||
// Disk int64 `json:"disk_bytes"`
|
|
||||||
|
|
||||||
// Current network transmit in & out for a container.
|
// Current network transmit in & out for a container.
|
||||||
Network NetworkStats `json:"network"`
|
Network NetworkStats `json:"network"`
|
||||||
|
|
||||||
|
// The current uptime of the container, in milliseconds.
|
||||||
|
Uptime int64 `json:"uptime"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NetworkStats struct {
|
type NetworkStats struct {
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -33,7 +33,6 @@ require (
|
|||||||
github.com/gorilla/websocket v1.4.2
|
github.com/gorilla/websocket v1.4.2
|
||||||
github.com/iancoleman/strcase v0.2.0
|
github.com/iancoleman/strcase v0.2.0
|
||||||
github.com/icza/dyno v0.0.0-20210726202311-f1bafe5d9996
|
github.com/icza/dyno v0.0.0-20210726202311-f1bafe5d9996
|
||||||
github.com/imdario/mergo v0.3.12
|
|
||||||
github.com/juju/ratelimit v1.0.1
|
github.com/juju/ratelimit v1.0.1
|
||||||
github.com/karrick/godirwalk v1.16.1
|
github.com/karrick/godirwalk v1.16.1
|
||||||
github.com/klauspost/compress v1.13.2 // indirect
|
github.com/klauspost/compress v1.13.2 // indirect
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -513,8 +513,6 @@ github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ
|
|||||||
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||||
github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||||
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
|
||||||
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA=
|
github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA=
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -103,7 +101,7 @@ func postServerPower(c *gin.Context) {
|
|||||||
func postServerCommands(c *gin.Context) {
|
func postServerCommands(c *gin.Context) {
|
||||||
s := ExtractServer(c)
|
s := ExtractServer(c)
|
||||||
|
|
||||||
if running, err := s.Environment.IsRunning(); err != nil {
|
if running, err := s.Environment.IsRunning(c.Request.Context()); err != nil {
|
||||||
NewServerError(err, s).Abort(c)
|
NewServerError(err, s).Abort(c)
|
||||||
return
|
return
|
||||||
} else if !running {
|
} else if !running {
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -12,6 +12,14 @@ import (
|
|||||||
"github.com/pterodactyl/wings/router/websocket"
|
"github.com/pterodactyl/wings/router/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var expectedCloseCodes = []int{
|
||||||
|
ws.CloseGoingAway,
|
||||||
|
ws.CloseAbnormalClosure,
|
||||||
|
ws.CloseNormalClosure,
|
||||||
|
ws.CloseNoStatusReceived,
|
||||||
|
ws.CloseServiceRestart,
|
||||||
|
}
|
||||||
|
|
||||||
// Upgrades a connection to a websocket and passes events along between.
|
// Upgrades a connection to a websocket and passes events along between.
|
||||||
func getServerWebsocket(c *gin.Context) {
|
func getServerWebsocket(c *gin.Context) {
|
||||||
manager := middleware.ExtractManager(c)
|
manager := middleware.ExtractManager(c)
|
||||||
@@ -24,8 +32,10 @@ func getServerWebsocket(c *gin.Context) {
|
|||||||
defer handler.Connection.Close()
|
defer handler.Connection.Close()
|
||||||
|
|
||||||
// Create a context that can be canceled when the user disconnects from this
|
// Create a context that can be canceled when the user disconnects from this
|
||||||
// socket that will also cancel listeners running in separate threads.
|
// socket that will also cancel listeners running in separate threads. If the
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
// connection itself is terminated listeners using this context will also be
|
||||||
|
// closed.
|
||||||
|
ctx, cancel := context.WithCancel(c.Request.Context())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Track this open connection on the server so that we can close them all programmatically
|
// Track this open connection on the server so that we can close them all programmatically
|
||||||
@@ -33,22 +43,19 @@ func getServerWebsocket(c *gin.Context) {
|
|||||||
s.Websockets().Push(handler.Uuid(), &cancel)
|
s.Websockets().Push(handler.Uuid(), &cancel)
|
||||||
defer s.Websockets().Remove(handler.Uuid())
|
defer s.Websockets().Remove(handler.Uuid())
|
||||||
|
|
||||||
// Listen for the context being canceled and then close the websocket connection. This normally
|
// If the server is deleted we need to send a close message to the connected client
|
||||||
// just happens because you're disconnecting from the socket in the browser, however in some
|
// so that they disconnect since there will be no more events sent along. Listen for
|
||||||
// cases we close the connections programmatically (e.g. deleting the server) and need to send
|
// the request context being closed to break this loop, otherwise this routine will
|
||||||
// a close message to the websocket so it disconnects.
|
// be left hanging in the background.
|
||||||
go func(ctx context.Context, c *ws.Conn) {
|
go func() {
|
||||||
ListenerLoop:
|
select {
|
||||||
for {
|
case <-ctx.Done():
|
||||||
select {
|
break
|
||||||
case <-ctx.Done():
|
case <-s.Context().Done():
|
||||||
handler.Connection.WriteControl(ws.CloseMessage, ws.FormatCloseMessage(ws.CloseGoingAway, "server deleted"), time.Now().Add(time.Second*5))
|
handler.Connection.WriteControl(ws.CloseMessage, ws.FormatCloseMessage(ws.CloseGoingAway, "server deleted"), time.Now().Add(time.Second*5))
|
||||||
// A break right here without defining the specific loop would only break the select
|
break
|
||||||
// and not actually break the for loop, thus causing this routine to stick around forever.
|
|
||||||
break ListenerLoop
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}(ctx, handler.Connection)
|
}()
|
||||||
|
|
||||||
go handler.ListenForServerEvents(ctx)
|
go handler.ListenForServerEvents(ctx)
|
||||||
go handler.ListenForExpiration(ctx)
|
go handler.ListenForExpiration(ctx)
|
||||||
@@ -58,14 +65,7 @@ func getServerWebsocket(c *gin.Context) {
|
|||||||
|
|
||||||
_, p, err := handler.Connection.ReadMessage()
|
_, p, err := handler.Connection.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !ws.IsCloseError(
|
if ws.IsUnexpectedCloseError(err, expectedCloseCodes...) {
|
||||||
err,
|
|
||||||
ws.CloseNormalClosure,
|
|
||||||
ws.CloseGoingAway,
|
|
||||||
ws.CloseNoStatusReceived,
|
|
||||||
ws.CloseServiceRestart,
|
|
||||||
ws.CloseAbnormalClosure,
|
|
||||||
) {
|
|
||||||
s.Log().WithField("error", err).Warn("error handling websocket message for server")
|
s.Log().WithField("error", err).Warn("error handling websocket message for server")
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -8,16 +8,14 @@ import (
|
|||||||
"github.com/pterodactyl/wings/server"
|
"github.com/pterodactyl/wings/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Checks the time to expiration on the JWT every 30 seconds until the token has
|
// ListenForExpiration checks the time to expiration on the JWT every 30 seconds
|
||||||
// expired. If we are within 3 minutes of the token expiring, send a notice over
|
// until the token has expired. If we are within 3 minutes of the token expiring,
|
||||||
// the socket that it is expiring soon. If it has expired, send that notice as well.
|
// send a notice over the socket that it is expiring soon. If it has expired,
|
||||||
|
// send that notice as well.
|
||||||
func (h *Handler) ListenForExpiration(ctx context.Context) {
|
func (h *Handler) ListenForExpiration(ctx context.Context) {
|
||||||
// Make a ticker and completion channel that is used to continuously poll the
|
// Make a ticker and completion channel that is used to continuously poll the
|
||||||
// JWT stored in the session to send events to the socket when it is expiring.
|
// JWT stored in the session to send events to the socket when it is expiring.
|
||||||
ticker := time.NewTicker(time.Second * 30)
|
ticker := time.NewTicker(time.Second * 30)
|
||||||
|
|
||||||
// Whenever this function is complete, end the ticker, close out the channel,
|
|
||||||
// and then close the websocket connection.
|
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -51,8 +49,9 @@ var e = []string{
|
|||||||
server.TransferStatusEvent,
|
server.TransferStatusEvent,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listens for different events happening on a server and sends them along
|
// ListenForServerEvents will listen for different events happening on a server
|
||||||
// to the connected websocket.
|
// and send them along to the connected websocket client. This function will
|
||||||
|
// block until the context provided to it is canceled.
|
||||||
func (h *Handler) ListenForServerEvents(ctx context.Context) {
|
func (h *Handler) ListenForServerEvents(ctx context.Context) {
|
||||||
h.server.Log().Debug("listening for server events over websocket")
|
h.server.Log().Debug("listening for server events over websocket")
|
||||||
callback := func(e events.Event) {
|
callback := func(e events.Event) {
|
||||||
@@ -67,13 +66,10 @@ func (h *Handler) ListenForServerEvents(ctx context.Context) {
|
|||||||
h.server.Events().On(evt, &callback)
|
h.server.Events().On(evt, &callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
go func(ctx context.Context) {
|
<-ctx.Done()
|
||||||
select {
|
// Block until the context is stopped and then de-register all of the event listeners
|
||||||
case <-ctx.Done():
|
// that we registered earlier.
|
||||||
// Once this context is stopped, de-register all of the listeners that have been registered.
|
for _, evt := range e {
|
||||||
for _, evt := range e {
|
h.server.Events().Off(evt, &callback)
|
||||||
h.server.Events().Off(evt, &callback)
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}(ctx)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -368,7 +368,9 @@ func (h *Handler) HandleInbound(m Message) error {
|
|||||||
}
|
}
|
||||||
case SendServerLogsEvent:
|
case SendServerLogsEvent:
|
||||||
{
|
{
|
||||||
if running, _ := h.server.Environment.IsRunning(); !running {
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second * 5)
|
||||||
|
defer cancel()
|
||||||
|
if running, _ := h.server.Environment.IsRunning(ctx); !running {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
114
rpm/ptero-wings.spec
Normal file
114
rpm/ptero-wings.spec
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
Name: ptero-wings
|
||||||
|
Version: 1.5.0
|
||||||
|
Release: 1%{?dist}
|
||||||
|
Summary: The server control plane for Pterodactyl Panel. Written from the ground-up with security, speed, and stability in mind.
|
||||||
|
BuildArch: x86_64
|
||||||
|
License: MIT
|
||||||
|
URL: https://github.com/pterodactyl/wings
|
||||||
|
Source0: https://github.com/pterodactyl/wings/releases/download/v%{version}/wings_linux_amd64
|
||||||
|
|
||||||
|
%if 0%{?rhel} && 0%{?rhel} <= 8
|
||||||
|
BuildRequires: systemd
|
||||||
|
%else
|
||||||
|
BuildRequires: systemd-rpm-macros
|
||||||
|
%endif
|
||||||
|
|
||||||
|
|
||||||
|
%description
|
||||||
|
Wings is Pterodactyl's server control plane, built for the rapidly
|
||||||
|
changing gaming industry and designed to be highly performant and
|
||||||
|
secure. Wings provides an HTTP API allowing you to interface directly
|
||||||
|
with running server instances, fetch server logs, generate backups,
|
||||||
|
and control all aspects of the server lifecycle.
|
||||||
|
|
||||||
|
In addition, Wings ships with a built-in SFTP server allowing your
|
||||||
|
system to remain free of Pterodactyl specific dependencies, and
|
||||||
|
allowing users to authenticate with the same credentials they would
|
||||||
|
normally use to access the Panel.
|
||||||
|
|
||||||
|
%prep
|
||||||
|
|
||||||
|
%build
|
||||||
|
#nothing required
|
||||||
|
|
||||||
|
%install
|
||||||
|
mkdir -p %{buildroot}%{_bindir}
|
||||||
|
mkdir -p %{buildroot}%{_unitdir}
|
||||||
|
cp %{_sourcedir}/wings_linux_amd64 %{buildroot}%{_bindir}/wings
|
||||||
|
|
||||||
|
cat > %{buildroot}%{_unitdir}/wings.service << EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Pterodactyl Wings Daemon
|
||||||
|
After=docker.service
|
||||||
|
Requires=docker.service
|
||||||
|
PartOf=docker.service
|
||||||
|
StartLimitIntervalSec=600
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
WorkingDirectory=/etc/pterodactyl
|
||||||
|
ExecStart=/usr/bin/wings
|
||||||
|
ExecReload=/bin/kill -HUP $MAINPID
|
||||||
|
Restart=on-failure
|
||||||
|
StartLimitInterval=180
|
||||||
|
StartLimitBurst=30
|
||||||
|
RestartSec=5s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
%files
|
||||||
|
%attr(0755, root, root) %{_prefix}/bin/wings
|
||||||
|
%attr(0644, root, root) %{_unitdir}/wings.service
|
||||||
|
|
||||||
|
%post
|
||||||
|
|
||||||
|
# Reload systemd
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
# Create the required directory structure
|
||||||
|
mkdir -p /etc/pterodactyl
|
||||||
|
mkdir -p /var/lib/pterodactyl/{archives,backups,volumes}
|
||||||
|
mkdir -p /var/log/pterodactyl/install
|
||||||
|
|
||||||
|
%preun
|
||||||
|
|
||||||
|
systemctl is-active %{name} >/dev/null 2>&1
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
systemctl stop %{name}
|
||||||
|
fi
|
||||||
|
|
||||||
|
systemctl is-enabled %{name} >/dev/null 2>&1
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
systemctl disable %{name}
|
||||||
|
fi
|
||||||
|
|
||||||
|
%postun
|
||||||
|
rm -rf /var/log/pterodactyl
|
||||||
|
|
||||||
|
%verifyscript
|
||||||
|
|
||||||
|
wings --version
|
||||||
|
|
||||||
|
%changelog
|
||||||
|
* Sun Sep 12 2021 Capitol Hosting Solutions Systems Engineering <syseng@chs.gg> - 1.5.0-1
|
||||||
|
- specfile by Capitol Hosting Solutions, Upstream by Pterodactyl
|
||||||
|
- Rebased for https://github.com/pterodactyl/wings/releases/tag/v1.5.0
|
||||||
|
- 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.
|
||||||
|
- Fixes potential issue where the underlying websocket connection is closed but the parent request context is not yet canceled causing a write over a closed connection.
|
||||||
|
- Fixes race condition when closing all active websocket connections when a server is deleted.
|
||||||
|
- Fixes logic to determine if a server's context is closed out and send a websocket close message to connected clients. Previously this fired off whenever the request itself was closed, and not when the server context was closed.
|
||||||
|
- Exposes 8080 in the wings Dockerfile to better support reverse proxy tools.
|
||||||
|
- 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.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
* Wed Aug 25 2021 Capitol Hosting Solutions Systems Engineering <syseng@chs.gg> - 1.4.7-1
|
||||||
|
- specfile by Capitol Hosting Solutions, Upstream by Pterodactyl
|
||||||
|
- Rebased for https://github.com/pterodactyl/wings/releases/tag/v1.4.7
|
||||||
|
- SFTP access is now properly denied if a server is suspended.
|
||||||
|
- Correctly uses start_on_completion and crash_detection_enabled for servers.
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ func (s *Server) HandlePowerAction(action PowerAction, waitSeconds ...int) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.Environment.Start()
|
return s.Environment.Start(s.Context())
|
||||||
case PowerActionStop:
|
case PowerActionStop:
|
||||||
// We're specifically waiting for the process to be stopped here, otherwise the lock is released
|
// We're specifically waiting for the process to be stopped here, otherwise the lock is released
|
||||||
// too soon, and you can rack up all sorts of issues.
|
// too soon, and you can rack up all sorts of issues.
|
||||||
@@ -151,7 +151,7 @@ func (s *Server) HandlePowerAction(action PowerAction, waitSeconds ...int) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.Environment.Start()
|
return s.Environment.Start(s.Context())
|
||||||
case PowerActionTerminate:
|
case PowerActionTerminate:
|
||||||
return s.Environment.Terminate(os.Kill)
|
return s.Environment.Terminate(os.Kill)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ func (ru *ResourceUsage) Reset() {
|
|||||||
|
|
||||||
ru.Memory = 0
|
ru.Memory = 0
|
||||||
ru.CpuAbsolute = 0
|
ru.CpuAbsolute = 0
|
||||||
|
ru.Uptime = 0
|
||||||
ru.Network.TxBytes = 0
|
ru.Network.TxBytes = 0
|
||||||
ru.Network.RxBytes = 0
|
ru.Network.RxBytes = 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -167,31 +167,50 @@ 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 {
|
CrashDetectionEnabled: config.Get().System.CrashDetection.CrashDetectionEnabled,
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
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 deferred 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 explicitly
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +261,7 @@ func (s *Server) EnsureDataDirectoryExists() error {
|
|||||||
if _, err := os.Lstat(s.fs.Path()); err != nil {
|
if _, err := os.Lstat(s.fs.Path()); err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
s.Log().Debug("server: creating root directory and setting permissions")
|
s.Log().Debug("server: creating root directory and setting permissions")
|
||||||
if err := os.MkdirAll(s.fs.Path(), 0700); err != nil {
|
if err := os.MkdirAll(s.fs.Path(), 0o700); err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
if err := s.fs.Chown("/"); err != nil {
|
if err := s.fs.Chown("/"); err != nil {
|
||||||
|
|||||||
149
server/update.go
149
server/update.go
@@ -1,146 +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
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, err := jsonparser.GetBoolean(data, "start_on_completion"); err != nil {
|
|
||||||
if err != jsonparser.KeyPathNotFoundError {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
c.StartOnCompletion = v
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, err := jsonparser.GetBoolean(data, "crash_detection_enabled"); err != nil {
|
|
||||||
if err != jsonparser.KeyPathNotFoundError {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
// Enable crash detection by default.
|
|
||||||
c.CrashDetectionEnabled = true
|
|
||||||
} else {
|
|
||||||
c.CrashDetectionEnabled = 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.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -44,11 +44,11 @@ func (w *WebsocketBag) Remove(u uuid.UUID) {
|
|||||||
w.mu.Unlock()
|
w.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// CancelAll cancels all the stored cancel functions which has the effect of disconnecting
|
// CancelAll cancels all the stored cancel functions which has the effect of
|
||||||
// every listening websocket for the server.
|
// disconnecting every listening websocket for the server.
|
||||||
func (w *WebsocketBag) CancelAll() {
|
func (w *WebsocketBag) CancelAll() {
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
w.mu.Unlock()
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
if w.conns != nil {
|
if w.conns != nil {
|
||||||
for _, cancel := range w.conns {
|
for _, cancel := range w.conns {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package sftp
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"io"
|
"io"
|
||||||
@@ -18,6 +17,7 @@ import (
|
|||||||
"emperror.dev/errors"
|
"emperror.dev/errors"
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/pkg/sftp"
|
"github.com/pkg/sftp"
|
||||||
|
"golang.org/x/crypto/ed25519"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
|
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
@@ -48,18 +48,20 @@ func New(m *server.Manager) *SFTPServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Starts the SFTP server and add a persistent listener to handle inbound SFTP connections.
|
// Run starts the SFTP server and add a persistent listener to handle inbound
|
||||||
|
// SFTP connections. This will automatically generate an ED25519 key if one does
|
||||||
|
// not already exist on the system for host key verification purposes.
|
||||||
func (c *SFTPServer) Run() error {
|
func (c *SFTPServer) Run() error {
|
||||||
if _, err := os.Stat(path.Join(c.BasePath, ".sftp/id_rsa")); os.IsNotExist(err) {
|
if _, err := os.Stat(c.PrivateKeyPath()); os.IsNotExist(err) {
|
||||||
if err := c.generatePrivateKey(); err != nil {
|
if err := c.generateED25519PrivateKey(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return errors.Wrap(err, "sftp/server: could not stat private key file")
|
return errors.Wrap(err, "sftp: could not stat private key file")
|
||||||
}
|
}
|
||||||
pb, err := ioutil.ReadFile(path.Join(c.BasePath, ".sftp/id_rsa"))
|
pb, err := ioutil.ReadFile(c.PrivateKeyPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "sftp/server: could not read private key file")
|
return errors.Wrap(err, "sftp: could not read private key file")
|
||||||
}
|
}
|
||||||
private, err := ssh.ParsePrivateKey(pb)
|
private, err := ssh.ParsePrivateKey(pb)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -78,7 +80,9 @@ func (c *SFTPServer) Run() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.WithField("listen", c.Listen).Info("sftp server listening for connections")
|
public := string(ssh.MarshalAuthorizedKey(private.PublicKey()))
|
||||||
|
log.WithField("listen", c.Listen).WithField("public_key", strings.Trim(public, "\n")).Info("sftp server listening for connections")
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if conn, _ := listener.Accept(); conn != nil {
|
if conn, _ := listener.Accept(); conn != nil {
|
||||||
go func(conn net.Conn) {
|
go func(conn net.Conn) {
|
||||||
@@ -148,26 +152,30 @@ func (c *SFTPServer) AcceptInbound(conn net.Conn, config *ssh.ServerConfig) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generates a private key that will be used by the SFTP server.
|
// Generates a new ED25519 private key that is used for host authentication when
|
||||||
func (c *SFTPServer) generatePrivateKey() error {
|
// a user connects to the SFTP server.
|
||||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
func (c *SFTPServer) generateED25519PrivateKey() error {
|
||||||
|
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.Wrap(err, "sftp: failed to generate ED25519 private key")
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(path.Join(c.BasePath, ".sftp"), 0755); err != nil {
|
if err := os.MkdirAll(path.Dir(c.PrivateKeyPath()), 0755); err != nil {
|
||||||
return errors.Wrap(err, "sftp/server: could not create .sftp directory")
|
return errors.Wrap(err, "sftp: could not create internal sftp data directory")
|
||||||
}
|
}
|
||||||
o, err := os.OpenFile(path.Join(c.BasePath, ".sftp/id_rsa"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
o, err := os.OpenFile(c.PrivateKeyPath(), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
defer o.Close()
|
defer o.Close()
|
||||||
|
|
||||||
err = pem.Encode(o, &pem.Block{
|
b, err := x509.MarshalPKCS8PrivateKey(priv)
|
||||||
Type: "RSA PRIVATE KEY",
|
if err != nil {
|
||||||
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
return errors.Wrap(err, "sftp: failed to marshal private key into bytes")
|
||||||
})
|
}
|
||||||
return errors.WithStack(err)
|
if err := pem.Encode(o, &pem.Block{Type: "PRIVATE KEY", Bytes: b}); err != nil {
|
||||||
|
return errors.Wrap(err, "sftp: failed to write ED25519 private key to disk")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// A function capable of validating user credentials with the Panel API.
|
// A function capable of validating user credentials with the Panel API.
|
||||||
@@ -209,3 +217,8 @@ func (c *SFTPServer) passwordCallback(conn ssh.ConnMetadata, pass []byte) (*ssh.
|
|||||||
|
|
||||||
return sshPerm, nil
|
return sshPerm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrivateKeyPath returns the path the host private key for this server instance.
|
||||||
|
func (c *SFTPServer) PrivateKeyPath() string {
|
||||||
|
return path.Join(c.BasePath, ".sftp/id_ed25519")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user