Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
020abec6f2 | ||
|
|
dac9685298 | ||
|
|
519d38f238 | ||
|
|
1d17233d6d | ||
|
|
774c0af0b0 | ||
|
|
71fbd9271e | ||
|
|
2d640209e5 | ||
|
|
304fd91283 | ||
|
|
18de96d7b8 | ||
|
|
a36cab1783 | ||
|
|
6e0c095bb8 | ||
|
|
14eea3b1e4 | ||
|
|
1bc77dc969 | ||
|
|
b8715d1d4f | ||
|
|
13d3490bcf | ||
|
|
e9b8b11fec | ||
|
|
43b7aa2536 | ||
|
|
9b8b3c90fb | ||
|
|
e74d8e3501 | ||
|
|
4b3bd2ff47 | ||
|
|
e652d2df84 |
6
.github/workflows/docker.yaml
vendored
6
.github/workflows/docker.yaml
vendored
@@ -18,11 +18,13 @@ jobs:
|
|||||||
- name: Code checkout
|
- name: Code checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Docker Meta
|
- name: Docker metadata
|
||||||
id: docker_meta
|
id: docker_meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/pterodactyl/wings
|
images: ghcr.io/pterodactyl/wings
|
||||||
|
flavor: |
|
||||||
|
latest=false
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.action == 'published' && github.event.release.prerelease == false }}
|
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.action == 'published' && github.event.release.prerelease == false }}
|
||||||
type=ref,event=tag
|
type=ref,event=tag
|
||||||
@@ -31,7 +33,7 @@ jobs:
|
|||||||
- name: Setup QEMU
|
- name: Setup QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
- name: Install buildx
|
- name: Setup Docker buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
|
|||||||
6
.github/workflows/push.yaml
vendored
6
.github/workflows/push.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-20.04]
|
os: [ubuntu-20.04]
|
||||||
go: ["1.18.8", "1.19.3"]
|
go: ["1.18.10", "1.19.5"]
|
||||||
goos: [linux]
|
goos: [linux]
|
||||||
goarch: [amd64, arm64]
|
goarch: [amd64, arm64]
|
||||||
|
|
||||||
@@ -86,14 +86,14 @@ jobs:
|
|||||||
go test -race $(go list ./...)
|
go test -race $(go list ./...)
|
||||||
|
|
||||||
- name: Upload Release Artifact
|
- name: Upload Release Artifact
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
if: ${{ 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_linux_${{ matrix.goarch }}
|
name: wings_linux_${{ matrix.goarch }}
|
||||||
path: dist/wings
|
path: dist/wings
|
||||||
|
|
||||||
- name: Upload Debug Artifact
|
- name: Upload Debug Artifact
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
if: ${{ 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_linux_${{ matrix.goarch }}_debug
|
name: wings_linux_${{ matrix.goarch }}_debug
|
||||||
|
|||||||
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: "1.18.8"
|
go-version: "1.18.10"
|
||||||
|
|
||||||
- name: Build release binaries
|
- name: Build release binaries
|
||||||
env:
|
env:
|
||||||
@@ -62,8 +62,6 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
name: ${{ github.ref }}
|
|
||||||
tag_name: ${{ github.ref }}
|
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: ${{ contains(github.ref, 'rc') || contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
|
prerelease: ${{ contains(github.ref, 'rc') || contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
|
||||||
body_path: ./RELEASE_CHANGELOG
|
body_path: ./RELEASE_CHANGELOG
|
||||||
|
|||||||
25
CHANGELOG.md
25
CHANGELOG.md
@@ -1,5 +1,26 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.11.3
|
||||||
|
### Fixed
|
||||||
|
* CVE-2023-25152
|
||||||
|
|
||||||
|
## v1.11.2
|
||||||
|
### Fixed
|
||||||
|
* Backups being restored from remote storage (s3) erroring out due to a closed stream.
|
||||||
|
* Fix IP validation logic for activity logs filtering out valid IPs instead of invalid IPs
|
||||||
|
|
||||||
|
## v1.11.1
|
||||||
|
### Changed
|
||||||
|
* Release binaries are now built with Go 1.18.10
|
||||||
|
* Timeout when stopping a server before a transfer begins has been reduced to 15 seconds from 1 minute
|
||||||
|
* Removed insecure SSH protocols for use with the SFTP server
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* Unnecessary Docker client connections being left open, causing a slow leak of file descriptors
|
||||||
|
* Files being left open in parts of the server's filesystem, causing a leak of file descriptors
|
||||||
|
* IPv6 addresses being corrupted by flawed port stripping logic for activity logs, old entries with malformed IPs will be deleted from the local SQLite database automatically
|
||||||
|
* A server that times out while being stopped at the beginning of a transfer no longer causes the server to become stuck in a transferring state
|
||||||
|
|
||||||
## v1.11.0
|
## v1.11.0
|
||||||
### Added (since 1.7.2)
|
### Added (since 1.7.2)
|
||||||
* More detailed information returned by the `/api/system` endpoint when using the `?v=2` query parameter.
|
* More detailed information returned by the `/api/system` endpoint when using the `?v=2` query parameter.
|
||||||
@@ -43,6 +64,10 @@
|
|||||||
* Archive progress is now reported correctly.
|
* Archive progress is now reported correctly.
|
||||||
* Labels for containers can now be set by the Panel.
|
* Labels for containers can now be set by the Panel.
|
||||||
|
|
||||||
|
## v1.7.3
|
||||||
|
### Fixed
|
||||||
|
* CVE-2023-25152
|
||||||
|
|
||||||
## v1.7.2
|
## v1.7.2
|
||||||
### Fixed
|
### Fixed
|
||||||
* The S3 backup driver now supports Cloudflare R2
|
* The S3 backup driver now supports Cloudflare R2
|
||||||
|
|||||||
@@ -15,14 +15,12 @@ dependencies, and allowing users to authenticate with the same credentials they
|
|||||||
|
|
||||||
## Sponsors
|
## Sponsors
|
||||||
|
|
||||||
I would like to extend my sincere thanks to the following sponsors for helping find Pterodactyl's developement.
|
I would like to extend my sincere thanks to the following sponsors for helping find Pterodactyl's development.
|
||||||
[Interested in becoming a sponsor?](https://github.com/sponsors/matthewpi)
|
[Interested in becoming a sponsor?](https://github.com/sponsors/matthewpi)
|
||||||
|
|
||||||
| Company | About |
|
| Company | About |
|
||||||
|-----------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| [**WISP**](https://wisp.gg) | Extra features. |
|
| [**WISP**](https://wisp.gg) | Extra features. |
|
||||||
| [**Fragnet**](https://fragnet.net) | Providing low latency, high-end game hosting solutions to gamers, game studios and eSports platforms. |
|
|
||||||
| [**RocketNode**](https://rocketnode.com/) | Innovative game server hosting combined with a straightforward control panel, affordable prices, and Rocket-Fast support. |
|
|
||||||
| [**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. |
|
| [**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. |
|
||||||
| [**BisectHosting**](https://www.bisecthosting.com/) | BisectHosting provides Minecraft, Valheim and other server hosting services with the highest reliability and lightning fast support since 2012. |
|
| [**BisectHosting**](https://www.bisecthosting.com/) | BisectHosting provides Minecraft, Valheim and other server hosting services with the highest reliability and lightning fast support since 2012. |
|
||||||
| [**MineStrator**](https://minestrator.com/) | Looking for the most highend French hosting company for your minecraft server? More than 24,000 members on our discord trust us. Give us a try! |
|
| [**MineStrator**](https://minestrator.com/) | Looking for the most highend French hosting company for your minecraft server? More than 24,000 members on our discord trust us. Give us a try! |
|
||||||
@@ -30,6 +28,7 @@ I would like to extend my sincere thanks to the following sponsors for helping f
|
|||||||
| [**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. |
|
| [**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. |
|
||||||
| [**Pterodactyl Market**](https://pterodactylmarket.com/) | Pterodactyl Market is a one-and-stop shop for Pterodactyl. In our market, you can find Add-ons, Themes, Eggs, and more for Pterodactyl. |
|
| [**Pterodactyl Market**](https://pterodactylmarket.com/) | Pterodactyl Market is a one-and-stop shop for Pterodactyl. In our market, you can find Add-ons, Themes, Eggs, and more for Pterodactyl. |
|
||||||
| [**UltraServers**](https://ultraservers.com/) | Deploy premium games hosting with the click of a button. Manage and swap games with ease and let us take care of the rest. We currently support Minecraft, Rust, ARK, 7 Days to Die, Garys MOD, CS:GO, Satisfactory and others. |
|
| [**UltraServers**](https://ultraservers.com/) | Deploy premium games hosting with the click of a button. Manage and swap games with ease and let us take care of the rest. We currently support Minecraft, Rust, ARK, 7 Days to Die, Garys MOD, CS:GO, Satisfactory and others. |
|
||||||
|
| [**Realms Hosting**](https://realmshosting.com/) | Want to build your Gaming Empire? Use Realms Hosting today to kick start your game server hosting with outstanding DDOS Protection, 24/7 Support, Cheap Prices and a Custom Control Panel. | |
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ func (e *Environment) Attach(ctx context.Context) error {
|
|||||||
|
|
||||||
// Set the stream again with the container.
|
// Set the stream again with the container.
|
||||||
if st, err := e.client.ContainerAttach(ctx, e.Id, opts); err != nil {
|
if st, err := e.client.ContainerAttach(ctx, e.Id, opts); err != nil {
|
||||||
return err
|
return errors.WrapIf(err, "environment/docker: error while attaching to container")
|
||||||
} else {
|
} else {
|
||||||
e.SetStream(&st)
|
e.SetStream(&st)
|
||||||
}
|
}
|
||||||
@@ -143,7 +143,7 @@ func (e *Environment) Create() error {
|
|||||||
if _, err := e.ContainerInspect(ctx); err == nil {
|
if _, err := e.ContainerInspect(ctx); err == nil {
|
||||||
return nil
|
return nil
|
||||||
} else if !client.IsErrNotFound(err) {
|
} else if !client.IsErrNotFound(err) {
|
||||||
return errors.Wrap(err, "environment/docker: failed to inspect container")
|
return errors.WrapIf(err, "environment/docker: failed to inspect container")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to pull the requested image before creating the container.
|
// Try to pull the requested image before creating the container.
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ func (e *Environment) ExitState() (uint32, bool, error) {
|
|||||||
if client.IsErrNotFound(err) {
|
if client.IsErrNotFound(err) {
|
||||||
return 1, false, nil
|
return 1, false, nil
|
||||||
}
|
}
|
||||||
return 0, false, err
|
return 0, false, errors.WrapIf(err, "environment/docker: failed to inspect container")
|
||||||
}
|
}
|
||||||
return uint32(c.State.ExitCode), c.State.OOMKilled, nil
|
return uint32(c.State.ExitCode), c.State.OOMKilled, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ func (e *Environment) Start(ctx context.Context) error {
|
|||||||
// 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(ctx); err != nil {
|
if err := e.OnBeforeStart(ctx); err != nil {
|
||||||
return errors.WithStackIf(err)
|
return errors.WrapIf(err, "environment/docker: failed to run pre-boot process")
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we cannot start & attach to the container in 30 seconds something has gone
|
// If we cannot start & attach to the container in 30 seconds something has gone
|
||||||
@@ -119,7 +119,7 @@ func (e *Environment) Start(ctx context.Context) error {
|
|||||||
// By explicitly attaching to the instance before we start it, we can immediately
|
// By explicitly attaching to the instance before we start it, we can immediately
|
||||||
// react to errors/output stopping/etc. when starting.
|
// react to errors/output stopping/etc. when starting.
|
||||||
if err := e.Attach(actx); err != nil {
|
if err := e.Attach(actx); err != nil {
|
||||||
return err
|
return errors.WrapIf(err, "environment/docker: failed to attach to container")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := e.client.ContainerStart(actx, e.Id, types.ContainerStartOptions{}); err != nil {
|
if err := e.client.ContainerStart(actx, e.Id, types.ContainerStartOptions{}); err != nil {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package cron
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net"
|
||||||
|
|
||||||
"emperror.dev/errors"
|
"emperror.dev/errors"
|
||||||
|
|
||||||
@@ -17,9 +18,9 @@ type activityCron struct {
|
|||||||
max int
|
max int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run executes the cronjob and ensures we fetch and send all of the stored activity to the
|
// Run executes the cronjob and ensures we fetch and send all the stored activity to the
|
||||||
// Panel instance. Once activity is sent it is deleted from the local database instance. Any
|
// Panel instance. Once activity is sent it is deleted from the local database instance. Any
|
||||||
// SFTP specific events are not handled in this cron, they're handled seperately to account
|
// SFTP specific events are not handled in this cron, they're handled separately to account
|
||||||
// for de-duplication and event merging.
|
// for de-duplication and event merging.
|
||||||
func (ac *activityCron) Run(ctx context.Context) error {
|
func (ac *activityCron) Run(ctx context.Context) error {
|
||||||
// Don't execute this cron if there is currently one running. Once this task is completed
|
// Don't execute this cron if there is currently one running. Once this task is completed
|
||||||
@@ -34,7 +35,6 @@ func (ac *activityCron) Run(ctx context.Context) error {
|
|||||||
Where("event NOT LIKE ?", "server:sftp.%").
|
Where("event NOT LIKE ?", "server:sftp.%").
|
||||||
Limit(ac.max).
|
Limit(ac.max).
|
||||||
Find(&activity)
|
Find(&activity)
|
||||||
|
|
||||||
if tx.Error != nil {
|
if tx.Error != nil {
|
||||||
return errors.WithStack(tx.Error)
|
return errors.WithStack(tx.Error)
|
||||||
}
|
}
|
||||||
@@ -42,15 +42,42 @@ func (ac *activityCron) Run(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ac.manager.Client().SendActivityLogs(ctx, activity); err != nil {
|
// ids to delete from the database.
|
||||||
|
ids := make([]int, 0, len(activity))
|
||||||
|
// activities to send to the panel.
|
||||||
|
activities := make([]models.Activity, 0, len(activity))
|
||||||
|
for _, v := range activity {
|
||||||
|
// Delete any activity that has an invalid IP address. This is a fix for
|
||||||
|
// a bug that truncated the last octet of an IPv6 address in the database.
|
||||||
|
if ip := net.ParseIP(v.IP); ip == nil {
|
||||||
|
ids = append(ids, v.ID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
activities = append(activities, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ids) > 0 {
|
||||||
|
tx = database.Instance().WithContext(ctx).Where("id IN ?", ids).Delete(&models.Activity{})
|
||||||
|
if tx.Error != nil {
|
||||||
|
return errors.WithStack(tx.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(activities) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ac.manager.Client().SendActivityLogs(ctx, activities); err != nil {
|
||||||
return errors.WrapIf(err, "cron: failed to send activity events to Panel")
|
return errors.WrapIf(err, "cron: failed to send activity events to Panel")
|
||||||
}
|
}
|
||||||
|
|
||||||
var ids []int
|
// Add all the successful activities to the list of IDs to delete.
|
||||||
for _, v := range activity {
|
ids = make([]int, len(activities))
|
||||||
ids = append(ids, v.ID)
|
for i, v := range activities {
|
||||||
|
ids[i] = v.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete all the activities that were sent to the Panel (or that were invalid).
|
||||||
tx = database.Instance().WithContext(ctx).Where("id IN ?", ids).Delete(&models.Activity{})
|
tx = database.Instance().WithContext(ctx).Where("id IN ?", ids).Delete(&models.Activity{})
|
||||||
if tx.Error != nil {
|
if tx.Error != nil {
|
||||||
return errors.WithStack(tx.Error)
|
return errors.WithStack(tx.Error)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/pterodactyl/wings/system"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Event string
|
type Event string
|
||||||
@@ -57,7 +57,9 @@ func (a Activity) SetUser(u string) *Activity {
|
|||||||
// is trimmed down to remove any extraneous data, and the timestamp is set to the current
|
// is trimmed down to remove any extraneous data, and the timestamp is set to the current
|
||||||
// system time and then stored as UTC.
|
// system time and then stored as UTC.
|
||||||
func (a *Activity) BeforeCreate(_ *gorm.DB) error {
|
func (a *Activity) BeforeCreate(_ *gorm.DB) error {
|
||||||
a.IP = system.TrimIPSuffix(a.IP)
|
if ip, _, err := net.SplitHostPort(strings.TrimSpace(a.IP)); err == nil {
|
||||||
|
a.IP = ip
|
||||||
|
}
|
||||||
if a.Timestamp.IsZero() {
|
if a.Timestamp.IsZero() {
|
||||||
a.Timestamp = time.Now()
|
a.Timestamp = time.Now()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ func getDownloadFile(c *gin.Context) {
|
|||||||
middleware.CaptureAndAbort(c, err)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
c.Header("Content-Length", strconv.Itoa(int(st.Size())))
|
c.Header("Content-Length", strconv.Itoa(int(st.Size())))
|
||||||
c.Header("Content-Disposition", "attachment; filename="+strconv.Quote(st.Name()))
|
c.Header("Content-Disposition", "attachment; filename="+strconv.Quote(st.Name()))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"emperror.dev/errors"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/pterodactyl/wings/environment"
|
"github.com/pterodactyl/wings/environment"
|
||||||
@@ -63,11 +64,11 @@ func postServerTransfer(c *gin.Context) {
|
|||||||
if s.Environment.State() != environment.ProcessOfflineState {
|
if s.Environment.State() != environment.ProcessOfflineState {
|
||||||
if err := s.Environment.WaitForStop(
|
if err := s.Environment.WaitForStop(
|
||||||
s.Context(),
|
s.Context(),
|
||||||
time.Minute,
|
time.Second*15,
|
||||||
false,
|
false,
|
||||||
); err != nil && !strings.Contains(strings.ToLower(err.Error()), "no such container") {
|
); err != nil && !strings.Contains(strings.ToLower(err.Error()), "no such container") {
|
||||||
notifyPanelOfFailure()
|
s.SetTransferring(false)
|
||||||
s.Log().WithError(err).Error("failed to stop server for transfer")
|
middleware.CaptureAndAbort(c, errors.Wrap(err, "failed to stop server for transfer"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ func getServerWebsocket(c *gin.Context) {
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
break
|
break
|
||||||
case <-s.Context().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))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -83,7 +83,7 @@ func getServerWebsocket(c *gin.Context) {
|
|||||||
|
|
||||||
go func(msg websocket.Message) {
|
go func(msg websocket.Message) {
|
||||||
if err := handler.HandleInbound(ctx, msg); err != nil {
|
if err := handler.HandleInbound(ctx, msg); err != nil {
|
||||||
handler.SendErrorJson(msg, err)
|
_ = handler.SendErrorJson(msg, err)
|
||||||
}
|
}
|
||||||
}(j)
|
}(j)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ func (b *LocalBackup) Restore(ctx context.Context, _ io.Reader, callback Restore
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
var reader io.Reader = f
|
var reader io.Reader = f
|
||||||
// Steal the logic we use for making backups which will be applied when restoring
|
// Steal the logic we use for making backups which will be applied when restoring
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"emperror.dev/errors"
|
||||||
|
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"github.com/pterodactyl/wings/environment"
|
"github.com/pterodactyl/wings/environment"
|
||||||
)
|
)
|
||||||
@@ -57,7 +59,7 @@ func (s *Server) handleServerCrash() error {
|
|||||||
|
|
||||||
exitCode, oomKilled, err := s.Environment.ExitState()
|
exitCode, oomKilled, err := s.Environment.ExitState()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.Wrap(err, "failed to get exit state for server process")
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the system is not configured to detect a clean exit code as a crash, and the
|
// If the system is not configured to detect a clean exit code as a crash, and the
|
||||||
@@ -85,5 +87,5 @@ func (s *Server) handleServerCrash() error {
|
|||||||
|
|
||||||
s.crasher.SetLastCrash(time.Now())
|
s.crasher.SetLastCrash(time.Now())
|
||||||
|
|
||||||
return s.HandlePowerAction(PowerActionStart)
|
return errors.Wrap(s.HandlePowerAction(PowerActionStart), "failed to start server after crash detection")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ func (fs *Filesystem) DecompressFileUnsafe(ctx context.Context, dir string, file
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// TODO: defer file close?
|
defer f.Close()
|
||||||
|
|
||||||
// Identify the type of archive we are dealing with.
|
// Identify the type of archive we are dealing with.
|
||||||
format, input, err := archiver.Identify(filepath.Base(file), f)
|
format, input, err := archiver.Identify(filepath.Base(file), f)
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ func (fs *Filesystem) DirectorySize(dir string) (int64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !e.IsDir() {
|
if !e.IsDir() {
|
||||||
syscall.Lstat(p, &st)
|
_ = syscall.Lstat(p, &st)
|
||||||
atomic.AddInt64(&size, st.Size)
|
atomic.AddInt64(&size, st.Size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,9 @@ func (fs *Filesystem) Touch(p string, flag int) (*os.File, error) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
if f != nil {
|
||||||
|
_ = f.Close()
|
||||||
|
}
|
||||||
// If the error is not because it doesn't exist then we just need to bail at this point.
|
// If the error is not because it doesn't exist then we just need to bail at this point.
|
||||||
if !errors.Is(err, os.ErrNotExist) {
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
return nil, errors.Wrap(err, "server/filesystem: touch: failed to open file handle")
|
return nil, errors.Wrap(err, "server/filesystem: touch: failed to open file handle")
|
||||||
@@ -162,7 +165,7 @@ func (fs *Filesystem) Writefile(p string, r io.Reader) error {
|
|||||||
// Adjust the disk usage to account for the old size and the new size of the file.
|
// Adjust the disk usage to account for the old size and the new size of the file.
|
||||||
fs.addDisk(sz - currentSize)
|
fs.addDisk(sz - currentSize)
|
||||||
|
|
||||||
return fs.Chown(cleaned)
|
return fs.unsafeChown(cleaned)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new directory (name) at a specified path (p) for the server.
|
// Creates a new directory (name) at a specified path (p) for the server.
|
||||||
@@ -220,7 +223,12 @@ func (fs *Filesystem) Chown(path string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
return fs.unsafeChown(cleaned)
|
||||||
|
}
|
||||||
|
|
||||||
|
// unsafeChown chowns the given path, without checking if the path is safe. This should only be used
|
||||||
|
// when the path has already been checked.
|
||||||
|
func (fs *Filesystem) unsafeChown(path string) error {
|
||||||
if fs.isTest {
|
if fs.isTest {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -229,19 +237,19 @@ func (fs *Filesystem) Chown(path string) error {
|
|||||||
gid := config.Get().System.User.Gid
|
gid := config.Get().System.User.Gid
|
||||||
|
|
||||||
// Start by just chowning the initial path that we received.
|
// Start by just chowning the initial path that we received.
|
||||||
if err := os.Chown(cleaned, uid, gid); err != nil {
|
if err := os.Chown(path, uid, gid); err != nil {
|
||||||
return errors.Wrap(err, "server/filesystem: chown: failed to chown path")
|
return errors.Wrap(err, "server/filesystem: chown: failed to chown path")
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this is not a directory we can now return from the function, there is nothing
|
// If this is not a directory we can now return from the function, there is nothing
|
||||||
// left that we need to do.
|
// left that we need to do.
|
||||||
if st, err := os.Stat(cleaned); err != nil || !st.IsDir() {
|
if st, err := os.Stat(path); err != nil || !st.IsDir() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this was a directory, begin walking over its contents recursively and ensure that all
|
// If this was a directory, begin walking over its contents recursively and ensure that all
|
||||||
// of the subfiles and directories get their permissions updated as well.
|
// of the subfiles and directories get their permissions updated as well.
|
||||||
err = godirwalk.Walk(cleaned, &godirwalk.Options{
|
err := godirwalk.Walk(path, &godirwalk.Options{
|
||||||
Unsorted: true,
|
Unsorted: true,
|
||||||
Callback: func(p string, e *godirwalk.Dirent) error {
|
Callback: func(p string, e *godirwalk.Dirent) error {
|
||||||
// Do not attempt to chown a symlink. Go's os.Chown function will affect the symlink
|
// Do not attempt to chown a symlink. Go's os.Chown function will affect the symlink
|
||||||
@@ -258,7 +266,6 @@ func (fs *Filesystem) Chown(path string) error {
|
|||||||
return os.Chown(p, uid, gid)
|
return os.Chown(p, uid, gid)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return errors.Wrap(err, "server/filesystem: chown: failed to chown during walk function")
|
return errors.Wrap(err, "server/filesystem: chown: failed to chown during walk function")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package filesystem
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
iofs "io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -33,8 +34,6 @@ func (fs *Filesystem) IsIgnored(paths ...string) error {
|
|||||||
// This logic is actually copied over from the SFTP server code. Ideally that eventually
|
// This logic is actually copied over from the SFTP server code. Ideally that eventually
|
||||||
// either gets ported into this application, or is able to make use of this package.
|
// either gets ported into this application, or is able to make use of this package.
|
||||||
func (fs *Filesystem) SafePath(p string) (string, error) {
|
func (fs *Filesystem) SafePath(p string) (string, error) {
|
||||||
var nonExistentPathResolution string
|
|
||||||
|
|
||||||
// Start with a cleaned up path before checking the more complex bits.
|
// Start with a cleaned up path before checking the more complex bits.
|
||||||
r := fs.unsafeFilePath(p)
|
r := fs.unsafeFilePath(p)
|
||||||
|
|
||||||
@@ -44,47 +43,24 @@ func (fs *Filesystem) SafePath(p string) (string, error) {
|
|||||||
if err != nil && !os.IsNotExist(err) {
|
if err != nil && !os.IsNotExist(err) {
|
||||||
return "", errors.Wrap(err, "server/filesystem: failed to evaluate symlink")
|
return "", errors.Wrap(err, "server/filesystem: failed to evaluate symlink")
|
||||||
} else if os.IsNotExist(err) {
|
} else if os.IsNotExist(err) {
|
||||||
// The requested directory doesn't exist, so at this point we need to iterate up the
|
// The target of one of the symlinks (EvalSymlinks is recursive) does not exist.
|
||||||
// path chain until we hit a directory that _does_ exist and can be validated.
|
// So we get what target path does not exist and check if it's within the data
|
||||||
parts := strings.Split(filepath.Dir(r), "/")
|
// directory. If it is, we return the original path, otherwise we return an error.
|
||||||
|
pErr, ok := err.(*iofs.PathError)
|
||||||
var try string
|
if !ok {
|
||||||
// Range over all of the path parts and form directory pathings from the end
|
return "", errors.Wrap(err, "server/filesystem: failed to evaluate symlink")
|
||||||
// moving up until we have a valid resolution or we run out of paths to try.
|
|
||||||
for k := range parts {
|
|
||||||
try = strings.Join(parts[:(len(parts)-k)], "/")
|
|
||||||
|
|
||||||
if !fs.unsafeIsInDataDirectory(try) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
t, err := filepath.EvalSymlinks(try)
|
|
||||||
if err == nil {
|
|
||||||
nonExistentPathResolution = t
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
ep = pErr.Path
|
||||||
|
|
||||||
// If the new path doesn't start with their root directory there is clearly an escape
|
|
||||||
// attempt going on, and we should NOT resolve this path for them.
|
|
||||||
if nonExistentPathResolution != "" {
|
|
||||||
if !fs.unsafeIsInDataDirectory(nonExistentPathResolution) {
|
|
||||||
return "", NewBadPathResolution(p, nonExistentPathResolution)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the nonExistentPathResolution variable is not empty then the initial path requested
|
|
||||||
// did not exist and we looped through the pathway until we found a match. At this point
|
|
||||||
// we've confirmed the first matched pathway exists in the root server directory, so we
|
|
||||||
// can go ahead and just return the path that was requested initially.
|
|
||||||
return r, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the requested directory from EvalSymlinks begins with the server root directory go
|
// If the requested directory from EvalSymlinks begins with the server root directory go
|
||||||
// ahead and return it. If not we'll return an error which will block any further action
|
// ahead and return it. If not we'll return an error which will block any further action
|
||||||
// on the file.
|
// on the file.
|
||||||
if fs.unsafeIsInDataDirectory(ep) {
|
if fs.unsafeIsInDataDirectory(ep) {
|
||||||
return ep, nil
|
// Returning the original path here instead of the resolved path ensures that
|
||||||
|
// whatever the user is trying to do will work as expected. If we returned the
|
||||||
|
// resolved path, the user would be unable to know that it is in fact a symlink.
|
||||||
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", NewBadPathResolution(p, r)
|
return "", NewBadPathResolution(p, r)
|
||||||
|
|||||||
@@ -115,6 +115,14 @@ func TestFilesystem_Blocks_Symlinks(t *testing.T) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := os.Symlink(filepath.Join(rfs.root, "malicious_does_not_exist.txt"), filepath.Join(rfs.root, "/server/symlinked_does_not_exist.txt")); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Symlink(filepath.Join(rfs.root, "/server/symlinked_does_not_exist.txt"), filepath.Join(rfs.root, "/server/symlinked_does_not_exist2.txt")); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := os.Symlink(filepath.Join(rfs.root, "/malicious_dir"), filepath.Join(rfs.root, "/server/external_dir")); err != nil {
|
if err := os.Symlink(filepath.Join(rfs.root, "/malicious_dir"), filepath.Join(rfs.root, "/server/external_dir")); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -128,6 +136,22 @@ func TestFilesystem_Blocks_Symlinks(t *testing.T) {
|
|||||||
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
|
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
g.It("cannot write to a non-existent file symlinked outside the root", func() {
|
||||||
|
r := bytes.NewReader([]byte("testing what the fuck"))
|
||||||
|
|
||||||
|
err := fs.Writefile("symlinked_does_not_exist.txt", r)
|
||||||
|
g.Assert(err).IsNotNil()
|
||||||
|
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
|
||||||
|
})
|
||||||
|
|
||||||
|
g.It("cannot write to chained symlinks with target that does not exist outside the root", func() {
|
||||||
|
r := bytes.NewReader([]byte("testing what the fuck"))
|
||||||
|
|
||||||
|
err := fs.Writefile("symlinked_does_not_exist2.txt", r)
|
||||||
|
g.Assert(err).IsNotNil()
|
||||||
|
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
|
||||||
|
})
|
||||||
|
|
||||||
g.It("cannot write a file to a directory symlinked outside the root", func() {
|
g.It("cannot write a file to a directory symlinked outside the root", func() {
|
||||||
r := bytes.NewReader([]byte("testing"))
|
r := bytes.NewReader([]byte("testing"))
|
||||||
|
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ func (s *Server) Context() context.Context {
|
|||||||
// server instance.
|
// server instance.
|
||||||
func (s *Server) GetEnvironmentVariables() []string {
|
func (s *Server) GetEnvironmentVariables() []string {
|
||||||
out := []string{
|
out := []string{
|
||||||
|
// TODO: allow this to be overridden by the user.
|
||||||
fmt.Sprintf("TZ=%s", config.Get().System.Timezone),
|
fmt.Sprintf("TZ=%s", config.Get().System.Timezone),
|
||||||
fmt.Sprintf("STARTUP=%s", s.Config().Invocation),
|
fmt.Sprintf("STARTUP=%s", s.Config().Invocation),
|
||||||
fmt.Sprintf("SERVER_MEMORY=%d", s.MemoryLimit()),
|
fmt.Sprintf("SERVER_MEMORY=%d", s.MemoryLimit()),
|
||||||
|
|||||||
@@ -68,6 +68,21 @@ func (c *SFTPServer) Run() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
conf := &ssh.ServerConfig{
|
conf := &ssh.ServerConfig{
|
||||||
|
Config: ssh.Config{
|
||||||
|
KeyExchanges: []string{
|
||||||
|
"curve25519-sha256", "curve25519-sha256@libssh.org",
|
||||||
|
"ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521",
|
||||||
|
"diffie-hellman-group14-sha256",
|
||||||
|
},
|
||||||
|
Ciphers: []string{
|
||||||
|
"aes128-gcm@openssh.com",
|
||||||
|
"chacha20-poly1305@openssh.com",
|
||||||
|
"aes128-ctr", "aes192-ctr", "aes256-ctr",
|
||||||
|
},
|
||||||
|
MACs: []string{
|
||||||
|
"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256",
|
||||||
|
},
|
||||||
|
},
|
||||||
NoClientAuth: false,
|
NoClientAuth: false,
|
||||||
MaxAuthTries: 6,
|
MaxAuthTries: 6,
|
||||||
PasswordCallback: func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
|
PasswordCallback: func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
package system
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/rand"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ipTrimRegex = regexp.MustCompile(`(:\d*)?$`)
|
|
||||||
|
|
||||||
const characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
|
|
||||||
|
|
||||||
// RandomString generates a random string of alpha-numeric characters using a
|
|
||||||
// pseudo-random number generator. The output of this function IS NOT cryptographically
|
|
||||||
// secure, it is used solely for generating random strings outside a security context.
|
|
||||||
func RandomString(n int) string {
|
|
||||||
var b strings.Builder
|
|
||||||
b.Grow(n)
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
b.WriteByte(characters[rand.Intn(len(characters))])
|
|
||||||
}
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TrimIPSuffix removes the internal port value from an IP address to ensure we're only
|
|
||||||
// ever working directly with the IP address.
|
|
||||||
func TrimIPSuffix(s string) string {
|
|
||||||
return ipTrimRegex.ReplaceAllString(s, "")
|
|
||||||
}
|
|
||||||
@@ -127,6 +127,7 @@ func GetDockerInfo(ctx context.Context) (types.Version, types.Info, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return types.Version{}, types.Info{}, err
|
return types.Version{}, types.Info{}, err
|
||||||
}
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
dockerVersion, err := c.ServerVersion(ctx)
|
dockerVersion, err := c.ServerVersion(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user