Compare commits

..

61 Commits

Author SHA1 Message Date
Dane Everitt
de0c67d066 Re-enable disabled tests 2020-11-08 15:17:47 -08:00
Dane Everitt
68bdcb3cbc Fix error handling and get tests back to working 2020-11-08 15:15:39 -08:00
Dane Everitt
205c4d541e Update changelog 2020-11-08 14:40:07 -08:00
Dane Everitt
ef999a039c Perhaps better error stacks for backups/archives; ref #2418 2020-11-08 14:07:26 -08:00
Dane Everitt
be9d1a3986 Replace error handling package with emperror; add better reporting for errors escaping server root 2020-11-08 13:52:20 -08:00
Dane Everitt
0989c78d4b Fix the profiling... 2020-11-08 12:33:24 -08:00
Dane Everitt
1683675807 Add more advanced profiling support 2020-11-08 12:12:21 -08:00
Dane Everitt
536f00a5e5 Add support for both memory and cpu profiling 2020-11-08 11:59:04 -08:00
Dane Everitt
4b17ac4f1c Fix atomic string handling for JSON output 2020-11-06 22:22:33 -08:00
Dane Everitt
944d381778 Improve server state logical handling; allow setting state directly on the environment 2020-11-06 21:53:00 -08:00
Dane Everitt
3fce1b98d5 Create atomic string to allow for simpler logic 2020-11-06 21:14:29 -08:00
Dane Everitt
a74be8f4eb configurable socket log count; closes pterodactyl/panel#2659 2020-11-06 20:56:21 -08:00
Dane Everitt
af9ed4bff1 less weird log message 2020-11-03 21:02:10 -08:00
Dane Everitt
08d1efb475 Support denying a JWT JTI via the API 2020-11-03 21:01:50 -08:00
Dane Everitt
65664b63e7 Add support for denying JWT JTI's that are generated before a specific time 2020-11-03 20:33:33 -08:00
Dane Everitt
912d95de24 Ensure environment starts in default offline state, rather than an empty string; closes pterodactyl/panel#2519 2020-11-02 19:43:49 -08:00
Dane Everitt
13c253780a Use 60 seconds, not 15 2020-11-01 17:26:45 -08:00
Dane Everitt
fe572beada Stop servers when exceeding their disk limits; closes pterodactyl/panel#2638 2020-11-01 17:20:42 -08:00
Dane Everitt
384b9a3c28 Correctly report errors when uploading files 2020-11-01 16:33:02 -08:00
Matthew Penner
05cfb59e18 Add better logging for s3 backup uploads 2020-11-01 15:36:16 -07:00
Dane Everitt
317e54acc5 Merge pull request #67 from pterodactyl/issue/2599
Switch to s3 multipart uploads for backups
2020-11-01 14:34:27 -08:00
Matthew Penner
5475cb02c1 Cleanup s3 backup uploads, add request timeouts 2020-11-01 15:23:46 -07:00
Matthew Penner
1239b1c0ca Switch to s3 multipart uploads for backups 2020-11-01 15:23:46 -07:00
Dane Everitt
b8598e90d4 Fix query parameter association on GET requests 2020-11-01 14:23:03 -08:00
Dane Everitt
fcccda2761 Endpoint fixes for GET requests and error handling 2020-11-01 14:04:57 -08:00
Dane Everitt
f67889c2ca Remove unused code 2020-11-01 13:17:17 -08:00
Dane Everitt
b8766d3c82 Start at next page, not current page 2020-10-31 13:38:19 -07:00
Dane Everitt
ca3becfb55 Correctly handle server responses and instantiation 2020-10-31 11:13:40 -07:00
Dane Everitt
41a67933eb Use a paginated loop to fetch servers from the Panel rather than a single massive request 2020-10-31 11:05:53 -07:00
Dane Everitt
334b3e8d10 Refactor HTTP endpoints to be less complicated and follow better standards 2020-10-31 10:04:20 -07:00
Dane Everitt
c4703f5541 Update CHANGELOG.md 2020-10-22 22:02:01 -07:00
Dane Everitt
1f3394b82d Update release.yml 2020-10-22 21:42:34 -07:00
Dane Everitt
bae63c4321 Fix bad yaml 2020-10-22 21:41:00 -07:00
Dane Everitt
f99640a42d Update CHANGELOG.md 2020-10-22 21:36:24 -07:00
Dane Everitt
c73d632e0d Use correct case-insensitive regex; closes pterodactyl/panel#2546 2020-10-22 20:22:27 -07:00
Dane Everitt
903902e123 Match multiple times 2020-10-22 20:17:58 -07:00
Dane Everitt
1c787b5f26 Fix handling of super long lines to not hit a scanner error; closes pterodactyl/panel#2549
Also fixes return handling in games like Minecraft to properly display the full line output.
2020-10-19 18:13:52 -07:00
Dane Everitt
3f9ac8b89a Fix local images not being pulled correctly; closes #2559 2020-10-19 16:18:33 -07:00
Dane Everitt
560c832cc6 Apply timezone cleaning to final result, closes #2546 2020-10-19 16:13:59 -07:00
Dane Everitt
13058ad64b Merge branch 'develop' of https://github.com/pterodactyl/wings into develop 2020-10-19 15:27:11 -07:00
Dane Everitt
305cd512a7 Update README.md 2020-10-19 15:27:10 -07:00
Matthew Penner
3cd17a2856 Add arm to release workflow 2020-10-17 15:55:05 -06:00
Matthew Penner
56789196d4 Disable flaky test so I can test cross-platform building 2020-10-17 15:47:30 -06:00
Matthew Penner
70804dd20f Remoe stat_arm.go, make stat_linux.go with with both 32 and 64 bit systems 2020-10-17 15:45:14 -06:00
Matthew Penner
19d821aab5 Fix arm build 2020-10-17 15:41:13 -06:00
Matthew Penner
4ce35d3d17 Fix race in filesystem_test.go 2020-10-17 15:31:40 -06:00
Matthew Penner
a62b588ace Add matrix to build-test.yml 2020-10-17 15:13:06 -06:00
Dane Everitt
9b54be06bb Remove unused config values 2020-10-17 14:09:02 -07:00
Dane Everitt
c031d37b91 Use single call to create/return the docker client 2020-10-17 14:04:56 -07:00
Dane Everitt
19051c99ef Support arm builds 2020-10-17 13:52:38 -07:00
Jakob
99fd416cee add version negotiation fix to changelog 2020-10-17 21:49:43 +02:00
Jakob
acf09180f0 Merge pull request #66 from pterodactyl/fix/version-negotiation
Fix version negotiation for installs and diagnostics
2020-10-17 21:43:00 +02:00
Jakob
b19fc88a95 add version negotiation to diagnostics docker client 2020-10-17 21:42:23 +02:00
Jakob
e185f597ba add version negotiation to server install docker client 2020-10-17 21:41:01 +02:00
Dane Everitt
3973747c9c Update README.md 2020-10-17 12:11:06 -07:00
Dane Everitt
947279a07c Don't abort entire boot process due to one bad server egg; closes pterodactyl/panel#2448 2020-10-17 12:06:47 -07:00
Dane Everitt
ad1ed0f24a Merge branch 'develop' of https://github.com/pterodactyl/wings into develop 2020-10-17 11:35:22 -07:00
Dane Everitt
80387bc294 Use more easily configurable timezone, remove /etc/timezone mounts from containers; closes pterodactyl/panel#2513
If this does not completely solve the issue in containers then we need to evaluate the image being used to determine what changes need to happen to the image itself to support the timezone.

ref pterodactyl/panel#2239
ref pterodactyl/panel#2329
ref pterodactyl/panel#2389
2020-10-17 11:35:20 -07:00
Dane Everitt
e8dbba5cc0 Merge pull request #65 from AreYouRlyScared/patch-3
Update diagnostics.go
2020-10-14 08:22:30 -07:00
Charles Morgan
f50f580dcc Update diagnostics.go 2020-10-14 05:06:14 -04:00
Dane Everitt
7e8033d96c Update README.md 2020-10-12 21:12:36 -07:00
74 changed files with 1778 additions and 1190 deletions

View File

@@ -9,25 +9,48 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-20.04 strategy:
# Default is true, cancels jobs for other platforms in the matrix if one fails
fail-fast: false
matrix:
os: [ ubuntu-20.04 ]
go: [ 1.15 ]
goos: [ linux ]
goarch: [ amd64, arm, arm64 ]
runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-go@v2 - uses: actions/setup-go@v2
with: with:
go-version: '1.15.2' go-version: ${{ matrix.go }}
- name: Build - name: Build
run: GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X github.com/pterodactyl/wings/system.Version=dev-${GIT_COMMIT:0:7}" -o build/wings_linux_amd64 -v wings.go env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: |
go build -v -ldflags="-s -w -X github.com/pterodactyl/wings/system.Version=dev-${GIT_COMMIT:0:7}" -o build/wings_${{ matrix.goos }}_${{ matrix.goarch }} wings.go
- name: Test - name: Test
run: go test ./... run: go test ./...
- name: Compress binary and make it executable - name: Compress binary and make it executable
if: ${{ github.ref == 'refs/heads/develop' || github.event_name == 'pull_request' }} if: ${{ github.ref == 'refs/heads/develop' || github.event_name == 'pull_request' }}
run: upx build/wings_linux_amd64 && chmod +x build/wings_linux_amd64 run: |
upx build/wings_${{ matrix.goos }}_${{ matrix.goarch }} && chmod +x build/wings_${{ matrix.goos }}_${{ matrix.goarch }}
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
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_amd64 name: wings_${{ matrix.goos }}_${{ matrix.goarch }}
path: build/wings_linux_amd64 path: build/wings_${{ matrix.goos }}_${{ matrix.goarch }}
- uses: actions/upload-artifact@v2
if: ${{ github.ref == 'refs/heads/develop' || github.event_name == 'pull_request' }}
with:
name: wings_${{ matrix.goos }}_${{ matrix.goarch }}
path: build/wings_${{ matrix.goos }}_${{ matrix.goarch }}

View File

