Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e74123c65 | ||
|
|
b82f5f9a32 | ||
|
|
1937d0366d | ||
|
|
963a906c30 | ||
|
|
3f6eb7e41a | ||
|
|
a822c7c340 | ||
|
|
b8fb86f5a4 | ||
|
|
ee0c7f09b3 | ||
|
|
d3ddf8cf39 |
@@ -48,3 +48,9 @@ debug
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
*.pprof
|
*.pprof
|
||||||
*.pdf
|
*.pdf
|
||||||
|
|
||||||
|
Dockerfile
|
||||||
|
CHANGELOG.md
|
||||||
|
Makefile
|
||||||
|
README.md
|
||||||
|
wings-api.paw
|
||||||
|
|||||||
11
CHANGELOG.md
11
CHANGELOG.md
@@ -1,5 +1,16 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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
|
## v1.2.0
|
||||||
### Fixed
|
### Fixed
|
||||||
* Fixes log compression being set on the Docker containers being created to avoid errors on some versions of Docker.
|
* 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)
|
# Stage 2 (Final)
|
||||||
FROM busybox:1.32.0
|
FROM busybox:1.33.0
|
||||||
|
|
||||||
LABEL org.opencontainers.image.title="Wings"
|
RUN echo "ID=\"busybox\"" > /etc/os-release
|
||||||
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"
|
|
||||||
|
|
||||||
COPY --from=builder /app/wings /usr/bin/
|
COPY --from=builder /app/wings /usr/bin/
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
// If files are not owned by this user there will be issues with permissions on Docker
|
||||||
// mount points.
|
// mount points.
|
||||||
func (c *Configuration) EnsurePterodactylUser() (*user.User, error) {
|
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)
|
u, err := user.Lookup(c.System.Username)
|
||||||
|
|
||||||
// If an error is returned but it isn't the unknown user error just abort
|
// 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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sysName, err := getSystemName()
|
command := fmt.Sprintf("useradd --system --no-create-home --shell /usr/sbin/nologin %s", c.System.Username)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
command := fmt.Sprintf("useradd --system --no-create-home --shell /bin/false %s", c.System.Username)
|
|
||||||
|
|
||||||
// Alpine Linux is the only OS we currently support that doesn't work with the useradd command, so
|
// Alpine Linux is the only OS we currently support that doesn't work with the useradd command, so
|
||||||
// in those cases we just modify the command a bit to work as expected.
|
// in those cases we just modify the command a bit to work as expected.
|
||||||
if strings.HasPrefix(sysName, "alpine") {
|
if strings.HasPrefix(sysName, "alpine") {
|
||||||
command = fmt.Sprintf("adduser -S -D -H -G %[1]s -s /bin/false %[1]s", c.System.Username)
|
command = fmt.Sprintf("adduser -S -D -H -G %[1]s -s /sbin/nologin %[1]s", c.System.Username)
|
||||||
|
|
||||||
// We have to create the group first on Alpine, so do that here before continuing on
|
// We have to create the group first on Alpine, so do that here before continuing on
|
||||||
// to the user creation process.
|
// to the user creation process.
|
||||||
@@ -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
|
// Set the system user into the configuration and then write it to the disk so that
|
||||||
// it is persisted on boot.
|
// it is persisted on boot.
|
||||||
func (c *Configuration) setSystemUser(u *user.User) error {
|
func (c *Configuration) setSystemUser(u *user.User) error {
|
||||||
uid, _ := strconv.Atoi(u.Uid)
|
uid, err := strconv.Atoi(u.Uid)
|
||||||
gid, _ := strconv.Atoi(u.Gid)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
gid, err := strconv.Atoi(u.Gid)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
c.Lock()
|
c.Lock()
|
||||||
c.System.Username = u.Username
|
c.System.Username = u.Username
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
wings:
|
wings:
|
||||||
image: ghcr.io/pterodactyl/wings:latest
|
image: ghcr.io/pterodactyl/wings:latest
|
||||||
@@ -11,7 +12,9 @@ services:
|
|||||||
tty: true
|
tty: true
|
||||||
environment:
|
environment:
|
||||||
TZ: "UTC"
|
TZ: "UTC"
|
||||||
DEBUG: "false"
|
WINGS_UID: 988
|
||||||
|
WINGS_GID: 988
|
||||||
|
WINGS_USERNAME: pterodactyl
|
||||||
volumes:
|
volumes:
|
||||||
- "/var/run/docker.sock:/var/run/docker.sock"
|
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||||
- "/var/lib/docker/containers/:/var/lib/docker/containers/"
|
- "/var/lib/docker/containers/:/var/lib/docker/containers/"
|
||||||
@@ -21,8 +24,9 @@ services:
|
|||||||
- "/tmp/pterodactyl/:/tmp/pterodactyl/"
|
- "/tmp/pterodactyl/:/tmp/pterodactyl/"
|
||||||
# you may need /srv/daemon-data if you are upgrading from an old daemon
|
# you may need /srv/daemon-data if you are upgrading from an old daemon
|
||||||
#- "/srv/daemon-data/:/srv/daemon-data/"
|
#- "/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/"
|
#- "/etc/letsencrypt/:/etc/letsencrypt/"
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
wings0:
|
wings0:
|
||||||
name: wings0
|
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
|
|
||||||
}
|
|
||||||
@@ -30,6 +30,10 @@ type imagePullStatus struct {
|
|||||||
// of the process stream. This should not be used for reading console data as you *will*
|
// 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
|
// miss important output at the beginning because of the time delay with attaching to the
|
||||||
// output.
|
// 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 {
|
func (e *Environment) Attach() error {
|
||||||
if e.IsAttached() {
|
if e.IsAttached() {
|
||||||
return nil
|
return nil
|
||||||
@@ -53,38 +57,47 @@ func (e *Environment) Attach() error {
|
|||||||
e.SetStream(&st)
|
e.SetStream(&st)
|
||||||
}
|
}
|
||||||
|
|
||||||
c := new(Console)
|
go func() {
|
||||||
go func(console *Console) {
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
defer cancel()
|
defer cancel()
|
||||||
defer e.stream.Close()
|
|
||||||
defer func() {
|
defer func() {
|
||||||
|
e.stream.Close()
|
||||||
e.SetState(environment.ProcessOfflineState)
|
e.SetState(environment.ProcessOfflineState)
|
||||||
e.SetStream(nil)
|
e.SetStream(nil)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Poll resources in a separate thread since this will block the copy call below
|
go func() {
|
||||||
// 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) {
|
|
||||||
if err := e.pollResources(ctx); err != nil {
|
if err := e.pollResources(ctx); err != nil {
|
||||||
l := log.WithField("environment_id", e.Id)
|
|
||||||
if !errors.Is(err, context.Canceled) {
|
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 {
|
} 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
|
// Block the completion of this routine until the container is no longer running. This allows
|
||||||
// throttling and sending the output to the user.
|
// the pollResources function to run until it needs to be stopped. Because the container
|
||||||
if _, err := io.Copy(console, e.stream.Reader); err != nil {
|
// can be polled for resource usage, even when sropped, we need to have this logic present
|
||||||
log.WithField("environment_id", e.Id).WithField("error", err).Error("error while copying environment output to console")
|
// in order to cancel the context and therefore stop the routine that is spawned.
|
||||||
|
ok, err := e.client.ContainerWait(ctx, e.Id, container.WaitConditionNotRunning)
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
// Do nothing, the context was canceled by a different process, there is no error
|
||||||
|
// to report at this point.
|
||||||
|
e.log().Debug("terminating ContainerWait blocking process, context canceled")
|
||||||
|
return
|
||||||
|
case _ = <-err:
|
||||||
|
// An error occurred with the ContainerWait call, report it here and then hope
|
||||||
|
// for the fucking best I guess?
|
||||||
|
e.log().WithField("error", err).Error("error while blocking using ContainerWait")
|
||||||
|
return
|
||||||
|
case <-ok:
|
||||||
|
// Do nothing, everything is running as expected. This will allow us to keep
|
||||||
|
// blocking the termination of this function until the container stops at which
|
||||||
|
// point all of our deferred functions can run.
|
||||||
}
|
}
|
||||||
}(c)
|
}()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -280,7 +293,6 @@ func (e *Environment) followOutput() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.New(fmt.Sprintf("no such container: %s", e.Id))
|
return errors.New(fmt.Sprintf("no such container: %s", e.Id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package docker
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"github.com/apex/log"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/pterodactyl/wings/api"
|
"github.com/pterodactyl/wings/api"
|
||||||
@@ -70,6 +71,10 @@ func New(id string, m *Metadata, c *environment.Configuration) (*Environment, er
|
|||||||
return e, nil
|
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 {
|
func (e *Environment) Type() string {
|
||||||
return "docker"
|
return "docker"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"emperror.dev/errors"
|
"emperror.dev/errors"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/apex/log"
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/pterodactyl/wings/environment"
|
"github.com/pterodactyl/wings/environment"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"sync/atomic"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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
|
||||||
@@ -19,63 +17,51 @@ func (e *Environment) pollResources(ctx context.Context) error {
|
|||||||
return errors.New("cannot enable resource polling on a stopped server")
|
return errors.New("cannot enable resource polling on a stopped server")
|
||||||
}
|
}
|
||||||
|
|
||||||
l := log.WithField("container_id", e.Id)
|
e.log().Info("starting resource polling for container")
|
||||||
l.Debug("starting resource polling for container")
|
defer e.log().Debug("stopped resource polling for container")
|
||||||
defer l.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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer stats.Body.Close()
|
defer stats.Body.Close()
|
||||||
|
|
||||||
dec := json.NewDecoder(stats.Body)
|
dec := json.NewDecoder(stats.Body)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
default:
|
default:
|
||||||
var v *types.StatsJSON
|
var v types.StatsJSON
|
||||||
|
|
||||||
if err := dec.Decode(&v); err != nil {
|
if err := dec.Decode(&v); err != nil {
|
||||||
if err != io.EOF {
|
if err != io.EOF && !errors.Is(err, context.Canceled) {
|
||||||
l.WithField("error", err).Warn("error while processing Docker stats output for container")
|
e.log().WithField("error", err).Warn("error while processing Docker stats output for container")
|
||||||
} else {
|
} else {
|
||||||
l.Debug("io.EOF encountered during stats decode, stopping polling...")
|
e.log().Debug("io.EOF encountered during stats decode, stopping polling...")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable collection if the server is in an offline state and this process is still running.
|
// Disable collection if the server is in an offline state and this process is still running.
|
||||||
if e.st.Load() == environment.ProcessOfflineState {
|
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
|
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{
|
st := environment.Stats{
|
||||||
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),
|
||||||
Network: struct {
|
Network: environment.NetworkStats{},
|
||||||
RxBytes uint64 `json:"rx_bytes"`
|
}
|
||||||
TxBytes uint64 `json:"tx_bytes"`
|
|
||||||
}{
|
for _, nw := range v.Networks {
|
||||||
RxBytes: rx,
|
st.Network.RxBytes += nw.RxBytes
|
||||||
TxBytes: tx,
|
st.Network.TxBytes += nw.TxBytes
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if b, err := json.Marshal(st); err != nil {
|
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 {
|
} else {
|
||||||
e.Events().Publish(environment.ResourceEvent, string(b))
|
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.
|
// by the defined CPU limits on the container.
|
||||||
//
|
//
|
||||||
// @see https://github.com/docker/cli/blob/aa097cf1aa19099da70930460250797c8920b709/cli/command/container/stats_helpers.go#L166
|
// @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.
|
// Calculate the change in CPU usage between the current and previous reading.
|
||||||
cpuDelta := float64(stats.CPUUsage.TotalUsage) - float64(pStats.CPUUsage.TotalUsage)
|
cpuDelta := float64(stats.CPUUsage.TotalUsage) - float64(pStats.CPUUsage.TotalUsage)
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,10 @@ type Stats struct {
|
|||||||
// Disk int64 `json:"disk_bytes"`
|
// Disk int64 `json:"disk_bytes"`
|
||||||
|
|
||||||
// Current network transmit in & out for a container.
|
// Current network transmit in & out for a container.
|
||||||
Network struct {
|
Network NetworkStats `json:"network"`
|
||||||
RxBytes uint64 `json:"rx_bytes"`
|
}
|
||||||
TxBytes uint64 `json:"tx_bytes"`
|
|
||||||
} `json:"network"`
|
type NetworkStats struct {
|
||||||
|
RxBytes uint64 `json:"rx_bytes"`
|
||||||
|
TxBytes uint64 `json:"tx_bytes"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,50 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"emperror.dev/errors"
|
"emperror.dev/errors"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/pterodactyl/wings/server"
|
"github.com/pterodactyl/wings/server"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"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 {
|
type Counter struct {
|
||||||
total int
|
total int
|
||||||
onWrite func(total int)
|
onWrite func(total int)
|
||||||
@@ -27,12 +60,6 @@ func (c *Counter) Write(p []byte) (int, error) {
|
|||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Downloader struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
downloadCache map[string]*Download
|
|
||||||
serverCache map[string][]string
|
|
||||||
}
|
|
||||||
|
|
||||||
type DownloadRequest struct {
|
type DownloadRequest struct {
|
||||||
URL *url.URL
|
URL *url.URL
|
||||||
Directory string
|
Directory string
|
||||||
@@ -47,16 +74,6 @@ type Download struct {
|
|||||||
cancelFunc *context.CancelFunc
|
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
|
// Starts a new tracked download which allows for cancelation later on by calling
|
||||||
// the Downloader.Cancel function.
|
// the Downloader.Cancel function.
|
||||||
func New(s *server.Server, r DownloadRequest) *Download {
|
func New(s *server.Server, r DownloadRequest) *Download {
|
||||||
@@ -108,15 +125,24 @@ func (dl *Download) Execute() error {
|
|||||||
dl.cancelFunc = &cancel
|
dl.cancelFunc = &cancel
|
||||||
defer dl.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)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, dl.req.URL.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WrapIf(err, "downloader: failed to create request")
|
return errors.WrapIf(err, "downloader: failed to create request")
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("User-Agent", "Pterodactyl Panel (https://pterodactyl.io)")
|
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 {
|
if err != nil {
|
||||||
return errors.New("downloader: failed opening request to download file")
|
return ErrDownloadFailed
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode != http.StatusOK {
|
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.
|
// Tracks a download in the internal cache for this instance.
|
||||||
func (d *Downloader) track(dl *Download) {
|
func (d *Downloader) track(dl *Download) {
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
@@ -222,3 +294,11 @@ func (d *Downloader) remove(dlid string) {
|
|||||||
d.serverCache[sid] = out
|
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 (
|
import (
|
||||||
"context"
|
"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"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -18,16 +10,21 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"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.
|
// Returns the contents of a file on the server.
|
||||||
func getServerFileContents(c *gin.Context) {
|
func getServerFileContents(c *gin.Context) {
|
||||||
s := ExtractServer(c)
|
s := ExtractServer(c)
|
||||||
f, err := url.QueryUnescape(c.Query("file"))
|
f := c.Query("file")
|
||||||
if err != nil {
|
|
||||||
WithError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
p := "/" + strings.TrimLeft(f, "/")
|
p := "/" + strings.TrimLeft(f, "/")
|
||||||
st, err := s.Filesystem().Stat(p)
|
st, err := s.Filesystem().Stat(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -64,11 +61,7 @@ func getServerFileContents(c *gin.Context) {
|
|||||||
// Returns the contents of a directory for a server.
|
// Returns the contents of a directory for a server.
|
||||||
func getServerListDirectory(c *gin.Context) {
|
func getServerListDirectory(c *gin.Context) {
|
||||||
s := ExtractServer(c)
|
s := ExtractServer(c)
|
||||||
dir, err := url.QueryUnescape(c.Query("directory"))
|
dir := c.Query("directory")
|
||||||
if err != nil {
|
|
||||||
WithError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if stats, err := s.Filesystem().ListDirectory(dir); err != nil {
|
if stats, err := s.Filesystem().ListDirectory(dir); err != nil {
|
||||||
WithError(c, err)
|
WithError(c, err)
|
||||||
} else {
|
} else {
|
||||||
@@ -212,11 +205,7 @@ func postServerDeleteFiles(c *gin.Context) {
|
|||||||
func postServerWriteFile(c *gin.Context) {
|
func postServerWriteFile(c *gin.Context) {
|
||||||
s := GetServer(c.Param("server"))
|
s := GetServer(c.Param("server"))
|
||||||
|
|
||||||
f, err := url.QueryUnescape(c.Query("file"))
|
f := c.Query("file")
|
||||||
if err != nil {
|
|
||||||
NewServerError(err, s).Abort(c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
f = "/" + strings.TrimLeft(f, "/")
|
f = "/" + strings.TrimLeft(f, "/")
|
||||||
|
|
||||||
if err := s.Filesystem().Writefile(f, c.Request.Body); err != nil {
|
if err := s.Filesystem().Writefile(f, c.Request.Body); err != nil {
|
||||||
|
|||||||
@@ -129,6 +129,9 @@ func postServerArchive(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark the server as not being transferred so it can actually be used.
|
||||||
|
s.SetTransferring(false)
|
||||||
|
|
||||||
s.Events().Publish(server.TransferStatusEvent, "failure")
|
s.Events().Publish(server.TransferStatusEvent, "failure")
|
||||||
|
|
||||||
sendTransferLog("Attempting to notify panel of archive failure..")
|
sendTransferLog("Attempting to notify panel of archive failure..")
|
||||||
|
|||||||
Reference in New Issue
Block a user