Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93417dddb1 | ||
|
|
044c46fc9a | ||
|
|
c9d972d544 | ||
|
|
0aab4b1ac2 | ||
|
|
4f4b4fd2e6 | ||
|
|
66c9be357c | ||
|
|
1d36811dfe | ||
|
|
6e74123c65 | ||
|
|
b82f5f9a32 | ||
|
|
1937d0366d | ||
|
|
963a906c30 | ||
|
|
3f6eb7e41a | ||
|
|
a822c7c340 | ||
|
|
b8fb86f5a4 | ||
|
|
ee0c7f09b3 | ||
|
|
d3ddf8cf39 |
@@ -48,3 +48,9 @@ debug
|
||||
.DS_Store
|
||||
*.pprof
|
||||
*.pdf
|
||||
|
||||
Dockerfile
|
||||
CHANGELOG.md
|
||||
Makefile
|
||||
README.md
|
||||
wings-api.paw
|
||||
|
||||
15
CHANGELOG.md
15
CHANGELOG.md
@@ -1,5 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
## v1.2.2
|
||||
### Fixed
|
||||
* Reverts changes to logic handling blocking until a server process is done running when polling stats. This change exposed a bug in the underlying Docker system causing servers to enter a state in which Wings was unable to terminate the process and Docker commands would hang if executed against the container.
|
||||
|
||||
## v1.2.1
|
||||
### Fixed
|
||||
* Fixes servers not be properly marked as no longer transfering if an error occurs during the archive process.
|
||||
* Fixes problems with user detection when running Wings inside a Docker container.
|
||||
* Fixes filename decoding issues with multiple endpoints related to the file manager (namely move/copy/delete).
|
||||
* **[Security]** Fixes vulnerability allowing a malicious user to abuse the remote file download utilitity to scan or access resources on the local network.
|
||||
* Fixes network `tx` stats not correctly being reported (was previously reporting `rx` for both `rx` and `tx`).
|
||||
|
||||
### Changed
|
||||
* Cleans up the logic related to polling resources for the server to make a little more sense and not do pointless `io.Copy()` operations.
|
||||
|
||||
## v1.2.0
|
||||
### Fixed
|
||||
* Fixes log compression being set on the Docker containers being created to avoid errors on some versions of Docker.
|
||||
|
||||
11
Dockerfile
11
Dockerfile
@@ -24,16 +24,9 @@ RUN upx wings
|
||||
# --------------------------------------- #
|
||||
|
||||
# Stage 2 (Final)
|
||||
FROM busybox:1.32.0
|
||||
FROM busybox:1.33.0
|
||||
|
||||
LABEL org.opencontainers.image.title="Wings"
|
||||
LABEL org.opencontainers.image.version="$VERSION"
|
||||
LABEL org.opencontainers.image.description="The server control plane for Pterodactyl Panel. Written from the ground-up with security, speed, and stability in mind."
|
||||
LABEL org.opencontainers.image.url="https://pterodactyl.io"
|
||||
LABEL org.opencontainers.image.documentation="https://pterodactyl.io/project/introduction.html"
|
||||
LABEL org.opencontainers.image.vendor="Pterodactyl Software"
|
||||
LABEL org.opencontainers.image.source="https://github.com/pterodactyl/wings"
|
||||
LABEL org.opencontainers.image.licenses="MIT"
|
||||
RUN echo "ID=\"busybox\"" > /etc/os-release
|
||||
|
||||
COPY --from=builder /app/wings /usr/bin/
|
||||
|
||||
|
||||
@@ -406,7 +406,7 @@ __ [blue][bold]Pterodactyl[reset] _____/___/_______ _______ ______
|
||||
\_____\ \/\/ / / / __ / ___/
|
||||
\___\ / / / / /_/ /___ /
|
||||
\___/\___/___/___/___/___ /______/
|
||||
/_______/ [bold]v%s[reset]
|
||||
/_______/ [bold]%s[reset]
|
||||
|
||||
Copyright © 2018 - 2021 Dane Everitt & Contributors
|
||||
|
||||
|
||||
@@ -223,6 +223,36 @@ func (c *Configuration) GetPath() string {
|
||||
// If files are not owned by this user there will be issues with permissions on Docker
|
||||
// mount points.
|
||||
func (c *Configuration) EnsurePterodactylUser() (*user.User, error) {
|
||||
sysName, err := getSystemName()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Our way of detecting if wings is running inside of Docker.
|
||||
if sysName == "busybox" {
|
||||
uid := os.Getenv("WINGS_UID")
|
||||
if uid == "" {
|
||||
uid = "988"
|
||||
}
|
||||
|
||||
gid := os.Getenv("WINGS_GID")
|
||||
if gid == "" {
|
||||
gid = "988"
|
||||
}
|
||||
|
||||
username := os.Getenv("WINGS_USERNAME")
|
||||
if username == "" {
|
||||
username = "pterodactyl"
|
||||
}
|
||||
|
||||
u := &user.User{
|
||||
Uid: uid,
|
||||
Gid: gid,
|
||||
Username: username,
|
||||
}
|
||||
return u, c.setSystemUser(u)
|
||||
}
|
||||
|
||||
u, err := user.Lookup(c.System.Username)
|
||||
|
||||
// If an error is returned but it isn't the unknown user error just abort
|
||||
@@ -233,17 +263,12 @@ func (c *Configuration) EnsurePterodactylUser() (*user.User, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sysName, err := getSystemName()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
command := fmt.Sprintf("useradd --system --no-create-home --shell /bin/false %s", c.System.Username)
|
||||
command := fmt.Sprintf("useradd --system --no-create-home --shell /usr/sbin/nologin %s", c.System.Username)
|
||||
|
||||
// Alpine Linux is the only OS we currently support that doesn't work with the useradd command, so
|
||||
// in those cases we just modify the command a bit to work as expected.
|
||||
if strings.HasPrefix(sysName, "alpine") {
|
||||
command = fmt.Sprintf("adduser -S -D -H -G %[1]s -s /bin/false %[1]s", c.System.Username)
|
||||
command = fmt.Sprintf("adduser -S -D -H -G %[1]s -s /sbin/nologin %[1]s", c.System.Username)
|
||||
|
||||
// We have to create the group first on Alpine, so do that here before continuing on
|
||||
// to the user creation process.
|
||||
@@ -267,8 +292,15 @@ func (c *Configuration) EnsurePterodactylUser() (*user.User, error) {
|
||||
// Set the system user into the configuration and then write it to the disk so that
|
||||
// it is persisted on boot.
|
||||
func (c *Configuration) setSystemUser(u *user.User) error {
|
||||
uid, _ := strconv.Atoi(u.Uid)
|
||||
gid, _ := strconv.Atoi(u.Gid)
|
||||
uid, err := strconv.Atoi(u.Uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gid, err := strconv.Atoi(u.Gid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Lock()
|
||||
c.System.Username = u.Username
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
wings:
|
||||
image: ghcr.io/pterodactyl/wings:latest
|
||||
@@ -11,7 +12,9 @@ services:
|
||||
tty: true
|
||||
environment:
|
||||
TZ: "UTC"
|
||||
DEBUG: "false"
|
||||
WINGS_UID: 988
|
||||
WINGS_GID: 988
|
||||
WINGS_USERNAME: pterodactyl
|
||||
volumes:
|
||||
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||
- "/var/lib/docker/containers/:/var/lib/docker/containers/"
|
||||
@@ -21,8 +24,9 @@ services:
|
||||
- "/tmp/pterodactyl/:/tmp/pterodactyl/"
|
||||
# you may need /srv/daemon-data if you are upgrading from an old daemon
|
||||
#- "/srv/daemon-data/:/srv/daemon-data/"
|
||||
# Required for ssl if you user let's encrypt. uncomment to use.
|
||||
# Required for ssl if you use let's encrypt. uncomment to use.
|
||||
#- "/etc/letsencrypt/:/etc/letsencrypt/"
|
||||
|
||||
networks:
|
||||
wings0:
|
||||
name: wings0
|
||||
@@ -31,4 +35,4 @@ networks:
|
||||
config:
|
||||
- subnet: "172.21.0.0/16"
|
||||
driver_opts:
|
||||
com.docker.network.bridge.name: wings0
|
||||
com.docker.network.bridge.name: wings0
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package docker
|
||||
|
||||
import "io"
|
||||
|
||||
type Console struct {
|
||||
HandlerFunc *func(string)
|
||||
}
|
||||
|
||||
var _ io.Writer = Console{}
|
||||
|
||||
func (c Console) Write(b []byte) (int, error) {
|
||||
if c.HandlerFunc != nil {
|
||||
l := make([]byte, len(b))
|
||||
copy(l, b)
|
||||
|
||||
(*c.HandlerFunc)(string(l))
|
||||
}
|
||||
|
||||
return len(b), nil
|
||||
}
|
||||
@@ -26,10 +26,26 @@ type imagePullStatus struct {
|
||||
Progress string `json:"progress"`
|
||||
}
|
||||
|
||||
// A custom console writer that allows us to keep a function blocked until the
|
||||
// given stream is properly closed. This does nothing special, only exists to
|
||||
// make a noop io.Writer.
|
||||
type noopWriter struct{}
|
||||
|
||||
var _ io.Writer = noopWriter{}
|
||||
|
||||
// Implement the required Write function to satisfy the io.Writer interface.
|
||||
func (nw noopWriter) Write(b []byte) (int, error) {
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
// Attaches to the docker container itself and ensures that we can pipe data in and out
|
||||
// of the process stream. This should not be used for reading console data as you *will*
|
||||
// miss important output at the beginning because of the time delay with attaching to the
|
||||
// output.
|
||||
//
|
||||
// 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 context will cause
|
||||
// background memory leaks as the goroutine will not exit.
|
||||
func (e *Environment) Attach() error {
|
||||
if e.IsAttached() {
|
||||
return nil
|
||||
@@ -53,10 +69,8 @@ func (e *Environment) Attach() error {
|
||||
e.SetStream(&st)
|
||||
}
|
||||
|
||||
c := new(Console)
|
||||
go func(console *Console) {
|
||||
go func() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
defer cancel()
|
||||
defer e.stream.Close()
|
||||
defer func() {
|
||||
@@ -64,27 +78,32 @@ func (e *Environment) Attach() error {
|
||||
e.SetStream(nil)
|
||||
}()
|
||||
|
||||
// Poll resources in a separate thread since this will block the copy call below
|
||||
// from being reached until it is completed if not run in a separate process. However,
|
||||
// we still want it to be stopped when the copy operation below is finished running which
|
||||
// indicates that the container is no longer running.
|
||||
go func(ctx context.Context) {
|
||||
go func() {
|
||||
if err := e.pollResources(ctx); err != nil {
|
||||
l := log.WithField("environment_id", e.Id)
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
l.WithField("error", err).Error("error during environment resource polling")
|
||||
e.log().WithField("error", err).Error("error during environment resource polling")
|
||||
} else {
|
||||
l.Warn("stopping server resource polling: context canceled")
|
||||
e.log().Warn("stopping server resource polling: context canceled")
|
||||
}
|
||||
}
|
||||
}(ctx)
|
||||
}()
|
||||
|
||||
// Stream the reader output to the console which will then fire off events and handle console
|
||||
// throttling and sending the output to the user.
|
||||
if _, err := io.Copy(console, e.stream.Reader); err != nil {
|
||||
log.WithField("environment_id", e.Id).WithField("error", err).Error("error while copying environment output to console")
|
||||
// Block the completion of this routine until the container is no longer running. This allows
|
||||
// the pollResources function to run until it needs to be stopped. Because the container
|
||||
// can be polled for resource usage, even when stopped, we need to have this logic present
|
||||
// in order to cancel the context and therefore stop the routine that is spawned.
|
||||
//
|
||||
// For now, DO NOT use client#ContainerWait from the Docker package. There is a nasty
|
||||
// bug causing containers to hang on deletion and cause servers to lock up on the system.
|
||||
//
|
||||
// This weird code isn't intuitive, but it keeps the function from ending until the container
|
||||
// is stopped and therefore the stream reader ends up closed.
|
||||
// @see https://github.com/moby/moby/issues/41827
|
||||
c := new(noopWriter)
|
||||
if _, err := io.Copy(c, e.stream.Reader); err != nil {
|
||||
e.log().WithField("error", err).Error("could not copy from environment stream to noop writer")
|
||||
}
|
||||
}(c)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -259,6 +278,8 @@ func (e *Environment) Destroy() error {
|
||||
Force: true,
|
||||
})
|
||||
|
||||
e.SetState(environment.ProcessOfflineState)
|
||||
|
||||
// Don't trigger a destroy failure if we try to delete a container that does not
|
||||
// exist on the system. We're just a step ahead of ourselves in that case.
|
||||
//
|
||||
@@ -267,8 +288,6 @@ func (e *Environment) Destroy() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
e.SetState(environment.ProcessOfflineState)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -280,7 +299,6 @@ func (e *Environment) followOutput() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return errors.New(fmt.Sprintf("no such container: %s", e.Id))
|
||||
}
|
||||
|
||||
@@ -295,19 +313,39 @@ func (e *Environment) followOutput() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func(reader io.ReadCloser) {
|
||||
defer reader.Close()
|
||||
evts := e.Events()
|
||||
err := system.ScanReader(reader, func(line string) {
|
||||
evts.Publish(environment.ConsoleOutputEvent, line)
|
||||
})
|
||||
if err != nil && err != io.EOF {
|
||||
log.WithField("error", err).WithField("container_id", e.Id).Warn("error processing scanner line in console output")
|
||||
}
|
||||
}(reader)
|
||||
|
||||
go e.scanOutput(reader)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Environment) scanOutput(reader io.ReadCloser) {
|
||||
defer reader.Close()
|
||||
|
||||
events := e.Events()
|
||||
|
||||
err := system.ScanReader(reader, func(line string) {
|
||||
events.Publish(environment.ConsoleOutputEvent, line)
|
||||
})
|
||||
|
||||
if err != nil && err != io.EOF {
|
||||
log.WithField("error", err).WithField("container_id", e.Id).Warn("error processing scanner line in console output")
|
||||
return
|
||||
}
|
||||
|
||||
// Return here if the server is offline or currently stopping.
|
||||
if e.State() == environment.ProcessStoppingState || e.State() == environment.ProcessOfflineState {
|
||||
return
|
||||
}
|
||||
|
||||
// Close the current reader before starting a new one, the defer will still run
|
||||
// but it will do nothing if we already closed the stream.
|
||||
_ = reader.Close()
|
||||
|
||||
// Start following the output of the server again.
|
||||
go e.followOutput()
|
||||
}
|
||||
|
||||
// Pulls the image from Docker. If there is an error while pulling the image from the source
|
||||
// but the image already exists locally, we will report that error to the logger but continue
|
||||
// with the process.
|
||||
@@ -391,9 +429,11 @@ func (e *Environment) ensureImageExists(image string) error {
|
||||
// I'm not sure what the best approach here is, but this will block execution until the image
|
||||
// is done being pulled, which is what we need.
|
||||
scanner := bufio.NewScanner(out)
|
||||
|
||||
for scanner.Scan() {
|
||||
s := imagePullStatus{}
|
||||
fmt.Println(scanner.Text())
|
||||
|
||||
if err := json.Unmarshal(scanner.Bytes(), &s); err == nil {
|
||||
e.Events().Publish(environment.DockerImagePullStatus, s.Status+" "+s.Progress)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/apex/log"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pterodactyl/wings/api"
|
||||
@@ -70,6 +71,10 @@ func New(id string, m *Metadata, c *environment.Configuration) (*Environment, er
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func (e *Environment) log() *log.Entry {
|
||||
return log.WithField("environment", e.Type()).WithField("container_id", e.Id)
|
||||
}
|
||||
|
||||
func (e *Environment) Type() string {
|
||||
return "docker"
|
||||
}
|
||||
@@ -77,8 +82,9 @@ func (e *Environment) Type() string {
|
||||
// Set if this process is currently attached to the process.
|
||||
func (e *Environment) SetStream(s *types.HijackedResponse) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
e.stream = s
|
||||
e.mu.Unlock()
|
||||
}
|
||||
|
||||
// Determine if the this process is currently attached to the container.
|
||||
@@ -93,6 +99,7 @@ func (e *Environment) Events() *events.EventBus {
|
||||
e.eventMu.Do(func() {
|
||||
e.emitter = events.New()
|
||||
})
|
||||
|
||||
return e.emitter
|
||||
}
|
||||
|
||||
@@ -169,12 +176,14 @@ func (e *Environment) Config() *environment.Configuration {
|
||||
// Sets the stop configuration for the environment.
|
||||
func (e *Environment) SetStopConfiguration(c api.ProcessStopConfiguration) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
e.meta.Stop = c
|
||||
e.mu.Unlock()
|
||||
}
|
||||
|
||||
func (e *Environment) SetImage(i string) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
e.meta.Image = i
|
||||
e.mu.Unlock()
|
||||
}
|
||||
|
||||
@@ -20,10 +20,9 @@ import (
|
||||
//
|
||||
// This process will also confirm that the server environment exists and is in a bootable
|
||||
// state. This ensures that unexpected container deletion while Wings is running does
|
||||
// not result in the server becoming unbootable.
|
||||
// not result in the server becoming un-bootable.
|
||||
func (e *Environment) OnBeforeStart() error {
|
||||
// Always destroy and re-create the server container to ensure that synced data from
|
||||
// the Panel is usee.
|
||||
// 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 !client.IsErrNotFound(err) {
|
||||
return errors.WithMessage(err, "failed to remove server docker container during pre-boot")
|
||||
@@ -49,6 +48,7 @@ func (e *Environment) OnBeforeStart() error {
|
||||
// call to OnBeforeStart().
|
||||
func (e *Environment) Start() error {
|
||||
sawError := false
|
||||
|
||||
// If sawError is set to true there was an error somewhere in the pipeline that
|
||||
// got passed up, but we also want to ensure we set the server to be offline at
|
||||
// that point.
|
||||
@@ -235,7 +235,7 @@ func (e *Environment) Terminate(signal os.Signal) error {
|
||||
|
||||
sig := strings.TrimSuffix(strings.TrimPrefix(signal.String(), "signal "), "ed")
|
||||
|
||||
if err := e.client.ContainerKill(context.Background(), e.Id, sig); err != nil {
|
||||
if err := e.client.ContainerKill(context.Background(), e.Id, sig); err != nil && !client.IsErrNotFound(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,10 @@ import (
|
||||
"context"
|
||||
"emperror.dev/errors"
|
||||
"encoding/json"
|
||||
"github.com/apex/log"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
"io"
|
||||
"math"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// Attach to the instance and then automatically emit an event whenever the resource usage for the
|
||||
@@ -19,63 +17,51 @@ func (e *Environment) pollResources(ctx context.Context) error {
|
||||
return errors.New("cannot enable resource polling on a stopped server")
|
||||
}
|
||||
|
||||
l := log.WithField("container_id", e.Id)
|
||||
l.Debug("starting resource polling for container")
|
||||
defer l.Debug("stopped resource polling for container")
|
||||
e.log().Info("starting resource polling for container")
|
||||
defer e.log().Debug("stopped resource polling for container")
|
||||
|
||||
stats, err := e.client.ContainerStats(context.Background(), e.Id, true)
|
||||
stats, err := e.client.ContainerStats(ctx, e.Id, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stats.Body.Close()
|
||||
|
||||
dec := json.NewDecoder(stats.Body)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
var v *types.StatsJSON
|
||||
|
||||
var v types.StatsJSON
|
||||
if err := dec.Decode(&v); err != nil {
|
||||
if err != io.EOF {
|
||||
l.WithField("error", err).Warn("error while processing Docker stats output for container")
|
||||
if err != io.EOF && !errors.Is(err, context.Canceled) {
|
||||
e.log().WithField("error", err).Warn("error while processing Docker stats output for container")
|
||||
} else {
|
||||
l.Debug("io.EOF encountered during stats decode, stopping polling...")
|
||||
e.log().Debug("io.EOF encountered during stats decode, stopping polling...")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disable collection if the server is in an offline state and this process is still running.
|
||||
if e.st.Load() == environment.ProcessOfflineState {
|
||||
l.Debug("process in offline state while resource polling is still active; stopping poll")
|
||||
e.log().Debug("process in offline state while resource polling is still active; stopping poll")
|
||||
return nil
|
||||
}
|
||||
|
||||
var rx uint64
|
||||
var tx uint64
|
||||
for _, nw := range v.Networks {
|
||||
atomic.AddUint64(&rx, nw.RxBytes)
|
||||
atomic.AddUint64(&tx, nw.RxBytes)
|
||||
}
|
||||
|
||||
st := environment.Stats{
|
||||
Memory: calculateDockerMemory(v.MemoryStats),
|
||||
MemoryLimit: v.MemoryStats.Limit,
|
||||
CpuAbsolute: calculateDockerAbsoluteCpu(&v.PreCPUStats, &v.CPUStats),
|
||||
Network: struct {
|
||||
RxBytes uint64 `json:"rx_bytes"`
|
||||
TxBytes uint64 `json:"tx_bytes"`
|
||||
}{
|
||||
RxBytes: rx,
|
||||
TxBytes: tx,
|
||||
},
|
||||
CpuAbsolute: calculateDockerAbsoluteCpu(v.PreCPUStats, v.CPUStats),
|
||||
Network: environment.NetworkStats{},
|
||||
}
|
||||
|
||||
for _, nw := range v.Networks {
|
||||
st.Network.RxBytes += nw.RxBytes
|
||||
st.Network.TxBytes += nw.TxBytes
|
||||
}
|
||||
|
||||
if b, err := json.Marshal(st); err != nil {
|
||||
l.WithField("error", err).Warn("error while marshaling stats object for environment")
|
||||
e.log().WithField("error", err).Warn("error while marshaling stats object for environment")
|
||||
} else {
|
||||
e.Events().Publish(environment.ResourceEvent, string(b))
|
||||
}
|
||||
@@ -108,7 +94,7 @@ func calculateDockerMemory(stats types.MemoryStats) uint64 {
|
||||
// by the defined CPU limits on the container.
|
||||
//
|
||||
// @see https://github.com/docker/cli/blob/aa097cf1aa19099da70930460250797c8920b709/cli/command/container/stats_helpers.go#L166
|
||||
func calculateDockerAbsoluteCpu(pStats *types.CPUStats, stats *types.CPUStats) float64 {
|
||||
func calculateDockerAbsoluteCpu(pStats types.CPUStats, stats types.CPUStats) float64 {
|
||||
// Calculate the change in CPU usage between the current and previous reading.
|
||||
cpuDelta := float64(stats.CPUUsage.TotalUsage) - float64(pStats.CPUUsage.TotalUsage)
|
||||
|
||||
|
||||
@@ -19,8 +19,9 @@ var ErrNotAttached = errors.New("not attached to instance")
|
||||
|
||||
func (e *Environment) setStream(s *types.HijackedResponse) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
e.stream = s
|
||||
e.mu.Unlock()
|
||||
}
|
||||
|
||||
// Sends the specified command to the stdin of the running container instance. There is no
|
||||
@@ -71,7 +72,7 @@ func (e *Environment) Readlog(lines int) ([]string, error) {
|
||||
// Docker stores the logs for server output in a JSON format. This function will iterate over the JSON
|
||||
// that was read from the log file and parse it into a more human readable format.
|
||||
func (e *Environment) parseLogToStrings(b []byte) ([]string, error) {
|
||||
var hasError = false
|
||||
hasError := false
|
||||
var out []string
|
||||
|
||||
scanner := bufio.NewScanner(bytes.NewReader(b))
|
||||
|
||||
@@ -24,8 +24,10 @@ type Stats struct {
|
||||
// Disk int64 `json:"disk_bytes"`
|
||||
|
||||
// Current network transmit in & out for a container.
|
||||
Network struct {
|
||||
RxBytes uint64 `json:"rx_bytes"`
|
||||
TxBytes uint64 `json:"tx_bytes"`
|
||||
} `json:"network"`
|
||||
Network NetworkStats `json:"network"`
|
||||
}
|
||||
|
||||
type NetworkStats struct {
|
||||
RxBytes uint64 `json:"rx_bytes"`
|
||||
TxBytes uint64 `json:"tx_bytes"`
|
||||
}
|
||||
|
||||
@@ -4,17 +4,50 @@ import (
|
||||
"context"
|
||||
"emperror.dev/errors"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pterodactyl/wings/server"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var client = &http.Client{Timeout: time.Hour * 12}
|
||||
var instance = &Downloader{
|
||||
// Tracks all of the active downloads.
|
||||
downloadCache: make(map[string]*Download),
|
||||
// Tracks all of the downloads active for a given server instance. This is
|
||||
// primarily used to make things quicker and keep the code a little more
|
||||
// legible throughout here.
|
||||
serverCache: make(map[string][]string),
|
||||
}
|
||||
|
||||
// Regex to match the end of an IPv4/IPv6 address. This allows the port to be removed
|
||||
// so that we are just working with the raw IP address in question.
|
||||
var ipMatchRegex = regexp.MustCompile(`(:\d+)$`)
|
||||
|
||||
// Internal IP ranges that should be blocked if the resource requested resolves within.
|
||||
var internalRanges = []*net.IPNet{
|
||||
mustParseCIDR("127.0.0.1/8"),
|
||||
mustParseCIDR("10.0.0.0/8"),
|
||||
mustParseCIDR("172.16.0.0/12"),
|
||||
mustParseCIDR("192.168.0.0/16"),
|
||||
mustParseCIDR("169.254.0.0/16"),
|
||||
mustParseCIDR("::1/128"),
|
||||
mustParseCIDR("fe80::/10"),
|
||||
mustParseCIDR("fc00::/7"),
|
||||
}
|
||||
|
||||
const ErrInternalResolution = errors.Sentinel("downloader: destination resolves to internal network location")
|
||||
const ErrInvalidIPAddress = errors.Sentinel("downloader: invalid IP address")
|
||||
const ErrDownloadFailed = errors.Sentinel("downloader: download request failed")
|
||||
|
||||
type Counter struct {
|
||||
total int
|
||||
onWrite func(total int)
|
||||
@@ -27,12 +60,6 @@ func (c *Counter) Write(p []byte) (int, error) {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
type Downloader struct {
|
||||
mu sync.RWMutex
|
||||
downloadCache map[string]*Download
|
||||
serverCache map[string][]string
|
||||
}
|
||||
|
||||
type DownloadRequest struct {
|
||||
URL *url.URL
|
||||
Directory string
|
||||
@@ -47,16 +74,6 @@ type Download struct {
|
||||
cancelFunc *context.CancelFunc
|
||||
}
|
||||
|
||||
var client = &http.Client{Timeout: time.Hour * 12}
|
||||
var instance = &Downloader{
|
||||
// Tracks all of the active downloads.
|
||||
downloadCache: make(map[string]*Download),
|
||||
// Tracks all of the downloads active for a given server instance. This is
|
||||
// primarily used to make things quicker and keep the code a little more
|
||||
// legible throughout here.
|
||||
serverCache: make(map[string][]string),
|
||||
}
|
||||
|
||||
// Starts a new tracked download which allows for cancelation later on by calling
|
||||
// the Downloader.Cancel function.
|
||||
func New(s *server.Server, r DownloadRequest) *Download {
|
||||
@@ -108,15 +125,24 @@ func (dl *Download) Execute() error {
|
||||
dl.cancelFunc = &cancel
|
||||
defer dl.Cancel()
|
||||
|
||||
// Always ensure that we're checking the destination for the download to avoid a malicious
|
||||
// user from accessing internal network resources.
|
||||
if err := dl.isExternalNetwork(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// At this point we have verified the destination is not within the local network, so we can
|
||||
// now make a request to that URL and pull down the file, saving it to the server's data
|
||||
// directory.
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, dl.req.URL.String(), nil)
|
||||
if err != nil {
|
||||
return errors.WrapIf(err, "downloader: failed to create request")
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "Pterodactyl Panel (https://pterodactyl.io)")
|
||||
res, err := client.Do(req) // lgtm [go/request-forgery]
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return errors.New("downloader: failed opening request to download file")
|
||||
return ErrDownloadFailed
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
@@ -178,6 +204,52 @@ func (dl *Download) counter(contentLength int64) *Counter {
|
||||
}
|
||||
}
|
||||
|
||||
// Verifies that a given download resolves to a location not within the current local
|
||||
// network for the machine. If the final destination of a resource is within the local
|
||||
// network an ErrInternalResolution error is returned.
|
||||
func (dl *Download) isExternalNetwork(ctx context.Context) error {
|
||||
dialer := &net.Dialer{
|
||||
LocalAddr: nil,
|
||||
}
|
||||
|
||||
host := dl.req.URL.Host
|
||||
if !ipMatchRegex.MatchString(host) {
|
||||
if dl.req.URL.Scheme == "https" {
|
||||
host = host + ":443"
|
||||
} else {
|
||||
host = host + ":80"
|
||||
}
|
||||
}
|
||||
|
||||
c, err := dialer.DialContext(ctx, "tcp", host)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
c.Close()
|
||||
|
||||
ip := net.ParseIP(ipMatchRegex.ReplaceAllString(c.RemoteAddr().String(), ""))
|
||||
if ip == nil {
|
||||
return errors.WithStack(ErrInvalidIPAddress)
|
||||
}
|
||||
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsInterfaceLocalMulticast() {
|
||||
return errors.WithStack(ErrInternalResolution)
|
||||
}
|
||||
for _, block := range internalRanges {
|
||||
if block.Contains(ip) {
|
||||
return errors.WithStack(ErrInternalResolution)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Defines a global downloader struct that keeps track of all currently processing downloads
|
||||
// for the machine.
|
||||
type Downloader struct {
|
||||
mu sync.RWMutex
|
||||
downloadCache map[string]*Download
|
||||
serverCache map[string][]string
|
||||
}
|
||||
|
||||
// Tracks a download in the internal cache for this instance.
|
||||
func (d *Downloader) track(dl *Download) {
|
||||
d.mu.Lock()
|
||||
@@ -222,3 +294,11 @@ func (d *Downloader) remove(dlid string) {
|
||||
d.serverCache[sid] = out
|
||||
}
|
||||
}
|
||||
|
||||
func mustParseCIDR(ip string) *net.IPNet {
|
||||
_, block, err := net.ParseCIDR(ip)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("downloader: failed to parse CIDR: %s", err))
|
||||
}
|
||||
return block
|
||||
}
|
||||
|
||||
@@ -2,14 +2,6 @@ package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"emperror.dev/errors"
|
||||
"github.com/apex/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pterodactyl/wings/router/downloader"
|
||||
"github.com/pterodactyl/wings/router/tokens"
|
||||
"github.com/pterodactyl/wings/server"
|
||||
"github.com/pterodactyl/wings/server/filesystem"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -18,16 +10,21 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"emperror.dev/errors"
|
||||
"github.com/apex/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pterodactyl/wings/router/downloader"
|
||||
"github.com/pterodactyl/wings/router/tokens"
|
||||
"github.com/pterodactyl/wings/server"
|
||||
"github.com/pterodactyl/wings/server/filesystem"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// Returns the contents of a file on the server.
|
||||
func getServerFileContents(c *gin.Context) {
|
||||
s := ExtractServer(c)
|
||||
f, err := url.QueryUnescape(c.Query("file"))
|
||||
if err != nil {
|
||||
WithError(c, err)
|
||||
return
|
||||
}
|
||||
f := c.Query("file")
|
||||
p := "/" + strings.TrimLeft(f, "/")
|
||||
st, err := s.Filesystem().Stat(p)
|
||||
if err != nil {
|
||||
@@ -64,11 +61,7 @@ func getServerFileContents(c *gin.Context) {
|
||||
// Returns the contents of a directory for a server.
|
||||
func getServerListDirectory(c *gin.Context) {
|
||||
s := ExtractServer(c)
|
||||
dir, err := url.QueryUnescape(c.Query("directory"))
|
||||
if err != nil {
|
||||
WithError(c, err)
|
||||
return
|
||||
}
|
||||
dir := c.Query("directory")
|
||||
if stats, err := s.Filesystem().ListDirectory(dir); err != nil {
|
||||
WithError(c, err)
|
||||
} else {
|
||||
@@ -212,11 +205,7 @@ func postServerDeleteFiles(c *gin.Context) {
|
||||
func postServerWriteFile(c *gin.Context) {
|
||||
s := GetServer(c.Param("server"))
|
||||
|
||||
f, err := url.QueryUnescape(c.Query("file"))
|
||||
if err != nil {
|
||||
NewServerError(err, s).Abort(c)
|
||||
return
|
||||
}
|
||||
f := c.Query("file")
|
||||
f = "/" + strings.TrimLeft(f, "/")
|
||||
|
||||
if err := s.Filesystem().Writefile(f, c.Request.Body); err != nil {
|
||||
|
||||
@@ -129,6 +129,9 @@ func postServerArchive(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Mark the server as not being transferred so it can actually be used.
|
||||
s.SetTransferring(false)
|
||||
|
||||
s.Events().Publish(server.TransferStatusEvent, "failure")
|
||||
|
||||
sendTransferLog("Attempting to notify panel of archive failure..")
|
||||
|
||||
@@ -64,9 +64,11 @@ func (s *Server) StartEventListeners() {
|
||||
// to terminate again.
|
||||
if s.Environment.State() != environment.ProcessStoppingState {
|
||||
s.Environment.SetState(environment.ProcessStoppingState)
|
||||
|
||||
go func() {
|
||||
s.Log().Warn("stopping server instance, violating throttle limits")
|
||||
s.PublishConsoleOutputFromDaemon("Your server is being stopped for outputting too much data in a short period of time.")
|
||||
|
||||
// Completely skip over server power actions and terminate the running instance. This gives the
|
||||
// server 15 seconds to finish stopping gracefully before it is forcefully terminated.
|
||||
if err := s.Environment.WaitForStop(config.Get().Throttles.StopGracePeriod, true); err != nil {
|
||||
|
||||
@@ -2,5 +2,5 @@ package system
|
||||
|
||||
var (
|
||||
// The current version of this software.
|
||||
Version = "0.0.1"
|
||||
Version = "v0.0.1"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user