@@ -8,8 +8,10 @@ on:
jobs: jobs:
release: release:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-go@v2 - uses: actions/setup-go@v2
with: with:
go-version: '1.15.2' go-version: '1.15.2'
@@ -17,13 +19,19 @@ jobs:
- name: Build - name: Build
env: env:
REF: ${{ github.ref }} REF: ${{ github.ref }}
run: GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X github.com/pterodactyl/wings/system.Version=${REF:11}" -o build/wings_linux_amd64 -v wings.go run: |
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X github.com/pterodactyl/wings/system.Version=${REF:11}" -o build/wings_linux_amd64 -v wings.go
GOOS=linux GOARCH=arm64 go build -ldflags="-s -w -X github.com/pterodactyl/wings/system.Version=${REF:11}" -o build/wings_linux_arm64 -v wings.go
GOOS=linux GOARCH=arm go build -ldflags="-s -w -X github.com/pterodactyl/wings/system.Version=${REF:11}" -o build/wings_linux_arm -v wings.go
- name: Test - name: Test
run: go test ./... run: go test ./...
- name: Compress binary and make it executable - name: Compress binary and make it executable
run: upx --brute build/wings_linux_amd64 && chmod +x build/wings_linux_amd64 run: |
upx --brute build/wings_linux_amd64 && chmod +x build/wings_linux_amd64
upx build/wings_linux_arm64 && chmod +x build/wings_linux_arm64
upx build/wings_linux_arm && chmod +x build/wings_linux_arm
- name: Extract changelog - name: Extract changelog
env: env:
@@ -35,8 +43,10 @@ jobs:
- name: Create checksum and add to changelog - name: Create checksum and add to changelog
run: | run: |
SUM=`cd build && sha256sum wings_linux_amd64` SUM=`cd build && sha256sum wings_linux_amd64`
echo -e "\n#### SHA256 Checksum\n\n\`\`\`\n$SUM\n\`\`\`\n" >> ./RELEASE_CHANGELOG SUM2=`cd build && sha256sum wings_linux_arm64`
echo $SUM > checksum.txt SUM3=`cd build && sha256sum wings_linux_arm`
echo -e "\n#### SHA256 Checksum\n\`\`\`\n$SUM\n$SUM2\n$SUM3\n\`\`\`\n" >> ./RELEASE_CHANGELOG
echo -e "$SUM\n$SUM2\n$SUM3" > checksums.txt
- name: Create release branch - name: Create release branch
env: env:
@@ -64,8 +74,7 @@ jobs:
draft: true draft: true
prerelease: ${{ contains(github.ref, 'beta') || contains(github.ref, 'alpha') }} prerelease: ${{ contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
- name: Upload binary - name: Upload amd64 Binary
id: upload-release-binary
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -75,13 +84,32 @@ jobs:
asset_name: wings_linux_amd64 asset_name: wings_linux_amd64
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
- name: Upload checksum - name: Upload arm64 Binary
id: upload-release-checksum
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./checksum.txt asset_path: build/wings_linux_arm64
asset_name: checksum.txt asset_name: wings_linux_arm64
asset_content_type: application/octet-stream
- name: Upload arm Binary
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: build/wings_linux_arm
asset_name: wings_linux_arm
asset_content_type: application/octet-stream
- name: Upload checksum
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./checksums.txt
asset_name: checksums.txt
asset_content_type: text/plain asset_content_type: text/plain

2
.gitignore vendored
View File

@@ -47,3 +47,5 @@ test_*/
debug debug
data/.states.json data/.states.json
.DS_Store .DS_Store
*.pprof
*.pdf

View File

@@ -1,5 +1,37 @@
# Changelog # Changelog
## v1.1.0
This release **requires** `Panel@1.1.0` or later to run due to API changes.
### Added
* Adds support for denying client JWT access to specific token keys generated before Wings starts, or before an arbitrary date from an API call.
* Adds support for a configurable number of log messages to be returned when connecting to a server socket and requesting the logs.
* Adds support for both CPU and Memory profiling of Wings via a CLI argument.
### Fixed
* Errors encountered while uploading files to Wings are now properly reported back to the client rather than causing a generic 500 error.
* Servers exceeding their disk limit are now properly stopped when they exceed limits while running.
* Fixes server environment starting as an empty value rather than an "offline" value.
### Changed
* Cleaned up code internals for handling API requests to make it easier on new developers and use a more sane system.
* Server configuration retrieval from the Panel is now done in a paginated loop rather than a single large call to allow systems with thousands of instances to boot properly.
* Switches to multipart S3 uploads to handle backups larger than 5GB in size.
* Switches the error handling package from `pkg/errors` to `emperror` to avoid overwriting existing stack traces associated with an error and provide additional functionality.
## v1.0.1
### Added
* Adds support for ARM to build outputs for wings.
### Fixed
* Fixed a few docker clients not having version negotiation enabled.
* Fixes local images prefixed with `~` getting pulled from remote sources rather than just using the local copy.
* Fixes console output breaking with certain games when excessive line length was output.
* Fixes an error when console lines were too long that would cause the console to stop updating until the server was restarted,
### Changed
* Simplified timezone logic for containers by properly grabbing the system timezone and then passing that through to containers with the `TZ=` environment variable.
## v1.0.0 ## v1.0.0
This is the first official stable release of Wings! Please be aware that while this specific version changelog is very short, This is the first official stable release of Wings! Please be aware that while this specific version changelog is very short,
it technically includes all of the previous beta and alpha releases within it. For the sake of version history and following it technically includes all of the previous beta and alpha releases within it. For the sake of version history and following

View File

@@ -1,5 +1,6 @@
build: build:
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -gcflags "all=-trimpath=$(PWD)" -o build/wings_linux_amd64 -v wings.go GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -gcflags "all=-trimpath=$(pwd)" -o build/wings_linux_amd64 -v wings.go
GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -gcflags "all=-trimpath=$(pwd)" -o build/wings_linux_arm64 -v wings.go
compress: compress:
upx --brute build/wings_* upx --brute build/wings_*

View File

@@ -1,6 +1,7 @@
[![Logo Image](https://cdn.pterodactyl.io/logos/new/pterodactyl_logo.png)](https://pterodactyl.io) [![Logo Image](https://cdn.pterodactyl.io/logos/new/pterodactyl_logo.png)](https://pterodactyl.io)
[![Discord](https://img.shields.io/discord/122900397965705216.svg?style=flat-square&label=Discord)](https://pterodactyl.io/discord) ![Discord](https://img.shields.io/discord/122900397965705216?label=Discord&logo=Discord&logoColor=white)
![GitHub Releases](https://img.shields.io/github/downloads/pterodactyl/wings/latest/total)
[![Go Report Card](https://goreportcard.com/badge/github.com/pterodactyl/wings)](https://goreportcard.com/report/github.com/pterodactyl/wings) [![Go Report Card](https://goreportcard.com/badge/github.com/pterodactyl/wings)](https://goreportcard.com/report/github.com/pterodactyl/wings)
# Pterodactyl Wings # Pterodactyl Wings
@@ -17,13 +18,15 @@ I would like to extend my sincere thanks to the following sponsors for helping f
| Company | About | | Company | About |
| ------- | ----- | | ------- | ----- |
| [**WISP**](https://wisp.gg) | Extra features. |
| [**Bloom.host**](https://bloom.host) | Bloom.host offers dedicated core VPS and Minecraft hosting with Ryzen 9 processors. With owned-hardware, we offer truly unbeatable prices on high-performance hosting. | | [**Bloom.host**](https://bloom.host) | Bloom.host offers dedicated core VPS and Minecraft hosting with Ryzen 9 processors. With owned-hardware, we offer truly unbeatable prices on high-performance hosting. |
| [**VersatileNode**](https://versatilenode.com/) | Looking to host a minecraft server, vps, or a website? VersatileNode is one of the most affordable hosting providers to provide quality yet cheap services with incredible support. |
| [**MineStrator**](https://minestrator.com/) | Looking for a French highend hosting company for you minecraft server? More than 14,000 members on our discord, trust us. | | [**MineStrator**](https://minestrator.com/) | Looking for a French highend hosting company for you minecraft server? More than 14,000 members on our discord, trust us. |
| [**DedicatedMC**](https://dedicatedmc.io/) | DedicatedMC provides Raw Power hosting at affordable pricing, making sure to never compromise on your performance and giving you the best performance money can buy. | | [**DedicatedMC**](https://dedicatedmc.io/) | DedicatedMC provides Raw Power hosting at affordable pricing, making sure to never compromise on your performance and giving you the best performance money can buy. |
| [**Skynode**](https://www.skynode.pro/) | Skynode provides blazing fast game servers along with a top-notch user experience. Whatever our clients are looking for, we're able to provide it! | | [**Skynode**](https://www.skynode.pro/) | Skynode provides blazing fast game servers along with a top-notch user experience. Whatever our clients are looking for, we're able to provide it! |
| [**XCORE-SERVER.de**](https://xcore-server.de/) | XCORE-SERVER.de offers High-End Servers for hosting and gaming since 2012. Fast, excellent and well-known for eSports Gaming. | | [**XCORE-SERVER.de**](https://xcore-server.de/) | XCORE-SERVER.de offers High-End Servers for hosting and gaming since 2012. Fast, excellent and well-known for eSports Gaming. |
| [**RoyaleHosting**](https://royalehosting.net/) | Build your dreams and deploy them with RoyaleHostings reliable servers and network. Easy to use, provisioned in a couple of minutes. | | [**RoyaleHosting**](https://royalehosting.net/) | Build your dreams and deploy them with RoyaleHostings reliable servers and network. Easy to use, provisioned in a couple of minutes. |
| [**Spill Hosting**](https://spillhosting.no/) | Spill Hosting is a Norwegian hosting service, which aims to cheap services on quality servers. Premium i9-9900K processors will run your game like a dream. |
| [**DeinServerHost**](https://deinserverhost.de/) | DeinServerHost offers Dedicated, vps and Gameservers for many popular Games like Minecraft and Rust in Germany since 2013. |
## Documentation ## Documentation
* [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html) * [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html)

View File

@@ -2,11 +2,13 @@ package api
import ( import (
"bytes" "bytes"
"emperror.dev/errors"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/apex/log" "github.com/apex/log"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/system"
"io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"strings" "strings"
@@ -14,30 +16,47 @@ import (
) )
// Initializes the requester instance. // Initializes the requester instance.
func NewRequester() *PanelRequest { func New() *Request {
return &PanelRequest{ return &Request{}
Response: nil,
}
} }
type PanelRequest struct { // A generic type allowing for easy binding use when making requests to API endpoints
Response *http.Response // that only expect a singular argument or something that would not benefit from being
// a typed struct.
//
// Inspired by gin.H, same concept.
type D map[string]interface{}
// Same concept as D, but a map of strings, used for querying GET requests.
type Q map[string]string
// A custom API requester struct for Wings.
type Request struct{}
// A custom response type that allows for commonly used error handling and response
// parsing from the Panel API. This just embeds the normal HTTP response from Go and
// we attach a few helper functions to it.
type Response struct {
*http.Response
}
// A pagination struct matching the expected pagination response from the Panel API.
type Pagination struct {
CurrentPage uint `json:"current_page"`
From uint `json:"from"`
LastPage uint `json:"last_page"`
PerPage uint `json:"per_page"`
To uint `json:"to"`
Total uint `json:"total"`
} }
// Builds the base request instance that can be used with the HTTP client. // Builds the base request instance that can be used with the HTTP client.
func (r *PanelRequest) GetClient() *http.Client { func (r *Request) Client() *http.Client {
return &http.Client{Timeout: time.Second * 30} return &http.Client{Timeout: time.Second * time.Duration(config.Get().RemoteQuery.Timeout)}
} }
func (r *PanelRequest) SetHeaders(req *http.Request) *http.Request { // Returns the given endpoint formatted as a URL to the Panel API.
req.Header.Set("Accept", "application/vnd.pterodactyl.v1+json") func (r *Request) Endpoint(endpoint string) string {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s.%s", config.Get().AuthenticationTokenId, config.Get().AuthenticationToken))
return req
}
func (r *PanelRequest) GetEndpoint(endpoint string) string {
return fmt.Sprintf( return fmt.Sprintf(
"%s/api/remote/%s", "%s/api/remote/%s",
strings.TrimSuffix(config.Get().PanelLocation, "/"), strings.TrimSuffix(config.Get().PanelLocation, "/"),
@@ -45,9 +64,35 @@ func (r *PanelRequest) GetEndpoint(endpoint string) string {
) )
} }
// Makes a HTTP request to the given endpoint, attaching the necessary request headers from
// Wings to ensure that the request is properly handled by the Panel.
func (r *Request) Make(method, url string, body io.Reader, opts ...func(r *http.Request)) (*Response, error) {
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, errors.WithStackIf(err)
}
req.Header.Set("User-Agent", fmt.Sprintf("Pterodactyl Wings/v%s (id:%s)", system.Version, config.Get().AuthenticationTokenId))
req.Header.Set("Accept", "application/vnd.pterodactyl.v1+json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s.%s", config.Get().AuthenticationTokenId, config.Get().AuthenticationToken))
// Make any options calls that will allow us to make modifications to the request
// before it is sent off.
for _, cb := range opts {
cb(req)
}
r.debug(req)
res, err := r.Client().Do(req)
return &Response{Response: res}, err
}
// Logs the request into the debug log with all of the important request bits. // Logs the request into the debug log with all of the important request bits.
// The authorization key will be cleaned up before being output. // The authorization key will be cleaned up before being output.
func (r *PanelRequest) logDebug(req *http.Request) { func (r *Request) debug(req *http.Request) {
headers := make(map[string][]string) headers := make(map[string][]string)
for k, v := range req.Header { for k, v := range req.Header {
if k != "Authorization" || len(v) == 0 { if k != "Authorization" || len(v) == 0 {
@@ -65,49 +110,44 @@ func (r *PanelRequest) logDebug(req *http.Request) {
}).Debug("making request to external HTTP endpoint") }).Debug("making request to external HTTP endpoint")
} }
func (r *PanelRequest) Get(url string) (*http.Response, error) { // Makes a GET request to the given Panel API endpoint. If any data is passed as the
c := r.GetClient() // second argument it will be passed through on the request as URL parameters.
func (r *Request) Get(url string, data Q) (*Response, error) {
return r.Make(http.MethodGet, r.Endpoint(url), nil, func(r *http.Request) {
q := r.URL.Query()
for k, v := range data {
q.Set(k, v)
}
req, err := http.NewRequest(http.MethodGet, r.GetEndpoint(url), nil) r.URL.RawQuery = q.Encode()
req = r.SetHeaders(req) })
}
// Makes a POST request to the given Panel API endpoint.
func (r *Request) Post(url string, data interface{}) (*Response, error) {
b, err := json.Marshal(data)
if err != nil { if err != nil {
return nil, err return nil, errors.WithStackIf(err)
} }
r.logDebug(req) return r.Make(http.MethodPost, r.Endpoint(url), bytes.NewBuffer(b))
return c.Do(req)
}
func (r *PanelRequest) Post(url string, data []byte) (*http.Response, error) {
c := r.GetClient()
req, err := http.NewRequest(http.MethodPost, r.GetEndpoint(url), bytes.NewBuffer(data))
req = r.SetHeaders(req)
if err != nil {
return nil, err
}
r.logDebug(req)
return c.Do(req)
} }
// Determines if the API call encountered an error. If no request has been made // Determines if the API call encountered an error. If no request has been made
// the response will be false. // the response will be false. This function will evaluate to true if the response
func (r *PanelRequest) HasError() bool { // code is anything 300 or higher.
func (r *Response) HasError() bool {
if r.Response == nil { if r.Response == nil {
return false return false
} }
return r.Response.StatusCode >= 300 || r.Response.StatusCode < 200 return r.StatusCode >= 300 || r.StatusCode < 200
} }
// Reads the body from the response and returns it, then replaces it on the response // Reads the body from the response and returns it, then replaces it on the response
// so that it can be read again later. // so that it can be read again later. This does not close the response body, so any
func (r *PanelRequest) ReadBody() ([]byte, error) { // functions calling this should be sure to manually defer a Body.Close() call.
func (r *Response) Read() ([]byte, error) {
var b []byte var b []byte
if r.Response == nil { if r.Response == nil {
return nil, errors.New("no response exists on interface") return nil, errors.New("no response exists on interface")
@@ -122,51 +162,30 @@ func (r *PanelRequest) ReadBody() ([]byte, error) {
return b, nil return b, nil
} }
func (r *PanelRequest) HttpResponseCode() int { // Binds a given interface with the data returned in the response. This is a shortcut
if r.Response == nil { // for calling Read and then manually calling json.Unmarshal on the raw bytes.
return 0 func (r *Response) Bind(v interface{}) error {
b, err := r.Read()
if err != nil {
return errors.WithStackIf(err)
} }
return r.Response.StatusCode return errors.WithStackIf(json.Unmarshal(b, &v))
}
func IsRequestError(err error) bool {
_, ok := err.(*RequestError)
return ok
}
type RequestError struct {
response *http.Response
Code string `json:"code"`
Status string `json:"status"`
Detail string `json:"detail"`
}
// Returns the error response in a string form that can be more easily consumed.
func (re *RequestError) Error() string {
return fmt.Sprintf("Error response from Panel: %s: %s (HTTP/%d)", re.Code, re.Detail, re.response.StatusCode)
}
func (re *RequestError) String() string {
return re.Error()
}
type RequestErrorBag struct {
Errors []RequestError `json:"errors"`
} }
// Returns the error message from the API call as a string. The error message will be formatted // Returns the error message from the API call as a string. The error message will be formatted
// similar to the below example: // similar to the below example:
// //
// HttpNotFoundException: The requested resource does not exist. (HTTP/404) // HttpNotFoundException: The requested resource does not exist. (HTTP/404)
func (r *PanelRequest) Error() *RequestError { func (r *Response) Error() error {
body, _ := r.ReadBody() if !r.HasError() {
return nil
}
bag := RequestErrorBag{} var bag RequestErrorBag
json.Unmarshal(body, &bag) _ = r.Bind(&bag)
e := new(RequestError) e := &RequestError{}
if len(bag.Errors) > 0 { if len(bag.Errors) > 0 {
e = &bag.Errors[0] e = &bag.Errors[0]
} }

View File

@@ -1,11 +1,37 @@
package api package api
import ( import (
"encoding/json" "emperror.dev/errors"
"fmt" "fmt"
"github.com/pkg/errors" "strconv"
) )
type BackupRemoteUploadResponse struct {
CompleteMultipartUpload string `json:"complete_multipart_upload"`
AbortMultipartUpload string `json:"abort_multipart_upload"`
Parts []string `json:"parts"`
PartSize int64 `json:"part_size"`
}
func (r *Request) GetBackupRemoteUploadURLs(backup string, size int64) (*BackupRemoteUploadResponse, error) {
resp, err := r.Get(fmt.Sprintf("/backups/%s", backup), Q{"size": strconv.FormatInt(size, 10)})
if err != nil {
return nil, errors.WithStackIf(err)
}
defer resp.Body.Close()
if resp.HasError() {
return nil, resp.Error()
}
var res BackupRemoteUploadResponse
if err := resp.Bind(&res); err != nil {
return nil, errors.WithStackIf(err)
}
return &res, nil
}
type BackupRequest struct { type BackupRequest struct {
Checksum string `json:"checksum"` Checksum string `json:"checksum"`
ChecksumType string `json:"checksum_type"` ChecksumType string `json:"checksum_type"`
@@ -15,22 +41,12 @@ type BackupRequest struct {
// Notifies the panel that a specific backup has been completed and is now // Notifies the panel that a specific backup has been completed and is now
// available for a user to view and download. // available for a user to view and download.
func (r *PanelRequest) SendBackupStatus(backup string, data BackupRequest) (*RequestError, error) { func (r *Request) SendBackupStatus(backup string, data BackupRequest) error {
b, err := json.Marshal(data) resp, err := r.Post(fmt.Sprintf("/backups/%s", backup), data)
if err != nil { if err != nil {
return nil, errors.WithStack(err) return errors.WithStackIf(err)
}
resp, err := r.Post(fmt.Sprintf("/backups/%s", backup), b)
if err != nil {
return nil, errors.WithStack(err)
} }
defer resp.Body.Close() defer resp.Body.Close()
r.Response = resp return resp.Error()
if r.HasError() {
return r.Error(), nil
}
return nil, nil
} }

33
api/error.go Normal file
View File

@@ -0,0 +1,33 @@
package api
import (
"fmt"
"net/http"
)
type RequestErrorBag struct {
Errors []RequestError `json:"errors"`
}
type RequestError struct {
response *http.Response
Code string `json:"code"`
Status string `json:"status"`
Detail string `json:"detail"`
}
func IsRequestError(err error) bool {
_, ok := err.(*RequestError)
return ok
}
// Returns the error response in a string form that can be more easily consumed.
func (re *RequestError) Error() string {
c := 0
if re.response != nil {
c = re.response.StatusCode
}
return fmt.Sprintf("Error response from Panel: %s: %s (HTTP/%d)", re.Code, re.Detail, c)
}

View File

@@ -1,9 +1,15 @@
package api package api
import ( import (
"context"
"emperror.dev/errors"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/pkg/errors" "github.com/apex/log"
"github.com/pterodactyl/wings/config"
"golang.org/x/sync/errgroup"
"strconv"
"sync"
) )
const ( const (
@@ -33,157 +39,173 @@ type InstallationScript struct {
Script string `json:"script"` Script string `json:"script"`
} }
// GetAllServerConfigurations fetches configurations for all servers assigned to this node. type allServerResponse struct {
func (r *PanelRequest) GetAllServerConfigurations() (map[string]*ServerConfigurationResponse, *RequestError, error) { Data []RawServerData `json:"data"`
resp, err := r.Get("/servers") Meta Pagination `json:"meta"`
}
type RawServerData struct {
Uuid string `json:"uuid"`
Settings json.RawMessage `json:"settings"`
ProcessConfiguration json.RawMessage `json:"process_configuration"`
}
// Fetches all of the server configurations from the Panel API. This will initially load the
// first 50 servers, and then check the pagination response to determine if more pages should
// be loaded. If so, those requests are spun-up in additional routines and the final resulting
// slice of all servers will be returned.
func (r *Request) GetServers() ([]RawServerData, error) {
resp, err := r.Get("/servers", Q{"per_page": strconv.Itoa(int(config.Get().RemoteQuery.BootServersPerPage))})
if err != nil { if err != nil {
return nil, nil, errors.WithStack(err) return nil, errors.WithStackIf(err)
} }
defer resp.Body.Close() defer resp.Body.Close()
r.Response = resp if resp.HasError() {
return nil, resp.Error()
if r.HasError() {
return nil, r.Error(), nil
} }
b, _ := r.ReadBody() var res allServerResponse
res := map[string]*ServerConfigurationResponse{} if err := resp.Bind(&res); err != nil {
if len(b) == 2 { return nil, errors.WithStackIf(err)
return res, nil, nil
} }
if err := json.Unmarshal(b, &res); err != nil { var mu sync.Mutex
return nil, nil, errors.WithStack(err) ret := res.Data
// Check for pagination, and if it exists we'll need to then make a request to the API
// for each page that would exist and get all of the resulting servers.
if res.Meta.LastPage > 1 {
pp := res.Meta.PerPage
log.WithField("per_page", pp).
WithField("total_pages", res.Meta.LastPage).
Debug("detected multiple pages of server configurations, fetching remaining...")
g, ctx := errgroup.WithContext(context.Background())
for i := res.Meta.CurrentPage + 1; i <= res.Meta.LastPage; i++ {
page := strconv.Itoa(int(i))
g.Go(func() error {
select {
case <-ctx.Done():
return ctx.Err()
default:
{
resp, err := r.Get("/servers", Q{"page": page, "per_page": strconv.Itoa(int(pp))})
if err != nil {
return err
}
defer resp.Body.Close()
if resp.Error() != nil {
return resp.Error()
} }
return res, nil, nil var servers allServerResponse
if err := resp.Bind(&servers); err != nil {
return err
}
mu.Lock()
defer mu.Unlock()
ret = append(ret, servers.Data...)
return nil
}
}
})
}
if err := g.Wait(); err != nil {
return nil, errors.WithStackIf(err)
}
}
return ret, nil
} }
// Fetches the server configuration and returns the struct for it. // Fetches the server configuration and returns the struct for it.
func (r *PanelRequest) GetServerConfiguration(uuid string) (*ServerConfigurationResponse, *RequestError, error) { func (r *Request) GetServerConfiguration(uuid string) (ServerConfigurationResponse, error) {
resp, err := r.Get(fmt.Sprintf("/servers/%s", uuid)) var cfg ServerConfigurationResponse
resp, err := r.Get(fmt.Sprintf("/servers/%s", uuid), nil)
if err != nil { if err != nil {
return nil, nil, errors.WithStack(err) return cfg, errors.WithStackIf(err)
} }
defer resp.Body.Close() defer resp.Body.Close()
r.Response = resp if resp.HasError() {
return cfg, resp.Error()
if r.HasError() {
return nil, r.Error(), nil
} }
res := &ServerConfigurationResponse{} if err := resp.Bind(&cfg); err != nil {
b, _ := r.ReadBody() return cfg, errors.WithStackIf(err)
if err := json.Unmarshal(b, res); err != nil {
return nil, nil, errors.WithStack(err)
} }
return res, nil, nil return cfg, nil
} }
// Fetches installation information for the server process. // Fetches installation information for the server process.
func (r *PanelRequest) GetInstallationScript(uuid string) (InstallationScript, *RequestError, error) { func (r *Request) GetInstallationScript(uuid string) (InstallationScript, error) {
res := InstallationScript{} var is InstallationScript
resp, err := r.Get(fmt.Sprintf("/servers/%s/install", uuid), nil)
resp, err := r.Get(fmt.Sprintf("/servers/%s/install", uuid))
if err != nil { if err != nil {
return res, nil, errors.WithStack(err) return is, errors.WithStackIf(err)
} }
defer resp.Body.Close() defer resp.Body.Close()
r.Response = resp if resp.HasError() {
return is, resp.Error()
if r.HasError() {
return res, r.Error(), nil
} }
b, _ := r.ReadBody() if err := resp.Bind(&is); err != nil {
return is, errors.WithStackIf(err)
if err := json.Unmarshal(b, &res); err != nil {
return res, nil, errors.WithStack(err)
} }
return res, nil, nil return is, nil
}
type installRequest struct {
Successful bool `json:"successful"`
} }
// Marks a server as being installed successfully or unsuccessfully on the panel. // Marks a server as being installed successfully or unsuccessfully on the panel.
func (r *PanelRequest) SendInstallationStatus(uuid string, successful bool) (*RequestError, error) { func (r *Request) SendInstallationStatus(uuid string, successful bool) error {
b, err := json.Marshal(installRequest{Successful: successful}) resp, err := r.Post(fmt.Sprintf("/servers/%s/install", uuid), D{"successful": successful})
if err != nil { if err != nil {
return nil, errors.WithStack(err) return errors.WithStackIf(err)
}
resp, err := r.Post(fmt.Sprintf("/servers/%s/install", uuid), b)
if err != nil {
return nil, errors.WithStack(err)
} }
defer resp.Body.Close() defer resp.Body.Close()
r.Response = resp if resp.HasError() {
if r.HasError() { return resp.Error()
return r.Error(), nil
} }
return nil, nil return nil
} }
type archiveRequest struct { func (r *Request) SendArchiveStatus(uuid string, successful bool) error {
Successful bool `json:"successful"` resp, err := r.Post(fmt.Sprintf("/servers/%s/archive", uuid), D{"successful": successful})
}
func (r *PanelRequest) SendArchiveStatus(uuid string, successful bool) (*RequestError, error) {
b, err := json.Marshal(archiveRequest{Successful: successful})
if err != nil { if err != nil {
return nil, errors.WithStack(err) return errors.WithStackIf(err)
}
resp, err := r.Post(fmt.Sprintf("/servers/%s/archive", uuid), b)
if err != nil {
return nil, errors.WithStack(err)
} }
defer resp.Body.Close() defer resp.Body.Close()
r.Response = resp return resp.Error()
if r.HasError() {
return r.Error(), nil
} }
return nil, nil func (r *Request) SendTransferFailure(uuid string) error {
} resp, err := r.Get(fmt.Sprintf("/servers/%s/transfer/failure", uuid), nil)
func (r *PanelRequest) SendTransferFailure(uuid string) (*RequestError, error) {
resp, err := r.Get(fmt.Sprintf("/servers/%s/transfer/failure", uuid))
if err != nil { if err != nil {
return nil, errors.WithStack(err) return errors.WithStackIf(err)
} }
defer resp.Body.Close() defer resp.Body.Close()
r.Response = resp return resp.Error()
if r.HasError() {
return r.Error(), nil
} }
return nil, nil func (r *Request) SendTransferSuccess(uuid string) error {
} resp, err := r.Get(fmt.Sprintf("/servers/%s/transfer/success", uuid), nil)
func (r *PanelRequest) SendTransferSuccess(uuid string) (*RequestError, error) {
resp, err := r.Get(fmt.Sprintf("/servers/%s/transfer/success", uuid))
if err != nil { if err != nil {
return nil, errors.WithStack(err) return errors.WithStackIf(err)
} }
defer resp.Body.Close() defer resp.Body.Close()
r.Response = resp return resp.Error()
if r.HasError() {
return r.Error(), nil
}
return nil, nil
} }

View File

@@ -1,9 +1,8 @@
package api package api
import ( import (
"encoding/json" "emperror.dev/errors"
"github.com/apex/log" "github.com/apex/log"
"github.com/pkg/errors"
"regexp" "regexp"
) )
@@ -39,7 +38,7 @@ func IsInvalidCredentialsError(err error) bool {
// server and sending a flood of usernames. // server and sending a flood of usernames.
var validUsernameRegexp = regexp.MustCompile(`^(?i)(.+)\.([a-z0-9]{8})$`) var validUsernameRegexp = regexp.MustCompile(`^(?i)(.+)\.([a-z0-9]{8})$`)
func (r *PanelRequest) ValidateSftpCredentials(request SftpAuthRequest) (*SftpAuthResponse, error) { func (r *Request) ValidateSftpCredentials(request SftpAuthRequest) (*SftpAuthResponse, error) {
// If the username doesn't meet the expected format that the Panel would even recognize just go ahead // If the username doesn't meet the expected format that the Panel would even recognize just go ahead
// and bail out of the process here to avoid accidentally brute forcing the panel if a bot decides // and bail out of the process here to avoid accidentally brute forcing the panel if a bot decides
// to connect to spam username attempts. // to connect to spam username attempts.
@@ -53,41 +52,33 @@ func (r *PanelRequest) ValidateSftpCredentials(request SftpAuthRequest) (*SftpAu
return nil, new(sftpInvalidCredentialsError) return nil, new(sftpInvalidCredentialsError)
} }
b, err := json.Marshal(request) resp, err := r.Post("/sftp/auth", request)
if err != nil {
return nil, err
}
resp, err := r.Post("/sftp/auth", b)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
r.Response = resp e := resp.Error()
if e != nil {
if r.HasError() { if resp.StatusCode >= 400 && resp.StatusCode < 500 {
if r.HttpResponseCode() >= 400 && r.HttpResponseCode() < 500 {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"subsystem": "sftp", "subsystem": "sftp",
"username": request.User, "username": request.User,
"ip": request.IP, "ip": request.IP,
}).Warn(r.Error().String()) }).Warn(e.Error())
return nil, new(sftpInvalidCredentialsError) return nil, &sftpInvalidCredentialsError{}
} }
rerr := errors.New(r.Error().String()) rerr := errors.New(e.Error())
return nil, rerr return nil, rerr
} }
response := new(SftpAuthResponse) var response SftpAuthResponse
body, _ := r.ReadBody() if err := resp.Bind(&response); err != nil {
if err := json.Unmarshal(body, response); err != nil {
return nil, err return nil, err
} }
return response, nil return &response, nil
} }

View File

@@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/pterodactyl/wings/environment"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
@@ -19,7 +20,6 @@ import (
"github.com/AlecAivazis/survey/v2/terminal" "github.com/AlecAivazis/survey/v2/terminal"
"github.com/docker/cli/components/engine/pkg/parsers/operatingsystem" "github.com/docker/cli/components/engine/pkg/parsers/operatingsystem"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/parsers/kernel" "github.com/docker/docker/pkg/parsers/kernel"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/system" "github.com/pterodactyl/wings/system"
@@ -91,7 +91,7 @@ func diagnosticsCmdRun(cmd *cobra.Command, args []string) {
printHeader(output, "Versions") printHeader(output, "Versions")
fmt.Fprintln(output, " wings:", system.Version) fmt.Fprintln(output, " wings:", system.Version)
if dockerErr == nil { if dockerErr == nil {
fmt.Fprintln(output, "Docker", dockerVersion.Version) fmt.Fprintln(output, "Docker:", dockerVersion.Version)
} }
if v, err := kernel.GetKernelVersion(); err == nil { if v, err := kernel.GetKernelVersion(); err == nil {
fmt.Fprintln(output, "Kernel:", v) fmt.Fprintln(output, "Kernel:", v)
@@ -105,7 +105,7 @@ func diagnosticsCmdRun(cmd *cobra.Command, args []string) {
if cfg != nil { if cfg != nil {
fmt.Fprintln(output, " Panel Location:", redact(cfg.PanelLocation)) fmt.Fprintln(output, " Panel Location:", redact(cfg.PanelLocation))
fmt.Fprintln(output, "") fmt.Fprintln(output, "")
fmt.Fprintln(output, "Internal Webserver:", redact(cfg.Api.Host) + ":", cfg.Api.Port) fmt.Fprintln(output, " Internal Webserver:", redact(cfg.Api.Host), ":", cfg.Api.Port)
fmt.Fprintln(output, " SSL Enabled:", cfg.Api.Ssl.Enabled) fmt.Fprintln(output, " SSL Enabled:", cfg.Api.Ssl.Enabled)
fmt.Fprintln(output, " SSL Certificate:", redact(cfg.Api.Ssl.CertificateFile)) fmt.Fprintln(output, " SSL Certificate:", redact(cfg.Api.Ssl.CertificateFile))
fmt.Fprintln(output, " SSL Key:", redact(cfg.Api.Ssl.KeyFile)) fmt.Fprintln(output, " SSL Key:", redact(cfg.Api.Ssl.KeyFile))
@@ -187,7 +187,7 @@ func diagnosticsCmdRun(cmd *cobra.Command, args []string) {
} }
func getDockerInfo() (types.Version, types.Info, error) { func getDockerInfo() (types.Version, types.Info, error) {
cli, err := client.NewClientWithOpts(client.FromEnv) cli, err := environment.DockerClient()
if err != nil { if err != nil {
return types.Version{}, types.Info{}, err return types.Version{}, types.Info{}, err
} }

View File

@@ -19,7 +19,7 @@ import (
"github.com/pterodactyl/wings/loggers/cli" "github.com/pterodactyl/wings/loggers/cli"
"golang.org/x/crypto/acme/autocert" "golang.org/x/crypto/acme/autocert"
"github.com/pkg/errors" "emperror.dev/errors"
"github.com/pkg/profile" "github.com/pkg/profile"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
@@ -30,12 +30,14 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var configPath = config.DefaultLocation var (
var debug = false profiler = ""
var shouldRunProfiler = false configPath = config.DefaultLocation
var useAutomaticTls = false debug = false
var tlsHostname = "" useAutomaticTls = false
var showVersion = false tlsHostname = ""
showVersion = false
)
var root = &cobra.Command{ var root = &cobra.Command{
Use: "wings", Use: "wings",
@@ -54,7 +56,7 @@ func init() {
root.PersistentFlags().BoolVar(&showVersion, "version", false, "show the version and exit") root.PersistentFlags().BoolVar(&showVersion, "version", false, "show the version and exit")
root.PersistentFlags().StringVar(&configPath, "config", config.DefaultLocation, "set the location for the configuration file") root.PersistentFlags().StringVar(&configPath, "config", config.DefaultLocation, "set the location for the configuration file")
root.PersistentFlags().BoolVar(&debug, "debug", false, "pass in order to run wings in debug mode") root.PersistentFlags().BoolVar(&debug, "debug", false, "pass in order to run wings in debug mode")
root.PersistentFlags().BoolVar(&shouldRunProfiler, "profile", false, "pass in order to profile wings") root.PersistentFlags().StringVar(&profiler, "profiler", "", "the profiler to run for this instance")
root.PersistentFlags().BoolVar(&useAutomaticTls, "auto-tls", false, "pass in order to have wings generate and manage it's own SSL certificates using Let's Encrypt") root.PersistentFlags().BoolVar(&useAutomaticTls, "auto-tls", false, "pass in order to have wings generate and manage it's own SSL certificates using Let's Encrypt")
root.PersistentFlags().StringVar(&tlsHostname, "tls-hostname", "", "required with --auto-tls, the FQDN for the generated SSL certificate") root.PersistentFlags().StringVar(&tlsHostname, "tls-hostname", "", "required with --auto-tls, the FQDN for the generated SSL certificate")
@@ -75,7 +77,7 @@ func readConfiguration() (*config.Configuration, error) {
} }
if s, err := os.Stat(p); err != nil { if s, err := os.Stat(p); err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStackIf(err)
} else if s.IsDir() { } else if s.IsDir() {
return nil, errors.New("cannot use directory as configuration file path") return nil, errors.New("cannot use directory as configuration file path")
} }
@@ -89,15 +91,30 @@ func rootCmdRun(*cobra.Command, []string) {
os.Exit(0) os.Exit(0)
} }
if shouldRunProfiler { switch profiler {
defer profile.Start().Stop() case "cpu":
defer profile.Start(profile.CPUProfile).Stop()
case "mem":
defer profile.Start(profile.MemProfile).Stop()
case "alloc":
defer profile.Start(profile.MemProfile, profile.MemProfileAllocs()).Stop()
case "heap":
defer profile.Start(profile.MemProfile, profile.MemProfileHeap()).Stop()
case "routines":
defer profile.Start(profile.GoroutineProfile).Stop()
case "mutex":
defer profile.Start(profile.MutexProfile).Stop()
case "threads":
defer profile.Start(profile.ThreadcreationProfile).Stop()
case "block":
defer profile.Start(profile.BlockProfile).Stop()
} }
// Only attempt configuration file relocation if a custom location has not // Only attempt configuration file relocation if a custom location has not
// been specified in the command startup. // been specified in the command startup.
if configPath == config.DefaultLocation { if configPath == config.DefaultLocation {
if err := RelocateConfiguration(); err != nil { if err := RelocateConfiguration(); err != nil {
if os.IsNotExist(err) { if errors.Is(err, os.ErrNotExist) {
exitWithConfigurationNotice() exitWithConfigurationNotice()
} }
@@ -132,6 +149,13 @@ func rootCmdRun(*cobra.Command, []string) {
config.Set(c) config.Set(c)
config.SetDebugViaFlag(debug) config.SetDebugViaFlag(debug)
if err := c.System.ConfigureTimezone(); err != nil {
log.WithField("error", err).Fatal("failed to detect system timezone or use supplied configuration value")
return
}
log.WithField("timezone", c.System.Timezone).Info("configured wings with system timezone")
if err := c.System.ConfigureDirectories(); err != nil { if err := c.System.ConfigureDirectories(); err != nil {
log.WithField("error", err).Fatal("failed to configure system directories for pterodactyl") log.WithField("error", err).Fatal("failed to configure system directories for pterodactyl")
return return
@@ -175,7 +199,7 @@ func rootCmdRun(*cobra.Command, []string) {
states, err := server.CachedServerStates() states, err := server.CachedServerStates()
if err != nil { if err != nil {
log.WithField("error", errors.WithStack(err)).Error("failed to retrieve locally cached server states from disk, assuming all servers in offline state") log.WithField("error", errors.WithStackIf(err)).Error("failed to retrieve locally cached server states from disk, assuming all servers in offline state")
} }
// Create a new workerpool that limits us to 4 servers being bootstrapped at a time // Create a new workerpool that limits us to 4 servers being bootstrapped at a time
@@ -211,7 +235,7 @@ func rootCmdRun(*cobra.Command, []string) {
// as a result will result in a slow boot. // as a result will result in a slow boot.
if !r && (st == environment.ProcessRunningState || st == environment.ProcessStartingState) { if !r && (st == environment.ProcessRunningState || st == environment.ProcessStartingState) {
if err := s.HandlePowerAction(server.PowerActionStart); err != nil { if err := s.HandlePowerAction(server.PowerActionStart); err != nil {
s.Log().WithField("error", errors.WithStack(err)).Warn("failed to return server to running state") s.Log().WithField("error", errors.WithStackIf(err)).Warn("failed to return server to running state")
} }
} else if r || (!r && s.IsRunning()) { } else if r || (!r && s.IsRunning()) {
// If the server is currently running on Docker, mark the process as being in that state. // If the server is currently running on Docker, mark the process as being in that state.
@@ -222,9 +246,9 @@ func rootCmdRun(*cobra.Command, []string) {
// is that it was running, but we see that the container process is not currently running. // is that it was running, but we see that the container process is not currently running.
s.Log().Info("detected server is running, re-attaching to process...") s.Log().Info("detected server is running, re-attaching to process...")
s.SetState(environment.ProcessRunningState) s.Environment.SetState(environment.ProcessRunningState)
if err := s.Environment.Attach(); err != nil { if err := s.Environment.Attach(); err != nil {
s.Log().WithField("error", errors.WithStack(err)).Warn("failed to attach to running server environment") s.Log().WithField("error", errors.WithStackIf(err)).Warn("failed to attach to running server environment")
} }
return return
@@ -232,7 +256,7 @@ func rootCmdRun(*cobra.Command, []string) {
// Addresses potentially invalid data in the stored file that can cause Wings to lose // Addresses potentially invalid data in the stored file that can cause Wings to lose
// track of what the actual server state is. // track of what the actual server state is.
_ = s.SetState(environment.ProcessOfflineState) s.Environment.SetState(environment.ProcessOfflineState)
}) })
} }
@@ -270,34 +294,20 @@ func rootCmdRun(*cobra.Command, []string) {
Handler: r, Handler: r,
TLSConfig: &tls.Config{ TLSConfig: &tls.Config{
NextProtos: []string{ NextProtos: []string{"h2", "http/1.1"},
"h2", // enable HTTP/2 // @see https://blog.cloudflare.com/exposing-go-on-the-internet
"http/1.1",
},
// https://blog.cloudflare.com/exposing-go-on-the-internet
CipherSuites: []uint16{ CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
}, },
PreferServerCipherSuites: true, PreferServerCipherSuites: true,
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS13, MaxVersion: tls.VersionTLS13,
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
CurvePreferences: []tls.CurveID{
tls.X25519,
tls.CurveP256,
},
// END https://blog.cloudflare.com/exposing-go-on-the-internet
}, },
} }
@@ -359,13 +369,13 @@ func Execute() error {
// in the code without having to pass around a logger instance. // in the code without having to pass around a logger instance.
func configureLogging(logDir string, debug bool) error { func configureLogging(logDir string, debug bool) error {
if err := os.MkdirAll(path.Join(logDir, "/install"), 0700); err != nil { if err := os.MkdirAll(path.Join(logDir, "/install"), 0700); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
p := filepath.Join(logDir, "/wings.log") p := filepath.Join(logDir, "/wings.log")
w, err := logrotate.NewFile(p) w, err := logrotate.NewFile(p)
if err != nil { if err != nil {
panic(errors.Wrap(err, "failed to open process log file")) panic(errors.WrapIf(err, "failed to open process log file"))
} }
if debug { if debug {

View File

@@ -1,11 +1,11 @@
package config package config
import ( import (
"emperror.dev/errors"
"fmt" "fmt"
"github.com/cobaugh/osrelease" "github.com/cobaugh/osrelease"
"github.com/creasty/defaults" "github.com/creasty/defaults"
"github.com/gbrlsnchs/jwt/v3" "github.com/gbrlsnchs/jwt/v3"
"github.com/pkg/errors"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
"io/ioutil" "io/ioutil"
"os" "os"
@@ -60,6 +60,7 @@ type Configuration struct {
// The location where the panel is running that this daemon should connect to // The location where the panel is running that this daemon should connect to
// to collect data and send events. // to collect data and send events.
PanelLocation string `json:"remote" yaml:"remote"` PanelLocation string `json:"remote" yaml:"remote"`
RemoteQuery RemoteQueryConfiguration `json:"remote_query" yaml:"remote_query"`
// AllowedMounts is a list of allowed host-system mount points. // AllowedMounts is a list of allowed host-system mount points.
// This is required to have the "Server Mounts" feature work properly. // This is required to have the "Server Mounts" feature work properly.
@@ -101,6 +102,27 @@ type ApiConfiguration struct {
UploadLimit int `default:"100" json:"upload_limit" yaml:"upload_limit"` UploadLimit int `default:"100" json:"upload_limit" yaml:"upload_limit"`
} }
// Defines the configuration settings for remote requests from Wings to the Panel.
type RemoteQueryConfiguration struct {
// The amount of time in seconds that Wings should allow for a request to the Panel API
// to complete. If this time passes the request will be marked as failed. If your requests
// are taking longer than 30 seconds to complete it is likely a performance issue that
// should be resolved on the Panel, and not something that should be resolved by upping this
// number.
Timeout uint `default:"30" yaml:"timeout"`
// The number of servers to load in a single request to the Panel API when booting the
// Wings instance. A single request is initially made to the Panel to get this number
// of servers, and then the pagination status is checked and additional requests are
// fired off in parallel to request the remaining pages.
//
// It is not recommended to change this from the default as you will likely encounter
// memory limits on your Panel instance. In the grand scheme of things 4 requests for
// 50 servers is likely just as quick as two for 100 or one for 400, and will certainly
// be less likely to cause performance issues on the Panel.
BootServersPerPage uint `default:"50" yaml:"boot_servers_per_page"`
}
// Reads the configuration from the provided file and returns the configuration // Reads the configuration from the provided file and returns the configuration
// object that can then be used. // object that can then be used.
func ReadConfiguration(path string) (*Configuration, error) { func ReadConfiguration(path string) (*Configuration, error) {
@@ -176,7 +198,7 @@ func GetJwtAlgorithm() *jwt.HMACSHA {
func NewFromPath(path string) (*Configuration, error) { func NewFromPath(path string) (*Configuration, error) {
c := new(Configuration) c := new(Configuration)
if err := defaults.Set(c); err != nil { if err := defaults.Set(c); err != nil {
return c, errors.WithStack(err) return c, errors.WithStackIf(err)
} }
c.unsafeSetPath(path) c.unsafeSetPath(path)
@@ -214,12 +236,12 @@ func (c *Configuration) EnsurePterodactylUser() (*user.User, error) {
if err == nil { if err == nil {
return u, c.setSystemUser(u) return u, c.setSystemUser(u)
} else if _, ok := err.(user.UnknownUserError); !ok { } else if _, ok := err.(user.UnknownUserError); !ok {
return nil, errors.WithStack(err) return nil, errors.WithStackIf(err)
} }
sysName, err := getSystemName() sysName, err := getSystemName()
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStackIf(err)
} }
var command = fmt.Sprintf("useradd --system --no-create-home --shell /bin/false %s", c.System.Username) var command = fmt.Sprintf("useradd --system --no-create-home --shell /bin/false %s", c.System.Username)
@@ -232,17 +254,17 @@ func (c *Configuration) EnsurePterodactylUser() (*user.User, error) {
// 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.
if _, err := exec.Command("addgroup", "-S", c.System.Username).Output(); err != nil { if _, err := exec.Command("addgroup", "-S", c.System.Username).Output(); err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStackIf(err)
} }
} }
split := strings.Split(command, " ") split := strings.Split(command, " ")
if _, err := exec.Command(split[0], split[1:]...).Output(); err != nil { if _, err := exec.Command(split[0], split[1:]...).Output(); err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStackIf(err)
} }
if u, err := user.Lookup(c.System.Username); err != nil { if u, err := user.Lookup(c.System.Username); err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStackIf(err)
} else { } else {
return u, c.setSystemUser(u) return u, c.setSystemUser(u)
} }
@@ -284,11 +306,11 @@ func (c *Configuration) WriteToDisk() error {
b, err := yaml.Marshal(&ccopy) b, err := yaml.Marshal(&ccopy)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
if err := ioutil.WriteFile(c.GetPath(), b, 0644); err != nil { if err := ioutil.WriteFile(c.GetPath(), b, 0644); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
return nil return nil
@@ -298,7 +320,7 @@ func (c *Configuration) WriteToDisk() error {
func getSystemName() (string, error) { func getSystemName() (string, error) {
// use osrelease to get release version and ID // use osrelease to get release version and ID
if release, err := osrelease.Read(); err != nil { if release, err := osrelease.Read(); err != nil {
return "", errors.WithStack(err) return "", errors.WithStackIf(err)
} else { } else {
return release["ID"], nil return release["ID"], nil
} }

View File

@@ -1,10 +1,10 @@
package config package config
import ( import (
"emperror.dev/errors"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/pkg/errors"
) )
type dockerNetworkInterfaces struct { type dockerNetworkInterfaces struct {
@@ -49,18 +49,6 @@ type DockerConfiguration struct {
// Domainname is the Docker domainname for all containers. // Domainname is the Docker domainname for all containers.
Domainname string `default:"" json:"domainname" yaml:"domainname"` Domainname string `default:"" json:"domainname" yaml:"domainname"`
// If true, container images will be updated when a server starts if there
// is an update available. If false the daemon will not attempt updates and will
// defer to the host system to manage image updates.
UpdateImages bool `default:"true" json:"update_images" yaml:"update_images"`
// The location of the Docker socket.
Socket string `default:"/var/run/docker.sock" json:"socket" yaml:"socket"`
// Defines the location of the timezone file on the host system that should
// be mounted into the created containers so that they all use the same time.
TimezonePath string `default:"/etc/timezone" json:"timezone_path" yaml:"timezone_path"`
// Registries . // Registries .
Registries map[string]RegistryConfiguration `json:"registries" yaml:"registries"` Registries map[string]RegistryConfiguration `json:"registries" yaml:"registries"`
@@ -85,7 +73,7 @@ func (c RegistryConfiguration) Base64() (string, error) {
b, err := json.Marshal(authConfig) b, err := json.Marshal(authConfig)
if err != nil { if err != nil {
return "", errors.WithStack(err) return "", errors.WithStackIf(err)
} }
return base64.URLEncoding.EncodeToString(b), nil return base64.URLEncoding.EncodeToString(b), nil

View File

@@ -1,12 +1,18 @@
package config package config
import ( import (
"context"
"emperror.dev/errors"
"fmt"
"github.com/apex/log" "github.com/apex/log"
"github.com/pkg/errors"
"html/template" "html/template"
"io/ioutil"
"os" "os"
"os/exec"
"path" "path"
"path/filepath" "path/filepath"
"regexp"
"time"
) )
// Defines basic system configuration settings. // Defines basic system configuration settings.
@@ -29,6 +35,13 @@ type SystemConfiguration struct {
// The user that should own all of the server files, and be used for containers. // The user that should own all of the server files, and be used for containers.
Username string `default:"pterodactyl" yaml:"username"` Username string `default:"pterodactyl" yaml:"username"`
// The timezone for this Wings instance. This is detected by Wings automatically if possible,
// and falls back to UTC if not able to be detected. If you need to set this manually, that
// can also be done.
//
// This timezone value is passed into all containers created by Wings.
Timezone string `yaml:"timezone"`
// Definitions for the user that gets created to ensure that we can quickly access // Definitions for the user that gets created to ensure that we can quickly access
// this information without constantly having to do a system lookup. // this information without constantly having to do a system lookup.
User struct { User struct {
@@ -57,6 +70,9 @@ type SystemConfiguration struct {
// when it boots and one is not detected. // when it boots and one is not detected.
EnableLogRotate bool `default:"true" yaml:"enable_log_rotate"` EnableLogRotate bool `default:"true" yaml:"enable_log_rotate"`
// The number of lines to send when a server connects to the websocket.
WebsocketLogCount int `default:"150" yaml:"websocket_log_count"`
Sftp SftpConfiguration `yaml:"sftp"` Sftp SftpConfiguration `yaml:"sftp"`
} }
@@ -78,7 +94,7 @@ func (sc *SystemConfiguration) ConfigureDirectories() error {
// that. // that.
if d, err := filepath.EvalSymlinks(sc.Data); err != nil { if d, err := filepath.EvalSymlinks(sc.Data); err != nil {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
} else if d != sc.Data { } else if d != sc.Data {
sc.Data = d sc.Data = d
@@ -114,13 +130,13 @@ func (sc *SystemConfiguration) EnableLogRotation() error {
} }
if st, err := os.Stat("/etc/logrotate.d"); err != nil && !os.IsNotExist(err) { if st, err := os.Stat("/etc/logrotate.d"); err != nil && !os.IsNotExist(err) {
return errors.WithStack(err) return errors.WithStackIf(err)
} else if (err != nil && os.IsNotExist(err)) || !st.IsDir() { } else if (err != nil && os.IsNotExist(err)) || !st.IsDir() {
return nil return nil
} }
if _, err := os.Stat("/etc/logrotate.d/wings"); err != nil && !os.IsNotExist(err) { if _, err := os.Stat("/etc/logrotate.d/wings"); err != nil && !os.IsNotExist(err) {
return errors.WithStack(err) return errors.WithStackIf(err)
} else if err == nil { } else if err == nil {
return nil return nil
} }
@@ -131,7 +147,7 @@ func (sc *SystemConfiguration) EnableLogRotation() error {
// it so files can be rotated easily. // it so files can be rotated easily.
f, err := os.Create("/etc/logrotate.d/wings") f, err := os.Create("/etc/logrotate.d/wings")
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
defer f.Close() defer f.Close()
@@ -151,10 +167,10 @@ func (sc *SystemConfiguration) EnableLogRotation() error {
}`) }`)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
return errors.Wrap(t.Execute(f, sc), "failed to write logrotate file to disk") return errors.WrapIf(t.Execute(f, sc), "failed to write logrotate file to disk")
} }
// Returns the location of the JSON file that tracks server states. // Returns the location of the JSON file that tracks server states.
@@ -166,3 +182,47 @@ func (sc *SystemConfiguration) GetStatesPath() string {
func (sc *SystemConfiguration) GetInstallLogPath() string { func (sc *SystemConfiguration) GetInstallLogPath() string {
return path.Join(sc.LogDirectory, "install/") return path.Join(sc.LogDirectory, "install/")
} }
// Configures the timezone data for the configuration if it is currently missing. If
// a value has been set, this functionality will only run to validate that the timezone
// being used is valid.
func (sc *SystemConfiguration) ConfigureTimezone() error {
if sc.Timezone == "" {
if b, err := ioutil.ReadFile("/etc/timezone"); err != nil {
if !os.IsNotExist(err) {
return errors.WrapIf(err, "failed to open /etc/timezone for automatic server timezone calibration")
}
ctx, _ := context.WithTimeout(context.Background(), time.Second*5)
// Okay, file isn't found on this OS, we will try using timedatectl to handle this. If this
// command fails, exit, but if it returns a value use that. If no value is returned we will
// fall through to UTC to get Wings booted at least.
out, err := exec.CommandContext(ctx, "timedatectl").Output()
if err != nil {
log.WithField("error", err).Warn("failed to execute \"timedatectl\" to determine system timezone, falling back to UTC")
sc.Timezone = "UTC"
return nil
}
r := regexp.MustCompile(`Time zone: ([\w/]+)`)
matches := r.FindSubmatch(out)
if len(matches) != 2 || string(matches[1]) == "" {
log.Warn("failed to parse timezone from \"timedatectl\" output, falling back to UTC")
sc.Timezone = "UTC"
return nil
}
sc.Timezone = string(matches[1])
} else {
sc.Timezone = string(b)
}
}
sc.Timezone = regexp.MustCompile(`(?i)[^a-z_/]+`).ReplaceAllString(sc.Timezone, "")
_, err := time.LoadLocation(sc.Timezone)
return errors.WrapIf(err, fmt.Sprintf("the supplied timezone %s is invalid", sc.Timezone))
}

View File

@@ -3,6 +3,8 @@ package environment
import ( import (
"context" "context"
"github.com/apex/log" "github.com/apex/log"
"strconv"
"sync"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
@@ -10,10 +12,28 @@ import (
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
) )
var _cmu sync.Mutex
var _client *client.Client
// Return a Docker client to be used throughout the codebase. Once a client has been created it
// will be returned for all subsequent calls to this function.
func DockerClient() (*client.Client, error) {
_cmu.Lock()
defer _cmu.Unlock()
if _client != nil {
return _client, nil
}
_client, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
return _client, err
}
// Configures the required network for the docker environment. // Configures the required network for the docker environment.
func ConfigureDocker(c *config.DockerConfiguration) error { func ConfigureDocker(c *config.DockerConfiguration) error {
// Ensure the required docker network exists on the system. // Ensure the required docker network exists on the system.
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) cli, err := DockerClient()
if err != nil { if err != nil {
return err return err
} }
@@ -64,7 +84,7 @@ func createDockerNetwork(cli *client.Client, c *config.DockerConfiguration) erro
Options: map[string]string{ Options: map[string]string{
"encryption": "false", "encryption": "false",
"com.docker.network.bridge.default_bridge": "false", "com.docker.network.bridge.default_bridge": "false",
"com.docker.network.bridge.enable_icc": "true", "com.docker.network.bridge.enable_icc": strconv.FormatBool(c.Network.EnableICC),
"com.docker.network.bridge.enable_ip_masquerade": "true", "com.docker.network.bridge.enable_ip_masquerade": "true",
"com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
"com.docker.network.bridge.name": "pterodactyl0", "com.docker.network.bridge.name": "pterodactyl0",

View File

@@ -2,7 +2,9 @@ package docker
import ( import (
"bufio" "bufio"
"bytes"
"context" "context"
"emperror.dev/errors"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/apex/log" "github.com/apex/log"
@@ -11,7 +13,6 @@ import (
"github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/mount"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/docker/docker/daemon/logger/jsonfilelog" "github.com/docker/docker/daemon/logger/jsonfilelog"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
"io" "io"
@@ -35,7 +36,7 @@ func (e *Environment) Attach() error {
} }
if err := e.followOutput(); err != nil { if err := e.followOutput(); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
opts := types.ContainerAttachOptions{ opts := types.ContainerAttachOptions{
@@ -47,7 +48,7 @@ func (e *Environment) Attach() error {
// Set the stream again with the container. // Set the stream again with the container.
if st, err := e.client.ContainerAttach(context.Background(), e.Id, opts); err != nil { if st, err := e.client.ContainerAttach(context.Background(), e.Id, opts); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} else { } else {
e.SetStream(&st) e.SetStream(&st)
} }
@@ -59,7 +60,7 @@ func (e *Environment) Attach() error {
defer cancel() defer cancel()
defer e.stream.Close() defer e.stream.Close()
defer func() { defer func() {
e.setState(environment.ProcessOfflineState) e.SetState(environment.ProcessOfflineState)
e.SetStream(nil) e.SetStream(nil)
}() }()
@@ -69,14 +70,14 @@ func (e *Environment) Attach() error {
// indicates that the container is no longer running. // indicates that the container is no longer running.
go func(ctx context.Context) { go func(ctx context.Context) {
if err := e.pollResources(ctx); err != nil { if err := e.pollResources(ctx); err != nil {
log.WithField("environment_id", e.Id).WithField("error", errors.WithStack(err)).Error("error during environment resource polling") log.WithField("environment_id", e.Id).WithField("error", errors.WithStackIf(err)).Error("error during environment resource polling")
} }
}(ctx) }(ctx)
// Stream the reader output to the console which will then fire off events and handle console // Stream the reader output to the console which will then fire off events and handle console
// throttling and sending the output to the user. // throttling and sending the output to the user.
if _, err := io.Copy(console, e.stream.Reader); err != nil { if _, err := io.Copy(console, e.stream.Reader); err != nil {
log.WithField("environment_id", e.Id).WithField("error", errors.WithStack(err)).Error("error while copying environment output to console") log.WithField("environment_id", e.Id).WithField("error", errors.WithStackIf(err)).Error("error while copying environment output to console")
} }
}(c) }(c)
@@ -114,7 +115,7 @@ func (e *Environment) InSituUpdate() error {
return nil return nil
} }
return errors.WithStack(err) return errors.WithStackIf(err)
} }
u := container.UpdateConfig{ u := container.UpdateConfig{
@@ -124,7 +125,7 @@ func (e *Environment) InSituUpdate() error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel() defer cancel()
if _, err := e.client.ContainerUpdate(ctx, e.Id, u); err != nil { if _, err := e.client.ContainerUpdate(ctx, e.Id, u); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
return nil return nil
@@ -139,12 +140,12 @@ func (e *Environment) Create() error {
if _, err := e.client.ContainerInspect(context.Background(), e.Id); err == nil { if _, err := e.client.ContainerInspect(context.Background(), e.Id); err == nil {
return nil return nil
} else if !client.IsErrNotFound(err) { } else if !client.IsErrNotFound(err) {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
// Try to pull the requested image before creating the container. // Try to pull the requested image before creating the container.
if err := e.ensureImageExists(e.meta.Image); err != nil { if err := e.ensureImageExists(e.meta.Image); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
a := e.Configuration.Allocations() a := e.Configuration.Allocations()
@@ -219,7 +220,7 @@ func (e *Environment) Create() error {
} }
if _, err := e.client.ContainerCreate(context.Background(), conf, hostConf, nil, e.Id); err != nil { if _, err := e.client.ContainerCreate(context.Background(), conf, hostConf, nil, e.Id); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
return nil return nil
@@ -244,7 +245,7 @@ func (e *Environment) convertMounts() []mount.Mount {
// it will be forcibly stopped by Docker. // it will be forcibly stopped by Docker.
func (e *Environment) Destroy() error { func (e *Environment) Destroy() error {
// We set it to stopping than offline to prevent crash detection from being triggered. // We set it to stopping than offline to prevent crash detection from being triggered.
e.setState(environment.ProcessStoppingState) e.SetState(environment.ProcessStoppingState)
err := e.client.ContainerRemove(context.Background(), e.Id, types.ContainerRemoveOptions{ err := e.client.ContainerRemove(context.Background(), e.Id, types.ContainerRemoveOptions{
RemoveVolumes: true, RemoveVolumes: true,
@@ -260,7 +261,7 @@ func (e *Environment) Destroy() error {
return nil return nil
} }
e.setState(environment.ProcessOfflineState) e.SetState(environment.ProcessOfflineState)
return err return err
} }
@@ -271,7 +272,7 @@ func (e *Environment) Destroy() error {
func (e *Environment) followOutput() error { func (e *Environment) followOutput() error {
if exists, err := e.Exists(); !exists { if exists, err := e.Exists(); !exists {
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
return errors.New(fmt.Sprintf("no such container: %s", e.Id)) return errors.New(fmt.Sprintf("no such container: %s", e.Id))
@@ -286,20 +287,58 @@ func (e *Environment) followOutput() error {
reader, err := e.client.ContainerLogs(context.Background(), e.Id, opts) reader, err := e.client.ContainerLogs(context.Background(), e.Id, opts)
go func(r io.ReadCloser) { go func(reader io.ReadCloser) {
defer r.Close() defer reader.Close()
s := bufio.NewScanner(r) r := bufio.NewReader(reader)
for s.Scan() { ParentLoop:
e.Events().Publish(environment.ConsoleOutputEvent, s.Text()) for {
var b bytes.Buffer
var line []byte
var isPrefix bool
for {
// Read the line and write it to the buffer.
line, isPrefix, err = r.ReadLine()
// Certain games like Minecraft output absolutely random carriage returns in the output seemingly
// in line with that it thinks is the terminal size. Those returns break a lot of output handling,
// so we'll just replace them with proper new-lines and then split it later and send each line as
// its own event in the response.
b.Write(bytes.ReplaceAll(line, []byte(" \r"), []byte("\r\n")))
// Finish this loop and begin outputting the line if there is no prefix (the line fit into
// the default buffer), or if we hit the end of the line.
if !isPrefix || err == io.EOF {
break
} }
if err := s.Err(); err != nil { // If we encountered an error with something in ReadLine that was not an EOF just abort
// the entire process here.
if err != nil {
break ParentLoop
}
}
// Publish the line for this loop. Break on new-line characters so every line is sent as a single
// output event, otherwise you get funky handling in the browser console.
for _, line := range strings.Split(b.String(), "\r\n") {
e.Events().Publish(environment.ConsoleOutputEvent, line)
}
// If the error we got previously that lead to the line being output is an io.EOF we want to
// exit the entire looping process.
if err == io.EOF {
break
}
}
if err != nil && err != io.EOF {
log.WithField("error", err).WithField("container_id", e.Id).Warn("error processing scanner line in console output") log.WithField("error", err).WithField("container_id", e.Id).Warn("error processing scanner line in console output")
} }
}(reader) }(reader)
return errors.WithStack(err) return errors.WithStackIf(err)
} }
// Pulls the image from Docker. If there is an error while pulling the image from the source // Pulls the image from Docker. If there is an error while pulling the image from the source
@@ -310,12 +349,15 @@ func (e *Environment) followOutput() error {
// need to block all of the servers from booting just because of that. I'd imagine in a lot of // need to block all of the servers from booting just because of that. I'd imagine in a lot of
// cases an outage shouldn't affect users too badly. It'll at least keep existing servers working // cases an outage shouldn't affect users too badly. It'll at least keep existing servers working
// correctly if anything. // correctly if anything.
//
// TODO: local images
func (e *Environment) ensureImageExists(image string) error { func (e *Environment) ensureImageExists(image string) error {
e.Events().Publish(environment.DockerImagePullStarted, "") e.Events().Publish(environment.DockerImagePullStarted, "")
defer e.Events().Publish(environment.DockerImagePullCompleted, "") defer e.Events().Publish(environment.DockerImagePullCompleted, "")
// Images prefixed with a ~ are local images that we do not need to try and pull.
if strings.HasPrefix(image, "~") {
return nil
}
// Give it up to 15 minutes to pull the image. I think this should cover 99.8% of cases where an // Give it up to 15 minutes to pull the image. I think this should cover 99.8% of cases where an
// image pull might fail. I can't imagine it will ever take more than 15 minutes to fully pull // image pull might fail. I can't imagine it will ever take more than 15 minutes to fully pull
// an image. Let me know when I am inevitably wrong here... // an image. Let me know when I am inevitably wrong here...

View File

@@ -2,19 +2,20 @@ package docker
import ( import (
"context" "context"
"emperror.dev/errors"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api" "github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/events" "github.com/pterodactyl/wings/events"
"github.com/pterodactyl/wings/system"
"io" "io"
"sync" "sync"
) )
type Metadata struct { type Metadata struct {
Image string Image string
Stop *api.ProcessStopConfiguration Stop api.ProcessStopConfiguration
} }
// Ensure that the Docker environment is always implementing all of the methods // Ensure that the Docker environment is always implementing all of the methods
@@ -47,15 +48,14 @@ type Environment struct {
emitter *events.EventBus emitter *events.EventBus
// Tracks the environment state. // Tracks the environment state.
st string st *system.AtomicString
stMu sync.RWMutex
} }
// Creates a new base Docker environment. The ID passed through will be the ID that is used to // Creates a new base Docker environment. The ID passed through will be the ID that is used to
// reference the container from here on out. This should be unique per-server (we use the UUID // reference the container from here on out. This should be unique per-server (we use the UUID
// by default). The container does not need to exist at this point. // by default). The container does not need to exist at this point.
func New(id string, m *Metadata, c *environment.Configuration) (*Environment, error) { func New(id string, m *Metadata, c *environment.Configuration) (*Environment, error) {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) cli, err := environment.DockerClient()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -65,6 +65,7 @@ func New(id string, m *Metadata, c *environment.Configuration) (*Environment, er
Configuration: c, Configuration: c,
meta: m, meta: m,
client: cli, client: cli,
st: system.NewAtomicString(environment.ProcessOfflineState),
} }
return e, nil return e, nil
@@ -155,7 +156,7 @@ func (e *Environment) ExitState() (uint32, bool, error) {
return 1, false, nil return 1, false, nil
} }
return 0, false, errors.WithStack(err) return 0, false, errors.WithStackIf(err)
} }
return uint32(c.State.ExitCode), c.State.OOMKilled, nil return uint32(c.State.ExitCode), c.State.OOMKilled, nil
@@ -171,7 +172,7 @@ func (e *Environment) Config() *environment.Configuration {
} }
// Sets the stop configuration for the environment. // Sets the stop configuration for the environment.
func (e *Environment) SetStopConfiguration(c *api.ProcessStopConfiguration) { func (e *Environment) SetStopConfiguration(c api.ProcessStopConfiguration) {
e.mu.Lock() e.mu.Lock()
e.meta.Stop = c e.meta.Stop = c
e.mu.Unlock() e.mu.Unlock()

View File

@@ -2,11 +2,11 @@ package docker
import ( import (
"context" "context"
"emperror.dev/errors"
"github.com/apex/log" "github.com/apex/log"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api" "github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
"os" "os"
@@ -26,7 +26,7 @@ func (e *Environment) OnBeforeStart() error {
// the Panel is usee. // the Panel is usee.
if err := e.client.ContainerRemove(context.Background(), e.Id, types.ContainerRemoveOptions{RemoveVolumes: true}); err != nil { if err := e.client.ContainerRemove(context.Background(), e.Id, types.ContainerRemoveOptions{RemoveVolumes: true}); err != nil {
if !client.IsErrNotFound(err) { if !client.IsErrNotFound(err) {
return errors.Wrap(err, "failed to remove server docker container during pre-boot") return errors.WrapIf(err, "failed to remove server docker container during pre-boot")
} }
} }
@@ -57,8 +57,8 @@ func (e *Environment) Start() error {
// If we don't set it to stopping first, you'll trigger crash detection which // If we don't set it to stopping first, you'll trigger crash detection which
// we don't want to do at this point since it'll just immediately try to do the // we don't want to do at this point since it'll just immediately try to do the
// exact same action that lead to it crashing in the first place... // exact same action that lead to it crashing in the first place...
e.setState(environment.ProcessStoppingState) e.SetState(environment.ProcessStoppingState)
e.setState(environment.ProcessOfflineState) e.SetState(environment.ProcessOfflineState)
} }
}() }()
@@ -69,12 +69,12 @@ func (e *Environment) Start() error {
// //
// @see https://github.com/pterodactyl/panel/issues/2000 // @see https://github.com/pterodactyl/panel/issues/2000
if !client.IsErrNotFound(err) { if !client.IsErrNotFound(err) {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
} else { } else {
// If the server is running update our internal state and continue on with the attach. // If the server is running update our internal state and continue on with the attach.
if c.State.Running { if c.State.Running {
e.setState(environment.ProcessRunningState) e.SetState(environment.ProcessRunningState)
return e.Attach() return e.Attach()
} }
@@ -84,12 +84,12 @@ func (e *Environment) Start() error {
// to truncate them. // to truncate them.
if _, err := os.Stat(c.LogPath); err == nil { if _, err := os.Stat(c.LogPath); err == nil {
if err := os.Truncate(c.LogPath, 0); err != nil { if err := os.Truncate(c.LogPath, 0); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
} }
} }
e.setState(environment.ProcessStartingState) e.SetState(environment.ProcessStartingState)
// Set this to true for now, we will set it to false once we reach the // Set this to true for now, we will set it to false once we reach the
// end of this chain. // end of this chain.
@@ -99,14 +99,14 @@ func (e *Environment) Start() 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(); err != nil { if err := e.OnBeforeStart(); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel() defer cancel()
if err := e.client.ContainerStart(ctx, e.Id, types.ContainerStartOptions{}); err != nil { if err := e.client.ContainerStart(ctx, e.Id, types.ContainerStartOptions{}); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
// No errors, good to continue through. // No errors, good to continue through.
@@ -126,8 +126,8 @@ func (e *Environment) Stop() error {
s := e.meta.Stop s := e.meta.Stop
e.mu.RUnlock() e.mu.RUnlock()
if s == nil || s.Type == api.ProcessStopSignal { if s.Type == "" || s.Type == api.ProcessStopSignal {
if s == nil { if s.Type == "" {
log.WithField("container_id", e.Id).Warn("no stop configuration detected for environment, using termination procedure") log.WithField("container_id", e.Id).Warn("no stop configuration detected for environment, using termination procedure")
} }
@@ -136,8 +136,8 @@ func (e *Environment) Stop() error {
// If the process is already offline don't switch it back to stopping. Just leave it how // If the process is already offline don't switch it back to stopping. Just leave it how
// it is and continue through to the stop handling for the process. // it is and continue through to the stop handling for the process.
if e.State() != environment.ProcessOfflineState { if e.st.Load() != environment.ProcessOfflineState {
e.setState(environment.ProcessStoppingState) e.SetState(environment.ProcessStoppingState)
} }
// Only attempt to send the stop command to the instance if we are actually attached to // Only attempt to send the stop command to the instance if we are actually attached to
@@ -153,7 +153,7 @@ func (e *Environment) Stop() error {
// an error. // an error.
if client.IsErrNotFound(err) { if client.IsErrNotFound(err) {
e.SetStream(nil) e.SetStream(nil)
e.setState(environment.ProcessOfflineState) e.SetState(environment.ProcessOfflineState)
return nil return nil
} }
@@ -169,7 +169,7 @@ func (e *Environment) Stop() error {
// will be terminated forcefully depending on the value of the second argument. // will be terminated forcefully depending on the value of the second argument.
func (e *Environment) WaitForStop(seconds uint, terminate bool) error { func (e *Environment) WaitForStop(seconds uint, terminate bool) error {
if err := e.Stop(); err != nil { if err := e.Stop(); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(seconds)*time.Second) ctx, cancel := context.WithTimeout(context.Background(), time.Duration(seconds)*time.Second)
@@ -185,20 +185,20 @@ func (e *Environment) WaitForStop(seconds uint, terminate bool) error {
if terminate { if terminate {
log.WithField("container_id", e.Id).Debug("server did not stop in time, executing process termination") log.WithField("container_id", e.Id).Debug("server did not stop in time, executing process termination")
return errors.WithStack(e.Terminate(os.Kill)) return errors.WithStackIf(e.Terminate(os.Kill))
} }
return errors.WithStack(ctxErr) return errors.WithStackIf(ctxErr)
} }
case err := <-errChan: case err := <-errChan:
if err != nil { if err != nil {
if terminate { if terminate {
log.WithField("container_id", e.Id).WithField("error", errors.WithStack(err)).Warn("error while waiting for container stop, attempting process termination") log.WithField("container_id", e.Id).WithField("error", errors.WithStackIf(err)).Warn("error while waiting for container stop, attempting process termination")
return errors.WithStack(e.Terminate(os.Kill)) return errors.WithStackIf(e.Terminate(os.Kill))
} }
return errors.WithStack(err) return errors.WithStackIf(err)
} }
case <-ok: case <-ok:
} }
@@ -210,23 +210,23 @@ func (e *Environment) WaitForStop(seconds uint, terminate bool) error {
func (e *Environment) Terminate(signal os.Signal) error { func (e *Environment) Terminate(signal os.Signal) error {
c, err := e.client.ContainerInspect(context.Background(), e.Id) c, err := e.client.ContainerInspect(context.Background(), e.Id)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
if !c.State.Running { if !c.State.Running {
// If the container is not running but we're not already in a stopped state go ahead // If the container is not running but we're not already in a stopped state go ahead
// and update things to indicate we should be completely stopped now. Set to stopping // and update things to indicate we should be completely stopped now. Set to stopping
// first so crash detection is not triggered. // first so crash detection is not triggered.
if e.State() != environment.ProcessOfflineState { if e.st.Load() != environment.ProcessOfflineState {
e.setState(environment.ProcessStoppingState) e.SetState(environment.ProcessStoppingState)
e.setState(environment.ProcessOfflineState) e.SetState(environment.ProcessOfflineState)
} }
return nil return nil
} }
// We set it to stopping than offline to prevent crash detection from being triggered. // We set it to stopping than offline to prevent crash detection from being triggered.
e.setState(environment.ProcessStoppingState) e.SetState(environment.ProcessStoppingState)
sig := strings.TrimSuffix(strings.TrimPrefix(signal.String(), "signal "), "ed") sig := strings.TrimSuffix(strings.TrimPrefix(signal.String(), "signal "), "ed")
@@ -234,7 +234,7 @@ func (e *Environment) Terminate(signal os.Signal) error {
return err return err
} }
e.setState(environment.ProcessOfflineState) e.SetState(environment.ProcessOfflineState)
return nil return nil
} }

View File

@@ -1,41 +1,29 @@
package docker package docker
import ( import (
"emperror.dev/errors"
"fmt" "fmt"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
) )
// Returns the current environment state.
func (e *Environment) State() string { func (e *Environment) State() string {
e.stMu.RLock() return e.st.Load()
defer e.stMu.RUnlock()
return e.st
} }
// Sets the state of the environment. This emits an event that server's can hook into to // Sets the state of the environment. This emits an event that server's can hook into to
// take their own actions and track their own state based on the environment. // take their own actions and track their own state based on the environment.
func (e *Environment) setState(state string) error { func (e *Environment) SetState(state string) {
if state != environment.ProcessOfflineState && if state != environment.ProcessOfflineState &&
state != environment.ProcessStartingState && state != environment.ProcessStartingState &&
state != environment.ProcessRunningState && state != environment.ProcessRunningState &&
state != environment.ProcessStoppingState { state != environment.ProcessStoppingState {
return errors.New(fmt.Sprintf("invalid server state received: %s", state)) panic(errors.New(fmt.Sprintf("invalid server state received: %s", state)))
} }
// Get the current state of the environment before changing it.
prevState := e.State()
// Emit the event to any listeners that are currently registered. // Emit the event to any listeners that are currently registered.
if prevState != state { if e.State() != state {
// If the state changed make sure we update the internal tracking to note that. // If the state changed make sure we update the internal tracking to note that.
e.stMu.Lock() e.st.Store(state)
e.st = state e.Events().Publish(environment.StateChangeEvent, state)
e.stMu.Unlock()
e.Events().Publish(environment.StateChangeEvent, e.State())
} }
return nil
} }

View File

@@ -2,10 +2,10 @@ package docker
import ( import (
"context" "context"
"emperror.dev/errors"
"encoding/json" "encoding/json"
"github.com/apex/log" "github.com/apex/log"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
"io" "io"
"math" "math"
@@ -20,13 +20,13 @@ func (e *Environment) pollResources(ctx context.Context) error {
l.Debug("starting resource polling for container") l.Debug("starting resource polling for container")
defer l.Debug("stopped resource polling for container") defer l.Debug("stopped resource polling for container")
if e.State() == environment.ProcessOfflineState { if e.st.Load() == environment.ProcessOfflineState {
return errors.New("cannot enable resource polling on a stopped server") return errors.New("cannot enable resource polling on a stopped server")
} }
stats, err := e.client.ContainerStats(context.Background(), e.Id, true) stats, err := e.client.ContainerStats(context.Background(), e.Id, true)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
defer stats.Body.Close() defer stats.Body.Close()
@@ -41,7 +41,7 @@ func (e *Environment) pollResources(ctx context.Context) error {
if err := dec.Decode(&v); err != nil { if err := dec.Decode(&v); err != nil {
if err != io.EOF { if err != io.EOF {
l.WithField("error", errors.WithStack(err)).Warn("error while processing Docker stats output for container") l.WithField("error", errors.WithStackIf(err)).Warn("error while processing Docker stats output for container")
} else { } else {
l.Debug("io.EOF encountered during stats decode, stopping polling...") l.Debug("io.EOF encountered during stats decode, stopping polling...")
} }
@@ -50,7 +50,7 @@ func (e *Environment) pollResources(ctx context.Context) error {
} }
// 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.State() == environment.ProcessOfflineState { if e.st.Load() == environment.ProcessOfflineState {
l.Debug("process in offline state while resource polling is still active; stopping poll") l.Debug("process in offline state while resource polling is still active; stopping poll")
return nil return nil
} }
@@ -76,7 +76,7 @@ func (e *Environment) pollResources(ctx context.Context) error {
} }
if b, err := json.Marshal(st); err != nil { if b, err := json.Marshal(st); err != nil {
l.WithField("error", errors.WithStack(err)).Warn("error while marshaling stats object for environment") l.WithField("error", errors.WithStackIf(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))
} }

View File

@@ -4,9 +4,9 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"context" "context"
"emperror.dev/errors"
"encoding/json" "encoding/json"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
"strconv" "strconv"
) )
@@ -15,7 +15,7 @@ type dockerLogLine struct {
Log string `json:"log"` Log string `json:"log"`
} }
var ErrNotAttached = errors.New("not attached to instance") var ErrNotAttached = errors.Sentinel("not attached to instance")
func (e *Environment) setStream(s *types.HijackedResponse) { func (e *Environment) setStream(s *types.HijackedResponse) {
e.mu.Lock() e.mu.Lock()
@@ -33,18 +33,16 @@ func (e *Environment) SendCommand(c string) error {
e.mu.RLock() e.mu.RLock()
defer e.mu.RUnlock() defer e.mu.RUnlock()
if e.meta.Stop != nil {
// If the command being processed is the same as the process stop command then we want to mark // If the command being processed is the same as the process stop command then we want to mark
// the server as entering the stopping state otherwise the process will stop and Wings will think // the server as entering the stopping state otherwise the process will stop and Wings will think
// it has crashed and attempt to restart it. // it has crashed and attempt to restart it.
if e.meta.Stop.Type == "command" && c == e.meta.Stop.Value { if e.meta.Stop.Type == "command" && c == e.meta.Stop.Value {
e.Events().Publish(environment.StateChangeEvent, environment.ProcessStoppingState) e.SetState(environment.ProcessStoppingState)
}
} }
_, err := e.stream.Conn.Write([]byte(c + "\n")) _, err := e.stream.Conn.Write([]byte(c + "\n"))
return errors.WithStack(err) return errors.WithStackIf(err)
} }
// Reads the log file for the server. This does not care if the server is running or not, it will // Reads the log file for the server. This does not care if the server is running or not, it will
@@ -56,7 +54,7 @@ func (e *Environment) Readlog(lines int) ([]string, error) {
Tail: strconv.Itoa(lines), Tail: strconv.Itoa(lines),
}) })
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStackIf(err)
} }
defer r.Close() defer r.Close()

View File

@@ -94,4 +94,12 @@ type ProcessEnvironment interface {
// Reads the log file for the process from the end backwards until the provided // Reads the log file for the process from the end backwards until the provided
// number of lines is met. // number of lines is met.
Readlog(int) ([]string, error) Readlog(int) ([]string, error)
// Returns the current state of the environment.
State() string
// Sets the current state of the environment. In general you should let the environment
// handle this itself, but there are some scenarios where it is helpful for the server
// to update the state externally (e.g. starting -> started).
SetState(string)
} }

View File

@@ -1,9 +1,9 @@
package events package events
import ( import (
"emperror.dev/errors"
"encoding/json" "encoding/json"
"github.com/gammazero/workerpool" "github.com/gammazero/workerpool"
"github.com/pkg/errors"
"strings" "strings"
"sync" "sync"
) )
@@ -69,7 +69,7 @@ func (e *EventBus) Publish(topic string, data string) {
func (e *EventBus) PublishJson(topic string, data interface{}) error { func (e *EventBus) PublishJson(topic string, data interface{}) error {
b, err := json.Marshal(data) b, err := json.Marshal(data)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
e.Publish(topic, string(b)) e.Publish(topic, string(b))

2
go.mod
View File

@@ -3,6 +3,7 @@ module github.com/pterodactyl/wings
go 1.13 go 1.13
require ( require (
emperror.dev/errors v0.8.0
github.com/AlecAivazis/survey/v2 v2.1.0 github.com/AlecAivazis/survey/v2 v2.1.0
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
github.com/Jeffail/gabs/v2 v2.5.1 github.com/Jeffail/gabs/v2 v2.5.1
@@ -57,7 +58,6 @@ require (
github.com/opencontainers/image-spec v1.0.1 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pierrec/lz4 v2.5.2+incompatible // indirect github.com/pierrec/lz4 v2.5.2+incompatible // indirect
github.com/pkg/errors v0.9.1
github.com/pkg/profile v1.5.0 github.com/pkg/profile v1.5.0
github.com/pkg/sftp v1.11.0 github.com/pkg/sftp v1.11.0
github.com/prometheus/common v0.11.1 // indirect github.com/prometheus/common v0.11.1 // indirect

6
go.sum
View File

@@ -1,5 +1,7 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
emperror.dev/errors v0.8.0 h1:4lycVEx0sdJkwDUfQ9pdu6SR0x7rgympt5f4+ok8jDk=
emperror.dev/errors v0.8.0/go.mod h1:YcRvLPh626Ubn2xqtoprejnA5nFha+TJ+2vew48kWuE=
github.com/AlecAivazis/survey/v2 v2.1.0 h1:AT4+23hOFopXYZaNGugbk7MWItkz0SfTmH/Hk92KeeE= github.com/AlecAivazis/survey/v2 v2.1.0 h1:AT4+23hOFopXYZaNGugbk7MWItkz0SfTmH/Hk92KeeE=
github.com/AlecAivazis/survey/v2 v2.1.0/go.mod h1:9FJRdMdDm8rnT+zHVbvQT2RTSTLq0Ttd6q3Vl2fahjk= github.com/AlecAivazis/survey/v2 v2.1.0/go.mod h1:9FJRdMdDm8rnT+zHVbvQT2RTSTLq0Ttd6q3Vl2fahjk=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
@@ -561,9 +563,13 @@ go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=

View File

@@ -1,10 +1,10 @@
package installer package installer
import ( import (
"emperror.dev/errors"
"encoding/json" "encoding/json"
"github.com/asaskevich/govalidator" "github.com/asaskevich/govalidator"
"github.com/buger/jsonparser" "github.com/buger/jsonparser"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api" "github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/server" "github.com/pterodactyl/wings/server"
@@ -43,33 +43,33 @@ func New(data []byte) (*Installer, error) {
// Unmarshal the environment variables from the request into the server struct. // Unmarshal the environment variables from the request into the server struct.
if b, _, _, err := jsonparser.Get(data, "environment"); err != nil { if b, _, _, err := jsonparser.Get(data, "environment"); err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStackIf(err)
} else { } else {
cfg.EnvVars = make(environment.Variables) cfg.EnvVars = make(environment.Variables)
if err := json.Unmarshal(b, &cfg.EnvVars); err != nil { if err := json.Unmarshal(b, &cfg.EnvVars); err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStackIf(err)
} }
} }
// Unmarshal the allocation mappings from the request into the server struct. // Unmarshal the allocation mappings from the request into the server struct.
if b, _, _, err := jsonparser.Get(data, "allocations", "mappings"); err != nil { if b, _, _, err := jsonparser.Get(data, "allocations", "mappings"); err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStackIf(err)
} else { } else {
cfg.Allocations.Mappings = make(map[string][]int) cfg.Allocations.Mappings = make(map[string][]int)
if err := json.Unmarshal(b, &cfg.Allocations.Mappings); err != nil { if err := json.Unmarshal(b, &cfg.Allocations.Mappings); err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStackIf(err)
} }
} }
cfg.Container.Image = getString(data, "container", "image") cfg.Container.Image = getString(data, "container", "image")
c, rerr, err := api.NewRequester().GetServerConfiguration(cfg.Uuid) c, err := api.New().GetServerConfiguration(cfg.Uuid)
if err != nil || rerr != nil {
if err != nil { if err != nil {
return nil, errors.WithStack(err) if !api.IsRequestError(err) {
return nil, errors.WithStackIf(err)
} }
return nil, errors.New(rerr.String()) return nil, errors.New(err.Error())
} }
// Create a new server instance using the configuration we wrote to the disk // Create a new server instance using the configuration we wrote to the disk

View File

@@ -6,7 +6,7 @@ import (
"github.com/apex/log/handlers/cli" "github.com/apex/log/handlers/cli"
color2 "github.com/fatih/color" color2 "github.com/fatih/color"
"github.com/mattn/go-colorable" "github.com/mattn/go-colorable"
"github.com/pkg/errors" "emperror.dev/errors"
"io" "io"
"os" "os"
"sync" "sync"
@@ -88,10 +88,10 @@ func getErrorStack(err error, i bool) errors.StackTrace {
if i { if i {
// Just abort out of this and return a stacktrace leading up to this point. It isn't perfect // Just abort out of this and return a stacktrace leading up to this point. It isn't perfect
// but it'll at least include what function lead to this being called which we can then handle. // but it'll at least include what function lead to this being called which we can then handle.
return errors.Wrap(err, "failed to generate stacktrace for caught error").(tracer).StackTrace() return errors.WrapIf(err, "failed to generate stacktrace for caught error").(tracer).StackTrace()
} }
return getErrorStack(errors.Wrap(err, err.Error()), true) return getErrorStack(errors.WrapIf(err, err.Error()), true)
} }
st := e.StackTrace() st := e.StackTrace()
@@ -101,7 +101,7 @@ func getErrorStack(err error, i bool) errors.StackTrace {
// trace since they'll point to the error that was generated by this function. // trace since they'll point to the error that was generated by this function.
f := 0 f := 0
if i { if i {
f = 4 f = 5
} }
if i && l > 9 { if i && l > 9 {

View File

@@ -2,11 +2,11 @@ package parser
import ( import (
"bytes" "bytes"
"emperror.dev/errors"
"github.com/Jeffail/gabs/v2" "github.com/Jeffail/gabs/v2"
"github.com/apex/log" "github.com/apex/log"
"github.com/buger/jsonparser" "github.com/buger/jsonparser"
"github.com/iancoleman/strcase" "github.com/iancoleman/strcase"
"github.com/pkg/errors"
"io/ioutil" "io/ioutil"
"os" "os"
"regexp" "regexp"
@@ -76,13 +76,13 @@ func (cfr *ConfigurationFileReplacement) getKeyValue(value []byte) interface{} {
func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error) { func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error) {
parsed, err := gabs.ParseJSON(data) parsed, err := gabs.ParseJSON(data)
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStackIf(err)
} }
for _, v := range f.Replace { for _, v := range f.Replace {
value, err := f.LookupConfigurationValue(v) value, err := f.LookupConfigurationValue(v)
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStackIf(err)
} }
// Check for a wildcard character, and if found split the key on that value to // Check for a wildcard character, and if found split the key on that value to
@@ -101,7 +101,7 @@ func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error
continue continue
} }
return nil, errors.Wrap(err, "failed to set config value of array child") return nil, errors.WrapIf(err, "failed to set config value of array child")
} }
} }
} else { } else {
@@ -110,7 +110,7 @@ func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error
continue continue
} }
return nil, errors.Wrap(err, "unable to set config value at pathway: "+v.Match) return nil, errors.WrapIf(err, "unable to set config value at pathway: "+v.Match)
} }
} }
} }
@@ -138,7 +138,7 @@ func setValueAtPath(c *gabs.Container, path string, value interface{}) error {
_, err = c.SetP(value, path) _, err = c.SetP(value, path)
} }
return errors.WithStack(err) return errors.WithStackIf(err)
} }
i, _ := strconv.Atoi(matches[2]) i, _ := strconv.Atoi(matches[2])
@@ -147,7 +147,7 @@ func setValueAtPath(c *gabs.Container, path string, value interface{}) error {
ct, err := c.ArrayElementP(i, matches[1]) ct, err := c.ArrayElementP(i, matches[1])
if err != nil { if err != nil {
if i != 0 || (!errors.Is(err, gabs.ErrNotArray) && !errors.Is(err, gabs.ErrNotFound)) { if i != 0 || (!errors.Is(err, gabs.ErrNotArray) && !errors.Is(err, gabs.ErrNotFound)) {
return errors.Wrap(err, "error while parsing array element at path") return errors.WrapIf(err, "error while parsing array element at path")
} }
var t = make([]interface{}, 1) var t = make([]interface{}, 1)
@@ -162,7 +162,7 @@ func setValueAtPath(c *gabs.Container, path string, value interface{}) error {
// an empty object if we have additional things to set on the array, or just an empty array type // an empty object if we have additional things to set on the array, or just an empty array type
// if there is not an object structure detected (no matches[3] available). // if there is not an object structure detected (no matches[3] available).
if _, err = c.SetP(t, matches[1]); err != nil { if _, err = c.SetP(t, matches[1]); err != nil {
return errors.Wrap(err, "failed to create empty array for missing element") return errors.WrapIf(err, "failed to create empty array for missing element")
} }
// Set our cursor to be the array element we expect, which in this case is just the first element // Set our cursor to be the array element we expect, which in this case is just the first element
@@ -170,7 +170,7 @@ func setValueAtPath(c *gabs.Container, path string, value interface{}) error {
// to match additional elements. In those cases the server will just have to be rebooted or something. // to match additional elements. In those cases the server will just have to be rebooted or something.
ct, err = c.ArrayElementP(0, matches[1]) ct, err = c.ArrayElementP(0, matches[1])
if err != nil { if err != nil {
return errors.Wrap(err, "failed to find array element at path") return errors.WrapIf(err, "failed to find array element at path")
} }
} }
@@ -187,7 +187,7 @@ func setValueAtPath(c *gabs.Container, path string, value interface{}) error {
} }
if err != nil { if err != nil {
return errors.Wrap(err, "failed to set value at config path: "+path) return errors.WrapIf(err, "failed to set value at config path: "+path)
} }
return nil return nil
@@ -253,7 +253,7 @@ func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplac
match, _, _, err := jsonparser.Get(f.configuration, path...) match, _, _, err := jsonparser.Get(f.configuration, path...)
if err != nil { if err != nil {
if err != jsonparser.KeyPathNotFoundError { if err != jsonparser.KeyPathNotFoundError {
return string(match), errors.WithStack(err) return string(match), errors.WithStackIf(err)
} }
log.WithFields(log.Fields{"path": path, "filename": f.FileName}).Debug("attempted to load a configuration value that does not exist") log.WithFields(log.Fields{"path": path, "filename": f.FileName}).Debug("attempted to load a configuration value that does not exist")

View File

@@ -2,20 +2,19 @@ package parser
import ( import (
"bufio" "bufio"
"emperror.dev/errors"
"encoding/json" "encoding/json"
"github.com/apex/log" "github.com/apex/log"
"github.com/beevik/etree" "github.com/beevik/etree"
"github.com/buger/jsonparser" "github.com/buger/jsonparser"
"github.com/icza/dyno" "github.com/icza/dyno"
"github.com/magiconair/properties" "github.com/magiconair/properties"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"gopkg.in/ini.v1" "gopkg.in/ini.v1"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"strconv" "strconv"
"strings" "strings"
) )
@@ -77,11 +76,6 @@ func (f *ConfigurationFile) UnmarshalJSON(data []byte) error {
return nil return nil
} }
// Regex to match paths such as foo[1].bar[2] and convert them into a format that
// gabs can work with, such as foo.1.bar.2 in this case. This is applied when creating
// the struct for the configuration file replacements.
var cfrMatchReplacement = regexp.MustCompile(`\[(\d+)]`)
// Defines a single find/replace instance for a given server configuration file. // Defines a single find/replace instance for a given server configuration file.
type ConfigurationFileReplacement struct { type ConfigurationFileReplacement struct {
Match string `json:"match"` Match string `json:"match"`
@@ -172,17 +166,17 @@ func (f *ConfigurationFile) Parse(path string, internal bool) error {
b := strings.TrimSuffix(path, filepath.Base(path)) b := strings.TrimSuffix(path, filepath.Base(path))
if err := os.MkdirAll(b, 0755); err != nil { if err := os.MkdirAll(b, 0755); err != nil {
return errors.Wrap(err, "failed to create base directory for missing configuration file") return errors.WrapIf(err, "failed to create base directory for missing configuration file")
} else { } else {
if _, err := os.Create(path); err != nil { if _, err := os.Create(path); err != nil {
return errors.Wrap(err, "failed to create missing configuration file") return errors.WrapIf(err, "failed to create missing configuration file")
} }
} }
return f.Parse(path, true) return f.Parse(path, true)
} }
return errors.WithStack(err) return errors.WithStackIf(err)
} }
// Parses an xml file. // Parses an xml file.
@@ -354,12 +348,12 @@ func (f *ConfigurationFile) parseJsonFile(path string) error {
func (f *ConfigurationFile) parseYamlFile(path string) error { func (f *ConfigurationFile) parseYamlFile(path string) error {
b, err := readFileBytes(path) b, err := readFileBytes(path)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
i := make(map[string]interface{}) i := make(map[string]interface{})
if err := yaml.Unmarshal(b, &i); err != nil { if err := yaml.Unmarshal(b, &i); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
// Unmarshal the yaml data into a JSON interface such that we can work with // Unmarshal the yaml data into a JSON interface such that we can work with
@@ -367,20 +361,20 @@ func (f *ConfigurationFile) parseYamlFile(path string) error {
// makes working with unknown JSON significantly easier. // makes working with unknown JSON significantly easier.
jsonBytes, err := json.Marshal(dyno.ConvertMapI2MapS(i)) jsonBytes, err := json.Marshal(dyno.ConvertMapI2MapS(i))
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
// Now that the data is converted, treat it just like JSON and pass it to the // Now that the data is converted, treat it just like JSON and pass it to the
// iterator function to update values as necessary. // iterator function to update values as necessary.
data, err := f.IterateOverJson(jsonBytes) data, err := f.IterateOverJson(jsonBytes)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
// Remarshal the JSON into YAML format before saving it back to the disk. // Remarshal the JSON into YAML format before saving it back to the disk.
marshaled, err := yaml.Marshal(data.Data()) marshaled, err := yaml.Marshal(data.Data())
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
return ioutil.WriteFile(path, marshaled, 0644) return ioutil.WriteFile(path, marshaled, 0644)
@@ -392,7 +386,7 @@ func (f *ConfigurationFile) parseYamlFile(path string) error {
func (f *ConfigurationFile) parseTextFile(path string) error { func (f *ConfigurationFile) parseTextFile(path string) error {
input, err := ioutil.ReadFile(path) input, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
lines := strings.Split(string(input), "\n") lines := strings.Split(string(input), "\n")
@@ -409,7 +403,7 @@ func (f *ConfigurationFile) parseTextFile(path string) error {
} }
if err := ioutil.WriteFile(path, []byte(strings.Join(lines, "\n")), 0644); err != nil { if err := ioutil.WriteFile(path, []byte(strings.Join(lines, "\n")), 0644); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
return nil return nil
@@ -421,7 +415,7 @@ func (f *ConfigurationFile) parsePropertiesFile(path string) error {
// Open the file. // Open the file.
f2, err := os.Open(path) f2, err := os.Open(path)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
var s strings.Builder var s strings.Builder
@@ -443,20 +437,20 @@ func (f *ConfigurationFile) parsePropertiesFile(path string) error {
// Handle any scanner errors. // Handle any scanner errors.
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
// Decode the properties file. // Decode the properties file.
p, err := properties.LoadFile(path, properties.UTF8) p, err := properties.LoadFile(path, properties.UTF8)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
// Replace any values that need to be replaced. // Replace any values that need to be replaced.
for _, replace := range f.Replace { for _, replace := range f.Replace {
data, err := f.LookupConfigurationValue(replace) data, err := f.LookupConfigurationValue(replace)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
v, ok := p.Get(replace.Match) v, ok := p.Get(replace.Match)
@@ -468,7 +462,7 @@ func (f *ConfigurationFile) parsePropertiesFile(path string) error {
} }
if _, _, err := p.Set(replace.Match, data); err != nil { if _, _, err := p.Set(replace.Match, data); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
} }
@@ -488,7 +482,7 @@ func (f *ConfigurationFile) parsePropertiesFile(path string) error {
// Open the file for writing. // Open the file for writing.
w, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) w, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
defer w.Close() defer w.Close()

View File

@@ -1,11 +1,11 @@
package router package router
import ( import (
"emperror.dev/errors"
"fmt" "fmt"
"github.com/apex/log" "github.com/apex/log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/server" "github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/server/filesystem" "github.com/pterodactyl/wings/server/filesystem"
"net/http" "net/http"
@@ -62,7 +62,7 @@ func (e *RequestError) SetMessage(msg string) *RequestError {
func (e *RequestError) AbortWithStatus(status int, c *gin.Context) { func (e *RequestError) AbortWithStatus(status int, c *gin.Context) {
// If this error is because the resource does not exist, we likely do not need to log // If this error is because the resource does not exist, we likely do not need to log
// the error anywhere, just return a 404 and move on with our lives. // the error anywhere, just return a 404 and move on with our lives.
if os.IsNotExist(e.Err) { if errors.Is(e.Err, os.ErrNotExist) {
e.logger().WithField("error", e.Err).Debug("encountered os.IsNotExist error while handling request") e.logger().WithField("error", e.Err).Debug("encountered os.IsNotExist error while handling request")
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
@@ -75,7 +75,7 @@ func (e *RequestError) AbortWithStatus(status int, c *gin.Context) {
if status >= 500 { if status >= 500 {
e.logger().WithField("error", e.Err).Error("encountered HTTP/500 error while handling request") e.logger().WithField("error", e.Err).Error("encountered HTTP/500 error while handling request")
c.Error(errors.WithStack(e)) c.Error(errors.WithStackIf(e))
} else { } else {
e.logger().WithField("error", e.Err).Debug("encountered non-HTTP/500 error while handling request") e.logger().WithField("error", e.Err).Debug("encountered non-HTTP/500 error while handling request")
} }
@@ -99,38 +99,32 @@ func (e *RequestError) AbortWithServerError(c *gin.Context) {
// Handle specific filesystem errors for a server. // Handle specific filesystem errors for a server.
func (e *RequestError) AbortFilesystemError(c *gin.Context) { func (e *RequestError) AbortFilesystemError(c *gin.Context) {
if errors.Is(e.Err, os.ErrNotExist) { if errors.Is(e.Err, os.ErrNotExist) || filesystem.IsBadPathResolutionError(e.Err) {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{ if filesystem.IsBadPathResolutionError(e.Err) {
"error": "The requested resource was not found.", e.logger().Warn(e.Err.Error())
}) }
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "The requested resource was not found."})
return return
} }
if errors.Is(e.Err, filesystem.ErrNotEnoughDiskSpace) { if errors.Is(e.Err, filesystem.ErrNotEnoughDiskSpace) {
c.AbortWithStatusJSON(http.StatusConflict, gin.H{ c.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": "There is not enough disk space available to perform that action."})
"error": "There is not enough disk space available to perform that action.",
})
return return
} }
if strings.HasSuffix(e.Err.Error(), "file name too long") { if strings.HasSuffix(e.Err.Error(), "file name too long") {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "File name is too long."})
"error": "File name is too long.",
})
return return
} }
if e, ok := e.Err.(*os.SyscallError); ok && e.Syscall == "readdirent" { if e, ok := e.Err.(*os.SyscallError); ok && e.Syscall == "readdirent" {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "The requested directory does not exist."})
"error": "The requested directory does not exist.",
})
return return
} }
if strings.HasSuffix(e.Err.Error(), "file name too long") { if strings.HasSuffix(e.Err.Error(), "file name too long") {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Cannot perform that action: file name is too long."})
"error": "Cannot perform that action: file name is too long.",
})
return return
} }

View File

@@ -70,6 +70,7 @@ func Configure() *gin.Engine {
server.POST("/commands", postServerCommands) server.POST("/commands", postServerCommands)
server.POST("/install", postServerInstall) server.POST("/install", postServerInstall)
server.POST("/reinstall", postServerReinstall) server.POST("/reinstall", postServerReinstall)
server.POST("/ws/deny", postServerDenyWSTokens)
// This archive request causes the archive to start being created // This archive request causes the archive to start being created
// this should only be triggered by the panel. // this should only be triggered by the panel.

View File

@@ -3,9 +3,10 @@ package router
import ( import (
"bytes" "bytes"
"context" "context"
"emperror.dev/errors"
"github.com/apex/log" "github.com/apex/log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pkg/errors" "github.com/pterodactyl/wings/router/tokens"
"github.com/pterodactyl/wings/server" "github.com/pterodactyl/wings/server"
"net/http" "net/http"
"os" "os"
@@ -226,7 +227,7 @@ func deleteServer(c *gin.Context) {
if err := os.RemoveAll(p); err != nil { if err := os.RemoveAll(p); err != nil {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"path": p, "path": p,
"error": errors.WithStack(err), "error": errors.WithStackIf(err),
}).Warn("failed to remove server files during deletion process") }).Warn("failed to remove server files during deletion process")
} }
}(s.Filesystem().Path()) }(s.Filesystem().Path())
@@ -241,3 +242,20 @@ func deleteServer(c *gin.Context) {
c.Status(http.StatusNoContent) c.Status(http.StatusNoContent)
} }
// Adds any of the JTIs passed through in the body to the deny list for the websocket
// preventing any JWT generated before the current time from being used to connect to
// the socket or send along commands.
func postServerDenyWSTokens(c *gin.Context) {
var data struct{ JTIs []string `json:"jtis"` }
if err := c.BindJSON(&data); err != nil {
return
}
for _, jti := range data.JTIs {
tokens.DenyJTI(jti)
}
c.Status(http.StatusNoContent)
}

View File

@@ -2,8 +2,8 @@ package router
import ( import (
"context" "context"
"emperror.dev/errors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/router/tokens" "github.com/pterodactyl/wings/router/tokens"
"github.com/pterodactyl/wings/server" "github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/server/filesystem" "github.com/pterodactyl/wings/server/filesystem"
@@ -35,6 +35,10 @@ func getServerFileContents(c *gin.Context) {
return return
} }
if err := s.Filesystem().Readfile(p, c.Writer); err != nil {
TrackedServerError(err, s).AbortFilesystemError(c)
return
} else {
c.Header("X-Mime-Type", st.Mimetype) c.Header("X-Mime-Type", st.Mimetype)
c.Header("Content-Length", strconv.Itoa(int(st.Info.Size()))) c.Header("Content-Length", strconv.Itoa(int(st.Info.Size())))
@@ -44,10 +48,6 @@ func getServerFileContents(c *gin.Context) {
c.Header("Content-Disposition", "attachment; filename="+st.Info.Name()) c.Header("Content-Disposition", "attachment; filename="+st.Info.Name())
c.Header("Content-Type", "application/octet-stream") c.Header("Content-Type", "application/octet-stream")
} }
if err := s.Filesystem().Readfile(p, c.Writer); err != nil {
TrackedServerError(err, s).AbortFilesystemError(c)
return
} }
} }
@@ -397,14 +397,14 @@ func postServerUploadFiles(c *gin.Context) {
for _, header := range headers { for _, header := range headers {
p, err := s.Filesystem().SafePath(filepath.Join(directory, header.Filename)) p, err := s.Filesystem().SafePath(filepath.Join(directory, header.Filename))
if err != nil { if err != nil {
c.AbortWithError(http.StatusInternalServerError, err) TrackedServerError(err, s).AbortFilesystemError(c)
return return
} }
// We run this in a different method so I can use defer without any of // We run this in a different method so I can use defer without any of
// the consequences caused by calling it in a loop. // the consequences caused by calling it in a loop.
if err := handleFileUpload(p, s, header); err != nil { if err := handleFileUpload(p, s, header); err != nil {
c.AbortWithError(http.StatusInternalServerError, err) TrackedServerError(err, s).AbortFilesystemError(c)
return return
} }
} }
@@ -413,12 +413,12 @@ func postServerUploadFiles(c *gin.Context) {
func handleFileUpload(p string, s *server.Server, header *multipart.FileHeader) error { func handleFileUpload(p string, s *server.Server, header *multipart.FileHeader) error {
file, err := header.Open() file, err := header.Open()
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
defer file.Close() defer file.Close()
if err := s.Filesystem().Writefile(p, file); err != nil { if err := s.Filesystem().Writefile(p, file); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
return nil return nil

View File

@@ -4,12 +4,12 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"crypto/sha256" "crypto/sha256"
"emperror.dev/errors"
"encoding/hex" "encoding/hex"
"github.com/apex/log" "github.com/apex/log"
"github.com/buger/jsonparser" "github.com/buger/jsonparser"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/mholt/archiver/v3" "github.com/mholt/archiver/v3"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api" "github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/installer" "github.com/pterodactyl/wings/installer"
@@ -52,7 +52,7 @@ func getServerArchive(c *gin.Context) {
st, err := s.Archiver.Stat() st, err := s.Archiver.Stat()
if err != nil { if err != nil {
if !os.IsNotExist(err) { if !errors.Is(err, os.ErrNotExist) {
TrackedServerError(err, s).SetMessage("failed to stat archive").AbortWithServerError(c) TrackedServerError(err, s).SetMessage("failed to stat archive").AbortWithServerError(c)
return return
} }
@@ -101,15 +101,15 @@ func postServerArchive(c *gin.Context) {
s.Log().Debug("successfully created server archive, notifying panel") s.Log().Debug("successfully created server archive, notifying panel")
r := api.NewRequester() r := api.New()
rerr, err := r.SendArchiveStatus(s.Id(), true) err := r.SendArchiveStatus(s.Id(), true)
if rerr != nil || err != nil {
if err != nil { if err != nil {
if !api.IsRequestError(err) {
s.Log().WithField("error", err).Error("failed to notify panel of archive status") s.Log().WithField("error", err).Error("failed to notify panel of archive status")
return return
} }
s.Log().WithField("error", rerr.String()).Error("panel returned an error when sending the archive status") s.Log().WithField("error", err.Error()).Error("panel returned an error when sending the archive status")
return return
} }
@@ -140,14 +140,14 @@ func postTransfer(c *gin.Context) {
} }
l.Info("server transfer failed, notifying panel") l.Info("server transfer failed, notifying panel")
rerr, err := api.NewRequester().SendTransferFailure(serverID) err := api.New().SendTransferFailure(serverID)
if rerr != nil || err != nil {
if err != nil { if err != nil {
if !api.IsRequestError(err) {
l.WithField("error", err).Error("failed to notify panel with transfer failure") l.WithField("error", err).Error("failed to notify panel with transfer failure")
return return
} }
l.WithField("error", errors.WithStack(rerr)).Error("received error response from panel while notifying of transfer failure") l.WithField("error", err.Error()).Error("received error response from panel while notifying of transfer failure")
return return
} }
@@ -157,7 +157,7 @@ func postTransfer(c *gin.Context) {
// Make a new GET request to the URL the panel gave us. // Make a new GET request to the URL the panel gave us.
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequest("GET", url, nil)
if err != nil { if err != nil {
log.WithField("error", errors.WithStack(err)).Error("failed to create http request for archive transfer") log.WithField("error", errors.WithStackIf(err)).Error("failed to create http request for archive transfer")
return return
} }
@@ -167,7 +167,7 @@ func postTransfer(c *gin.Context) {
// Execute the http request. // Execute the http request.
res, err := client.Do(req) res, err := client.Do(req)
if err != nil { if err != nil {
l.WithField("error", errors.WithStack(err)).Error("failed to send archive http request") l.WithField("error", errors.WithStackIf(err)).Error("failed to send archive http request")
return return
} }
defer res.Body.Close() defer res.Body.Close()
@@ -176,12 +176,12 @@ func postTransfer(c *gin.Context) {
if res.StatusCode != 200 { if res.StatusCode != 200 {
_, err := ioutil.ReadAll(res.Body) _, err := ioutil.ReadAll(res.Body)
if err != nil { if err != nil {
l.WithField("error", errors.WithStack(err)).WithField("status", res.StatusCode).Error("failed read transfer response body") l.WithField("error", errors.WithStackIf(err)).WithField("status", res.StatusCode).Error("failed read transfer response body")
return return
} }
l.WithField("error", errors.WithStack(err)).WithField("status", res.StatusCode).Error("failed to request server archive") l.WithField("error", errors.WithStackIf(err)).WithField("status", res.StatusCode).Error("failed to request server archive")
return return
} }
@@ -193,12 +193,12 @@ func postTransfer(c *gin.Context) {
_, err = os.Stat(archivePath) _, err = os.Stat(archivePath)
if err != nil { if err != nil {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
l.WithField("error", errors.WithStack(err)).Error("failed to stat archive file") l.WithField("error", errors.WithStackIf(err)).Error("failed to stat archive file")
return return
} }
} else { } else {
if err := os.Remove(archivePath); err != nil { if err := os.Remove(archivePath); err != nil {
l.WithField("error", errors.WithStack(err)).Warn("failed to remove old archive file") l.WithField("error", errors.WithStackIf(err)).Warn("failed to remove old archive file")
return return
} }
@@ -207,7 +207,7 @@ func postTransfer(c *gin.Context) {
// Create the file. // Create the file.
file, err := os.Create(archivePath) file, err := os.Create(archivePath)
if err != nil { if err != nil {
l.WithField("error", errors.WithStack(err)).Error("failed to open archive on disk") l.WithField("error", errors.WithStackIf(err)).Error("failed to open archive on disk")
return return
} }
@@ -216,14 +216,14 @@ func postTransfer(c *gin.Context) {
buf := make([]byte, 1024*4) buf := make([]byte, 1024*4)
_, err = io.CopyBuffer(file, res.Body, buf) _, err = io.CopyBuffer(file, res.Body, buf)
if err != nil { if err != nil {
l.WithField("error", errors.WithStack(err)).Error("failed to copy archive file to disk") l.WithField("error", errors.WithStackIf(err)).Error("failed to copy archive file to disk")
return return
} }
// Close the file so it can be opened to verify the checksum. // Close the file so it can be opened to verify the checksum.
if err := file.Close(); err != nil { if err := file.Close(); err != nil {
l.WithField("error", errors.WithStack(err)).Error("failed to close archive file") l.WithField("error", errors.WithStackIf(err)).Error("failed to close archive file")
return return
} }
@@ -233,7 +233,7 @@ func postTransfer(c *gin.Context) {
// Open the archive file for computing a checksum. // Open the archive file for computing a checksum.
file, err = os.Open(archivePath) file, err = os.Open(archivePath)
if err != nil { if err != nil {
l.WithField("error", errors.WithStack(err)).Error("failed to open archive on disk") l.WithField("error", errors.WithStackIf(err)).Error("failed to open archive on disk")
return return
} }
@@ -241,7 +241,7 @@ func postTransfer(c *gin.Context) {
hash := sha256.New() hash := sha256.New()
buf = make([]byte, 1024*4) buf = make([]byte, 1024*4)
if _, err := io.CopyBuffer(hash, file, buf); err != nil { if _, err := io.CopyBuffer(hash, file, buf); err != nil {
l.WithField("error", errors.WithStack(err)).Error("failed to copy archive file for checksum verification") l.WithField("error", errors.WithStackIf(err)).Error("failed to copy archive file for checksum verification")
return return
} }
@@ -253,7 +253,7 @@ func postTransfer(c *gin.Context) {
// Close the file. // Close the file.
if err := file.Close(); err != nil { if err := file.Close(); err != nil {
l.WithField("error", errors.WithStack(err)).Error("failed to close archive file after calculating checksum") l.WithField("error", errors.WithStackIf(err)).Error("failed to close archive file after calculating checksum")
return return
} }
@@ -269,7 +269,7 @@ func postTransfer(c *gin.Context) {
// Create a new server installer (note this does not execute the install script) // Create a new server installer (note this does not execute the install script)
i, err := installer.New(serverData) i, err := installer.New(serverData)
if err != nil { if err != nil {
l.WithField("error", errors.WithStack(err)).Error("failed to validate received server data") l.WithField("error", errors.WithStackIf(err)).Error("failed to validate received server data")
return return
} }
@@ -284,7 +284,7 @@ func postTransfer(c *gin.Context) {
// Un-archive the archive. That sounds weird.. // Un-archive the archive. That sounds weird..
if err := archiver.NewTarGz().Unarchive(archivePath, i.Server().Filesystem().Path()); err != nil { if err := archiver.NewTarGz().Unarchive(archivePath, i.Server().Filesystem().Path()); err != nil {
l.WithField("error", errors.WithStack(err)).Error("failed to extract server archive") l.WithField("error", errors.WithStackIf(err)).Error("failed to extract server archive")
return return
} }
@@ -296,14 +296,14 @@ func postTransfer(c *gin.Context) {
hasError = false hasError = false
// Notify the panel that the transfer succeeded. // Notify the panel that the transfer succeeded.
rerr, err := api.NewRequester().SendTransferSuccess(serverID) err = api.New().SendTransferSuccess(serverID)
if rerr != nil || err != nil {
if err != nil { if err != nil {
l.WithField("error", errors.WithStack(err)).Error("failed to notify panel of transfer success") if !api.IsRequestError(err) {
l.WithField("error", errors.WithStackIf(err)).Error("failed to notify panel of transfer success")
return return
} }
l.WithField("error", errors.WithStack(rerr)).Error("panel responded with error after transfer success") l.WithField("error", err.Error()).Error("panel responded with error after transfer success")
return return
} }

View File

@@ -2,11 +2,40 @@ package tokens
import ( import (
"encoding/json" "encoding/json"
"github.com/apex/log"
"github.com/gbrlsnchs/jwt/v3" "github.com/gbrlsnchs/jwt/v3"
"strings" "strings"
"sync" "sync"
"time"
) )
// The time at which Wings was booted. No JWT's created before this time are allowed to
// connect to the socket since they may have been marked as denied already and therefore
// could be invalid at this point.
//
// By doing this we make it so that a user who gets disconnected from Wings due to a Wings
// reboot just needs to request a new token as if their old token had expired naturally.
var wingsBootTime = time.Now()
// A map that contains any JTI's that have been denied by the Panel and the time at which
// they were marked as denied. Therefore any JWT with the same JTI and an IssuedTime that
// is the same as or before this time should be considered invalid.
//
// This is used to allow the Panel to revoke tokens en-masse for a given user & server
// combination since the JTI for tokens is just MD5(user.id + server.uuid). When a server
// is booted this listing is fetched from the panel and the Websocket is dynamically updated.
var denylist sync.Map
// Adds a JTI to the denylist by marking any JWTs generated before the current time as
// being invalid if they use the same JTI.
func DenyJTI(jti string) {
log.WithField("jti", jti).Debugf("adding \"%s\" to JTI denylist", jti)
denylist.Store(jti, time.Now())
}
// A JWT payload for Websocket connections. This JWT is passed along to the Websocket after
// it has been connected to by sending an "auth" event.
type WebsocketPayload struct { type WebsocketPayload struct {
jwt.Payload jwt.Payload
sync.RWMutex sync.RWMutex
@@ -24,6 +53,7 @@ func (p *WebsocketPayload) GetPayload() *jwt.Payload {
return &p.Payload return &p.Payload
} }
// Returns the UUID of the server associated with this JWT.
func (p *WebsocketPayload) GetServerUuid() string { func (p *WebsocketPayload) GetServerUuid() string {
p.RLock() p.RLock()
defer p.RUnlock() defer p.RUnlock()
@@ -31,6 +61,33 @@ func (p *WebsocketPayload) GetServerUuid() string {
return p.ServerUUID return p.ServerUUID
} }
// Check if the JWT has been marked as denied by the instance due to either being issued
// before Wings was booted, or because we have denied all tokens with the same JTI
// occurring before a set time.
func (p *WebsocketPayload) Denylisted() bool {
// If there is no IssuedAt present for the token, we cannot validate the token so
// just immediately mark it as not valid.
if p.IssuedAt == nil {
return true
}
// If the time that the token was issued is before the time at which Wings was booted
// then the token is invalid for our purposes, even if the token "has permission".
if p.IssuedAt.Time.Before(wingsBootTime) {
return true
}
// Finally, if the token was issued before a time that is currently denied for this
// token instance, ignore the permissions response.
if t, ok := denylist.Load(p.JWTID); ok {
if p.IssuedAt.Time.Before(t.(time.Time)) {
return true
}
}
return false
}
// Checks if the given token payload has a permission string. // Checks if the given token payload has a permission string.
func (p *WebsocketPayload) HasPermission(permission string) bool { func (p *WebsocketPayload) HasPermission(permission string) bool {
p.RLock() p.RLock()
@@ -38,7 +95,7 @@ func (p *WebsocketPayload) HasPermission(permission string) bool {
for _, k := range p.Permissions { for _, k := range p.Permissions {
if k == permission || (!strings.HasPrefix(permission, "admin") && k == "*") { if k == permission || (!strings.HasPrefix(permission, "admin") && k == "*") {
return true return !p.Denylisted()
} }
} }

View File

@@ -2,19 +2,18 @@ package websocket
import ( import (
"context" "context"
"emperror.dev/errors"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/apex/log" "github.com/apex/log"
"github.com/gbrlsnchs/jwt/v3" "github.com/gbrlsnchs/jwt/v3"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/environment/docker" "github.com/pterodactyl/wings/environment/docker"
"github.com/pterodactyl/wings/router/tokens" "github.com/pterodactyl/wings/router/tokens"
"github.com/pterodactyl/wings/server" "github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/server/filesystem"
"net/http" "net/http"
"strings" "strings"
"sync" "sync"
@@ -42,15 +41,17 @@ type Handler struct {
} }
var ( var (
ErrJwtNotPresent = errors.New("jwt: no jwt present") ErrJwtNotPresent = errors.Sentinel("jwt: no jwt present")
ErrJwtNoConnectPerm = errors.New("jwt: missing connect permission") ErrJwtNoConnectPerm = errors.Sentinel("jwt: missing connect permission")
ErrJwtUuidMismatch = errors.New("jwt: server uuid mismatch") ErrJwtUuidMismatch = errors.Sentinel("jwt: server uuid mismatch")
ErrJwtOnDenylist = errors.Sentinel("jwt: created too far in past (denylist)")
) )
func IsJwtError(err error) bool { func IsJwtError(err error) bool {
return errors.Is(err, ErrJwtNotPresent) || return errors.Is(err, ErrJwtNotPresent) ||
errors.Is(err, ErrJwtNoConnectPerm) || errors.Is(err, ErrJwtNoConnectPerm) ||
errors.Is(err, ErrJwtUuidMismatch) || errors.Is(err, ErrJwtUuidMismatch) ||
errors.Is(err, ErrJwtOnDenylist) ||
errors.Is(err, jwt.ErrExpValidation) errors.Is(err, jwt.ErrExpValidation)
} }
@@ -62,8 +63,12 @@ func NewTokenPayload(token []byte) (*tokens.WebsocketPayload, error) {
return nil, err return nil, err
} }
if payload.Denylisted() {
return nil, ErrJwtOnDenylist
}
if !payload.HasPermission(PermissionConnect) { if !payload.HasPermission(PermissionConnect) {
return nil, errors.New("not authorized to connect to this socket") return nil, ErrJwtNoConnectPerm
} }
return &payload, nil return &payload, nil
@@ -103,7 +108,7 @@ func GetHandler(s *server.Server, w http.ResponseWriter, r *http.Request) (*Hand
u, err := uuid.NewRandom() u, err := uuid.NewRandom()
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStackIf(err)
} }
return &Handler{ return &Handler{
@@ -188,6 +193,10 @@ func (h *Handler) TokenValid() error {
return err return err
} }
if j.Denylisted() {
return ErrJwtOnDenylist
}
if !j.HasPermission(PermissionConnect) { if !j.HasPermission(PermissionConnect) {
return ErrJwtNoConnectPerm return ErrJwtNoConnectPerm
} }
@@ -204,26 +213,24 @@ func (h *Handler) TokenValid() error {
// error message, otherwise we just send back a standard error message. // error message, otherwise we just send back a standard error message.
func (h *Handler) SendErrorJson(msg Message, err error, shouldLog ...bool) error { func (h *Handler) SendErrorJson(msg Message, err error, shouldLog ...bool) error {
j := h.GetJwt() j := h.GetJwt()
expected := errors.Is(err, server.ErrSuspended) || isJWTError := IsJwtError(err)
errors.Is(err, server.ErrIsRunning) ||
errors.Is(err, filesystem.ErrNotEnoughDiskSpace)
message := "an unexpected error was encountered while handling this request" wsm := Message{
if expected || (j != nil && j.HasPermission(PermissionReceiveErrors)) { Event: ErrorEvent,
message = err.Error() Args: []string{"an unexpected error was encountered while handling this request"},
}
if isJWTError || (j != nil && j.HasPermission(PermissionReceiveErrors)) {
wsm.Event = JwtErrorEvent
wsm.Args = []string{err.Error()}
} }
m, u := h.GetErrorMessage(message) m, u := h.GetErrorMessage(wsm.Args[0])
wsm := Message{Event: ErrorEvent}
wsm.Args = []string{m} wsm.Args = []string{m}
if len(shouldLog) == 0 || (len(shouldLog) == 1 && shouldLog[0] == true) { if !isJWTError && (len(shouldLog) == 0 || (len(shouldLog) == 1 && shouldLog[0] == true)) {
if !expected && !IsJwtError(err) {
h.server.Log().WithFields(log.Fields{"event": msg.Event, "error_identifier": u.String(), "error": err}). h.server.Log().WithFields(log.Fields{"event": msg.Event, "error_identifier": u.String(), "error": err}).
Error("failed to handle websocket process; an error was encountered processing an event") Error("failed to handle websocket process; an error was encountered processing an event")
} }
}
return h.unsafeSendJson(wsm) return h.unsafeSendJson(wsm)
} }
@@ -361,7 +368,7 @@ func (h *Handler) HandleInbound(m Message) error {
return nil return nil
} }
logs, err := h.server.Environment.Readlog(100) logs, err := h.server.Environment.Readlog(config.Get().System.WebsocketLogCount)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -2,9 +2,9 @@ package server
import ( import (
"crypto/sha256" "crypto/sha256"
"emperror.dev/errors"
"encoding/hex" "encoding/hex"
"github.com/mholt/archiver/v3" "github.com/mholt/archiver/v3"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/server/filesystem" "github.com/pterodactyl/wings/server/filesystem"
"io" "io"
@@ -58,7 +58,7 @@ func (a *Archiver) Archive() error {
var files []string var files []string
fileInfo, err := ioutil.ReadDir(path) fileInfo, err := ioutil.ReadDir(path)
if err != nil { if err != nil {
return err return errors.WithStack(err)
} }
for _, file := range fileInfo { for _, file := range fileInfo {
@@ -72,7 +72,6 @@ func (a *Archiver) Archive() error {
// and not the actual file in this listing. // and not the actual file in this listing.
if file.Mode()&os.ModeSymlink != 0 { if file.Mode()&os.ModeSymlink != 0 {
f, err = a.Server.Filesystem().SafePath(filepath.Join(path, file.Name())) f, err = a.Server.Filesystem().SafePath(filepath.Join(path, file.Name()))
if err != nil { if err != nil {
return err return err
} }
@@ -95,21 +94,17 @@ func (a *Archiver) DeleteIfExists() error {
return nil return nil
} }
return err return errors.WithStackIf(err)
} }
if err := os.Remove(a.Path()); err != nil { return errors.WrapIf(os.Remove(a.Path()), "archiver: failed to delete archive from system")
return errors.WithStack(err)
}
return nil
} }
// Checksum computes a SHA256 checksum of the server's archive. // Checksum computes a SHA256 checksum of the server's archive.
func (a *Archiver) Checksum() (string, error) { func (a *Archiver) Checksum() (string, error) {
file, err := os.Open(a.Path()) file, err := os.Open(a.Path())
if err != nil { if err != nil {
return "", err return "", errors.WithStack(err)
} }
defer file.Close() defer file.Close()
@@ -117,7 +112,7 @@ func (a *Archiver) Checksum() (string, error) {
buf := make([]byte, 1024*4) buf := make([]byte, 1024*4)
if _, err := io.CopyBuffer(hash, file, buf); err != nil { if _, err := io.CopyBuffer(hash, file, buf); err != nil {
return "", err return "", errors.WithStack(err)
} }
return hex.EncodeToString(hash.Sum(nil)), nil return hex.EncodeToString(hash.Sum(nil)), nil

View File

@@ -2,8 +2,8 @@ package server
import ( import (
"bufio" "bufio"
"emperror.dev/errors"
"github.com/apex/log" "github.com/apex/log"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api" "github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/server/backup" "github.com/pterodactyl/wings/server/backup"
"os" "os"
@@ -13,19 +13,18 @@ import (
// Notifies the panel of a backup's state and returns an error if one is encountered // Notifies the panel of a backup's state and returns an error if one is encountered
// while performing this action. // while performing this action.
func (s *Server) notifyPanelOfBackup(uuid string, ad *backup.ArchiveDetails, successful bool) error { func (s *Server) notifyPanelOfBackup(uuid string, ad *backup.ArchiveDetails, successful bool) error {
r := api.NewRequester() err := api.New().SendBackupStatus(uuid, ad.ToRequest(successful))
rerr, err := r.SendBackupStatus(uuid, ad.ToRequest(successful))
if rerr != nil || err != nil {
if err != nil { if err != nil {
if !api.IsRequestError(err) {
s.Log().WithFields(log.Fields{ s.Log().WithFields(log.Fields{
"backup": uuid, "backup": uuid,
"error": err, "error": err,
}).Error("failed to notify panel of backup status due to wings error") }).Error("failed to notify panel of backup status due to wings error")
return err return errors.WithStackIf(err)
} }
return errors.New(rerr.String()) return errors.New(err.Error())
} }
return nil return nil
@@ -38,7 +37,7 @@ func (s *Server) getServerwideIgnoredFiles() ([]string, error) {
f, err := os.Open(path.Join(s.Filesystem().Path(), ".pteroignore")) f, err := os.Open(path.Join(s.Filesystem().Path(), ".pteroignore"))
if err != nil { if err != nil {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
return nil, err return nil, errors.WithStack(err)
} }
} else { } else {
scanner := bufio.NewScanner(f) scanner := bufio.NewScanner(f)
@@ -50,7 +49,7 @@ func (s *Server) getServerwideIgnoredFiles() ([]string, error) {
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
return nil, err return nil, errors.WithStack(err)
} }
} }
@@ -80,7 +79,7 @@ func (s *Server) Backup(b backup.BackupInterface) error {
// Get the included files based on the root path and the ignored files provided. // Get the included files based on the root path and the ignored files provided.
inc, err := s.GetIncludedBackupFiles(b.Ignored()) inc, err := s.GetIncludedBackupFiles(b.Ignored())
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
ad, err := b.Generate(inc, s.Filesystem().Path()) ad, err := b.Generate(inc, s.Filesystem().Path())
@@ -100,7 +99,7 @@ func (s *Server) Backup(b backup.BackupInterface) error {
"file_size": 0, "file_size": 0,
}) })
return errors.Wrap(err, "error while generating server backup") return errors.WrapIf(err, "backup: error while generating server backup")
} }
// Try to notify the panel about the status of this backup. If for some reason this request // Try to notify the panel about the status of this backup. If for some reason this request
@@ -108,7 +107,7 @@ func (s *Server) Backup(b backup.BackupInterface) error {
if notifyError := s.notifyPanelOfBackup(b.Identifier(), ad, true); notifyError != nil { if notifyError := s.notifyPanelOfBackup(b.Identifier(), ad, true); notifyError != nil {
b.Remove() b.Remove()
return notifyError return errors.WithStackIf(err)
} }
// Emit an event over the socket so we can update the backup in realtime on // Emit an event over the socket so we can update the backup in realtime on

View File

@@ -3,9 +3,9 @@ package backup
import ( import (
"archive/tar" "archive/tar"
"context" "context"
"emperror.dev/errors"
"github.com/apex/log" "github.com/apex/log"
gzip "github.com/klauspost/pgzip" gzip "github.com/klauspost/pgzip"
"github.com/pkg/errors"
"github.com/remeh/sizedwaitgroup" "github.com/remeh/sizedwaitgroup"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"io" "io"
@@ -26,7 +26,7 @@ type Archive struct {
func (a *Archive) Create(dst string, ctx context.Context) error { func (a *Archive) Create(dst string, ctx context.Context) error {
f, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) f, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
defer f.Close() defer f.Close()
@@ -58,7 +58,7 @@ func (a *Archive) Create(dst string, ctx context.Context) error {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return errors.WithStack(ctx.Err()) return errors.WithStackIf(ctx.Err())
default: default:
return a.addToArchive(p, tw) return a.addToArchive(p, tw)
} }
@@ -75,7 +75,7 @@ func (a *Archive) Create(dst string, ctx context.Context) error {
log.WithField("location", dst).Warn("failed to delete corrupted backup archive") log.WithField("location", dst).Warn("failed to delete corrupted backup archive")
} }
return errors.WithStack(err) return errors.WithStackIf(err)
} }
return nil return nil
@@ -91,7 +91,7 @@ func (a *Archive) addToArchive(p string, w *tar.Writer) error {
return nil return nil
} }
return errors.WithStack(err) return errors.WithStackIf(err)
} }
defer f.Close() defer f.Close()
@@ -102,7 +102,7 @@ func (a *Archive) addToArchive(p string, w *tar.Writer) error {
return nil return nil
} }
return errors.WithStack(err) return errors.WithStackIf(err)
} }
header := &tar.Header{ header := &tar.Header{
@@ -120,12 +120,12 @@ func (a *Archive) addToArchive(p string, w *tar.Writer) error {
defer a.Unlock() defer a.Unlock()
if err := w.WriteHeader(header); err != nil { if err := w.WriteHeader(header); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
buf := make([]byte, 4*1024) buf := make([]byte, 4*1024)
if _, err := io.CopyBuffer(w, f, buf); err != nil { if _, err := io.CopyBuffer(w, f, buf); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
return nil return nil

View File

@@ -2,9 +2,9 @@ package backup
import ( import (
"crypto/sha1" "crypto/sha1"
"emperror.dev/errors"
"encoding/hex" "encoding/hex"
"github.com/apex/log" "github.com/apex/log"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api" "github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"io" "io"
@@ -87,7 +87,7 @@ func (b *Backup) Path() string {
func (b *Backup) Size() (int64, error) { func (b *Backup) Size() (int64, error) {
st, err := os.Stat(b.Path()) st, err := os.Stat(b.Path())
if err != nil { if err != nil {
return 0, errors.WithStack(err) return 0, errors.WithStackIf(err)
} }
return st.Size(), nil return st.Size(), nil
@@ -99,7 +99,7 @@ func (b *Backup) Checksum() ([]byte, error) {
f, err := os.Open(b.Path()) f, err := os.Open(b.Path())
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStackIf(err)
} }
defer f.Close() defer f.Close()

View File

@@ -2,7 +2,7 @@ package backup
import ( import (
"context" "context"
"github.com/pkg/errors" "emperror.dev/errors"
"os" "os"
) )
@@ -24,7 +24,7 @@ func LocateLocal(uuid string) (*LocalBackup, os.FileInfo, error) {
st, err := os.Stat(b.Path()) st, err := os.Stat(b.Path())
if err != nil { if err != nil {
return nil, nil, errors.WithStack(err) return nil, nil, errors.WithStackIf(err)
} }
if st.IsDir() { if st.IsDir() {
@@ -48,7 +48,7 @@ func (b *LocalBackup) Generate(included *IncludedFiles, prefix string) (*Archive
} }
if err := a.Create(b.Path(), context.Background()); err != nil { if err := a.Create(b.Path(), context.Background()); err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStackIf(err)
} }
return b.Details(), nil return b.Details(), nil

View File

@@ -1,15 +1,14 @@
package backup package backup
import ( import (
"emperror.dev/errors"
"fmt" "fmt"
"github.com/pkg/errors"
) )
type Request struct { type Request struct {
Adapter string `json:"adapter"` Adapter string `json:"adapter"`
Uuid string `json:"uuid"` Uuid string `json:"uuid"`
IgnoredFiles []string `json:"ignored_files"` IgnoredFiles []string `json:"ignored_files"`
PresignedUrl string `json:"presigned_url"`
} }
// Generates a new local backup struct. // Generates a new local backup struct.
@@ -32,15 +31,10 @@ func (r *Request) NewS3Backup() (*S3Backup, error) {
return nil, errors.New(fmt.Sprintf("cannot create s3 backup using [%s] adapter", r.Adapter)) return nil, errors.New(fmt.Sprintf("cannot create s3 backup using [%s] adapter", r.Adapter))
} }
if len(r.PresignedUrl) == 0 {
return nil, errors.New("a valid presigned S3 upload URL must be provided to use the [s3] adapter")
}
return &S3Backup{ return &S3Backup{
Backup: Backup{ Backup: Backup{
Uuid: r.Uuid, Uuid: r.Uuid,
IgnoredFiles: r.IgnoredFiles, IgnoredFiles: r.IgnoredFiles,
}, },
PresignedUrl: r.PresignedUrl,
}, nil }, nil
} }

View File

@@ -1,24 +1,21 @@
package backup package backup
import ( import (
"bytes"
"context" "context"
"emperror.dev/errors"
"fmt" "fmt"
"github.com/apex/log" "github.com/apex/log"
"github.com/pkg/errors" "github.com/pterodactyl/wings/api"
"io" "io"
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
"time"
) )
type S3Backup struct { type S3Backup struct {
Backup Backup
// The pre-signed upload endpoint for the generated backup. This must be
// provided otherwise this request will fail. This allows us to keep all
// of the keys off the daemon instances and the panel can handle generating
// the credentials for us.
PresignedUrl string
} }
var _ BackupInterface = (*S3Backup)(nil) var _ BackupInterface = (*S3Backup)(nil)
@@ -34,23 +31,17 @@ func (s *S3Backup) Generate(included *IncludedFiles, prefix string) (*ArchiveDet
} }
if err := a.Create(s.Path(), context.Background()); err != nil { if err := a.Create(s.Path(), context.Background()); err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStackIf(err)
} }
rc, err := os.Open(s.Path()) rc, err := os.Open(s.Path())
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStackIf(err)
} }
defer rc.Close() defer rc.Close()
if resp, err := s.generateRemoteRequest(rc); err != nil { if err := s.generateRemoteRequest(rc); err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStackIf(err)
} else {
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to put S3 object, %d:%s", resp.StatusCode, resp.Status)
}
} }
return s.Details(), err return s.Details(), err
@@ -61,27 +52,144 @@ func (s *S3Backup) Remove() error {
return os.Remove(s.Path()) return os.Remove(s.Path())
} }
// Reader provides a wrapper around an existing io.Reader
// but implements io.Closer in order to satisfy an io.ReadCloser.
type Reader struct {
io.Reader
}
func (Reader) Close() error {
return nil
}
// Generates the remote S3 request and begins the upload. // Generates the remote S3 request and begins the upload.
func (s *S3Backup) generateRemoteRequest(rc io.ReadCloser) (*http.Response, error) { func (s *S3Backup) generateRemoteRequest(rc io.ReadCloser) error {
r, err := http.NewRequest(http.MethodPut, s.PresignedUrl, nil) defer rc.Close()
size, err := s.Backup.Size()
if err != nil { if err != nil {
return nil, err return err
} }
if sz, err := s.Size(); err != nil { urls, err := api.New().GetBackupRemoteUploadURLs(s.Backup.Uuid, size)
return nil, err if err != nil {
} else { return err
r.ContentLength = sz
r.Header.Add("Content-Length", strconv.Itoa(int(sz)))
r.Header.Add("Content-Type", "application/x-gzip")
} }
r.Body = rc
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"endpoint": s.PresignedUrl, "backup_id": s.Uuid,
"headers": r.Header, "adapter": "s3",
}).Debug("uploading backup to remote S3 endpoint") }).Info("attempting to upload backup..")
return http.DefaultClient.Do(r) handlePart := func(part string, size int64) (string, error) {
r, err := http.NewRequest(http.MethodPut, part, nil)
if err != nil {
return "", err
}
r.ContentLength = size
r.Header.Add("Content-Length", strconv.Itoa(int(size)))
r.Header.Add("Content-Type", "application/x-gzip")
// Limit the reader to the size of the part.
r.Body = Reader{io.LimitReader(rc, size)}
// This http request can block forever due to it not having a timeout,
// but we are uploading up to 5GB of data, so there is not really
// a good way to handle a timeout on this.
res, err := http.DefaultClient.Do(r)
if err != nil {
return "", err
}
defer res.Body.Close()
// Handle non-200 status codes.
if res.StatusCode != http.StatusOK {
return "", fmt.Errorf("failed to put S3 object part, %d:%s", res.StatusCode, res.Status)
}
// Get the ETag from the uploaded part, this should be sent with the CompleteMultipartUpload request.
return res.Header.Get("ETag"), nil
}
// Start assembling the body that will be sent as apart of the CompleteMultipartUpload request.
var completeUploadBody bytes.Buffer
completeUploadBody.WriteString("<CompleteMultipartUpload>\n")
partCount := len(urls.Parts)
for i, part := range urls.Parts {
// Get the size for the current part.
var partSize int64
if i+1 < partCount {
partSize = urls.PartSize
} else {
// This is the remaining size for the last part,
// there is not a minimum size limit for the last part.
partSize = size - (int64(i) * urls.PartSize)
}
// Attempt to upload the part.
etag, err := handlePart(part, partSize)
if err != nil {
log.WithError(err).Warn("failed to upload part")
// Send an AbortMultipartUpload request.
if err := s.finishUpload(urls.AbortMultipartUpload, nil); err != nil {
log.WithError(err).Warn("failed to abort multipart backup upload")
}
return err
}
// Add the part to the CompleteMultipartUpload body.
completeUploadBody.WriteString("\t<Part>\n")
completeUploadBody.WriteString("\t\t<ETag>\"" + etag + "\"</ETag>\n")
completeUploadBody.WriteString("\t\t<PartNumber>" + strconv.Itoa(i+1) + "</PartNumber>\n")
completeUploadBody.WriteString("\t</Part>\n")
}
completeUploadBody.WriteString("</CompleteMultipartUpload>")
// Send a CompleteMultipartUpload request.
if err := s.finishUpload(urls.CompleteMultipartUpload, &completeUploadBody); err != nil {
return err
}
log.WithFields(log.Fields{
"backup_id": s.Uuid,
"adapter": "s3",
}).Info("backup has been successfully uploaded")
return nil
}
// finishUpload sends a requests to the specified url to either complete or abort the upload.
func (s *S3Backup) finishUpload(url string, body io.Reader) error {
r, err := http.NewRequest(http.MethodPost, url, body)
if err != nil {
return err
}
// Create a new http client with a 10 second timeout.
c := &http.Client{
Timeout: 10 * time.Second,
}
res, err := c.Do(r)
if err != nil {
return err
}
defer res.Body.Close()
// Handle non-200 status codes.
if res.StatusCode != http.StatusOK {
// If no body was sent, we were aborting the upload.
if body == nil {
return fmt.Errorf("failed to abort S3 multipart upload, %d:%s", res.StatusCode, res.Status)
}
// If a body was sent we were completing the upload.
// TODO: Attempt to send abort request?
return fmt.Errorf("failed to complete S3 multipart upload, %d:%s", res.StatusCode, res.Status)
}
return nil
} }

View File

@@ -2,9 +2,9 @@ package server
import ( import (
"context" "context"
"emperror.dev/errors"
"fmt" "fmt"
"github.com/mitchellh/colorstring" "github.com/mitchellh/colorstring"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/system" "github.com/pterodactyl/wings/system"
"sync" "sync"
@@ -12,7 +12,7 @@ import (
"time" "time"
) )
var ErrTooMuchConsoleData = errors.New("console is outputting too much data") var ErrTooMuchConsoleData = errors.Sentinel("console is outputting too much data")
type ConsoleThrottler struct { type ConsoleThrottler struct {
mu sync.Mutex mu sync.Mutex

View File

@@ -1,8 +1,8 @@
package server package server
import ( import (
"emperror.dev/errors"
"fmt" "fmt"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
"sync" "sync"
@@ -57,7 +57,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 errors.WithStack(err) return errors.WithStackIf(err)
} }
// 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

View File

@@ -1,9 +1,9 @@
package server package server
import "github.com/pkg/errors" import "emperror.dev/errors"
var ErrIsRunning = errors.New("server is running") var ErrIsRunning = errors.Sentinel("server is running")
var ErrSuspended = errors.New("server is currently in a suspended state") var ErrSuspended = errors.Sentinel("server is currently in a suspended state")
type crashTooFrequent struct { type crashTooFrequent struct {
} }

View File

@@ -1,7 +1,7 @@
package server package server
import ( import (
"github.com/pkg/errors" "emperror.dev/errors"
"github.com/pterodactyl/wings/server/filesystem" "github.com/pterodactyl/wings/server/filesystem"
"os" "os"
) )
@@ -13,12 +13,12 @@ func (s *Server) Filesystem() *filesystem.Filesystem {
// Ensures that the data directory for the server instance exists. // Ensures that the data directory for the server instance exists.
func (s *Server) EnsureDataDirectoryExists() error { func (s *Server) EnsureDataDirectoryExists() error {
if _, err := os.Stat(s.fs.Path()); err != nil && !os.IsNotExist(err) { if _, err := os.Stat(s.fs.Path()); err != nil && !os.IsNotExist(err) {
return errors.WithStack(err) return errors.WithStackIf(err)
} else if err != nil { } else if err != nil {
// Create the server data directory because it does not currently exist // Create the server data directory because it does not currently exist
// on the system. // on the system.
if err := os.MkdirAll(s.fs.Path(), 0700); err != nil { if err := os.MkdirAll(s.fs.Path(), 0700); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
if err := s.fs.Chown("/"); err != nil { if err := s.fs.Chown("/"); err != nil {

View File

@@ -2,9 +2,9 @@ package filesystem
import ( import (
"context" "context"
"emperror.dev/errors"
"fmt" "fmt"
"github.com/karrick/godirwalk" "github.com/karrick/godirwalk"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/server/backup" "github.com/pterodactyl/wings/server/backup"
ignore "github.com/sabhiram/go-gitignore" ignore "github.com/sabhiram/go-gitignore"
"os" "os"
@@ -27,7 +27,7 @@ func (fs *Filesystem) GetIncludedFiles(dir string, ignored []string) (*backup.In
i, err := ignore.CompileIgnoreLines(ignored...) i, err := ignore.CompileIgnoreLines(ignored...)
if err != nil { if err != nil {
return nil, err return nil, errors.WithStack(err)
} }
// Walk through all of the files and directories on a server. This callback only returns // Walk through all of the files and directories on a server. This callback only returns
@@ -41,7 +41,7 @@ func (fs *Filesystem) GetIncludedFiles(dir string, ignored []string) (*backup.In
if e.IsSymlink() { if e.IsSymlink() {
sp, err = fs.SafePath(p) sp, err = fs.SafePath(p)
if err != nil { if err != nil {
if errors.Is(err, ErrBadPathResolution) { if IsBadPathResolutionError(err) {
return godirwalk.SkipThis return godirwalk.SkipThis
} }
@@ -65,7 +65,7 @@ func (fs *Filesystem) GetIncludedFiles(dir string, ignored []string) (*backup.In
}, },
}) })
return inc, errors.WithStack(err) return inc, errors.WithStackIf(err)
} }
// Compresses all of the files matching the given paths in the specified directory. This function // Compresses all of the files matching the given paths in the specified directory. This function
@@ -115,7 +115,7 @@ func (fs *Filesystem) CompressFiles(dir string, paths []string) (os.FileInfo, er
// use the resolved location for the rest of this function. // use the resolved location for the rest of this function.
sp, err = fs.SafePath(p) sp, err = fs.SafePath(p)
if err != nil { if err != nil {
if errors.Is(err, ErrBadPathResolution) { if IsBadPathResolutionError(err) {
return godirwalk.SkipThis return godirwalk.SkipThis
} }
@@ -141,7 +141,7 @@ func (fs *Filesystem) CompressFiles(dir string, paths []string) (os.FileInfo, er
d := path.Join(cleanedRootDir, fmt.Sprintf("archive-%s.tar.gz", strings.ReplaceAll(time.Now().Format(time.RFC3339), ":", ""))) d := path.Join(cleanedRootDir, fmt.Sprintf("archive-%s.tar.gz", strings.ReplaceAll(time.Now().Format(time.RFC3339), ":", "")))
if err := a.Create(d, context.Background()); err != nil { if err := a.Create(d, context.Background()); err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStackIf(err)
} }
f, err := os.Stat(d) f, err := os.Stat(d)

View File

@@ -4,9 +4,9 @@ import (
"archive/tar" "archive/tar"
"archive/zip" "archive/zip"
"compress/gzip" "compress/gzip"
"emperror.dev/errors"
"fmt" "fmt"
"github.com/mholt/archiver/v3" "github.com/mholt/archiver/v3"
"github.com/pkg/errors"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
@@ -47,10 +47,10 @@ func (fs *Filesystem) SpaceAvailableForDecompression(dir string, file string) (b
return false, ErrUnknownArchiveFormat return false, ErrUnknownArchiveFormat
} }
return false, errors.WithStack(err) return false, errors.WithStackIf(err)
} }
return true, errors.WithStack(err) return true, errors.WithStackIf(err)
} }
// Decompress a file in a given directory by using the archiver tool to infer the file // Decompress a file in a given directory by using the archiver tool to infer the file
@@ -60,12 +60,12 @@ func (fs *Filesystem) SpaceAvailableForDecompression(dir string, file string) (b
func (fs *Filesystem) DecompressFile(dir string, file string) error { func (fs *Filesystem) DecompressFile(dir string, file string) error {
source, err := fs.SafePath(filepath.Join(dir, file)) source, err := fs.SafePath(filepath.Join(dir, file))
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
// Make sure the file exists basically. // Make sure the file exists basically.
if _, err := os.Stat(source); err != nil { if _, err := os.Stat(source); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
// Walk over all of the files spinning up an additional go-routine for each file we've encountered // Walk over all of the files spinning up an additional go-routine for each file we've encountered
@@ -93,17 +93,17 @@ func (fs *Filesystem) DecompressFile(dir string, file string) error {
p, err := fs.SafePath(filepath.Join(dir, name)) p, err := fs.SafePath(filepath.Join(dir, name))
if err != nil { if err != nil {
return errors.Wrap(err, "failed to generate a safe path to server file") return errors.WrapIf(err, "failed to generate a safe path to server file")
} }
return errors.Wrap(fs.Writefile(p, f), "could not extract file from archive") return errors.WrapIf(fs.Writefile(p, f), "could not extract file from archive")
}) })
if err != nil { if err != nil {
if strings.HasPrefix(err.Error(), "format ") { if strings.HasPrefix(err.Error(), "format ") {
return errors.WithStack(ErrUnknownArchiveFormat) return errors.WithStackIf(ErrUnknownArchiveFormat)
} }
return errors.WithStack(err) return errors.WithStackIf(err)
} }
return nil return nil

View File

@@ -1,9 +1,9 @@
package filesystem package filesystem
import ( import (
"emperror.dev/errors"
"github.com/apex/log" "github.com/apex/log"
"github.com/karrick/godirwalk" "github.com/karrick/godirwalk"
"github.com/pkg/errors"
"sync" "sync"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
@@ -153,7 +153,7 @@ func (fs *Filesystem) updateCachedDiskUsage() (int64, error) {
func (fs *Filesystem) DirectorySize(dir string) (int64, error) { func (fs *Filesystem) DirectorySize(dir string) (int64, error) {
d, err := fs.SafePath(dir) d, err := fs.SafePath(dir)
if err != nil { if err != nil {
return 0, errors.WithStack(err) return 0, errors.WithStackIf(err)
} }
var size int64 var size int64
@@ -167,7 +167,7 @@ func (fs *Filesystem) DirectorySize(dir string) (int64, error) {
// it. Otherwise, allow it to continue. // it. Otherwise, allow it to continue.
if e.IsSymlink() { if e.IsSymlink() {
if _, err := fs.SafePath(p); err != nil { if _, err := fs.SafePath(p); err != nil {
if errors.Is(err, ErrBadPathResolution) { if IsBadPathResolutionError(err) {
return godirwalk.SkipThis return godirwalk.SkipThis
} }
@@ -184,7 +184,7 @@ func (fs *Filesystem) DirectorySize(dir string) (int64, error) {
}, },
}) })
return size, errors.WithStack(err) return size, errors.WithStackIf(err)
} }
// Helper function to determine if a server has space available for a file of a given size. // Helper function to determine if a server has space available for a file of a given size.
@@ -197,7 +197,7 @@ func (fs *Filesystem) hasSpaceFor(size int64) error {
s, err := fs.DiskUsage(true) s, err := fs.DiskUsage(true)
if err != nil { if err != nil {
return err return errors.WithStackIf(err)
} }
if (s + size) > fs.MaxDisk() { if (s + size) > fs.MaxDisk() {
@@ -209,7 +209,8 @@ func (fs *Filesystem) hasSpaceFor(size int64) error {
// Updates the disk usage for the Filesystem instance. // Updates the disk usage for the Filesystem instance.
func (fs *Filesystem) addDisk(i int64) int64 { func (fs *Filesystem) addDisk(i int64) int64 {
var size = atomic.LoadInt64(&fs.diskUsed) size := atomic.LoadInt64(&fs.diskUsed)
// Sorry go gods. This is ugly but the best approach I can come up with for right // Sorry go gods. This is ugly but the best approach I can come up with for right
// now without completely re-evaluating the logic we use for determining disk space. // now without completely re-evaluating the logic we use for determining disk space.
// //

View File

@@ -1,16 +1,49 @@
package filesystem package filesystem
import ( import (
"emperror.dev/errors"
"fmt"
"github.com/apex/log" "github.com/apex/log"
"github.com/pkg/errors"
"os" "os"
"path/filepath" "path/filepath"
) )
var ErrIsDirectory = errors.New("filesystem: is a directory") var ErrIsDirectory = errors.Sentinel("filesystem: is a directory")
var ErrNotEnoughDiskSpace = errors.New("filesystem: not enough disk space") var ErrNotEnoughDiskSpace = errors.Sentinel("filesystem: not enough disk space")
var ErrBadPathResolution = errors.New("filesystem: invalid path resolution") var ErrUnknownArchiveFormat = errors.Sentinel("filesystem: unknown archive format")
var ErrUnknownArchiveFormat = errors.New("filesystem: unknown archive format")
type BadPathResolutionError struct {
path string
resolved string
}
// Returns the specific error for a bad path resolution.
func (b *BadPathResolutionError) Error() string {
r := b.resolved
if r == "" {
r = "<empty>"
}
return fmt.Sprintf("filesystem: server path [%s] resolves to a location outside the server root: %s", b.path, r)
}
// Returns a new BadPathResolution error.
func NewBadPathResolution(path string, resolved string) *BadPathResolutionError {
return &BadPathResolutionError{path, resolved}
}
// Determines if the given error is a bad path resolution error.
func IsBadPathResolutionError(err error) bool {
e := errors.Unwrap(err)
if e == nil {
e = err
}
if _, ok := e.(*BadPathResolutionError); ok {
return true
}
return false
}
// Generates an error logger instance with some basic information. // Generates an error logger instance with some basic information.
func (fs *Filesystem) error(err error) *log.Entry { func (fs *Filesystem) error(err error) *log.Entry {
@@ -23,8 +56,8 @@ func (fs *Filesystem) error(err error) *log.Entry {
// directory, otherwise return nil. Returning this error for a file will stop the walking // directory, otherwise return nil. Returning this error for a file will stop the walking
// for the remainder of the directory. This is assuming an os.FileInfo struct was even returned. // for the remainder of the directory. This is assuming an os.FileInfo struct was even returned.
func (fs *Filesystem) handleWalkerError(err error, f os.FileInfo) error { func (fs *Filesystem) handleWalkerError(err error, f os.FileInfo) error {
if !errors.Is(err, ErrBadPathResolution) { if !IsBadPathResolutionError(err) {
return err return errors.WithStackIf(err)
} }
if f != nil && f.IsDir() { if f != nil && f.IsDir() {

View File

@@ -0,0 +1,24 @@
package filesystem
import (
. "github.com/franela/goblin"
"testing"
)
func TestFilesystem_PathResolutionError(t *testing.T) {
g := Goblin(t)
g.Describe("NewBadPathResolutionError", func() {
g.It("is can detect itself as an error correctly", func() {
err := NewBadPathResolution("foo", "bar")
g.Assert(IsBadPathResolutionError(err)).IsTrue()
g.Assert(err.Error()).Equal("filesystem: server path [foo] resolves to a location outside the server root: bar")
g.Assert(IsBadPathResolutionError(ErrIsDirectory)).IsFalse()
})
g.It("returns <empty> if no destination path is provided", func() {
err := NewBadPathResolution("foo", "")
g.Assert(err.Error()).Equal("filesystem: server path [foo] resolves to a location outside the server root: <empty>")
})
})
}

View File

@@ -2,9 +2,9 @@ package filesystem
import ( import (
"bufio" "bufio"
"emperror.dev/errors"
"github.com/gabriel-vasile/mimetype" "github.com/gabriel-vasile/mimetype"
"github.com/karrick/godirwalk" "github.com/karrick/godirwalk"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/system" "github.com/pterodactyl/wings/system"
"io" "io"
@@ -60,27 +60,27 @@ func (fs *Filesystem) Readfile(p string, w io.Writer) error {
} }
if st, err := os.Stat(cleaned); err != nil { if st, err := os.Stat(cleaned); err != nil {
return err return errors.WithStack(err)
} else if st.IsDir() { } else if st.IsDir() {
return ErrIsDirectory return errors.WithStack(ErrIsDirectory)
} }
f, err := os.Open(cleaned) f, err := os.Open(cleaned)
if err != nil { if err != nil {
return err return errors.WithStack(err)
} }
defer f.Close() defer f.Close()
_, err = bufio.NewReader(f).WriteTo(w) _, err = bufio.NewReader(f).WriteTo(w)
return err return errors.WithStack(err)
} }
// Writes a file to the system. If the file does not already exist one will be created. // Writes a file to the system. If the file does not already exist one will be created.
func (fs *Filesystem) Writefile(p string, r io.Reader) error { func (fs *Filesystem) Writefile(p string, r io.Reader) error {
cleaned, err := fs.SafePath(p) cleaned, err := fs.SafePath(p)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
var currentSize int64 var currentSize int64
@@ -88,19 +88,19 @@ func (fs *Filesystem) Writefile(p string, r io.Reader) error {
// to it and an empty file. We'll then write to it later on after this completes. // to it and an empty file. We'll then write to it later on after this completes.
if stat, err := os.Stat(cleaned); err != nil { if stat, err := os.Stat(cleaned); err != nil {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
if err := os.MkdirAll(filepath.Dir(cleaned), 0755); err != nil { if err := os.MkdirAll(filepath.Dir(cleaned), 0755); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
if err := fs.Chown(filepath.Dir(cleaned)); err != nil { if err := fs.Chown(filepath.Dir(cleaned)); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
} else { } else {
if stat.IsDir() { if stat.IsDir() {
return ErrIsDirectory return errors.WithStack(ErrIsDirectory)
} }
currentSize = stat.Size() currentSize = stat.Size()
@@ -119,7 +119,7 @@ func (fs *Filesystem) Writefile(p string, r io.Reader) error {
// truncate the existing file. // truncate the existing file.
file, err := o.open(cleaned, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) file, err := o.open(cleaned, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
defer file.Close() defer file.Close()
@@ -138,7 +138,7 @@ func (fs *Filesystem) Writefile(p string, r io.Reader) error {
func (fs *Filesystem) CreateDirectory(name string, p string) error { func (fs *Filesystem) CreateDirectory(name string, p string) error {
cleaned, err := fs.SafePath(path.Join(p, name)) cleaned, err := fs.SafePath(path.Join(p, name))
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
return os.MkdirAll(cleaned, 0755) return os.MkdirAll(cleaned, 0755)
@@ -148,12 +148,12 @@ func (fs *Filesystem) CreateDirectory(name string, p string) error {
func (fs *Filesystem) Rename(from string, to string) error { func (fs *Filesystem) Rename(from string, to string) error {
cleanedFrom, err := fs.SafePath(from) cleanedFrom, err := fs.SafePath(from)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
cleanedTo, err := fs.SafePath(to) cleanedTo, err := fs.SafePath(to)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
// If the target file or directory already exists the rename function will fail, so just // If the target file or directory already exists the rename function will fail, so just
@@ -171,7 +171,7 @@ func (fs *Filesystem) Rename(from string, to string) error {
// we're not at the root directory level. // we're not at the root directory level.
if d != fs.Path() { if d != fs.Path() {
if mkerr := os.MkdirAll(d, 0755); mkerr != nil { if mkerr := os.MkdirAll(d, 0755); mkerr != nil {
return errors.Wrap(mkerr, "failed to create directory structure for file rename") return errors.WrapIf(mkerr, "failed to create directory structure for file rename")
} }
} }
@@ -185,7 +185,7 @@ func (fs *Filesystem) Rename(from string, to string) error {
func (fs *Filesystem) Chown(path string) error { func (fs *Filesystem) Chown(path string) error {
cleaned, err := fs.SafePath(path) cleaned, err := fs.SafePath(path)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
if fs.isTest { if fs.isTest {
@@ -197,7 +197,7 @@ func (fs *Filesystem) Chown(path string) error {
// 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(cleaned, uid, gid); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
// 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
@@ -248,8 +248,8 @@ func (fs *Filesystem) findCopySuffix(dir string, name string, extension string)
// If we stat the file and it does not exist that means we're good to create the copy. If it // If we stat the file and it does not exist that means we're good to create the copy. If it
// does exist, we'll just continue to the next loop and try again. // does exist, we'll just continue to the next loop and try again.
if _, err := fs.Stat(path.Join(dir, n)); err != nil { if _, err := fs.Stat(path.Join(dir, n)); err != nil {
if !os.IsNotExist(err) { if !errors.Is(err, os.ErrNotExist) {
return "", err return "", errors.WithStackIf(err)
} }
break break
@@ -268,12 +268,12 @@ func (fs *Filesystem) findCopySuffix(dir string, name string, extension string)
func (fs *Filesystem) Copy(p string) error { func (fs *Filesystem) Copy(p string) error {
cleaned, err := fs.SafePath(p) cleaned, err := fs.SafePath(p)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
s, err := os.Stat(cleaned) s, err := os.Stat(cleaned)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} else if s.IsDir() || !s.Mode().IsRegular() { } else if s.IsDir() || !s.Mode().IsRegular() {
// If this is a directory or not a regular file, just throw a not-exist error // If this is a directory or not a regular file, just throw a not-exist error
// since anything calling this function should understand what that means. // since anything calling this function should understand what that means.
@@ -300,11 +300,14 @@ func (fs *Filesystem) Copy(p string) error {
source, err := os.Open(cleaned) source, err := os.Open(cleaned)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
defer source.Close() defer source.Close()
n, err := fs.findCopySuffix(relative, name, extension) n, err := fs.findCopySuffix(relative, name, extension)
if err != nil {
return err
}
return fs.Writefile(path.Join(relative, n), source) return fs.Writefile(path.Join(relative, n), source)
} }
@@ -324,7 +327,7 @@ func (fs *Filesystem) Delete(p string) error {
// exists within the data directory. // exists within the data directory.
resolved := fs.unsafeFilePath(p) resolved := fs.unsafeFilePath(p)
if !fs.unsafeIsInDataDirectory(resolved) { if !fs.unsafeIsInDataDirectory(resolved) {
return ErrBadPathResolution return NewBadPathResolution(p, resolved)
} }
// Block any whoopsies. // Block any whoopsies.

View File

@@ -9,6 +9,7 @@ import (
"math/rand" "math/rand"
"os" "os"
"path/filepath" "path/filepath"
"sync/atomic"
"testing" "testing"
"unicode/utf8" "unicode/utf8"
) )
@@ -69,224 +70,6 @@ func (rfs *rootFs) reset() {
} }
} }
func TestFilesystem_Path(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()
g.Describe("Path", func() {
g.It("returns the root path for the instance", func() {
g.Assert(fs.Path()).Equal(filepath.Join(rfs.root, "/server"))
})
})
}
func TestFilesystem_SafePath(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()
prefix := filepath.Join(rfs.root, "/server")
g.Describe("SafePath", func() {
g.It("returns a cleaned path to a given file", func() {
p, err := fs.SafePath("test.txt")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/test.txt")
p, err = fs.SafePath("/test.txt")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/test.txt")
p, err = fs.SafePath("./test.txt")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/test.txt")
p, err = fs.SafePath("/foo/../test.txt")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/test.txt")
p, err = fs.SafePath("/foo/bar")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/foo/bar")
})
g.It("handles root directory access", func() {
p, err := fs.SafePath("/")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix)
p, err = fs.SafePath("")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix)
})
g.It("removes trailing slashes from paths", func() {
p, err := fs.SafePath("/foo/bar/")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/foo/bar")
})
g.It("handles deeply nested directories that do not exist", func() {
p, err := fs.SafePath("/foo/bar/baz/quaz/../../ducks/testing.txt")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/foo/bar/ducks/testing.txt")
})
g.It("blocks access to files outside the root directory", func() {
p, err := fs.SafePath("../test.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
g.Assert(p).Equal("")
p, err = fs.SafePath("/../test.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
g.Assert(p).Equal("")
p, err = fs.SafePath("./foo/../../test.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
g.Assert(p).Equal("")
p, err = fs.SafePath("..")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
g.Assert(p).Equal("")
})
})
}
// We test against accessing files outside the root directory in the tests, however it
// is still possible for someone to mess up and not properly use this safe path call. In
// order to truly confirm this, we'll try to pass in a symlinked malicious file to all of
// the calls and ensure they all fail with the same reason.
func TestFilesystem_Blocks_Symlinks(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()
if err := rfs.CreateServerFile("/../malicious.txt", "external content"); err != nil {
panic(err)
}
if err := os.Mkdir(filepath.Join(rfs.root, "/malicious_dir"), 0777); err != nil {
panic(err)
}
if err := os.Symlink(filepath.Join(rfs.root, "malicious.txt"), filepath.Join(rfs.root, "/server/symlinked.txt")); err != nil {
panic(err)
}
if err := os.Symlink(filepath.Join(rfs.root, "/malicious_dir"), filepath.Join(rfs.root, "/server/external_dir")); err != nil {
panic(err)
}
g.Describe("Readfile", func() {
g.It("cannot read a file symlinked outside the root", func() {
b := bytes.Buffer{}
err := fs.Readfile("symlinked.txt", &b)
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
})
g.Describe("Writefile", func() {
g.It("cannot write to a file symlinked outside the root", func() {
r := bytes.NewReader([]byte("testing"))
err := fs.Writefile("symlinked.txt", r)
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
g.It("cannot write a file to a directory symlinked outside the root", func() {
r := bytes.NewReader([]byte("testing"))
err := fs.Writefile("external_dir/foo.txt", r)
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
})
g.Describe("CreateDirectory", func() {
g.It("cannot create a directory outside the root", func() {
err := fs.CreateDirectory("my_dir", "external_dir")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
g.It("cannot create a nested directory outside the root", func() {
err := fs.CreateDirectory("my/nested/dir", "external_dir/foo/bar")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
g.It("cannot create a nested directory outside the root", func() {
err := fs.CreateDirectory("my/nested/dir", "external_dir/server")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
})
g.Describe("Rename", func() {
g.It("cannot rename a file symlinked outside the directory root", func() {
err := fs.Rename("symlinked.txt", "foo.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
g.It("cannot rename a symlinked directory outside the root", func() {
err := fs.Rename("external_dir", "foo")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
g.It("cannot rename a file to a location outside the directory root", func() {
rfs.CreateServerFile("my_file.txt", "internal content")
err := fs.Rename("my_file.txt", "external_dir/my_file.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
})
g.Describe("Chown", func() {
g.It("cannot chown a file symlinked outside the directory root", func() {
err := fs.Chown("symlinked.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
g.It("cannot chown a directory symlinked outside the directory root", func() {
err := fs.Chown("external_dir")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
})
g.Describe("Copy", func() {
g.It("cannot copy a file symlinked outside the directory root", func() {
err := fs.Copy("symlinked.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
})
g.Describe("Delete", func() {
g.It("deletes the symlinked file but leaves the source", func() {
err := fs.Delete("symlinked.txt")
g.Assert(err).IsNil()
_, err = os.Stat(filepath.Join(rfs.root, "malicious.txt"))
g.Assert(err).IsNil()
_, err = rfs.StatServerFile("symlinked.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrNotExist)).IsTrue()
})
})
rfs.reset()
}
func TestFilesystem_Readfile(t *testing.T) { func TestFilesystem_Readfile(t *testing.T) {
g := Goblin(t) g := Goblin(t)
fs, rfs := NewFs() fs, rfs := NewFs()
@@ -324,12 +107,12 @@ func TestFilesystem_Readfile(t *testing.T) {
err = fs.Readfile("/../test.txt", buf) err = fs.Readfile("/../test.txt", buf)
g.Assert(err).IsNotNil() g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue() g.Assert(IsBadPathResolutionError(err)).IsTrue()
}) })
g.AfterEach(func() { g.AfterEach(func() {
buf.Truncate(0) buf.Truncate(0)
fs.diskUsed = 0 atomic.StoreInt64(&fs.diskUsed, 0)
rfs.reset() rfs.reset()
}) })
}) })
@@ -347,7 +130,7 @@ func TestFilesystem_Writefile(t *testing.T) {
g.It("can create a new file", func() { g.It("can create a new file", func() {
r := bytes.NewReader([]byte("test file content")) r := bytes.NewReader([]byte("test file content"))
g.Assert(fs.diskUsed).Equal(int64(0)) g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(0))
err := fs.Writefile("test.txt", r) err := fs.Writefile("test.txt", r)
g.Assert(err).IsNil() g.Assert(err).IsNil()
@@ -355,7 +138,7 @@ func TestFilesystem_Writefile(t *testing.T) {
err = fs.Readfile("test.txt", buf) err = fs.Readfile("test.txt", buf)
g.Assert(err).IsNil() g.Assert(err).IsNil()
g.Assert(buf.String()).Equal("test file content") g.Assert(buf.String()).Equal("test file content")
g.Assert(fs.diskUsed).Equal(r.Size()) g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(r.Size())
}) })
g.It("can create a new file inside a nested directory with leading slash", func() { g.It("can create a new file inside a nested directory with leading slash", func() {
@@ -385,11 +168,11 @@ func TestFilesystem_Writefile(t *testing.T) {
err := fs.Writefile("/some/../foo/../../test.txt", r) err := fs.Writefile("/some/../foo/../../test.txt", r)
g.Assert(err).IsNotNil() g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue() g.Assert(IsBadPathResolutionError(err)).IsTrue()
}) })
g.It("cannot write a file that exceedes the disk limits", func() { g.It("cannot write a file that exceeds the disk limits", func() {
fs.diskLimit = 1024 atomic.StoreInt64(&fs.diskLimit, 1024)
b := make([]byte, 1025) b := make([]byte, 1025)
_, err := rand.Read(b) _, err := rand.Read(b)
@@ -402,8 +185,8 @@ func TestFilesystem_Writefile(t *testing.T) {
g.Assert(errors.Is(err, ErrNotEnoughDiskSpace)).IsTrue() g.Assert(errors.Is(err, ErrNotEnoughDiskSpace)).IsTrue()
}) })
g.It("updates the total space used when a file is appended to", func() { /*g.It("updates the total space used when a file is appended to", func() {
fs.diskUsed = 100 atomic.StoreInt64(&fs.diskUsed, 100)
b := make([]byte, 100) b := make([]byte, 100)
_, _ = rand.Read(b) _, _ = rand.Read(b)
@@ -411,7 +194,7 @@ func TestFilesystem_Writefile(t *testing.T) {
r := bytes.NewReader(b) r := bytes.NewReader(b)
err := fs.Writefile("test.txt", r) err := fs.Writefile("test.txt", r)
g.Assert(err).IsNil() g.Assert(err).IsNil()
g.Assert(fs.diskUsed).Equal(int64(200)) g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(200))
// If we write less data than already exists, we should expect the total // If we write less data than already exists, we should expect the total
// disk used to be decremented. // disk used to be decremented.
@@ -421,8 +204,8 @@ func TestFilesystem_Writefile(t *testing.T) {
r = bytes.NewReader(b) r = bytes.NewReader(b)
err = fs.Writefile("test.txt", r) err = fs.Writefile("test.txt", r)
g.Assert(err).IsNil() g.Assert(err).IsNil()
g.Assert(fs.diskUsed).Equal(int64(150)) g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(150))
}) })*/
g.It("truncates the file when writing new contents", func() { g.It("truncates the file when writing new contents", func() {
r := bytes.NewReader([]byte("original data")) r := bytes.NewReader([]byte("original data"))
@@ -441,8 +224,9 @@ func TestFilesystem_Writefile(t *testing.T) {
g.AfterEach(func() { g.AfterEach(func() {
buf.Truncate(0) buf.Truncate(0)
rfs.reset() rfs.reset()
fs.diskUsed = 0
fs.diskLimit = 0 atomic.StoreInt64(&fs.diskUsed, 0)
atomic.StoreInt64(&fs.diskLimit, 0)
}) })
}) })
} }
@@ -475,13 +259,13 @@ func TestFilesystem_CreateDirectory(t *testing.T) {
g.It("should not allow the creation of directories outside the root", func() { g.It("should not allow the creation of directories outside the root", func() {
err := fs.CreateDirectory("test", "e/../../something") err := fs.CreateDirectory("test", "e/../../something")
g.Assert(err).IsNotNil() g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue() g.Assert(IsBadPathResolutionError(err)).IsTrue()
}) })
g.It("should not increment the disk usage", func() { g.It("should not increment the disk usage", func() {
err := fs.CreateDirectory("test", "/") err := fs.CreateDirectory("test", "/")
g.Assert(err).IsNil() g.Assert(err).IsNil()
g.Assert(fs.diskUsed).Equal(int64(0)) g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(0))
}) })
g.AfterEach(func() { g.AfterEach(func() {
@@ -525,7 +309,7 @@ func TestFilesystem_Rename(t *testing.T) {
g.It("does not allow renaming to a location outside the root", func() { g.It("does not allow renaming to a location outside the root", func() {
err := fs.Rename("source.txt", "../target.txt") err := fs.Rename("source.txt", "../target.txt")
g.Assert(err).IsNotNil() g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue() g.Assert(IsBadPathResolutionError(err)).IsTrue()
}) })
g.It("does not allow renaming from a location outside the root", func() { g.It("does not allow renaming from a location outside the root", func() {
@@ -533,7 +317,7 @@ func TestFilesystem_Rename(t *testing.T) {
err = fs.Rename("/../ext-source.txt", "target.txt") err = fs.Rename("/../ext-source.txt", "target.txt")
g.Assert(err).IsNotNil() g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue() g.Assert(IsBadPathResolutionError(err)).IsTrue()
}) })
g.It("allows a file to be renamed", func() { g.It("allows a file to be renamed", func() {
@@ -597,7 +381,7 @@ func TestFilesystem_Copy(t *testing.T) {
panic(err) panic(err)
} }
fs.diskUsed = int64(utf8.RuneCountInString("test content")) atomic.StoreInt64(&fs.diskUsed, int64(utf8.RuneCountInString("test content")))
}) })
g.It("should return an error if the source does not exist", func() { g.It("should return an error if the source does not exist", func() {
@@ -611,7 +395,7 @@ func TestFilesystem_Copy(t *testing.T) {
err = fs.Copy("../ext-source.txt") err = fs.Copy("../ext-source.txt")
g.Assert(err).IsNotNil() g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue() g.Assert(IsBadPathResolutionError(err)).IsTrue()
}) })
g.It("should return an error if the source directory is outside the root", func() { g.It("should return an error if the source directory is outside the root", func() {
@@ -623,11 +407,11 @@ func TestFilesystem_Copy(t *testing.T) {
err = fs.Copy("../nested/in/dir/ext-source.txt") err = fs.Copy("../nested/in/dir/ext-source.txt")
g.Assert(err).IsNotNil() g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue() g.Assert(IsBadPathResolutionError(err)).IsTrue()
err = fs.Copy("nested/in/../../../nested/in/dir/ext-source.txt") err = fs.Copy("nested/in/../../../nested/in/dir/ext-source.txt")
g.Assert(err).IsNotNil() g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue() g.Assert(IsBadPathResolutionError(err)).IsTrue()
}) })
g.It("should return an error if the source is a directory", func() { g.It("should return an error if the source is a directory", func() {
@@ -640,7 +424,7 @@ func TestFilesystem_Copy(t *testing.T) {
}) })
g.It("should return an error if there is not space to copy the file", func() { g.It("should return an error if there is not space to copy the file", func() {
fs.diskLimit = 2 atomic.StoreInt64(&fs.diskLimit, 2)
err := fs.Copy("source.txt") err := fs.Copy("source.txt")
g.Assert(err).IsNotNil() g.Assert(err).IsNotNil()
@@ -672,7 +456,7 @@ func TestFilesystem_Copy(t *testing.T) {
g.Assert(err).IsNil() g.Assert(err).IsNil()
} }
g.Assert(fs.diskUsed).Equal(int64(utf8.RuneCountInString("test content")) * 3) g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(utf8.RuneCountInString("test content")) * 3)
}) })
g.It("should create a copy inside of a directory", func() { g.It("should create a copy inside of a directory", func() {
@@ -694,8 +478,9 @@ func TestFilesystem_Copy(t *testing.T) {
g.AfterEach(func() { g.AfterEach(func() {
rfs.reset() rfs.reset()
fs.diskUsed = 0
fs.diskLimit = 0 atomic.StoreInt64(&fs.diskUsed, 0)
atomic.StoreInt64(&fs.diskLimit, 0)
}) })
}) })
} }
@@ -710,7 +495,7 @@ func TestFilesystem_Delete(t *testing.T) {
panic(err) panic(err)
} }
fs.diskUsed = int64(utf8.RuneCountInString("test content")) atomic.StoreInt64(&fs.diskUsed, int64(utf8.RuneCountInString("test content")))
}) })
g.It("does not delete files outside the root directory", func() { g.It("does not delete files outside the root directory", func() {
@@ -718,7 +503,7 @@ func TestFilesystem_Delete(t *testing.T) {
err = fs.Delete("../ext-source.txt") err = fs.Delete("../ext-source.txt")
g.Assert(err).IsNotNil() g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue() g.Assert(IsBadPathResolutionError(err)).IsTrue()
}) })
g.It("does not allow the deletion of the root directory", func() { g.It("does not allow the deletion of the root directory", func() {
@@ -744,7 +529,7 @@ func TestFilesystem_Delete(t *testing.T) {
g.Assert(err).IsNotNil() g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrNotExist)).IsTrue() g.Assert(errors.Is(err, os.ErrNotExist)).IsTrue()
g.Assert(fs.diskUsed).Equal(int64(0)) g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(0))
}) })
g.It("deletes all items inside a directory if the directory is deleted", func() { g.It("deletes all items inside a directory if the directory is deleted", func() {
@@ -762,11 +547,11 @@ func TestFilesystem_Delete(t *testing.T) {
g.Assert(err).IsNil() g.Assert(err).IsNil()
} }
fs.diskUsed = int64(utf8.RuneCountInString("test content") * 3) atomic.StoreInt64(&fs.diskUsed, int64(utf8.RuneCountInString("test content")*3))
err = fs.Delete("foo") err = fs.Delete("foo")
g.Assert(err).IsNil() g.Assert(err).IsNil()
g.Assert(fs.diskUsed).Equal(int64(0)) g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(0))
for _, s := range sources { for _, s := range sources {
_, err = rfs.StatServerFile(s) _, err = rfs.StatServerFile(s)
@@ -777,8 +562,9 @@ func TestFilesystem_Delete(t *testing.T) {
g.AfterEach(func() { g.AfterEach(func() {
rfs.reset() rfs.reset()
fs.diskUsed = 0
fs.diskLimit = 0 atomic.StoreInt64(&fs.diskUsed, 0)
atomic.StoreInt64(&fs.diskLimit, 0)
}) })
}) })
} }

View File

@@ -2,6 +2,7 @@ package filesystem
import ( import (
"context" "context"
"emperror.dev/errors"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"os" "os"
"path/filepath" "path/filepath"
@@ -23,9 +24,9 @@ func (fs *Filesystem) SafePath(p string) (string, error) {
// At the same time, evaluate the symlink status and determine where this file or folder // At the same time, evaluate the symlink status and determine where this file or folder
// is truly pointing to. // is truly pointing to.
p, err := filepath.EvalSymlinks(r) ep, err := filepath.EvalSymlinks(r)
if err != nil && !os.IsNotExist(err) { if err != nil && !os.IsNotExist(err) {
return "", err return "", errors.WithStackIf(err)
} 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 requested directory doesn't exist, so at this point we need to iterate up the
// path chain until we hit a directory that _does_ exist and can be validated. // path chain until we hit a directory that _does_ exist and can be validated.
@@ -53,7 +54,7 @@ func (fs *Filesystem) SafePath(p string) (string, error) {
// attempt going on, and we should NOT resolve this path for them. // attempt going on, and we should NOT resolve this path for them.
if nonExistentPathResolution != "" { if nonExistentPathResolution != "" {
if !fs.unsafeIsInDataDirectory(nonExistentPathResolution) { if !fs.unsafeIsInDataDirectory(nonExistentPathResolution) {
return "", ErrBadPathResolution return "", NewBadPathResolution(p, nonExistentPathResolution)
} }
// If the nonExistentPathResolution variable is not empty then the initial path requested // If the nonExistentPathResolution variable is not empty then the initial path requested
@@ -66,11 +67,11 @@ func (fs *Filesystem) SafePath(p string) (string, error) {
// 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(p) { if fs.unsafeIsInDataDirectory(ep) {
return p, nil return ep, nil
} }
return "", ErrBadPathResolution return "", NewBadPathResolution(p, r)
} }
// Generate a path to the file by cleaning it up and appending the root server path to it. This // Generate a path to the file by cleaning it up and appending the root server path to it. This
@@ -137,5 +138,5 @@ func (fs *Filesystem) ParallelSafePath(paths []string) ([]string, error) {
} }
// Block until all of the routines finish and have returned a value. // Block until all of the routines finish and have returned a value.
return cleaned, g.Wait() return cleaned, errors.WithStackIf(g.Wait())
} }

View File

@@ -0,0 +1,228 @@
package filesystem
import (
"bytes"
"emperror.dev/errors"
. "github.com/franela/goblin"
"os"
"path/filepath"
"testing"
)
func TestFilesystem_Path(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()
g.Describe("Path", func() {
g.It("returns the root path for the instance", func() {
g.Assert(fs.Path()).Equal(filepath.Join(rfs.root, "/server"))
})
})
}
func TestFilesystem_SafePath(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()
prefix := filepath.Join(rfs.root, "/server")
g.Describe("SafePath", func() {
g.It("returns a cleaned path to a given file", func() {
p, err := fs.SafePath("test.txt")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/test.txt")
p, err = fs.SafePath("/test.txt")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/test.txt")
p, err = fs.SafePath("./test.txt")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/test.txt")
p, err = fs.SafePath("/foo/../test.txt")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/test.txt")
p, err = fs.SafePath("/foo/bar")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/foo/bar")
})
g.It("handles root directory access", func() {
p, err := fs.SafePath("/")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix)
p, err = fs.SafePath("")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix)
})
g.It("removes trailing slashes from paths", func() {
p, err := fs.SafePath("/foo/bar/")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/foo/bar")
})
g.It("handles deeply nested directories that do not exist", func() {
p, err := fs.SafePath("/foo/bar/baz/quaz/../../ducks/testing.txt")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/foo/bar/ducks/testing.txt")
})
g.It("blocks access to files outside the root directory", func() {
p, err := fs.SafePath("../test.txt")
g.Assert(err).IsNotNil()
g.Assert(IsBadPathResolutionError(err)).IsTrue()
g.Assert(p).Equal("")
p, err = fs.SafePath("/../test.txt")
g.Assert(err).IsNotNil()
g.Assert(IsBadPathResolutionError(err)).IsTrue()
g.Assert(p).Equal("")
p, err = fs.SafePath("./foo/../../test.txt")
g.Assert(err).IsNotNil()
g.Assert(IsBadPathResolutionError(err)).IsTrue()
g.Assert(p).Equal("")
p, err = fs.SafePath("..")
g.Assert(err).IsNotNil()
g.Assert(IsBadPathResolutionError(err)).IsTrue()
g.Assert(p).Equal("")
})
})
}
// We test against accessing files outside the root directory in the tests, however it
// is still possible for someone to mess up and not properly use this safe path call. In
// order to truly confirm this, we'll try to pass in a symlinked malicious file to all of
// the calls and ensure they all fail with the same reason.
func TestFilesystem_Blocks_Symlinks(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()
if err := rfs.CreateServerFile("/../malicious.txt", "external content"); err != nil {
panic(err)
}
if err := os.Mkdir(filepath.Join(rfs.root, "/malicious_dir"), 0777); err != nil {
panic(err)
}
if err := os.Symlink(filepath.Join(rfs.root, "malicious.txt"), filepath.Join(rfs.root, "/server/symlinked.txt")); err != nil {
panic(err)
}
if err := os.Symlink(filepath.Join(rfs.root, "/malicious_dir"), filepath.Join(rfs.root, "/server/external_dir")); err != nil {
panic(err)
}
g.Describe("Readfile", func() {
g.It("cannot read a file symlinked outside the root", func() {
b := bytes.Buffer{}
err := fs.Readfile("symlinked.txt", &b)
g.Assert(err).IsNotNil()
g.Assert(IsBadPathResolutionError(err)).IsTrue()
})
})
g.Describe("Writefile", func() {
g.It("cannot write to a file symlinked outside the root", func() {
r := bytes.NewReader([]byte("testing"))
err := fs.Writefile("symlinked.txt", r)
g.Assert(err).IsNotNil()
g.Assert(IsBadPathResolutionError(err)).IsTrue()
})
g.It("cannot write a file to a directory symlinked outside the root", func() {
r := bytes.NewReader([]byte("testing"))
err := fs.Writefile("external_dir/foo.txt", r)
g.Assert(err).IsNotNil()
g.Assert(IsBadPathResolutionError(err)).IsTrue()
})
})
g.Describe("CreateDirectory", func() {
g.It("cannot create a directory outside the root", func() {
err := fs.CreateDirectory("my_dir", "external_dir")
g.Assert(err).IsNotNil()
g.Assert(IsBadPathResolutionError(err)).IsTrue()
})
g.It("cannot create a nested directory outside the root", func() {
err := fs.CreateDirectory("my/nested/dir", "external_dir/foo/bar")
g.Assert(err).IsNotNil()
g.Assert(IsBadPathResolutionError(err)).IsTrue()
})
g.It("cannot create a nested directory outside the root", func() {
err := fs.CreateDirectory("my/nested/dir", "external_dir/server")
g.Assert(err).IsNotNil()
g.Assert(IsBadPathResolutionError(err)).IsTrue()
})
})
g.Describe("Rename", func() {
g.It("cannot rename a file symlinked outside the directory root", func() {
err := fs.Rename("symlinked.txt", "foo.txt")
g.Assert(err).IsNotNil()
g.Assert(IsBadPathResolutionError(err)).IsTrue()
})
g.It("cannot rename a symlinked directory outside the root", func() {
err := fs.Rename("external_dir", "foo")
g.Assert(err).IsNotNil()
g.Assert(IsBadPathResolutionError(err)).IsTrue()
})
g.It("cannot rename a file to a location outside the directory root", func() {
rfs.CreateServerFile("my_file.txt", "internal content")
err := fs.Rename("my_file.txt", "external_dir/my_file.txt")
g.Assert(err).IsNotNil()
g.Assert(IsBadPathResolutionError(err)).IsTrue()
})
})
g.Describe("Chown", func() {
g.It("cannot chown a file symlinked outside the directory root", func() {
err := fs.Chown("symlinked.txt")
g.Assert(err).IsNotNil()
g.Assert(IsBadPathResolutionError(err)).IsTrue()
})
g.It("cannot chown a directory symlinked outside the directory root", func() {
err := fs.Chown("external_dir")
g.Assert(err).IsNotNil()
g.Assert(IsBadPathResolutionError(err)).IsTrue()
})
})
g.Describe("Copy", func() {
g.It("cannot copy a file symlinked outside the directory root", func() {
err := fs.Copy("symlinked.txt")
g.Assert(err).IsNotNil()
g.Assert(IsBadPathResolutionError(err)).IsTrue()
})
})
g.Describe("Delete", func() {
g.It("deletes the symlinked file but leaves the source", func() {
err := fs.Delete("symlinked.txt")
g.Assert(err).IsNil()
_, err = os.Stat(filepath.Join(rfs.root, "malicious.txt"))
g.Assert(err).IsNil()
_, err = rfs.StatServerFile("symlinked.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrNotExist)).IsTrue()
})
})
rfs.reset()
}

View File

@@ -1,6 +1,7 @@
package filesystem package filesystem
import ( import (
"emperror.dev/errors"
"encoding/json" "encoding/json"
"github.com/gabriel-vasile/mimetype" "github.com/gabriel-vasile/mimetype"
"os" "os"
@@ -50,14 +51,14 @@ func (fs *Filesystem) Stat(p string) (*Stat, error) {
func (fs *Filesystem) unsafeStat(p string) (*Stat, error) { func (fs *Filesystem) unsafeStat(p string) (*Stat, error) {
s, err := os.Stat(p) s, err := os.Stat(p)
if err != nil { if err != nil {
return nil, err return nil, errors.WithStackIf(err)
} }
var m *mimetype.MIME var m *mimetype.MIME
if !s.IsDir() { if !s.IsDir() {
m, err = mimetype.DetectFile(p) m, err = mimetype.DetectFile(p)
if err != nil { if err != nil {
return nil, err return nil, errors.WithStackIf(err)
} }
} }

View File

@@ -9,5 +9,6 @@ import (
func (s *Stat) CTime() time.Time { func (s *Stat) CTime() time.Time {
st := s.Info.Sys().(*syscall.Stat_t) st := s.Info.Sys().(*syscall.Stat_t)
return time.Unix(st.Ctim.Sec, st.Ctim.Nsec) // Do not remove these "redundant" type-casts, they are required for 32-bit builds to work.
return time.Unix(int64(st.Ctim.Sec), int64(st.Ctim.Nsec))
} }

View File

@@ -4,12 +4,12 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"context" "context"
"emperror.dev/errors"
"github.com/apex/log" "github.com/apex/log"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/mount"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api" "github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
@@ -63,7 +63,7 @@ func (s *Server) Install(sync bool) error {
// Ensure that the server is marked as offline at this point, otherwise you end up // Ensure that the server is marked as offline at this point, otherwise you end up
// with a blank value which is a bit confusing. // with a blank value which is a bit confusing.
s.SetState(environment.ProcessOfflineState) s.Environment.SetState(environment.ProcessOfflineState)
// Push an event to the websocket so we can auto-refresh the information in the panel once // Push an event to the websocket so we can auto-refresh the information in the panel once
// the install is completed. // the install is completed.
@@ -87,18 +87,18 @@ func (s *Server) Reinstall() error {
// Internal installation function used to simplify reporting back to the Panel. // Internal installation function used to simplify reporting back to the Panel.
func (s *Server) internalInstall() error { func (s *Server) internalInstall() error {
script, rerr, err := api.NewRequester().GetInstallationScript(s.Id()) script, err := api.New().GetInstallationScript(s.Id())
if err != nil || rerr != nil {
if err != nil { if err != nil {
return err if !api.IsRequestError(err) {
return errors.WithStackIf(err)
} }
return errors.New(rerr.String()) return errors.New(err.Error())
} }
p, err := NewInstallationProcess(s, &script) p, err := NewInstallationProcess(s, &script)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
s.Log().Info("beginning installation process for server") s.Log().Info("beginning installation process for server")
@@ -129,8 +129,8 @@ func NewInstallationProcess(s *Server, script *api.InstallationScript) (*Install
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
s.installer.cancel = &cancel s.installer.cancel = &cancel
if c, err := client.NewClientWithOpts(client.FromEnv); err != nil { if c, err := environment.DockerClient(); err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStackIf(err)
} else { } else {
proc.client = c proc.client = c
proc.context = ctx proc.context = ctx
@@ -193,7 +193,7 @@ func (ip *InstallationProcess) RemoveContainer() {
}) })
if err != nil && !client.IsErrNotFound(err) { if err != nil && !client.IsErrNotFound(err) {
ip.Server.Log().WithField("error", errors.WithStack(err)).Warn("failed to delete server install container") ip.Server.Log().WithField("error", errors.WithStackIf(err)).Warn("failed to delete server install container")
} }
} }
@@ -218,14 +218,14 @@ func (ip *InstallationProcess) Run() error {
}() }()
if err := ip.BeforeExecute(); err != nil { if err := ip.BeforeExecute(); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
cid, err := ip.Execute() cid, err := ip.Execute()
if err != nil { if err != nil {
ip.RemoveContainer() ip.RemoveContainer()
return errors.WithStack(err) return errors.WithStackIf(err)
} }
// If this step fails, log a warning but don't exit out of the process. This is completely // If this step fails, log a warning but don't exit out of the process. This is completely
@@ -248,12 +248,12 @@ func (ip *InstallationProcess) writeScriptToDisk() error {
// Make sure the temp directory root exists before trying to make a directory within it. The // Make sure the temp directory root exists before trying to make a directory within it. The
// ioutil.TempDir call expects this base to exist, it won't create it for you. // ioutil.TempDir call expects this base to exist, it won't create it for you.
if err := os.MkdirAll(ip.tempDir(), 0700); err != nil { if err := os.MkdirAll(ip.tempDir(), 0700); err != nil {
return errors.Wrap(err, "could not create temporary directory for install process") return errors.WrapIf(err, "could not create temporary directory for install process")
} }
f, err := os.OpenFile(filepath.Join(ip.tempDir(), "install.sh"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) f, err := os.OpenFile(filepath.Join(ip.tempDir(), "install.sh"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to write server installation script to disk before mount") return errors.WrapIf(err, "failed to write server installation script to disk before mount")
} }
defer f.Close() defer f.Close()
@@ -265,7 +265,7 @@ func (ip *InstallationProcess) writeScriptToDisk() error {
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
w.Flush() w.Flush()
@@ -277,7 +277,7 @@ func (ip *InstallationProcess) writeScriptToDisk() error {
func (ip *InstallationProcess) pullInstallationImage() error { func (ip *InstallationProcess) pullInstallationImage() error {
r, err := ip.client.ImagePull(ip.context, ip.Script.ContainerImage, types.ImagePullOptions{}) r, err := ip.client.ImagePull(ip.context, ip.Script.ContainerImage, types.ImagePullOptions{})
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
// Block continuation until the image has been pulled successfully. // Block continuation until the image has been pulled successfully.
@@ -287,7 +287,7 @@ func (ip *InstallationProcess) pullInstallationImage() error {
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
return nil return nil
@@ -298,11 +298,11 @@ func (ip *InstallationProcess) pullInstallationImage() error {
// manner, if either one fails the error is returned. // manner, if either one fails the error is returned.
func (ip *InstallationProcess) BeforeExecute() error { func (ip *InstallationProcess) BeforeExecute() error {
if err := ip.writeScriptToDisk(); err != nil { if err := ip.writeScriptToDisk(); err != nil {
return errors.Wrap(err, "failed to write installation script to disk") return errors.WrapIf(err, "failed to write installation script to disk")
} }
if err := ip.pullInstallationImage(); err != nil { if err := ip.pullInstallationImage(); err != nil {
return errors.Wrap(err, "failed to pull updated installation container image for server") return errors.WrapIf(err, "failed to pull updated installation container image for server")
} }
opts := types.ContainerRemoveOptions{ opts := types.ContainerRemoveOptions{
@@ -312,7 +312,7 @@ func (ip *InstallationProcess) BeforeExecute() error {
if err := ip.client.ContainerRemove(ip.context, ip.Server.Id()+"_installer", opts); err != nil { if err := ip.client.ContainerRemove(ip.context, ip.Server.Id()+"_installer", opts); err != nil {
if !client.IsErrNotFound(err) { if !client.IsErrNotFound(err) {
return errors.Wrap(err, "failed to remove existing install container for server") return errors.WrapIf(err, "failed to remove existing install container for server")
} }
} }
@@ -338,12 +338,12 @@ func (ip *InstallationProcess) AfterExecute(containerId string) error {
}) })
if err != nil && !client.IsErrNotFound(err) { if err != nil && !client.IsErrNotFound(err) {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
f, err := os.OpenFile(ip.GetLogPath(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) f, err := os.OpenFile(ip.GetLogPath(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
defer f.Close() defer f.Close()
@@ -372,15 +372,15 @@ func (ip *InstallationProcess) AfterExecute(containerId string) error {
| ------------------------------ | ------------------------------
`) `)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
if err := tmpl.Execute(f, ip); err != nil { if err := tmpl.Execute(f, ip); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
if _, err := io.Copy(f, reader); err != nil { if _, err := io.Copy(f, reader); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
return nil return nil
@@ -448,7 +448,7 @@ func (ip *InstallationProcess) Execute() (string, error) {
r, err := ip.client.ContainerCreate(ip.context, conf, hostConf, nil, ip.Server.Id()+"_installer") r, err := ip.client.ContainerCreate(ip.context, conf, hostConf, nil, ip.Server.Id()+"_installer")
if err != nil { if err != nil {
return "", errors.WithStack(err) return "", errors.WithStackIf(err)
} }
ip.Server.Log().WithField("container_id", r.ID).Info("running installation script for server in container") ip.Server.Log().WithField("container_id", r.ID).Info("running installation script for server in container")
@@ -468,7 +468,7 @@ func (ip *InstallationProcess) Execute() (string, error) {
select { select {
case err := <-eChan: case err := <-eChan:
if err != nil { if err != nil {
return "", errors.WithStack(err) return "", errors.WithStackIf(err)
} }
case <-sChan: case <-sChan:
} }
@@ -487,7 +487,7 @@ func (ip *InstallationProcess) StreamOutput(id string) error {
}) })
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
defer reader.Close() defer reader.Close()
@@ -500,7 +500,7 @@ func (ip *InstallationProcess) StreamOutput(id string) error {
if err := s.Err(); err != nil { if err := s.Err(); err != nil {
ip.Server.Log().WithFields(log.Fields{ ip.Server.Log().WithFields(log.Fields{
"container_id": id, "container_id": id,
"error": errors.WithStack(err), "error": errors.WithStackIf(err),
}).Warn("error processing scanner line in installation output for server") }).Warn("error processing scanner line in installation output for server")
} }
@@ -512,15 +512,13 @@ func (ip *InstallationProcess) StreamOutput(id string) error {
// value of "true" means everything was successful, "false" means something went // value of "true" means everything was successful, "false" means something went
// wrong and the server must be deleted and re-created. // wrong and the server must be deleted and re-created.
func (s *Server) SyncInstallState(successful bool) error { func (s *Server) SyncInstallState(successful bool) error {
r := api.NewRequester() err := api.New().SendInstallationStatus(s.Id(), successful)
rerr, err := r.SendInstallationStatus(s.Id(), successful)
if rerr != nil || err != nil {
if err != nil { if err != nil {
return errors.WithStack(err) if !api.IsRequestError(err) {
return errors.WithStackIf(err)
} }
return errors.New(rerr.String()) return errors.New(err.Error())
} }
return nil return nil

View File

@@ -1,15 +1,16 @@
package server package server
import ( import (
"emperror.dev/errors"
"encoding/json" "encoding/json"
"github.com/apex/log" "github.com/apex/log"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api" "github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/events" "github.com/pterodactyl/wings/events"
"regexp" "regexp"
"strconv" "strconv"
"sync"
) )
var dockerEvents = []string{ var dockerEvents = []string{
@@ -18,6 +19,37 @@ var dockerEvents = []string{
environment.DockerImagePullCompleted, environment.DockerImagePullCompleted,
} }
type diskSpaceLimiter struct {
o sync.Once
mu sync.Mutex
server *Server
}
func newDiskLimiter(s *Server) *diskSpaceLimiter {
return &diskSpaceLimiter{server: s}
}
// Reset the disk space limiter status.
func (dsl *diskSpaceLimiter) Reset() {
dsl.mu.Lock()
dsl.o = sync.Once{}
dsl.mu.Unlock()
}
// Trigger the disk space limiter which will attempt to stop a running server instance within
// 15 seconds, and terminate it forcefully if it does not stop.
//
// This function is only executed one time, so whenever a server is marked as booting the limiter
// should be reset so it can properly be triggered as needed.
func (dsl *diskSpaceLimiter) Trigger() {
dsl.o.Do(func() {
dsl.server.PublishConsoleOutputFromDaemon("Server is exceeding the assigned disk space limit, stopping process now.")
if err := dsl.server.Environment.WaitForStop(60, true); err != nil {
dsl.server.Log().WithField("error", err).Error("failed to stop server after exceeding space limit!")
}
})
}
// Adds all of the internal event listeners we want to use for a server. These listeners can only be // Adds all of the internal event listeners we want to use for a server. These listeners can only be
// removed by deleting the server as they should last for the duration of the process' lifetime. // removed by deleting the server as they should last for the duration of the process' lifetime.
func (s *Server) StartEventListeners() { func (s *Server) StartEventListeners() {
@@ -31,8 +63,8 @@ func (s *Server) StartEventListeners() {
if err != nil { if err != nil {
// If the process is already stopping, just let it continue with that action rather than attempting // If the process is already stopping, just let it continue with that action rather than attempting
// to terminate again. // to terminate again.
if s.GetState() != environment.ProcessStoppingState { if s.Environment.State() != environment.ProcessStoppingState {
s.SetState(environment.ProcessStoppingState) s.Environment.SetState(environment.ProcessStoppingState)
go func() { go func() {
s.Log().Warn("stopping server instance, violating throttle limits") 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.") s.PublishConsoleOutputFromDaemon("Your server is being stopped for outputting too much data in a short period of time.")
@@ -41,11 +73,11 @@ func (s *Server) StartEventListeners() {
if err := s.Environment.WaitForStop(config.Get().Throttles.StopGracePeriod, true); err != nil { if err := s.Environment.WaitForStop(config.Get().Throttles.StopGracePeriod, true); err != nil {
// If there is an error set the process back to running so that this throttler is called // If there is an error set the process back to running so that this throttler is called
// again and hopefully kills the server. // again and hopefully kills the server.
if s.GetState() != environment.ProcessOfflineState { if s.Environment.State() != environment.ProcessOfflineState {
s.SetState(environment.ProcessRunningState) s.Environment.SetState(environment.ProcessRunningState)
} }
s.Log().WithField("error", errors.WithStack(err)).Error("failed to terminate environment after triggering throttle") s.Log().WithField("error", errors.WithStackIf(err)).Error("failed to terminate environment after triggering throttle")
} }
}() }()
} }
@@ -60,19 +92,21 @@ func (s *Server) StartEventListeners() {
s.onConsoleOutput(e.Data) s.onConsoleOutput(e.Data)
} }
l := newDiskLimiter(s)
state := func(e events.Event) { state := func(e events.Event) {
// Reset the throttler when the process is started. // Reset the throttler when the process is started.
if e.Data == environment.ProcessStartingState { if e.Data == environment.ProcessStartingState {
l.Reset()
s.Throttler().Reset() s.Throttler().Reset()
} }
s.SetState(e.Data) s.OnStateChange()
} }
stats := func(e events.Event) { stats := func(e events.Event) {
st := new(environment.Stats) st := new(environment.Stats)
if err := json.Unmarshal([]byte(e.Data), st); err != nil { if err := json.Unmarshal([]byte(e.Data), st); err != nil {
s.Log().WithField("error", errors.WithStack(err)).Warn("failed to unmarshal server environment stats") s.Log().WithField("error", errors.WithStackIf(err)).Warn("failed to unmarshal server environment stats")
return return
} }
@@ -81,7 +115,11 @@ func (s *Server) StartEventListeners() {
s.resources.Stats = *st s.resources.Stats = *st
s.resources.mu.Unlock() s.resources.mu.Unlock()
s.Filesystem().HasSpaceAvailable(true) // If there is no disk space available at this point, trigger the server disk limiter logic
// which will start to stop the running instance.
if !s.Filesystem().HasSpaceAvailable(true) {
l.Trigger()
}
s.emitProcUsage() s.emitProcUsage()
} }
@@ -135,7 +173,7 @@ func (s *Server) onConsoleOutput(data string) {
// If the specific line of output is one that would mark the server as started, // If the specific line of output is one that would mark the server as started,
// set the server to that state. Only do this if the server is not currently stopped // set the server to that state. Only do this if the server is not currently stopped
// or stopping. // or stopping.
_ = s.SetState(environment.ProcessRunningState) s.Environment.SetState(environment.ProcessRunningState)
break break
} }
} }
@@ -147,7 +185,7 @@ func (s *Server) onConsoleOutput(data string) {
stop := processConfiguration.Stop stop := processConfiguration.Stop
if stop.Type == api.ProcessStopCommand && data == stop.Value { if stop.Type == api.ProcessStopCommand && data == stop.Value {
_ = s.SetState(environment.ProcessOfflineState) s.Environment.SetState(environment.ProcessOfflineState)
} }
} }
} }

View File

@@ -1,11 +1,12 @@
package server package server
import ( import (
"emperror.dev/errors"
"encoding/json"
"fmt" "fmt"
"github.com/apex/log" "github.com/apex/log"
"github.com/creasty/defaults" "github.com/creasty/defaults"
"github.com/gammazero/workerpool" "github.com/gammazero/workerpool"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api" "github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
@@ -31,28 +32,40 @@ func LoadDirectory() error {
} }
log.Info("fetching list of servers from API") log.Info("fetching list of servers from API")
configs, rerr, err := api.NewRequester().GetAllServerConfigurations() configs, err := api.New().GetServers()
if err != nil || rerr != nil {
if err != nil { if err != nil {
return errors.WithStack(err) if !api.IsRequestError(err) {
return errors.WithStackIf(err)
} }
return errors.New(rerr.String()) return errors.New(err.Error())
} }
start := time.Now() start := time.Now()
log.WithField("total_configs", len(configs)).Info("processing servers returned by the API") log.WithField("total_configs", len(configs)).Info("processing servers returned by the API")
pool := workerpool.New(runtime.NumCPU()) pool := workerpool.New(runtime.NumCPU())
for uuid, data := range configs { log.Debugf("using %d workerpools to instantiate server instances", runtime.NumCPU())
uuid := uuid for _, data := range configs {
data := data data := data
pool.Submit(func() { pool.Submit(func() {
log.WithField("server", uuid).Info("creating new server object from API response") // Parse the json.RawMessage into an expected struct value. We do this here so that a single broken
s, err := FromConfiguration(data) // server does not cause the entire boot process to hang, and allows us to show more useful error
// messaging in the output.
d := api.ServerConfigurationResponse{
Settings: data.Settings,
}
log.WithField("server", data.Uuid).Info("creating new server object from API response")
if err := json.Unmarshal(data.ProcessConfiguration, &d.ProcessConfiguration); err != nil {
log.WithField("server", data.Uuid).WithField("error", err).Error("failed to parse server configuration from API response, skipping...")
return
}
s, err := FromConfiguration(d)
if err != nil { if err != nil {
log.WithField("server", uuid).WithField("error", err).Error("failed to load server, skipping...") log.WithField("server", data.Uuid).WithField("error", err).Error("failed to load server, skipping...")
return return
} }
@@ -73,15 +86,15 @@ func LoadDirectory() error {
// Initializes a server using a data byte array. This will be marshaled into the // Initializes a server using a data byte array. This will be marshaled into the
// given struct using a YAML marshaler. This will also configure the given environment // given struct using a YAML marshaler. This will also configure the given environment
// for a server. // for a server.
func FromConfiguration(data *api.ServerConfigurationResponse) (*Server, error) { func FromConfiguration(data api.ServerConfigurationResponse) (*Server, error) {
cfg := Configuration{} cfg := Configuration{}
if err := defaults.Set(&cfg); err != nil { if err := defaults.Set(&cfg); err != nil {
return nil, errors.Wrap(err, "failed to set struct defaults for server configuration") return nil, errors.WrapIf(err, "failed to set struct defaults for server configuration")
} }
s := new(Server) s := new(Server)
if err := defaults.Set(s); err != nil { if err := defaults.Set(s); err != nil {
return nil, errors.Wrap(err, "failed to set struct defaults for server") return nil, errors.WrapIf(err, "failed to set struct defaults for server")
} }
s.cfg = cfg s.cfg = cfg
@@ -91,6 +104,7 @@ func FromConfiguration(data *api.ServerConfigurationResponse) (*Server, error) {
s.resources = ResourceUsage{} s.resources = ResourceUsage{}
defaults.Set(&s.resources) defaults.Set(&s.resources)
s.resources.State.Store(environment.ProcessOfflineState)
s.Archiver = Archiver{Server: s} s.Archiver = Archiver{Server: s}
s.fs = filesystem.New(filepath.Join(config.Get().System.Data, s.Id()), s.DiskSpace()) s.fs = filesystem.New(filepath.Join(config.Get().System.Data, s.Id()), s.DiskSpace())

View File

@@ -2,10 +2,8 @@ package server
import ( import (
"github.com/apex/log" "github.com/apex/log"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
"os"
"path/filepath" "path/filepath"
"strings" "strings"
) )
@@ -16,41 +14,17 @@ import (
type Mount environment.Mount type Mount environment.Mount
// Returns the default container mounts for the server instance. This includes the data directory // Returns the default container mounts for the server instance. This includes the data directory
// for the server as well as any timezone related files if they exist on the host system so that // for the server. Previously this would also mount in host timezone files, however we've moved from
// servers running within the container will use the correct time. // that approach to just setting `TZ=Timezone` environment values in containers which should work
// in most scenarios.
func (s *Server) Mounts() []environment.Mount { func (s *Server) Mounts() []environment.Mount {
var m []environment.Mount m := []environment.Mount{
{
m = append(m, environment.Mount{
Default: true, Default: true,
Target: "/home/container", Target: "/home/container",
Source: s.Filesystem().Path(), Source: s.Filesystem().Path(),
ReadOnly: false, ReadOnly: false,
}) },
// Try to mount in /etc/localtime and /etc/timezone if they exist on the host system.
if _, err := os.Stat("/etc/localtime"); err != nil {
if !os.IsNotExist(err) {
log.WithField("error", errors.WithStack(err)).Warn("failed to stat /etc/localtime due to an error")
}
} else {
m = append(m, environment.Mount{
Target: "/etc/localtime",
Source: "/etc/localtime",
ReadOnly: true,
})
}
if _, err := os.Stat("/etc/timezone"); err != nil {
if !os.IsNotExist(err) {
log.WithField("error", errors.WithStack(err)).Warn("failed to stat /etc/timezone due to an error")
}
} else {
m = append(m, environment.Mount{
Target: "/etc/timezone",
Source: "/etc/timezone",
ReadOnly: true,
})
} }
// Also include any of this server's custom mounts when returning them. // Also include any of this server's custom mounts when returning them.

View File

@@ -2,7 +2,7 @@ package server
import ( import (
"context" "context"
"github.com/pkg/errors" "emperror.dev/errors"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/server/filesystem" "github.com/pterodactyl/wings/server/filesystem"
@@ -80,13 +80,13 @@ func (s *Server) HandlePowerAction(action PowerAction, waitSeconds ...int) error
// time than that passes an error will be propagated back up the chain and this // time than that passes an error will be propagated back up the chain and this
// request will be aborted. // request will be aborted.
if err := s.powerLock.Acquire(ctx, 1); err != nil { if err := s.powerLock.Acquire(ctx, 1); err != nil {
return errors.Wrap(err, "could not acquire lock on power state") return errors.WrapIf(err, "could not acquire lock on power state")
} }
} else { } else {
// If no wait duration was provided we will attempt to immediately acquire the lock // If no wait duration was provided we will attempt to immediately acquire the lock
// and bail out with a context deadline error if it is not acquired immediately. // and bail out with a context deadline error if it is not acquired immediately.
if ok := s.powerLock.TryAcquire(1); !ok { if ok := s.powerLock.TryAcquire(1); !ok {
return errors.Wrap(context.DeadlineExceeded, "could not acquire lock on power state") return errors.WrapIf(context.DeadlineExceeded, "could not acquire lock on power state")
} }
} }
@@ -149,7 +149,7 @@ func (s *Server) HandlePowerAction(action PowerAction, waitSeconds ...int) error
func (s *Server) onBeforeStart() error { func (s *Server) onBeforeStart() error {
s.Log().Info("syncing server configuration with panel") s.Log().Info("syncing server configuration with panel")
if err := s.Sync(); err != nil { if err := s.Sync(); err != nil {
return errors.Wrap(err, "unable to sync server data from Panel instance") return errors.WrapIf(err, "unable to sync server data from Panel instance")
} }
// Disallow start & restart if the server is suspended. Do this check after performing a sync // Disallow start & restart if the server is suspended. Do this check after performing a sync
@@ -185,7 +185,7 @@ func (s *Server) onBeforeStart() error {
s.PublishConsoleOutputFromDaemon("Ensuring file permissions are set correctly, this could take a few seconds...") s.PublishConsoleOutputFromDaemon("Ensuring file permissions are set correctly, this could take a few seconds...")
// Ensure all of the server file permissions are set correctly before booting the process. // Ensure all of the server file permissions are set correctly before booting the process.
if err := s.Filesystem().Chown("/"); err != nil { if err := s.Filesystem().Chown("/"); err != nil {
return errors.Wrap(err, "failed to chown root server directory during pre-boot process") return errors.WrapIf(err, "failed to chown root server directory during pre-boot process")
} }
} }

View File

@@ -3,7 +3,9 @@ package server
import ( import (
"encoding/json" "encoding/json"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/system"
"sync" "sync"
"sync/atomic"
) )
// Defines the current resource usage for a given server instance. If a server is offline you // Defines the current resource usage for a given server instance. If a server is offline you
@@ -16,7 +18,7 @@ type ResourceUsage struct {
environment.Stats environment.Stats
// The current server status. // The current server status.
State string `json:"state" default:"offline"` State *system.AtomicString `json:"state" default:"{}"`
// The current disk space being used by the server. This value is not guaranteed to be accurate // The current disk space being used by the server. This value is not guaranteed to be accurate
// at all times. It is "manually" set whenever server.Proc() is called. This is kind of just a // at all times. It is "manually" set whenever server.Proc() is called. This is kind of just a
@@ -24,16 +26,16 @@ type ResourceUsage struct {
Disk int64 `json:"disk_bytes"` Disk int64 `json:"disk_bytes"`
} }
// Alias the resource usage so that we don't infinitely recurse when marshaling the struct.
type IResourceUsage ResourceUsage
// Custom marshaler to ensure that the object is locked when we're converting it to JSON in // Custom marshaler to ensure that the object is locked when we're converting it to JSON in
// order to avoid race conditions. // order to avoid race conditions.
func (ru *ResourceUsage) MarshalJSON() ([]byte, error) { func (ru *ResourceUsage) MarshalJSON() ([]byte, error) {
ru.mu.Lock() ru.mu.Lock()
defer ru.mu.Unlock() defer ru.mu.Unlock()
return json.Marshal(IResourceUsage(*ru)) // Alias the resource usage so that we don't infinitely recurse when marshaling the struct.
type alias ResourceUsage
return json.Marshal(alias(*ru))
} }
// Returns the resource usage stats for the server instance. If the server is not running, only the // Returns the resource usage stats for the server instance. If the server is not running, only the
@@ -42,10 +44,10 @@ func (ru *ResourceUsage) MarshalJSON() ([]byte, error) {
// //
// When a process is stopped all of the stats are zeroed out except for the disk. // When a process is stopped all of the stats are zeroed out except for the disk.
func (s *Server) Proc() *ResourceUsage { func (s *Server) Proc() *ResourceUsage {
s.resources.SetDisk(s.Filesystem().CachedUsage()) // Store the updated disk usage when requesting process usage.
atomic.StoreInt64(&s.resources.Disk, s.Filesystem().CachedUsage())
// Get a read lock on the resources at this point. Don't do this before setting // Acquire a lock before attempting to return the value of resources.
// the disk, otherwise you'll cause a deadlock.
s.resources.mu.RLock() s.resources.mu.RLock()
defer s.resources.mu.RUnlock() defer s.resources.mu.RUnlock()
@@ -57,24 +59,3 @@ func (s *Server) emitProcUsage() {
s.Log().WithField("error", err).Warn("error while emitting server resource usage to listeners") s.Log().WithField("error", err).Warn("error while emitting server resource usage to listeners")
} }
} }
// Returns the servers current state.
func (ru *ResourceUsage) getInternalState() string {
ru.mu.RLock()
defer ru.mu.RUnlock()
return ru.State
}
// Sets the new state for the server.
func (ru *ResourceUsage) setInternalState(state string) {
ru.mu.Lock()
ru.State = state
ru.mu.Unlock()
}
func (ru *ResourceUsage) SetDisk(i int64) {
ru.mu.Lock()
ru.Disk = i
ru.mu.Unlock()
}

View File

@@ -2,10 +2,11 @@ package server
import ( import (
"context" "context"
"emperror.dev/errors"
"fmt" "fmt"
"github.com/apex/log" "github.com/apex/log"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api" "github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/environment/docker" "github.com/pterodactyl/wings/environment/docker"
"github.com/pterodactyl/wings/events" "github.com/pterodactyl/wings/events"
@@ -13,7 +14,6 @@ import (
"golang.org/x/sync/semaphore" "golang.org/x/sync/semaphore"
"strings" "strings"
"sync" "sync"
"time"
) )
// High level definition for a server instance being controlled by Wings. // High level definition for a server instance being controlled by Wings.
@@ -78,10 +78,8 @@ func (s *Server) Id() string {
// Returns all of the environment variables that should be assigned to a running // Returns all of the environment variables that should be assigned to a running
// server instance. // server instance.
func (s *Server) GetEnvironmentVariables() []string { func (s *Server) GetEnvironmentVariables() []string {
zone, _ := time.Now().In(time.Local).Zone()
var out = []string{ var out = []string{
fmt.Sprintf("TZ=%s", zone), 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()),
fmt.Sprintf("SERVER_IP=%s", s.Config().Allocations.DefaultMapping.Ip), fmt.Sprintf("SERVER_IP=%s", s.Config().Allocations.DefaultMapping.Ip),
@@ -90,6 +88,7 @@ func (s *Server) GetEnvironmentVariables() []string {
eloop: eloop:
for k := range s.Config().EnvVars { for k := range s.Config().EnvVars {
// Don't allow any environment variables that we have already set above.
for _, e := range out { for _, e := range out {
if strings.HasPrefix(e, strings.ToUpper(k)) { if strings.HasPrefix(e, strings.ToUpper(k)) {
continue eloop continue eloop
@@ -113,26 +112,26 @@ func (s *Server) Log() *log.Entry {
// This also means mass actions can be performed against servers on the Panel and they // This also means mass actions can be performed against servers on the Panel and they
// will automatically sync with Wings when the server is started. // will automatically sync with Wings when the server is started.
func (s *Server) Sync() error { func (s *Server) Sync() error {
cfg, rerr, err := s.GetProcessConfiguration() cfg, err := api.New().GetServerConfiguration(s.Id())
if err != nil || rerr != nil {
if err != nil { if err != nil {
return errors.WithStack(err) if !api.IsRequestError(err) {
return errors.WithStackIf(err)
} }
if rerr.Status == "404" { if err.(*api.RequestError).Status == "404" {
return &serverDoesNotExist{} return &serverDoesNotExist{}
} }
return errors.New(rerr.String()) return errors.New(err.Error())
} }
return s.SyncWithConfiguration(cfg) return s.SyncWithConfiguration(cfg)
} }
func (s *Server) SyncWithConfiguration(cfg *api.ServerConfigurationResponse) error { func (s *Server) SyncWithConfiguration(cfg api.ServerConfigurationResponse) error {
// Update the data structure and persist it to the disk. // Update the data structure and persist it to the disk.
if err := s.UpdateDataStructure(cfg.Settings); err != nil { if err := s.UpdateDataStructure(cfg.Settings); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
s.Lock() s.Lock()
@@ -148,7 +147,7 @@ func (s *Server) SyncWithConfiguration(cfg *api.ServerConfigurationResponse) err
if e, ok := s.Environment.(*docker.Environment); ok { if e, ok := s.Environment.(*docker.Environment); ok {
s.Log().Debug("syncing stop configuration with configured docker environment") s.Log().Debug("syncing stop configuration with configured docker environment")
e.SetImage(s.Config().Container.Image) e.SetImage(s.Config().Container.Image)
e.SetStopConfiguration(&cfg.ProcessConfiguration.Stop) e.SetStopConfiguration(cfg.ProcessConfiguration.Stop)
} }
return nil return nil
@@ -172,17 +171,12 @@ func (s *Server) IsBootable() bool {
func (s *Server) CreateEnvironment() error { func (s *Server) CreateEnvironment() error {
// Ensure the data directory exists before getting too far through this process. // Ensure the data directory exists before getting too far through this process.
if err := s.EnsureDataDirectoryExists(); err != nil { if err := s.EnsureDataDirectoryExists(); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
return s.Environment.Create() return s.Environment.Create()
} }
// Gets the process configuration data for the server.
func (s *Server) GetProcessConfiguration() (*api.ServerConfigurationResponse, *api.RequestError, error) {
return api.NewRequester().GetServerConfiguration(s.Id())
}
// Checks if the server is marked as being suspended or not on the system. // Checks if the server is marked as being suspended or not on the system.
func (s *Server) IsSuspended() bool { func (s *Server) IsSuspended() bool {
return s.Config().Suspended return s.Config().Suspended

View File

@@ -1,9 +1,8 @@
package server package server
import ( import (
"emperror.dev/errors"
"encoding/json" "encoding/json"
"fmt"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
"io" "io"
@@ -23,14 +22,14 @@ func CachedServerStates() (map[string]string, error) {
// Open the states file. // Open the states file.
f, err := os.OpenFile(config.Get().System.GetStatesPath(), os.O_RDONLY|os.O_CREATE, 0644) f, err := os.OpenFile(config.Get().System.GetStatesPath(), os.O_RDONLY|os.O_CREATE, 0644)
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStackIf(err)
} }
defer f.Close() defer f.Close()
// Convert the json object to a map. // Convert the json object to a map.
states := map[string]string{} states := map[string]string{}
if err := json.NewDecoder(f).Decode(&states); err != nil && err != io.EOF { if err := json.NewDecoder(f).Decode(&states); err != nil && err != io.EOF {
return nil, errors.WithStack(err) return nil, errors.WithStackIf(err)
} }
return states, nil return states, nil
@@ -47,7 +46,7 @@ func saveServerStates() error {
// Convert the map to a json object. // Convert the map to a json object.
data, err := json.Marshal(states) data, err := json.Marshal(states)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
stateMutex.Lock() stateMutex.Lock()
@@ -55,7 +54,7 @@ func saveServerStates() error {
// Write the data to the file // Write the data to the file
if err := ioutil.WriteFile(config.Get().System.GetStatesPath(), data, 0644); err != nil { if err := ioutil.WriteFile(config.Get().System.GetStatesPath(), data, 0644); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
return nil return nil
@@ -63,23 +62,17 @@ func saveServerStates() error {
// Sets the state of the server internally. This function handles crash detection as // Sets the state of the server internally. This function handles crash detection as
// well as reporting to event listeners for the server. // well as reporting to event listeners for the server.
func (s *Server) SetState(state string) error { func (s *Server) OnStateChange() {
if state != environment.ProcessOfflineState && prevState := s.Proc().State.Load()
state != environment.ProcessStartingState &&
state != environment.ProcessRunningState &&
state != environment.ProcessStoppingState {
return errors.New(fmt.Sprintf("invalid server state received: %s", state))
}
prevState := s.GetState()
st := s.Environment.State()
// Update the currently tracked state for the server. // Update the currently tracked state for the server.
s.Proc().setInternalState(state) s.Proc().State.Store(st)
// Emit the event to any listeners that are currently registered. // Emit the event to any listeners that are currently registered.
if prevState != state { if prevState != s.Environment.State() {
s.Log().WithField("status", s.Proc().getInternalState()).Debug("saw server status change event") s.Log().WithField("status", st).Debug("saw server status change event")
s.Events().Publish(StatusEvent, s.Proc().getInternalState()) s.Events().Publish(StatusEvent, st)
} }
// Persist this change to the disk immediately so that should the Daemon be stopped or // Persist this change to the disk immediately so that should the Daemon be stopped or
@@ -98,7 +91,7 @@ func (s *Server) SetState(state string) error {
// Reset the resource usage to 0 when the process fully stops so that all of the UI // Reset the resource usage to 0 when the process fully stops so that all of the UI
// views in the Panel correctly display 0. // views in the Panel correctly display 0.
if state == environment.ProcessOfflineState { if st == environment.ProcessOfflineState {
s.resources.mu.Lock() s.resources.mu.Lock()
s.resources.Empty() s.resources.Empty()
s.resources.mu.Unlock() s.resources.mu.Unlock()
@@ -127,13 +120,13 @@ func (s *Server) SetState(state string) error {
} }
}(s) }(s)
} }
return nil
} }
// Returns the current state of the server in a race-safe manner. // Returns the current state of the server in a race-safe manner.
// Deprecated
// use Environment.State()
func (s *Server) GetState() string { func (s *Server) GetState() string {
return s.Proc().getInternalState() return s.Environment.State()
} }
// Determines if the server state is running or not. This is different than the // Determines if the server state is running or not. This is different than the

View File

@@ -1,10 +1,10 @@
package server package server
import ( import (
"emperror.dev/errors"
"encoding/json" "encoding/json"
"github.com/buger/jsonparser" "github.com/buger/jsonparser"
"github.com/imdario/mergo" "github.com/imdario/mergo"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment"
) )
@@ -18,7 +18,7 @@ import (
func (s *Server) UpdateDataStructure(data []byte) error { func (s *Server) UpdateDataStructure(data []byte) error {
src := new(Configuration) src := new(Configuration)
if err := json.Unmarshal(data, src); err != nil { if err := json.Unmarshal(data, src); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
// Don't allow obviously corrupted data to pass through into this function. If the UUID // Don't allow obviously corrupted data to pass through into this function. If the UUID
@@ -47,7 +47,7 @@ func (s *Server) UpdateDataStructure(data []byte) error {
// Merge the new data object that we have received with the existing server data object // Merge the new data object that we have received with the existing server data object
// and then save it to the disk so it is persistent. // and then save it to the disk so it is persistent.
if err := mergo.Merge(&c, src, mergo.WithOverride); err != nil { if err := mergo.Merge(&c, src, mergo.WithOverride); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
// Don't explode if we're setting CPU limits to 0. Mergo sees that as an empty value // Don't explode if we're setting CPU limits to 0. Mergo sees that as an empty value
@@ -65,7 +65,7 @@ func (s *Server) UpdateDataStructure(data []byte) error {
// request is going to be boolean. Allegedly. // request is going to be boolean. Allegedly.
if v, err := jsonparser.GetBoolean(data, "container", "oom_disabled"); err != nil { if v, err := jsonparser.GetBoolean(data, "container", "oom_disabled"); err != nil {
if err != jsonparser.KeyPathNotFoundError { if err != jsonparser.KeyPathNotFoundError {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
} else { } else {
c.Build.OOMDisabled = v c.Build.OOMDisabled = v
@@ -74,7 +74,7 @@ func (s *Server) UpdateDataStructure(data []byte) error {
// Mergo also cannot handle this boolean value. // Mergo also cannot handle this boolean value.
if v, err := jsonparser.GetBoolean(data, "suspended"); err != nil { if v, err := jsonparser.GetBoolean(data, "suspended"); err != nil {
if err != jsonparser.KeyPathNotFoundError { if err != jsonparser.KeyPathNotFoundError {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
} else { } else {
c.Suspended = v c.Suspended = v
@@ -82,7 +82,7 @@ func (s *Server) UpdateDataStructure(data []byte) error {
if v, err := jsonparser.GetBoolean(data, "skip_egg_scripts"); err != nil { if v, err := jsonparser.GetBoolean(data, "skip_egg_scripts"); err != nil {
if err != jsonparser.KeyPathNotFoundError { if err != jsonparser.KeyPathNotFoundError {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
} else { } else {
c.SkipEggScripts = v c.SkipEggScripts = v

View File

@@ -1,9 +1,9 @@
package sftp package sftp
import ( import (
"emperror.dev/errors"
"github.com/apex/log" "github.com/apex/log"
"github.com/patrickmn/go-cache" "github.com/patrickmn/go-cache"
"github.com/pkg/errors"
"github.com/pkg/sftp" "github.com/pkg/sftp"
"io" "io"
"io/ioutil" "io/ioutil"
@@ -58,14 +58,14 @@ func (fs FileSystem) Fileread(request *sftp.Request) (io.ReaderAt, error) {
if _, err := os.Stat(p); os.IsNotExist(err) { if _, err := os.Stat(p); os.IsNotExist(err) {
return nil, sftp.ErrSshFxNoSuchFile return nil, sftp.ErrSshFxNoSuchFile
} else if err != nil { } else if err != nil {
fs.logger.WithField("error", errors.WithStack(err)).Error("error while processing file stat") fs.logger.WithField("error", errors.WithStackIf(err)).Error("error while processing file stat")
return nil, sftp.ErrSshFxFailure return nil, sftp.ErrSshFxFailure
} }
file, err := os.Open(p) file, err := os.Open(p)
if err != nil { if err != nil {
fs.logger.WithField("source", p).WithField("error", errors.WithStack(err)).Error("could not open file for reading") fs.logger.WithField("source", p).WithField("error", errors.WithStackIf(err)).Error("could not open file for reading")
return nil, sftp.ErrSshFxFailure return nil, sftp.ErrSshFxFailure
} }
@@ -108,7 +108,7 @@ func (fs FileSystem) Filewrite(request *sftp.Request) (io.WriterAt, error) {
if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil { if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
l.WithFields(log.Fields{ l.WithFields(log.Fields{
"path": filepath.Dir(p), "path": filepath.Dir(p),
"error": errors.WithStack(err), "error": errors.WithStackIf(err),
}).Error("error making path for file") }).Error("error making path for file")
return nil, sftp.ErrSshFxFailure return nil, sftp.ErrSshFxFailure
@@ -116,7 +116,7 @@ func (fs FileSystem) Filewrite(request *sftp.Request) (io.WriterAt, error) {
file, err := os.Create(p) file, err := os.Create(p)
if err != nil { if err != nil {
l.WithField("error", errors.WithStack(err)).Error("failed to create file") l.WithField("error", errors.WithStackIf(err)).Error("failed to create file")
return nil, sftp.ErrSshFxFailure return nil, sftp.ErrSshFxFailure
} }
@@ -124,7 +124,7 @@ func (fs FileSystem) Filewrite(request *sftp.Request) (io.WriterAt, error) {
// Not failing here is intentional. We still made the file, it is just owned incorrectly // Not failing here is intentional. We still made the file, it is just owned incorrectly
// and will likely cause some issues. // and will likely cause some issues.
if err := os.Chown(p, fs.User.Uid, fs.User.Gid); err != nil { if err := os.Chown(p, fs.User.Uid, fs.User.Gid); err != nil {
l.WithField("error", errors.WithStack(err)).Warn("failed to set permissions on file") l.WithField("error", errors.WithStackIf(err)).Warn("failed to set permissions on file")
} }
return file, nil return file, nil
@@ -133,7 +133,7 @@ func (fs FileSystem) Filewrite(request *sftp.Request) (io.WriterAt, error) {
// If the stat error isn't about the file not existing, there is some other issue // If the stat error isn't about the file not existing, there is some other issue
// at play and we need to go ahead and bail out of the process. // at play and we need to go ahead and bail out of the process.
if statErr != nil { if statErr != nil {
l.WithField("error", errors.WithStack(statErr)).Error("encountered error performing file stat") l.WithField("error", errors.WithStackIf(statErr)).Error("encountered error performing file stat")
return nil, sftp.ErrSshFxFailure return nil, sftp.ErrSshFxFailure
} }
@@ -159,14 +159,14 @@ func (fs FileSystem) Filewrite(request *sftp.Request) (io.WriterAt, error) {
return nil, sftp.ErrSSHFxNoSuchFile return nil, sftp.ErrSSHFxNoSuchFile
} }
l.WithField("flags", request.Flags).WithField("error", errors.WithStack(err)).Error("failed to open existing file on system") l.WithField("flags", request.Flags).WithField("error", errors.WithStackIf(err)).Error("failed to open existing file on system")
return nil, sftp.ErrSshFxFailure return nil, sftp.ErrSshFxFailure
} }
// Not failing here is intentional. We still made the file, it is just owned incorrectly // Not failing here is intentional. We still made the file, it is just owned incorrectly
// and will likely cause some issues. // and will likely cause some issues.
if err := os.Chown(p, fs.User.Uid, fs.User.Gid); err != nil { if err := os.Chown(p, fs.User.Uid, fs.User.Gid); err != nil {
l.WithField("error", errors.WithStack(err)).Warn("error chowning file") l.WithField("error", errors.WithStackIf(err)).Warn("error chowning file")
} }
return file, nil return file, nil
@@ -220,7 +220,7 @@ func (fs FileSystem) Filecmd(request *sftp.Request) error {
return sftp.ErrSSHFxNoSuchFile return sftp.ErrSSHFxNoSuchFile
} }
l.WithField("error", errors.WithStack(err)).Error("failed to perform setstat on item") l.WithField("error", errors.WithStackIf(err)).Error("failed to perform setstat on item")
return sftp.ErrSSHFxFailure return sftp.ErrSSHFxFailure
} }
return nil return nil
@@ -234,7 +234,7 @@ func (fs FileSystem) Filecmd(request *sftp.Request) error {
return sftp.ErrSSHFxNoSuchFile return sftp.ErrSSHFxNoSuchFile
} }
l.WithField("target", target).WithField("error", errors.WithStack(err)).Error("failed to rename file") l.WithField("target", target).WithField("error", errors.WithStackIf(err)).Error("failed to rename file")
return sftp.ErrSshFxFailure return sftp.ErrSshFxFailure
} }
@@ -246,7 +246,7 @@ func (fs FileSystem) Filecmd(request *sftp.Request) error {
} }
if err := os.RemoveAll(p); err != nil { if err := os.RemoveAll(p); err != nil {
l.WithField("error", errors.WithStack(err)).Error("failed to remove directory") l.WithField("error", errors.WithStackIf(err)).Error("failed to remove directory")
return sftp.ErrSshFxFailure return sftp.ErrSshFxFailure
} }
@@ -258,7 +258,7 @@ func (fs FileSystem) Filecmd(request *sftp.Request) error {
} }
if err := os.MkdirAll(p, 0755); err != nil { if err := os.MkdirAll(p, 0755); err != nil {
l.WithField("error", errors.WithStack(err)).Error("failed to create directory") l.WithField("error", errors.WithStackIf(err)).Error("failed to create directory")
return sftp.ErrSshFxFailure return sftp.ErrSshFxFailure
} }
@@ -270,7 +270,7 @@ func (fs FileSystem) Filecmd(request *sftp.Request) error {
} }
if err := os.Symlink(p, target); err != nil { if err := os.Symlink(p, target); err != nil {
l.WithField("target", target).WithField("error", errors.WithStack(err)).Error("failed to create symlink") l.WithField("target", target).WithField("error", errors.WithStackIf(err)).Error("failed to create symlink")
return sftp.ErrSshFxFailure return sftp.ErrSshFxFailure
} }
@@ -286,7 +286,7 @@ func (fs FileSystem) Filecmd(request *sftp.Request) error {
return sftp.ErrSSHFxNoSuchFile return sftp.ErrSSHFxNoSuchFile
} }
l.WithField("error", errors.WithStack(err)).Error("failed to remove a file") l.WithField("error", errors.WithStackIf(err)).Error("failed to remove a file")
return sftp.ErrSshFxFailure return sftp.ErrSshFxFailure
} }
@@ -305,7 +305,7 @@ func (fs FileSystem) Filecmd(request *sftp.Request) error {
// and will likely cause some issues. There is no logical check for if the file was removed // and will likely cause some issues. There is no logical check for if the file was removed
// because both of those cases (Rmdir, Remove) have an explicit return rather than break. // because both of those cases (Rmdir, Remove) have an explicit return rather than break.
if err := os.Chown(fileLocation, fs.User.Uid, fs.User.Gid); err != nil { if err := os.Chown(fileLocation, fs.User.Uid, fs.User.Gid); err != nil {
l.WithField("error", errors.WithStack(err)).Warn("error chowning file") l.WithField("error", errors.WithStackIf(err)).Warn("error chowning file")
} }
return sftp.ErrSshFxOk return sftp.ErrSshFxOk
@@ -327,7 +327,7 @@ func (fs FileSystem) Filelist(request *sftp.Request) (sftp.ListerAt, error) {
files, err := ioutil.ReadDir(p) files, err := ioutil.ReadDir(p)
if err != nil { if err != nil {
fs.logger.WithField("error", errors.WithStack(err)).Error("error while listing directory") fs.logger.WithField("error", errors.WithStackIf(err)).Error("error while listing directory")
return nil, sftp.ErrSshFxFailure return nil, sftp.ErrSshFxFailure
} }
@@ -342,7 +342,7 @@ func (fs FileSystem) Filelist(request *sftp.Request) (sftp.ListerAt, error) {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return nil, sftp.ErrSshFxNoSuchFile return nil, sftp.ErrSshFxNoSuchFile
} else if err != nil { } else if err != nil {
fs.logger.WithField("source", p).WithField("error", errors.WithStack(err)).Error("error performing stat on file") fs.logger.WithField("source", p).WithField("error", errors.WithStackIf(err)).Error("error performing stat on file")
return nil, sftp.ErrSshFxFailure return nil, sftp.ErrSshFxFailure
} }

View File

@@ -1,8 +1,8 @@
package sftp package sftp
import ( import (
"emperror.dev/errors"
"github.com/apex/log" "github.com/apex/log"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api" "github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/server" "github.com/pterodactyl/wings/server"
@@ -28,14 +28,14 @@ func Initialize(config config.SystemConfiguration) error {
} }
if err := New(s); err != nil { if err := New(s); err != nil {
return errors.WithStack(err) return errors.WithStackIf(err)
} }
// Initialize the SFTP server in a background thread since this is // Initialize the SFTP server in a background thread since this is
// a long running operation. // a long running operation.
go func(s *Server) { go func(s *Server) {
if err := s.Initialize(); err != nil { if err := s.Initialize(); err != nil {
log.WithField("subsystem", "sftp").WithField("error", errors.WithStack(err)).Error("failed to initialize SFTP subsystem") log.WithField("subsystem", "sftp").WithField("error", errors.WithStackIf(err)).Error("failed to initialize SFTP subsystem")
} }
}(s) }(s)
@@ -72,7 +72,7 @@ func validateCredentials(c api.SftpAuthRequest) (*api.SftpAuthResponse, error) {
f := log.Fields{"subsystem": "sftp", "username": c.User, "ip": c.IP} f := log.Fields{"subsystem": "sftp", "username": c.User, "ip": c.IP}
log.WithFields(f).Debug("validating credentials for SFTP connection") log.WithFields(f).Debug("validating credentials for SFTP connection")
resp, err := api.NewRequester().ValidateSftpCredentials(c) resp, err := api.New().ValidateSftpCredentials(c)
if err != nil { if err != nil {
if api.IsInvalidCredentialsError(err) { if api.IsInvalidCredentialsError(err) {
log.WithFields(f).Warn("failed to validate user credentials (invalid username or password)") log.WithFields(f).Warn("failed to validate user credentials (invalid username or password)")

View File

@@ -1,20 +0,0 @@
package system
import "sync/atomic"
type AtomicBool struct {
flag uint32
}
func (ab *AtomicBool) Set(v bool) {
i := 0
if v {
i = 1
}
atomic.StoreUint32(&ab.flag, uint32(i))
}
func (ab *AtomicBool) Get() bool {
return atomic.LoadUint32(&ab.flag) == 1
}

58
system/utils.go Normal file
View File

@@ -0,0 +1,58 @@
package system
import (
"sync"
"sync/atomic"
)
type AtomicBool struct {
flag uint32
}
func (ab *AtomicBool) Set(v bool) {
i := 0
if v {
i = 1
}
atomic.StoreUint32(&ab.flag, uint32(i))
}
func (ab *AtomicBool) Get() bool {
return atomic.LoadUint32(&ab.flag) == 1
}
// AtomicString allows for reading/writing to a given struct field without having to worry
// about a potential race condition scenario. Under the hood it uses a simple sync.RWMutex
// to control access to the value.
type AtomicString struct {
v string
mu sync.RWMutex
}
func NewAtomicString(v string) *AtomicString {
return &AtomicString{v: v}
}
// Stores the string value passed atomically.
func (as *AtomicString) Store(v string) {
as.mu.Lock()
as.v = v
as.mu.Unlock()
}
// Loads the string value and returns it.
func (as *AtomicString) Load() string {
as.mu.RLock()
defer as.mu.RUnlock()
return as.v
}
func (as *AtomicString) UnmarshalText(b []byte) error {
as.Store(string(b))
return nil
}
func (as *AtomicString) MarshalText() ([]byte, error) {
return []byte(as.Load()), nil
}