Compare commits
126 Commits
v1.0.0-rc.
...
v1.1.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04d714f19c | ||
|
|
a18f60bd05 | ||
|
|
deea5babbc | ||
|
|
e41b3dc09a | ||
|
|
6366794838 | ||
|
|
c01a39d881 | ||
|
|
3f2ce59766 | ||
|
|
bcf0c72e47 | ||
|
|
bd5892b70c | ||
|
|
70ea61f22f | ||
|
|
1b0c2e1764 | ||
|
|
3d532f6e0b | ||
|
|
81fd1a3758 | ||
|
|
b52c3fb61e | ||
|
|
bc3d92f9e6 | ||
|
|
ee08829a28 | ||
|
|
83f0d2c953 | ||
|
|
605be3ebad | ||
|
|
ca6dc2c964 | ||
|
|
dc41126e25 | ||
|
|
da4c542724 | ||
|
|
e7d93a5248 | ||
|
|
51aa4c73cd | ||
|
|
4a7510d36f | ||
|
|
ba0a1a651e | ||
|
|
068f41393d | ||
|
|
169e8b8f8b | ||
|
|
c6e2889075 | ||
|
|
f62f714863 | ||
|
|
da9ace5d9d | ||
|
|
97345123ce | ||
|
|
8aa9105ed3 | ||
|
|
e8088f85d0 | ||
|
|
0c8476c79b | ||
|
|
a7a66e8bc0 | ||
|
|
65a861a9b6 | ||
|
|
de51fd1c51 | ||
|
|
40c70673cd | ||
|
|
73b221d022 | ||
|
|
287b286940 | ||
|
|
1d0e85cf55 | ||
|
|
30ec6dc78d | ||
|
|
121a4d1146 | ||
|
|
b9be373671 | ||
|
|
aedd0e406c | ||
|
|
82b23ef638 | ||
|
|
d970ec35b7 | ||
|
|
e2872e786e | ||
|
|
f81e35d960 | ||
|
|
672fb860ea | ||
|
|
8081c83de4 | ||
|
|
f379d0e54a | ||
|
|
ffb6bd72ef | ||
|
|
488ef9de54 | ||
|
|
34349d4b48 | ||
|
|
2197d82957 | ||
|
|
20ece60a72 | ||
|
|
de0c67d066 | ||
|
|
68bdcb3cbc | ||
|
|
205c4d541e | ||
|
|
ef999a039c | ||
|
|
be9d1a3986 | ||
|
|
0989c78d4b | ||
|
|
1683675807 | ||
|
|
536f00a5e5 | ||
|
|
33e584b447 | ||
|
|
4b17ac4f1c | ||
|
|
944d381778 | ||
|
|
3fce1b98d5 | ||
|
|
a74be8f4eb | ||
|
|
af9ed4bff1 | ||
|
|
08d1efb475 | ||
|
|
65664b63e7 | ||
|
|
912d95de24 | ||
|
|
13c253780a | ||
|
|
fe572beada | ||
|
|
384b9a3c28 | ||
|
|
05cfb59e18 | ||
|
|
317e54acc5 | ||
|
|
5475cb02c1 | ||
|
|
1239b1c0ca | ||
|
|
b8598e90d4 | ||
|
|
fcccda2761 | ||
|
|
f67889c2ca | ||
|
|
b8766d3c82 | ||
|
|
ca3becfb55 | ||
|
|
41a67933eb | ||
|
|
334b3e8d10 | ||
|
|
c4703f5541 | ||
|
|
1f3394b82d | ||
|
|
bae63c4321 | ||
|
|
f99640a42d | ||
|
|
c73d632e0d | ||
|
|
903902e123 | ||
|
|
1c787b5f26 | ||
|
|
3f9ac8b89a | ||
|
|
560c832cc6 | ||
|
|
13058ad64b | ||
|
|
305cd512a7 | ||
|
|
3cd17a2856 | ||
|
|
56789196d4 | ||
|
|
70804dd20f | ||
|
|
19d821aab5 | ||
|
|
4ce35d3d17 | ||
|
|
a62b588ace | ||
|
|
9b54be06bb | ||
|
|
c031d37b91 | ||
|
|
19051c99ef | ||
|
|
99fd416cee | ||
|
|
acf09180f0 | ||
|
|
b19fc88a95 | ||
|
|
e185f597ba | ||
|
|
3973747c9c | ||
|
|
947279a07c | ||
|
|
ad1ed0f24a | ||
|
|
80387bc294 | ||
|
|
e8dbba5cc0 | ||
|
|
f50f580dcc | ||
|
|
7e8033d96c | ||
|
|
1b94eadc68 | ||
|
|
3a496af9f1 | ||
|
|
7f4771068f | ||
|
|
3463c223e8 | ||
|
|
6c291d9b0e | ||
|
|
e29789114c | ||
|
|
4c9aaeb769 |
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
github: [DaneEveritt]
|
||||
custom: ["https://paypal.me/PterodactylSoftware"]
|
||||
67
.github/workflows/build-test.yml
vendored
67
.github/workflows/build-test.yml
vendored
@@ -1,33 +1,82 @@
|
||||
name: "Build & Test"
|
||||
|
||||
name: Run Tests
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'master'
|
||||
- 'release/**'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
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.6 ]
|
||||
goos: [ linux ]
|
||||
goarch: [ amd64, arm, arm64 ]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '1.15.2'
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: Print Environment
|
||||
id: env
|
||||
run: |
|
||||
printf "Go Executable Path: $(which go)\n"
|
||||
printf "Go Version: $(go version)\n"
|
||||
printf "\n\nGo Environment:\n\n"
|
||||
go env
|
||||
printf "\n\nSystem Environment:\n\n"
|
||||
env
|
||||
|
||||
echo "::set-output name=version_tag::${GITHUB_REF/refs\/tags\//}"
|
||||
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
|
||||
echo "::set-output name=go_cache::$(go env GOCACHE)"
|
||||
|
||||
- name: Build Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.env.outputs.go_cache }}
|
||||
key: ${{ runner.os }}-${{ matrix.go }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ matrix.go }}-go
|
||||
|
||||
- name: Get Dependencies
|
||||
run: |
|
||||
go get -v -t -d ./...
|
||||
|
||||
- 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 }}
|
||||
CGO_ENABLED: 0
|
||||
SRC_PATH: github.com/pterodactyl/wings
|
||||
run: |
|
||||
go build -v -trimpath -ldflags="-s -w -X ${SRC_PATH}/system.Version=dev-${GIT_COMMIT:0:7}" -o build/wings_${{ matrix.goos }}_${{ matrix.goarch }} wings.go
|
||||
|
||||
- name: Test
|
||||
run: go test ./...
|
||||
|
||||
- name: Compress binary and make it executable
|
||||
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
|
||||
if: ${{ github.ref == 'refs/heads/develop' || github.event_name == 'pull_request' }}
|
||||
with:
|
||||
name: wings_linux_amd64
|
||||
path: build/wings_linux_amd64
|
||||
name: wings_${{ matrix.goos }}_${{ matrix.goarch }}
|
||||
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 }}
|
||||
|
||||
11
.github/workflows/codeql-analysis.yml
vendored
11
.github/workflows/codeql-analysis.yml
vendored
@@ -1,16 +1,12 @@
|
||||
name: "Code scanning - action"
|
||||
|
||||
name: CodeQL Scanning
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'develop'
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '0 21 * * 6'
|
||||
|
||||
jobs:
|
||||
CodeQL-Build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
@@ -18,7 +14,6 @@ jobs:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
- run: git checkout HEAD^2
|
||||
|
||||
47
.github/workflows/docker.yml
vendored
Normal file
47
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: Publish Docker Image
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'develop'
|
||||
tags:
|
||||
- 'v*'
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Image to GitHub Packages
|
||||
runs-on: ubuntu-latest
|
||||
# Always run against a tag, even if the commit into the tag has [docker skip]
|
||||
# within the commit message.
|
||||
if: "!contains(github.ref, 'develop') || (!contains(github.event.head_commit.message, 'skip docker') && !contains(github.event.head_commit.message, 'docker skip'))"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: crazy-max/ghaction-docker-meta@v1
|
||||
id: docker_meta
|
||||
with:
|
||||
images: ghcr.io/pterodactyl/wings
|
||||
- uses: docker/setup-qemu-action@v1
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
- name: Release Production Build
|
||||
uses: docker/build-push-action@v2
|
||||
if: "!contains(github.ref, 'develop')"
|
||||
env:
|
||||
REF: ${{ github.ref }}
|
||||
with:
|
||||
push: true
|
||||
build-args: |
|
||||
VERSION=${REF:11}
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||
- name: Release Development Build
|
||||
uses: docker/build-push-action@v2
|
||||
if: "contains(github.ref, 'develop')"
|
||||
with:
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
build-args: |
|
||||
VERSION=dev-${GIT_COMMIT:0:7}
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||
52
.github/workflows/release.yml
vendored
52
.github/workflows/release.yml
vendored
@@ -1,15 +1,15 @@
|
||||
name: "Release"
|
||||
|
||||
name: Create Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '1.15.2'
|
||||
@@ -17,13 +17,19 @@ jobs:
|
||||
- name: Build
|
||||
env:
|
||||
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: |
|
||||
CGO_ENABLED=0 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
|
||||
CGO_ENABLED=0 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
|
||||
CGO_ENABLED=0 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
|
||||
run: go test ./...
|
||||
|
||||
- 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
|
||||
env:
|
||||
@@ -35,8 +41,10 @@ jobs:
|
||||
- name: Create checksum and add to changelog
|
||||
run: |
|
||||
SUM=`cd build && sha256sum wings_linux_amd64`
|
||||
echo -e "\n#### SHA256 Checksum\n\n\`\`\`\n$SUM\n\`\`\`\n" >> ./RELEASE_CHANGELOG
|
||||
echo $SUM > checksum.txt
|
||||
SUM2=`cd build && sha256sum wings_linux_arm64`
|
||||
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
|
||||
env:
|
||||
@@ -64,8 +72,7 @@ jobs:
|
||||
draft: true
|
||||
prerelease: ${{ contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
|
||||
|
||||
- name: Upload binary
|
||||
id: upload-release-binary
|
||||
- name: Upload amd64 Binary
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -75,13 +82,32 @@ jobs:
|
||||
asset_name: wings_linux_amd64
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload checksum
|
||||
id: upload-release-checksum
|
||||
- name: Upload arm64 Binary
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./checksum.txt
|
||||
asset_name: checksum.txt
|
||||
asset_path: build/wings_linux_arm64
|
||||
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
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -47,3 +47,5 @@ test_*/
|
||||
debug
|
||||
data/.states.json
|
||||
.DS_Store
|
||||
*.pprof
|
||||
*.pdf
|
||||
47
.travis.yml
47
.travis.yml
@@ -1,47 +0,0 @@
|
||||
os: linux
|
||||
dist: xenial
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.13.x
|
||||
|
||||
go_import_path: "github.com/pterodactyl/wings"
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
install:
|
||||
- mkdir -p $GOPATH/bin
|
||||
|
||||
# Install used tools
|
||||
- go get github.com/mitchellh/gox
|
||||
- go get github.com/haya14busa/goverage
|
||||
- go get github.com/schrej/godacov
|
||||
|
||||
- go mod download
|
||||
|
||||
script:
|
||||
- make cross-build
|
||||
- goverage -v -coverprofile=coverage.out ./...
|
||||
- godacov -t $CODACY_TOKEN -r ./coverage.out -c $TRAVIS_COMMIT
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
secure: HQ8AvnSsOW2aDUKv25sU83SswK9rReGeFi68SotLGPdWyFBWJbp/JEHhw9swSqvhLPykx5QqLnRPG4nomOp2i5dVTXgM/7C3wQ2ULymkJDZqDJEAxjm1IuNsjXgcFqp0zcNXL3g0moaorHS2XZpzbgaewlCyYoEb+3SZUGzOCPIjSFvoIBaAYx6kRn+pyWo1I0mQChno2i7SGvAoZwh/hZIO6L5FZe5PcpBs/SxkZ+/shsGMk7CIyNMhG6CQTE1tlr+ZenluXjtliZfc4XwkHG/9MICNl8ihUrnN6YfdvJZXLQvolZQ0QJ5Eyb04jQd1yzKR1hcLx2S42IAWxaWTy5QxSN8QyG5wBRNg567ib5FEqY4M1nyQnWQbUbaiYloYBp14aR1L9DQw8+xmXnlgnTUPq1w+cOpQLeY/RENCalgHe7NoI3lClC2b7/c1j+O7RA68yYUFUod0y7ZXcCwsJkbRk7xgyDEAGs+rq8wLknj6f8y8cfNm179lRARwblnmo9uA43Tlee8DBSziSvJy/mYMzdIQeb+PHuznXjr4fze7x+zvronkiD/JH8MjJl3SWaE7DGtc5jz4+aRxU3rMbHwToEOY6u5pIsvz5PRFYWBvKX2+VoxmdR+m1qhAxsg0wtbA0CTnqgHNGMIFDWVTDQSy8LvJt+usUn1RtrYyyiI=
|
||||
file_glob: true
|
||||
file: build/*
|
||||
on:
|
||||
tags: true
|
||||
branch: master
|
||||
|
||||
notifications:
|
||||
email: false
|
||||
webhooks:
|
||||
urls:
|
||||
- https://misc.schrej.net/travistodiscord/pterodev.php
|
||||
on_success: change
|
||||
on_failure: always
|
||||
on_error: always
|
||||
on_cancel: always
|
||||
on_start: never
|
||||
335
CHANGELOG.md
335
CHANGELOG.md
@@ -1,5 +1,307 @@
|
||||
# Changelog
|
||||
|
||||
## v1.1.3
|
||||
### Fixed
|
||||
* Fixes `archive/tar: write too long` error when creating a server backup.
|
||||
* Fixes server installation docker images not using authentication properly during the pull.
|
||||
* Fixes temporary transfer files not being removed after the transfer is completed.
|
||||
* Fixes TLS certificate checking to be all lowercase to avoid any lookup issues when an all-caps domain is provided.
|
||||
* Fixes multiple interfaces with the same port not being publishable for a server.
|
||||
* Fixes errors encountered during websocket processes being incorrectly passed back to the Panel as a JWT error rather than a generic Wings error for admin users.
|
||||
|
||||
### Added
|
||||
* Added logic to notify the Panel when archive generation fails.
|
||||
* Added endpoint to run `chmod` commands against server files and updated API response to include the mode bits when requesting files.
|
||||
|
||||
### Changed
|
||||
* Updated internals to call `Server.Environment.State()` rather than deprecated `Server.GetState()` functions.
|
||||
* Improved error handling logic and massively simplified error passing around the codebase.
|
||||
|
||||
## v1.1.2
|
||||
### Fixed
|
||||
* Fixes binaries built as part of the release process not being usable in MUSL based environments (such as our Docker images).
|
||||
* Fixes server states being incorrectly set back to offline when a server is started after a system restart.
|
||||
|
||||
### Changed
|
||||
* Improved logic for cleaning `allowed_mount` paths for consistency.
|
||||
* Certain context cancelation deadline errors are no longer wrong reported at an error level (since they're expected).
|
||||
* Very minor micro-optimizations for some string handling with server console output.
|
||||
|
||||
### Added
|
||||
* Added a hidden option to disable all disk checking for servers by setting the `disk_check_interval` to `0` in the config file.
|
||||
|
||||
## v1.1.1
|
||||
### Fixed
|
||||
* Fixes certain files returning invalid data in the request due to a bad header set after sending data down the line.
|
||||
|
||||
## 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
|
||||
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
|
||||
along though, I've only included the differences between this version and the previous RC build.
|
||||
|
||||
### Fixed
|
||||
* Fixes file parser not properly appending newline character to modified lines.
|
||||
* Fixes server disk usage not being properly reported back to the API and websocket.
|
||||
|
||||
### Changed
|
||||
* Changes diagnostics endpoint URL to be `ptero.co` rather than `hastebin.com`.
|
||||
* Diagnostics report now includes system time for easier debugging of logs and container issues.
|
||||
|
||||
## v1.0.0-rc.7
|
||||
### Fixed
|
||||
* **[Security]** Prevents the `allowed_mounts` configuration value from being set by a remote API call.
|
||||
* Fixes an unexpected error when attempting to make a copy of an archive file.
|
||||
* Fixes certain expected filesystems errors being returned to the API as a 500 error rather than a 4XX series error.
|
||||
* Fixes a panic crash when there is no text on a line and the parser is attempting to determine if the line is a comment.
|
||||
* Fixes multiple filesystem operations to correctly check, increment, and decrement disk usage for a server.
|
||||
* Fixes a negative disk space usage issue when deleting a file due to a bad int swap.
|
||||
* Errors from a websocket connection being closed will no longer spam the console.
|
||||
* Fixes an extraneous `.` in the install script for servers causing errors in some scenarios.
|
||||
* Fixes unexpected error spam due to a change in how `os.ErrNotExist` is returned from some functions.
|
||||
|
||||
### Changed
|
||||
* Stacktrace is no longer emitted when warning that an image exists locally.
|
||||
* Configuration parser now attempts to create the directory structure leading to a configuration file if it is missing.
|
||||
* If a file name is too long for the system a nice error is returned to the caller.
|
||||
* Enables client version negotiation for Docker to support more versions.
|
||||
* Disk space errors are no longer logged into the wings logs.
|
||||
* Servers can no longer be reinstalled while another power action is currently running. This avoids data collisions and wings running into issues with the container state.
|
||||
* Wings now uses `1024` rather than `1000` bytes when calculating the disk space usage for a server to match how the Panel reports usage.
|
||||
* JWT errors in the websocket are now sent back to the connection as a specific event type allowing them to be handled even if the timer fails to execute or executes but is not being listened for.
|
||||
* A server struct is no longer embedded in the virtual filesystem allowing for easier testing and modularization of the codebase.
|
||||
* Server websockets are now closed when a server is deleted, disconnecting any currently connected clients.
|
||||
|
||||
### Removed
|
||||
* **[Security]** Removes the `SafeJoin` function which could wrongly assume the state of a file and allow a user to escape the root if the developer implementing the call calls `Stat` rather than `Lstat`.
|
||||
|
||||
## v1.0.0-rc.6
|
||||
### Fixed
|
||||
* Fixes race condition when checking if the running state of a server has changed.
|
||||
* Fixes files mistakenly unpacking themselves to the root directory rather than the directory they are told to unpack to.
|
||||
* Fixes console output not being sent to the websocket in the same order that it was received.
|
||||
* Fixes a file busy error causing a 500 error response when unpacking an archive rather than a 400 error with a message indicating what is wrong to the user.
|
||||
* Fixes docker image not properly updating when a server is started.
|
||||
|
||||
### Changed
|
||||
* Replaces fragile event bus logic with a more robust and easier to understand system. This fixes all remaining console and stats output issues that have been reported.
|
||||
* Cleans up API response error messaging to avoid empty errors being logged which cannot be acted upon.
|
||||
* Adds support for retrying a file write a few times with a back-off if the file is busy when the write occurs.
|
||||
|
||||
### Added
|
||||
* Docker image pull data is shown in the console when an admin is connected to the websocket.
|
||||
* Adds console throttling to stop a server if too much data is being piped out of the console. This logic mimics the logic present in the old Nodejs daemon, but with a 2x line count limit (1000 -> 2000) per period.
|
||||
|
||||
## v1.0.0-rc.5
|
||||
### Fixed
|
||||
* Fixes long standing bug with console output not properly sending back to the client with server stats in certain edge case scenarios. This was "fixed" in `rc.4` but ended up breaking many servers in significantly more painful manners, but did allow us to better track down the issue.
|
||||
* Fixes build flags during release process to correctly strip unnessary code allowing the final binary size to be reduced back down to `~5MB`
|
||||
* Fixes Wings returning the last `16384` lines of the log file when connecting to the websocket.
|
||||
* Fixes pre-boot actions always running for a server start event even if the server is already running.
|
||||
|
||||
### Added
|
||||
* Adds support for configuring the amount of time that can elapse between server disk size checking before the value is considered stale.
|
||||
|
||||
## v1.0.0-rc.4
|
||||
### Fixed
|
||||
* Fixes server files being inaccessible if the root data path is a symlink to another location on the machine.
|
||||
* Fixes some console output being written to the logs accidentally truncating other lines due to special ANSI sequences being output.
|
||||
* Fixes `server.properties` files getting mangled by the automatic configuration editor when booting a server.
|
||||
* Fixes a missed stream close when stopping resource polling that would lead to memory leaks.
|
||||
* Fixes port bindings being incorrectly re-assigned when using `127.0.0.1` with Docker. These are now properly re-mapped to the `pterodactyl0` interface so that networking operates as expected for the server.
|
||||
* Fixes handling of values within arrays in `yaml` and `json` configuration files.
|
||||
* Fixes a 304 error being returned rather than a 400 error when a file upload is invalid.
|
||||
* Fixes deadlocks and response delays when processing server console and stat events. This was causing server consoles to become completely unresponsive after random periods of time and was only resolvable via a Wings restart.
|
||||
|
||||
### Changed
|
||||
* Allows a stale value to be present when starting a server if the server is allowed an unlimited amount of disk space.
|
||||
* Removed all remaining traces of the `zap` logging library from the codebase.
|
||||
* Servers no longer auto-restart as if they crashed when a stop command is manually sent via the server console to the instance.
|
||||
* Changes the CORS checks to allow `*` as a remote origin.
|
||||
|
||||
### Added
|
||||
* Added an auto-generated logrotate file which is written to the normal logrotate directory when wings is first started.
|
||||
* Added additional debug logging within the internals of the resource polling for servers to better trace unexpected behavior.
|
||||
* Adds additional logic check to avoid even trying to stop a suspended server if it is already stopped.
|
||||
|
||||
## v1.0.0-rc.3
|
||||
### Fixed
|
||||
* Errors during the backup process are now correctly reported to the Panel and logged into the output correctly.
|
||||
* Empty directories are no longer pushed into the backup file list (which was causing errors to occur previously).
|
||||
* Covered an edge case to prevent errors if a file gets deleted while a backup is in progress.
|
||||
* Fixed a bug causing Wings to panic and crash if an invalid environment variable value was passed through. These invalid values are now logged to the output to better detect and an empty string is returned in their place.
|
||||
* Fixed startup variables and other server information not properly being updated when a server is restarted.
|
||||
* Suspension state of a server is now properly returned by the API.
|
||||
* Fixes an error being thrown if a backup does not exist on the local machine. A 404 is now properly returned and handled by the Panel when this occurs.
|
||||
* Fixes an error when attempting to move or rename a folder due to the target being created accidentally before the rename occurred.
|
||||
* Fixes install scripts running even when the checkbox to not run them on installation was selected in the Panel.
|
||||
|
||||
### Changed
|
||||
* Disk space checking modified to not block in as many parts of the codebase and allow returning a stale cache value where that is appropriate. Thanks @cyberkitsune!
|
||||
* SFTP package code is now merged into the codebase to make keeping up with all of the code simpler, and reduce the complexity of the SFTP server.
|
||||
|
||||
### Added
|
||||
* Added the ability for a node to be configured to skip file permissions checking when starting a server.
|
||||
* Added console output message to indicate that server disk space checking is occurring rather than silently sitting there.
|
||||
|
||||
## v1.0.0-rc.2
|
||||
### Fixed
|
||||
* Fixes significant performance degradation due to excessive `syscall` actions when determining directory sizes on large servers. This was previously causing CPU & I/O lockups on servers and should be significantly more performant and less impactful on the system now.
|
||||
* Fixes panic crash when booting wings with no log directory created.
|
||||
|
||||
### Changed
|
||||
* Changed default interval for disk space calculation from every 60 seconds to every 2.5 minutes.
|
||||
|
||||
## v1.0.0-rc.1
|
||||
### Fixed
|
||||
* Servers are no longer incorrectly marked as stopping when they are, in fact, offline.
|
||||
* Release build version is now correctly output when starting wings.
|
||||
* Termination signals can now always be sent to a server instance even if the instance is currently starting/stopping.
|
||||
* Removed the file chown on wings boot to avoid slowing down the boot process unnecessarily when working with hundreds of servers on a node.
|
||||
* Fixed a multitude of race conditions throughout the code that cropped up during testing and made power handling even more robust for server instances.
|
||||
* In general multiple fragile areas of the codebase have been improved and are more likely to return useful errors should they break.
|
||||
* Addressed serious CPU usage issues when generating backups, and additionally decreased the amount of time they take to generate.
|
||||
|
||||
### Changed
|
||||
* Internal server now uses more secure and recommended TLS settings.
|
||||
* Environment handling is now completely separate from the server package itself, allowing the environments to no longer be tightly coupled to the server.
|
||||
* `/tmp` directory mounted into containers can now be programmatically managed and uses better defaults to avoid people even needing to edit it.
|
||||
|
||||
### Added
|
||||
* Wings logs are now properly persisted to the disk.
|
||||
* Adds the ability for an egg to use ANSI-stripped matching when determining if a server is done booting.
|
||||
|
||||
## v1.0.0-beta.9
|
||||
### Fixed
|
||||
* Fixes server resource usage stats not being returned correctly by the API endpoint.
|
||||
* Fixes an exception thrown when attempting to write server installation logs.
|
||||
* Fixes error handling to provide a more accurate stack-trace in more scenarios where one is missing initially.
|
||||
* Fixes a memory leak and zombie event listeners when disconnecting from a server's websocket.
|
||||
* Fixes a race condition when wings is attempting to register/de-register event subscribers.
|
||||
* Server data directories now correctly have their permissions set recursively when booting Wings.
|
||||
* Fixes a race condition when a server's console stream was not fully closed before the next power action was started.
|
||||
|
||||
### Changed
|
||||
* Server power handling is now handled in a synchronous manner. This avoids endless bugs and race conditions that would crop up if someone triggered two restart processes back to back. The new logic prevents performing any additional power actions until the currently executing action is completed.
|
||||
* Server disk usage is now correctly calculated when restarting the daemon as long as the server data directory exists.
|
||||
* Multiple code pathways within the boot process were cleaned up and modified to be less IO intensive and overall easier to reason about as a developer.
|
||||
* Additional timezone data is now mounted into containers to better improve the ability for instances running to be using the correct timezone.
|
||||
|
||||
### Added
|
||||
* Adds basic internal file upload endpoints (these are currently not exposed anywhere in the Panel).
|
||||
* Added additional process events for installation start and completion.
|
||||
* Additional CORS allowed origins for the websocket can now be defined in the configuration file.
|
||||
* Adds the ability to authenticate with a docker registry when pulling images.
|
||||
|
||||
## v1.0.0-beta.8
|
||||
### Fixed
|
||||
* Server state is synced with the Panel before performing a reinstall to ensure that the latest information is used.
|
||||
* Wings no longer crashes when a non-string environment variable value is passed through.
|
||||
* SFTP server authentication no longer attempts to contact the Panel to validate credentials if the format is known to already be incorrect.
|
||||
* Some previously missing error stacks are now properly returned when encountered.
|
||||
* Renaming a file no longer triggers an error if the base path does not exist.
|
||||
* Disk space is now properly calculated for a server even if it is assigned unlimited space.
|
||||
* **[Security]** Prevent symlink files from unintentionally chmodding their target file when a server is booted when the target file resides outside the server data directory.
|
||||
* **[Security]** Cleans up multiple code pathways that may unintentionally allow a malicious user to impact files outside their home directory.
|
||||
* Fixes numerous race conditions during a server's boot process that may lead to unintended data states.
|
||||
* Fixes an error thrown when attempting to delete a file that points to a symlink outside the server data directory.
|
||||
* Deleting a symlink will no longer attempt to remove the source file, only the symlink itself.
|
||||
* Websocket no longer blocks when handling a long running process for a user.
|
||||
|
||||
### Changed
|
||||
* Install log for servers is now more detailed and more useful for debugging what might be going wrong.
|
||||
* Certain file management API endpoints now support passing through multiple paths at once to make mass actions easier to perform.
|
||||
* Re-worked file walking implementation to not cause a server crash when working with very large directories, and avoid race conditions when recursively walking directories.
|
||||
* Server configuration structure re-worked to be more manageable in the code base and avoid additional race conditions and complexity while making on-the-fly changes to it.
|
||||
|
||||
### Added
|
||||
* Support for configuring additional file mount points in a container via the Panel.
|
||||
* Support for automated SSL certificate generation when booting the Daemon.
|
||||
* Added wings diagnostics command.
|
||||
* New API endpoints to compress and decompress files on a server.
|
||||
|
||||
## v1.0.0-beta.7
|
||||
### Fixed
|
||||
* Stacktraces are now displayed once in the error output, rather than twice when certain errors are encountered.
|
||||
* Additional errors that did not previously output a stack trace have been fixed to output.
|
||||
* Fixes a bug where server space available would be triggered when creating a new server from a remote configuration before that file location existed on the disk, leading to an error.
|
||||
* Fixes context timeouts while pulling server docker images. Time was upped from 10 seconds to 15 minutes.
|
||||
* Configuration file replacement values are now properly de-escaped when writing to the disk. `\/no\/more\/slashes`
|
||||
* `.properties` files are now saved correctly to the disk with newlines, rather than shoved on a single line.
|
||||
* `./wings configure` command can now properly save the configuration to the disk.
|
||||
* Custom SSL locations are no longer obliterated when making changes to the Node's configuration via the Panel.
|
||||
|
||||
### Changed
|
||||
* Exclusive lock is now acquired when performing a server installation to avoid two install processes being triggered at the same time. This also allows an install to be properly canceled if the server is deleted before it is completed.
|
||||
|
||||
## v1.0.0-beta.6
|
||||
### Fixed
|
||||
* Server status is no longer sent to all connected websocket clients when a new client connects to the socket.
|
||||
* Server disk usage is sent back over the socket when connecting.
|
||||
* Default configuration for the SFTP server is now properly returned to `on` rather than `off`
|
||||
* Server boot is no longer blocked if there is an error fetching the Docker image as long as that image exists on the host.
|
||||
* The websocket no longer gets locked up when Wings attempts to send an error to the client.
|
||||
* Fixed a crash loop when an error is thrown during pre-boot phase of server startup.
|
||||
* Errors with `BindJSON` in API endpoints are now properly handled and returned.
|
||||
* Fixed warning about Gin running in non-release mode, even when the binary is running in release mode.
|
||||
|
||||
### Changed
|
||||
* Switched logging libraries to output data in a clearer format more suited to the CLI where this application runs.
|
||||
* Cleaner debug messaging in debug mode from the router.
|
||||
|
||||
## v1.0.0-beta.5
|
||||
### Fixed
|
||||
* Default config location settled on `/etc/pterodactyl/config.yml`; wings will now check all of the previous locations for the configuration and move it automatically to the new location.
|
||||
* Deleting a server no longer fails the process if the container cannot be found.
|
||||
* Fixes permissions checking for subusers connecting to the SFTP instance.
|
||||
* S3 backups now properly send back hash data to the panel.
|
||||
* Server installation containers are now always deleted, even if the installer process fails for the instance.
|
||||
* Files and folders with special characters and spaces in them no longer return a 404 error.
|
||||
* Servers using eggs with bad configurations will no longer cause the daemon to fail booting; these bad configurations are simply skipped over and a warning is emitted into the logs.
|
||||
* Environment variables passed to containers no longer contain improper quotes around them.
|
||||
* Matching on array indexes in configurations now works correctly; `foo[0]` is quietly transformed into the proper `foo.0` syntax.
|
||||
|
||||
### Added
|
||||
* New banner error message when the daemon is unable to locate the configuration file. This should better clarify what the user needs to do in order to resolve the issue.
|
||||
* Adds ability to configure the default networking driver used by docker.
|
||||
|
||||
## v1.0.0-beta.4
|
||||
### Fixed
|
||||
* Fixes unexpected nil-pointer panic when attempting to start some newly created servers, or any server that was missing a container on the system.
|
||||
* Fixes memory usage of process being reported differently than the `docker stats` output leading to some confusion. These numbers should be more correct now.
|
||||
* Fixes possible nil-pointer panic when detecting a deleted container as being crashed.
|
||||
|
||||
## v1.0.0-beta.3
|
||||
### Fixed
|
||||
* Daemon will no longer crash if someone requests a websocket for a deleted server.
|
||||
@@ -11,6 +313,39 @@
|
||||
### Changed
|
||||
* Memory overhead for containers is now 5/10/15% higher than the passed limit to account for JVM heap and prevent crashing.
|
||||
|
||||
## v1.0.0-beta.2
|
||||
### Changed
|
||||
* Backup functionality made significantly more modular to ease adding additional methods in the future.
|
||||
* Websocket permissions changed to use same name as in panel.
|
||||
* Container memory hard-limits are now adjusted up by 15% (< 2G memory), 10% (< 4G memory), or 5% to avoid unexpected OOM crashes for memory heavy games.
|
||||
* Wings executable is now 80% smaller thanks to better compilation arguments.
|
||||
|
||||
### Added
|
||||
* Adds support for ignoring files and directories when generating a new backup.
|
||||
* Added internal directory walker with callback-continue support.
|
||||
|
||||
### Fixed
|
||||
* Fixed linux distro matching when booting the daemon.
|
||||
* Fixes DNS to be configurable for docker containers that are made for servers.
|
||||
* Fixes incorrect file truncation when making modifications to a server's configuration files.
|
||||
|
||||
## v1.0.0-beta.1
|
||||
### Added
|
||||
* Added support for passing specific threads to the docker environment when running a server.
|
||||
* Added support for reinstalling an existing server.
|
||||
* Added support for restarting a server.
|
||||
* Adds support for transferring servers between daemon instances.
|
||||
* Added auto-deploy command to fetch configuration from Panel automatically.
|
||||
|
||||
### Changed
|
||||
* Server file and backup downloads now hit a direct endpoint with a one-time JWT to avoid proxying large downloads through the panel.
|
||||
|
||||
### Fixed
|
||||
* Fixes a go routine causing a memory and CPU leak.
|
||||
* Fixed improper chown of server directories on boot.
|
||||
* Miscellaneous fixes to CPU usage, abandoned go-routines, and race conditions.
|
||||
* Fixes support for Alpine based systems.
|
||||
|
||||
## v1.0.0-alpha.2
|
||||
### Added
|
||||
* Ability to run an installation process for a server and notify the panel when completed.
|
||||
|
||||
11
Dockerfile
11
Dockerfile
@@ -1,14 +1,11 @@
|
||||
# ----------------------------------
|
||||
# Pterodactyl Panel Dockerfile
|
||||
# ----------------------------------
|
||||
|
||||
FROM golang:1.15-alpine
|
||||
ARG VERSION="develop"
|
||||
COPY . /go/wings/
|
||||
WORKDIR /go/wings/
|
||||
RUN apk add --no-cache upx \
|
||||
&& go build -ldflags="-s -w" \
|
||||
&& upx --brute wings
|
||||
&& CGO_ENABLED=0 go build -ldflags="-s -w -X github.com/pterodactyl/wings/system.Version=${VERSION}" \
|
||||
&& upx wings
|
||||
|
||||
FROM alpine:latest
|
||||
COPY --from=0 /go/wings/wings /usr/bin/
|
||||
CMD ["wings","--config", "/etc/pterodactyl/config.yml"]
|
||||
CMD ["wings", "--config", "/etc/pterodactyl/config.yml"]
|
||||
3
Makefile
3
Makefile
@@ -1,5 +1,6 @@
|
||||
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:
|
||||
upx --brute build/wings_*
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[](https://pterodactyl.io)
|
||||
|
||||
[](https://pterodactyl.io/discord)
|
||||

|
||||

|
||||
[](https://goreportcard.com/report/github.com/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 |
|
||||
| ------- | ----- |
|
||||
| [**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. |
|
||||
| [**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. |
|
||||
| [**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! |
|
||||
| [**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**](https://xcore-server.de/) | XCORE 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 RoyaleHosting’s 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
|
||||
* [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html)
|
||||
|
||||
177
api/api.go
177
api/api.go
@@ -7,6 +7,8 @@ import (
|
||||
"github.com/apex/log"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/system"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -14,30 +16,47 @@ import (
|
||||
)
|
||||
|
||||
// Initializes the requester instance.
|
||||
func NewRequester() *PanelRequest {
|
||||
return &PanelRequest{
|
||||
Response: nil,
|
||||
}
|
||||
func New() *Request {
|
||||
return &Request{}
|
||||
}
|
||||
|
||||
type PanelRequest struct {
|
||||
Response *http.Response
|
||||
// A generic type allowing for easy binding use when making requests to API endpoints
|
||||
// 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.
|
||||
func (r *PanelRequest) GetClient() *http.Client {
|
||||
return &http.Client{Timeout: time.Second * 30}
|
||||
func (r *Request) Client() *http.Client {
|
||||
return &http.Client{Timeout: time.Second * time.Duration(config.Get().RemoteQuery.Timeout)}
|
||||
}
|
||||
|
||||
func (r *PanelRequest) SetHeaders(req *http.Request) *http.Request {
|
||||
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))
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func (r *PanelRequest) GetEndpoint(endpoint string) string {
|
||||
// Returns the given endpoint formatted as a URL to the Panel API.
|
||||
func (r *Request) Endpoint(endpoint string) string {
|
||||
return fmt.Sprintf(
|
||||
"%s/api/remote/%s",
|
||||
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, 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.
|
||||
// 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)
|
||||
for k, v := range req.Header {
|
||||
if k != "Authorization" || len(v) == 0 {
|
||||
@@ -65,49 +110,44 @@ func (r *PanelRequest) logDebug(req *http.Request) {
|
||||
}).Debug("making request to external HTTP endpoint")
|
||||
}
|
||||
|
||||
func (r *PanelRequest) Get(url string) (*http.Response, error) {
|
||||
c := r.GetClient()
|
||||
// Makes a GET request to the given Panel API endpoint. If any data is passed as the
|
||||
// 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)
|
||||
req = r.SetHeaders(req)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.logDebug(req)
|
||||
|
||||
return c.Do(req)
|
||||
r.URL.RawQuery = q.Encode()
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// 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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.logDebug(req)
|
||||
|
||||
return c.Do(req)
|
||||
return r.Make(http.MethodPost, r.Endpoint(url), bytes.NewBuffer(b))
|
||||
}
|
||||
|
||||
// Determines if the API call encountered an error. If no request has been made
|
||||
// the response will be false.
|
||||
func (r *PanelRequest) HasError() bool {
|
||||
// the response will be false. This function will evaluate to true if the response
|
||||
// code is anything 300 or higher.
|
||||
func (r *Response) HasError() bool {
|
||||
if r.Response == nil {
|
||||
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
|
||||
// so that it can be read again later.
|
||||
func (r *PanelRequest) ReadBody() ([]byte, error) {
|
||||
// so that it can be read again later. This does not close the response body, so any
|
||||
// functions calling this should be sure to manually defer a Body.Close() call.
|
||||
func (r *Response) Read() ([]byte, error) {
|
||||
var b []byte
|
||||
if r.Response == nil {
|
||||
return nil, errors.New("no response exists on interface")
|
||||
@@ -122,51 +162,30 @@ func (r *PanelRequest) ReadBody() ([]byte, error) {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (r *PanelRequest) HttpResponseCode() int {
|
||||
if r.Response == nil {
|
||||
return 0
|
||||
// Binds a given interface with the data returned in the response. This is a shortcut
|
||||
// for calling Read and then manually calling json.Unmarshal on the raw bytes.
|
||||
func (r *Response) Bind(v interface{}) error {
|
||||
b, err := r.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.Response.StatusCode
|
||||
}
|
||||
|
||||
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"`
|
||||
return json.Unmarshal(b, &v)
|
||||
}
|
||||
|
||||
// Returns the error message from the API call as a string. The error message will be formatted
|
||||
// similar to the below example:
|
||||
//
|
||||
// HttpNotFoundException: The requested resource does not exist. (HTTP/404)
|
||||
func (r *PanelRequest) Error() *RequestError {
|
||||
body, _ := r.ReadBody()
|
||||
func (r *Response) Error() error {
|
||||
if !r.HasError() {
|
||||
return nil
|
||||
}
|
||||
|
||||
bag := RequestErrorBag{}
|
||||
json.Unmarshal(body, &bag)
|
||||
var bag RequestErrorBag
|
||||
_ = r.Bind(&bag)
|
||||
|
||||
e := new(RequestError)
|
||||
e := &RequestError{}
|
||||
if len(bag.Errors) > 0 {
|
||||
e = &bag.Errors[0]
|
||||
}
|
||||
|
||||
@@ -1,12 +1,47 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// backupUploadIDs stores a cache of active S3 backups.
|
||||
var backupUploadIDs = cache.New(time.Hour*3, time.Minute*5)
|
||||
|
||||
type BackupRemoteUploadResponse struct {
|
||||
UploadID string `json:"upload_id"`
|
||||
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, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.HasError() {
|
||||
return nil, resp.Error()
|
||||
}
|
||||
|
||||
var res BackupRemoteUploadResponse
|
||||
if err := resp.Bind(&res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Store the backup upload id for later use, this is a janky way to be able to use it later with SendBackupStatus.
|
||||
// Yes, the timeout of 3 hours is intentional, if this value is removed before the backup completes,
|
||||
// the backup will fail even if it uploaded properly.
|
||||
backupUploadIDs.Set(backup, res.UploadID, 0)
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
type BackupRequest struct {
|
||||
UploadID string `json:"upload_id"`
|
||||
Checksum string `json:"checksum"`
|
||||
ChecksumType string `json:"checksum_type"`
|
||||
Size int64 `json:"size"`
|
||||
@@ -15,22 +50,17 @@ type BackupRequest struct {
|
||||
|
||||
// Notifies the panel that a specific backup has been completed and is now
|
||||
// available for a user to view and download.
|
||||
func (r *PanelRequest) SendBackupStatus(backup string, data BackupRequest) (*RequestError, error) {
|
||||
b, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
func (r *Request) SendBackupStatus(backup string, data BackupRequest) error {
|
||||
// Set the UploadID on the data.
|
||||
if v, ok := backupUploadIDs.Get(backup); ok {
|
||||
data.UploadID = v.(string)
|
||||
}
|
||||
|
||||
resp, err := r.Post(fmt.Sprintf("/backups/%s", backup), b)
|
||||
resp, err := r.Post(fmt.Sprintf("/backups/%s", backup), data)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
r.Response = resp
|
||||
if r.HasError() {
|
||||
return r.Error(), nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
return resp.Error()
|
||||
}
|
||||
|
||||
33
api/error.go
Normal file
33
api/error.go
Normal 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)
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/apex/log"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"strconv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -33,157 +38,173 @@ type InstallationScript struct {
|
||||
Script string `json:"script"`
|
||||
}
|
||||
|
||||
// GetAllServerConfigurations fetches configurations for all servers assigned to this node.
|
||||
func (r *PanelRequest) GetAllServerConfigurations() (map[string]*ServerConfigurationResponse, *RequestError, error) {
|
||||
resp, err := r.Get("/servers")
|
||||
type allServerResponse struct {
|
||||
Data []RawServerData `json:"data"`
|
||||
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 {
|
||||
return nil, nil, errors.WithStack(err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
r.Response = resp
|
||||
|
||||
if r.HasError() {
|
||||
return nil, r.Error(), nil
|
||||
if resp.HasError() {
|
||||
return nil, resp.Error()
|
||||
}
|
||||
|
||||
b, _ := r.ReadBody()
|
||||
res := map[string]*ServerConfigurationResponse{}
|
||||
if len(b) == 2 {
|
||||
return res, nil, nil
|
||||
var res allServerResponse
|
||||
if err := resp.Bind(&res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(b, &res); err != nil {
|
||||
return nil, nil, errors.WithStack(err)
|
||||
var mu sync.Mutex
|
||||
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()
|
||||
}
|
||||
|
||||
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, err
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil, nil
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Fetches the server configuration and returns the struct for it.
|
||||
func (r *PanelRequest) GetServerConfiguration(uuid string) (*ServerConfigurationResponse, *RequestError, error) {
|
||||
resp, err := r.Get(fmt.Sprintf("/servers/%s", uuid))
|
||||
func (r *Request) GetServerConfiguration(uuid string) (ServerConfigurationResponse, error) {
|
||||
var cfg ServerConfigurationResponse
|
||||
|
||||
resp, err := r.Get(fmt.Sprintf("/servers/%s", uuid), nil)
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithStack(err)
|
||||
return cfg, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
r.Response = resp
|
||||
|
||||
if r.HasError() {
|
||||
return nil, r.Error(), nil
|
||||
if resp.HasError() {
|
||||
return cfg, resp.Error()
|
||||
}
|
||||
|
||||
res := &ServerConfigurationResponse{}
|
||||
b, _ := r.ReadBody()
|
||||
|
||||
if err := json.Unmarshal(b, res); err != nil {
|
||||
return nil, nil, errors.WithStack(err)
|
||||
if err := resp.Bind(&cfg); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
return res, nil, nil
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// Fetches installation information for the server process.
|
||||
func (r *PanelRequest) GetInstallationScript(uuid string) (InstallationScript, *RequestError, error) {
|
||||
res := InstallationScript{}
|
||||
|
||||
resp, err := r.Get(fmt.Sprintf("/servers/%s/install", uuid))
|
||||
func (r *Request) GetInstallationScript(uuid string) (InstallationScript, error) {
|
||||
var is InstallationScript
|
||||
resp, err := r.Get(fmt.Sprintf("/servers/%s/install", uuid), nil)
|
||||
if err != nil {
|
||||
return res, nil, errors.WithStack(err)
|
||||
return is, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
r.Response = resp
|
||||
|
||||
if r.HasError() {
|
||||
return res, r.Error(), nil
|
||||
if resp.HasError() {
|
||||
return is, resp.Error()
|
||||
}
|
||||
|
||||
b, _ := r.ReadBody()
|
||||
|
||||
if err := json.Unmarshal(b, &res); err != nil {
|
||||
return res, nil, errors.WithStack(err)
|
||||
if err := resp.Bind(&is); err != nil {
|
||||
return is, err
|
||||
}
|
||||
|
||||
return res, nil, nil
|
||||
}
|
||||
|
||||
type installRequest struct {
|
||||
Successful bool `json:"successful"`
|
||||
return is, nil
|
||||
}
|
||||
|
||||
// Marks a server as being installed successfully or unsuccessfully on the panel.
|
||||
func (r *PanelRequest) SendInstallationStatus(uuid string, successful bool) (*RequestError, error) {
|
||||
b, err := json.Marshal(installRequest{Successful: successful})
|
||||
func (r *Request) SendInstallationStatus(uuid string, successful bool) error {
|
||||
resp, err := r.Post(fmt.Sprintf("/servers/%s/install", uuid), D{"successful": successful})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
resp, err := r.Post(fmt.Sprintf("/servers/%s/install", uuid), b)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
r.Response = resp
|
||||
if r.HasError() {
|
||||
return r.Error(), nil
|
||||
if resp.HasError() {
|
||||
return resp.Error()
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
type archiveRequest struct {
|
||||
Successful bool `json:"successful"`
|
||||
}
|
||||
|
||||
func (r *PanelRequest) SendArchiveStatus(uuid string, successful bool) (*RequestError, error) {
|
||||
b, err := json.Marshal(archiveRequest{Successful: successful})
|
||||
func (r *Request) SendArchiveStatus(uuid string, successful bool) error {
|
||||
resp, err := r.Post(fmt.Sprintf("/servers/%s/archive", uuid), D{"successful": successful})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
resp, err := r.Post(fmt.Sprintf("/servers/%s/archive", uuid), b)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
r.Response = resp
|
||||
if r.HasError() {
|
||||
return r.Error(), nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
return resp.Error()
|
||||
}
|
||||
|
||||
func (r *PanelRequest) SendTransferFailure(uuid string) (*RequestError, error) {
|
||||
resp, err := r.Get(fmt.Sprintf("/servers/%s/transfer/failure", uuid))
|
||||
func (r *Request) SendTransferFailure(uuid string) error {
|
||||
resp, err := r.Get(fmt.Sprintf("/servers/%s/transfer/failure", uuid), nil)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
r.Response = resp
|
||||
if r.HasError() {
|
||||
return r.Error(), nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
return resp.Error()
|
||||
}
|
||||
|
||||
func (r *PanelRequest) SendTransferSuccess(uuid string) (*RequestError, error) {
|
||||
resp, err := r.Get(fmt.Sprintf("/servers/%s/transfer/success", uuid))
|
||||
func (r *Request) SendTransferSuccess(uuid string) error {
|
||||
resp, err := r.Get(fmt.Sprintf("/servers/%s/transfer/success", uuid), nil)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
r.Response = resp
|
||||
if r.HasError() {
|
||||
return r.Error(), nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
return resp.Error()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/apex/log"
|
||||
"github.com/pkg/errors"
|
||||
"regexp"
|
||||
@@ -39,7 +38,7 @@ func IsInvalidCredentialsError(err error) bool {
|
||||
// server and sending a flood of usernames.
|
||||
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
|
||||
// and bail out of the process here to avoid accidentally brute forcing the panel if a bot decides
|
||||
// to connect to spam username attempts.
|
||||
@@ -53,41 +52,33 @@ func (r *PanelRequest) ValidateSftpCredentials(request SftpAuthRequest) (*SftpAu
|
||||
return nil, new(sftpInvalidCredentialsError)
|
||||
}
|
||||
|
||||
b, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := r.Post("/sftp/auth", b)
|
||||
resp, err := r.Post("/sftp/auth", request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
r.Response = resp
|
||||
|
||||
if r.HasError() {
|
||||
if r.HttpResponseCode() >= 400 && r.HttpResponseCode() < 500 {
|
||||
e := resp.Error()
|
||||
if e != nil {
|
||||
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
|
||||
log.WithFields(log.Fields{
|
||||
"subsystem": "sftp",
|
||||
"username": request.User,
|
||||
"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
|
||||
}
|
||||
|
||||
response := new(SftpAuthResponse)
|
||||
body, _ := r.ReadBody()
|
||||
|
||||
if err := json.Unmarshal(body, response); err != nil {
|
||||
var response SftpAuthResponse
|
||||
if err := resp.Bind(&response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
@@ -13,20 +14,20 @@ import (
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
"github.com/docker/cli/components/engine/pkg/parsers/operatingsystem"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/parsers/kernel"
|
||||
"github.com/docker/docker/pkg/parsers/operatingsystem"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/system"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const DefaultHastebinUrl = "https://hastebin.com"
|
||||
const DefaultLogLines = 50
|
||||
const DefaultHastebinUrl = "https://ptero.co"
|
||||
const DefaultLogLines = 200
|
||||
|
||||
var (
|
||||
diagnosticsArgs struct {
|
||||
@@ -88,37 +89,39 @@ func diagnosticsCmdRun(cmd *cobra.Command, args []string) {
|
||||
output := &strings.Builder{}
|
||||
fmt.Fprintln(output, "Pterodactyl Wings - Diagnostics Report")
|
||||
printHeader(output, "Versions")
|
||||
fmt.Fprintln(output, "wings:", system.Version)
|
||||
fmt.Fprintln(output, " wings:", system.Version)
|
||||
if dockerErr == nil {
|
||||
fmt.Fprintln(output, "Docker", dockerVersion.Version)
|
||||
fmt.Fprintln(output, "Docker:", dockerVersion.Version)
|
||||
}
|
||||
if v, err := kernel.GetKernelVersion(); err == nil {
|
||||
fmt.Fprintln(output, "Kernel:", v)
|
||||
}
|
||||
if os, err := operatingsystem.GetOperatingSystem(); err == nil {
|
||||
fmt.Fprintln(output, "OS:", os)
|
||||
fmt.Fprintln(output, " OS:", os)
|
||||
}
|
||||
|
||||
printHeader(output, "Wings Configuration")
|
||||
cfg, err := config.ReadConfiguration(config.DefaultLocation)
|
||||
if cfg != nil {
|
||||
fmt.Fprintln(output, "Panel Location:", redact(cfg.PanelLocation))
|
||||
fmt.Fprintln(output, "Api Host:", redact(cfg.Api.Host))
|
||||
fmt.Fprintln(output, "Api Port:", cfg.Api.Port)
|
||||
fmt.Fprintln(output, "Api Ssl Enabled:", cfg.Api.Ssl.Enabled)
|
||||
fmt.Fprintln(output, "Api Ssl Certificate:", redact(cfg.Api.Ssl.CertificateFile))
|
||||
fmt.Fprintln(output, "Api Ssl Key:", redact(cfg.Api.Ssl.KeyFile))
|
||||
fmt.Fprintln(output, "Sftp Address:", redact(cfg.System.Sftp.Address))
|
||||
fmt.Fprintln(output, "Sftp Port:", cfg.System.Sftp.Port)
|
||||
fmt.Fprintln(output, "Sftp Read Only:", cfg.System.Sftp.ReadOnly)
|
||||
fmt.Fprintln(output, "Sftp Diskchecking Disabled:", cfg.System.Sftp.DisableDiskChecking)
|
||||
fmt.Fprintln(output, "System Root Directory:", cfg.System.RootDirectory)
|
||||
fmt.Fprintln(output, "System Logs Directory:", cfg.System.LogDirectory)
|
||||
fmt.Fprintln(output, "System Data Directory:", cfg.System.Data)
|
||||
fmt.Fprintln(output, "System Archive Directory:", cfg.System.ArchiveDirectory)
|
||||
fmt.Fprintln(output, "System Backup Directory:", cfg.System.BackupDirectory)
|
||||
fmt.Fprintln(output, "System Username:", cfg.System.Username)
|
||||
fmt.Fprintln(output, "Debug Enabled:", cfg.Debug)
|
||||
fmt.Fprintln(output, " Panel Location:", redact(cfg.PanelLocation))
|
||||
fmt.Fprintln(output, "")
|
||||
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 Certificate:", redact(cfg.Api.Ssl.CertificateFile))
|
||||
fmt.Fprintln(output, " SSL Key:", redact(cfg.Api.Ssl.KeyFile))
|
||||
fmt.Fprintln(output, "")
|
||||
fmt.Fprintln(output, " SFTP Server:", redact(cfg.System.Sftp.Address), ":", cfg.System.Sftp.Port)
|
||||
fmt.Fprintln(output, " SFTP Read-Only:", cfg.System.Sftp.ReadOnly)
|
||||
fmt.Fprintln(output, "")
|
||||
fmt.Fprintln(output, " Root Directory:", cfg.System.RootDirectory)
|
||||
fmt.Fprintln(output, " Logs Directory:", cfg.System.LogDirectory)
|
||||
fmt.Fprintln(output, " Data Directory:", cfg.System.Data)
|
||||
fmt.Fprintln(output, " Archive Directory:", cfg.System.ArchiveDirectory)
|
||||
fmt.Fprintln(output, " Backup Directory:", cfg.System.BackupDirectory)
|
||||
fmt.Fprintln(output, "")
|
||||
fmt.Fprintln(output, " Username:", cfg.System.Username)
|
||||
fmt.Fprintln(output, " Server Time:", time.Now().Format(time.RFC1123Z))
|
||||
fmt.Fprintln(output, " Debug Mode:", cfg.Debug)
|
||||
} else {
|
||||
fmt.Println("Failed to load configuration.", err)
|
||||
}
|
||||
@@ -137,7 +140,7 @@ func diagnosticsCmdRun(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(output, "LoggingDriver:", dockerInfo.LoggingDriver)
|
||||
fmt.Fprintln(output, "CgroupDriver:", dockerInfo.CgroupDriver)
|
||||
fmt.Fprintln(output, " CgroupDriver:", dockerInfo.CgroupDriver)
|
||||
if len(dockerInfo.Warnings) > 0 {
|
||||
for _, w := range dockerInfo.Warnings {
|
||||
fmt.Fprintln(output, w)
|
||||
@@ -184,7 +187,7 @@ func diagnosticsCmdRun(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
|
||||
func getDockerInfo() (types.Version, types.Info, error) {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||
cli, err := environment.DockerClient()
|
||||
if err != nil {
|
||||
return types.Version{}, types.Info{}, err
|
||||
}
|
||||
|
||||
98
cmd/root.go
98
cmd/root.go
@@ -30,12 +30,14 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var configPath = config.DefaultLocation
|
||||
var debug = false
|
||||
var shouldRunProfiler = false
|
||||
var useAutomaticTls = false
|
||||
var tlsHostname = ""
|
||||
var showVersion = false
|
||||
var (
|
||||
profiler = ""
|
||||
configPath = config.DefaultLocation
|
||||
debug = false
|
||||
useAutomaticTls = false
|
||||
tlsHostname = ""
|
||||
showVersion = false
|
||||
)
|
||||
|
||||
var root = &cobra.Command{
|
||||
Use: "wings",
|
||||
@@ -54,7 +56,7 @@ func init() {
|
||||
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().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().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 {
|
||||
return nil, errors.WithStack(err)
|
||||
return nil, err
|
||||
} else if s.IsDir() {
|
||||
return nil, errors.New("cannot use directory as configuration file path")
|
||||
}
|
||||
@@ -89,15 +91,30 @@ func rootCmdRun(*cobra.Command, []string) {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if shouldRunProfiler {
|
||||
defer profile.Start().Stop()
|
||||
switch profiler {
|
||||
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
|
||||
// been specified in the command startup.
|
||||
if configPath == config.DefaultLocation {
|
||||
if err := RelocateConfiguration(); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
exitWithConfigurationNotice()
|
||||
}
|
||||
|
||||
@@ -132,6 +149,13 @@ func rootCmdRun(*cobra.Command, []string) {
|
||||
config.Set(c)
|
||||
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 {
|
||||
log.WithField("error", err).Fatal("failed to configure system directories for pterodactyl")
|
||||
return
|
||||
@@ -175,7 +199,7 @@ func rootCmdRun(*cobra.Command, []string) {
|
||||
|
||||
states, err := server.CachedServerStates()
|
||||
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", 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
|
||||
@@ -211,7 +235,7 @@ func rootCmdRun(*cobra.Command, []string) {
|
||||
// as a result will result in a slow boot.
|
||||
if !r && (st == environment.ProcessRunningState || st == environment.ProcessStartingState) {
|
||||
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", err).Warn("failed to return server to running state")
|
||||
}
|
||||
} else if r || (!r && s.IsRunning()) {
|
||||
// If the server is currently running on Docker, mark the process as being in that state.
|
||||
@@ -222,17 +246,16 @@ func rootCmdRun(*cobra.Command, []string) {
|
||||
// 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.SetState(environment.ProcessRunningState)
|
||||
s.Environment.SetState(environment.ProcessRunningState)
|
||||
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", err).Warn("failed to attach to running server environment")
|
||||
}
|
||||
|
||||
return
|
||||
} else {
|
||||
// At this point we've determined that the server should indeed be in an offline state, so we'll
|
||||
// make a call to set that state just to ensure we don't ever accidentally end up with some invalid
|
||||
// state being tracked.
|
||||
s.Environment.SetState(environment.ProcessOfflineState)
|
||||
}
|
||||
|
||||
// Addresses potentially invalid data in the stored file that can cause Wings to lose
|
||||
// track of what the actual server state is.
|
||||
_ = s.SetState(environment.ProcessOfflineState)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -270,34 +293,20 @@ func rootCmdRun(*cobra.Command, []string) {
|
||||
Handler: r,
|
||||
|
||||
TLSConfig: &tls.Config{
|
||||
NextProtos: []string{
|
||||
"h2", // enable HTTP/2
|
||||
"http/1.1",
|
||||
},
|
||||
|
||||
// https://blog.cloudflare.com/exposing-go-on-the-internet
|
||||
NextProtos: []string{"h2", "http/1.1"},
|
||||
// @see https://blog.cloudflare.com/exposing-go-on-the-internet
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
},
|
||||
|
||||
PreferServerCipherSuites: true,
|
||||
|
||||
MinVersion: tls.VersionTLS12,
|
||||
MaxVersion: tls.VersionTLS13,
|
||||
|
||||
CurvePreferences: []tls.CurveID{
|
||||
tls.X25519,
|
||||
tls.CurveP256,
|
||||
},
|
||||
// END https://blog.cloudflare.com/exposing-go-on-the-internet
|
||||
MinVersion: tls.VersionTLS12,
|
||||
MaxVersion: tls.VersionTLS13,
|
||||
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -327,7 +336,6 @@ func rootCmdRun(*cobra.Command, []string) {
|
||||
if err := s.ListenAndServeTLS("", ""); err != nil {
|
||||
log.WithFields(log.Fields{"auto_tls": true, "tls_hostname": tlsHostname, "error": err}).
|
||||
Fatal("failed to configure HTTP server using auto-tls")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return
|
||||
@@ -335,9 +343,8 @@ func rootCmdRun(*cobra.Command, []string) {
|
||||
|
||||
// Check if main http server should run with TLS.
|
||||
if c.Api.Ssl.Enabled {
|
||||
if err := s.ListenAndServeTLS(c.Api.Ssl.CertificateFile, c.Api.Ssl.KeyFile); err != nil {
|
||||
if err := s.ListenAndServeTLS(strings.ToLower(c.Api.Ssl.CertificateFile), strings.ToLower(c.Api.Ssl.KeyFile)); err != nil {
|
||||
log.WithFields(log.Fields{"auto_tls": false, "error": err}).Fatal("failed to configure HTTPS server")
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -346,7 +353,6 @@ func rootCmdRun(*cobra.Command, []string) {
|
||||
s.TLSConfig = nil
|
||||
if err := s.ListenAndServe(); err != nil {
|
||||
log.WithField("error", err).Fatal("failed to configure HTTP server")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,13 +365,13 @@ func Execute() error {
|
||||
// in the code without having to pass around a logger instance.
|
||||
func configureLogging(logDir string, debug bool) error {
|
||||
if err := os.MkdirAll(path.Join(logDir, "/install"), 0700); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
p := filepath.Join(logDir, "/wings.log")
|
||||
w, err := logrotate.NewFile(p)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "failed to open process log file"))
|
||||
panic(errors.WithMessage(err, "failed to open process log file"))
|
||||
}
|
||||
|
||||
if debug {
|
||||
|
||||
@@ -47,19 +47,14 @@ type Configuration struct {
|
||||
System SystemConfiguration `json:"system" yaml:"system"`
|
||||
Docker DockerConfiguration `json:"docker" yaml:"docker"`
|
||||
|
||||
// The amount of time in seconds that should elapse between disk usage checks
|
||||
// run by the daemon. Setting a higher number can result in better IO performance
|
||||
// at an increased risk of a malicious user creating a process that goes over
|
||||
// the assigned disk limits.
|
||||
DiskCheckTimeout int `yaml:"disk_check_timeout"`
|
||||
|
||||
// Defines internal throttling configurations for server processes to prevent
|
||||
// someone from running an endless loop that spams data to logs.
|
||||
Throttles ConsoleThrottles
|
||||
|
||||
// The location where the panel is running that this daemon should connect to
|
||||
// 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.
|
||||
// This is required to have the "Server Mounts" feature work properly.
|
||||
@@ -73,9 +68,6 @@ type Configuration struct {
|
||||
|
||||
// Defines the configuration of the internal SFTP server.
|
||||
type SftpConfiguration struct {
|
||||
// If set to true disk checking will not be performed. This will prevent the SFTP
|
||||
// server from checking the total size of a directory when uploading files.
|
||||
DisableDiskChecking bool `default:"false" yaml:"disable_disk_checking"`
|
||||
// The bind address of the SFTP server.
|
||||
Address string `default:"0.0.0.0" json:"bind_address" yaml:"bind_address"`
|
||||
// The bind port of the SFTP server.
|
||||
@@ -104,6 +96,27 @@ type ApiConfiguration struct {
|
||||
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
|
||||
// object that can then be used.
|
||||
func ReadConfiguration(path string) (*Configuration, error) {
|
||||
@@ -179,7 +192,7 @@ func GetJwtAlgorithm() *jwt.HMACSHA {
|
||||
func NewFromPath(path string) (*Configuration, error) {
|
||||
c := new(Configuration)
|
||||
if err := defaults.Set(c); err != nil {
|
||||
return c, errors.WithStack(err)
|
||||
return c, err
|
||||
}
|
||||
|
||||
c.unsafeSetPath(path)
|
||||
@@ -217,12 +230,12 @@ func (c *Configuration) EnsurePterodactylUser() (*user.User, error) {
|
||||
if err == nil {
|
||||
return u, c.setSystemUser(u)
|
||||
} else if _, ok := err.(user.UnknownUserError); !ok {
|
||||
return nil, errors.WithStack(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sysName, err := getSystemName()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var command = fmt.Sprintf("useradd --system --no-create-home --shell /bin/false %s", c.System.Username)
|
||||
@@ -235,17 +248,17 @@ func (c *Configuration) EnsurePterodactylUser() (*user.User, error) {
|
||||
// We have to create the group first on Alpine, so do that here before continuing on
|
||||
// to the user creation process.
|
||||
if _, err := exec.Command("addgroup", "-S", c.System.Username).Output(); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
split := strings.Split(command, " ")
|
||||
if _, err := exec.Command(split[0], split[1:]...).Output(); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u, err := user.Lookup(c.System.Username); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
return nil, err
|
||||
} else {
|
||||
return u, c.setSystemUser(u)
|
||||
}
|
||||
@@ -287,11 +300,11 @@ func (c *Configuration) WriteToDisk() error {
|
||||
|
||||
b, err := yaml.Marshal(&ccopy)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(c.GetPath(), b, 0644); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -301,7 +314,7 @@ func (c *Configuration) WriteToDisk() error {
|
||||
func getSystemName() (string, error) {
|
||||
// use osrelease to get release version and ID
|
||||
if release, err := osrelease.Read(); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
return "", err
|
||||
} else {
|
||||
return release["ID"], nil
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type dockerNetworkInterfaces struct {
|
||||
@@ -49,18 +48,6 @@ type DockerConfiguration struct {
|
||||
// Domainname is the Docker domainname for all containers.
|
||||
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 map[string]RegistryConfiguration `json:"registries" yaml:"registries"`
|
||||
|
||||
@@ -85,7 +72,7 @@ func (c RegistryConfiguration) Base64() (string, error) {
|
||||
|
||||
b, err := json.Marshal(authConfig)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return base64.URLEncoding.EncodeToString(b), nil
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/apex/log"
|
||||
"github.com/pkg/errors"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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.
|
||||
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
|
||||
// this information without constantly having to do a system lookup.
|
||||
User struct {
|
||||
@@ -40,13 +53,12 @@ type SystemConfiguration struct {
|
||||
// considered stale and a re-check should occur. DANGER: setting this value too low can seriously
|
||||
// impact system performance and cause massive I/O bottlenecks and high CPU usage for the Wings
|
||||
// process.
|
||||
//
|
||||
// Set to 0 to disable disk checking entirely. This will always return 0 for the disk space used
|
||||
// by a server and should only be set in extreme scenarios where performance is critical and
|
||||
// disk usage is not a concern.
|
||||
DiskCheckInterval int64 `default:"150" yaml:"disk_check_interval"`
|
||||
|
||||
// Determines if Wings should detect a server that stops with a normal exit code of
|
||||
// "0" as being crashed if the process stopped without any Wings interaction. E.g.
|
||||
// the user did not press the stop button, but the process stopped cleanly.
|
||||
DetectCleanExitAsCrash bool `default:"true" yaml:"detect_clean_exit_as_crash"`
|
||||
|
||||
// If set to true, file permissions for a server will be checked when the process is
|
||||
// booted. This can cause boot delays if the server has a large amount of files. In most
|
||||
// cases disabling this should not have any major impact unless external processes are
|
||||
@@ -57,7 +69,24 @@ type SystemConfiguration struct {
|
||||
// when it boots and one is not detected.
|
||||
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"`
|
||||
|
||||
CrashDetection CrashDetection `yaml:"crash_detection"`
|
||||
}
|
||||
|
||||
type CrashDetection struct {
|
||||
// Determines if Wings should detect a server that stops with a normal exit code of
|
||||
// "0" as being crashed if the process stopped without any Wings interaction. E.g.
|
||||
// the user did not press the stop button, but the process stopped cleanly.
|
||||
DetectCleanExitAsCrash bool `default:"true" yaml:"detect_clean_exit_as_crash"`
|
||||
|
||||
// Timeout specifies the timeout between crashes that will not cause the server
|
||||
// to be automatically restarted, this value is used to prevent servers from
|
||||
// becoming stuck in a boot-loop after multiple consecutive crashes.
|
||||
Timeout int `default:"60" json:"timeout"`
|
||||
}
|
||||
|
||||
// Ensures that all of the system directories exist on the system. These directories are
|
||||
@@ -78,7 +107,7 @@ func (sc *SystemConfiguration) ConfigureDirectories() error {
|
||||
// that.
|
||||
if d, err := filepath.EvalSymlinks(sc.Data); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
} else if d != sc.Data {
|
||||
sc.Data = d
|
||||
@@ -114,13 +143,13 @@ func (sc *SystemConfiguration) EnableLogRotation() error {
|
||||
}
|
||||
|
||||
if st, err := os.Stat("/etc/logrotate.d"); err != nil && !os.IsNotExist(err) {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
} else if (err != nil && os.IsNotExist(err)) || !st.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := os.Stat("/etc/logrotate.d/wings"); err != nil && !os.IsNotExist(err) {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
} else if err == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -131,7 +160,7 @@ func (sc *SystemConfiguration) EnableLogRotation() error {
|
||||
// it so files can be rotated easily.
|
||||
f, err := os.Create("/etc/logrotate.d/wings")
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
@@ -151,10 +180,10 @@ func (sc *SystemConfiguration) EnableLogRotation() error {
|
||||
}`)
|
||||
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
return errors.Wrap(t.Execute(f, sc), "failed to write logrotate file to disk")
|
||||
return errors.WithMessage(t.Execute(f, sc), "failed to write logrotate file to disk")
|
||||
}
|
||||
|
||||
// Returns the location of the JSON file that tracks server states.
|
||||
@@ -166,3 +195,47 @@ func (sc *SystemConfiguration) GetStatesPath() string {
|
||||
func (sc *SystemConfiguration) GetInstallLogPath() string {
|
||||
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.WithMessage(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.WithMessage(err, fmt.Sprintf("the supplied timezone %s is invalid", sc.Timezone))
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
version: '3.5'
|
||||
version: '3.8'
|
||||
services:
|
||||
daemon:
|
||||
build: .
|
||||
wings:
|
||||
image: ghcr.io/pterodactyl/wings:latest
|
||||
restart: always
|
||||
networks:
|
||||
- daemon0
|
||||
- wings0
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "2022:2022"
|
||||
tty: true
|
||||
environment:
|
||||
- "DEBUG=false"
|
||||
- "TZ=UTC" # change to the three letter timezone of your choosing
|
||||
TZ: "UTC"
|
||||
DEBUG: "false"
|
||||
volumes:
|
||||
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||
- "/var/lib/docker/containers/:/var/lib/docker/containers/"
|
||||
@@ -19,17 +19,16 @@ services:
|
||||
- "/var/lib/pterodactyl/:/var/lib/pterodactyl/"
|
||||
- "/var/log/pterodactyl/:/var/log/pterodactyl/"
|
||||
- "/tmp/pterodactyl/:/tmp/pterodactyl/"
|
||||
## you may need /srv/daemon-data if you are upgrading from an old daemon
|
||||
## - "/srv/daemon-data/:/srv/daemon-data/"
|
||||
## Required for ssl if you user let's encrypt. uncomment to use.
|
||||
## - "/etc/letsencrypt/:/etc/letsencrypt/"
|
||||
|
||||
# you may need /srv/daemon-data if you are upgrading from an old daemon
|
||||
#- "/srv/daemon-data/:/srv/daemon-data/"
|
||||
# Required for ssl if you user let's encrypt. uncomment to use.
|
||||
#- "/etc/letsencrypt/:/etc/letsencrypt/"
|
||||
networks:
|
||||
daemon0:
|
||||
name: daemon0
|
||||
wings0:
|
||||
name: wings0
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "172.21.0.0/16"
|
||||
driver_opts:
|
||||
com.docker.network.bridge.name: daemon0
|
||||
com.docker.network.bridge.name: wings0
|
||||
@@ -38,15 +38,16 @@ func (a *Allocations) Bindings() nat.PortMap {
|
||||
continue
|
||||
}
|
||||
|
||||
binding := []nat.PortBinding{
|
||||
{
|
||||
HostIP: ip,
|
||||
HostPort: strconv.Itoa(port),
|
||||
},
|
||||
binding := nat.PortBinding{
|
||||
HostIP: ip,
|
||||
HostPort: strconv.Itoa(port),
|
||||
}
|
||||
|
||||
out[nat.Port(fmt.Sprintf("%d/tcp", port))] = binding
|
||||
out[nat.Port(fmt.Sprintf("%d/udp", port))] = binding
|
||||
tcp := nat.Port(fmt.Sprintf("%d/tcp", port))
|
||||
udp := nat.Port(fmt.Sprintf("%d/udp", port))
|
||||
|
||||
out[tcp] = append(out[tcp], binding)
|
||||
out[udp] = append(out[udp], binding)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ package environment
|
||||
import (
|
||||
"context"
|
||||
"github.com/apex/log"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
@@ -10,10 +12,28 @@ import (
|
||||
"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.
|
||||
func ConfigureDocker(c *config.DockerConfiguration) error {
|
||||
// Ensure the required docker network exists on the system.
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
cli, err := DockerClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -64,7 +84,7 @@ func createDockerNetwork(cli *client.Client, c *config.DockerConfiguration) erro
|
||||
Options: map[string]string{
|
||||
"encryption": "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.host_binding_ipv4": "0.0.0.0",
|
||||
"com.docker.network.bridge.name": "pterodactyl0",
|
||||
|
||||
@@ -2,6 +2,7 @@ package docker
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -35,7 +36,7 @@ func (e *Environment) Attach() error {
|
||||
}
|
||||
|
||||
if err := e.followOutput(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
opts := types.ContainerAttachOptions{
|
||||
@@ -47,7 +48,7 @@ func (e *Environment) Attach() error {
|
||||
|
||||
// Set the stream again with the container.
|
||||
if st, err := e.client.ContainerAttach(context.Background(), e.Id, opts); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
} else {
|
||||
e.SetStream(&st)
|
||||
}
|
||||
@@ -59,7 +60,7 @@ func (e *Environment) Attach() error {
|
||||
defer cancel()
|
||||
defer e.stream.Close()
|
||||
defer func() {
|
||||
e.setState(environment.ProcessOfflineState)
|
||||
e.SetState(environment.ProcessOfflineState)
|
||||
e.SetStream(nil)
|
||||
}()
|
||||
|
||||
@@ -69,14 +70,19 @@ func (e *Environment) Attach() error {
|
||||
// indicates that the container is no longer running.
|
||||
go func(ctx context.Context) {
|
||||
if err := e.pollResources(ctx); err != nil {
|
||||
log.WithField("environment_id", e.Id).WithField("error", errors.WithStack(err)).Error("error during environment resource polling")
|
||||
l := log.WithField("environment_id", e.Id)
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
l.WithField("error", err).Error("error during environment resource polling")
|
||||
} else {
|
||||
l.Warn("stopping server resource polling: context canceled")
|
||||
}
|
||||
}
|
||||
}(ctx)
|
||||
|
||||
// Stream the reader output to the console which will then fire off events and handle console
|
||||
// throttling and sending the output to the user.
|
||||
if _, err := io.Copy(console, e.stream.Reader); err != nil {
|
||||
log.WithField("environment_id", e.Id).WithField("error", errors.WithStack(err)).Error("error while copying environment output to console")
|
||||
log.WithField("environment_id", e.Id).WithField("error", err).Error("error while copying environment output to console")
|
||||
}
|
||||
}(c)
|
||||
|
||||
@@ -114,7 +120,7 @@ func (e *Environment) InSituUpdate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
u := container.UpdateConfig{
|
||||
@@ -124,7 +130,7 @@ func (e *Environment) InSituUpdate() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
if _, err := e.client.ContainerUpdate(ctx, e.Id, u); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -139,12 +145,12 @@ func (e *Environment) Create() error {
|
||||
if _, err := e.client.ContainerInspect(context.Background(), e.Id); err == nil {
|
||||
return nil
|
||||
} else if !client.IsErrNotFound(err) {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Try to pull the requested image before creating the container.
|
||||
if err := e.ensureImageExists(e.meta.Image); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
a := e.Configuration.Allocations()
|
||||
@@ -219,7 +225,7 @@ func (e *Environment) Create() error {
|
||||
}
|
||||
|
||||
if _, err := e.client.ContainerCreate(context.Background(), conf, hostConf, nil, e.Id); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -244,7 +250,7 @@ func (e *Environment) convertMounts() []mount.Mount {
|
||||
// it will be forcibly stopped by Docker.
|
||||
func (e *Environment) Destroy() error {
|
||||
// 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{
|
||||
RemoveVolumes: true,
|
||||
@@ -260,7 +266,7 @@ func (e *Environment) Destroy() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
e.setState(environment.ProcessOfflineState)
|
||||
e.SetState(environment.ProcessOfflineState)
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -271,7 +277,7 @@ func (e *Environment) Destroy() error {
|
||||
func (e *Environment) followOutput() error {
|
||||
if exists, err := e.Exists(); !exists {
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
return errors.New(fmt.Sprintf("no such container: %s", e.Id))
|
||||
@@ -286,20 +292,68 @@ func (e *Environment) followOutput() error {
|
||||
|
||||
reader, err := e.client.ContainerLogs(context.Background(), e.Id, opts)
|
||||
|
||||
go func(r io.ReadCloser) {
|
||||
defer r.Close()
|
||||
go func(reader io.ReadCloser) {
|
||||
defer reader.Close()
|
||||
|
||||
s := bufio.NewScanner(r)
|
||||
for s.Scan() {
|
||||
e.Events().Publish(environment.ConsoleOutputEvent, s.Text())
|
||||
r := bufio.NewReader(reader)
|
||||
|
||||
// Micro-optimization to create these replacements one time when this routine
|
||||
// fires up, rather than on every line that is executed.
|
||||
cr := []byte(" \r")
|
||||
crr := []byte("\r\n")
|
||||
|
||||
// Avoid constantly re-allocating memory when we're flooding lines through this
|
||||
// function by using the same buffer for the duration of the call and just truncating
|
||||
// the value back to 0 every loop.
|
||||
var str strings.Builder
|
||||
ParentLoop:
|
||||
for {
|
||||
str.Reset()
|
||||
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.
|
||||
str.Write(bytes.Replace(line, cr, crr, -1))
|
||||
|
||||
// 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 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(str.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 := s.Err(); err != nil {
|
||||
if err != nil && err != io.EOF {
|
||||
log.WithField("error", err).WithField("container_id", e.Id).Warn("error processing scanner line in console output")
|
||||
}
|
||||
}(reader)
|
||||
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Pulls the image from Docker. If there is an error while pulling the image from the source
|
||||
@@ -310,12 +364,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
|
||||
// cases an outage shouldn't affect users too badly. It'll at least keep existing servers working
|
||||
// correctly if anything.
|
||||
//
|
||||
// TODO: local images
|
||||
func (e *Environment) ensureImageExists(image string) error {
|
||||
e.Events().Publish(environment.DockerImagePullStarted, "")
|
||||
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
|
||||
// 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...
|
||||
|
||||
@@ -4,17 +4,17 @@ import (
|
||||
"context"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pterodactyl/wings/api"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
"github.com/pterodactyl/wings/events"
|
||||
"github.com/pterodactyl/wings/system"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Metadata struct {
|
||||
Image string
|
||||
Stop *api.ProcessStopConfiguration
|
||||
Stop api.ProcessStopConfiguration
|
||||
}
|
||||
|
||||
// Ensure that the Docker environment is always implementing all of the methods
|
||||
@@ -47,15 +47,14 @@ type Environment struct {
|
||||
emitter *events.EventBus
|
||||
|
||||
// Tracks the environment state.
|
||||
st string
|
||||
stMu sync.RWMutex
|
||||
st *system.AtomicString
|
||||
}
|
||||
|
||||
// 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
|
||||
// by default). The container does not need to exist at this point.
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -65,6 +64,7 @@ func New(id string, m *Metadata, c *environment.Configuration) (*Environment, er
|
||||
Configuration: c,
|
||||
meta: m,
|
||||
client: cli,
|
||||
st: system.NewAtomicString(environment.ProcessOfflineState),
|
||||
}
|
||||
|
||||
return e, nil
|
||||
@@ -155,7 +155,7 @@ func (e *Environment) ExitState() (uint32, bool, error) {
|
||||
return 1, false, nil
|
||||
}
|
||||
|
||||
return 0, false, errors.WithStack(err)
|
||||
return 0, false, err
|
||||
}
|
||||
|
||||
return uint32(c.State.ExitCode), c.State.OOMKilled, nil
|
||||
@@ -171,7 +171,7 @@ func (e *Environment) Config() *environment.Configuration {
|
||||
}
|
||||
|
||||
// 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.meta.Stop = c
|
||||
e.mu.Unlock()
|
||||
|
||||
@@ -26,7 +26,7 @@ func (e *Environment) OnBeforeStart() error {
|
||||
// the Panel is usee.
|
||||
if err := e.client.ContainerRemove(context.Background(), e.Id, types.ContainerRemoveOptions{RemoveVolumes: true}); err != nil {
|
||||
if !client.IsErrNotFound(err) {
|
||||
return errors.Wrap(err, "failed to remove server docker container during pre-boot")
|
||||
return errors.WithMessage(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
|
||||
// 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...
|
||||
e.setState(environment.ProcessStoppingState)
|
||||
e.setState(environment.ProcessOfflineState)
|
||||
e.SetState(environment.ProcessStoppingState)
|
||||
e.SetState(environment.ProcessOfflineState)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -69,12 +69,12 @@ func (e *Environment) Start() error {
|
||||
//
|
||||
// @see https://github.com/pterodactyl/panel/issues/2000
|
||||
if !client.IsErrNotFound(err) {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// If the server is running update our internal state and continue on with the attach.
|
||||
if c.State.Running {
|
||||
e.setState(environment.ProcessRunningState)
|
||||
e.SetState(environment.ProcessRunningState)
|
||||
|
||||
return e.Attach()
|
||||
}
|
||||
@@ -84,12 +84,12 @@ func (e *Environment) Start() error {
|
||||
// to truncate them.
|
||||
if _, err := os.Stat(c.LogPath); err == nil {
|
||||
if err := os.Truncate(c.LogPath, 0); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return 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
|
||||
// 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
|
||||
// occur.
|
||||
if err := e.OnBeforeStart(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
if err := e.client.ContainerStart(ctx, e.Id, types.ContainerStartOptions{}); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// No errors, good to continue through.
|
||||
@@ -126,8 +126,8 @@ func (e *Environment) Stop() error {
|
||||
s := e.meta.Stop
|
||||
e.mu.RUnlock()
|
||||
|
||||
if s == nil || s.Type == api.ProcessStopSignal {
|
||||
if s == nil {
|
||||
if s.Type == "" || s.Type == api.ProcessStopSignal {
|
||||
if s.Type == "" {
|
||||
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
|
||||
// it is and continue through to the stop handling for the process.
|
||||
if e.State() != environment.ProcessOfflineState {
|
||||
e.setState(environment.ProcessStoppingState)
|
||||
if e.st.Load() != environment.ProcessOfflineState {
|
||||
e.SetState(environment.ProcessStoppingState)
|
||||
}
|
||||
|
||||
// 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.
|
||||
if client.IsErrNotFound(err) {
|
||||
e.SetStream(nil)
|
||||
e.setState(environment.ProcessOfflineState)
|
||||
e.SetState(environment.ProcessOfflineState)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -169,7 +169,7 @@ func (e *Environment) Stop() error {
|
||||
// will be terminated forcefully depending on the value of the second argument.
|
||||
func (e *Environment) WaitForStop(seconds uint, terminate bool) error {
|
||||
if err := e.Stop(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(seconds)*time.Second)
|
||||
@@ -183,22 +183,27 @@ func (e *Environment) WaitForStop(seconds uint, terminate bool) error {
|
||||
case <-ctx.Done():
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
if terminate {
|
||||
log.WithField("container_id", e.Id).Debug("server did not stop in time, executing process termination")
|
||||
log.WithField("container_id", e.Id).Info("server did not stop in time, executing process termination")
|
||||
|
||||
return errors.WithStack(e.Terminate(os.Kill))
|
||||
return e.Terminate(os.Kill)
|
||||
}
|
||||
|
||||
return errors.WithStack(ctxErr)
|
||||
return ctxErr
|
||||
}
|
||||
case err := <-errChan:
|
||||
if err != nil {
|
||||
if terminate {
|
||||
log.WithField("container_id", e.Id).WithField("error", errors.WithStack(err)).Warn("error while waiting for container stop, attempting process termination")
|
||||
l := log.WithField("container_id", e.Id)
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
l.Warn("deadline exceeded for container stop; terminating process")
|
||||
} else {
|
||||
l.WithField("error", err).Warn("error while waiting for container stop; terminating process")
|
||||
}
|
||||
|
||||
return errors.WithStack(e.Terminate(os.Kill))
|
||||
return e.Terminate(os.Kill)
|
||||
}
|
||||
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
case <-ok:
|
||||
}
|
||||
@@ -210,23 +215,23 @@ func (e *Environment) WaitForStop(seconds uint, terminate bool) error {
|
||||
func (e *Environment) Terminate(signal os.Signal) error {
|
||||
c, err := e.client.ContainerInspect(context.Background(), e.Id)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !c.State.Running {
|
||||
// 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
|
||||
// first so crash detection is not triggered.
|
||||
if e.State() != environment.ProcessOfflineState {
|
||||
e.setState(environment.ProcessStoppingState)
|
||||
e.setState(environment.ProcessOfflineState)
|
||||
if e.st.Load() != environment.ProcessOfflineState {
|
||||
e.SetState(environment.ProcessStoppingState)
|
||||
e.SetState(environment.ProcessOfflineState)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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")
|
||||
|
||||
@@ -234,7 +239,7 @@ func (e *Environment) Terminate(signal os.Signal) error {
|
||||
return err
|
||||
}
|
||||
|
||||
e.setState(environment.ProcessOfflineState)
|
||||
e.SetState(environment.ProcessOfflineState)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,36 +6,24 @@ import (
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
)
|
||||
|
||||
// Returns the current environment state.
|
||||
func (e *Environment) State() string {
|
||||
e.stMu.RLock()
|
||||
defer e.stMu.RUnlock()
|
||||
|
||||
return e.st
|
||||
return e.st.Load()
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (e *Environment) setState(state string) error {
|
||||
func (e *Environment) SetState(state string) {
|
||||
if state != environment.ProcessOfflineState &&
|
||||
state != environment.ProcessStartingState &&
|
||||
state != environment.ProcessRunningState &&
|
||||
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.
|
||||
if prevState != state {
|
||||
if e.State() != state {
|
||||
// If the state changed make sure we update the internal tracking to note that.
|
||||
e.stMu.Lock()
|
||||
e.st = state
|
||||
e.stMu.Unlock()
|
||||
|
||||
e.Events().Publish(environment.StateChangeEvent, e.State())
|
||||
e.st.Store(state)
|
||||
e.Events().Publish(environment.StateChangeEvent, state)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -15,18 +15,17 @@ import (
|
||||
// Attach to the instance and then automatically emit an event whenever the resource usage for the
|
||||
// server process changes.
|
||||
func (e *Environment) pollResources(ctx context.Context) error {
|
||||
l := log.WithField("container_id", e.Id)
|
||||
|
||||
l.Debug("starting 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")
|
||||
}
|
||||
|
||||
l := log.WithField("container_id", e.Id)
|
||||
l.Debug("starting resource polling for container")
|
||||
defer l.Debug("stopped resource polling for container")
|
||||
|
||||
stats, err := e.client.ContainerStats(context.Background(), e.Id, true)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
defer stats.Body.Close()
|
||||
|
||||
@@ -41,7 +40,7 @@ func (e *Environment) pollResources(ctx context.Context) error {
|
||||
|
||||
if err := dec.Decode(&v); err != nil {
|
||||
if err != io.EOF {
|
||||
l.WithField("error", errors.WithStack(err)).Warn("error while processing Docker stats output for container")
|
||||
l.WithField("error", err).Warn("error while processing Docker stats output for container")
|
||||
} else {
|
||||
l.Debug("io.EOF encountered during stats decode, stopping polling...")
|
||||
}
|
||||
@@ -50,7 +49,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.
|
||||
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")
|
||||
return nil
|
||||
}
|
||||
@@ -76,7 +75,7 @@ func (e *Environment) pollResources(ctx context.Context) error {
|
||||
}
|
||||
|
||||
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", err).Warn("error while marshaling stats object for environment")
|
||||
} else {
|
||||
e.Events().Publish(environment.ResourceEvent, string(b))
|
||||
}
|
||||
|
||||
@@ -33,18 +33,16 @@ func (e *Environment) SendCommand(c string) error {
|
||||
e.mu.RLock()
|
||||
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
|
||||
// the server as entering the stopping state otherwise the process will stop and Wings will think
|
||||
// it has crashed and attempt to restart it.
|
||||
if e.meta.Stop.Type == "command" && c == e.meta.Stop.Value {
|
||||
e.Events().Publish(environment.StateChangeEvent, environment.ProcessStoppingState)
|
||||
}
|
||||
// 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
|
||||
// it has crashed and attempt to restart it.
|
||||
if e.meta.Stop.Type == "command" && c == e.meta.Stop.Value {
|
||||
e.SetState(environment.ProcessStoppingState)
|
||||
}
|
||||
|
||||
_, err := e.stream.Conn.Write([]byte(c + "\n"))
|
||||
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 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),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
return nil, err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
|
||||
@@ -94,4 +94,12 @@ type ProcessEnvironment interface {
|
||||
// Reads the log file for the process from the end backwards until the provided
|
||||
// number of lines is met.
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package events
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/gammazero/workerpool"
|
||||
"github.com/pkg/errors"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
@@ -69,7 +68,7 @@ func (e *EventBus) Publish(topic string, data string) {
|
||||
func (e *EventBus) PublishJson(topic string, data interface{}) error {
|
||||
b, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
e.Publish(topic, string(b))
|
||||
|
||||
1
go.mod
1
go.mod
@@ -17,7 +17,6 @@ require (
|
||||
github.com/containerd/containerd v1.3.7 // indirect
|
||||
github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b // indirect
|
||||
github.com/creasty/defaults v1.5.0
|
||||
github.com/docker/cli v17.12.1-ce-rc2+incompatible
|
||||
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible
|
||||
github.com/docker/go-connections v0.4.0
|
||||
|
||||
2
go.sum
2
go.sum
@@ -93,8 +93,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/docker/cli v17.12.1-ce-rc2+incompatible h1:ESUycEAqvFuLglAHkUW66rCc2djYtd3i1x231svLq9o=
|
||||
github.com/docker/cli v17.12.1-ce-rc2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible h1:iWPIG7pWIsCwT6ZtHnTUpoVMnete7O/pzd9HFE3+tn8=
|
||||
|
||||
@@ -43,33 +43,33 @@ func New(data []byte) (*Installer, error) {
|
||||
|
||||
// Unmarshal the environment variables from the request into the server struct.
|
||||
if b, _, _, err := jsonparser.Get(data, "environment"); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
return nil, err
|
||||
} else {
|
||||
cfg.EnvVars = make(environment.Variables)
|
||||
if err := json.Unmarshal(b, &cfg.EnvVars); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Unmarshal the allocation mappings from the request into the server struct.
|
||||
if b, _, _, err := jsonparser.Get(data, "allocations", "mappings"); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
return nil, err
|
||||
} else {
|
||||
cfg.Allocations.Mappings = make(map[string][]int)
|
||||
if err := json.Unmarshal(b, &cfg.Allocations.Mappings); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
cfg.Container.Image = getString(data, "container", "image")
|
||||
|
||||
c, rerr, err := api.NewRequester().GetServerConfiguration(cfg.Uuid)
|
||||
if err != nil || rerr != nil {
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
c, err := api.New().GetServerConfiguration(cfg.Uuid)
|
||||
if err != nil {
|
||||
if !api.IsRequestError(err) {
|
||||
return nil, 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
|
||||
|
||||
@@ -8,14 +8,15 @@ import (
|
||||
"github.com/mattn/go-colorable"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var Default = New(os.Stderr, true)
|
||||
|
||||
var bold = color2.New(color2.Bold)
|
||||
var boldred = color2.New(color2.Bold, color2.FgRed)
|
||||
|
||||
var Strings = [...]string{
|
||||
log.DebugLevel: "DEBUG",
|
||||
@@ -60,7 +61,6 @@ func (h *Handler) HandleLog(e *log.Entry) error {
|
||||
if name == "source" {
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Fprintf(h.Writer, " %s=%v", color.Sprint(name), e.Fields.Get(name))
|
||||
}
|
||||
|
||||
@@ -70,45 +70,16 @@ func (h *Handler) HandleLog(e *log.Entry) error {
|
||||
if name != "error" {
|
||||
continue
|
||||
}
|
||||
|
||||
var br = color2.New(color2.Bold, color2.FgRed)
|
||||
if err, ok := e.Fields.Get("error").(error); ok {
|
||||
fmt.Fprintf(h.Writer, "\n%s%+v\n\n", br.Sprintf("Stacktrace:"), getErrorStack(err, false))
|
||||
} else {
|
||||
fmt.Fprintf(h.Writer, "\n%s%+v\n\n", br.Sprintf("Invalid Error:"), err)
|
||||
if e, ok := errors.Cause(err).(tracer); ok {
|
||||
st := e.StackTrace()
|
||||
l := math.Min(float64(len(st)), 10)
|
||||
fmt.Fprintf(h.Writer, "\n%s%+v\n\n", boldred.Sprintf("Stacktrace:"), st[0:int(l)])
|
||||
} else {
|
||||
fmt.Fprintf(h.Writer, "\n%s\n%+v\n\n", boldred.Sprintf("Stacktrace:"), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getErrorStack(err error, i bool) errors.StackTrace {
|
||||
e, ok := err.(tracer)
|
||||
if !ok {
|
||||
if i {
|
||||
// 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.
|
||||
return errors.Wrap(err, "failed to generate stacktrace for caught error").(tracer).StackTrace()
|
||||
}
|
||||
|
||||
return getErrorStack(errors.Wrap(err, err.Error()), true)
|
||||
}
|
||||
|
||||
st := e.StackTrace()
|
||||
|
||||
l := len(st)
|
||||
// If this was an internal stack generation we're going to skip over the top four items in the stack
|
||||
// trace since they'll point to the error that was generated by this function.
|
||||
f := 0
|
||||
if i {
|
||||
f = 4
|
||||
}
|
||||
|
||||
if i && l > 9 {
|
||||
l = 9
|
||||
} else if !i && l > 5 {
|
||||
l = 5
|
||||
}
|
||||
|
||||
return st[f:l]
|
||||
}
|
||||
|
||||
@@ -76,13 +76,13 @@ func (cfr *ConfigurationFileReplacement) getKeyValue(value []byte) interface{} {
|
||||
func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error) {
|
||||
parsed, err := gabs.ParseJSON(data)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, v := range f.Replace {
|
||||
value, err := f.LookupConfigurationValue(v)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
return nil, errors.Wrap(err, "failed to set config value of array child")
|
||||
return nil, errors.WithMessage(err, "failed to set config value of array child")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -110,7 +110,7 @@ func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, errors.Wrap(err, "unable to set config value at pathway: "+v.Match)
|
||||
return nil, errors.WithMessage(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)
|
||||
}
|
||||
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
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])
|
||||
if err != nil {
|
||||
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.WithMessage(err, "error while parsing array element at path")
|
||||
}
|
||||
|
||||
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
|
||||
// if there is not an object structure detected (no matches[3] available).
|
||||
if _, err = c.SetP(t, matches[1]); err != nil {
|
||||
return errors.Wrap(err, "failed to create empty array for missing element")
|
||||
return errors.WithMessage(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
|
||||
@@ -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.
|
||||
ct, err = c.ArrayElementP(0, matches[1])
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to find array element at path")
|
||||
return errors.WithMessage(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 {
|
||||
return errors.Wrap(err, "failed to set value at config path: "+path)
|
||||
return errors.WithMessage(err, "failed to set value at config path: "+path)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -253,7 +253,7 @@ func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplac
|
||||
match, _, _, err := jsonparser.Get(f.configuration, path...)
|
||||
if err != nil {
|
||||
if err != jsonparser.KeyPathNotFoundError {
|
||||
return string(match), errors.WithStack(err)
|
||||
return string(match), err
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{"path": path, "filename": f.FileName}).Debug("attempted to load a configuration value that does not exist")
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@@ -77,11 +76,6 @@ func (f *ConfigurationFile) UnmarshalJSON(data []byte) error {
|
||||
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.
|
||||
type ConfigurationFileReplacement struct {
|
||||
Match string `json:"match"`
|
||||
@@ -172,17 +166,17 @@ func (f *ConfigurationFile) Parse(path string, internal bool) error {
|
||||
|
||||
b := strings.TrimSuffix(path, filepath.Base(path))
|
||||
if err := os.MkdirAll(b, 0755); err != nil {
|
||||
return errors.Wrap(err, "failed to create base directory for missing configuration file")
|
||||
return errors.WithMessage(err, "failed to create base directory for missing configuration file")
|
||||
} else {
|
||||
if _, err := os.Create(path); err != nil {
|
||||
return errors.Wrap(err, "failed to create missing configuration file")
|
||||
return errors.WithMessage(err, "failed to create missing configuration file")
|
||||
}
|
||||
}
|
||||
|
||||
return f.Parse(path, true)
|
||||
}
|
||||
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Parses an xml file.
|
||||
@@ -354,12 +348,12 @@ func (f *ConfigurationFile) parseJsonFile(path string) error {
|
||||
func (f *ConfigurationFile) parseYamlFile(path string) error {
|
||||
b, err := readFileBytes(path)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
i := make(map[string]interface{})
|
||||
if err := yaml.Unmarshal(b, &i); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 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.
|
||||
jsonBytes, err := json.Marshal(dyno.ConvertMapI2MapS(i))
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Now that the data is converted, treat it just like JSON and pass it to the
|
||||
// iterator function to update values as necessary.
|
||||
data, err := f.IterateOverJson(jsonBytes)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Remarshal the JSON into YAML format before saving it back to the disk.
|
||||
marshaled, err := yaml.Marshal(data.Data())
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(path, marshaled, 0644)
|
||||
@@ -390,38 +384,25 @@ func (f *ConfigurationFile) parseYamlFile(path string) error {
|
||||
// scanning a file and performing a replacement. You should attempt to use anything other
|
||||
// than this function where possible.
|
||||
func (f *ConfigurationFile) parseTextFile(path string) error {
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0644)
|
||||
input, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
hasReplaced := false
|
||||
t := scanner.Text()
|
||||
|
||||
// Iterate over the potential replacements for the line and check if there are
|
||||
// any matches.
|
||||
lines := strings.Split(string(input), "\n")
|
||||
for i, line := range lines {
|
||||
for _, replace := range f.Replace {
|
||||
if !strings.HasPrefix(t, replace.Match) {
|
||||
// If this line doesn't match what we expect for the replacement, move on to the next
|
||||
// line. Otherwise, update the line to have the replacement value.
|
||||
if !strings.HasPrefix(line, replace.Match) {
|
||||
continue
|
||||
}
|
||||
|
||||
hasReplaced = true
|
||||
t = strings.Replace(t, replace.Match, replace.ReplaceWith.String(), 1)
|
||||
}
|
||||
|
||||
// If there was a replacement that occurred on this specific line, do a write to the file
|
||||
// immediately to write that modified content to the disk.
|
||||
if hasReplaced {
|
||||
if _, err := file.WriteAt([]byte(t+"\n"), int64(len(scanner.Bytes()))); err != nil {
|
||||
return err
|
||||
}
|
||||
lines[i] = replace.ReplaceWith.String()
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
if err := ioutil.WriteFile(path, []byte(strings.Join(lines, "\n")), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -434,7 +415,7 @@ func (f *ConfigurationFile) parsePropertiesFile(path string) error {
|
||||
// Open the file.
|
||||
f2, err := os.Open(path)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
var s strings.Builder
|
||||
@@ -456,20 +437,20 @@ func (f *ConfigurationFile) parsePropertiesFile(path string) error {
|
||||
|
||||
// Handle any scanner errors.
|
||||
if err := scanner.Err(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Decode the properties file.
|
||||
p, err := properties.LoadFile(path, properties.UTF8)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Replace any values that need to be replaced.
|
||||
for _, replace := range f.Replace {
|
||||
data, err := f.LookupConfigurationValue(replace)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
v, ok := p.Get(replace.Match)
|
||||
@@ -481,7 +462,7 @@ func (f *ConfigurationFile) parsePropertiesFile(path string) error {
|
||||
}
|
||||
|
||||
if _, _, err := p.Set(replace.Match, data); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,7 +482,7 @@ func (f *ConfigurationFile) parsePropertiesFile(path string) error {
|
||||
// Open the file for writing.
|
||||
w, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ func (e *RequestError) SetMessage(msg string) *RequestError {
|
||||
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
|
||||
// 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")
|
||||
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
||||
@@ -75,7 +75,7 @@ func (e *RequestError) AbortWithStatus(status int, c *gin.Context) {
|
||||
if status >= 500 {
|
||||
e.logger().WithField("error", e.Err).Error("encountered HTTP/500 error while handling request")
|
||||
|
||||
c.Error(errors.WithStack(e))
|
||||
c.Error(e)
|
||||
} else {
|
||||
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.
|
||||
func (e *RequestError) AbortFilesystemError(c *gin.Context) {
|
||||
if errors.Is(e.Err, os.ErrNotExist) {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
||||
"error": "The requested resource was not found.",
|
||||
})
|
||||
if errors.Is(e.Err, os.ErrNotExist) || filesystem.IsBadPathResolutionError(e.Err) {
|
||||
if filesystem.IsBadPathResolutionError(e.Err) {
|
||||
e.logger().Warn(e.Err.Error())
|
||||
}
|
||||
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "The requested resource was not found."})
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Is(e.Err, filesystem.ErrNotEnoughDiskSpace) {
|
||||
c.AbortWithStatusJSON(http.StatusConflict, gin.H{
|
||||
"error": "There is not enough disk space available to perform that action.",
|
||||
})
|
||||
c.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": "There is not enough disk space available to perform that action."})
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasSuffix(e.Err.Error(), "file name too long") {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
||||
"error": "File name is too long.",
|
||||
})
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "File name is too long."})
|
||||
return
|
||||
}
|
||||
|
||||
if e, ok := e.Err.(*os.SyscallError); ok && e.Syscall == "readdirent" {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
||||
"error": "The requested directory does not exist.",
|
||||
})
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "The requested directory does not exist."})
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasSuffix(e.Err.Error(), "file name too long") {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Cannot perform that action: file name is too long.",
|
||||
})
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Cannot perform that action: file name is too long."})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ func Configure() *gin.Engine {
|
||||
server.POST("/commands", postServerCommands)
|
||||
server.POST("/install", postServerInstall)
|
||||
server.POST("/reinstall", postServerReinstall)
|
||||
server.POST("/ws/deny", postServerDenyWSTokens)
|
||||
|
||||
// This archive request causes the archive to start being created
|
||||
// this should only be triggered by the panel.
|
||||
@@ -86,6 +87,7 @@ func Configure() *gin.Engine {
|
||||
files.POST("/delete", postServerDeleteFiles)
|
||||
files.POST("/compress", postServerCompressFiles)
|
||||
files.POST("/decompress", postServerDecompressFiles)
|
||||
files.POST("/chmod", postServerChmodFile)
|
||||
}
|
||||
|
||||
backup := server.Group("/backup")
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/apex/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pterodactyl/wings/router/tokens"
|
||||
"github.com/pterodactyl/wings/server"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -226,7 +227,7 @@ func deleteServer(c *gin.Context) {
|
||||
if err := os.RemoveAll(p); err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"path": p,
|
||||
"error": errors.WithStack(err),
|
||||
"error": err,
|
||||
}).Warn("failed to remove server files during deletion process")
|
||||
}
|
||||
}(s.Filesystem().Path())
|
||||
@@ -241,3 +242,22 @@ func deleteServer(c *gin.Context) {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/apex/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pterodactyl/wings/router/tokens"
|
||||
@@ -45,6 +46,15 @@ func getServerFileContents(c *gin.Context) {
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
}
|
||||
|
||||
// TODO(dane): should probably come up with a different approach here. If an error is encountered
|
||||
// by this Readfile call you'll end up causing a (recovered) panic in the program because so many
|
||||
// headers have already been set. We should probably add a RawReadfile that just returns the file
|
||||
// to be read and then we can stream from that safely without error.
|
||||
//
|
||||
// Until that becomes a problem though I'm just going to leave this how it is. The panic is recovered
|
||||
// and a normal 500 error is returned to the client to my knowledge. It is also very unlikely to
|
||||
// happen since we're doing so much before this point that would normally throw an error if there
|
||||
// was a problem with the file.
|
||||
if err := s.Filesystem().Readfile(p, c.Writer); err != nil {
|
||||
TrackedServerError(err, s).AbortFilesystemError(c)
|
||||
return
|
||||
@@ -356,6 +366,68 @@ func postServerDecompressFiles(c *gin.Context) {
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
type chmodFile struct {
|
||||
File string `json:"file"`
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
|
||||
func postServerChmodFile(c *gin.Context) {
|
||||
s := GetServer(c.Param("server"))
|
||||
|
||||
var data struct {
|
||||
Root string `json:"root"`
|
||||
Files []chmodFile `json:"files"`
|
||||
}
|
||||
|
||||
if err := c.BindJSON(&data); err != nil {
|
||||
log.Debug(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(data.Files) == 0 {
|
||||
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
|
||||
"error": "No files to chmod were provided.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
g, ctx := errgroup.WithContext(context.Background())
|
||||
|
||||
// Loop over the array of files passed in and perform the move or rename action against each.
|
||||
for _, p := range data.Files {
|
||||
g.Go(func() error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
mode, err := strconv.ParseUint(p.Mode, 8, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.Filesystem().Chmod(path.Join(data.Root, p.File), os.FileMode(mode)); err != nil {
|
||||
// Return nil if the error is an is not exists.
|
||||
// NOTE: os.IsNotExist() does not work if the error is wrapped.
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
TrackedServerError(err, s).AbortFilesystemError(c)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func postServerUploadFiles(c *gin.Context) {
|
||||
token := tokens.UploadPayload{}
|
||||
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
|
||||
@@ -397,14 +469,14 @@ func postServerUploadFiles(c *gin.Context) {
|
||||
for _, header := range headers {
|
||||
p, err := s.Filesystem().SafePath(filepath.Join(directory, header.Filename))
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
TrackedServerError(err, s).AbortFilesystemError(c)
|
||||
return
|
||||
}
|
||||
|
||||
// We run this in a different method so I can use defer without any of
|
||||
// the consequences caused by calling it in a loop.
|
||||
if err := handleFileUpload(p, s, header); err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
TrackedServerError(err, s).AbortFilesystemError(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -413,12 +485,12 @@ func postServerUploadFiles(c *gin.Context) {
|
||||
func handleFileUpload(p string, s *server.Server, header *multipart.FileHeader) error {
|
||||
file, err := header.Open()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if err := s.Filesystem().Writefile(p, file); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -89,8 +89,8 @@ func postUpdateConfiguration(c *gin.Context) {
|
||||
//
|
||||
// If you pass through manual locations in the API call this logic will be skipped.
|
||||
if strings.HasPrefix(cfg.Api.Ssl.KeyFile, "/etc/letsencrypt/live/") {
|
||||
cfg.Api.Ssl.KeyFile = ccopy.Api.Ssl.KeyFile
|
||||
cfg.Api.Ssl.CertificateFile = ccopy.Api.Ssl.CertificateFile
|
||||
cfg.Api.Ssl.KeyFile = strings.ToLower(ccopy.Api.Ssl.KeyFile)
|
||||
cfg.Api.Ssl.CertificateFile = strings.ToLower(ccopy.Api.Ssl.CertificateFile)
|
||||
}
|
||||
|
||||
config.Set(&cfg)
|
||||
|
||||
@@ -52,7 +52,7 @@ func getServerArchive(c *gin.Context) {
|
||||
|
||||
st, err := s.Archiver.Stat()
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
TrackedServerError(err, s).SetMessage("failed to stat archive").AbortWithServerError(c)
|
||||
return
|
||||
}
|
||||
@@ -94,27 +94,40 @@ func postServerArchive(c *gin.Context) {
|
||||
s := GetServer(c.Param("server"))
|
||||
|
||||
go func(s *server.Server) {
|
||||
r := api.New()
|
||||
|
||||
// Attempt to get an archive of the server. This **WILL NOT** modify the source files of a server,
|
||||
// this process is 100% safe and will not corrupt a server's files if it fails.
|
||||
if err := s.Archiver.Archive(); err != nil {
|
||||
s.Log().WithField("error", err).Error("failed to get archive for server")
|
||||
|
||||
if err := r.SendArchiveStatus(s.Id(), false); err != nil {
|
||||
if !api.IsRequestError(err) {
|
||||
s.Log().WithField("error", err).Error("failed to notify panel of failed archive status")
|
||||
return
|
||||
}
|
||||
|
||||
s.Log().WithField("error", err.Error()).Error("panel returned an error when notifying it of a failed archive status")
|
||||
return
|
||||
}
|
||||
|
||||
s.Log().Info("successfully notified panel of failed archive status")
|
||||
return
|
||||
}
|
||||
|
||||
s.Log().Debug("successfully created server archive, notifying panel")
|
||||
|
||||
r := api.NewRequester()
|
||||
rerr, err := r.SendArchiveStatus(s.Id(), true)
|
||||
if rerr != nil || err != nil {
|
||||
if err != nil {
|
||||
s.Log().WithField("error", err).Error("failed to notify panel of archive status")
|
||||
if err := r.SendArchiveStatus(s.Id(), true); err != nil {
|
||||
if !api.IsRequestError(err) {
|
||||
s.Log().WithField("error", err).Error("failed to notify panel of successful archive status")
|
||||
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 notifying it of a successful archive status")
|
||||
return
|
||||
}
|
||||
|
||||
s.Log().Debug("successfully notified panel of archive status")
|
||||
s.Log().Info("successfully notified panel of successful archive status")
|
||||
}(s)
|
||||
|
||||
c.Status(http.StatusAccepted)
|
||||
@@ -140,14 +153,13 @@ func postTransfer(c *gin.Context) {
|
||||
}
|
||||
|
||||
l.Info("server transfer failed, notifying panel")
|
||||
rerr, err := api.NewRequester().SendTransferFailure(serverID)
|
||||
if rerr != nil || err != nil {
|
||||
if err != nil {
|
||||
if err := api.New().SendTransferFailure(serverID); err != nil {
|
||||
if !api.IsRequestError(err) {
|
||||
l.WithField("error", err).Error("failed to notify panel with transfer failure")
|
||||
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
|
||||
}
|
||||
|
||||
@@ -157,7 +169,7 @@ func postTransfer(c *gin.Context) {
|
||||
// Make a new GET request to the URL the panel gave us.
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
log.WithField("error", errors.WithStack(err)).Error("failed to create http request for archive transfer")
|
||||
log.WithField("error", err).Error("failed to create http request for archive transfer")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -167,7 +179,7 @@ func postTransfer(c *gin.Context) {
|
||||
// Execute the http request.
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
l.WithField("error", errors.WithStack(err)).Error("failed to send archive http request")
|
||||
l.WithField("error", err).Error("failed to send archive http request")
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
@@ -176,13 +188,11 @@ func postTransfer(c *gin.Context) {
|
||||
if res.StatusCode != 200 {
|
||||
_, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
l.WithField("error", errors.WithStack(err)).WithField("status", res.StatusCode).Error("failed read transfer response body")
|
||||
|
||||
l.WithField("error", err).WithField("status", res.StatusCode).Error("failed read transfer response body")
|
||||
return
|
||||
}
|
||||
|
||||
l.WithField("error", errors.WithStack(err)).WithField("status", res.StatusCode).Error("failed to request server archive")
|
||||
|
||||
l.WithField("error", err).WithField("status", res.StatusCode).Error("failed to request server archive")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -190,16 +200,14 @@ func postTransfer(c *gin.Context) {
|
||||
archivePath := filepath.Join(config.Get().System.ArchiveDirectory, serverID+".tar.gz")
|
||||
|
||||
// Check if the archive already exists and delete it if it does.
|
||||
_, err = os.Stat(archivePath)
|
||||
if err != nil {
|
||||
if _, err = os.Stat(archivePath); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
l.WithField("error", errors.WithStack(err)).Error("failed to stat archive file")
|
||||
l.WithField("error", err).Error("failed to stat archive file")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := os.Remove(archivePath); err != nil {
|
||||
l.WithField("error", errors.WithStack(err)).Warn("failed to remove old archive file")
|
||||
|
||||
l.WithField("error", err).Warn("failed to remove old archive file")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -207,8 +215,7 @@ func postTransfer(c *gin.Context) {
|
||||
// Create the file.
|
||||
file, err := os.Create(archivePath)
|
||||
if err != nil {
|
||||
l.WithField("error", errors.WithStack(err)).Error("failed to open archive on disk")
|
||||
|
||||
l.WithField("error", err).Error("failed to open archive on disk")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -216,24 +223,35 @@ func postTransfer(c *gin.Context) {
|
||||
buf := make([]byte, 1024*4)
|
||||
_, err = io.CopyBuffer(file, res.Body, buf)
|
||||
if err != nil {
|
||||
l.WithField("error", errors.WithStack(err)).Error("failed to copy archive file to disk")
|
||||
|
||||
l.WithField("error", err).Error("failed to copy archive file to disk")
|
||||
return
|
||||
}
|
||||
|
||||
// Close the file so it can be opened to verify the checksum.
|
||||
if err := file.Close(); err != nil {
|
||||
l.WithField("error", errors.WithStack(err)).Error("failed to close archive file")
|
||||
|
||||
l.WithField("error", err).Error("failed to close archive file")
|
||||
return
|
||||
}
|
||||
|
||||
l.WithField("server", serverID).Debug("server archive downloaded, computing checksum...")
|
||||
// Whenever the transfer fails or succeeds, delete the temporary transfer archive.
|
||||
defer func() {
|
||||
log.WithField("server", serverID).Debug("deleting temporary transfer archive..")
|
||||
if err := os.Remove(archivePath); err != nil && !os.IsNotExist(err) {
|
||||
l.WithFields(log.Fields{
|
||||
"server": serverID,
|
||||
"error": err,
|
||||
}).Warn("failed to delete transfer archive")
|
||||
} else {
|
||||
l.Debug("deleted temporary transfer archive successfully")
|
||||
}
|
||||
}()
|
||||
|
||||
l.Debug("server archive downloaded, computing checksum...")
|
||||
|
||||
// Open the archive file for computing a checksum.
|
||||
file, err = os.Open(archivePath)
|
||||
if err != nil {
|
||||
l.WithField("error", errors.WithStack(err)).Error("failed to open archive on disk")
|
||||
l.WithField("error", err).Error("failed to open archive on disk")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -241,7 +259,7 @@ func postTransfer(c *gin.Context) {
|
||||
hash := sha256.New()
|
||||
buf = make([]byte, 1024*4)
|
||||
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", err).Error("failed to copy archive file for checksum verification")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -253,7 +271,7 @@ func postTransfer(c *gin.Context) {
|
||||
|
||||
// Close the file.
|
||||
if err := file.Close(); err != nil {
|
||||
l.WithField("error", errors.WithStack(err)).Error("failed to close archive file after calculating checksum")
|
||||
l.WithField("error", err).Error("failed to close archive file after calculating checksum")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -269,7 +287,7 @@ func postTransfer(c *gin.Context) {
|
||||
// Create a new server installer (note this does not execute the install script)
|
||||
i, err := installer.New(serverData)
|
||||
if err != nil {
|
||||
l.WithField("error", errors.WithStack(err)).Error("failed to validate received server data")
|
||||
l.WithField("error", err).Error("failed to validate received server data")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -282,9 +300,9 @@ func postTransfer(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 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 {
|
||||
l.WithField("error", errors.WithStack(err)).Error("failed to extract server archive")
|
||||
l.WithField("error", err).Error("failed to extract server archive")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -296,15 +314,14 @@ func postTransfer(c *gin.Context) {
|
||||
hasError = false
|
||||
|
||||
// Notify the panel that the transfer succeeded.
|
||||
rerr, err := api.NewRequester().SendTransferSuccess(serverID)
|
||||
if rerr != nil || err != nil {
|
||||
if err != nil {
|
||||
l.WithField("error", errors.WithStack(err)).Error("failed to notify panel of transfer success")
|
||||
err = api.New().SendTransferSuccess(serverID)
|
||||
if err != nil {
|
||||
if !api.IsRequestError(err) {
|
||||
l.WithField("error", err).Error("failed to notify panel of transfer success")
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,40 @@ package tokens
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/apex/log"
|
||||
"github.com/gbrlsnchs/jwt/v3"
|
||||
"strings"
|
||||
"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 {
|
||||
jwt.Payload
|
||||
sync.RWMutex
|
||||
@@ -24,6 +53,7 @@ func (p *WebsocketPayload) GetPayload() *jwt.Payload {
|
||||
return &p.Payload
|
||||
}
|
||||
|
||||
// Returns the UUID of the server associated with this JWT.
|
||||
func (p *WebsocketPayload) GetServerUuid() string {
|
||||
p.RLock()
|
||||
defer p.RUnlock()
|
||||
@@ -31,6 +61,33 @@ func (p *WebsocketPayload) GetServerUuid() string {
|
||||
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.
|
||||
func (p *WebsocketPayload) HasPermission(permission string) bool {
|
||||
p.RLock()
|
||||
@@ -38,7 +95,7 @@ func (p *WebsocketPayload) HasPermission(permission string) bool {
|
||||
|
||||
for _, k := range p.Permissions {
|
||||
if k == permission || (!strings.HasPrefix(permission, "admin") && k == "*") {
|
||||
return true
|
||||
return !p.Denylisted()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"github.com/pterodactyl/wings/environment/docker"
|
||||
"github.com/pterodactyl/wings/router/tokens"
|
||||
"github.com/pterodactyl/wings/server"
|
||||
"github.com/pterodactyl/wings/server/filesystem"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -33,10 +32,9 @@ const (
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
sync.RWMutex
|
||||
|
||||
Connection *websocket.Conn
|
||||
jwt *tokens.WebsocketPayload `json:"-"`
|
||||
sync.RWMutex `json:"-"`
|
||||
Connection *websocket.Conn `json:"-"`
|
||||
jwt *tokens.WebsocketPayload
|
||||
server *server.Server
|
||||
uuid uuid.UUID
|
||||
}
|
||||
@@ -45,12 +43,14 @@ var (
|
||||
ErrJwtNotPresent = errors.New("jwt: no jwt present")
|
||||
ErrJwtNoConnectPerm = errors.New("jwt: missing connect permission")
|
||||
ErrJwtUuidMismatch = errors.New("jwt: server uuid mismatch")
|
||||
ErrJwtOnDenylist = errors.New("jwt: created too far in past (denylist)")
|
||||
)
|
||||
|
||||
func IsJwtError(err error) bool {
|
||||
return errors.Is(err, ErrJwtNotPresent) ||
|
||||
errors.Is(err, ErrJwtNoConnectPerm) ||
|
||||
errors.Is(err, ErrJwtUuidMismatch) ||
|
||||
errors.Is(err, ErrJwtOnDenylist) ||
|
||||
errors.Is(err, jwt.ErrExpValidation)
|
||||
}
|
||||
|
||||
@@ -62,8 +62,12 @@ func NewTokenPayload(token []byte) (*tokens.WebsocketPayload, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if payload.Denylisted() {
|
||||
return nil, ErrJwtOnDenylist
|
||||
}
|
||||
|
||||
if !payload.HasPermission(PermissionConnect) {
|
||||
return nil, errors.New("not authorized to connect to this socket")
|
||||
return nil, ErrJwtNoConnectPerm
|
||||
}
|
||||
|
||||
return &payload, nil
|
||||
@@ -103,7 +107,7 @@ func GetHandler(s *server.Server, w http.ResponseWriter, r *http.Request) (*Hand
|
||||
|
||||
u, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Handler{
|
||||
@@ -125,7 +129,6 @@ func (h *Handler) SendJson(v *Message) error {
|
||||
Event: JwtErrorEvent,
|
||||
Args: []string{err.Error()},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -188,6 +191,10 @@ func (h *Handler) TokenValid() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if j.Denylisted() {
|
||||
return ErrJwtOnDenylist
|
||||
}
|
||||
|
||||
if !j.HasPermission(PermissionConnect) {
|
||||
return ErrJwtNoConnectPerm
|
||||
}
|
||||
@@ -204,25 +211,26 @@ func (h *Handler) TokenValid() error {
|
||||
// error message, otherwise we just send back a standard error message.
|
||||
func (h *Handler) SendErrorJson(msg Message, err error, shouldLog ...bool) error {
|
||||
j := h.GetJwt()
|
||||
expected := errors.Is(err, server.ErrSuspended) ||
|
||||
errors.Is(err, server.ErrIsRunning) ||
|
||||
errors.Is(err, filesystem.ErrNotEnoughDiskSpace)
|
||||
isJWTError := IsJwtError(err)
|
||||
|
||||
message := "an unexpected error was encountered while handling this request"
|
||||
if expected || (j != nil && j.HasPermission(PermissionReceiveErrors)) {
|
||||
message = err.Error()
|
||||
wsm := Message{
|
||||
Event: ErrorEvent,
|
||||
Args: []string{"an unexpected error was encountered while handling this request"},
|
||||
}
|
||||
|
||||
m, u := h.GetErrorMessage(message)
|
||||
if isJWTError || (j != nil && j.HasPermission(PermissionReceiveErrors)) {
|
||||
if isJWTError {
|
||||
wsm.Event = JwtErrorEvent
|
||||
}
|
||||
wsm.Args = []string{err.Error()}
|
||||
}
|
||||
|
||||
wsm := Message{Event: ErrorEvent}
|
||||
m, u := h.GetErrorMessage(wsm.Args[0])
|
||||
wsm.Args = []string{m}
|
||||
|
||||
if 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}).
|
||||
Error("failed to handle websocket process; an error was encountered processing an event")
|
||||
}
|
||||
if !isJWTError && (len(shouldLog) == 0 || (len(shouldLog) == 1 && shouldLog[0] == true)) {
|
||||
h.server.Log().WithFields(log.Fields{"event": msg.Event, "error_identifier": u.String(), "error": err}).
|
||||
Errorf("error processing websocket event \"%s\"", msg.Event)
|
||||
}
|
||||
|
||||
return h.unsafeSendJson(wsm)
|
||||
@@ -260,7 +268,6 @@ func (h *Handler) HandleInbound(m Message) error {
|
||||
Event: JwtErrorEvent,
|
||||
Args: []string{err.Error()},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -304,7 +311,7 @@ func (h *Handler) HandleInbound(m Message) error {
|
||||
|
||||
// On every authentication event, send the current server status back
|
||||
// to the client. :)
|
||||
state := h.server.GetState()
|
||||
state := h.server.Environment.State()
|
||||
h.SendJson(&Message{
|
||||
Event: server.StatusEvent,
|
||||
Args: []string{state},
|
||||
@@ -361,7 +368,7 @@ func (h *Handler) HandleInbound(m Message) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
logs, err := h.server.Environment.Readlog(100)
|
||||
logs, err := h.server.Environment.Readlog(config.Get().System.WebsocketLogCount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -391,7 +398,7 @@ func (h *Handler) HandleInbound(m Message) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if h.server.GetState() == environment.ProcessOfflineState {
|
||||
if h.server.Environment.State() == environment.ProcessOfflineState {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -399,7 +406,7 @@ func (h *Handler) HandleInbound(m Message) error {
|
||||
// so that we can better handle this and only set the environment to booted once we're attached.
|
||||
//
|
||||
// Or maybe just an IsBooted function?
|
||||
if h.server.GetState() == environment.ProcessStartingState {
|
||||
if h.server.Environment.State() == environment.ProcessStartingState {
|
||||
if e, ok := h.server.Environment.(*docker.Environment); ok {
|
||||
if !e.IsAttached() {
|
||||
return nil
|
||||
|
||||
@@ -41,7 +41,7 @@ func (a *Archiver) Exists() bool {
|
||||
func (a *Archiver) Stat() (*filesystem.Stat, error) {
|
||||
s, err := os.Stat(a.Path())
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &filesystem.Stat{
|
||||
@@ -72,7 +72,6 @@ func (a *Archiver) Archive() error {
|
||||
// and not the actual file in this listing.
|
||||
if file.Mode()&os.ModeSymlink != 0 {
|
||||
f, err = a.Server.Filesystem().SafePath(filepath.Join(path, file.Name()))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -98,11 +97,7 @@ func (a *Archiver) DeleteIfExists() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Remove(a.Path()); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return errors.WithMessage(os.Remove(a.Path()), "archiver: failed to delete archive from system")
|
||||
}
|
||||
|
||||
// Checksum computes a SHA256 checksum of the server's archive.
|
||||
|
||||
@@ -13,10 +13,8 @@ import (
|
||||
// Notifies the panel of a backup's state and returns an error if one is encountered
|
||||
// while performing this action.
|
||||
func (s *Server) notifyPanelOfBackup(uuid string, ad *backup.ArchiveDetails, successful bool) error {
|
||||
r := api.NewRequester()
|
||||
rerr, err := r.SendBackupStatus(uuid, ad.ToRequest(successful))
|
||||
if rerr != nil || err != nil {
|
||||
if err != nil {
|
||||
if err := api.New().SendBackupStatus(uuid, ad.ToRequest(successful)); err != nil {
|
||||
if !api.IsRequestError(err) {
|
||||
s.Log().WithFields(log.Fields{
|
||||
"backup": uuid,
|
||||
"error": err,
|
||||
@@ -25,7 +23,7 @@ func (s *Server) notifyPanelOfBackup(uuid string, ad *backup.ArchiveDetails, suc
|
||||
return err
|
||||
}
|
||||
|
||||
return errors.New(rerr.String())
|
||||
return errors.New(err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -80,7 +78,7 @@ func (s *Server) Backup(b backup.BackupInterface) error {
|
||||
// Get the included files based on the root path and the ignored files provided.
|
||||
inc, err := s.GetIncludedBackupFiles(b.Ignored())
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
ad, err := b.Generate(inc, s.Filesystem().Path())
|
||||
@@ -90,6 +88,11 @@ func (s *Server) Backup(b backup.BackupInterface) error {
|
||||
"backup": b.Identifier(),
|
||||
"error": notifyError,
|
||||
}).Warn("failed to notify panel of failed backup state")
|
||||
} else {
|
||||
s.Log().WithFields(log.Fields{
|
||||
"backup": b.Identifier(),
|
||||
"error": err,
|
||||
}).Info("notified panel of failed backup state")
|
||||
}
|
||||
|
||||
s.Events().PublishJson(BackupCompletedEvent+":"+b.Identifier(), map[string]interface{}{
|
||||
@@ -100,15 +103,17 @@ func (s *Server) Backup(b backup.BackupInterface) error {
|
||||
"file_size": 0,
|
||||
})
|
||||
|
||||
return errors.Wrap(err, "error while generating server backup")
|
||||
return errors.WithMessage(err, "backup: error while generating server backup")
|
||||
}
|
||||
|
||||
// Try to notify the panel about the status of this backup. If for some reason this request
|
||||
// fails, delete the archive from the daemon and return that error up the chain to the caller.
|
||||
if notifyError := s.notifyPanelOfBackup(b.Identifier(), ad, true); notifyError != nil {
|
||||
b.Remove()
|
||||
|
||||
return notifyError
|
||||
s.Log().WithField("error", notifyError).Info("failed to notify panel of successful backup state")
|
||||
return err
|
||||
} else {
|
||||
s.Log().WithField("backup", b.Identifier()).Info("notified panel of successful backup state")
|
||||
}
|
||||
|
||||
// Emit an event over the socket so we can update the backup in realtime on
|
||||
|
||||
@@ -26,7 +26,7 @@ type Archive struct {
|
||||
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)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
@@ -58,7 +58,7 @@ func (a *Archive) Create(dst string, ctx context.Context) error {
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return errors.WithStack(ctx.Err())
|
||||
return ctx.Err()
|
||||
default:
|
||||
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")
|
||||
}
|
||||
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -91,7 +91,7 @@ func (a *Archive) addToArchive(p string, w *tar.Writer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
@@ -102,17 +102,15 @@ func (a *Archive) addToArchive(p string, w *tar.Writer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
header := &tar.Header{
|
||||
// Trim the long server path from the name of the file so that the resulting
|
||||
// archive is exactly how the user would see it in the panel file manager.
|
||||
Name: strings.TrimPrefix(p, a.TrimPrefix),
|
||||
Size: s.Size(),
|
||||
Mode: int64(s.Mode()),
|
||||
ModTime: s.ModTime(),
|
||||
name := strings.TrimPrefix(p, a.TrimPrefix)
|
||||
header, err := tar.FileInfoHeader(s, name)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to get tar#FileInfoHeader for "+name)
|
||||
}
|
||||
header.Name = name
|
||||
|
||||
// These actions must occur sequentially, even if this function is called multiple
|
||||
// in parallel. You'll get some nasty panic's otherwise.
|
||||
@@ -120,12 +118,12 @@ func (a *Archive) addToArchive(p string, w *tar.Writer) error {
|
||||
defer a.Unlock()
|
||||
|
||||
if err := w.WriteHeader(header); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
buf := make([]byte, 4*1024)
|
||||
if _, err := io.CopyBuffer(w, f, buf); err != nil {
|
||||
return errors.WithStack(err)
|
||||
if _, err := io.CopyBuffer(w, io.LimitReader(f, header.Size), buf); err != nil {
|
||||
return errors.WithMessage(err, "failed to copy "+header.Name+" to archive")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"github.com/apex/log"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pterodactyl/wings/api"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"io"
|
||||
@@ -87,7 +86,7 @@ func (b *Backup) Path() string {
|
||||
func (b *Backup) Size() (int64, error) {
|
||||
st, err := os.Stat(b.Path())
|
||||
if err != nil {
|
||||
return 0, errors.WithStack(err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return st.Size(), nil
|
||||
@@ -99,7 +98,7 @@ func (b *Backup) Checksum() ([]byte, error) {
|
||||
|
||||
f, err := os.Open(b.Path())
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
@@ -128,6 +127,7 @@ func (b *Backup) Details() *ArchiveDetails {
|
||||
"backup": b.Identifier(),
|
||||
"error": err,
|
||||
}).Error("failed to calculate checksum for backup")
|
||||
return
|
||||
}
|
||||
|
||||
checksum = hex.EncodeToString(resp)
|
||||
|
||||
@@ -2,7 +2,7 @@ package backup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/pkg/errors"
|
||||
"errors"
|
||||
"os"
|
||||
)
|
||||
|
||||
@@ -24,7 +24,7 @@ func LocateLocal(uuid string) (*LocalBackup, os.FileInfo, error) {
|
||||
|
||||
st, err := os.Stat(b.Path())
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithStack(err)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, errors.WithStack(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b.Details(), nil
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
Adapter string `json:"adapter"`
|
||||
Uuid string `json:"uuid"`
|
||||
IgnoredFiles []string `json:"ignored_files"`
|
||||
PresignedUrl string `json:"presigned_url"`
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
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{
|
||||
Backup: Backup{
|
||||
Uuid: r.Uuid,
|
||||
IgnoredFiles: r.IgnoredFiles,
|
||||
},
|
||||
PresignedUrl: r.PresignedUrl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/apex/log"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pterodactyl/wings/api"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -13,12 +13,6 @@ import (
|
||||
|
||||
type S3Backup struct {
|
||||
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)
|
||||
@@ -34,26 +28,20 @@ func (s *S3Backup) Generate(included *IncludedFiles, prefix string) (*ArchiveDet
|
||||
}
|
||||
|
||||
if err := a.Create(s.Path(), context.Background()); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rc, err := os.Open(s.Path())
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
return nil, err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
if resp, err := s.generateRemoteRequest(rc); err != nil {
|
||||
return nil, errors.WithStack(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)
|
||||
}
|
||||
if err := s.generateRemoteRequest(rc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.Details(), err
|
||||
return s.Details(), nil
|
||||
}
|
||||
|
||||
// Removes a backup from the system.
|
||||
@@ -61,27 +49,88 @@ func (s *S3Backup) Remove() error {
|
||||
return os.Remove(s.Path())
|
||||
}
|
||||
|
||||
// Generates the remote S3 request and begins the upload.
|
||||
func (s *S3Backup) generateRemoteRequest(rc io.ReadCloser) (*http.Response, error) {
|
||||
r, err := http.NewRequest(http.MethodPut, s.PresignedUrl, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if sz, err := s.Size(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
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{
|
||||
"endpoint": s.PresignedUrl,
|
||||
"headers": r.Header,
|
||||
}).Debug("uploading backup to remote S3 endpoint")
|
||||
|
||||
return http.DefaultClient.Do(r)
|
||||
// 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.
|
||||
func (s *S3Backup) generateRemoteRequest(rc io.ReadCloser) error {
|
||||
defer rc.Close()
|
||||
|
||||
size, err := s.Backup.Size()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
urls, err := api.New().GetBackupRemoteUploadURLs(s.Backup.Uuid, size)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l := log.WithFields(log.Fields{
|
||||
"backup_id": s.Uuid,
|
||||
"adapter": "s3",
|
||||
})
|
||||
|
||||
l.Info("attempting to upload backup..")
|
||||
|
||||
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{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
|
||||
}
|
||||
|
||||
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.
|
||||
if _, err := handlePart(part, partSize); err != nil {
|
||||
l.WithField("part_id", part).WithError(err).Warn("failed to upload part")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
l.Info("backup has been successfully uploaded")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -45,11 +45,10 @@ func (s *Server) handleServerCrash() error {
|
||||
// No point in doing anything here if the server isn't currently offline, there
|
||||
// is no reason to do a crash detection event. If the server crash detection is
|
||||
// disabled we want to skip anything after this as well.
|
||||
if s.GetState() != environment.ProcessOfflineState || !s.Config().CrashDetectionEnabled {
|
||||
if s.Environment.State() != environment.ProcessOfflineState || !s.Config().CrashDetectionEnabled {
|
||||
if !s.Config().CrashDetectionEnabled {
|
||||
s.Log().Debug("server triggered crash detection but handler is disabled for server process")
|
||||
|
||||
s.PublishConsoleOutputFromDaemon("Server detected as crashed; crash detection is disabled for this instance.")
|
||||
s.PublishConsoleOutputFromDaemon("Aborting automatic restart, crash detection is disabled for this instance.")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -57,14 +56,13 @@ func (s *Server) handleServerCrash() error {
|
||||
|
||||
exitCode, oomKilled, err := s.Environment.ExitState()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// If the system is not configured to detect a clean exit code as a crash, and the
|
||||
// crash is not the result of the program running out of memory, do nothing.
|
||||
if exitCode == 0 && !oomKilled && !config.Get().System.DetectCleanExitAsCrash {
|
||||
if exitCode == 0 && !oomKilled && !config.Get().System.CrashDetection.DetectCleanExitAsCrash {
|
||||
s.Log().Debug("server exited with successful exit code; system is configured to not detect this as a crash")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -73,11 +71,14 @@ func (s *Server) handleServerCrash() error {
|
||||
s.PublishConsoleOutputFromDaemon(fmt.Sprintf("Out of memory: %t", oomKilled))
|
||||
|
||||
c := s.crasher.LastCrashTime()
|
||||
// If the last crash time was within the last 60 seconds we do not want to perform
|
||||
// an automatic reboot of the process. Return an error that can be handled.
|
||||
if !c.IsZero() && c.Add(time.Second*60).After(time.Now()) {
|
||||
s.PublishConsoleOutputFromDaemon("Aborting automatic reboot: last crash occurred less than 60 seconds ago.")
|
||||
timeout := config.Get().System.CrashDetection.Timeout
|
||||
|
||||
// If the last crash time was within the last `timeout` seconds we do not want to perform
|
||||
// an automatic reboot of the process. Return an error that can be handled.
|
||||
//
|
||||
// If timeout is set to 0, always reboot the server (this is probably a terrible idea, but some people want it)
|
||||
if timeout != 0 && !c.IsZero() && c.Add(time.Second*time.Duration(config.Get().System.CrashDetection.Timeout)).After(time.Now()) {
|
||||
s.PublishConsoleOutputFromDaemon("Aborting automatic restart, last crash occurred less than " + strconv.Itoa(timeout) + " seconds ago.")
|
||||
return &crashTooFrequent{}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package server
|
||||
|
||||
import "github.com/pkg/errors"
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var ErrIsRunning = errors.New("server is running")
|
||||
var ErrSuspended = errors.New("server is currently in a suspended state")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pterodactyl/wings/server/filesystem"
|
||||
"os"
|
||||
)
|
||||
@@ -13,12 +12,12 @@ func (s *Server) Filesystem() *filesystem.Filesystem {
|
||||
// Ensures that the data directory for the server instance exists.
|
||||
func (s *Server) EnsureDataDirectoryExists() error {
|
||||
if _, err := os.Stat(s.fs.Path()); err != nil && !os.IsNotExist(err) {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
} else if err != nil {
|
||||
// Create the server data directory because it does not currently exist
|
||||
// on the system.
|
||||
if err := os.MkdirAll(s.fs.Path(), 0700); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.fs.Chown("/"); err != nil {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/karrick/godirwalk"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pterodactyl/wings/server/backup"
|
||||
ignore "github.com/sabhiram/go-gitignore"
|
||||
"os"
|
||||
@@ -41,7 +40,7 @@ func (fs *Filesystem) GetIncludedFiles(dir string, ignored []string) (*backup.In
|
||||
if e.IsSymlink() {
|
||||
sp, err = fs.SafePath(p)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrBadPathResolution) {
|
||||
if IsBadPathResolutionError(err) {
|
||||
return godirwalk.SkipThis
|
||||
}
|
||||
|
||||
@@ -65,7 +64,7 @@ func (fs *Filesystem) GetIncludedFiles(dir string, ignored []string) (*backup.In
|
||||
},
|
||||
})
|
||||
|
||||
return inc, errors.WithStack(err)
|
||||
return inc, err
|
||||
}
|
||||
|
||||
// Compresses all of the files matching the given paths in the specified directory. This function
|
||||
@@ -115,7 +114,7 @@ func (fs *Filesystem) CompressFiles(dir string, paths []string) (os.FileInfo, er
|
||||
// use the resolved location for the rest of this function.
|
||||
sp, err = fs.SafePath(p)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrBadPathResolution) {
|
||||
if IsBadPathResolutionError(err) {
|
||||
return godirwalk.SkipThis
|
||||
}
|
||||
|
||||
@@ -141,7 +140,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), ":", "")))
|
||||
|
||||
if err := a.Create(d, context.Background()); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f, err := os.Stat(d)
|
||||
|
||||
@@ -47,10 +47,10 @@ func (fs *Filesystem) SpaceAvailableForDecompression(dir string, file string) (b
|
||||
return false, ErrUnknownArchiveFormat
|
||||
}
|
||||
|
||||
return false, errors.WithStack(err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, errors.WithStack(err)
|
||||
return true, err
|
||||
}
|
||||
|
||||
// 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 {
|
||||
source, err := fs.SafePath(filepath.Join(dir, file))
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Make sure the file exists basically.
|
||||
if _, err := os.Stat(source); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 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))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate a safe path to server file")
|
||||
return errors.WithMessage(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.WithMessage(fs.Writefile(p, f), "could not extract file from archive")
|
||||
})
|
||||
if err != nil {
|
||||
if strings.HasPrefix(err.Error(), "format ") {
|
||||
return errors.WithStack(ErrUnknownArchiveFormat)
|
||||
return ErrUnknownArchiveFormat
|
||||
}
|
||||
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -3,7 +3,6 @@ package filesystem
|
||||
import (
|
||||
"github.com/apex/log"
|
||||
"github.com/karrick/godirwalk"
|
||||
"github.com/pkg/errors"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
@@ -75,6 +74,16 @@ func (fs *Filesystem) HasSpaceAvailable(allowStaleValue bool) bool {
|
||||
return size <= fs.MaxDisk()
|
||||
}
|
||||
|
||||
// Returns the cached value for the amount of disk space used by the filesystem. Do not rely on this
|
||||
// function for critical logical checks. It should only be used in areas where the actual disk usage
|
||||
// does not need to be perfect, e.g. API responses for server resource usage.
|
||||
func (fs *Filesystem) CachedUsage() int64 {
|
||||
fs.mu.RLock()
|
||||
defer fs.mu.RUnlock()
|
||||
|
||||
return fs.diskUsed
|
||||
}
|
||||
|
||||
// Internal helper function to allow other parts of the codebase to check the total used disk space
|
||||
// as needed without overly taxing the system. This will prioritize the value from the cache to avoid
|
||||
// excessive IO usage. We will only walk the filesystem and determine the size of the directory if there
|
||||
@@ -87,6 +96,11 @@ func (fs *Filesystem) HasSpaceAvailable(allowStaleValue bool) bool {
|
||||
// This is primarily to avoid a bunch of I/O operations from piling up on the server, especially on servers
|
||||
// with a large amount of files.
|
||||
func (fs *Filesystem) DiskUsage(allowStaleValue bool) (int64, error) {
|
||||
// A disk check interval of 0 means this functionality is completely disabled.
|
||||
if fs.diskCheckInterval == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if !fs.lastLookupTime.Get().After(time.Now().Add(time.Second * fs.diskCheckInterval * -1)) {
|
||||
// If we are now allowing a stale response go ahead and perform the lookup and return the fresh
|
||||
// value. This is a blocking operation to the calling process.
|
||||
@@ -143,7 +157,7 @@ func (fs *Filesystem) updateCachedDiskUsage() (int64, error) {
|
||||
func (fs *Filesystem) DirectorySize(dir string) (int64, error) {
|
||||
d, err := fs.SafePath(dir)
|
||||
if err != nil {
|
||||
return 0, errors.WithStack(err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var size int64
|
||||
@@ -157,7 +171,7 @@ func (fs *Filesystem) DirectorySize(dir string) (int64, error) {
|
||||
// it. Otherwise, allow it to continue.
|
||||
if e.IsSymlink() {
|
||||
if _, err := fs.SafePath(p); err != nil {
|
||||
if errors.Is(err, ErrBadPathResolution) {
|
||||
if IsBadPathResolutionError(err) {
|
||||
return godirwalk.SkipThis
|
||||
}
|
||||
|
||||
@@ -174,7 +188,7 @@ func (fs *Filesystem) DirectorySize(dir string) (int64, error) {
|
||||
},
|
||||
})
|
||||
|
||||
return size, errors.WithStack(err)
|
||||
return size, err
|
||||
}
|
||||
|
||||
// Helper function to determine if a server has space available for a file of a given size.
|
||||
@@ -199,7 +213,8 @@ func (fs *Filesystem) hasSpaceFor(size int64) error {
|
||||
|
||||
// Updates the disk usage for the Filesystem instance.
|
||||
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
|
||||
// now without completely re-evaluating the logic we use for determining disk space.
|
||||
//
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/apex/log"
|
||||
"github.com/pkg/errors"
|
||||
"os"
|
||||
@@ -9,9 +10,42 @@ import (
|
||||
|
||||
var ErrIsDirectory = errors.New("filesystem: is a directory")
|
||||
var ErrNotEnoughDiskSpace = errors.New("filesystem: not enough disk space")
|
||||
var ErrBadPathResolution = errors.New("filesystem: invalid path resolution")
|
||||
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.
|
||||
func (fs *Filesystem) error(err error) *log.Entry {
|
||||
return log.WithField("subsystem", "filesystem").WithField("root", fs.root).WithField("error", err)
|
||||
@@ -23,7 +57,7 @@ func (fs *Filesystem) error(err error) *log.Entry {
|
||||
// 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.
|
||||
func (fs *Filesystem) handleWalkerError(err error, f os.FileInfo) error {
|
||||
if !errors.Is(err, ErrBadPathResolution) {
|
||||
if !IsBadPathResolutionError(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
24
server/filesystem/errors_test.go
Normal file
24
server/filesystem/errors_test.go
Normal 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>")
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -80,7 +80,7 @@ func (fs *Filesystem) Readfile(p string, w io.Writer) error {
|
||||
func (fs *Filesystem) Writefile(p string, r io.Reader) error {
|
||||
cleaned, err := fs.SafePath(p)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
var currentSize int64
|
||||
@@ -88,15 +88,15 @@ 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.
|
||||
if stat, err := os.Stat(cleaned); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(cleaned), 0755); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fs.Chown(filepath.Dir(cleaned)); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if stat.IsDir() {
|
||||
@@ -119,7 +119,7 @@ func (fs *Filesystem) Writefile(p string, r io.Reader) error {
|
||||
// truncate the existing file.
|
||||
file, err := o.open(cleaned, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
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 {
|
||||
cleaned, err := fs.SafePath(path.Join(p, name))
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
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 {
|
||||
cleanedFrom, err := fs.SafePath(from)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
cleanedTo, err := fs.SafePath(to)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 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.
|
||||
if d != fs.Path() {
|
||||
if mkerr := os.MkdirAll(d, 0755); mkerr != nil {
|
||||
return errors.Wrap(mkerr, "failed to create directory structure for file rename")
|
||||
return errors.WithMessage(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 {
|
||||
cleaned, err := fs.SafePath(path)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if fs.isTest {
|
||||
@@ -197,7 +197,7 @@ func (fs *Filesystem) Chown(path string) error {
|
||||
|
||||
// Start by just chowning the initial path that we received.
|
||||
if err := os.Chown(cleaned, uid, gid); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// If this is not a directory we can now return from the function, there is nothing
|
||||
@@ -227,6 +227,23 @@ func (fs *Filesystem) Chown(path string) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (fs *Filesystem) Chmod(path string, mode os.FileMode) error {
|
||||
cleaned, err := fs.SafePath(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if fs.isTest {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.Chmod(cleaned, mode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Begin looping up to 50 times to try and create a unique copy file name. This will take
|
||||
// an input of "file.txt" and generate "file copy.txt". If that name is already taken, it will
|
||||
// then try to write "file copy 2.txt" and so on, until reaching 50 loops. At that point we
|
||||
@@ -248,7 +265,7 @@ 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
|
||||
// does exist, we'll just continue to the next loop and try again.
|
||||
if _, err := fs.Stat(path.Join(dir, n)); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -268,12 +285,12 @@ func (fs *Filesystem) findCopySuffix(dir string, name string, extension string)
|
||||
func (fs *Filesystem) Copy(p string) error {
|
||||
cleaned, err := fs.SafePath(p)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
s, err := os.Stat(cleaned)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
} else if s.IsDir() || !s.Mode().IsRegular() {
|
||||
// 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.
|
||||
@@ -300,11 +317,14 @@ func (fs *Filesystem) Copy(p string) error {
|
||||
|
||||
source, err := os.Open(cleaned)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
defer source.Close()
|
||||
|
||||
n, err := fs.findCopySuffix(relative, name, extension)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fs.Writefile(path.Join(relative, n), source)
|
||||
}
|
||||
@@ -324,7 +344,7 @@ func (fs *Filesystem) Delete(p string) error {
|
||||
// exists within the data directory.
|
||||
resolved := fs.unsafeFilePath(p)
|
||||
if !fs.unsafeIsInDataDirectory(resolved) {
|
||||
return ErrBadPathResolution
|
||||
return NewBadPathResolution(p, resolved)
|
||||
}
|
||||
|
||||
// Block any whoopsies.
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"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) {
|
||||
g := Goblin(t)
|
||||
fs, rfs := NewFs()
|
||||
@@ -324,12 +107,12 @@ func TestFilesystem_Readfile(t *testing.T) {
|
||||
|
||||
err = fs.Readfile("/../test.txt", buf)
|
||||
g.Assert(err).IsNotNil()
|
||||
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
|
||||
g.Assert(IsBadPathResolutionError(err)).IsTrue()
|
||||
})
|
||||
|
||||
g.AfterEach(func() {
|
||||
buf.Truncate(0)
|
||||
fs.diskUsed = 0
|
||||
atomic.StoreInt64(&fs.diskUsed, 0)
|
||||
rfs.reset()
|
||||
})
|
||||
})
|
||||
@@ -347,7 +130,7 @@ func TestFilesystem_Writefile(t *testing.T) {
|
||||
g.It("can create a new file", func() {
|
||||
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)
|
||||
g.Assert(err).IsNil()
|
||||
@@ -355,7 +138,7 @@ func TestFilesystem_Writefile(t *testing.T) {
|
||||
err = fs.Readfile("test.txt", buf)
|
||||
g.Assert(err).IsNil()
|
||||
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() {
|
||||
@@ -385,11 +168,11 @@ func TestFilesystem_Writefile(t *testing.T) {
|
||||
|
||||
err := fs.Writefile("/some/../foo/../../test.txt", r)
|
||||
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() {
|
||||
fs.diskLimit = 1024
|
||||
g.It("cannot write a file that exceeds the disk limits", func() {
|
||||
atomic.StoreInt64(&fs.diskLimit, 1024)
|
||||
|
||||
b := make([]byte, 1025)
|
||||
_, err := rand.Read(b)
|
||||
@@ -402,8 +185,8 @@ func TestFilesystem_Writefile(t *testing.T) {
|
||||
g.Assert(errors.Is(err, ErrNotEnoughDiskSpace)).IsTrue()
|
||||
})
|
||||
|
||||
g.It("updates the total space used when a file is appended to", func() {
|
||||
fs.diskUsed = 100
|
||||
/*g.It("updates the total space used when a file is appended to", func() {
|
||||
atomic.StoreInt64(&fs.diskUsed, 100)
|
||||
|
||||
b := make([]byte, 100)
|
||||
_, _ = rand.Read(b)
|
||||
@@ -411,7 +194,7 @@ func TestFilesystem_Writefile(t *testing.T) {
|
||||
r := bytes.NewReader(b)
|
||||
err := fs.Writefile("test.txt", r)
|
||||
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
|
||||
// disk used to be decremented.
|
||||
@@ -421,8 +204,8 @@ func TestFilesystem_Writefile(t *testing.T) {
|
||||
r = bytes.NewReader(b)
|
||||
err = fs.Writefile("test.txt", r)
|
||||
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() {
|
||||
r := bytes.NewReader([]byte("original data"))
|
||||
@@ -441,8 +224,9 @@ func TestFilesystem_Writefile(t *testing.T) {
|
||||
g.AfterEach(func() {
|
||||
buf.Truncate(0)
|
||||
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() {
|
||||
err := fs.CreateDirectory("test", "e/../../something")
|
||||
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() {
|
||||
err := fs.CreateDirectory("test", "/")
|
||||
g.Assert(err).IsNil()
|
||||
g.Assert(fs.diskUsed).Equal(int64(0))
|
||||
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(0))
|
||||
})
|
||||
|
||||
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() {
|
||||
err := fs.Rename("source.txt", "../target.txt")
|
||||
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() {
|
||||
@@ -533,7 +317,7 @@ func TestFilesystem_Rename(t *testing.T) {
|
||||
|
||||
err = fs.Rename("/../ext-source.txt", "target.txt")
|
||||
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() {
|
||||
@@ -597,7 +381,7 @@ func TestFilesystem_Copy(t *testing.T) {
|
||||
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() {
|
||||
@@ -611,7 +395,7 @@ func TestFilesystem_Copy(t *testing.T) {
|
||||
|
||||
err = fs.Copy("../ext-source.txt")
|
||||
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() {
|
||||
@@ -623,11 +407,11 @@ func TestFilesystem_Copy(t *testing.T) {
|
||||
|
||||
err = fs.Copy("../nested/in/dir/ext-source.txt")
|
||||
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")
|
||||
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() {
|
||||
@@ -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() {
|
||||
fs.diskLimit = 2
|
||||
atomic.StoreInt64(&fs.diskLimit, 2)
|
||||
|
||||
err := fs.Copy("source.txt")
|
||||
g.Assert(err).IsNotNil()
|
||||
@@ -672,7 +456,7 @@ func TestFilesystem_Copy(t *testing.T) {
|
||||
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() {
|
||||
@@ -694,8 +478,9 @@ func TestFilesystem_Copy(t *testing.T) {
|
||||
|
||||
g.AfterEach(func() {
|
||||
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)
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -718,7 +503,7 @@ func TestFilesystem_Delete(t *testing.T) {
|
||||
|
||||
err = fs.Delete("../ext-source.txt")
|
||||
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() {
|
||||
@@ -744,7 +529,7 @@ func TestFilesystem_Delete(t *testing.T) {
|
||||
g.Assert(err).IsNotNil()
|
||||
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() {
|
||||
@@ -762,11 +547,11 @@ func TestFilesystem_Delete(t *testing.T) {
|
||||
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")
|
||||
g.Assert(err).IsNil()
|
||||
g.Assert(fs.diskUsed).Equal(int64(0))
|
||||
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(0))
|
||||
|
||||
for _, s := range sources {
|
||||
_, err = rfs.StatServerFile(s)
|
||||
@@ -777,8 +562,9 @@ func TestFilesystem_Delete(t *testing.T) {
|
||||
|
||||
g.AfterEach(func() {
|
||||
rfs.reset()
|
||||
fs.diskUsed = 0
|
||||
fs.diskLimit = 0
|
||||
|
||||
atomic.StoreInt64(&fs.diskUsed, 0)
|
||||
atomic.StoreInt64(&fs.diskLimit, 0)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func (fs *Filesystem) SafePath(p string) (string, error) {
|
||||
|
||||
// At the same time, evaluate the symlink status and determine where this file or folder
|
||||
// is truly pointing to.
|
||||
p, err := filepath.EvalSymlinks(r)
|
||||
ep, err := filepath.EvalSymlinks(r)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return "", err
|
||||
} else if os.IsNotExist(err) {
|
||||
@@ -53,7 +53,7 @@ func (fs *Filesystem) SafePath(p string) (string, error) {
|
||||
// attempt going on, and we should NOT resolve this path for them.
|
||||
if nonExistentPathResolution != "" {
|
||||
if !fs.unsafeIsInDataDirectory(nonExistentPathResolution) {
|
||||
return "", ErrBadPathResolution
|
||||
return "", NewBadPathResolution(p, nonExistentPathResolution)
|
||||
}
|
||||
|
||||
// If the nonExistentPathResolution variable is not empty then the initial path requested
|
||||
@@ -66,11 +66,11 @@ func (fs *Filesystem) SafePath(p string) (string, error) {
|
||||
// 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
|
||||
// on the file.
|
||||
if fs.unsafeIsInDataDirectory(p) {
|
||||
return p, nil
|
||||
if fs.unsafeIsInDataDirectory(ep) {
|
||||
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
|
||||
|
||||
228
server/filesystem/path_test.go
Normal file
228
server/filesystem/path_test.go
Normal file
@@ -0,0 +1,228 @@
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
. "github.com/franela/goblin"
|
||||
"github.com/pkg/errors"
|
||||
"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()
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -18,16 +19,19 @@ func (s *Stat) MarshalJSON() ([]byte, error) {
|
||||
Created string `json:"created"`
|
||||
Modified string `json:"modified"`
|
||||
Mode string `json:"mode"`
|
||||
ModeBits string `json:"mode_bits"`
|
||||
Size int64 `json:"size"`
|
||||
Directory bool `json:"directory"`
|
||||
File bool `json:"file"`
|
||||
Symlink bool `json:"symlink"`
|
||||
Mime string `json:"mime"`
|
||||
}{
|
||||
Name: s.Info.Name(),
|
||||
Created: s.CTime().Format(time.RFC3339),
|
||||
Modified: s.Info.ModTime().Format(time.RFC3339),
|
||||
Mode: s.Info.Mode().String(),
|
||||
Name: s.Info.Name(),
|
||||
Created: s.CTime().Format(time.RFC3339),
|
||||
Modified: s.Info.ModTime().Format(time.RFC3339),
|
||||
Mode: s.Info.Mode().String(),
|
||||
// Using `&os.ModePerm` on the file's mode will cause the mode to only have the permission values, and nothing else.
|
||||
ModeBits: strconv.FormatUint(uint64(s.Info.Mode()&os.ModePerm), 8),
|
||||
Size: s.Info.Size(),
|
||||
Directory: s.Info.IsDir(),
|
||||
File: !s.Info.IsDir(),
|
||||
|
||||
@@ -9,5 +9,6 @@ import (
|
||||
func (s *Stat) CTime() time.Time {
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -63,7 +64,7 @@ func (s *Server) Install(sync bool) error {
|
||||
|
||||
// Ensure that the server is marked as offline at this point, otherwise you end up
|
||||
// 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
|
||||
// the install is completed.
|
||||
@@ -75,7 +76,7 @@ func (s *Server) Install(sync bool) error {
|
||||
// Reinstalls a server's software by utilizing the install script for the server egg. This
|
||||
// does not touch any existing files for the server, other than what the script modifies.
|
||||
func (s *Server) Reinstall() error {
|
||||
if s.GetState() != environment.ProcessOfflineState {
|
||||
if s.Environment.State() != environment.ProcessOfflineState {
|
||||
s.Log().Debug("waiting for server instance to enter a stopped state")
|
||||
if err := s.Environment.WaitForStop(10, true); err != nil {
|
||||
return err
|
||||
@@ -87,18 +88,18 @@ func (s *Server) Reinstall() error {
|
||||
|
||||
// Internal installation function used to simplify reporting back to the Panel.
|
||||
func (s *Server) internalInstall() error {
|
||||
script, rerr, err := api.NewRequester().GetInstallationScript(s.Id())
|
||||
if err != nil || rerr != nil {
|
||||
if err != nil {
|
||||
script, err := api.New().GetInstallationScript(s.Id())
|
||||
if err != nil {
|
||||
if !api.IsRequestError(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return errors.New(rerr.String())
|
||||
return errors.New(err.Error())
|
||||
}
|
||||
|
||||
p, err := NewInstallationProcess(s, &script)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
s.Log().Info("beginning installation process for server")
|
||||
@@ -129,8 +130,8 @@ func NewInstallationProcess(s *Server, script *api.InstallationScript) (*Install
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s.installer.cancel = &cancel
|
||||
|
||||
if c, err := client.NewClientWithOpts(client.FromEnv); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
if c, err := environment.DockerClient(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
proc.client = c
|
||||
proc.context = ctx
|
||||
@@ -193,7 +194,7 @@ func (ip *InstallationProcess) RemoveContainer() {
|
||||
})
|
||||
|
||||
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", err).Warn("failed to delete server install container")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,14 +219,14 @@ func (ip *InstallationProcess) Run() error {
|
||||
}()
|
||||
|
||||
if err := ip.BeforeExecute(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
cid, err := ip.Execute()
|
||||
if err != nil {
|
||||
ip.RemoveContainer()
|
||||
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// If this step fails, log a warning but don't exit out of the process. This is completely
|
||||
@@ -248,12 +249,12 @@ func (ip *InstallationProcess) writeScriptToDisk() error {
|
||||
// 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.
|
||||
if err := os.MkdirAll(ip.tempDir(), 0700); err != nil {
|
||||
return errors.Wrap(err, "could not create temporary directory for install process")
|
||||
return errors.WithMessage(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)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to write server installation script to disk before mount")
|
||||
return errors.WithMessage(err, "failed to write server installation script to disk before mount")
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
@@ -265,7 +266,7 @@ func (ip *InstallationProcess) writeScriptToDisk() error {
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
w.Flush()
|
||||
@@ -275,11 +276,62 @@ func (ip *InstallationProcess) writeScriptToDisk() error {
|
||||
|
||||
// Pulls the docker image to be used for the installation container.
|
||||
func (ip *InstallationProcess) pullInstallationImage() error {
|
||||
r, err := ip.client.ImagePull(ip.context, ip.Script.ContainerImage, types.ImagePullOptions{})
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
// Get a registry auth configuration from the config.
|
||||
var registryAuth *config.RegistryConfiguration
|
||||
for registry, c := range config.Get().Docker.Registries {
|
||||
if !strings.HasPrefix(ip.Script.ContainerImage, registry) {
|
||||
continue
|
||||
}
|
||||
|
||||
log.WithField("registry", registry).Debug("using authentication for registry")
|
||||
registryAuth = &c
|
||||
break
|
||||
}
|
||||
|
||||
// Get the ImagePullOptions.
|
||||
imagePullOptions := types.ImagePullOptions{All: false}
|
||||
if registryAuth != nil {
|
||||
b64, err := registryAuth.Base64()
|
||||
if err != nil {
|
||||
log.WithError(err).Error("failed to get registry auth credentials")
|
||||
}
|
||||
|
||||
// b64 is a string so if there is an error it will just be empty, not nil.
|
||||
imagePullOptions.RegistryAuth = b64
|
||||
}
|
||||
|
||||
r, err := ip.client.ImagePull(context.Background(), ip.Script.ContainerImage, imagePullOptions)
|
||||
if err != nil {
|
||||
images, ierr := ip.client.ImageList(context.Background(), types.ImageListOptions{})
|
||||
if ierr != nil {
|
||||
// Well damn, something has gone really wrong here, just go ahead and abort there
|
||||
// isn't much anything we can do to try and self-recover from this.
|
||||
return ierr
|
||||
}
|
||||
|
||||
for _, img := range images {
|
||||
for _, t := range img.RepoTags {
|
||||
if t != ip.Script.ContainerImage {
|
||||
continue
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"image": ip.Script.ContainerImage,
|
||||
"err": err.Error(),
|
||||
}).Warn("unable to pull requested image from remote source, however the image exists locally")
|
||||
|
||||
// Okay, we found a matching container image, in that case just go ahead and return
|
||||
// from this function, since there is nothing else we need to do here.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
log.WithField("image", ip.Script.ContainerImage).Debug("pulling docker image... this could take a bit of time")
|
||||
|
||||
// Block continuation until the image has been pulled successfully.
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
@@ -287,7 +339,7 @@ func (ip *InstallationProcess) pullInstallationImage() error {
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -298,11 +350,11 @@ func (ip *InstallationProcess) pullInstallationImage() error {
|
||||
// manner, if either one fails the error is returned.
|
||||
func (ip *InstallationProcess) BeforeExecute() error {
|
||||
if err := ip.writeScriptToDisk(); err != nil {
|
||||
return errors.Wrap(err, "failed to write installation script to disk")
|
||||
return errors.WithMessage(err, "failed to write installation script to disk")
|
||||
}
|
||||
|
||||
if err := ip.pullInstallationImage(); err != nil {
|
||||
return errors.Wrap(err, "failed to pull updated installation container image for server")
|
||||
return errors.WithMessage(err, "failed to pull updated installation container image for server")
|
||||
}
|
||||
|
||||
opts := types.ContainerRemoveOptions{
|
||||
@@ -312,7 +364,7 @@ func (ip *InstallationProcess) BeforeExecute() error {
|
||||
|
||||
if err := ip.client.ContainerRemove(ip.context, ip.Server.Id()+"_installer", opts); err != nil {
|
||||
if !client.IsErrNotFound(err) {
|
||||
return errors.Wrap(err, "failed to remove existing install container for server")
|
||||
return errors.WithMessage(err, "failed to remove existing install container for server")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,12 +390,12 @@ func (ip *InstallationProcess) AfterExecute(containerId string) error {
|
||||
})
|
||||
|
||||
if err != nil && !client.IsErrNotFound(err) {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(ip.GetLogPath(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
@@ -372,15 +424,15 @@ func (ip *InstallationProcess) AfterExecute(containerId string) error {
|
||||
| ------------------------------
|
||||
`)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tmpl.Execute(f, ip); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(f, reader); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -448,7 +500,7 @@ func (ip *InstallationProcess) Execute() (string, error) {
|
||||
|
||||
r, err := ip.client.ContainerCreate(ip.context, conf, hostConf, nil, ip.Server.Id()+"_installer")
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
ip.Server.Log().WithField("container_id", r.ID).Info("running installation script for server in container")
|
||||
@@ -468,7 +520,7 @@ func (ip *InstallationProcess) Execute() (string, error) {
|
||||
select {
|
||||
case err := <-eChan:
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
return "", err
|
||||
}
|
||||
case <-sChan:
|
||||
}
|
||||
@@ -487,7 +539,7 @@ func (ip *InstallationProcess) StreamOutput(id string) error {
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
defer reader.Close()
|
||||
@@ -500,7 +552,7 @@ func (ip *InstallationProcess) StreamOutput(id string) error {
|
||||
if err := s.Err(); err != nil {
|
||||
ip.Server.Log().WithFields(log.Fields{
|
||||
"container_id": id,
|
||||
"error": errors.WithStack(err),
|
||||
"error": err,
|
||||
}).Warn("error processing scanner line in installation output for server")
|
||||
}
|
||||
|
||||
@@ -512,15 +564,13 @@ func (ip *InstallationProcess) StreamOutput(id string) error {
|
||||
// value of "true" means everything was successful, "false" means something went
|
||||
// wrong and the server must be deleted and re-created.
|
||||
func (s *Server) SyncInstallState(successful bool) error {
|
||||
r := api.NewRequester()
|
||||
|
||||
rerr, err := r.SendInstallationStatus(s.Id(), successful)
|
||||
if rerr != nil || err != nil {
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
err := api.New().SendInstallationStatus(s.Id(), successful)
|
||||
if err != nil {
|
||||
if !api.IsRequestError(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return errors.New(rerr.String())
|
||||
return errors.New(err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -3,13 +3,13 @@ package server
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/apex/log"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pterodactyl/wings/api"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
"github.com/pterodactyl/wings/events"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var dockerEvents = []string{
|
||||
@@ -18,6 +18,37 @@ var dockerEvents = []string{
|
||||
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
|
||||
// removed by deleting the server as they should last for the duration of the process' lifetime.
|
||||
func (s *Server) StartEventListeners() {
|
||||
@@ -31,8 +62,8 @@ func (s *Server) StartEventListeners() {
|
||||
if err != nil {
|
||||
// If the process is already stopping, just let it continue with that action rather than attempting
|
||||
// to terminate again.
|
||||
if s.GetState() != environment.ProcessStoppingState {
|
||||
s.SetState(environment.ProcessStoppingState)
|
||||
if s.Environment.State() != environment.ProcessStoppingState {
|
||||
s.Environment.SetState(environment.ProcessStoppingState)
|
||||
go func() {
|
||||
s.Log().Warn("stopping server instance, violating throttle limits")
|
||||
s.PublishConsoleOutputFromDaemon("Your server is being stopped for outputting too much data in a short period of time.")
|
||||
@@ -41,11 +72,11 @@ func (s *Server) StartEventListeners() {
|
||||
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
|
||||
// again and hopefully kills the server.
|
||||
if s.GetState() != environment.ProcessOfflineState {
|
||||
s.SetState(environment.ProcessRunningState)
|
||||
if s.Environment.State() != environment.ProcessOfflineState {
|
||||
s.Environment.SetState(environment.ProcessRunningState)
|
||||
}
|
||||
|
||||
s.Log().WithField("error", errors.WithStack(err)).Error("failed to terminate environment after triggering throttle")
|
||||
s.Log().WithField("error", err).Error("failed to terminate environment after triggering throttle")
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -60,19 +91,21 @@ func (s *Server) StartEventListeners() {
|
||||
s.onConsoleOutput(e.Data)
|
||||
}
|
||||
|
||||
l := newDiskLimiter(s)
|
||||
state := func(e events.Event) {
|
||||
// Reset the throttler when the process is started.
|
||||
if e.Data == environment.ProcessStartingState {
|
||||
l.Reset()
|
||||
s.Throttler().Reset()
|
||||
}
|
||||
|
||||
s.SetState(e.Data)
|
||||
s.OnStateChange()
|
||||
}
|
||||
|
||||
stats := func(e events.Event) {
|
||||
st := new(environment.Stats)
|
||||
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", err).Warn("failed to unmarshal server environment stats")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -81,7 +114,11 @@ func (s *Server) StartEventListeners() {
|
||||
s.resources.Stats = *st
|
||||
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()
|
||||
}
|
||||
@@ -114,7 +151,7 @@ func (s *Server) onConsoleOutput(data string) {
|
||||
processConfiguration := s.ProcessConfiguration()
|
||||
|
||||
// Check if the server is currently starting.
|
||||
if s.GetState() == environment.ProcessStartingState {
|
||||
if s.Environment.State() == environment.ProcessStartingState {
|
||||
// Check if we should strip ansi color codes.
|
||||
if processConfiguration.Startup.StripAnsi {
|
||||
// Strip ansi color codes from the data string.
|
||||
@@ -135,7 +172,7 @@ func (s *Server) onConsoleOutput(data string) {
|
||||
// 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
|
||||
// or stopping.
|
||||
_ = s.SetState(environment.ProcessRunningState)
|
||||
s.Environment.SetState(environment.ProcessRunningState)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -147,7 +184,7 @@ func (s *Server) onConsoleOutput(data string) {
|
||||
stop := processConfiguration.Stop
|
||||
|
||||
if stop.Type == api.ProcessStopCommand && data == stop.Value {
|
||||
_ = s.SetState(environment.ProcessOfflineState)
|
||||
s.Environment.SetState(environment.ProcessOfflineState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/apex/log"
|
||||
"github.com/creasty/defaults"
|
||||
@@ -31,28 +32,40 @@ func LoadDirectory() error {
|
||||
}
|
||||
|
||||
log.Info("fetching list of servers from API")
|
||||
configs, rerr, err := api.NewRequester().GetAllServerConfigurations()
|
||||
if err != nil || rerr != nil {
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
configs, err := api.New().GetServers()
|
||||
if err != nil {
|
||||
if !api.IsRequestError(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return errors.New(rerr.String())
|
||||
return errors.New(err.Error())
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
log.WithField("total_configs", len(configs)).Info("processing servers returned by the API")
|
||||
|
||||
pool := workerpool.New(runtime.NumCPU())
|
||||
for uuid, data := range configs {
|
||||
uuid := uuid
|
||||
log.Debugf("using %d workerpools to instantiate server instances", runtime.NumCPU())
|
||||
for _, data := range configs {
|
||||
data := data
|
||||
|
||||
pool.Submit(func() {
|
||||
log.WithField("server", uuid).Info("creating new server object from API response")
|
||||
s, err := FromConfiguration(data)
|
||||
// Parse the json.RawMessage into an expected struct value. We do this here so that a single broken
|
||||
// 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 {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -73,15 +86,15 @@ func LoadDirectory() error {
|
||||
// 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
|
||||
// for a server.
|
||||
func FromConfiguration(data *api.ServerConfigurationResponse) (*Server, error) {
|
||||
func FromConfiguration(data api.ServerConfigurationResponse) (*Server, error) {
|
||||
cfg := Configuration{}
|
||||
if err := defaults.Set(&cfg); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to set struct defaults for server configuration")
|
||||
return nil, errors.WithMessage(err, "failed to set struct defaults for server configuration")
|
||||
}
|
||||
|
||||
s := new(Server)
|
||||
if err := defaults.Set(s); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to set struct defaults for server")
|
||||
return nil, errors.WithMessage(err, "failed to set struct defaults for server")
|
||||
}
|
||||
|
||||
s.cfg = cfg
|
||||
@@ -91,6 +104,7 @@ func FromConfiguration(data *api.ServerConfigurationResponse) (*Server, error) {
|
||||
|
||||
s.resources = ResourceUsage{}
|
||||
defaults.Set(&s.resources)
|
||||
s.resources.State.Store(environment.ProcessOfflineState)
|
||||
|
||||
s.Archiver = Archiver{Server: s}
|
||||
s.fs = filesystem.New(filepath.Join(config.Get().System.Data, s.Id()), s.DiskSpace())
|
||||
|
||||
@@ -2,10 +2,8 @@ package server
|
||||
|
||||
import (
|
||||
"github.com/apex/log"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
@@ -16,41 +14,17 @@ import (
|
||||
type Mount environment.Mount
|
||||
|
||||
// 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
|
||||
// servers running within the container will use the correct time.
|
||||
// for the server. Previously this would also mount in host timezone files, however we've moved from
|
||||
// that approach to just setting `TZ=Timezone` environment values in containers which should work
|
||||
// in most scenarios.
|
||||
func (s *Server) Mounts() []environment.Mount {
|
||||
var m []environment.Mount
|
||||
|
||||
m = append(m, environment.Mount{
|
||||
Default: true,
|
||||
Target: "/home/container",
|
||||
Source: s.Filesystem().Path(),
|
||||
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,
|
||||
})
|
||||
m := []environment.Mount{
|
||||
{
|
||||
Default: true,
|
||||
Target: "/home/container",
|
||||
Source: s.Filesystem().Path(),
|
||||
ReadOnly: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Also include any of this server's custom mounts when returning them.
|
||||
@@ -75,7 +49,9 @@ func (s *Server) customMounts() []environment.Mount {
|
||||
|
||||
mounted := false
|
||||
for _, allowed := range config.Get().AllowedMounts {
|
||||
if !strings.HasPrefix(source, allowed) {
|
||||
// Check if the source path is included in the allowed mounts list.
|
||||
// filepath.Clean will strip all trailing slashes (unless the path is a root directory).
|
||||
if !strings.HasPrefix(source, filepath.Clean(allowed)) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
// request will be aborted.
|
||||
if err := s.powerLock.Acquire(ctx, 1); err != nil {
|
||||
return errors.Wrap(err, "could not acquire lock on power state")
|
||||
return errors.WithMessage(err, "could not acquire lock on power state")
|
||||
}
|
||||
} else {
|
||||
// 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.
|
||||
if ok := s.powerLock.TryAcquire(1); !ok {
|
||||
return errors.Wrap(context.DeadlineExceeded, "could not acquire lock on power state")
|
||||
return errors.WithMessage(context.DeadlineExceeded, "could not acquire lock on power state")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ func (s *Server) HandlePowerAction(action PowerAction, waitSeconds ...int) error
|
||||
|
||||
switch action {
|
||||
case PowerActionStart:
|
||||
if s.GetState() != environment.ProcessOfflineState {
|
||||
if s.Environment.State() != environment.ProcessOfflineState {
|
||||
return ErrIsRunning
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ func (s *Server) HandlePowerAction(action PowerAction, waitSeconds ...int) error
|
||||
func (s *Server) onBeforeStart() error {
|
||||
s.Log().Info("syncing server configuration with panel")
|
||||
if err := s.Sync(); err != nil {
|
||||
return errors.Wrap(err, "unable to sync server data from Panel instance")
|
||||
return errors.WithMessage(err, "unable to sync server data from Panel instance")
|
||||
}
|
||||
|
||||
// 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...")
|
||||
// Ensure all of the server file permissions are set correctly before booting the process.
|
||||
if err := s.Filesystem().Chown("/"); err != nil {
|
||||
return errors.Wrap(err, "failed to chown root server directory during pre-boot process")
|
||||
return errors.WithMessage(err, "failed to chown root server directory during pre-boot process")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
"github.com/pterodactyl/wings/system"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// Defines the current resource usage for a given server instance. If a server is offline you
|
||||
@@ -15,19 +18,36 @@ type ResourceUsage struct {
|
||||
environment.Stats
|
||||
|
||||
// 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 is cached to prevent slow lookup
|
||||
// issues on frequent refreshes.
|
||||
// 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
|
||||
// hacky solution for now to avoid passing events all over the place.
|
||||
Disk int64 `json:"disk_bytes"`
|
||||
}
|
||||
|
||||
// Custom marshaler to ensure that the object is locked when we're converting it to JSON in
|
||||
// order to avoid race conditions.
|
||||
func (ru *ResourceUsage) MarshalJSON() ([]byte, error) {
|
||||
ru.mu.Lock()
|
||||
defer ru.mu.Unlock()
|
||||
|
||||
// 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
|
||||
// disk space currently used will be returned. When the server is running all of the other stats will
|
||||
// be returned.
|
||||
//
|
||||
// When a process is stopped all of the stats are zeroed out except for the disk.
|
||||
func (s *Server) Proc() *ResourceUsage {
|
||||
// Store the updated disk usage when requesting process usage.
|
||||
atomic.StoreInt64(&s.resources.Disk, s.Filesystem().CachedUsage())
|
||||
|
||||
// Acquire a lock before attempting to return the value of resources.
|
||||
s.resources.mu.RLock()
|
||||
defer s.resources.mu.RUnlock()
|
||||
|
||||
@@ -35,30 +55,7 @@ func (s *Server) Proc() *ResourceUsage {
|
||||
}
|
||||
|
||||
func (s *Server) emitProcUsage() {
|
||||
s.resources.mu.RLock()
|
||||
if err := s.Events().PublishJson(StatsEvent, s.resources); err != nil {
|
||||
if err := s.Events().PublishJson(StatsEvent, s.Proc()); err != nil {
|
||||
s.Log().WithField("error", err).Warn("error while emitting server resource usage to listeners")
|
||||
}
|
||||
s.resources.mu.RUnlock()
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/apex/log"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pterodactyl/wings/api"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
"github.com/pterodactyl/wings/environment/docker"
|
||||
"github.com/pterodactyl/wings/events"
|
||||
@@ -13,7 +14,6 @@ import (
|
||||
"golang.org/x/sync/semaphore"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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
|
||||
// server instance.
|
||||
func (s *Server) GetEnvironmentVariables() []string {
|
||||
zone, _ := time.Now().In(time.Local).Zone()
|
||||
|
||||
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("SERVER_MEMORY=%d", s.MemoryLimit()),
|
||||
fmt.Sprintf("SERVER_IP=%s", s.Config().Allocations.DefaultMapping.Ip),
|
||||
@@ -90,6 +88,7 @@ func (s *Server) GetEnvironmentVariables() []string {
|
||||
|
||||
eloop:
|
||||
for k := range s.Config().EnvVars {
|
||||
// Don't allow any environment variables that we have already set above.
|
||||
for _, e := range out {
|
||||
if strings.HasPrefix(e, strings.ToUpper(k)) {
|
||||
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
|
||||
// will automatically sync with Wings when the server is started.
|
||||
func (s *Server) Sync() error {
|
||||
cfg, rerr, err := s.GetProcessConfiguration()
|
||||
if err != nil || rerr != nil {
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
cfg, err := api.New().GetServerConfiguration(s.Id())
|
||||
if err != nil {
|
||||
if !api.IsRequestError(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if rerr.Status == "404" {
|
||||
if err.(*api.RequestError).Status == "404" {
|
||||
return &serverDoesNotExist{}
|
||||
}
|
||||
|
||||
return errors.New(rerr.String())
|
||||
return errors.New(err.Error())
|
||||
}
|
||||
|
||||
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.
|
||||
if err := s.UpdateDataStructure(cfg.Settings); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
s.Lock()
|
||||
@@ -148,7 +147,7 @@ func (s *Server) SyncWithConfiguration(cfg *api.ServerConfigurationResponse) err
|
||||
if e, ok := s.Environment.(*docker.Environment); ok {
|
||||
s.Log().Debug("syncing stop configuration with configured docker environment")
|
||||
e.SetImage(s.Config().Container.Image)
|
||||
e.SetStopConfiguration(&cfg.ProcessConfiguration.Stop)
|
||||
e.SetStopConfiguration(cfg.ProcessConfiguration.Stop)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -172,17 +171,12 @@ func (s *Server) IsBootable() bool {
|
||||
func (s *Server) CreateEnvironment() error {
|
||||
// Ensure the data directory exists before getting too far through this process.
|
||||
if err := s.EnsureDataDirectoryExists(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
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.
|
||||
func (s *Server) IsSuspended() bool {
|
||||
return s.Config().Suspended
|
||||
|
||||
@@ -2,8 +2,6 @@ package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
"io"
|
||||
@@ -23,14 +21,14 @@ func CachedServerStates() (map[string]string, error) {
|
||||
// Open the states file.
|
||||
f, err := os.OpenFile(config.Get().System.GetStatesPath(), os.O_RDONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Convert the json object to a map.
|
||||
states := map[string]string{}
|
||||
if err := json.NewDecoder(f).Decode(&states); err != nil && err != io.EOF {
|
||||
return nil, errors.WithStack(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return states, nil
|
||||
@@ -41,13 +39,13 @@ func saveServerStates() error {
|
||||
// Get the states of all servers on the daemon.
|
||||
states := map[string]string{}
|
||||
for _, s := range GetServers().All() {
|
||||
states[s.Id()] = s.GetState()
|
||||
states[s.Id()] = s.Environment.State()
|
||||
}
|
||||
|
||||
// Convert the map to a json object.
|
||||
data, err := json.Marshal(states)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
stateMutex.Lock()
|
||||
@@ -55,7 +53,7 @@ func saveServerStates() error {
|
||||
|
||||
// Write the data to the file
|
||||
if err := ioutil.WriteFile(config.Get().System.GetStatesPath(), data, 0644); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -63,23 +61,17 @@ func saveServerStates() error {
|
||||
|
||||
// Sets the state of the server internally. This function handles crash detection as
|
||||
// well as reporting to event listeners for the server.
|
||||
func (s *Server) SetState(state string) error {
|
||||
if state != environment.ProcessOfflineState &&
|
||||
state != environment.ProcessStartingState &&
|
||||
state != environment.ProcessRunningState &&
|
||||
state != environment.ProcessStoppingState {
|
||||
return errors.New(fmt.Sprintf("invalid server state received: %s", state))
|
||||
}
|
||||
|
||||
prevState := s.GetState()
|
||||
func (s *Server) OnStateChange() {
|
||||
prevState := s.Proc().State.Load()
|
||||
|
||||
st := s.Environment.State()
|
||||
// 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.
|
||||
if prevState != state {
|
||||
s.Log().WithField("status", s.Proc().getInternalState()).Debug("saw server status change event")
|
||||
s.Events().Publish(StatusEvent, s.Proc().getInternalState())
|
||||
if prevState != s.Environment.State() {
|
||||
s.Log().WithField("status", st).Debug("saw server status change event")
|
||||
s.Events().Publish(StatusEvent, st)
|
||||
}
|
||||
|
||||
// Persist this change to the disk immediately so that should the Daemon be stopped or
|
||||
@@ -98,7 +90,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
|
||||
// views in the Panel correctly display 0.
|
||||
if state == environment.ProcessOfflineState {
|
||||
if st == environment.ProcessOfflineState {
|
||||
s.resources.mu.Lock()
|
||||
s.resources.Empty()
|
||||
s.resources.mu.Unlock()
|
||||
@@ -114,7 +106,7 @@ func (s *Server) SetState(state string) error {
|
||||
// automatically attempt to start the process back up for the user. This is done in a
|
||||
// separate thread as to not block any actions currently taking place in the flow
|
||||
// that called this function.
|
||||
if (prevState == environment.ProcessStartingState || prevState == environment.ProcessRunningState) && s.GetState() == environment.ProcessOfflineState {
|
||||
if (prevState == environment.ProcessStartingState || prevState == environment.ProcessRunningState) && s.Environment.State() == environment.ProcessOfflineState {
|
||||
s.Log().Info("detected server as entering a crashed state; running crash handler")
|
||||
|
||||
go func(server *Server) {
|
||||
@@ -122,25 +114,26 @@ func (s *Server) SetState(state string) error {
|
||||
if IsTooFrequentCrashError(err) {
|
||||
server.Log().Info("did not restart server after crash; occurred too soon after the last")
|
||||
} else {
|
||||
s.PublishConsoleOutputFromDaemon("Server crash was detected but an error occurred while handling it.")
|
||||
server.Log().WithField("error", err).Error("failed to handle server crash")
|
||||
}
|
||||
}
|
||||
}(s)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns the current state of the server in a race-safe manner.
|
||||
// Deprecated
|
||||
// use Environment.State()
|
||||
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
|
||||
// environment state, it is simply the tracked state from this daemon instance, and
|
||||
// not the response from Docker.
|
||||
func (s *Server) IsRunning() bool {
|
||||
st := s.GetState()
|
||||
st := s.Environment.State()
|
||||
|
||||
return st == environment.ProcessRunningState || st == environment.ProcessStartingState
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
func (s *Server) UpdateDataStructure(data []byte) error {
|
||||
src := new(Configuration)
|
||||
if err := json.Unmarshal(data, src); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 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
|
||||
// and then save it to the disk so it is persistent.
|
||||
if err := mergo.Merge(&c, src, mergo.WithOverride); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 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.
|
||||
if v, err := jsonparser.GetBoolean(data, "container", "oom_disabled"); err != nil {
|
||||
if err != jsonparser.KeyPathNotFoundError {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
c.Build.OOMDisabled = v
|
||||
@@ -74,7 +74,7 @@ func (s *Server) UpdateDataStructure(data []byte) error {
|
||||
// Mergo also cannot handle this boolean value.
|
||||
if v, err := jsonparser.GetBoolean(data, "suspended"); err != nil {
|
||||
if err != jsonparser.KeyPathNotFoundError {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
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 err != jsonparser.KeyPathNotFoundError {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
c.SkipEggScripts = v
|
||||
@@ -143,7 +143,7 @@ func (s *Server) SyncWithEnvironment() {
|
||||
} else {
|
||||
// Checks if the server is now in a suspended state. If so and a server process is currently running it
|
||||
// will be gracefully stopped (and terminated if it refuses to stop).
|
||||
if s.GetState() != environment.ProcessOfflineState {
|
||||
if s.Environment.State() != environment.ProcessOfflineState {
|
||||
s.Log().Info("server suspended with running process state, terminating now")
|
||||
|
||||
go func(s *Server) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
type WebsocketBag struct {
|
||||
mu sync.Mutex
|
||||
mu sync.Mutex
|
||||
conns map[uuid.UUID]*context.CancelFunc
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package sftp
|
||||
import (
|
||||
"github.com/apex/log"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pkg/sftp"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -58,14 +57,14 @@ func (fs FileSystem) Fileread(request *sftp.Request) (io.ReaderAt, error) {
|
||||
if _, err := os.Stat(p); os.IsNotExist(err) {
|
||||
return nil, sftp.ErrSshFxNoSuchFile
|
||||
} else if err != nil {
|
||||
fs.logger.WithField("error", errors.WithStack(err)).Error("error while processing file stat")
|
||||
fs.logger.WithField("error", err).Error("error while processing file stat")
|
||||
|
||||
return nil, sftp.ErrSshFxFailure
|
||||
}
|
||||
|
||||
file, err := os.Open(p)
|
||||
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", err).Error("could not open file for reading")
|
||||
return nil, sftp.ErrSshFxFailure
|
||||
}
|
||||
|
||||
@@ -108,7 +107,7 @@ func (fs FileSystem) Filewrite(request *sftp.Request) (io.WriterAt, error) {
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
|
||||
l.WithFields(log.Fields{
|
||||
"path": filepath.Dir(p),
|
||||
"error": errors.WithStack(err),
|
||||
"error": err,
|
||||
}).Error("error making path for file")
|
||||
|
||||
return nil, sftp.ErrSshFxFailure
|
||||
@@ -116,7 +115,7 @@ func (fs FileSystem) Filewrite(request *sftp.Request) (io.WriterAt, error) {
|
||||
|
||||
file, err := os.Create(p)
|
||||
if err != nil {
|
||||
l.WithField("error", errors.WithStack(err)).Error("failed to create file")
|
||||
l.WithField("error", err).Error("failed to create file")
|
||||
|
||||
return nil, sftp.ErrSshFxFailure
|
||||
}
|
||||
@@ -124,7 +123,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
|
||||
// and will likely cause some issues.
|
||||
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", err).Warn("failed to set permissions on file")
|
||||
}
|
||||
|
||||
return file, nil
|
||||
@@ -133,7 +132,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
|
||||
// at play and we need to go ahead and bail out of the process.
|
||||
if statErr != nil {
|
||||
l.WithField("error", errors.WithStack(statErr)).Error("encountered error performing file stat")
|
||||
l.WithField("error", statErr).Error("encountered error performing file stat")
|
||||
|
||||
return nil, sftp.ErrSshFxFailure
|
||||
}
|
||||
@@ -159,14 +158,14 @@ func (fs FileSystem) Filewrite(request *sftp.Request) (io.WriterAt, error) {
|
||||
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", err).Error("failed to open existing file on system")
|
||||
return nil, sftp.ErrSshFxFailure
|
||||
}
|
||||
|
||||
// Not failing here is intentional. We still made the file, it is just owned incorrectly
|
||||
// and will likely cause some issues.
|
||||
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", err).Warn("error chowning file")
|
||||
}
|
||||
|
||||
return file, nil
|
||||
@@ -220,7 +219,7 @@ func (fs FileSystem) Filecmd(request *sftp.Request) error {
|
||||
return sftp.ErrSSHFxNoSuchFile
|
||||
}
|
||||
|
||||
l.WithField("error", errors.WithStack(err)).Error("failed to perform setstat on item")
|
||||
l.WithField("error", err).Error("failed to perform setstat on item")
|
||||
return sftp.ErrSSHFxFailure
|
||||
}
|
||||
return nil
|
||||
@@ -234,7 +233,7 @@ func (fs FileSystem) Filecmd(request *sftp.Request) error {
|
||||
return sftp.ErrSSHFxNoSuchFile
|
||||
}
|
||||
|
||||
l.WithField("target", target).WithField("error", errors.WithStack(err)).Error("failed to rename file")
|
||||
l.WithField("target", target).WithField("error", err).Error("failed to rename file")
|
||||
|
||||
return sftp.ErrSshFxFailure
|
||||
}
|
||||
@@ -246,7 +245,7 @@ func (fs FileSystem) Filecmd(request *sftp.Request) error {
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(p); err != nil {
|
||||
l.WithField("error", errors.WithStack(err)).Error("failed to remove directory")
|
||||
l.WithField("error", err).Error("failed to remove directory")
|
||||
|
||||
return sftp.ErrSshFxFailure
|
||||
}
|
||||
@@ -258,7 +257,7 @@ func (fs FileSystem) Filecmd(request *sftp.Request) error {
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(p, 0755); err != nil {
|
||||
l.WithField("error", errors.WithStack(err)).Error("failed to create directory")
|
||||
l.WithField("error", err).Error("failed to create directory")
|
||||
|
||||
return sftp.ErrSshFxFailure
|
||||
}
|
||||
@@ -270,7 +269,7 @@ func (fs FileSystem) Filecmd(request *sftp.Request) error {
|
||||
}
|
||||
|
||||
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", err).Error("failed to create symlink")
|
||||
|
||||
return sftp.ErrSshFxFailure
|
||||
}
|
||||
@@ -286,7 +285,7 @@ func (fs FileSystem) Filecmd(request *sftp.Request) error {
|
||||
return sftp.ErrSSHFxNoSuchFile
|
||||
}
|
||||
|
||||
l.WithField("error", errors.WithStack(err)).Error("failed to remove a file")
|
||||
l.WithField("error", err).Error("failed to remove a file")
|
||||
|
||||
return sftp.ErrSshFxFailure
|
||||
}
|
||||
@@ -305,7 +304,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
|
||||
// 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 {
|
||||
l.WithField("error", errors.WithStack(err)).Warn("error chowning file")
|
||||
l.WithField("error", err).Warn("error chowning file")
|
||||
}
|
||||
|
||||
return sftp.ErrSshFxOk
|
||||
@@ -327,7 +326,7 @@ func (fs FileSystem) Filelist(request *sftp.Request) (sftp.ListerAt, error) {
|
||||
|
||||
files, err := ioutil.ReadDir(p)
|
||||
if err != nil {
|
||||
fs.logger.WithField("error", errors.WithStack(err)).Error("error while listing directory")
|
||||
fs.logger.WithField("error", err).Error("error while listing directory")
|
||||
|
||||
return nil, sftp.ErrSshFxFailure
|
||||
}
|
||||
@@ -342,7 +341,7 @@ func (fs FileSystem) Filelist(request *sftp.Request) (sftp.ListerAt, error) {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, sftp.ErrSshFxNoSuchFile
|
||||
} 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", err).Error("error performing stat on file")
|
||||
|
||||
return nil, sftp.ErrSshFxFailure
|
||||
}
|
||||
|
||||
@@ -28,14 +28,14 @@ func Initialize(config config.SystemConfiguration) error {
|
||||
}
|
||||
|
||||
if err := New(s); err != nil {
|
||||
return errors.WithStack(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Initialize the SFTP server in a background thread since this is
|
||||
// a long running operation.
|
||||
go func(s *Server) {
|
||||
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", err).Error("failed to initialize SFTP subsystem")
|
||||
}
|
||||
}(s)
|
||||
|
||||
@@ -72,7 +72,7 @@ func validateCredentials(c api.SftpAuthRequest) (*api.SftpAuthResponse, error) {
|
||||
f := log.Fields{"subsystem": "sftp", "username": c.User, "ip": c.IP}
|
||||
|
||||
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 api.IsInvalidCredentialsError(err) {
|
||||
log.WithFields(f).Warn("failed to validate user credentials (invalid username or password)")
|
||||
|
||||
@@ -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
58
system/utils.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user