Compare commits
89 Commits
| 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 |
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"]
|
||||||
36
.github/workflows/build-test.yml
vendored
36
.github/workflows/build-test.yml
vendored
@@ -1,12 +1,10 @@
|
|||||||
name: "Build & Test"
|
name: Run Tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches-ignore:
|
branches-ignore:
|
||||||
- 'master'
|
- 'master'
|
||||||
- 'release/**'
|
- 'release/**'
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
@@ -15,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-20.04 ]
|
os: [ ubuntu-20.04 ]
|
||||||
go: [ 1.15 ]
|
go: [ 1.15.6 ]
|
||||||
goos: [ linux ]
|
goos: [ linux ]
|
||||||
goarch: [ amd64, arm, arm64 ]
|
goarch: [ amd64, arm, arm64 ]
|
||||||
|
|
||||||
@@ -28,12 +26,40 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
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
|
- name: Build
|
||||||
env:
|
env:
|
||||||
GOOS: ${{ matrix.goos }}
|
GOOS: ${{ matrix.goos }}
|
||||||
GOARCH: ${{ matrix.goarch }}
|
GOARCH: ${{ matrix.goarch }}
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
SRC_PATH: github.com/pterodactyl/wings
|
||||||
run: |
|
run: |
|
||||||
go build -v -ldflags="-s -w -X github.com/pterodactyl/wings/system.Version=dev-${GIT_COMMIT:0:7}" -o build/wings_${{ matrix.goos }}_${{ matrix.goarch }} wings.go
|
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
|
- name: Test
|
||||||
run: go test ./...
|
run: go test ./...
|
||||||
|
|||||||
13
.github/workflows/codeql-analysis.yml
vendored
13
.github/workflows/codeql-analysis.yml
vendored
@@ -1,16 +1,12 @@
|
|||||||
name: "Code scanning - action"
|
name: CodeQL Scanning
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- 'develop'
|
||||||
pull_request:
|
pull_request:
|
||||||
schedule:
|
|
||||||
- cron: '0 21 * * 6'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
CodeQL-Build:
|
CodeQL-Build:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -18,12 +14,11 @@ jobs:
|
|||||||
# We must fetch at least the immediate parents so that if this is
|
# We must fetch at least the immediate parents so that if this is
|
||||||
# a pull request then we can checkout the head.
|
# a pull request then we can checkout the head.
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
# If this run was triggered by a pull request event, then checkout
|
# If this run was triggered by a pull request event, then checkout
|
||||||
# the head of the pull request instead of the merge commit.
|
# the head of the pull request instead of the merge commit.
|
||||||
- run: git checkout HEAD^2
|
- run: git checkout HEAD^2
|
||||||
if: ${{ github.event_name == 'pull_request' }}
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@v1
|
||||||
|
|||||||
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 }}
|
||||||
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -1,10 +1,8 @@
|
|||||||
name: "Release"
|
name: Create Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
@@ -20,9 +18,9 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
REF: ${{ github.ref }}
|
REF: ${{ github.ref }}
|
||||||
run: |
|
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
|
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
|
||||||
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=arm64 go build -ldflags="-s -w -X github.com/pterodactyl/wings/system.Version=${REF:11}" -o build/wings_linux_arm64 -v wings.go
|
||||||
GOOS=linux GOARCH=arm go build -ldflags="-s -w -X github.com/pterodactyl/wings/system.Version=${REF:11}" -o build/wings_linux_arm -v wings.go
|
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
|
- name: Test
|
||||||
run: go test ./...
|
run: go test ./...
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -47,3 +47,5 @@ test_*/
|
|||||||
debug
|
debug
|
||||||
data/.states.json
|
data/.states.json
|
||||||
.DS_Store
|
.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
|
|
||||||
55
CHANGELOG.md
55
CHANGELOG.md
@@ -1,6 +1,59 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## next
|
## 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
|
### Added
|
||||||
* Adds support for ARM to build outputs for wings.
|
* Adds support for ARM to build outputs for wings.
|
||||||
|
|
||||||
|
|||||||
11
Dockerfile
11
Dockerfile
@@ -1,14 +1,11 @@
|
|||||||
# ----------------------------------
|
|
||||||
# Pterodactyl Panel Dockerfile
|
|
||||||
# ----------------------------------
|
|
||||||
|
|
||||||
FROM golang:1.15-alpine
|
FROM golang:1.15-alpine
|
||||||
|
ARG VERSION="develop"
|
||||||
COPY . /go/wings/
|
COPY . /go/wings/
|
||||||
WORKDIR /go/wings/
|
WORKDIR /go/wings/
|
||||||
RUN apk add --no-cache upx \
|
RUN apk add --no-cache upx \
|
||||||
&& go build -ldflags="-s -w" \
|
&& CGO_ENABLED=0 go build -ldflags="-s -w -X github.com/pterodactyl/wings/system.Version=${VERSION}" \
|
||||||
&& upx --brute wings
|
&& upx wings
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
COPY --from=0 /go/wings/wings /usr/bin/
|
COPY --from=0 /go/wings/wings /usr/bin/
|
||||||
CMD ["wings","--config", "/etc/pterodactyl/config.yml"]
|
CMD ["wings", "--config", "/etc/pterodactyl/config.yml"]
|
||||||
@@ -23,7 +23,7 @@ I would like to extend my sincere thanks to the following sponsors for helping f
|
|||||||
| [**MineStrator**](https://minestrator.com/) | Looking for a French highend hosting company for you minecraft server? More than 14,000 members on our discord, trust us. |
|
| [**MineStrator**](https://minestrator.com/) | Looking for a French highend hosting company for you minecraft server? More than 14,000 members on our discord, trust us. |
|
||||||
| [**DedicatedMC**](https://dedicatedmc.io/) | DedicatedMC provides Raw Power hosting at affordable pricing, making sure to never compromise on your performance and giving you the best performance money can buy. |
|
| [**DedicatedMC**](https://dedicatedmc.io/) | DedicatedMC provides Raw Power hosting at affordable pricing, making sure to never compromise on your performance and giving you the best performance money can buy. |
|
||||||
| [**Skynode**](https://www.skynode.pro/) | Skynode provides blazing fast game servers along with a top-notch user experience. Whatever our clients are looking for, we're able to provide it! |
|
| [**Skynode**](https://www.skynode.pro/) | Skynode provides blazing fast game servers along with a top-notch user experience. Whatever our clients are looking for, we're able to provide it! |
|
||||||
| [**XCORE-SERVER.de**](https://xcore-server.de/) | XCORE-SERVER.de offers High-End Servers for hosting and gaming since 2012. Fast, excellent and well-known for eSports Gaming. |
|
| [**XCORE**](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. |
|
| [**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. |
|
| [**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. |
|
| [**DeinServerHost**](https://deinserverhost.de/) | DeinServerHost offers Dedicated, vps and Gameservers for many popular Games like Minecraft and Rust in Germany since 2013. |
|
||||||
|
|||||||
177
api/api.go
177
api/api.go
@@ -7,6 +7,8 @@ import (
|
|||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"github.com/pterodactyl/wings/system"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -14,30 +16,47 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Initializes the requester instance.
|
// Initializes the requester instance.
|
||||||
func NewRequester() *PanelRequest {
|
func New() *Request {
|
||||||
return &PanelRequest{
|
return &Request{}
|
||||||
Response: nil,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PanelRequest struct {
|
// A generic type allowing for easy binding use when making requests to API endpoints
|
||||||
Response *http.Response
|
// that only expect a singular argument or something that would not benefit from being
|
||||||
|
// a typed struct.
|
||||||
|
//
|
||||||
|
// Inspired by gin.H, same concept.
|
||||||
|
type D map[string]interface{}
|
||||||
|
|
||||||
|
// Same concept as D, but a map of strings, used for querying GET requests.
|
||||||
|
type Q map[string]string
|
||||||
|
|
||||||
|
// A custom API requester struct for Wings.
|
||||||
|
type Request struct{}
|
||||||
|
|
||||||
|
// A custom response type that allows for commonly used error handling and response
|
||||||
|
// parsing from the Panel API. This just embeds the normal HTTP response from Go and
|
||||||
|
// we attach a few helper functions to it.
|
||||||
|
type Response struct {
|
||||||
|
*http.Response
|
||||||
|
}
|
||||||
|
|
||||||
|
// A pagination struct matching the expected pagination response from the Panel API.
|
||||||
|
type Pagination struct {
|
||||||
|
CurrentPage uint `json:"current_page"`
|
||||||
|
From uint `json:"from"`
|
||||||
|
LastPage uint `json:"last_page"`
|
||||||
|
PerPage uint `json:"per_page"`
|
||||||
|
To uint `json:"to"`
|
||||||
|
Total uint `json:"total"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Builds the base request instance that can be used with the HTTP client.
|
// Builds the base request instance that can be used with the HTTP client.
|
||||||
func (r *PanelRequest) GetClient() *http.Client {
|
func (r *Request) Client() *http.Client {
|
||||||
return &http.Client{Timeout: time.Second * 30}
|
return &http.Client{Timeout: time.Second * time.Duration(config.Get().RemoteQuery.Timeout)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *PanelRequest) SetHeaders(req *http.Request) *http.Request {
|
// Returns the given endpoint formatted as a URL to the Panel API.
|
||||||
req.Header.Set("Accept", "application/vnd.pterodactyl.v1+json")
|
func (r *Request) Endpoint(endpoint string) string {
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s.%s", config.Get().AuthenticationTokenId, config.Get().AuthenticationToken))
|
|
||||||
|
|
||||||
return req
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PanelRequest) GetEndpoint(endpoint string) string {
|
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
"%s/api/remote/%s",
|
"%s/api/remote/%s",
|
||||||
strings.TrimSuffix(config.Get().PanelLocation, "/"),
|
strings.TrimSuffix(config.Get().PanelLocation, "/"),
|
||||||
@@ -45,9 +64,35 @@ func (r *PanelRequest) GetEndpoint(endpoint string) string {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Makes a HTTP request to the given endpoint, attaching the necessary request headers from
|
||||||
|
// Wings to ensure that the request is properly handled by the Panel.
|
||||||
|
func (r *Request) Make(method, url string, body io.Reader, opts ...func(r *http.Request)) (*Response, error) {
|
||||||
|
req, err := http.NewRequest(method, url, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("User-Agent", fmt.Sprintf("Pterodactyl Wings/v%s (id:%s)", system.Version, config.Get().AuthenticationTokenId))
|
||||||
|
req.Header.Set("Accept", "application/vnd.pterodactyl.v1+json")
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s.%s", config.Get().AuthenticationTokenId, config.Get().AuthenticationToken))
|
||||||
|
|
||||||
|
// Make any options calls that will allow us to make modifications to the request
|
||||||
|
// before it is sent off.
|
||||||
|
for _, cb := range opts {
|
||||||
|
cb(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.debug(req)
|
||||||
|
|
||||||
|
res, err := r.Client().Do(req)
|
||||||
|
|
||||||
|
return &Response{Response: res}, err
|
||||||
|
}
|
||||||
|
|
||||||
// Logs the request into the debug log with all of the important request bits.
|
// Logs the request into the debug log with all of the important request bits.
|
||||||
// The authorization key will be cleaned up before being output.
|
// The authorization key will be cleaned up before being output.
|
||||||
func (r *PanelRequest) logDebug(req *http.Request) {
|
func (r *Request) debug(req *http.Request) {
|
||||||
headers := make(map[string][]string)
|
headers := make(map[string][]string)
|
||||||
for k, v := range req.Header {
|
for k, v := range req.Header {
|
||||||
if k != "Authorization" || len(v) == 0 {
|
if k != "Authorization" || len(v) == 0 {
|
||||||
@@ -65,49 +110,44 @@ func (r *PanelRequest) logDebug(req *http.Request) {
|
|||||||
}).Debug("making request to external HTTP endpoint")
|
}).Debug("making request to external HTTP endpoint")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *PanelRequest) Get(url string) (*http.Response, error) {
|
// Makes a GET request to the given Panel API endpoint. If any data is passed as the
|
||||||
c := r.GetClient()
|
// second argument it will be passed through on the request as URL parameters.
|
||||||
|
func (r *Request) Get(url string, data Q) (*Response, error) {
|
||||||
|
return r.Make(http.MethodGet, r.Endpoint(url), nil, func(r *http.Request) {
|
||||||
|
q := r.URL.Query()
|
||||||
|
for k, v := range data {
|
||||||
|
q.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodGet, r.GetEndpoint(url), nil)
|
r.URL.RawQuery = q.Encode()
|
||||||
req = r.SetHeaders(req)
|
})
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
r.logDebug(req)
|
|
||||||
|
|
||||||
return c.Do(req)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *PanelRequest) Post(url string, data []byte) (*http.Response, error) {
|
// Makes a POST request to the given Panel API endpoint.
|
||||||
c := r.GetClient()
|
func (r *Request) Post(url string, data interface{}) (*Response, error) {
|
||||||
|
b, err := json.Marshal(data)
|
||||||
req, err := http.NewRequest(http.MethodPost, r.GetEndpoint(url), bytes.NewBuffer(data))
|
|
||||||
req = r.SetHeaders(req)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
r.logDebug(req)
|
return r.Make(http.MethodPost, r.Endpoint(url), bytes.NewBuffer(b))
|
||||||
|
|
||||||
return c.Do(req)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determines if the API call encountered an error. If no request has been made
|
// Determines if the API call encountered an error. If no request has been made
|
||||||
// the response will be false.
|
// the response will be false. This function will evaluate to true if the response
|
||||||
func (r *PanelRequest) HasError() bool {
|
// code is anything 300 or higher.
|
||||||
|
func (r *Response) HasError() bool {
|
||||||
if r.Response == nil {
|
if r.Response == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return r.Response.StatusCode >= 300 || r.Response.StatusCode < 200
|
return r.StatusCode >= 300 || r.StatusCode < 200
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reads the body from the response and returns it, then replaces it on the response
|
// Reads the body from the response and returns it, then replaces it on the response
|
||||||
// so that it can be read again later.
|
// so that it can be read again later. This does not close the response body, so any
|
||||||
func (r *PanelRequest) ReadBody() ([]byte, error) {
|
// functions calling this should be sure to manually defer a Body.Close() call.
|
||||||
|
func (r *Response) Read() ([]byte, error) {
|
||||||
var b []byte
|
var b []byte
|
||||||
if r.Response == nil {
|
if r.Response == nil {
|
||||||
return nil, errors.New("no response exists on interface")
|
return nil, errors.New("no response exists on interface")
|
||||||
@@ -122,51 +162,30 @@ func (r *PanelRequest) ReadBody() ([]byte, error) {
|
|||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *PanelRequest) HttpResponseCode() int {
|
// Binds a given interface with the data returned in the response. This is a shortcut
|
||||||
if r.Response == nil {
|
// for calling Read and then manually calling json.Unmarshal on the raw bytes.
|
||||||
return 0
|
func (r *Response) Bind(v interface{}) error {
|
||||||
|
b, err := r.Read()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return r.Response.StatusCode
|
return json.Unmarshal(b, &v)
|
||||||
}
|
|
||||||
|
|
||||||
func IsRequestError(err error) bool {
|
|
||||||
_, ok := err.(*RequestError)
|
|
||||||
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
type RequestError struct {
|
|
||||||
response *http.Response
|
|
||||||
Code string `json:"code"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Detail string `json:"detail"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the error response in a string form that can be more easily consumed.
|
|
||||||
func (re *RequestError) Error() string {
|
|
||||||
return fmt.Sprintf("Error response from Panel: %s: %s (HTTP/%d)", re.Code, re.Detail, re.response.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (re *RequestError) String() string {
|
|
||||||
return re.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
type RequestErrorBag struct {
|
|
||||||
Errors []RequestError `json:"errors"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the error message from the API call as a string. The error message will be formatted
|
// Returns the error message from the API call as a string. The error message will be formatted
|
||||||
// similar to the below example:
|
// similar to the below example:
|
||||||
//
|
//
|
||||||
// HttpNotFoundException: The requested resource does not exist. (HTTP/404)
|
// HttpNotFoundException: The requested resource does not exist. (HTTP/404)
|
||||||
func (r *PanelRequest) Error() *RequestError {
|
func (r *Response) Error() error {
|
||||||
body, _ := r.ReadBody()
|
if !r.HasError() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
bag := RequestErrorBag{}
|
var bag RequestErrorBag
|
||||||
json.Unmarshal(body, &bag)
|
_ = r.Bind(&bag)
|
||||||
|
|
||||||
e := new(RequestError)
|
e := &RequestError{}
|
||||||
if len(bag.Errors) > 0 {
|
if len(bag.Errors) > 0 {
|
||||||
e = &bag.Errors[0]
|
e = &bag.Errors[0]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,47 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"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 {
|
type BackupRequest struct {
|
||||||
|
UploadID string `json:"upload_id"`
|
||||||
Checksum string `json:"checksum"`
|
Checksum string `json:"checksum"`
|
||||||
ChecksumType string `json:"checksum_type"`
|
ChecksumType string `json:"checksum_type"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
@@ -15,22 +50,17 @@ type BackupRequest struct {
|
|||||||
|
|
||||||
// Notifies the panel that a specific backup has been completed and is now
|
// Notifies the panel that a specific backup has been completed and is now
|
||||||
// available for a user to view and download.
|
// available for a user to view and download.
|
||||||
func (r *PanelRequest) SendBackupStatus(backup string, data BackupRequest) (*RequestError, error) {
|
func (r *Request) SendBackupStatus(backup string, data BackupRequest) error {
|
||||||
b, err := json.Marshal(data)
|
// Set the UploadID on the data.
|
||||||
if err != nil {
|
if v, ok := backupUploadIDs.Get(backup); ok {
|
||||||
return nil, errors.WithStack(err)
|
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 {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
r.Response = resp
|
return resp.Error()
|
||||||
if r.HasError() {
|
|
||||||
return r.Error(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
33
api/error.go
Normal file
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
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/pkg/errors"
|
"github.com/apex/log"
|
||||||
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -33,156 +38,173 @@ type InstallationScript struct {
|
|||||||
Script string `json:"script"`
|
Script string `json:"script"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllServerConfigurations fetches configurations for all servers assigned to this node.
|
type allServerResponse struct {
|
||||||
func (r *PanelRequest) GetAllServerConfigurations() (map[string]json.RawMessage, *RequestError, error) {
|
Data []RawServerData `json:"data"`
|
||||||
resp, err := r.Get("/servers")
|
Meta Pagination `json:"meta"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RawServerData struct {
|
||||||
|
Uuid string `json:"uuid"`
|
||||||
|
Settings json.RawMessage `json:"settings"`
|
||||||
|
ProcessConfiguration json.RawMessage `json:"process_configuration"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetches all of the server configurations from the Panel API. This will initially load the
|
||||||
|
// first 50 servers, and then check the pagination response to determine if more pages should
|
||||||
|
// be loaded. If so, those requests are spun-up in additional routines and the final resulting
|
||||||
|
// slice of all servers will be returned.
|
||||||
|
func (r *Request) GetServers() ([]RawServerData, error) {
|
||||||
|
resp, err := r.Get("/servers", Q{"per_page": strconv.Itoa(int(config.Get().RemoteQuery.BootServersPerPage))})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
r.Response = resp
|
if resp.HasError() {
|
||||||
|
return nil, resp.Error()
|
||||||
if r.HasError() {
|
|
||||||
return nil, r.Error(), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
b, _ := r.ReadBody()
|
var res allServerResponse
|
||||||
res := map[string]json.RawMessage{}
|
if err := resp.Bind(&res); err != nil {
|
||||||
if len(b) == 2 {
|
return nil, err
|
||||||
return res, nil, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(b, &res); err != nil {
|
var mu sync.Mutex
|
||||||
return nil, nil, errors.WithStack(err)
|
ret := res.Data
|
||||||
|
|
||||||
|
// Check for pagination, and if it exists we'll need to then make a request to the API
|
||||||
|
// for each page that would exist and get all of the resulting servers.
|
||||||
|
if res.Meta.LastPage > 1 {
|
||||||
|
pp := res.Meta.PerPage
|
||||||
|
log.WithField("per_page", pp).
|
||||||
|
WithField("total_pages", res.Meta.LastPage).
|
||||||
|
Debug("detected multiple pages of server configurations, fetching remaining...")
|
||||||
|
|
||||||
|
g, ctx := errgroup.WithContext(context.Background())
|
||||||
|
for i := res.Meta.CurrentPage + 1; i <= res.Meta.LastPage; i++ {
|
||||||
|
page := strconv.Itoa(int(i))
|
||||||
|
|
||||||
|
g.Go(func() error {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
resp, err := r.Get("/servers", Q{"page": page, "per_page": strconv.Itoa(int(pp))})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.Error() != nil {
|
||||||
|
return resp.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
// Fetches the server configuration and returns the struct for it.
|
||||||
func (r *PanelRequest) GetServerConfiguration(uuid string) (ServerConfigurationResponse, *RequestError, error) {
|
func (r *Request) GetServerConfiguration(uuid string) (ServerConfigurationResponse, error) {
|
||||||
res := ServerConfigurationResponse{}
|
var cfg ServerConfigurationResponse
|
||||||
|
|
||||||
resp, err := r.Get(fmt.Sprintf("/servers/%s", uuid))
|
resp, err := r.Get(fmt.Sprintf("/servers/%s", uuid), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, nil, errors.WithStack(err)
|
return cfg, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
r.Response = resp
|
if resp.HasError() {
|
||||||
if r.HasError() {
|
return cfg, resp.Error()
|
||||||
return res, r.Error(), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
b, _ := r.ReadBody()
|
if err := resp.Bind(&cfg); err != nil {
|
||||||
if err := json.Unmarshal(b, &res); err != nil {
|
return cfg, err
|
||||||
return res, nil, errors.WithStack(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res, nil, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetches installation information for the server process.
|
// Fetches installation information for the server process.
|
||||||
func (r *PanelRequest) GetInstallationScript(uuid string) (InstallationScript, *RequestError, error) {
|
func (r *Request) GetInstallationScript(uuid string) (InstallationScript, error) {
|
||||||
res := InstallationScript{}
|
var is InstallationScript
|
||||||
|
resp, err := r.Get(fmt.Sprintf("/servers/%s/install", uuid), nil)
|
||||||
resp, err := r.Get(fmt.Sprintf("/servers/%s/install", uuid))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, nil, errors.WithStack(err)
|
return is, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
r.Response = resp
|
if resp.HasError() {
|
||||||
|
return is, resp.Error()
|
||||||
if r.HasError() {
|
|
||||||
return res, r.Error(), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
b, _ := r.ReadBody()
|
if err := resp.Bind(&is); err != nil {
|
||||||
|
return is, err
|
||||||
if err := json.Unmarshal(b, &res); err != nil {
|
|
||||||
return res, nil, errors.WithStack(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res, nil, nil
|
return is, nil
|
||||||
}
|
|
||||||
|
|
||||||
type installRequest struct {
|
|
||||||
Successful bool `json:"successful"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marks a server as being installed successfully or unsuccessfully on the panel.
|
// Marks a server as being installed successfully or unsuccessfully on the panel.
|
||||||
func (r *PanelRequest) SendInstallationStatus(uuid string, successful bool) (*RequestError, error) {
|
func (r *Request) SendInstallationStatus(uuid string, successful bool) error {
|
||||||
b, err := json.Marshal(installRequest{Successful: successful})
|
resp, err := r.Post(fmt.Sprintf("/servers/%s/install", uuid), D{"successful": successful})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := r.Post(fmt.Sprintf("/servers/%s/install", uuid), b)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
r.Response = resp
|
if resp.HasError() {
|
||||||
if r.HasError() {
|
return resp.Error()
|
||||||
return r.Error(), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type archiveRequest struct {
|
func (r *Request) SendArchiveStatus(uuid string, successful bool) error {
|
||||||
Successful bool `json:"successful"`
|
resp, err := r.Post(fmt.Sprintf("/servers/%s/archive", uuid), D{"successful": successful})
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PanelRequest) SendArchiveStatus(uuid string, successful bool) (*RequestError, error) {
|
|
||||||
b, err := json.Marshal(archiveRequest{Successful: successful})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := r.Post(fmt.Sprintf("/servers/%s/archive", uuid), b)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
r.Response = resp
|
return resp.Error()
|
||||||
if r.HasError() {
|
|
||||||
return r.Error(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *PanelRequest) SendTransferFailure(uuid string) (*RequestError, error) {
|
func (r *Request) SendTransferFailure(uuid string) error {
|
||||||
resp, err := r.Get(fmt.Sprintf("/servers/%s/transfer/failure", uuid))
|
resp, err := r.Get(fmt.Sprintf("/servers/%s/transfer/failure", uuid), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
r.Response = resp
|
return resp.Error()
|
||||||
if r.HasError() {
|
|
||||||
return r.Error(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *PanelRequest) SendTransferSuccess(uuid string) (*RequestError, error) {
|
func (r *Request) SendTransferSuccess(uuid string) error {
|
||||||
resp, err := r.Get(fmt.Sprintf("/servers/%s/transfer/success", uuid))
|
resp, err := r.Get(fmt.Sprintf("/servers/%s/transfer/success", uuid), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
r.Response = resp
|
return resp.Error()
|
||||||
if r.HasError() {
|
|
||||||
return r.Error(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -39,7 +38,7 @@ func IsInvalidCredentialsError(err error) bool {
|
|||||||
// server and sending a flood of usernames.
|
// server and sending a flood of usernames.
|
||||||
var validUsernameRegexp = regexp.MustCompile(`^(?i)(.+)\.([a-z0-9]{8})$`)
|
var validUsernameRegexp = regexp.MustCompile(`^(?i)(.+)\.([a-z0-9]{8})$`)
|
||||||
|
|
||||||
func (r *PanelRequest) ValidateSftpCredentials(request SftpAuthRequest) (*SftpAuthResponse, error) {
|
func (r *Request) ValidateSftpCredentials(request SftpAuthRequest) (*SftpAuthResponse, error) {
|
||||||
// If the username doesn't meet the expected format that the Panel would even recognize just go ahead
|
// If the username doesn't meet the expected format that the Panel would even recognize just go ahead
|
||||||
// and bail out of the process here to avoid accidentally brute forcing the panel if a bot decides
|
// and bail out of the process here to avoid accidentally brute forcing the panel if a bot decides
|
||||||
// to connect to spam username attempts.
|
// to connect to spam username attempts.
|
||||||
@@ -53,41 +52,33 @@ func (r *PanelRequest) ValidateSftpCredentials(request SftpAuthRequest) (*SftpAu
|
|||||||
return nil, new(sftpInvalidCredentialsError)
|
return nil, new(sftpInvalidCredentialsError)
|
||||||
}
|
}
|
||||||
|
|
||||||
b, err := json.Marshal(request)
|
resp, err := r.Post("/sftp/auth", request)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := r.Post("/sftp/auth", b)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
r.Response = resp
|
e := resp.Error()
|
||||||
|
if e != nil {
|
||||||
if r.HasError() {
|
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
|
||||||
if r.HttpResponseCode() >= 400 && r.HttpResponseCode() < 500 {
|
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"subsystem": "sftp",
|
"subsystem": "sftp",
|
||||||
"username": request.User,
|
"username": request.User,
|
||||||
"ip": request.IP,
|
"ip": request.IP,
|
||||||
}).Warn(r.Error().String())
|
}).Warn(e.Error())
|
||||||
|
|
||||||
return nil, new(sftpInvalidCredentialsError)
|
return nil, &sftpInvalidCredentialsError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
rerr := errors.New(r.Error().String())
|
rerr := errors.New(e.Error())
|
||||||
|
|
||||||
return nil, rerr
|
return nil, rerr
|
||||||
}
|
}
|
||||||
|
|
||||||
response := new(SftpAuthResponse)
|
var response SftpAuthResponse
|
||||||
body, _ := r.ReadBody()
|
if err := resp.Bind(&response); err != nil {
|
||||||
|
|
||||||
if err := json.Unmarshal(body, response); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return response, nil
|
return &response, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ import (
|
|||||||
|
|
||||||
"github.com/AlecAivazis/survey/v2"
|
"github.com/AlecAivazis/survey/v2"
|
||||||
"github.com/AlecAivazis/survey/v2/terminal"
|
"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/api/types"
|
||||||
"github.com/docker/docker/pkg/parsers/kernel"
|
"github.com/docker/docker/pkg/parsers/kernel"
|
||||||
|
"github.com/docker/docker/pkg/parsers/operatingsystem"
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"github.com/pterodactyl/wings/system"
|
"github.com/pterodactyl/wings/system"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|||||||
91
cmd/root.go
91
cmd/root.go
@@ -30,12 +30,14 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var configPath = config.DefaultLocation
|
var (
|
||||||
var debug = false
|
profiler = ""
|
||||||
var shouldRunProfiler = false
|
configPath = config.DefaultLocation
|
||||||
var useAutomaticTls = false
|
debug = false
|
||||||
var tlsHostname = ""
|
useAutomaticTls = false
|
||||||
var showVersion = false
|
tlsHostname = ""
|
||||||
|
showVersion = false
|
||||||
|
)
|
||||||
|
|
||||||
var root = &cobra.Command{
|
var root = &cobra.Command{
|
||||||
Use: "wings",
|
Use: "wings",
|
||||||
@@ -54,7 +56,7 @@ func init() {
|
|||||||
root.PersistentFlags().BoolVar(&showVersion, "version", false, "show the version and exit")
|
root.PersistentFlags().BoolVar(&showVersion, "version", false, "show the version and exit")
|
||||||
root.PersistentFlags().StringVar(&configPath, "config", config.DefaultLocation, "set the location for the configuration file")
|
root.PersistentFlags().StringVar(&configPath, "config", config.DefaultLocation, "set the location for the configuration file")
|
||||||
root.PersistentFlags().BoolVar(&debug, "debug", false, "pass in order to run wings in debug mode")
|
root.PersistentFlags().BoolVar(&debug, "debug", false, "pass in order to run wings in debug mode")
|
||||||
root.PersistentFlags().BoolVar(&shouldRunProfiler, "profile", false, "pass in order to profile wings")
|
root.PersistentFlags().StringVar(&profiler, "profiler", "", "the profiler to run for this instance")
|
||||||
root.PersistentFlags().BoolVar(&useAutomaticTls, "auto-tls", false, "pass in order to have wings generate and manage it's own SSL certificates using Let's Encrypt")
|
root.PersistentFlags().BoolVar(&useAutomaticTls, "auto-tls", false, "pass in order to have wings generate and manage it's own SSL certificates using Let's Encrypt")
|
||||||
root.PersistentFlags().StringVar(&tlsHostname, "tls-hostname", "", "required with --auto-tls, the FQDN for the generated SSL certificate")
|
root.PersistentFlags().StringVar(&tlsHostname, "tls-hostname", "", "required with --auto-tls, the FQDN for the generated SSL certificate")
|
||||||
|
|
||||||
@@ -75,7 +77,7 @@ func readConfiguration() (*config.Configuration, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if s, err := os.Stat(p); err != nil {
|
if s, err := os.Stat(p); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
} else if s.IsDir() {
|
} else if s.IsDir() {
|
||||||
return nil, errors.New("cannot use directory as configuration file path")
|
return nil, errors.New("cannot use directory as configuration file path")
|
||||||
}
|
}
|
||||||
@@ -89,15 +91,30 @@ func rootCmdRun(*cobra.Command, []string) {
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if shouldRunProfiler {
|
switch profiler {
|
||||||
defer profile.Start().Stop()
|
case "cpu":
|
||||||
|
defer profile.Start(profile.CPUProfile).Stop()
|
||||||
|
case "mem":
|
||||||
|
defer profile.Start(profile.MemProfile).Stop()
|
||||||
|
case "alloc":
|
||||||
|
defer profile.Start(profile.MemProfile, profile.MemProfileAllocs()).Stop()
|
||||||
|
case "heap":
|
||||||
|
defer profile.Start(profile.MemProfile, profile.MemProfileHeap()).Stop()
|
||||||
|
case "routines":
|
||||||
|
defer profile.Start(profile.GoroutineProfile).Stop()
|
||||||
|
case "mutex":
|
||||||
|
defer profile.Start(profile.MutexProfile).Stop()
|
||||||
|
case "threads":
|
||||||
|
defer profile.Start(profile.ThreadcreationProfile).Stop()
|
||||||
|
case "block":
|
||||||
|
defer profile.Start(profile.BlockProfile).Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only attempt configuration file relocation if a custom location has not
|
// Only attempt configuration file relocation if a custom location has not
|
||||||
// been specified in the command startup.
|
// been specified in the command startup.
|
||||||
if configPath == config.DefaultLocation {
|
if configPath == config.DefaultLocation {
|
||||||
if err := RelocateConfiguration(); err != nil {
|
if err := RelocateConfiguration(); err != nil {
|
||||||
if os.IsNotExist(err) {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
exitWithConfigurationNotice()
|
exitWithConfigurationNotice()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +199,7 @@ func rootCmdRun(*cobra.Command, []string) {
|
|||||||
|
|
||||||
states, err := server.CachedServerStates()
|
states, err := server.CachedServerStates()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithField("error", errors.WithStack(err)).Error("failed to retrieve locally cached server states from disk, assuming all servers in offline state")
|
log.WithField("error", err).Error("failed to retrieve locally cached server states from disk, assuming all servers in offline state")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new workerpool that limits us to 4 servers being bootstrapped at a time
|
// Create a new workerpool that limits us to 4 servers being bootstrapped at a time
|
||||||
@@ -218,7 +235,7 @@ func rootCmdRun(*cobra.Command, []string) {
|
|||||||
// as a result will result in a slow boot.
|
// as a result will result in a slow boot.
|
||||||
if !r && (st == environment.ProcessRunningState || st == environment.ProcessStartingState) {
|
if !r && (st == environment.ProcessRunningState || st == environment.ProcessStartingState) {
|
||||||
if err := s.HandlePowerAction(server.PowerActionStart); err != nil {
|
if err := s.HandlePowerAction(server.PowerActionStart); err != nil {
|
||||||
s.Log().WithField("error", errors.WithStack(err)).Warn("failed to return server to running state")
|
s.Log().WithField("error", err).Warn("failed to return server to running state")
|
||||||
}
|
}
|
||||||
} else if r || (!r && s.IsRunning()) {
|
} else if r || (!r && s.IsRunning()) {
|
||||||
// If the server is currently running on Docker, mark the process as being in that state.
|
// If the server is currently running on Docker, mark the process as being in that state.
|
||||||
@@ -229,17 +246,16 @@ func rootCmdRun(*cobra.Command, []string) {
|
|||||||
// is that it was running, but we see that the container process is not currently running.
|
// is that it was running, but we see that the container process is not currently running.
|
||||||
s.Log().Info("detected server is running, re-attaching to process...")
|
s.Log().Info("detected server is running, re-attaching to process...")
|
||||||
|
|
||||||
s.SetState(environment.ProcessRunningState)
|
s.Environment.SetState(environment.ProcessRunningState)
|
||||||
if err := s.Environment.Attach(); err != nil {
|
if err := s.Environment.Attach(); err != nil {
|
||||||
s.Log().WithField("error", errors.WithStack(err)).Warn("failed to attach to running server environment")
|
s.Log().WithField("error", err).Warn("failed to attach to running server environment")
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
return
|
// 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)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,34 +293,20 @@ func rootCmdRun(*cobra.Command, []string) {
|
|||||||
Handler: r,
|
Handler: r,
|
||||||
|
|
||||||
TLSConfig: &tls.Config{
|
TLSConfig: &tls.Config{
|
||||||
NextProtos: []string{
|
NextProtos: []string{"h2", "http/1.1"},
|
||||||
"h2", // enable HTTP/2
|
// @see https://blog.cloudflare.com/exposing-go-on-the-internet
|
||||||
"http/1.1",
|
|
||||||
},
|
|
||||||
|
|
||||||
// https://blog.cloudflare.com/exposing-go-on-the-internet
|
|
||||||
CipherSuites: []uint16{
|
CipherSuites: []uint16{
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||||
|
|
||||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||||
|
|
||||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
|
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||||
|
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||||
},
|
},
|
||||||
|
|
||||||
PreferServerCipherSuites: true,
|
PreferServerCipherSuites: true,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
MinVersion: tls.VersionTLS12,
|
MaxVersion: tls.VersionTLS13,
|
||||||
MaxVersion: tls.VersionTLS13,
|
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
|
||||||
|
|
||||||
CurvePreferences: []tls.CurveID{
|
|
||||||
tls.X25519,
|
|
||||||
tls.CurveP256,
|
|
||||||
},
|
|
||||||
// END https://blog.cloudflare.com/exposing-go-on-the-internet
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,7 +336,6 @@ func rootCmdRun(*cobra.Command, []string) {
|
|||||||
if err := s.ListenAndServeTLS("", ""); err != nil {
|
if err := s.ListenAndServeTLS("", ""); err != nil {
|
||||||
log.WithFields(log.Fields{"auto_tls": true, "tls_hostname": tlsHostname, "error": err}).
|
log.WithFields(log.Fields{"auto_tls": true, "tls_hostname": tlsHostname, "error": err}).
|
||||||
Fatal("failed to configure HTTP server using auto-tls")
|
Fatal("failed to configure HTTP server using auto-tls")
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -342,9 +343,8 @@ func rootCmdRun(*cobra.Command, []string) {
|
|||||||
|
|
||||||
// Check if main http server should run with TLS.
|
// Check if main http server should run with TLS.
|
||||||
if c.Api.Ssl.Enabled {
|
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")
|
log.WithFields(log.Fields{"auto_tls": false, "error": err}).Fatal("failed to configure HTTPS server")
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -353,7 +353,6 @@ func rootCmdRun(*cobra.Command, []string) {
|
|||||||
s.TLSConfig = nil
|
s.TLSConfig = nil
|
||||||
if err := s.ListenAndServe(); err != nil {
|
if err := s.ListenAndServe(); err != nil {
|
||||||
log.WithField("error", err).Fatal("failed to configure HTTP server")
|
log.WithField("error", err).Fatal("failed to configure HTTP server")
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,13 +365,13 @@ func Execute() error {
|
|||||||
// in the code without having to pass around a logger instance.
|
// in the code without having to pass around a logger instance.
|
||||||
func configureLogging(logDir string, debug bool) error {
|
func configureLogging(logDir string, debug bool) error {
|
||||||
if err := os.MkdirAll(path.Join(logDir, "/install"), 0700); err != nil {
|
if err := os.MkdirAll(path.Join(logDir, "/install"), 0700); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
p := filepath.Join(logDir, "/wings.log")
|
p := filepath.Join(logDir, "/wings.log")
|
||||||
w, err := logrotate.NewFile(p)
|
w, err := logrotate.NewFile(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(errors.Wrap(err, "failed to open process log file"))
|
panic(errors.WithMessage(err, "failed to open process log file"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if debug {
|
if debug {
|
||||||
|
|||||||
@@ -47,19 +47,14 @@ type Configuration struct {
|
|||||||
System SystemConfiguration `json:"system" yaml:"system"`
|
System SystemConfiguration `json:"system" yaml:"system"`
|
||||||
Docker DockerConfiguration `json:"docker" yaml:"docker"`
|
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
|
// Defines internal throttling configurations for server processes to prevent
|
||||||
// someone from running an endless loop that spams data to logs.
|
// someone from running an endless loop that spams data to logs.
|
||||||
Throttles ConsoleThrottles
|
Throttles ConsoleThrottles
|
||||||
|
|
||||||
// The location where the panel is running that this daemon should connect to
|
// The location where the panel is running that this daemon should connect to
|
||||||
// to collect data and send events.
|
// to collect data and send events.
|
||||||
PanelLocation string `json:"remote" yaml:"remote"`
|
PanelLocation string `json:"remote" yaml:"remote"`
|
||||||
|
RemoteQuery RemoteQueryConfiguration `json:"remote_query" yaml:"remote_query"`
|
||||||
|
|
||||||
// AllowedMounts is a list of allowed host-system mount points.
|
// AllowedMounts is a list of allowed host-system mount points.
|
||||||
// This is required to have the "Server Mounts" feature work properly.
|
// This is required to have the "Server Mounts" feature work properly.
|
||||||
@@ -101,6 +96,27 @@ type ApiConfiguration struct {
|
|||||||
UploadLimit int `default:"100" json:"upload_limit" yaml:"upload_limit"`
|
UploadLimit int `default:"100" json:"upload_limit" yaml:"upload_limit"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Defines the configuration settings for remote requests from Wings to the Panel.
|
||||||
|
type RemoteQueryConfiguration struct {
|
||||||
|
// The amount of time in seconds that Wings should allow for a request to the Panel API
|
||||||
|
// to complete. If this time passes the request will be marked as failed. If your requests
|
||||||
|
// are taking longer than 30 seconds to complete it is likely a performance issue that
|
||||||
|
// should be resolved on the Panel, and not something that should be resolved by upping this
|
||||||
|
// number.
|
||||||
|
Timeout uint `default:"30" yaml:"timeout"`
|
||||||
|
|
||||||
|
// The number of servers to load in a single request to the Panel API when booting the
|
||||||
|
// Wings instance. A single request is initially made to the Panel to get this number
|
||||||
|
// of servers, and then the pagination status is checked and additional requests are
|
||||||
|
// fired off in parallel to request the remaining pages.
|
||||||
|
//
|
||||||
|
// It is not recommended to change this from the default as you will likely encounter
|
||||||
|
// memory limits on your Panel instance. In the grand scheme of things 4 requests for
|
||||||
|
// 50 servers is likely just as quick as two for 100 or one for 400, and will certainly
|
||||||
|
// be less likely to cause performance issues on the Panel.
|
||||||
|
BootServersPerPage uint `default:"50" yaml:"boot_servers_per_page"`
|
||||||
|
}
|
||||||
|
|
||||||
// Reads the configuration from the provided file and returns the configuration
|
// Reads the configuration from the provided file and returns the configuration
|
||||||
// object that can then be used.
|
// object that can then be used.
|
||||||
func ReadConfiguration(path string) (*Configuration, error) {
|
func ReadConfiguration(path string) (*Configuration, error) {
|
||||||
@@ -176,7 +192,7 @@ func GetJwtAlgorithm() *jwt.HMACSHA {
|
|||||||
func NewFromPath(path string) (*Configuration, error) {
|
func NewFromPath(path string) (*Configuration, error) {
|
||||||
c := new(Configuration)
|
c := new(Configuration)
|
||||||
if err := defaults.Set(c); err != nil {
|
if err := defaults.Set(c); err != nil {
|
||||||
return c, errors.WithStack(err)
|
return c, err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.unsafeSetPath(path)
|
c.unsafeSetPath(path)
|
||||||
@@ -214,12 +230,12 @@ func (c *Configuration) EnsurePterodactylUser() (*user.User, error) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return u, c.setSystemUser(u)
|
return u, c.setSystemUser(u)
|
||||||
} else if _, ok := err.(user.UnknownUserError); !ok {
|
} else if _, ok := err.(user.UnknownUserError); !ok {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sysName, err := getSystemName()
|
sysName, err := getSystemName()
|
||||||
if err != nil {
|
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)
|
var command = fmt.Sprintf("useradd --system --no-create-home --shell /bin/false %s", c.System.Username)
|
||||||
@@ -232,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
|
// We have to create the group first on Alpine, so do that here before continuing on
|
||||||
// to the user creation process.
|
// to the user creation process.
|
||||||
if _, err := exec.Command("addgroup", "-S", c.System.Username).Output(); err != nil {
|
if _, err := exec.Command("addgroup", "-S", c.System.Username).Output(); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
split := strings.Split(command, " ")
|
split := strings.Split(command, " ")
|
||||||
if _, err := exec.Command(split[0], split[1:]...).Output(); err != nil {
|
if _, err := exec.Command(split[0], split[1:]...).Output(); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if u, err := user.Lookup(c.System.Username); err != nil {
|
if u, err := user.Lookup(c.System.Username); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
return u, c.setSystemUser(u)
|
return u, c.setSystemUser(u)
|
||||||
}
|
}
|
||||||
@@ -284,11 +300,11 @@ func (c *Configuration) WriteToDisk() error {
|
|||||||
|
|
||||||
b, err := yaml.Marshal(&ccopy)
|
b, err := yaml.Marshal(&ccopy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ioutil.WriteFile(c.GetPath(), b, 0644); err != nil {
|
if err := ioutil.WriteFile(c.GetPath(), b, 0644); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -298,7 +314,7 @@ func (c *Configuration) WriteToDisk() error {
|
|||||||
func getSystemName() (string, error) {
|
func getSystemName() (string, error) {
|
||||||
// use osrelease to get release version and ID
|
// use osrelease to get release version and ID
|
||||||
if release, err := osrelease.Read(); err != nil {
|
if release, err := osrelease.Read(); err != nil {
|
||||||
return "", errors.WithStack(err)
|
return "", err
|
||||||
} else {
|
} else {
|
||||||
return release["ID"], nil
|
return release["ID"], nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type dockerNetworkInterfaces struct {
|
type dockerNetworkInterfaces struct {
|
||||||
@@ -73,7 +72,7 @@ func (c RegistryConfiguration) Base64() (string, error) {
|
|||||||
|
|
||||||
b, err := json.Marshal(authConfig)
|
b, err := json.Marshal(authConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.WithStack(err)
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return base64.URLEncoding.EncodeToString(b), nil
|
return base64.URLEncoding.EncodeToString(b), nil
|
||||||
|
|||||||
@@ -53,13 +53,12 @@ type SystemConfiguration struct {
|
|||||||
// considered stale and a re-check should occur. DANGER: setting this value too low can seriously
|
// 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
|
// impact system performance and cause massive I/O bottlenecks and high CPU usage for the Wings
|
||||||
// process.
|
// 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"`
|
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
|
// 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
|
// 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
|
// cases disabling this should not have any major impact unless external processes are
|
||||||
@@ -70,7 +69,24 @@ type SystemConfiguration struct {
|
|||||||
// when it boots and one is not detected.
|
// when it boots and one is not detected.
|
||||||
EnableLogRotate bool `default:"true" yaml:"enable_log_rotate"`
|
EnableLogRotate bool `default:"true" yaml:"enable_log_rotate"`
|
||||||
|
|
||||||
|
// The number of lines to send when a server connects to the websocket.
|
||||||
|
WebsocketLogCount int `default:"150" yaml:"websocket_log_count"`
|
||||||
|
|
||||||
Sftp SftpConfiguration `yaml:"sftp"`
|
Sftp SftpConfiguration `yaml:"sftp"`
|
||||||
|
|
||||||
|
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
|
// Ensures that all of the system directories exist on the system. These directories are
|
||||||
@@ -91,7 +107,7 @@ func (sc *SystemConfiguration) ConfigureDirectories() error {
|
|||||||
// that.
|
// that.
|
||||||
if d, err := filepath.EvalSymlinks(sc.Data); err != nil {
|
if d, err := filepath.EvalSymlinks(sc.Data); err != nil {
|
||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
} else if d != sc.Data {
|
} else if d != sc.Data {
|
||||||
sc.Data = d
|
sc.Data = d
|
||||||
@@ -127,13 +143,13 @@ func (sc *SystemConfiguration) EnableLogRotation() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if st, err := os.Stat("/etc/logrotate.d"); err != nil && !os.IsNotExist(err) {
|
if st, err := os.Stat("/etc/logrotate.d"); err != nil && !os.IsNotExist(err) {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
} else if (err != nil && os.IsNotExist(err)) || !st.IsDir() {
|
} else if (err != nil && os.IsNotExist(err)) || !st.IsDir() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat("/etc/logrotate.d/wings"); err != nil && !os.IsNotExist(err) {
|
if _, err := os.Stat("/etc/logrotate.d/wings"); err != nil && !os.IsNotExist(err) {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
} else if err == nil {
|
} else if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -144,7 +160,7 @@ func (sc *SystemConfiguration) EnableLogRotation() error {
|
|||||||
// it so files can be rotated easily.
|
// it so files can be rotated easily.
|
||||||
f, err := os.Create("/etc/logrotate.d/wings")
|
f, err := os.Create("/etc/logrotate.d/wings")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
@@ -164,10 +180,10 @@ func (sc *SystemConfiguration) EnableLogRotation() error {
|
|||||||
}`)
|
}`)
|
||||||
|
|
||||||
if err != nil {
|
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.
|
// Returns the location of the JSON file that tracks server states.
|
||||||
@@ -187,10 +203,10 @@ func (sc *SystemConfiguration) ConfigureTimezone() error {
|
|||||||
if sc.Timezone == "" {
|
if sc.Timezone == "" {
|
||||||
if b, err := ioutil.ReadFile("/etc/timezone"); err != nil {
|
if b, err := ioutil.ReadFile("/etc/timezone"); err != nil {
|
||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
return errors.Wrap(err, "failed to open /etc/timezone for automatic server timezone calibration")
|
return errors.WithMessage(err, "failed to open /etc/timezone for automatic server timezone calibration")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, _ := context.WithTimeout(context.Background(), time.Second * 5)
|
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
|
// 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
|
// 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.
|
// fall through to UTC to get Wings booted at least.
|
||||||
@@ -221,5 +237,5 @@ func (sc *SystemConfiguration) ConfigureTimezone() error {
|
|||||||
|
|
||||||
_, err := time.LoadLocation(sc.Timezone)
|
_, err := time.LoadLocation(sc.Timezone)
|
||||||
|
|
||||||
return errors.Wrap(err, fmt.Sprintf("the supplied timezone %s is invalid", 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:
|
services:
|
||||||
daemon:
|
wings:
|
||||||
build: .
|
image: ghcr.io/pterodactyl/wings:latest
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
- daemon0
|
- wings0
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
- "2022:2022"
|
- "2022:2022"
|
||||||
tty: true
|
tty: true
|
||||||
environment:
|
environment:
|
||||||
- "DEBUG=false"
|
TZ: "UTC"
|
||||||
- "TZ=UTC" # change to the three letter timezone of your choosing
|
DEBUG: "false"
|
||||||
volumes:
|
volumes:
|
||||||
- "/var/run/docker.sock:/var/run/docker.sock"
|
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||||
- "/var/lib/docker/containers/:/var/lib/docker/containers/"
|
- "/var/lib/docker/containers/:/var/lib/docker/containers/"
|
||||||
@@ -19,17 +19,16 @@ services:
|
|||||||
- "/var/lib/pterodactyl/:/var/lib/pterodactyl/"
|
- "/var/lib/pterodactyl/:/var/lib/pterodactyl/"
|
||||||
- "/var/log/pterodactyl/:/var/log/pterodactyl/"
|
- "/var/log/pterodactyl/:/var/log/pterodactyl/"
|
||||||
- "/tmp/pterodactyl/:/tmp/pterodactyl/"
|
- "/tmp/pterodactyl/:/tmp/pterodactyl/"
|
||||||
## you may need /srv/daemon-data if you are upgrading from an old daemon
|
# you may need /srv/daemon-data if you are upgrading from an old daemon
|
||||||
## - "/srv/daemon-data/:/srv/daemon-data/"
|
#- "/srv/daemon-data/:/srv/daemon-data/"
|
||||||
## Required for ssl if you user let's encrypt. uncomment to use.
|
# Required for ssl if you user let's encrypt. uncomment to use.
|
||||||
## - "/etc/letsencrypt/:/etc/letsencrypt/"
|
#- "/etc/letsencrypt/:/etc/letsencrypt/"
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
daemon0:
|
wings0:
|
||||||
name: daemon0
|
name: wings0
|
||||||
driver: bridge
|
driver: bridge
|
||||||
ipam:
|
ipam:
|
||||||
config:
|
config:
|
||||||
- subnet: "172.21.0.0/16"
|
- subnet: "172.21.0.0/16"
|
||||||
driver_opts:
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
binding := []nat.PortBinding{
|
binding := nat.PortBinding{
|
||||||
{
|
HostIP: ip,
|
||||||
HostIP: ip,
|
HostPort: strconv.Itoa(port),
|
||||||
HostPort: strconv.Itoa(port),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
out[nat.Port(fmt.Sprintf("%d/tcp", port))] = binding
|
tcp := nat.Port(fmt.Sprintf("%d/tcp", port))
|
||||||
out[nat.Port(fmt.Sprintf("%d/udp", port))] = binding
|
udp := nat.Port(fmt.Sprintf("%d/udp", port))
|
||||||
|
|
||||||
|
out[tcp] = append(out[tcp], binding)
|
||||||
|
out[udp] = append(out[udp], binding)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func (e *Environment) Attach() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := e.followOutput(); err != nil {
|
if err := e.followOutput(); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := types.ContainerAttachOptions{
|
opts := types.ContainerAttachOptions{
|
||||||
@@ -48,7 +48,7 @@ func (e *Environment) Attach() error {
|
|||||||
|
|
||||||
// Set the stream again with the container.
|
// Set the stream again with the container.
|
||||||
if st, err := e.client.ContainerAttach(context.Background(), e.Id, opts); err != nil {
|
if st, err := e.client.ContainerAttach(context.Background(), e.Id, opts); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
} else {
|
} else {
|
||||||
e.SetStream(&st)
|
e.SetStream(&st)
|
||||||
}
|
}
|
||||||
@@ -60,7 +60,7 @@ func (e *Environment) Attach() error {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
defer e.stream.Close()
|
defer e.stream.Close()
|
||||||
defer func() {
|
defer func() {
|
||||||
e.setState(environment.ProcessOfflineState)
|
e.SetState(environment.ProcessOfflineState)
|
||||||
e.SetStream(nil)
|
e.SetStream(nil)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -70,14 +70,19 @@ func (e *Environment) Attach() error {
|
|||||||
// indicates that the container is no longer running.
|
// indicates that the container is no longer running.
|
||||||
go func(ctx context.Context) {
|
go func(ctx context.Context) {
|
||||||
if err := e.pollResources(ctx); err != nil {
|
if err := e.pollResources(ctx); err != nil {
|
||||||
log.WithField("environment_id", e.Id).WithField("error", errors.WithStack(err)).Error("error during environment resource polling")
|
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)
|
}(ctx)
|
||||||
|
|
||||||
// Stream the reader output to the console which will then fire off events and handle console
|
// Stream the reader output to the console which will then fire off events and handle console
|
||||||
// throttling and sending the output to the user.
|
// throttling and sending the output to the user.
|
||||||
if _, err := io.Copy(console, e.stream.Reader); err != nil {
|
if _, err := io.Copy(console, e.stream.Reader); err != nil {
|
||||||
log.WithField("environment_id", e.Id).WithField("error", errors.WithStack(err)).Error("error while copying environment output to console")
|
log.WithField("environment_id", e.Id).WithField("error", err).Error("error while copying environment output to console")
|
||||||
}
|
}
|
||||||
}(c)
|
}(c)
|
||||||
|
|
||||||
@@ -115,7 +120,7 @@ func (e *Environment) InSituUpdate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
u := container.UpdateConfig{
|
u := container.UpdateConfig{
|
||||||
@@ -125,7 +130,7 @@ func (e *Environment) InSituUpdate() error {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if _, err := e.client.ContainerUpdate(ctx, e.Id, u); err != nil {
|
if _, err := e.client.ContainerUpdate(ctx, e.Id, u); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -140,12 +145,12 @@ func (e *Environment) Create() error {
|
|||||||
if _, err := e.client.ContainerInspect(context.Background(), e.Id); err == nil {
|
if _, err := e.client.ContainerInspect(context.Background(), e.Id); err == nil {
|
||||||
return nil
|
return nil
|
||||||
} else if !client.IsErrNotFound(err) {
|
} else if !client.IsErrNotFound(err) {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to pull the requested image before creating the container.
|
// Try to pull the requested image before creating the container.
|
||||||
if err := e.ensureImageExists(e.meta.Image); err != nil {
|
if err := e.ensureImageExists(e.meta.Image); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
a := e.Configuration.Allocations()
|
a := e.Configuration.Allocations()
|
||||||
@@ -220,7 +225,7 @@ func (e *Environment) Create() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, err := e.client.ContainerCreate(context.Background(), conf, hostConf, nil, e.Id); err != nil {
|
if _, err := e.client.ContainerCreate(context.Background(), conf, hostConf, nil, e.Id); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -245,7 +250,7 @@ func (e *Environment) convertMounts() []mount.Mount {
|
|||||||
// it will be forcibly stopped by Docker.
|
// it will be forcibly stopped by Docker.
|
||||||
func (e *Environment) Destroy() error {
|
func (e *Environment) Destroy() error {
|
||||||
// We set it to stopping than offline to prevent crash detection from being triggered.
|
// We set it to stopping than offline to prevent crash detection from being triggered.
|
||||||
e.setState(environment.ProcessStoppingState)
|
e.SetState(environment.ProcessStoppingState)
|
||||||
|
|
||||||
err := e.client.ContainerRemove(context.Background(), e.Id, types.ContainerRemoveOptions{
|
err := e.client.ContainerRemove(context.Background(), e.Id, types.ContainerRemoveOptions{
|
||||||
RemoveVolumes: true,
|
RemoveVolumes: true,
|
||||||
@@ -261,7 +266,7 @@ func (e *Environment) Destroy() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
e.setState(environment.ProcessOfflineState)
|
e.SetState(environment.ProcessOfflineState)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -272,7 +277,7 @@ func (e *Environment) Destroy() error {
|
|||||||
func (e *Environment) followOutput() error {
|
func (e *Environment) followOutput() error {
|
||||||
if exists, err := e.Exists(); !exists {
|
if exists, err := e.Exists(); !exists {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.New(fmt.Sprintf("no such container: %s", e.Id))
|
return errors.New(fmt.Sprintf("no such container: %s", e.Id))
|
||||||
@@ -291,9 +296,19 @@ func (e *Environment) followOutput() error {
|
|||||||
defer reader.Close()
|
defer reader.Close()
|
||||||
|
|
||||||
r := bufio.NewReader(reader)
|
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:
|
ParentLoop:
|
||||||
for {
|
for {
|
||||||
var b bytes.Buffer
|
str.Reset()
|
||||||
var line []byte
|
var line []byte
|
||||||
var isPrefix bool
|
var isPrefix bool
|
||||||
|
|
||||||
@@ -305,7 +320,7 @@ func (e *Environment) followOutput() error {
|
|||||||
// in line with that it thinks is the terminal size. Those returns break a lot of output handling,
|
// 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
|
// 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.
|
// its own event in the response.
|
||||||
b.Write(bytes.ReplaceAll(line, []byte(" \r"), []byte("\r\n")))
|
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
|
// 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.
|
// the default buffer), or if we hit the end of the line.
|
||||||
@@ -322,7 +337,7 @@ func (e *Environment) followOutput() error {
|
|||||||
|
|
||||||
// Publish the line for this loop. Break on new-line characters so every line is sent as a single
|
// 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.
|
// output event, otherwise you get funky handling in the browser console.
|
||||||
for _, line := range strings.Split(b.String(), "\r\n") {
|
for _, line := range strings.Split(str.String(), "\r\n") {
|
||||||
e.Events().Publish(environment.ConsoleOutputEvent, line)
|
e.Events().Publish(environment.ConsoleOutputEvent, line)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,7 +353,7 @@ func (e *Environment) followOutput() error {
|
|||||||
}
|
}
|
||||||
}(reader)
|
}(reader)
|
||||||
|
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pulls the image from Docker. If there is an error while pulling the image from the source
|
// Pulls the image from Docker. If there is an error while pulling the image from the source
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/pterodactyl/wings/api"
|
"github.com/pterodactyl/wings/api"
|
||||||
"github.com/pterodactyl/wings/environment"
|
"github.com/pterodactyl/wings/environment"
|
||||||
"github.com/pterodactyl/wings/events"
|
"github.com/pterodactyl/wings/events"
|
||||||
|
"github.com/pterodactyl/wings/system"
|
||||||
"io"
|
"io"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
@@ -47,8 +47,7 @@ type Environment struct {
|
|||||||
emitter *events.EventBus
|
emitter *events.EventBus
|
||||||
|
|
||||||
// Tracks the environment state.
|
// Tracks the environment state.
|
||||||
st string
|
st *system.AtomicString
|
||||||
stMu sync.RWMutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new base Docker environment. The ID passed through will be the ID that is used to
|
// Creates a new base Docker environment. The ID passed through will be the ID that is used to
|
||||||
@@ -65,6 +64,7 @@ func New(id string, m *Metadata, c *environment.Configuration) (*Environment, er
|
|||||||
Configuration: c,
|
Configuration: c,
|
||||||
meta: m,
|
meta: m,
|
||||||
client: cli,
|
client: cli,
|
||||||
|
st: system.NewAtomicString(environment.ProcessOfflineState),
|
||||||
}
|
}
|
||||||
|
|
||||||
return e, nil
|
return e, nil
|
||||||
@@ -155,7 +155,7 @@ func (e *Environment) ExitState() (uint32, bool, error) {
|
|||||||
return 1, false, nil
|
return 1, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0, false, errors.WithStack(err)
|
return 0, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return uint32(c.State.ExitCode), c.State.OOMKilled, nil
|
return uint32(c.State.ExitCode), c.State.OOMKilled, nil
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func (e *Environment) OnBeforeStart() error {
|
|||||||
// the Panel is usee.
|
// the Panel is usee.
|
||||||
if err := e.client.ContainerRemove(context.Background(), e.Id, types.ContainerRemoveOptions{RemoveVolumes: true}); err != nil {
|
if err := e.client.ContainerRemove(context.Background(), e.Id, types.ContainerRemoveOptions{RemoveVolumes: true}); err != nil {
|
||||||
if !client.IsErrNotFound(err) {
|
if !client.IsErrNotFound(err) {
|
||||||
return errors.Wrap(err, "failed to remove server docker container during pre-boot")
|
return errors.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
|
// If we don't set it to stopping first, you'll trigger crash detection which
|
||||||
// we don't want to do at this point since it'll just immediately try to do the
|
// we don't want to do at this point since it'll just immediately try to do the
|
||||||
// exact same action that lead to it crashing in the first place...
|
// exact same action that lead to it crashing in the first place...
|
||||||
e.setState(environment.ProcessStoppingState)
|
e.SetState(environment.ProcessStoppingState)
|
||||||
e.setState(environment.ProcessOfflineState)
|
e.SetState(environment.ProcessOfflineState)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -69,12 +69,12 @@ func (e *Environment) Start() error {
|
|||||||
//
|
//
|
||||||
// @see https://github.com/pterodactyl/panel/issues/2000
|
// @see https://github.com/pterodactyl/panel/issues/2000
|
||||||
if !client.IsErrNotFound(err) {
|
if !client.IsErrNotFound(err) {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If the server is running update our internal state and continue on with the attach.
|
// If the server is running update our internal state and continue on with the attach.
|
||||||
if c.State.Running {
|
if c.State.Running {
|
||||||
e.setState(environment.ProcessRunningState)
|
e.SetState(environment.ProcessRunningState)
|
||||||
|
|
||||||
return e.Attach()
|
return e.Attach()
|
||||||
}
|
}
|
||||||
@@ -84,12 +84,12 @@ func (e *Environment) Start() error {
|
|||||||
// to truncate them.
|
// to truncate them.
|
||||||
if _, err := os.Stat(c.LogPath); err == nil {
|
if _, err := os.Stat(c.LogPath); err == nil {
|
||||||
if err := os.Truncate(c.LogPath, 0); err != nil {
|
if err := os.Truncate(c.LogPath, 0); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
e.setState(environment.ProcessStartingState)
|
e.SetState(environment.ProcessStartingState)
|
||||||
|
|
||||||
// Set this to true for now, we will set it to false once we reach the
|
// Set this to true for now, we will set it to false once we reach the
|
||||||
// end of this chain.
|
// end of this chain.
|
||||||
@@ -99,14 +99,14 @@ func (e *Environment) Start() error {
|
|||||||
// exists on the system, and rebuild the container if that is required for server booting to
|
// exists on the system, and rebuild the container if that is required for server booting to
|
||||||
// occur.
|
// occur.
|
||||||
if err := e.OnBeforeStart(); err != nil {
|
if err := e.OnBeforeStart(); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
if err := e.client.ContainerStart(ctx, e.Id, types.ContainerStartOptions{}); err != nil {
|
if err := e.client.ContainerStart(ctx, e.Id, types.ContainerStartOptions{}); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// No errors, good to continue through.
|
// No errors, good to continue through.
|
||||||
@@ -136,8 +136,8 @@ func (e *Environment) Stop() error {
|
|||||||
|
|
||||||
// If the process is already offline don't switch it back to stopping. Just leave it how
|
// If the process is already offline don't switch it back to stopping. Just leave it how
|
||||||
// it is and continue through to the stop handling for the process.
|
// it is and continue through to the stop handling for the process.
|
||||||
if e.State() != environment.ProcessOfflineState {
|
if e.st.Load() != environment.ProcessOfflineState {
|
||||||
e.setState(environment.ProcessStoppingState)
|
e.SetState(environment.ProcessStoppingState)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only attempt to send the stop command to the instance if we are actually attached to
|
// Only attempt to send the stop command to the instance if we are actually attached to
|
||||||
@@ -153,7 +153,7 @@ func (e *Environment) Stop() error {
|
|||||||
// an error.
|
// an error.
|
||||||
if client.IsErrNotFound(err) {
|
if client.IsErrNotFound(err) {
|
||||||
e.SetStream(nil)
|
e.SetStream(nil)
|
||||||
e.setState(environment.ProcessOfflineState)
|
e.SetState(environment.ProcessOfflineState)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -169,7 +169,7 @@ func (e *Environment) Stop() error {
|
|||||||
// will be terminated forcefully depending on the value of the second argument.
|
// will be terminated forcefully depending on the value of the second argument.
|
||||||
func (e *Environment) WaitForStop(seconds uint, terminate bool) error {
|
func (e *Environment) WaitForStop(seconds uint, terminate bool) error {
|
||||||
if err := e.Stop(); err != nil {
|
if err := e.Stop(); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(seconds)*time.Second)
|
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():
|
case <-ctx.Done():
|
||||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||||
if terminate {
|
if terminate {
|
||||||
log.WithField("container_id", e.Id).Debug("server did not stop in time, executing process termination")
|
log.WithField("container_id", e.Id).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:
|
case err := <-errChan:
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if terminate {
|
if terminate {
|
||||||
log.WithField("container_id", e.Id).WithField("error", errors.WithStack(err)).Warn("error while waiting for container stop, attempting process termination")
|
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:
|
case <-ok:
|
||||||
}
|
}
|
||||||
@@ -210,23 +215,23 @@ func (e *Environment) WaitForStop(seconds uint, terminate bool) error {
|
|||||||
func (e *Environment) Terminate(signal os.Signal) error {
|
func (e *Environment) Terminate(signal os.Signal) error {
|
||||||
c, err := e.client.ContainerInspect(context.Background(), e.Id)
|
c, err := e.client.ContainerInspect(context.Background(), e.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !c.State.Running {
|
if !c.State.Running {
|
||||||
// If the container is not running but we're not already in a stopped state go ahead
|
// If the container is not running but we're not already in a stopped state go ahead
|
||||||
// and update things to indicate we should be completely stopped now. Set to stopping
|
// and update things to indicate we should be completely stopped now. Set to stopping
|
||||||
// first so crash detection is not triggered.
|
// first so crash detection is not triggered.
|
||||||
if e.State() != environment.ProcessOfflineState {
|
if e.st.Load() != environment.ProcessOfflineState {
|
||||||
e.setState(environment.ProcessStoppingState)
|
e.SetState(environment.ProcessStoppingState)
|
||||||
e.setState(environment.ProcessOfflineState)
|
e.SetState(environment.ProcessOfflineState)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// We set it to stopping than offline to prevent crash detection from being triggered.
|
// We set it to stopping than offline to prevent crash detection from being triggered.
|
||||||
e.setState(environment.ProcessStoppingState)
|
e.SetState(environment.ProcessStoppingState)
|
||||||
|
|
||||||
sig := strings.TrimSuffix(strings.TrimPrefix(signal.String(), "signal "), "ed")
|
sig := strings.TrimSuffix(strings.TrimPrefix(signal.String(), "signal "), "ed")
|
||||||
|
|
||||||
@@ -234,7 +239,7 @@ func (e *Environment) Terminate(signal os.Signal) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
e.setState(environment.ProcessOfflineState)
|
e.SetState(environment.ProcessOfflineState)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,36 +6,24 @@ import (
|
|||||||
"github.com/pterodactyl/wings/environment"
|
"github.com/pterodactyl/wings/environment"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Returns the current environment state.
|
|
||||||
func (e *Environment) State() string {
|
func (e *Environment) State() string {
|
||||||
e.stMu.RLock()
|
return e.st.Load()
|
||||||
defer e.stMu.RUnlock()
|
|
||||||
|
|
||||||
return e.st
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets the state of the environment. This emits an event that server's can hook into to
|
// Sets the state of the environment. This emits an event that server's can hook into to
|
||||||
// take their own actions and track their own state based on the environment.
|
// take their own actions and track their own state based on the environment.
|
||||||
func (e *Environment) setState(state string) error {
|
func (e *Environment) SetState(state string) {
|
||||||
if state != environment.ProcessOfflineState &&
|
if state != environment.ProcessOfflineState &&
|
||||||
state != environment.ProcessStartingState &&
|
state != environment.ProcessStartingState &&
|
||||||
state != environment.ProcessRunningState &&
|
state != environment.ProcessRunningState &&
|
||||||
state != environment.ProcessStoppingState {
|
state != environment.ProcessStoppingState {
|
||||||
return errors.New(fmt.Sprintf("invalid server state received: %s", state))
|
panic(errors.New(fmt.Sprintf("invalid server state received: %s", state)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the current state of the environment before changing it.
|
|
||||||
prevState := e.State()
|
|
||||||
|
|
||||||
// Emit the event to any listeners that are currently registered.
|
// Emit the event to any listeners that are currently registered.
|
||||||
if prevState != state {
|
if e.State() != state {
|
||||||
// If the state changed make sure we update the internal tracking to note that.
|
// If the state changed make sure we update the internal tracking to note that.
|
||||||
e.stMu.Lock()
|
e.st.Store(state)
|
||||||
e.st = state
|
e.Events().Publish(environment.StateChangeEvent, state)
|
||||||
e.stMu.Unlock()
|
|
||||||
|
|
||||||
e.Events().Publish(environment.StateChangeEvent, e.State())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,18 +15,17 @@ import (
|
|||||||
// Attach to the instance and then automatically emit an event whenever the resource usage for the
|
// Attach to the instance and then automatically emit an event whenever the resource usage for the
|
||||||
// server process changes.
|
// server process changes.
|
||||||
func (e *Environment) pollResources(ctx context.Context) error {
|
func (e *Environment) pollResources(ctx context.Context) error {
|
||||||
l := log.WithField("container_id", e.Id)
|
if e.st.Load() == environment.ProcessOfflineState {
|
||||||
|
|
||||||
l.Debug("starting resource polling for container")
|
|
||||||
defer l.Debug("stopped resource polling for container")
|
|
||||||
|
|
||||||
if e.State() == environment.ProcessOfflineState {
|
|
||||||
return errors.New("cannot enable resource polling on a stopped server")
|
return errors.New("cannot enable resource polling on a stopped server")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
l := log.WithField("container_id", e.Id)
|
||||||
|
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)
|
stats, err := e.client.ContainerStats(context.Background(), e.Id, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
defer stats.Body.Close()
|
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 := dec.Decode(&v); err != nil {
|
||||||
if err != io.EOF {
|
if err != io.EOF {
|
||||||
l.WithField("error", errors.WithStack(err)).Warn("error while processing Docker stats output for container")
|
l.WithField("error", err).Warn("error while processing Docker stats output for container")
|
||||||
} else {
|
} else {
|
||||||
l.Debug("io.EOF encountered during stats decode, stopping polling...")
|
l.Debug("io.EOF encountered during stats decode, stopping polling...")
|
||||||
}
|
}
|
||||||
@@ -50,7 +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.
|
// Disable collection if the server is in an offline state and this process is still running.
|
||||||
if e.State() == environment.ProcessOfflineState {
|
if e.st.Load() == environment.ProcessOfflineState {
|
||||||
l.Debug("process in offline state while resource polling is still active; stopping poll")
|
l.Debug("process in offline state while resource polling is still active; stopping poll")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -76,7 +75,7 @@ func (e *Environment) pollResources(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if b, err := json.Marshal(st); err != nil {
|
if b, err := json.Marshal(st); err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).Warn("error while marshaling stats object for environment")
|
l.WithField("error", err).Warn("error while marshaling stats object for environment")
|
||||||
} else {
|
} else {
|
||||||
e.Events().Publish(environment.ResourceEvent, string(b))
|
e.Events().Publish(environment.ResourceEvent, string(b))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,12 +37,12 @@ func (e *Environment) SendCommand(c string) error {
|
|||||||
// the server as entering the stopping state otherwise the process will stop and Wings will think
|
// the server as entering the stopping state otherwise the process will stop and Wings will think
|
||||||
// it has crashed and attempt to restart it.
|
// it has crashed and attempt to restart it.
|
||||||
if e.meta.Stop.Type == "command" && c == e.meta.Stop.Value {
|
if e.meta.Stop.Type == "command" && c == e.meta.Stop.Value {
|
||||||
e.Events().Publish(environment.StateChangeEvent, environment.ProcessStoppingState)
|
e.SetState(environment.ProcessStoppingState)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := e.stream.Conn.Write([]byte(c + "\n"))
|
_, err := e.stream.Conn.Write([]byte(c + "\n"))
|
||||||
|
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reads the log file for the server. This does not care if the server is running or not, it will
|
// Reads the log file for the server. This does not care if the server is running or not, it will
|
||||||
@@ -54,7 +54,7 @@ func (e *Environment) Readlog(lines int) ([]string, error) {
|
|||||||
Tail: strconv.Itoa(lines),
|
Tail: strconv.Itoa(lines),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
defer r.Close()
|
defer r.Close()
|
||||||
|
|
||||||
|
|||||||
@@ -94,4 +94,12 @@ type ProcessEnvironment interface {
|
|||||||
// Reads the log file for the process from the end backwards until the provided
|
// Reads the log file for the process from the end backwards until the provided
|
||||||
// number of lines is met.
|
// number of lines is met.
|
||||||
Readlog(int) ([]string, error)
|
Readlog(int) ([]string, error)
|
||||||
|
|
||||||
|
// Returns the current state of the environment.
|
||||||
|
State() string
|
||||||
|
|
||||||
|
// Sets the current state of the environment. In general you should let the environment
|
||||||
|
// handle this itself, but there are some scenarios where it is helpful for the server
|
||||||
|
// to update the state externally (e.g. starting -> started).
|
||||||
|
SetState(string)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package events
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/gammazero/workerpool"
|
"github.com/gammazero/workerpool"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
@@ -69,7 +68,7 @@ func (e *EventBus) Publish(topic string, data string) {
|
|||||||
func (e *EventBus) PublishJson(topic string, data interface{}) error {
|
func (e *EventBus) PublishJson(topic string, data interface{}) error {
|
||||||
b, err := json.Marshal(data)
|
b, err := json.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
e.Publish(topic, string(b))
|
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/containerd v1.3.7 // indirect
|
||||||
github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b // indirect
|
github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b // indirect
|
||||||
github.com/creasty/defaults v1.5.0
|
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/distribution v2.7.1+incompatible // indirect
|
||||||
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible
|
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible
|
||||||
github.com/docker/go-connections v0.4.0
|
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/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/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/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 h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
|
||||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
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=
|
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.
|
// Unmarshal the environment variables from the request into the server struct.
|
||||||
if b, _, _, err := jsonparser.Get(data, "environment"); err != nil {
|
if b, _, _, err := jsonparser.Get(data, "environment"); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
cfg.EnvVars = make(environment.Variables)
|
cfg.EnvVars = make(environment.Variables)
|
||||||
if err := json.Unmarshal(b, &cfg.EnvVars); err != nil {
|
if err := json.Unmarshal(b, &cfg.EnvVars); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unmarshal the allocation mappings from the request into the server struct.
|
// Unmarshal the allocation mappings from the request into the server struct.
|
||||||
if b, _, _, err := jsonparser.Get(data, "allocations", "mappings"); err != nil {
|
if b, _, _, err := jsonparser.Get(data, "allocations", "mappings"); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
cfg.Allocations.Mappings = make(map[string][]int)
|
cfg.Allocations.Mappings = make(map[string][]int)
|
||||||
if err := json.Unmarshal(b, &cfg.Allocations.Mappings); err != nil {
|
if err := json.Unmarshal(b, &cfg.Allocations.Mappings); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.Container.Image = getString(data, "container", "image")
|
cfg.Container.Image = getString(data, "container", "image")
|
||||||
|
|
||||||
c, rerr, err := api.NewRequester().GetServerConfiguration(cfg.Uuid)
|
c, err := api.New().GetServerConfiguration(cfg.Uuid)
|
||||||
if err != nil || rerr != nil {
|
if err != nil {
|
||||||
if err != nil {
|
if !api.IsRequestError(err) {
|
||||||
return nil, errors.WithStack(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
|
// 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/mattn/go-colorable"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"io"
|
"io"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Default = New(os.Stderr, true)
|
var Default = New(os.Stderr, true)
|
||||||
|
|
||||||
var bold = color2.New(color2.Bold)
|
var bold = color2.New(color2.Bold)
|
||||||
|
var boldred = color2.New(color2.Bold, color2.FgRed)
|
||||||
|
|
||||||
var Strings = [...]string{
|
var Strings = [...]string{
|
||||||
log.DebugLevel: "DEBUG",
|
log.DebugLevel: "DEBUG",
|
||||||
@@ -60,7 +61,6 @@ func (h *Handler) HandleLog(e *log.Entry) error {
|
|||||||
if name == "source" {
|
if name == "source" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(h.Writer, " %s=%v", color.Sprint(name), e.Fields.Get(name))
|
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" {
|
if name != "error" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var br = color2.New(color2.Bold, color2.FgRed)
|
|
||||||
if err, ok := e.Fields.Get("error").(error); ok {
|
if err, ok := e.Fields.Get("error").(error); ok {
|
||||||
fmt.Fprintf(h.Writer, "\n%s%+v\n\n", br.Sprintf("Stacktrace:"), getErrorStack(err, false))
|
if e, ok := errors.Cause(err).(tracer); ok {
|
||||||
} else {
|
st := e.StackTrace()
|
||||||
fmt.Fprintf(h.Writer, "\n%s%+v\n\n", br.Sprintf("Invalid Error:"), err)
|
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
|
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) {
|
func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error) {
|
||||||
parsed, err := gabs.ParseJSON(data)
|
parsed, err := gabs.ParseJSON(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range f.Replace {
|
for _, v := range f.Replace {
|
||||||
value, err := f.LookupConfigurationValue(v)
|
value, err := f.LookupConfigurationValue(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a wildcard character, and if found split the key on that value to
|
// Check for a wildcard character, and if found split the key on that value to
|
||||||
@@ -101,7 +101,7 @@ func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.Wrap(err, "failed to set config value of array child")
|
return nil, errors.WithMessage(err, "failed to set config value of array child")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -110,7 +110,7 @@ func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.Wrap(err, "unable to set config value at pathway: "+v.Match)
|
return nil, errors.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)
|
_, err = c.SetP(value, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
i, _ := strconv.Atoi(matches[2])
|
i, _ := strconv.Atoi(matches[2])
|
||||||
@@ -147,7 +147,7 @@ func setValueAtPath(c *gabs.Container, path string, value interface{}) error {
|
|||||||
ct, err := c.ArrayElementP(i, matches[1])
|
ct, err := c.ArrayElementP(i, matches[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if i != 0 || (!errors.Is(err, gabs.ErrNotArray) && !errors.Is(err, gabs.ErrNotFound)) {
|
if i != 0 || (!errors.Is(err, gabs.ErrNotArray) && !errors.Is(err, gabs.ErrNotFound)) {
|
||||||
return errors.Wrap(err, "error while parsing array element at path")
|
return errors.WithMessage(err, "error while parsing array element at path")
|
||||||
}
|
}
|
||||||
|
|
||||||
var t = make([]interface{}, 1)
|
var t = make([]interface{}, 1)
|
||||||
@@ -162,7 +162,7 @@ func setValueAtPath(c *gabs.Container, path string, value interface{}) error {
|
|||||||
// an empty object if we have additional things to set on the array, or just an empty array type
|
// an empty object if we have additional things to set on the array, or just an empty array type
|
||||||
// if there is not an object structure detected (no matches[3] available).
|
// if there is not an object structure detected (no matches[3] available).
|
||||||
if _, err = c.SetP(t, matches[1]); err != nil {
|
if _, err = c.SetP(t, matches[1]); err != nil {
|
||||||
return errors.Wrap(err, "failed to create empty array for missing element")
|
return errors.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
|
// Set our cursor to be the array element we expect, which in this case is just the first element
|
||||||
@@ -170,7 +170,7 @@ func setValueAtPath(c *gabs.Container, path string, value interface{}) error {
|
|||||||
// to match additional elements. In those cases the server will just have to be rebooted or something.
|
// to match additional elements. In those cases the server will just have to be rebooted or something.
|
||||||
ct, err = c.ArrayElementP(0, matches[1])
|
ct, err = c.ArrayElementP(0, matches[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to find array element at path")
|
return errors.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 {
|
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
|
return nil
|
||||||
@@ -253,7 +253,7 @@ func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplac
|
|||||||
match, _, _, err := jsonparser.Get(f.configuration, path...)
|
match, _, _, err := jsonparser.Get(f.configuration, path...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != jsonparser.KeyPathNotFoundError {
|
if err != jsonparser.KeyPathNotFoundError {
|
||||||
return string(match), errors.WithStack(err)
|
return string(match), err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.WithFields(log.Fields{"path": path, "filename": f.FileName}).Debug("attempted to load a configuration value that does not exist")
|
log.WithFields(log.Fields{"path": path, "filename": f.FileName}).Debug("attempted to load a configuration value that does not exist")
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -77,11 +76,6 @@ func (f *ConfigurationFile) UnmarshalJSON(data []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regex to match paths such as foo[1].bar[2] and convert them into a format that
|
|
||||||
// gabs can work with, such as foo.1.bar.2 in this case. This is applied when creating
|
|
||||||
// the struct for the configuration file replacements.
|
|
||||||
var cfrMatchReplacement = regexp.MustCompile(`\[(\d+)]`)
|
|
||||||
|
|
||||||
// Defines a single find/replace instance for a given server configuration file.
|
// Defines a single find/replace instance for a given server configuration file.
|
||||||
type ConfigurationFileReplacement struct {
|
type ConfigurationFileReplacement struct {
|
||||||
Match string `json:"match"`
|
Match string `json:"match"`
|
||||||
@@ -172,17 +166,17 @@ func (f *ConfigurationFile) Parse(path string, internal bool) error {
|
|||||||
|
|
||||||
b := strings.TrimSuffix(path, filepath.Base(path))
|
b := strings.TrimSuffix(path, filepath.Base(path))
|
||||||
if err := os.MkdirAll(b, 0755); err != nil {
|
if err := os.MkdirAll(b, 0755); err != nil {
|
||||||
return errors.Wrap(err, "failed to create base directory for missing configuration file")
|
return errors.WithMessage(err, "failed to create base directory for missing configuration file")
|
||||||
} else {
|
} else {
|
||||||
if _, err := os.Create(path); err != nil {
|
if _, err := os.Create(path); err != nil {
|
||||||
return errors.Wrap(err, "failed to create missing configuration file")
|
return errors.WithMessage(err, "failed to create missing configuration file")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return f.Parse(path, true)
|
return f.Parse(path, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parses an xml file.
|
// Parses an xml file.
|
||||||
@@ -354,12 +348,12 @@ func (f *ConfigurationFile) parseJsonFile(path string) error {
|
|||||||
func (f *ConfigurationFile) parseYamlFile(path string) error {
|
func (f *ConfigurationFile) parseYamlFile(path string) error {
|
||||||
b, err := readFileBytes(path)
|
b, err := readFileBytes(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
i := make(map[string]interface{})
|
i := make(map[string]interface{})
|
||||||
if err := yaml.Unmarshal(b, &i); err != nil {
|
if err := yaml.Unmarshal(b, &i); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unmarshal the yaml data into a JSON interface such that we can work with
|
// Unmarshal the yaml data into a JSON interface such that we can work with
|
||||||
@@ -367,20 +361,20 @@ func (f *ConfigurationFile) parseYamlFile(path string) error {
|
|||||||
// makes working with unknown JSON significantly easier.
|
// makes working with unknown JSON significantly easier.
|
||||||
jsonBytes, err := json.Marshal(dyno.ConvertMapI2MapS(i))
|
jsonBytes, err := json.Marshal(dyno.ConvertMapI2MapS(i))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now that the data is converted, treat it just like JSON and pass it to the
|
// Now that the data is converted, treat it just like JSON and pass it to the
|
||||||
// iterator function to update values as necessary.
|
// iterator function to update values as necessary.
|
||||||
data, err := f.IterateOverJson(jsonBytes)
|
data, err := f.IterateOverJson(jsonBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remarshal the JSON into YAML format before saving it back to the disk.
|
// Remarshal the JSON into YAML format before saving it back to the disk.
|
||||||
marshaled, err := yaml.Marshal(data.Data())
|
marshaled, err := yaml.Marshal(data.Data())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return ioutil.WriteFile(path, marshaled, 0644)
|
return ioutil.WriteFile(path, marshaled, 0644)
|
||||||
@@ -392,7 +386,7 @@ func (f *ConfigurationFile) parseYamlFile(path string) error {
|
|||||||
func (f *ConfigurationFile) parseTextFile(path string) error {
|
func (f *ConfigurationFile) parseTextFile(path string) error {
|
||||||
input, err := ioutil.ReadFile(path)
|
input, err := ioutil.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := strings.Split(string(input), "\n")
|
lines := strings.Split(string(input), "\n")
|
||||||
@@ -409,7 +403,7 @@ func (f *ConfigurationFile) parseTextFile(path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := ioutil.WriteFile(path, []byte(strings.Join(lines, "\n")), 0644); err != nil {
|
if err := ioutil.WriteFile(path, []byte(strings.Join(lines, "\n")), 0644); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -421,7 +415,7 @@ func (f *ConfigurationFile) parsePropertiesFile(path string) error {
|
|||||||
// Open the file.
|
// Open the file.
|
||||||
f2, err := os.Open(path)
|
f2, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
@@ -443,20 +437,20 @@ func (f *ConfigurationFile) parsePropertiesFile(path string) error {
|
|||||||
|
|
||||||
// Handle any scanner errors.
|
// Handle any scanner errors.
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode the properties file.
|
// Decode the properties file.
|
||||||
p, err := properties.LoadFile(path, properties.UTF8)
|
p, err := properties.LoadFile(path, properties.UTF8)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace any values that need to be replaced.
|
// Replace any values that need to be replaced.
|
||||||
for _, replace := range f.Replace {
|
for _, replace := range f.Replace {
|
||||||
data, err := f.LookupConfigurationValue(replace)
|
data, err := f.LookupConfigurationValue(replace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
v, ok := p.Get(replace.Match)
|
v, ok := p.Get(replace.Match)
|
||||||
@@ -468,7 +462,7 @@ func (f *ConfigurationFile) parsePropertiesFile(path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, _, err := p.Set(replace.Match, data); err != nil {
|
if _, _, err := p.Set(replace.Match, data); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,7 +482,7 @@ func (f *ConfigurationFile) parsePropertiesFile(path string) error {
|
|||||||
// Open the file for writing.
|
// Open the file for writing.
|
||||||
w, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
w, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
defer w.Close()
|
defer w.Close()
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ func (e *RequestError) SetMessage(msg string) *RequestError {
|
|||||||
func (e *RequestError) AbortWithStatus(status int, c *gin.Context) {
|
func (e *RequestError) AbortWithStatus(status int, c *gin.Context) {
|
||||||
// If this error is because the resource does not exist, we likely do not need to log
|
// If this error is because the resource does not exist, we likely do not need to log
|
||||||
// the error anywhere, just return a 404 and move on with our lives.
|
// the error anywhere, just return a 404 and move on with our lives.
|
||||||
if os.IsNotExist(e.Err) {
|
if errors.Is(e.Err, os.ErrNotExist) {
|
||||||
e.logger().WithField("error", e.Err).Debug("encountered os.IsNotExist error while handling request")
|
e.logger().WithField("error", e.Err).Debug("encountered os.IsNotExist error while handling request")
|
||||||
|
|
||||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
||||||
@@ -75,7 +75,7 @@ func (e *RequestError) AbortWithStatus(status int, c *gin.Context) {
|
|||||||
if status >= 500 {
|
if status >= 500 {
|
||||||
e.logger().WithField("error", e.Err).Error("encountered HTTP/500 error while handling request")
|
e.logger().WithField("error", e.Err).Error("encountered HTTP/500 error while handling request")
|
||||||
|
|
||||||
c.Error(errors.WithStack(e))
|
c.Error(e)
|
||||||
} else {
|
} else {
|
||||||
e.logger().WithField("error", e.Err).Debug("encountered non-HTTP/500 error while handling request")
|
e.logger().WithField("error", e.Err).Debug("encountered non-HTTP/500 error while handling request")
|
||||||
}
|
}
|
||||||
@@ -99,38 +99,32 @@ func (e *RequestError) AbortWithServerError(c *gin.Context) {
|
|||||||
|
|
||||||
// Handle specific filesystem errors for a server.
|
// Handle specific filesystem errors for a server.
|
||||||
func (e *RequestError) AbortFilesystemError(c *gin.Context) {
|
func (e *RequestError) AbortFilesystemError(c *gin.Context) {
|
||||||
if errors.Is(e.Err, os.ErrNotExist) {
|
if errors.Is(e.Err, os.ErrNotExist) || filesystem.IsBadPathResolutionError(e.Err) {
|
||||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
if filesystem.IsBadPathResolutionError(e.Err) {
|
||||||
"error": "The requested resource was not found.",
|
e.logger().Warn(e.Err.Error())
|
||||||
})
|
}
|
||||||
|
|
||||||
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "The requested resource was not found."})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.Is(e.Err, filesystem.ErrNotEnoughDiskSpace) {
|
if errors.Is(e.Err, filesystem.ErrNotEnoughDiskSpace) {
|
||||||
c.AbortWithStatusJSON(http.StatusConflict, gin.H{
|
c.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": "There is not enough disk space available to perform that action."})
|
||||||
"error": "There is not enough disk space available to perform that action.",
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasSuffix(e.Err.Error(), "file name too long") {
|
if strings.HasSuffix(e.Err.Error(), "file name too long") {
|
||||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "File name is too long."})
|
||||||
"error": "File name is too long.",
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if e, ok := e.Err.(*os.SyscallError); ok && e.Syscall == "readdirent" {
|
if e, ok := e.Err.(*os.SyscallError); ok && e.Syscall == "readdirent" {
|
||||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "The requested directory does not exist."})
|
||||||
"error": "The requested directory does not exist.",
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasSuffix(e.Err.Error(), "file name too long") {
|
if strings.HasSuffix(e.Err.Error(), "file name too long") {
|
||||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Cannot perform that action: file name is too long."})
|
||||||
"error": "Cannot perform that action: file name is too long.",
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ func Configure() *gin.Engine {
|
|||||||
server.POST("/commands", postServerCommands)
|
server.POST("/commands", postServerCommands)
|
||||||
server.POST("/install", postServerInstall)
|
server.POST("/install", postServerInstall)
|
||||||
server.POST("/reinstall", postServerReinstall)
|
server.POST("/reinstall", postServerReinstall)
|
||||||
|
server.POST("/ws/deny", postServerDenyWSTokens)
|
||||||
|
|
||||||
// This archive request causes the archive to start being created
|
// This archive request causes the archive to start being created
|
||||||
// this should only be triggered by the panel.
|
// this should only be triggered by the panel.
|
||||||
@@ -86,6 +87,7 @@ func Configure() *gin.Engine {
|
|||||||
files.POST("/delete", postServerDeleteFiles)
|
files.POST("/delete", postServerDeleteFiles)
|
||||||
files.POST("/compress", postServerCompressFiles)
|
files.POST("/compress", postServerCompressFiles)
|
||||||
files.POST("/decompress", postServerDecompressFiles)
|
files.POST("/decompress", postServerDecompressFiles)
|
||||||
|
files.POST("/chmod", postServerChmodFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
backup := server.Group("/backup")
|
backup := server.Group("/backup")
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/pterodactyl/wings/router/tokens"
|
||||||
"github.com/pterodactyl/wings/server"
|
"github.com/pterodactyl/wings/server"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -226,7 +227,7 @@ func deleteServer(c *gin.Context) {
|
|||||||
if err := os.RemoveAll(p); err != nil {
|
if err := os.RemoveAll(p); err != nil {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"path": p,
|
"path": p,
|
||||||
"error": errors.WithStack(err),
|
"error": err,
|
||||||
}).Warn("failed to remove server files during deletion process")
|
}).Warn("failed to remove server files during deletion process")
|
||||||
}
|
}
|
||||||
}(s.Filesystem().Path())
|
}(s.Filesystem().Path())
|
||||||
@@ -241,3 +242,22 @@ func deleteServer(c *gin.Context) {
|
|||||||
|
|
||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adds any of the JTIs passed through in the body to the deny list for the websocket
|
||||||
|
// preventing any JWT generated before the current time from being used to connect to
|
||||||
|
// the socket or send along commands.
|
||||||
|
func postServerDenyWSTokens(c *gin.Context) {
|
||||||
|
var data struct {
|
||||||
|
JTIs []string `json:"jtis"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.BindJSON(&data); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, jti := range data.JTIs {
|
||||||
|
tokens.DenyJTI(jti)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package router
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"github.com/apex/log"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/pterodactyl/wings/router/tokens"
|
"github.com/pterodactyl/wings/router/tokens"
|
||||||
@@ -45,6 +46,15 @@ func getServerFileContents(c *gin.Context) {
|
|||||||
c.Header("Content-Type", "application/octet-stream")
|
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 {
|
if err := s.Filesystem().Readfile(p, c.Writer); err != nil {
|
||||||
TrackedServerError(err, s).AbortFilesystemError(c)
|
TrackedServerError(err, s).AbortFilesystemError(c)
|
||||||
return
|
return
|
||||||
@@ -356,6 +366,68 @@ func postServerDecompressFiles(c *gin.Context) {
|
|||||||
c.Status(http.StatusNoContent)
|
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) {
|
func postServerUploadFiles(c *gin.Context) {
|
||||||
token := tokens.UploadPayload{}
|
token := tokens.UploadPayload{}
|
||||||
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
|
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
|
||||||
@@ -397,14 +469,14 @@ func postServerUploadFiles(c *gin.Context) {
|
|||||||
for _, header := range headers {
|
for _, header := range headers {
|
||||||
p, err := s.Filesystem().SafePath(filepath.Join(directory, header.Filename))
|
p, err := s.Filesystem().SafePath(filepath.Join(directory, header.Filename))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
TrackedServerError(err, s).AbortFilesystemError(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// We run this in a different method so I can use defer without any of
|
// We run this in a different method so I can use defer without any of
|
||||||
// the consequences caused by calling it in a loop.
|
// the consequences caused by calling it in a loop.
|
||||||
if err := handleFileUpload(p, s, header); err != nil {
|
if err := handleFileUpload(p, s, header); err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
TrackedServerError(err, s).AbortFilesystemError(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -413,12 +485,12 @@ func postServerUploadFiles(c *gin.Context) {
|
|||||||
func handleFileUpload(p string, s *server.Server, header *multipart.FileHeader) error {
|
func handleFileUpload(p string, s *server.Server, header *multipart.FileHeader) error {
|
||||||
file, err := header.Open()
|
file, err := header.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
if err := s.Filesystem().Writefile(p, file); err != nil {
|
if err := s.Filesystem().Writefile(p, file); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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 you pass through manual locations in the API call this logic will be skipped.
|
||||||
if strings.HasPrefix(cfg.Api.Ssl.KeyFile, "/etc/letsencrypt/live/") {
|
if strings.HasPrefix(cfg.Api.Ssl.KeyFile, "/etc/letsencrypt/live/") {
|
||||||
cfg.Api.Ssl.KeyFile = ccopy.Api.Ssl.KeyFile
|
cfg.Api.Ssl.KeyFile = strings.ToLower(ccopy.Api.Ssl.KeyFile)
|
||||||
cfg.Api.Ssl.CertificateFile = ccopy.Api.Ssl.CertificateFile
|
cfg.Api.Ssl.CertificateFile = strings.ToLower(ccopy.Api.Ssl.CertificateFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
config.Set(&cfg)
|
config.Set(&cfg)
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ func getServerArchive(c *gin.Context) {
|
|||||||
|
|
||||||
st, err := s.Archiver.Stat()
|
st, err := s.Archiver.Stat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !os.IsNotExist(err) {
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
TrackedServerError(err, s).SetMessage("failed to stat archive").AbortWithServerError(c)
|
TrackedServerError(err, s).SetMessage("failed to stat archive").AbortWithServerError(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -94,27 +94,40 @@ func postServerArchive(c *gin.Context) {
|
|||||||
s := GetServer(c.Param("server"))
|
s := GetServer(c.Param("server"))
|
||||||
|
|
||||||
go func(s *server.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 {
|
if err := s.Archiver.Archive(); err != nil {
|
||||||
s.Log().WithField("error", err).Error("failed to get archive for server")
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Log().Debug("successfully created server archive, notifying panel")
|
s.Log().Debug("successfully created server archive, notifying panel")
|
||||||
|
|
||||||
r := api.NewRequester()
|
if err := r.SendArchiveStatus(s.Id(), true); err != nil {
|
||||||
rerr, err := r.SendArchiveStatus(s.Id(), true)
|
if !api.IsRequestError(err) {
|
||||||
if rerr != nil || err != nil {
|
s.Log().WithField("error", err).Error("failed to notify panel of successful archive status")
|
||||||
if err != nil {
|
|
||||||
s.Log().WithField("error", err).Error("failed to notify panel of archive status")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Log().WithField("error", rerr.String()).Error("panel returned an error when sending the archive status")
|
s.Log().WithField("error", err.Error()).Error("panel returned an error when notifying it of a successful archive status")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Log().Debug("successfully notified panel of archive status")
|
s.Log().Info("successfully notified panel of successful archive status")
|
||||||
}(s)
|
}(s)
|
||||||
|
|
||||||
c.Status(http.StatusAccepted)
|
c.Status(http.StatusAccepted)
|
||||||
@@ -140,14 +153,13 @@ func postTransfer(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
l.Info("server transfer failed, notifying panel")
|
l.Info("server transfer failed, notifying panel")
|
||||||
rerr, err := api.NewRequester().SendTransferFailure(serverID)
|
if err := api.New().SendTransferFailure(serverID); err != nil {
|
||||||
if rerr != nil || err != nil {
|
if !api.IsRequestError(err) {
|
||||||
if err != nil {
|
|
||||||
l.WithField("error", err).Error("failed to notify panel with transfer failure")
|
l.WithField("error", err).Error("failed to notify panel with transfer failure")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l.WithField("error", errors.WithStack(rerr)).Error("received error response from panel while notifying of transfer failure")
|
l.WithField("error", err.Error()).Error("received error response from panel while notifying of transfer failure")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +169,7 @@ func postTransfer(c *gin.Context) {
|
|||||||
// Make a new GET request to the URL the panel gave us.
|
// Make a new GET request to the URL the panel gave us.
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithField("error", errors.WithStack(err)).Error("failed to create http request for archive transfer")
|
log.WithField("error", err).Error("failed to create http request for archive transfer")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +179,7 @@ func postTransfer(c *gin.Context) {
|
|||||||
// Execute the http request.
|
// Execute the http request.
|
||||||
res, err := client.Do(req)
|
res, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to send archive http request")
|
l.WithField("error", err).Error("failed to send archive http request")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
@@ -176,13 +188,11 @@ func postTransfer(c *gin.Context) {
|
|||||||
if res.StatusCode != 200 {
|
if res.StatusCode != 200 {
|
||||||
_, err := ioutil.ReadAll(res.Body)
|
_, err := ioutil.ReadAll(res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).WithField("status", res.StatusCode).Error("failed read transfer response body")
|
l.WithField("error", err).WithField("status", res.StatusCode).Error("failed read transfer response body")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l.WithField("error", errors.WithStack(err)).WithField("status", res.StatusCode).Error("failed to request server archive")
|
l.WithField("error", err).WithField("status", res.StatusCode).Error("failed to request server archive")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,16 +200,14 @@ func postTransfer(c *gin.Context) {
|
|||||||
archivePath := filepath.Join(config.Get().System.ArchiveDirectory, serverID+".tar.gz")
|
archivePath := filepath.Join(config.Get().System.ArchiveDirectory, serverID+".tar.gz")
|
||||||
|
|
||||||
// Check if the archive already exists and delete it if it does.
|
// Check if the archive already exists and delete it if it does.
|
||||||
_, err = os.Stat(archivePath)
|
if _, err = os.Stat(archivePath); err != nil {
|
||||||
if err != nil {
|
|
||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to stat archive file")
|
l.WithField("error", err).Error("failed to stat archive file")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := os.Remove(archivePath); err != nil {
|
if err := os.Remove(archivePath); err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).Warn("failed to remove old archive file")
|
l.WithField("error", err).Warn("failed to remove old archive file")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,8 +215,7 @@ func postTransfer(c *gin.Context) {
|
|||||||
// Create the file.
|
// Create the file.
|
||||||
file, err := os.Create(archivePath)
|
file, err := os.Create(archivePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to open archive on disk")
|
l.WithField("error", err).Error("failed to open archive on disk")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,24 +223,35 @@ func postTransfer(c *gin.Context) {
|
|||||||
buf := make([]byte, 1024*4)
|
buf := make([]byte, 1024*4)
|
||||||
_, err = io.CopyBuffer(file, res.Body, buf)
|
_, err = io.CopyBuffer(file, res.Body, buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to copy archive file to disk")
|
l.WithField("error", err).Error("failed to copy archive file to disk")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the file so it can be opened to verify the checksum.
|
// Close the file so it can be opened to verify the checksum.
|
||||||
if err := file.Close(); err != nil {
|
if err := file.Close(); err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to close archive file")
|
l.WithField("error", err).Error("failed to close archive file")
|
||||||
|
|
||||||
return
|
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.
|
// Open the archive file for computing a checksum.
|
||||||
file, err = os.Open(archivePath)
|
file, err = os.Open(archivePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to open archive on disk")
|
l.WithField("error", err).Error("failed to open archive on disk")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,7 +259,7 @@ func postTransfer(c *gin.Context) {
|
|||||||
hash := sha256.New()
|
hash := sha256.New()
|
||||||
buf = make([]byte, 1024*4)
|
buf = make([]byte, 1024*4)
|
||||||
if _, err := io.CopyBuffer(hash, file, buf); err != nil {
|
if _, err := io.CopyBuffer(hash, file, buf); err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to copy archive file for checksum verification")
|
l.WithField("error", err).Error("failed to copy archive file for checksum verification")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,7 +271,7 @@ func postTransfer(c *gin.Context) {
|
|||||||
|
|
||||||
// Close the file.
|
// Close the file.
|
||||||
if err := file.Close(); err != nil {
|
if err := file.Close(); err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to close archive file after calculating checksum")
|
l.WithField("error", err).Error("failed to close archive file after calculating checksum")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,7 +287,7 @@ func postTransfer(c *gin.Context) {
|
|||||||
// Create a new server installer (note this does not execute the install script)
|
// Create a new server installer (note this does not execute the install script)
|
||||||
i, err := installer.New(serverData)
|
i, err := installer.New(serverData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to validate received server data")
|
l.WithField("error", err).Error("failed to validate received server data")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,9 +300,9 @@ func postTransfer(c *gin.Context) {
|
|||||||
return
|
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 {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,15 +314,14 @@ func postTransfer(c *gin.Context) {
|
|||||||
hasError = false
|
hasError = false
|
||||||
|
|
||||||
// Notify the panel that the transfer succeeded.
|
// Notify the panel that the transfer succeeded.
|
||||||
rerr, err := api.NewRequester().SendTransferSuccess(serverID)
|
err = api.New().SendTransferSuccess(serverID)
|
||||||
if rerr != nil || err != nil {
|
if err != nil {
|
||||||
if err != nil {
|
if !api.IsRequestError(err) {
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to notify panel of transfer success")
|
l.WithField("error", err).Error("failed to notify panel of transfer success")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l.WithField("error", errors.WithStack(rerr)).Error("panel responded with error after transfer success")
|
l.WithField("error", err.Error()).Error("panel responded with error after transfer success")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,40 @@ package tokens
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"github.com/apex/log"
|
||||||
"github.com/gbrlsnchs/jwt/v3"
|
"github.com/gbrlsnchs/jwt/v3"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// The time at which Wings was booted. No JWT's created before this time are allowed to
|
||||||
|
// connect to the socket since they may have been marked as denied already and therefore
|
||||||
|
// could be invalid at this point.
|
||||||
|
//
|
||||||
|
// By doing this we make it so that a user who gets disconnected from Wings due to a Wings
|
||||||
|
// reboot just needs to request a new token as if their old token had expired naturally.
|
||||||
|
var wingsBootTime = time.Now()
|
||||||
|
|
||||||
|
// A map that contains any JTI's that have been denied by the Panel and the time at which
|
||||||
|
// they were marked as denied. Therefore any JWT with the same JTI and an IssuedTime that
|
||||||
|
// is the same as or before this time should be considered invalid.
|
||||||
|
//
|
||||||
|
// This is used to allow the Panel to revoke tokens en-masse for a given user & server
|
||||||
|
// combination since the JTI for tokens is just MD5(user.id + server.uuid). When a server
|
||||||
|
// is booted this listing is fetched from the panel and the Websocket is dynamically updated.
|
||||||
|
var denylist sync.Map
|
||||||
|
|
||||||
|
// Adds a JTI to the denylist by marking any JWTs generated before the current time as
|
||||||
|
// being invalid if they use the same JTI.
|
||||||
|
func DenyJTI(jti string) {
|
||||||
|
log.WithField("jti", jti).Debugf("adding \"%s\" to JTI denylist", jti)
|
||||||
|
|
||||||
|
denylist.Store(jti, time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
// A JWT payload for Websocket connections. This JWT is passed along to the Websocket after
|
||||||
|
// it has been connected to by sending an "auth" event.
|
||||||
type WebsocketPayload struct {
|
type WebsocketPayload struct {
|
||||||
jwt.Payload
|
jwt.Payload
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
@@ -24,6 +53,7 @@ func (p *WebsocketPayload) GetPayload() *jwt.Payload {
|
|||||||
return &p.Payload
|
return &p.Payload
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns the UUID of the server associated with this JWT.
|
||||||
func (p *WebsocketPayload) GetServerUuid() string {
|
func (p *WebsocketPayload) GetServerUuid() string {
|
||||||
p.RLock()
|
p.RLock()
|
||||||
defer p.RUnlock()
|
defer p.RUnlock()
|
||||||
@@ -31,6 +61,33 @@ func (p *WebsocketPayload) GetServerUuid() string {
|
|||||||
return p.ServerUUID
|
return p.ServerUUID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the JWT has been marked as denied by the instance due to either being issued
|
||||||
|
// before Wings was booted, or because we have denied all tokens with the same JTI
|
||||||
|
// occurring before a set time.
|
||||||
|
func (p *WebsocketPayload) Denylisted() bool {
|
||||||
|
// If there is no IssuedAt present for the token, we cannot validate the token so
|
||||||
|
// just immediately mark it as not valid.
|
||||||
|
if p.IssuedAt == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the time that the token was issued is before the time at which Wings was booted
|
||||||
|
// then the token is invalid for our purposes, even if the token "has permission".
|
||||||
|
if p.IssuedAt.Time.Before(wingsBootTime) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, if the token was issued before a time that is currently denied for this
|
||||||
|
// token instance, ignore the permissions response.
|
||||||
|
if t, ok := denylist.Load(p.JWTID); ok {
|
||||||
|
if p.IssuedAt.Time.Before(t.(time.Time)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Checks if the given token payload has a permission string.
|
// Checks if the given token payload has a permission string.
|
||||||
func (p *WebsocketPayload) HasPermission(permission string) bool {
|
func (p *WebsocketPayload) HasPermission(permission string) bool {
|
||||||
p.RLock()
|
p.RLock()
|
||||||
@@ -38,7 +95,7 @@ func (p *WebsocketPayload) HasPermission(permission string) bool {
|
|||||||
|
|
||||||
for _, k := range p.Permissions {
|
for _, k := range p.Permissions {
|
||||||
if k == permission || (!strings.HasPrefix(permission, "admin") && k == "*") {
|
if k == permission || (!strings.HasPrefix(permission, "admin") && k == "*") {
|
||||||
return true
|
return !p.Denylisted()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
"github.com/pterodactyl/wings/environment/docker"
|
"github.com/pterodactyl/wings/environment/docker"
|
||||||
"github.com/pterodactyl/wings/router/tokens"
|
"github.com/pterodactyl/wings/router/tokens"
|
||||||
"github.com/pterodactyl/wings/server"
|
"github.com/pterodactyl/wings/server"
|
||||||
"github.com/pterodactyl/wings/server/filesystem"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -33,10 +32,9 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
sync.RWMutex
|
sync.RWMutex `json:"-"`
|
||||||
|
Connection *websocket.Conn `json:"-"`
|
||||||
Connection *websocket.Conn
|
jwt *tokens.WebsocketPayload
|
||||||
jwt *tokens.WebsocketPayload `json:"-"`
|
|
||||||
server *server.Server
|
server *server.Server
|
||||||
uuid uuid.UUID
|
uuid uuid.UUID
|
||||||
}
|
}
|
||||||
@@ -45,12 +43,14 @@ var (
|
|||||||
ErrJwtNotPresent = errors.New("jwt: no jwt present")
|
ErrJwtNotPresent = errors.New("jwt: no jwt present")
|
||||||
ErrJwtNoConnectPerm = errors.New("jwt: missing connect permission")
|
ErrJwtNoConnectPerm = errors.New("jwt: missing connect permission")
|
||||||
ErrJwtUuidMismatch = errors.New("jwt: server uuid mismatch")
|
ErrJwtUuidMismatch = errors.New("jwt: server uuid mismatch")
|
||||||
|
ErrJwtOnDenylist = errors.New("jwt: created too far in past (denylist)")
|
||||||
)
|
)
|
||||||
|
|
||||||
func IsJwtError(err error) bool {
|
func IsJwtError(err error) bool {
|
||||||
return errors.Is(err, ErrJwtNotPresent) ||
|
return errors.Is(err, ErrJwtNotPresent) ||
|
||||||
errors.Is(err, ErrJwtNoConnectPerm) ||
|
errors.Is(err, ErrJwtNoConnectPerm) ||
|
||||||
errors.Is(err, ErrJwtUuidMismatch) ||
|
errors.Is(err, ErrJwtUuidMismatch) ||
|
||||||
|
errors.Is(err, ErrJwtOnDenylist) ||
|
||||||
errors.Is(err, jwt.ErrExpValidation)
|
errors.Is(err, jwt.ErrExpValidation)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,8 +62,12 @@ func NewTokenPayload(token []byte) (*tokens.WebsocketPayload, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if payload.Denylisted() {
|
||||||
|
return nil, ErrJwtOnDenylist
|
||||||
|
}
|
||||||
|
|
||||||
if !payload.HasPermission(PermissionConnect) {
|
if !payload.HasPermission(PermissionConnect) {
|
||||||
return nil, errors.New("not authorized to connect to this socket")
|
return nil, ErrJwtNoConnectPerm
|
||||||
}
|
}
|
||||||
|
|
||||||
return &payload, nil
|
return &payload, nil
|
||||||
@@ -103,7 +107,7 @@ func GetHandler(s *server.Server, w http.ResponseWriter, r *http.Request) (*Hand
|
|||||||
|
|
||||||
u, err := uuid.NewRandom()
|
u, err := uuid.NewRandom()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Handler{
|
return &Handler{
|
||||||
@@ -125,7 +129,6 @@ func (h *Handler) SendJson(v *Message) error {
|
|||||||
Event: JwtErrorEvent,
|
Event: JwtErrorEvent,
|
||||||
Args: []string{err.Error()},
|
Args: []string{err.Error()},
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +191,10 @@ func (h *Handler) TokenValid() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if j.Denylisted() {
|
||||||
|
return ErrJwtOnDenylist
|
||||||
|
}
|
||||||
|
|
||||||
if !j.HasPermission(PermissionConnect) {
|
if !j.HasPermission(PermissionConnect) {
|
||||||
return ErrJwtNoConnectPerm
|
return ErrJwtNoConnectPerm
|
||||||
}
|
}
|
||||||
@@ -204,25 +211,26 @@ func (h *Handler) TokenValid() error {
|
|||||||
// error message, otherwise we just send back a standard error message.
|
// error message, otherwise we just send back a standard error message.
|
||||||
func (h *Handler) SendErrorJson(msg Message, err error, shouldLog ...bool) error {
|
func (h *Handler) SendErrorJson(msg Message, err error, shouldLog ...bool) error {
|
||||||
j := h.GetJwt()
|
j := h.GetJwt()
|
||||||
expected := errors.Is(err, server.ErrSuspended) ||
|
isJWTError := IsJwtError(err)
|
||||||
errors.Is(err, server.ErrIsRunning) ||
|
|
||||||
errors.Is(err, filesystem.ErrNotEnoughDiskSpace)
|
|
||||||
|
|
||||||
message := "an unexpected error was encountered while handling this request"
|
wsm := Message{
|
||||||
if expected || (j != nil && j.HasPermission(PermissionReceiveErrors)) {
|
Event: ErrorEvent,
|
||||||
message = err.Error()
|
Args: []string{"an unexpected error was encountered while handling this request"},
|
||||||
}
|
}
|
||||||
|
|
||||||
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}
|
wsm.Args = []string{m}
|
||||||
|
|
||||||
if len(shouldLog) == 0 || (len(shouldLog) == 1 && shouldLog[0] == true) {
|
if !isJWTError && (len(shouldLog) == 0 || (len(shouldLog) == 1 && shouldLog[0] == true)) {
|
||||||
if !expected && !IsJwtError(err) {
|
h.server.Log().WithFields(log.Fields{"event": msg.Event, "error_identifier": u.String(), "error": err}).
|
||||||
h.server.Log().WithFields(log.Fields{"event": msg.Event, "error_identifier": u.String(), "error": err}).
|
Errorf("error processing websocket event \"%s\"", msg.Event)
|
||||||
Error("failed to handle websocket process; an error was encountered processing an event")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return h.unsafeSendJson(wsm)
|
return h.unsafeSendJson(wsm)
|
||||||
@@ -260,7 +268,6 @@ func (h *Handler) HandleInbound(m Message) error {
|
|||||||
Event: JwtErrorEvent,
|
Event: JwtErrorEvent,
|
||||||
Args: []string{err.Error()},
|
Args: []string{err.Error()},
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,7 +311,7 @@ func (h *Handler) HandleInbound(m Message) error {
|
|||||||
|
|
||||||
// On every authentication event, send the current server status back
|
// On every authentication event, send the current server status back
|
||||||
// to the client. :)
|
// to the client. :)
|
||||||
state := h.server.GetState()
|
state := h.server.Environment.State()
|
||||||
h.SendJson(&Message{
|
h.SendJson(&Message{
|
||||||
Event: server.StatusEvent,
|
Event: server.StatusEvent,
|
||||||
Args: []string{state},
|
Args: []string{state},
|
||||||
@@ -361,7 +368,7 @@ func (h *Handler) HandleInbound(m Message) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
logs, err := h.server.Environment.Readlog(100)
|
logs, err := h.server.Environment.Readlog(config.Get().System.WebsocketLogCount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -391,7 +398,7 @@ func (h *Handler) HandleInbound(m Message) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.server.GetState() == environment.ProcessOfflineState {
|
if h.server.Environment.State() == environment.ProcessOfflineState {
|
||||||
return nil
|
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.
|
// so that we can better handle this and only set the environment to booted once we're attached.
|
||||||
//
|
//
|
||||||
// Or maybe just an IsBooted function?
|
// 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, ok := h.server.Environment.(*docker.Environment); ok {
|
||||||
if !e.IsAttached() {
|
if !e.IsAttached() {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func (a *Archiver) Exists() bool {
|
|||||||
func (a *Archiver) Stat() (*filesystem.Stat, error) {
|
func (a *Archiver) Stat() (*filesystem.Stat, error) {
|
||||||
s, err := os.Stat(a.Path())
|
s, err := os.Stat(a.Path())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &filesystem.Stat{
|
return &filesystem.Stat{
|
||||||
@@ -72,7 +72,6 @@ func (a *Archiver) Archive() error {
|
|||||||
// and not the actual file in this listing.
|
// and not the actual file in this listing.
|
||||||
if file.Mode()&os.ModeSymlink != 0 {
|
if file.Mode()&os.ModeSymlink != 0 {
|
||||||
f, err = a.Server.Filesystem().SafePath(filepath.Join(path, file.Name()))
|
f, err = a.Server.Filesystem().SafePath(filepath.Join(path, file.Name()))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -98,11 +97,7 @@ func (a *Archiver) DeleteIfExists() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.Remove(a.Path()); err != nil {
|
return errors.WithMessage(os.Remove(a.Path()), "archiver: failed to delete archive from system")
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checksum computes a SHA256 checksum of the server's archive.
|
// Checksum computes a SHA256 checksum of the server's archive.
|
||||||
|
|||||||
@@ -13,10 +13,8 @@ import (
|
|||||||
// Notifies the panel of a backup's state and returns an error if one is encountered
|
// Notifies the panel of a backup's state and returns an error if one is encountered
|
||||||
// while performing this action.
|
// while performing this action.
|
||||||
func (s *Server) notifyPanelOfBackup(uuid string, ad *backup.ArchiveDetails, successful bool) error {
|
func (s *Server) notifyPanelOfBackup(uuid string, ad *backup.ArchiveDetails, successful bool) error {
|
||||||
r := api.NewRequester()
|
if err := api.New().SendBackupStatus(uuid, ad.ToRequest(successful)); err != nil {
|
||||||
rerr, err := r.SendBackupStatus(uuid, ad.ToRequest(successful))
|
if !api.IsRequestError(err) {
|
||||||
if rerr != nil || err != nil {
|
|
||||||
if err != nil {
|
|
||||||
s.Log().WithFields(log.Fields{
|
s.Log().WithFields(log.Fields{
|
||||||
"backup": uuid,
|
"backup": uuid,
|
||||||
"error": err,
|
"error": err,
|
||||||
@@ -25,7 +23,7 @@ func (s *Server) notifyPanelOfBackup(uuid string, ad *backup.ArchiveDetails, suc
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.New(rerr.String())
|
return errors.New(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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.
|
// Get the included files based on the root path and the ignored files provided.
|
||||||
inc, err := s.GetIncludedBackupFiles(b.Ignored())
|
inc, err := s.GetIncludedBackupFiles(b.Ignored())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ad, err := b.Generate(inc, s.Filesystem().Path())
|
ad, err := b.Generate(inc, s.Filesystem().Path())
|
||||||
@@ -90,6 +88,11 @@ func (s *Server) Backup(b backup.BackupInterface) error {
|
|||||||
"backup": b.Identifier(),
|
"backup": b.Identifier(),
|
||||||
"error": notifyError,
|
"error": notifyError,
|
||||||
}).Warn("failed to notify panel of failed backup state")
|
}).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{}{
|
s.Events().PublishJson(BackupCompletedEvent+":"+b.Identifier(), map[string]interface{}{
|
||||||
@@ -100,15 +103,17 @@ func (s *Server) Backup(b backup.BackupInterface) error {
|
|||||||
"file_size": 0,
|
"file_size": 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
return errors.Wrap(err, "error while generating server backup")
|
return errors.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
|
// 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.
|
// 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 {
|
if notifyError := s.notifyPanelOfBackup(b.Identifier(), ad, true); notifyError != nil {
|
||||||
b.Remove()
|
b.Remove()
|
||||||
|
s.Log().WithField("error", notifyError).Info("failed to notify panel of successful backup state")
|
||||||
return notifyError
|
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
|
// 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 {
|
func (a *Archive) Create(dst string, ctx context.Context) error {
|
||||||
f, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
f, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ func (a *Archive) Create(dst string, ctx context.Context) error {
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return errors.WithStack(ctx.Err())
|
return ctx.Err()
|
||||||
default:
|
default:
|
||||||
return a.addToArchive(p, tw)
|
return a.addToArchive(p, tw)
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ func (a *Archive) Create(dst string, ctx context.Context) error {
|
|||||||
log.WithField("location", dst).Warn("failed to delete corrupted backup archive")
|
log.WithField("location", dst).Warn("failed to delete corrupted backup archive")
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -91,7 +91,7 @@ func (a *Archive) addToArchive(p string, w *tar.Writer) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
@@ -102,17 +102,15 @@ func (a *Archive) addToArchive(p string, w *tar.Writer) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
header := &tar.Header{
|
name := strings.TrimPrefix(p, a.TrimPrefix)
|
||||||
// Trim the long server path from the name of the file so that the resulting
|
header, err := tar.FileInfoHeader(s, name)
|
||||||
// archive is exactly how the user would see it in the panel file manager.
|
if err != nil {
|
||||||
Name: strings.TrimPrefix(p, a.TrimPrefix),
|
return errors.WithMessage(err, "failed to get tar#FileInfoHeader for "+name)
|
||||||
Size: s.Size(),
|
|
||||||
Mode: int64(s.Mode()),
|
|
||||||
ModTime: s.ModTime(),
|
|
||||||
}
|
}
|
||||||
|
header.Name = name
|
||||||
|
|
||||||
// These actions must occur sequentially, even if this function is called multiple
|
// These actions must occur sequentially, even if this function is called multiple
|
||||||
// in parallel. You'll get some nasty panic's otherwise.
|
// 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()
|
defer a.Unlock()
|
||||||
|
|
||||||
if err := w.WriteHeader(header); err != nil {
|
if err := w.WriteHeader(header); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := make([]byte, 4*1024)
|
buf := make([]byte, 4*1024)
|
||||||
if _, err := io.CopyBuffer(w, f, buf); err != nil {
|
if _, err := io.CopyBuffer(w, io.LimitReader(f, header.Size), buf); err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithMessage(err, "failed to copy "+header.Name+" to archive")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/pterodactyl/wings/api"
|
"github.com/pterodactyl/wings/api"
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"io"
|
"io"
|
||||||
@@ -87,7 +86,7 @@ func (b *Backup) Path() string {
|
|||||||
func (b *Backup) Size() (int64, error) {
|
func (b *Backup) Size() (int64, error) {
|
||||||
st, err := os.Stat(b.Path())
|
st, err := os.Stat(b.Path())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, errors.WithStack(err)
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return st.Size(), nil
|
return st.Size(), nil
|
||||||
@@ -99,7 +98,7 @@ func (b *Backup) Checksum() ([]byte, error) {
|
|||||||
|
|
||||||
f, err := os.Open(b.Path())
|
f, err := os.Open(b.Path())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
@@ -128,6 +127,7 @@ func (b *Backup) Details() *ArchiveDetails {
|
|||||||
"backup": b.Identifier(),
|
"backup": b.Identifier(),
|
||||||
"error": err,
|
"error": err,
|
||||||
}).Error("failed to calculate checksum for backup")
|
}).Error("failed to calculate checksum for backup")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
checksum = hex.EncodeToString(resp)
|
checksum = hex.EncodeToString(resp)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package backup
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/pkg/errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ func LocateLocal(uuid string) (*LocalBackup, os.FileInfo, error) {
|
|||||||
|
|
||||||
st, err := os.Stat(b.Path())
|
st, err := os.Stat(b.Path())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, errors.WithStack(err)
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if st.IsDir() {
|
if st.IsDir() {
|
||||||
@@ -48,7 +48,7 @@ func (b *LocalBackup) Generate(included *IncludedFiles, prefix string) (*Archive
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := a.Create(b.Path(), context.Background()); err != nil {
|
if err := a.Create(b.Path(), context.Background()); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.Details(), nil
|
return b.Details(), nil
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
package backup
|
package backup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Request struct {
|
type Request struct {
|
||||||
Adapter string `json:"adapter"`
|
Adapter string `json:"adapter"`
|
||||||
Uuid string `json:"uuid"`
|
Uuid string `json:"uuid"`
|
||||||
IgnoredFiles []string `json:"ignored_files"`
|
IgnoredFiles []string `json:"ignored_files"`
|
||||||
PresignedUrl string `json:"presigned_url"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generates a new local backup struct.
|
// Generates a new local backup struct.
|
||||||
@@ -32,15 +31,10 @@ func (r *Request) NewS3Backup() (*S3Backup, error) {
|
|||||||
return nil, errors.New(fmt.Sprintf("cannot create s3 backup using [%s] adapter", r.Adapter))
|
return nil, errors.New(fmt.Sprintf("cannot create s3 backup using [%s] adapter", r.Adapter))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(r.PresignedUrl) == 0 {
|
|
||||||
return nil, errors.New("a valid presigned S3 upload URL must be provided to use the [s3] adapter")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &S3Backup{
|
return &S3Backup{
|
||||||
Backup: Backup{
|
Backup: Backup{
|
||||||
Uuid: r.Uuid,
|
Uuid: r.Uuid,
|
||||||
IgnoredFiles: r.IgnoredFiles,
|
IgnoredFiles: r.IgnoredFiles,
|
||||||
},
|
},
|
||||||
PresignedUrl: r.PresignedUrl,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/pkg/errors"
|
"github.com/pterodactyl/wings/api"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -13,12 +13,6 @@ import (
|
|||||||
|
|
||||||
type S3Backup struct {
|
type S3Backup struct {
|
||||||
Backup
|
Backup
|
||||||
|
|
||||||
// The pre-signed upload endpoint for the generated backup. This must be
|
|
||||||
// provided otherwise this request will fail. This allows us to keep all
|
|
||||||
// of the keys off the daemon instances and the panel can handle generating
|
|
||||||
// the credentials for us.
|
|
||||||
PresignedUrl string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ BackupInterface = (*S3Backup)(nil)
|
var _ BackupInterface = (*S3Backup)(nil)
|
||||||
@@ -34,26 +28,20 @@ func (s *S3Backup) Generate(included *IncludedFiles, prefix string) (*ArchiveDet
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := a.Create(s.Path(), context.Background()); err != nil {
|
if err := a.Create(s.Path(), context.Background()); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rc, err := os.Open(s.Path())
|
rc, err := os.Open(s.Path())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rc.Close()
|
defer rc.Close()
|
||||||
|
|
||||||
if resp, err := s.generateRemoteRequest(rc); err != nil {
|
if err := s.generateRemoteRequest(rc); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
} else {
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("failed to put S3 object, %d:%s", resp.StatusCode, resp.Status)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.Details(), err
|
return s.Details(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removes a backup from the system.
|
// Removes a backup from the system.
|
||||||
@@ -61,27 +49,88 @@ func (s *S3Backup) Remove() error {
|
|||||||
return os.Remove(s.Path())
|
return os.Remove(s.Path())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generates the remote S3 request and begins the upload.
|
// Reader provides a wrapper around an existing io.Reader
|
||||||
func (s *S3Backup) generateRemoteRequest(rc io.ReadCloser) (*http.Response, error) {
|
// but implements io.Closer in order to satisfy an io.ReadCloser.
|
||||||
r, err := http.NewRequest(http.MethodPut, s.PresignedUrl, nil)
|
type Reader struct {
|
||||||
if err != nil {
|
io.Reader
|
||||||
return nil, err
|
}
|
||||||
}
|
|
||||||
|
func (Reader) Close() error {
|
||||||
if sz, err := s.Size(); err != nil {
|
return nil
|
||||||
return nil, err
|
}
|
||||||
} else {
|
|
||||||
r.ContentLength = sz
|
// Generates the remote S3 request and begins the upload.
|
||||||
r.Header.Add("Content-Length", strconv.Itoa(int(sz)))
|
func (s *S3Backup) generateRemoteRequest(rc io.ReadCloser) error {
|
||||||
r.Header.Add("Content-Type", "application/x-gzip")
|
defer rc.Close()
|
||||||
}
|
|
||||||
|
size, err := s.Backup.Size()
|
||||||
r.Body = rc
|
if err != nil {
|
||||||
|
return err
|
||||||
log.WithFields(log.Fields{
|
}
|
||||||
"endpoint": s.PresignedUrl,
|
|
||||||
"headers": r.Header,
|
urls, err := api.New().GetBackupRemoteUploadURLs(s.Backup.Uuid, size)
|
||||||
}).Debug("uploading backup to remote S3 endpoint")
|
if err != nil {
|
||||||
|
return err
|
||||||
return http.DefaultClient.Do(r)
|
}
|
||||||
|
|
||||||
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"github.com/pterodactyl/wings/environment"
|
"github.com/pterodactyl/wings/environment"
|
||||||
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -45,11 +45,10 @@ func (s *Server) handleServerCrash() error {
|
|||||||
// No point in doing anything here if the server isn't currently offline, there
|
// 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
|
// 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.
|
// 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 {
|
if !s.Config().CrashDetectionEnabled {
|
||||||
s.Log().Debug("server triggered crash detection but handler is disabled for server process")
|
s.Log().Debug("server triggered crash detection but handler is disabled for server process")
|
||||||
|
s.PublishConsoleOutputFromDaemon("Aborting automatic restart, crash detection is disabled for this instance.")
|
||||||
s.PublishConsoleOutputFromDaemon("Server detected as crashed; crash detection is disabled for this instance.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -57,14 +56,13 @@ func (s *Server) handleServerCrash() error {
|
|||||||
|
|
||||||
exitCode, oomKilled, err := s.Environment.ExitState()
|
exitCode, oomKilled, err := s.Environment.ExitState()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the system is not configured to detect a clean exit code as a crash, and the
|
// If the system is not configured to detect a clean exit code as a crash, and the
|
||||||
// crash is not the result of the program running out of memory, do nothing.
|
// 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")
|
s.Log().Debug("server exited with successful exit code; system is configured to not detect this as a crash")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,11 +71,14 @@ func (s *Server) handleServerCrash() error {
|
|||||||
s.PublishConsoleOutputFromDaemon(fmt.Sprintf("Out of memory: %t", oomKilled))
|
s.PublishConsoleOutputFromDaemon(fmt.Sprintf("Out of memory: %t", oomKilled))
|
||||||
|
|
||||||
c := s.crasher.LastCrashTime()
|
c := s.crasher.LastCrashTime()
|
||||||
// If the last crash time was within the last 60 seconds we do not want to perform
|
timeout := config.Get().System.CrashDetection.Timeout
|
||||||
// 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.")
|
|
||||||
|
|
||||||
|
// 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{}
|
return &crashTooFrequent{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import "github.com/pkg/errors"
|
import (
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
var ErrIsRunning = errors.New("server is running")
|
var ErrIsRunning = errors.New("server is running")
|
||||||
var ErrSuspended = errors.New("server is currently in a suspended state")
|
var ErrSuspended = errors.New("server is currently in a suspended state")
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/pterodactyl/wings/server/filesystem"
|
"github.com/pterodactyl/wings/server/filesystem"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
@@ -13,12 +12,12 @@ func (s *Server) Filesystem() *filesystem.Filesystem {
|
|||||||
// Ensures that the data directory for the server instance exists.
|
// Ensures that the data directory for the server instance exists.
|
||||||
func (s *Server) EnsureDataDirectoryExists() error {
|
func (s *Server) EnsureDataDirectoryExists() error {
|
||||||
if _, err := os.Stat(s.fs.Path()); err != nil && !os.IsNotExist(err) {
|
if _, err := os.Stat(s.fs.Path()); err != nil && !os.IsNotExist(err) {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
// Create the server data directory because it does not currently exist
|
// Create the server data directory because it does not currently exist
|
||||||
// on the system.
|
// on the system.
|
||||||
if err := os.MkdirAll(s.fs.Path(), 0700); err != nil {
|
if err := os.MkdirAll(s.fs.Path(), 0700); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.fs.Chown("/"); err != nil {
|
if err := s.fs.Chown("/"); err != nil {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/karrick/godirwalk"
|
"github.com/karrick/godirwalk"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/pterodactyl/wings/server/backup"
|
"github.com/pterodactyl/wings/server/backup"
|
||||||
ignore "github.com/sabhiram/go-gitignore"
|
ignore "github.com/sabhiram/go-gitignore"
|
||||||
"os"
|
"os"
|
||||||
@@ -41,7 +40,7 @@ func (fs *Filesystem) GetIncludedFiles(dir string, ignored []string) (*backup.In
|
|||||||
if e.IsSymlink() {
|
if e.IsSymlink() {
|
||||||
sp, err = fs.SafePath(p)
|
sp, err = fs.SafePath(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, ErrBadPathResolution) {
|
if IsBadPathResolutionError(err) {
|
||||||
return godirwalk.SkipThis
|
return godirwalk.SkipThis
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +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
|
// 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.
|
// use the resolved location for the rest of this function.
|
||||||
sp, err = fs.SafePath(p)
|
sp, err = fs.SafePath(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, ErrBadPathResolution) {
|
if IsBadPathResolutionError(err) {
|
||||||
return godirwalk.SkipThis
|
return godirwalk.SkipThis
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +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), ":", "")))
|
d := path.Join(cleanedRootDir, fmt.Sprintf("archive-%s.tar.gz", strings.ReplaceAll(time.Now().Format(time.RFC3339), ":", "")))
|
||||||
|
|
||||||
if err := a.Create(d, context.Background()); err != nil {
|
if err := a.Create(d, context.Background()); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.Stat(d)
|
f, err := os.Stat(d)
|
||||||
|
|||||||
@@ -47,10 +47,10 @@ func (fs *Filesystem) SpaceAvailableForDecompression(dir string, file string) (b
|
|||||||
return false, ErrUnknownArchiveFormat
|
return false, ErrUnknownArchiveFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, errors.WithStack(err)
|
return false, 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
|
// Decompress a file in a given directory by using the archiver tool to infer the file
|
||||||
@@ -60,12 +60,12 @@ func (fs *Filesystem) SpaceAvailableForDecompression(dir string, file string) (b
|
|||||||
func (fs *Filesystem) DecompressFile(dir string, file string) error {
|
func (fs *Filesystem) DecompressFile(dir string, file string) error {
|
||||||
source, err := fs.SafePath(filepath.Join(dir, file))
|
source, err := fs.SafePath(filepath.Join(dir, file))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the file exists basically.
|
// Make sure the file exists basically.
|
||||||
if _, err := os.Stat(source); err != nil {
|
if _, err := os.Stat(source); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Walk over all of the files spinning up an additional go-routine for each file we've encountered
|
// Walk over all of the files spinning up an additional go-routine for each file we've encountered
|
||||||
@@ -93,17 +93,17 @@ func (fs *Filesystem) DecompressFile(dir string, file string) error {
|
|||||||
|
|
||||||
p, err := fs.SafePath(filepath.Join(dir, name))
|
p, err := fs.SafePath(filepath.Join(dir, name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to generate a safe path to server file")
|
return errors.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 err != nil {
|
||||||
if strings.HasPrefix(err.Error(), "format ") {
|
if strings.HasPrefix(err.Error(), "format ") {
|
||||||
return errors.WithStack(ErrUnknownArchiveFormat)
|
return ErrUnknownArchiveFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package filesystem
|
|||||||
import (
|
import (
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/karrick/godirwalk"
|
"github.com/karrick/godirwalk"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"syscall"
|
"syscall"
|
||||||
@@ -97,6 +96,11 @@ func (fs *Filesystem) CachedUsage() int64 {
|
|||||||
// This is primarily to avoid a bunch of I/O operations from piling up on the server, especially on servers
|
// 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.
|
// with a large amount of files.
|
||||||
func (fs *Filesystem) DiskUsage(allowStaleValue bool) (int64, error) {
|
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 !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
|
// 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.
|
// value. This is a blocking operation to the calling process.
|
||||||
@@ -153,7 +157,7 @@ func (fs *Filesystem) updateCachedDiskUsage() (int64, error) {
|
|||||||
func (fs *Filesystem) DirectorySize(dir string) (int64, error) {
|
func (fs *Filesystem) DirectorySize(dir string) (int64, error) {
|
||||||
d, err := fs.SafePath(dir)
|
d, err := fs.SafePath(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, errors.WithStack(err)
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var size int64
|
var size int64
|
||||||
@@ -167,7 +171,7 @@ func (fs *Filesystem) DirectorySize(dir string) (int64, error) {
|
|||||||
// it. Otherwise, allow it to continue.
|
// it. Otherwise, allow it to continue.
|
||||||
if e.IsSymlink() {
|
if e.IsSymlink() {
|
||||||
if _, err := fs.SafePath(p); err != nil {
|
if _, err := fs.SafePath(p); err != nil {
|
||||||
if errors.Is(err, ErrBadPathResolution) {
|
if IsBadPathResolutionError(err) {
|
||||||
return godirwalk.SkipThis
|
return godirwalk.SkipThis
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +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.
|
// Helper function to determine if a server has space available for a file of a given size.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package filesystem
|
package filesystem
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"os"
|
"os"
|
||||||
@@ -9,9 +10,42 @@ import (
|
|||||||
|
|
||||||
var ErrIsDirectory = errors.New("filesystem: is a directory")
|
var ErrIsDirectory = errors.New("filesystem: is a directory")
|
||||||
var ErrNotEnoughDiskSpace = errors.New("filesystem: not enough disk space")
|
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")
|
var ErrUnknownArchiveFormat = errors.New("filesystem: unknown archive format")
|
||||||
|
|
||||||
|
type BadPathResolutionError struct {
|
||||||
|
path string
|
||||||
|
resolved string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the specific error for a bad path resolution.
|
||||||
|
func (b *BadPathResolutionError) Error() string {
|
||||||
|
r := b.resolved
|
||||||
|
if r == "" {
|
||||||
|
r = "<empty>"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("filesystem: server path [%s] resolves to a location outside the server root: %s", b.path, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a new BadPathResolution error.
|
||||||
|
func NewBadPathResolution(path string, resolved string) *BadPathResolutionError {
|
||||||
|
return &BadPathResolutionError{path, resolved}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determines if the given error is a bad path resolution error.
|
||||||
|
func IsBadPathResolutionError(err error) bool {
|
||||||
|
e := errors.Unwrap(err)
|
||||||
|
if e == nil {
|
||||||
|
e = err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := e.(*BadPathResolutionError); ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Generates an error logger instance with some basic information.
|
// Generates an error logger instance with some basic information.
|
||||||
func (fs *Filesystem) error(err error) *log.Entry {
|
func (fs *Filesystem) error(err error) *log.Entry {
|
||||||
return log.WithField("subsystem", "filesystem").WithField("root", fs.root).WithField("error", err)
|
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
|
// directory, otherwise return nil. Returning this error for a file will stop the walking
|
||||||
// for the remainder of the directory. This is assuming an os.FileInfo struct was even returned.
|
// for the remainder of the directory. This is assuming an os.FileInfo struct was even returned.
|
||||||
func (fs *Filesystem) handleWalkerError(err error, f os.FileInfo) error {
|
func (fs *Filesystem) handleWalkerError(err error, f os.FileInfo) error {
|
||||||
if !errors.Is(err, ErrBadPathResolution) {
|
if !IsBadPathResolutionError(err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,4 +66,4 @@ func (fs *Filesystem) handleWalkerError(err error, f os.FileInfo) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
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 {
|
func (fs *Filesystem) Writefile(p string, r io.Reader) error {
|
||||||
cleaned, err := fs.SafePath(p)
|
cleaned, err := fs.SafePath(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentSize int64
|
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.
|
// to it and an empty file. We'll then write to it later on after this completes.
|
||||||
if stat, err := os.Stat(cleaned); err != nil {
|
if stat, err := os.Stat(cleaned); err != nil {
|
||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(cleaned), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(cleaned), 0755); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := fs.Chown(filepath.Dir(cleaned)); err != nil {
|
if err := fs.Chown(filepath.Dir(cleaned)); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if stat.IsDir() {
|
if stat.IsDir() {
|
||||||
@@ -119,7 +119,7 @@ func (fs *Filesystem) Writefile(p string, r io.Reader) error {
|
|||||||
// truncate the existing file.
|
// truncate the existing file.
|
||||||
file, err := o.open(cleaned, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
file, err := o.open(cleaned, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ func (fs *Filesystem) Writefile(p string, r io.Reader) error {
|
|||||||
func (fs *Filesystem) CreateDirectory(name string, p string) error {
|
func (fs *Filesystem) CreateDirectory(name string, p string) error {
|
||||||
cleaned, err := fs.SafePath(path.Join(p, name))
|
cleaned, err := fs.SafePath(path.Join(p, name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.MkdirAll(cleaned, 0755)
|
return os.MkdirAll(cleaned, 0755)
|
||||||
@@ -148,12 +148,12 @@ func (fs *Filesystem) CreateDirectory(name string, p string) error {
|
|||||||
func (fs *Filesystem) Rename(from string, to string) error {
|
func (fs *Filesystem) Rename(from string, to string) error {
|
||||||
cleanedFrom, err := fs.SafePath(from)
|
cleanedFrom, err := fs.SafePath(from)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanedTo, err := fs.SafePath(to)
|
cleanedTo, err := fs.SafePath(to)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the target file or directory already exists the rename function will fail, so just
|
// If the target file or directory already exists the rename function will fail, so just
|
||||||
@@ -171,7 +171,7 @@ func (fs *Filesystem) Rename(from string, to string) error {
|
|||||||
// we're not at the root directory level.
|
// we're not at the root directory level.
|
||||||
if d != fs.Path() {
|
if d != fs.Path() {
|
||||||
if mkerr := os.MkdirAll(d, 0755); mkerr != nil {
|
if mkerr := os.MkdirAll(d, 0755); mkerr != nil {
|
||||||
return errors.Wrap(mkerr, "failed to create directory structure for file rename")
|
return errors.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 {
|
func (fs *Filesystem) Chown(path string) error {
|
||||||
cleaned, err := fs.SafePath(path)
|
cleaned, err := fs.SafePath(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if fs.isTest {
|
if fs.isTest {
|
||||||
@@ -197,7 +197,7 @@ func (fs *Filesystem) Chown(path string) error {
|
|||||||
|
|
||||||
// Start by just chowning the initial path that we received.
|
// Start by just chowning the initial path that we received.
|
||||||
if err := os.Chown(cleaned, uid, gid); err != nil {
|
if err := os.Chown(cleaned, uid, gid); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this is not a directory we can now return from the function, there is nothing
|
// If this is not a directory we can now return from the function, there is nothing
|
||||||
@@ -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
|
// 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
|
// 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
|
// 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
|
// If we stat the file and it does not exist that means we're good to create the copy. If it
|
||||||
// does exist, we'll just continue to the next loop and try again.
|
// does exist, we'll just continue to the next loop and try again.
|
||||||
if _, err := fs.Stat(path.Join(dir, n)); err != nil {
|
if _, err := fs.Stat(path.Join(dir, n)); err != nil {
|
||||||
if !os.IsNotExist(err) {
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,12 +285,12 @@ func (fs *Filesystem) findCopySuffix(dir string, name string, extension string)
|
|||||||
func (fs *Filesystem) Copy(p string) error {
|
func (fs *Filesystem) Copy(p string) error {
|
||||||
cleaned, err := fs.SafePath(p)
|
cleaned, err := fs.SafePath(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err := os.Stat(cleaned)
|
s, err := os.Stat(cleaned)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
} else if s.IsDir() || !s.Mode().IsRegular() {
|
} else if s.IsDir() || !s.Mode().IsRegular() {
|
||||||
// If this is a directory or not a regular file, just throw a not-exist error
|
// If this is a directory or not a regular file, just throw a not-exist error
|
||||||
// since anything calling this function should understand what that means.
|
// since anything calling this function should understand what that means.
|
||||||
@@ -300,11 +317,14 @@ func (fs *Filesystem) Copy(p string) error {
|
|||||||
|
|
||||||
source, err := os.Open(cleaned)
|
source, err := os.Open(cleaned)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
defer source.Close()
|
defer source.Close()
|
||||||
|
|
||||||
n, err := fs.findCopySuffix(relative, name, extension)
|
n, err := fs.findCopySuffix(relative, name, extension)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return fs.Writefile(path.Join(relative, n), source)
|
return fs.Writefile(path.Join(relative, n), source)
|
||||||
}
|
}
|
||||||
@@ -324,7 +344,7 @@ func (fs *Filesystem) Delete(p string) error {
|
|||||||
// exists within the data directory.
|
// exists within the data directory.
|
||||||
resolved := fs.unsafeFilePath(p)
|
resolved := fs.unsafeFilePath(p)
|
||||||
if !fs.unsafeIsInDataDirectory(resolved) {
|
if !fs.unsafeIsInDataDirectory(resolved) {
|
||||||
return ErrBadPathResolution
|
return NewBadPathResolution(p, resolved)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Block any whoopsies.
|
// Block any whoopsies.
|
||||||
|
|||||||
@@ -70,224 +70,6 @@ func (rfs *rootFs) reset() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFilesystem_Path(t *testing.T) {
|
|
||||||
g := Goblin(t)
|
|
||||||
fs, rfs := NewFs()
|
|
||||||
|
|
||||||
g.Describe("Path", func() {
|
|
||||||
g.It("returns the root path for the instance", func() {
|
|
||||||
g.Assert(fs.Path()).Equal(filepath.Join(rfs.root, "/server"))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFilesystem_SafePath(t *testing.T) {
|
|
||||||
g := Goblin(t)
|
|
||||||
fs, rfs := NewFs()
|
|
||||||
prefix := filepath.Join(rfs.root, "/server")
|
|
||||||
|
|
||||||
g.Describe("SafePath", func() {
|
|
||||||
g.It("returns a cleaned path to a given file", func() {
|
|
||||||
p, err := fs.SafePath("test.txt")
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(p).Equal(prefix + "/test.txt")
|
|
||||||
|
|
||||||
p, err = fs.SafePath("/test.txt")
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(p).Equal(prefix + "/test.txt")
|
|
||||||
|
|
||||||
p, err = fs.SafePath("./test.txt")
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(p).Equal(prefix + "/test.txt")
|
|
||||||
|
|
||||||
p, err = fs.SafePath("/foo/../test.txt")
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(p).Equal(prefix + "/test.txt")
|
|
||||||
|
|
||||||
p, err = fs.SafePath("/foo/bar")
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(p).Equal(prefix + "/foo/bar")
|
|
||||||
})
|
|
||||||
|
|
||||||
g.It("handles root directory access", func() {
|
|
||||||
p, err := fs.SafePath("/")
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(p).Equal(prefix)
|
|
||||||
|
|
||||||
p, err = fs.SafePath("")
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(p).Equal(prefix)
|
|
||||||
})
|
|
||||||
|
|
||||||
g.It("removes trailing slashes from paths", func() {
|
|
||||||
p, err := fs.SafePath("/foo/bar/")
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(p).Equal(prefix + "/foo/bar")
|
|
||||||
})
|
|
||||||
|
|
||||||
g.It("handles deeply nested directories that do not exist", func() {
|
|
||||||
p, err := fs.SafePath("/foo/bar/baz/quaz/../../ducks/testing.txt")
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
g.Assert(p).Equal(prefix + "/foo/bar/ducks/testing.txt")
|
|
||||||
})
|
|
||||||
|
|
||||||
g.It("blocks access to files outside the root directory", func() {
|
|
||||||
p, err := fs.SafePath("../test.txt")
|
|
||||||
g.Assert(err).IsNotNil()
|
|
||||||
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
|
|
||||||
g.Assert(p).Equal("")
|
|
||||||
|
|
||||||
p, err = fs.SafePath("/../test.txt")
|
|
||||||
g.Assert(err).IsNotNil()
|
|
||||||
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
|
|
||||||
g.Assert(p).Equal("")
|
|
||||||
|
|
||||||
p, err = fs.SafePath("./foo/../../test.txt")
|
|
||||||
g.Assert(err).IsNotNil()
|
|
||||||
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
|
|
||||||
g.Assert(p).Equal("")
|
|
||||||
|
|
||||||
p, err = fs.SafePath("..")
|
|
||||||
g.Assert(err).IsNotNil()
|
|
||||||
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
|
|
||||||
g.Assert(p).Equal("")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// We test against accessing files outside the root directory in the tests, however it
|
|
||||||
// is still possible for someone to mess up and not properly use this safe path call. In
|
|
||||||
// order to truly confirm this, we'll try to pass in a symlinked malicious file to all of
|
|
||||||
// the calls and ensure they all fail with the same reason.
|
|
||||||
func TestFilesystem_Blocks_Symlinks(t *testing.T) {
|
|
||||||
g := Goblin(t)
|
|
||||||
fs, rfs := NewFs()
|
|
||||||
|
|
||||||
if err := rfs.CreateServerFile("/../malicious.txt", "external content"); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.Mkdir(filepath.Join(rfs.root, "/malicious_dir"), 0777); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.Symlink(filepath.Join(rfs.root, "malicious.txt"), filepath.Join(rfs.root, "/server/symlinked.txt")); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.Symlink(filepath.Join(rfs.root, "/malicious_dir"), filepath.Join(rfs.root, "/server/external_dir")); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
g.Describe("Readfile", func() {
|
|
||||||
g.It("cannot read a file symlinked outside the root", func() {
|
|
||||||
b := bytes.Buffer{}
|
|
||||||
|
|
||||||
err := fs.Readfile("symlinked.txt", &b)
|
|
||||||
g.Assert(err).IsNotNil()
|
|
||||||
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
g.Describe("Writefile", func() {
|
|
||||||
g.It("cannot write to a file symlinked outside the root", func() {
|
|
||||||
r := bytes.NewReader([]byte("testing"))
|
|
||||||
|
|
||||||
err := fs.Writefile("symlinked.txt", r)
|
|
||||||
g.Assert(err).IsNotNil()
|
|
||||||
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
|
|
||||||
})
|
|
||||||
|
|
||||||
g.It("cannot write a file to a directory symlinked outside the root", func() {
|
|
||||||
r := bytes.NewReader([]byte("testing"))
|
|
||||||
|
|
||||||
err := fs.Writefile("external_dir/foo.txt", r)
|
|
||||||
g.Assert(err).IsNotNil()
|
|
||||||
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
g.Describe("CreateDirectory", func() {
|
|
||||||
g.It("cannot create a directory outside the root", func() {
|
|
||||||
err := fs.CreateDirectory("my_dir", "external_dir")
|
|
||||||
g.Assert(err).IsNotNil()
|
|
||||||
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
|
|
||||||
})
|
|
||||||
|
|
||||||
g.It("cannot create a nested directory outside the root", func() {
|
|
||||||
err := fs.CreateDirectory("my/nested/dir", "external_dir/foo/bar")
|
|
||||||
g.Assert(err).IsNotNil()
|
|
||||||
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
|
|
||||||
})
|
|
||||||
|
|
||||||
g.It("cannot create a nested directory outside the root", func() {
|
|
||||||
err := fs.CreateDirectory("my/nested/dir", "external_dir/server")
|
|
||||||
g.Assert(err).IsNotNil()
|
|
||||||
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
g.Describe("Rename", func() {
|
|
||||||
g.It("cannot rename a file symlinked outside the directory root", func() {
|
|
||||||
err := fs.Rename("symlinked.txt", "foo.txt")
|
|
||||||
g.Assert(err).IsNotNil()
|
|
||||||
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
|
|
||||||
})
|
|
||||||
|
|
||||||
g.It("cannot rename a symlinked directory outside the root", func() {
|
|
||||||
err := fs.Rename("external_dir", "foo")
|
|
||||||
g.Assert(err).IsNotNil()
|
|
||||||
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
|
|
||||||
})
|
|
||||||
|
|
||||||
g.It("cannot rename a file to a location outside the directory root", func() {
|
|
||||||
rfs.CreateServerFile("my_file.txt", "internal content")
|
|
||||||
|
|
||||||
err := fs.Rename("my_file.txt", "external_dir/my_file.txt")
|
|
||||||
g.Assert(err).IsNotNil()
|
|
||||||
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
g.Describe("Chown", func() {
|
|
||||||
g.It("cannot chown a file symlinked outside the directory root", func() {
|
|
||||||
err := fs.Chown("symlinked.txt")
|
|
||||||
g.Assert(err).IsNotNil()
|
|
||||||
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
|
|
||||||
})
|
|
||||||
|
|
||||||
g.It("cannot chown a directory symlinked outside the directory root", func() {
|
|
||||||
err := fs.Chown("external_dir")
|
|
||||||
g.Assert(err).IsNotNil()
|
|
||||||
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
g.Describe("Copy", func() {
|
|
||||||
g.It("cannot copy a file symlinked outside the directory root", func() {
|
|
||||||
err := fs.Copy("symlinked.txt")
|
|
||||||
g.Assert(err).IsNotNil()
|
|
||||||
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
g.Describe("Delete", func() {
|
|
||||||
g.It("deletes the symlinked file but leaves the source", func() {
|
|
||||||
err := fs.Delete("symlinked.txt")
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
|
|
||||||
_, err = os.Stat(filepath.Join(rfs.root, "malicious.txt"))
|
|
||||||
g.Assert(err).IsNil()
|
|
||||||
|
|
||||||
_, err = rfs.StatServerFile("symlinked.txt")
|
|
||||||
g.Assert(err).IsNotNil()
|
|
||||||
g.Assert(errors.Is(err, os.ErrNotExist)).IsTrue()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
rfs.reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFilesystem_Readfile(t *testing.T) {
|
func TestFilesystem_Readfile(t *testing.T) {
|
||||||
g := Goblin(t)
|
g := Goblin(t)
|
||||||
fs, rfs := NewFs()
|
fs, rfs := NewFs()
|
||||||
@@ -325,7 +107,7 @@ func TestFilesystem_Readfile(t *testing.T) {
|
|||||||
|
|
||||||
err = fs.Readfile("/../test.txt", buf)
|
err = fs.Readfile("/../test.txt", buf)
|
||||||
g.Assert(err).IsNotNil()
|
g.Assert(err).IsNotNil()
|
||||||
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
|
g.Assert(IsBadPathResolutionError(err)).IsTrue()
|
||||||
})
|
})
|
||||||
|
|
||||||
g.AfterEach(func() {
|
g.AfterEach(func() {
|
||||||
@@ -386,7 +168,7 @@ func TestFilesystem_Writefile(t *testing.T) {
|
|||||||
|
|
||||||
err := fs.Writefile("/some/../foo/../../test.txt", r)
|
err := fs.Writefile("/some/../foo/../../test.txt", r)
|
||||||
g.Assert(err).IsNotNil()
|
g.Assert(err).IsNotNil()
|
||||||
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
|
g.Assert(IsBadPathResolutionError(err)).IsTrue()
|
||||||
})
|
})
|
||||||
|
|
||||||
g.It("cannot write a file that exceeds the disk limits", func() {
|
g.It("cannot write a file that exceeds the disk limits", func() {
|
||||||
@@ -477,7 +259,7 @@ func TestFilesystem_CreateDirectory(t *testing.T) {
|
|||||||
g.It("should not allow the creation of directories outside the root", func() {
|
g.It("should not allow the creation of directories outside the root", func() {
|
||||||
err := fs.CreateDirectory("test", "e/../../something")
|
err := fs.CreateDirectory("test", "e/../../something")
|
||||||
g.Assert(err).IsNotNil()
|
g.Assert(err).IsNotNil()
|
||||||
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
|
g.Assert(IsBadPathResolutionError(err)).IsTrue()
|
||||||
})
|
})
|
||||||
|
|
||||||
g.It("should not increment the disk usage", func() {
|
g.It("should not increment the disk usage", func() {
|
||||||
@@ -527,7 +309,7 @@ func TestFilesystem_Rename(t *testing.T) {
|
|||||||
g.It("does not allow renaming to a location outside the root", func() {
|
g.It("does not allow renaming to a location outside the root", func() {
|
||||||
err := fs.Rename("source.txt", "../target.txt")
|
err := fs.Rename("source.txt", "../target.txt")
|
||||||
g.Assert(err).IsNotNil()
|
g.Assert(err).IsNotNil()
|
||||||
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
|
g.Assert(IsBadPathResolutionError(err)).IsTrue()
|
||||||
})
|
})
|
||||||
|
|
||||||
g.It("does not allow renaming from a location outside the root", func() {
|
g.It("does not allow renaming from a location outside the root", func() {
|
||||||
@@ -535,7 +317,7 @@ func TestFilesystem_Rename(t *testing.T) {
|
|||||||
|
|
||||||
err = fs.Rename("/../ext-source.txt", "target.txt")
|
err = fs.Rename("/../ext-source.txt", "target.txt")
|
||||||
g.Assert(err).IsNotNil()
|
g.Assert(err).IsNotNil()
|
||||||
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
|
g.Assert(IsBadPathResolutionError(err)).IsTrue()
|
||||||
})
|
})
|
||||||
|
|
||||||
g.It("allows a file to be renamed", func() {
|
g.It("allows a file to be renamed", func() {
|
||||||
@@ -613,7 +395,7 @@ func TestFilesystem_Copy(t *testing.T) {
|
|||||||
|
|
||||||
err = fs.Copy("../ext-source.txt")
|
err = fs.Copy("../ext-source.txt")
|
||||||
g.Assert(err).IsNotNil()
|
g.Assert(err).IsNotNil()
|
||||||
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
|
g.Assert(IsBadPathResolutionError(err)).IsTrue()
|
||||||
})
|
})
|
||||||
|
|
||||||
g.It("should return an error if the source directory is outside the root", func() {
|
g.It("should return an error if the source directory is outside the root", func() {
|
||||||
@@ -625,11 +407,11 @@ func TestFilesystem_Copy(t *testing.T) {
|
|||||||
|
|
||||||
err = fs.Copy("../nested/in/dir/ext-source.txt")
|
err = fs.Copy("../nested/in/dir/ext-source.txt")
|
||||||
g.Assert(err).IsNotNil()
|
g.Assert(err).IsNotNil()
|
||||||
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
|
g.Assert(IsBadPathResolutionError(err)).IsTrue()
|
||||||
|
|
||||||
err = fs.Copy("nested/in/../../../nested/in/dir/ext-source.txt")
|
err = fs.Copy("nested/in/../../../nested/in/dir/ext-source.txt")
|
||||||
g.Assert(err).IsNotNil()
|
g.Assert(err).IsNotNil()
|
||||||
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
|
g.Assert(IsBadPathResolutionError(err)).IsTrue()
|
||||||
})
|
})
|
||||||
|
|
||||||
g.It("should return an error if the source is a directory", func() {
|
g.It("should return an error if the source is a directory", func() {
|
||||||
@@ -721,7 +503,7 @@ func TestFilesystem_Delete(t *testing.T) {
|
|||||||
|
|
||||||
err = fs.Delete("../ext-source.txt")
|
err = fs.Delete("../ext-source.txt")
|
||||||
g.Assert(err).IsNotNil()
|
g.Assert(err).IsNotNil()
|
||||||
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
|
g.Assert(IsBadPathResolutionError(err)).IsTrue()
|
||||||
})
|
})
|
||||||
|
|
||||||
g.It("does not allow the deletion of the root directory", func() {
|
g.It("does not allow the deletion of the root directory", func() {
|
||||||
|
|||||||
@@ -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
|
// At the same time, evaluate the symlink status and determine where this file or folder
|
||||||
// is truly pointing to.
|
// is truly pointing to.
|
||||||
p, err := filepath.EvalSymlinks(r)
|
ep, err := filepath.EvalSymlinks(r)
|
||||||
if err != nil && !os.IsNotExist(err) {
|
if err != nil && !os.IsNotExist(err) {
|
||||||
return "", err
|
return "", err
|
||||||
} else if os.IsNotExist(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.
|
// attempt going on, and we should NOT resolve this path for them.
|
||||||
if nonExistentPathResolution != "" {
|
if nonExistentPathResolution != "" {
|
||||||
if !fs.unsafeIsInDataDirectory(nonExistentPathResolution) {
|
if !fs.unsafeIsInDataDirectory(nonExistentPathResolution) {
|
||||||
return "", ErrBadPathResolution
|
return "", NewBadPathResolution(p, nonExistentPathResolution)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the nonExistentPathResolution variable is not empty then the initial path requested
|
// If the nonExistentPathResolution variable is not empty then the initial path requested
|
||||||
@@ -66,11 +66,11 @@ func (fs *Filesystem) SafePath(p string) (string, error) {
|
|||||||
// If the requested directory from EvalSymlinks begins with the server root directory go
|
// If the requested directory from EvalSymlinks begins with the server root directory go
|
||||||
// ahead and return it. If not we'll return an error which will block any further action
|
// ahead and return it. If not we'll return an error which will block any further action
|
||||||
// on the file.
|
// on the file.
|
||||||
if fs.unsafeIsInDataDirectory(p) {
|
if fs.unsafeIsInDataDirectory(ep) {
|
||||||
return p, nil
|
return ep, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", ErrBadPathResolution
|
return "", NewBadPathResolution(p, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a path to the file by cleaning it up and appending the root server path to it. This
|
// Generate a path to the file by cleaning it up and appending the root server path to it. This
|
||||||
|
|||||||
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"
|
"encoding/json"
|
||||||
"github.com/gabriel-vasile/mimetype"
|
"github.com/gabriel-vasile/mimetype"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,16 +19,19 @@ func (s *Stat) MarshalJSON() ([]byte, error) {
|
|||||||
Created string `json:"created"`
|
Created string `json:"created"`
|
||||||
Modified string `json:"modified"`
|
Modified string `json:"modified"`
|
||||||
Mode string `json:"mode"`
|
Mode string `json:"mode"`
|
||||||
|
ModeBits string `json:"mode_bits"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
Directory bool `json:"directory"`
|
Directory bool `json:"directory"`
|
||||||
File bool `json:"file"`
|
File bool `json:"file"`
|
||||||
Symlink bool `json:"symlink"`
|
Symlink bool `json:"symlink"`
|
||||||
Mime string `json:"mime"`
|
Mime string `json:"mime"`
|
||||||
}{
|
}{
|
||||||
Name: s.Info.Name(),
|
Name: s.Info.Name(),
|
||||||
Created: s.CTime().Format(time.RFC3339),
|
Created: s.CTime().Format(time.RFC3339),
|
||||||
Modified: s.Info.ModTime().Format(time.RFC3339),
|
Modified: s.Info.ModTime().Format(time.RFC3339),
|
||||||
Mode: s.Info.Mode().String(),
|
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(),
|
Size: s.Info.Size(),
|
||||||
Directory: s.Info.IsDir(),
|
Directory: s.Info.IsDir(),
|
||||||
File: !s.Info.IsDir(),
|
File: !s.Info.IsDir(),
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"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
|
// Ensure that the server is marked as offline at this point, otherwise you end up
|
||||||
// with a blank value which is a bit confusing.
|
// with a blank value which is a bit confusing.
|
||||||
s.SetState(environment.ProcessOfflineState)
|
s.Environment.SetState(environment.ProcessOfflineState)
|
||||||
|
|
||||||
// Push an event to the websocket so we can auto-refresh the information in the panel once
|
// Push an event to the websocket so we can auto-refresh the information in the panel once
|
||||||
// the install is completed.
|
// the install is completed.
|
||||||
@@ -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
|
// 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.
|
// does not touch any existing files for the server, other than what the script modifies.
|
||||||
func (s *Server) Reinstall() error {
|
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")
|
s.Log().Debug("waiting for server instance to enter a stopped state")
|
||||||
if err := s.Environment.WaitForStop(10, true); err != nil {
|
if err := s.Environment.WaitForStop(10, true); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -87,18 +88,18 @@ func (s *Server) Reinstall() error {
|
|||||||
|
|
||||||
// Internal installation function used to simplify reporting back to the Panel.
|
// Internal installation function used to simplify reporting back to the Panel.
|
||||||
func (s *Server) internalInstall() error {
|
func (s *Server) internalInstall() error {
|
||||||
script, rerr, err := api.NewRequester().GetInstallationScript(s.Id())
|
script, err := api.New().GetInstallationScript(s.Id())
|
||||||
if err != nil || rerr != nil {
|
if err != nil {
|
||||||
if err != nil {
|
if !api.IsRequestError(err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.New(rerr.String())
|
return errors.New(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
p, err := NewInstallationProcess(s, &script)
|
p, err := NewInstallationProcess(s, &script)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Log().Info("beginning installation process for server")
|
s.Log().Info("beginning installation process for server")
|
||||||
@@ -130,7 +131,7 @@ func NewInstallationProcess(s *Server, script *api.InstallationScript) (*Install
|
|||||||
s.installer.cancel = &cancel
|
s.installer.cancel = &cancel
|
||||||
|
|
||||||
if c, err := environment.DockerClient(); err != nil {
|
if c, err := environment.DockerClient(); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
proc.client = c
|
proc.client = c
|
||||||
proc.context = ctx
|
proc.context = ctx
|
||||||
@@ -193,7 +194,7 @@ func (ip *InstallationProcess) RemoveContainer() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil && !client.IsErrNotFound(err) {
|
if err != nil && !client.IsErrNotFound(err) {
|
||||||
ip.Server.Log().WithField("error", errors.WithStack(err)).Warn("failed to delete server install container")
|
ip.Server.Log().WithField("error", err).Warn("failed to delete server install container")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,14 +219,14 @@ func (ip *InstallationProcess) Run() error {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
if err := ip.BeforeExecute(); err != nil {
|
if err := ip.BeforeExecute(); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cid, err := ip.Execute()
|
cid, err := ip.Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ip.RemoveContainer()
|
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
|
// 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
|
// Make sure the temp directory root exists before trying to make a directory within it. The
|
||||||
// ioutil.TempDir call expects this base to exist, it won't create it for you.
|
// ioutil.TempDir call expects this base to exist, it won't create it for you.
|
||||||
if err := os.MkdirAll(ip.tempDir(), 0700); err != nil {
|
if err := os.MkdirAll(ip.tempDir(), 0700); err != nil {
|
||||||
return errors.Wrap(err, "could not create temporary directory for install process")
|
return errors.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)
|
f, err := os.OpenFile(filepath.Join(ip.tempDir(), "install.sh"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to write server installation script to disk before mount")
|
return errors.WithMessage(err, "failed to write server installation script to disk before mount")
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
@@ -265,7 +266,7 @@ func (ip *InstallationProcess) writeScriptToDisk() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Flush()
|
w.Flush()
|
||||||
@@ -275,11 +276,62 @@ func (ip *InstallationProcess) writeScriptToDisk() error {
|
|||||||
|
|
||||||
// Pulls the docker image to be used for the installation container.
|
// Pulls the docker image to be used for the installation container.
|
||||||
func (ip *InstallationProcess) pullInstallationImage() error {
|
func (ip *InstallationProcess) pullInstallationImage() error {
|
||||||
r, err := ip.client.ImagePull(ip.context, ip.Script.ContainerImage, types.ImagePullOptions{})
|
// Get a registry auth configuration from the config.
|
||||||
if err != nil {
|
var registryAuth *config.RegistryConfiguration
|
||||||
return errors.WithStack(err)
|
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.
|
// Block continuation until the image has been pulled successfully.
|
||||||
scanner := bufio.NewScanner(r)
|
scanner := bufio.NewScanner(r)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
@@ -287,7 +339,7 @@ func (ip *InstallationProcess) pullInstallationImage() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -298,11 +350,11 @@ func (ip *InstallationProcess) pullInstallationImage() error {
|
|||||||
// manner, if either one fails the error is returned.
|
// manner, if either one fails the error is returned.
|
||||||
func (ip *InstallationProcess) BeforeExecute() error {
|
func (ip *InstallationProcess) BeforeExecute() error {
|
||||||
if err := ip.writeScriptToDisk(); err != nil {
|
if err := ip.writeScriptToDisk(); err != nil {
|
||||||
return errors.Wrap(err, "failed to write installation script to disk")
|
return errors.WithMessage(err, "failed to write installation script to disk")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ip.pullInstallationImage(); err != nil {
|
if err := ip.pullInstallationImage(); err != nil {
|
||||||
return errors.Wrap(err, "failed to pull updated installation container image for server")
|
return errors.WithMessage(err, "failed to pull updated installation container image for server")
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := types.ContainerRemoveOptions{
|
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 err := ip.client.ContainerRemove(ip.context, ip.Server.Id()+"_installer", opts); err != nil {
|
||||||
if !client.IsErrNotFound(err) {
|
if !client.IsErrNotFound(err) {
|
||||||
return errors.Wrap(err, "failed to remove existing install container for server")
|
return errors.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) {
|
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)
|
f, err := os.OpenFile(ip.GetLogPath(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
@@ -372,15 +424,15 @@ func (ip *InstallationProcess) AfterExecute(containerId string) error {
|
|||||||
| ------------------------------
|
| ------------------------------
|
||||||
`)
|
`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tmpl.Execute(f, ip); err != nil {
|
if err := tmpl.Execute(f, ip); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := io.Copy(f, reader); err != nil {
|
if _, err := io.Copy(f, reader); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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")
|
r, err := ip.client.ContainerCreate(ip.context, conf, hostConf, nil, ip.Server.Id()+"_installer")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.WithStack(err)
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
ip.Server.Log().WithField("container_id", r.ID).Info("running installation script for server in container")
|
ip.Server.Log().WithField("container_id", r.ID).Info("running installation script for server in container")
|
||||||
@@ -468,7 +520,7 @@ func (ip *InstallationProcess) Execute() (string, error) {
|
|||||||
select {
|
select {
|
||||||
case err := <-eChan:
|
case err := <-eChan:
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.WithStack(err)
|
return "", err
|
||||||
}
|
}
|
||||||
case <-sChan:
|
case <-sChan:
|
||||||
}
|
}
|
||||||
@@ -487,7 +539,7 @@ func (ip *InstallationProcess) StreamOutput(id string) error {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer reader.Close()
|
defer reader.Close()
|
||||||
@@ -500,7 +552,7 @@ func (ip *InstallationProcess) StreamOutput(id string) error {
|
|||||||
if err := s.Err(); err != nil {
|
if err := s.Err(); err != nil {
|
||||||
ip.Server.Log().WithFields(log.Fields{
|
ip.Server.Log().WithFields(log.Fields{
|
||||||
"container_id": id,
|
"container_id": id,
|
||||||
"error": errors.WithStack(err),
|
"error": err,
|
||||||
}).Warn("error processing scanner line in installation output for server")
|
}).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
|
// value of "true" means everything was successful, "false" means something went
|
||||||
// wrong and the server must be deleted and re-created.
|
// wrong and the server must be deleted and re-created.
|
||||||
func (s *Server) SyncInstallState(successful bool) error {
|
func (s *Server) SyncInstallState(successful bool) error {
|
||||||
r := api.NewRequester()
|
err := api.New().SendInstallationStatus(s.Id(), successful)
|
||||||
|
if err != nil {
|
||||||
rerr, err := r.SendInstallationStatus(s.Id(), successful)
|
if !api.IsRequestError(err) {
|
||||||
if rerr != nil || err != nil {
|
return err
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.New(rerr.String())
|
return errors.New(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ package server
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/pterodactyl/wings/api"
|
"github.com/pterodactyl/wings/api"
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"github.com/pterodactyl/wings/environment"
|
"github.com/pterodactyl/wings/environment"
|
||||||
"github.com/pterodactyl/wings/events"
|
"github.com/pterodactyl/wings/events"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
var dockerEvents = []string{
|
var dockerEvents = []string{
|
||||||
@@ -18,6 +18,37 @@ var dockerEvents = []string{
|
|||||||
environment.DockerImagePullCompleted,
|
environment.DockerImagePullCompleted,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type diskSpaceLimiter struct {
|
||||||
|
o sync.Once
|
||||||
|
mu sync.Mutex
|
||||||
|
server *Server
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDiskLimiter(s *Server) *diskSpaceLimiter {
|
||||||
|
return &diskSpaceLimiter{server: s}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the disk space limiter status.
|
||||||
|
func (dsl *diskSpaceLimiter) Reset() {
|
||||||
|
dsl.mu.Lock()
|
||||||
|
dsl.o = sync.Once{}
|
||||||
|
dsl.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger the disk space limiter which will attempt to stop a running server instance within
|
||||||
|
// 15 seconds, and terminate it forcefully if it does not stop.
|
||||||
|
//
|
||||||
|
// This function is only executed one time, so whenever a server is marked as booting the limiter
|
||||||
|
// should be reset so it can properly be triggered as needed.
|
||||||
|
func (dsl *diskSpaceLimiter) Trigger() {
|
||||||
|
dsl.o.Do(func() {
|
||||||
|
dsl.server.PublishConsoleOutputFromDaemon("Server is exceeding the assigned disk space limit, stopping process now.")
|
||||||
|
if err := dsl.server.Environment.WaitForStop(60, true); err != nil {
|
||||||
|
dsl.server.Log().WithField("error", err).Error("failed to stop server after exceeding space limit!")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Adds all of the internal event listeners we want to use for a server. These listeners can only be
|
// Adds all of the internal event listeners we want to use for a server. These listeners can only be
|
||||||
// removed by deleting the server as they should last for the duration of the process' lifetime.
|
// removed by deleting the server as they should last for the duration of the process' lifetime.
|
||||||
func (s *Server) StartEventListeners() {
|
func (s *Server) StartEventListeners() {
|
||||||
@@ -31,8 +62,8 @@ func (s *Server) StartEventListeners() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
// If the process is already stopping, just let it continue with that action rather than attempting
|
// If the process is already stopping, just let it continue with that action rather than attempting
|
||||||
// to terminate again.
|
// to terminate again.
|
||||||
if s.GetState() != environment.ProcessStoppingState {
|
if s.Environment.State() != environment.ProcessStoppingState {
|
||||||
s.SetState(environment.ProcessStoppingState)
|
s.Environment.SetState(environment.ProcessStoppingState)
|
||||||
go func() {
|
go func() {
|
||||||
s.Log().Warn("stopping server instance, violating throttle limits")
|
s.Log().Warn("stopping server instance, violating throttle limits")
|
||||||
s.PublishConsoleOutputFromDaemon("Your server is being stopped for outputting too much data in a short period of time.")
|
s.PublishConsoleOutputFromDaemon("Your server is being stopped for outputting too much data in a short period of time.")
|
||||||
@@ -41,11 +72,11 @@ func (s *Server) StartEventListeners() {
|
|||||||
if err := s.Environment.WaitForStop(config.Get().Throttles.StopGracePeriod, true); err != nil {
|
if err := s.Environment.WaitForStop(config.Get().Throttles.StopGracePeriod, true); err != nil {
|
||||||
// If there is an error set the process back to running so that this throttler is called
|
// If there is an error set the process back to running so that this throttler is called
|
||||||
// again and hopefully kills the server.
|
// again and hopefully kills the server.
|
||||||
if s.GetState() != environment.ProcessOfflineState {
|
if s.Environment.State() != environment.ProcessOfflineState {
|
||||||
s.SetState(environment.ProcessRunningState)
|
s.Environment.SetState(environment.ProcessRunningState)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Log().WithField("error", errors.WithStack(err)).Error("failed to terminate environment after triggering throttle")
|
s.Log().WithField("error", err).Error("failed to terminate environment after triggering throttle")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -60,19 +91,21 @@ func (s *Server) StartEventListeners() {
|
|||||||
s.onConsoleOutput(e.Data)
|
s.onConsoleOutput(e.Data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
l := newDiskLimiter(s)
|
||||||
state := func(e events.Event) {
|
state := func(e events.Event) {
|
||||||
// Reset the throttler when the process is started.
|
// Reset the throttler when the process is started.
|
||||||
if e.Data == environment.ProcessStartingState {
|
if e.Data == environment.ProcessStartingState {
|
||||||
|
l.Reset()
|
||||||
s.Throttler().Reset()
|
s.Throttler().Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
s.SetState(e.Data)
|
s.OnStateChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
stats := func(e events.Event) {
|
stats := func(e events.Event) {
|
||||||
st := new(environment.Stats)
|
st := new(environment.Stats)
|
||||||
if err := json.Unmarshal([]byte(e.Data), st); err != nil {
|
if err := json.Unmarshal([]byte(e.Data), st); err != nil {
|
||||||
s.Log().WithField("error", errors.WithStack(err)).Warn("failed to unmarshal server environment stats")
|
s.Log().WithField("error", err).Warn("failed to unmarshal server environment stats")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +114,11 @@ func (s *Server) StartEventListeners() {
|
|||||||
s.resources.Stats = *st
|
s.resources.Stats = *st
|
||||||
s.resources.mu.Unlock()
|
s.resources.mu.Unlock()
|
||||||
|
|
||||||
s.Filesystem().HasSpaceAvailable(true)
|
// If there is no disk space available at this point, trigger the server disk limiter logic
|
||||||
|
// which will start to stop the running instance.
|
||||||
|
if !s.Filesystem().HasSpaceAvailable(true) {
|
||||||
|
l.Trigger()
|
||||||
|
}
|
||||||
|
|
||||||
s.emitProcUsage()
|
s.emitProcUsage()
|
||||||
}
|
}
|
||||||
@@ -114,7 +151,7 @@ func (s *Server) onConsoleOutput(data string) {
|
|||||||
processConfiguration := s.ProcessConfiguration()
|
processConfiguration := s.ProcessConfiguration()
|
||||||
|
|
||||||
// Check if the server is currently starting.
|
// 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.
|
// Check if we should strip ansi color codes.
|
||||||
if processConfiguration.Startup.StripAnsi {
|
if processConfiguration.Startup.StripAnsi {
|
||||||
// Strip ansi color codes from the data string.
|
// 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,
|
// If the specific line of output is one that would mark the server as started,
|
||||||
// set the server to that state. Only do this if the server is not currently stopped
|
// set the server to that state. Only do this if the server is not currently stopped
|
||||||
// or stopping.
|
// or stopping.
|
||||||
_ = s.SetState(environment.ProcessRunningState)
|
s.Environment.SetState(environment.ProcessRunningState)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,7 +184,7 @@ func (s *Server) onConsoleOutput(data string) {
|
|||||||
stop := processConfiguration.Stop
|
stop := processConfiguration.Stop
|
||||||
|
|
||||||
if stop.Type == api.ProcessStopCommand && data == stop.Value {
|
if stop.Type == api.ProcessStopCommand && data == stop.Value {
|
||||||
_ = s.SetState(environment.ProcessOfflineState)
|
s.Environment.SetState(environment.ProcessOfflineState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,38 +32,40 @@ func LoadDirectory() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Info("fetching list of servers from API")
|
log.Info("fetching list of servers from API")
|
||||||
configs, rerr, err := api.NewRequester().GetAllServerConfigurations()
|
configs, err := api.New().GetServers()
|
||||||
if err != nil || rerr != nil {
|
if err != nil {
|
||||||
if err != nil {
|
if !api.IsRequestError(err) {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.New(rerr.String())
|
return errors.New(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
log.WithField("total_configs", len(configs)).Info("processing servers returned by the API")
|
log.WithField("total_configs", len(configs)).Info("processing servers returned by the API")
|
||||||
|
|
||||||
pool := workerpool.New(runtime.NumCPU())
|
pool := workerpool.New(runtime.NumCPU())
|
||||||
for uuid, data := range configs {
|
log.Debugf("using %d workerpools to instantiate server instances", runtime.NumCPU())
|
||||||
uuid := uuid
|
for _, data := range configs {
|
||||||
data := data
|
data := data
|
||||||
|
|
||||||
pool.Submit(func() {
|
pool.Submit(func() {
|
||||||
// Parse the json.RawMessage into an expected struct value. We do this here so that a single broken
|
// 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
|
// server does not cause the entire boot process to hang, and allows us to show more useful error
|
||||||
// messaging in the output.
|
// messaging in the output.
|
||||||
d := api.ServerConfigurationResponse{}
|
d := api.ServerConfigurationResponse{
|
||||||
|
Settings: data.Settings,
|
||||||
|
}
|
||||||
|
|
||||||
log.WithField("server", uuid).Info("creating new server object from API response")
|
log.WithField("server", data.Uuid).Info("creating new server object from API response")
|
||||||
if err := json.Unmarshal(data, &d); err != nil {
|
if err := json.Unmarshal(data.ProcessConfiguration, &d.ProcessConfiguration); err != nil {
|
||||||
log.WithField("server", uuid).WithField("error", err).Error("failed to parse server configuration from API response, skipping...")
|
log.WithField("server", data.Uuid).WithField("error", err).Error("failed to parse server configuration from API response, skipping...")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err := FromConfiguration(d)
|
s, err := FromConfiguration(d)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithField("server", uuid).WithField("error", err).Error("failed to load server, skipping...")
|
log.WithField("server", data.Uuid).WithField("error", err).Error("failed to load server, skipping...")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,12 +89,12 @@ func LoadDirectory() error {
|
|||||||
func FromConfiguration(data api.ServerConfigurationResponse) (*Server, error) {
|
func FromConfiguration(data api.ServerConfigurationResponse) (*Server, error) {
|
||||||
cfg := Configuration{}
|
cfg := Configuration{}
|
||||||
if err := defaults.Set(&cfg); err != nil {
|
if err := defaults.Set(&cfg); err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to set struct defaults for server configuration")
|
return nil, errors.WithMessage(err, "failed to set struct defaults for server configuration")
|
||||||
}
|
}
|
||||||
|
|
||||||
s := new(Server)
|
s := new(Server)
|
||||||
if err := defaults.Set(s); err != nil {
|
if err := defaults.Set(s); err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to set struct defaults for server")
|
return nil, errors.WithMessage(err, "failed to set struct defaults for server")
|
||||||
}
|
}
|
||||||
|
|
||||||
s.cfg = cfg
|
s.cfg = cfg
|
||||||
@@ -102,6 +104,7 @@ func FromConfiguration(data api.ServerConfigurationResponse) (*Server, error) {
|
|||||||
|
|
||||||
s.resources = ResourceUsage{}
|
s.resources = ResourceUsage{}
|
||||||
defaults.Set(&s.resources)
|
defaults.Set(&s.resources)
|
||||||
|
s.resources.State.Store(environment.ProcessOfflineState)
|
||||||
|
|
||||||
s.Archiver = Archiver{Server: s}
|
s.Archiver = Archiver{Server: s}
|
||||||
s.fs = filesystem.New(filepath.Join(config.Get().System.Data, s.Id()), s.DiskSpace())
|
s.fs = filesystem.New(filepath.Join(config.Get().System.Data, s.Id()), s.DiskSpace())
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ func (s *Server) customMounts() []environment.Mount {
|
|||||||
|
|
||||||
mounted := false
|
mounted := false
|
||||||
for _, allowed := range config.Get().AllowedMounts {
|
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
|
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
|
// time than that passes an error will be propagated back up the chain and this
|
||||||
// request will be aborted.
|
// request will be aborted.
|
||||||
if err := s.powerLock.Acquire(ctx, 1); err != nil {
|
if err := s.powerLock.Acquire(ctx, 1); err != nil {
|
||||||
return errors.Wrap(err, "could not acquire lock on power state")
|
return errors.WithMessage(err, "could not acquire lock on power state")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If no wait duration was provided we will attempt to immediately acquire the lock
|
// If no wait duration was provided we will attempt to immediately acquire the lock
|
||||||
// and bail out with a context deadline error if it is not acquired immediately.
|
// and bail out with a context deadline error if it is not acquired immediately.
|
||||||
if ok := s.powerLock.TryAcquire(1); !ok {
|
if ok := s.powerLock.TryAcquire(1); !ok {
|
||||||
return errors.Wrap(context.DeadlineExceeded, "could not acquire lock on power state")
|
return errors.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 {
|
switch action {
|
||||||
case PowerActionStart:
|
case PowerActionStart:
|
||||||
if s.GetState() != environment.ProcessOfflineState {
|
if s.Environment.State() != environment.ProcessOfflineState {
|
||||||
return ErrIsRunning
|
return ErrIsRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ func (s *Server) HandlePowerAction(action PowerAction, waitSeconds ...int) error
|
|||||||
func (s *Server) onBeforeStart() error {
|
func (s *Server) onBeforeStart() error {
|
||||||
s.Log().Info("syncing server configuration with panel")
|
s.Log().Info("syncing server configuration with panel")
|
||||||
if err := s.Sync(); err != nil {
|
if err := s.Sync(); err != nil {
|
||||||
return errors.Wrap(err, "unable to sync server data from Panel instance")
|
return errors.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
|
// Disallow start & restart if the server is suspended. Do this check after performing a sync
|
||||||
@@ -185,7 +185,7 @@ func (s *Server) onBeforeStart() error {
|
|||||||
s.PublishConsoleOutputFromDaemon("Ensuring file permissions are set correctly, this could take a few seconds...")
|
s.PublishConsoleOutputFromDaemon("Ensuring file permissions are set correctly, this could take a few seconds...")
|
||||||
// Ensure all of the server file permissions are set correctly before booting the process.
|
// Ensure all of the server file permissions are set correctly before booting the process.
|
||||||
if err := s.Filesystem().Chown("/"); err != nil {
|
if err := s.Filesystem().Chown("/"); err != nil {
|
||||||
return errors.Wrap(err, "failed to chown root server directory during pre-boot process")
|
return errors.WithMessage(err, "failed to chown root server directory during pre-boot process")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package server
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/pterodactyl/wings/environment"
|
"github.com/pterodactyl/wings/environment"
|
||||||
|
"github.com/pterodactyl/wings/system"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Defines the current resource usage for a given server instance. If a server is offline you
|
// Defines the current resource usage for a given server instance. If a server is offline you
|
||||||
@@ -16,7 +18,7 @@ type ResourceUsage struct {
|
|||||||
environment.Stats
|
environment.Stats
|
||||||
|
|
||||||
// The current server status.
|
// The current server status.
|
||||||
State string `json:"state" default:"offline"`
|
State *system.AtomicString `json:"state" default:"{}"`
|
||||||
|
|
||||||
// The current disk space being used by the server. This value is not guaranteed to be accurate
|
// The current disk space being used by the server. This value is not guaranteed to be accurate
|
||||||
// at all times. It is "manually" set whenever server.Proc() is called. This is kind of just a
|
// at all times. It is "manually" set whenever server.Proc() is called. This is kind of just a
|
||||||
@@ -24,16 +26,16 @@ type ResourceUsage struct {
|
|||||||
Disk int64 `json:"disk_bytes"`
|
Disk int64 `json:"disk_bytes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alias the resource usage so that we don't infinitely recurse when marshaling the struct.
|
|
||||||
type IResourceUsage ResourceUsage
|
|
||||||
|
|
||||||
// Custom marshaler to ensure that the object is locked when we're converting it to JSON in
|
// Custom marshaler to ensure that the object is locked when we're converting it to JSON in
|
||||||
// order to avoid race conditions.
|
// order to avoid race conditions.
|
||||||
func (ru *ResourceUsage) MarshalJSON() ([]byte, error) {
|
func (ru *ResourceUsage) MarshalJSON() ([]byte, error) {
|
||||||
ru.mu.Lock()
|
ru.mu.Lock()
|
||||||
defer ru.mu.Unlock()
|
defer ru.mu.Unlock()
|
||||||
|
|
||||||
return json.Marshal(IResourceUsage(*ru))
|
// Alias the resource usage so that we don't infinitely recurse when marshaling the struct.
|
||||||
|
type alias ResourceUsage
|
||||||
|
|
||||||
|
return json.Marshal(alias(*ru))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the resource usage stats for the server instance. If the server is not running, only the
|
// Returns the resource usage stats for the server instance. If the server is not running, only the
|
||||||
@@ -42,10 +44,10 @@ func (ru *ResourceUsage) MarshalJSON() ([]byte, error) {
|
|||||||
//
|
//
|
||||||
// When a process is stopped all of the stats are zeroed out except for the disk.
|
// When a process is stopped all of the stats are zeroed out except for the disk.
|
||||||
func (s *Server) Proc() *ResourceUsage {
|
func (s *Server) Proc() *ResourceUsage {
|
||||||
s.resources.SetDisk(s.Filesystem().CachedUsage())
|
// Store the updated disk usage when requesting process usage.
|
||||||
|
atomic.StoreInt64(&s.resources.Disk, s.Filesystem().CachedUsage())
|
||||||
|
|
||||||
// Get a read lock on the resources at this point. Don't do this before setting
|
// Acquire a lock before attempting to return the value of resources.
|
||||||
// the disk, otherwise you'll cause a deadlock.
|
|
||||||
s.resources.mu.RLock()
|
s.resources.mu.RLock()
|
||||||
defer s.resources.mu.RUnlock()
|
defer s.resources.mu.RUnlock()
|
||||||
|
|
||||||
@@ -57,24 +59,3 @@ func (s *Server) emitProcUsage() {
|
|||||||
s.Log().WithField("error", err).Warn("error while emitting server resource usage to listeners")
|
s.Log().WithField("error", err).Warn("error while emitting server resource usage to listeners")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the servers current state.
|
|
||||||
func (ru *ResourceUsage) getInternalState() string {
|
|
||||||
ru.mu.RLock()
|
|
||||||
defer ru.mu.RUnlock()
|
|
||||||
|
|
||||||
return ru.State
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sets the new state for the server.
|
|
||||||
func (ru *ResourceUsage) setInternalState(state string) {
|
|
||||||
ru.mu.Lock()
|
|
||||||
ru.State = state
|
|
||||||
ru.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ru *ResourceUsage) SetDisk(i int64) {
|
|
||||||
ru.mu.Lock()
|
|
||||||
ru.Disk = i
|
|
||||||
ru.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -112,17 +112,17 @@ func (s *Server) Log() *log.Entry {
|
|||||||
// This also means mass actions can be performed against servers on the Panel and they
|
// This also means mass actions can be performed against servers on the Panel and they
|
||||||
// will automatically sync with Wings when the server is started.
|
// will automatically sync with Wings when the server is started.
|
||||||
func (s *Server) Sync() error {
|
func (s *Server) Sync() error {
|
||||||
cfg, rerr, err := s.GetProcessConfiguration()
|
cfg, err := api.New().GetServerConfiguration(s.Id())
|
||||||
if err != nil || rerr != nil {
|
if err != nil {
|
||||||
if err != nil {
|
if !api.IsRequestError(err) {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if rerr.Status == "404" {
|
if err.(*api.RequestError).Status == "404" {
|
||||||
return &serverDoesNotExist{}
|
return &serverDoesNotExist{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.New(rerr.String())
|
return errors.New(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.SyncWithConfiguration(cfg)
|
return s.SyncWithConfiguration(cfg)
|
||||||
@@ -131,7 +131,7 @@ func (s *Server) Sync() error {
|
|||||||
func (s *Server) SyncWithConfiguration(cfg api.ServerConfigurationResponse) error {
|
func (s *Server) SyncWithConfiguration(cfg api.ServerConfigurationResponse) error {
|
||||||
// Update the data structure and persist it to the disk.
|
// Update the data structure and persist it to the disk.
|
||||||
if err := s.UpdateDataStructure(cfg.Settings); err != nil {
|
if err := s.UpdateDataStructure(cfg.Settings); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Lock()
|
s.Lock()
|
||||||
@@ -171,17 +171,12 @@ func (s *Server) IsBootable() bool {
|
|||||||
func (s *Server) CreateEnvironment() error {
|
func (s *Server) CreateEnvironment() error {
|
||||||
// Ensure the data directory exists before getting too far through this process.
|
// Ensure the data directory exists before getting too far through this process.
|
||||||
if err := s.EnsureDataDirectoryExists(); err != nil {
|
if err := s.EnsureDataDirectoryExists(); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.Environment.Create()
|
return s.Environment.Create()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets the process configuration data for the server.
|
|
||||||
func (s *Server) GetProcessConfiguration() (api.ServerConfigurationResponse, *api.RequestError, error) {
|
|
||||||
return api.NewRequester().GetServerConfiguration(s.Id())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if the server is marked as being suspended or not on the system.
|
// Checks if the server is marked as being suspended or not on the system.
|
||||||
func (s *Server) IsSuspended() bool {
|
func (s *Server) IsSuspended() bool {
|
||||||
return s.Config().Suspended
|
return s.Config().Suspended
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"github.com/pterodactyl/wings/environment"
|
"github.com/pterodactyl/wings/environment"
|
||||||
"io"
|
"io"
|
||||||
@@ -23,14 +21,14 @@ func CachedServerStates() (map[string]string, error) {
|
|||||||
// Open the states file.
|
// Open the states file.
|
||||||
f, err := os.OpenFile(config.Get().System.GetStatesPath(), os.O_RDONLY|os.O_CREATE, 0644)
|
f, err := os.OpenFile(config.Get().System.GetStatesPath(), os.O_RDONLY|os.O_CREATE, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
// Convert the json object to a map.
|
// Convert the json object to a map.
|
||||||
states := map[string]string{}
|
states := map[string]string{}
|
||||||
if err := json.NewDecoder(f).Decode(&states); err != nil && err != io.EOF {
|
if err := json.NewDecoder(f).Decode(&states); err != nil && err != io.EOF {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return states, nil
|
return states, nil
|
||||||
@@ -41,13 +39,13 @@ func saveServerStates() error {
|
|||||||
// Get the states of all servers on the daemon.
|
// Get the states of all servers on the daemon.
|
||||||
states := map[string]string{}
|
states := map[string]string{}
|
||||||
for _, s := range GetServers().All() {
|
for _, s := range GetServers().All() {
|
||||||
states[s.Id()] = s.GetState()
|
states[s.Id()] = s.Environment.State()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the map to a json object.
|
// Convert the map to a json object.
|
||||||
data, err := json.Marshal(states)
|
data, err := json.Marshal(states)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
stateMutex.Lock()
|
stateMutex.Lock()
|
||||||
@@ -55,7 +53,7 @@ func saveServerStates() error {
|
|||||||
|
|
||||||
// Write the data to the file
|
// Write the data to the file
|
||||||
if err := ioutil.WriteFile(config.Get().System.GetStatesPath(), data, 0644); err != nil {
|
if err := ioutil.WriteFile(config.Get().System.GetStatesPath(), data, 0644); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -63,23 +61,17 @@ func saveServerStates() error {
|
|||||||
|
|
||||||
// Sets the state of the server internally. This function handles crash detection as
|
// Sets the state of the server internally. This function handles crash detection as
|
||||||
// well as reporting to event listeners for the server.
|
// well as reporting to event listeners for the server.
|
||||||
func (s *Server) SetState(state string) error {
|
func (s *Server) OnStateChange() {
|
||||||
if state != environment.ProcessOfflineState &&
|
prevState := s.Proc().State.Load()
|
||||||
state != environment.ProcessStartingState &&
|
|
||||||
state != environment.ProcessRunningState &&
|
|
||||||
state != environment.ProcessStoppingState {
|
|
||||||
return errors.New(fmt.Sprintf("invalid server state received: %s", state))
|
|
||||||
}
|
|
||||||
|
|
||||||
prevState := s.GetState()
|
|
||||||
|
|
||||||
|
st := s.Environment.State()
|
||||||
// Update the currently tracked state for the server.
|
// Update the currently tracked state for the server.
|
||||||
s.Proc().setInternalState(state)
|
s.Proc().State.Store(st)
|
||||||
|
|
||||||
// Emit the event to any listeners that are currently registered.
|
// Emit the event to any listeners that are currently registered.
|
||||||
if prevState != state {
|
if prevState != s.Environment.State() {
|
||||||
s.Log().WithField("status", s.Proc().getInternalState()).Debug("saw server status change event")
|
s.Log().WithField("status", st).Debug("saw server status change event")
|
||||||
s.Events().Publish(StatusEvent, s.Proc().getInternalState())
|
s.Events().Publish(StatusEvent, st)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist this change to the disk immediately so that should the Daemon be stopped or
|
// Persist this change to the disk immediately so that should the Daemon be stopped or
|
||||||
@@ -98,7 +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
|
// Reset the resource usage to 0 when the process fully stops so that all of the UI
|
||||||
// views in the Panel correctly display 0.
|
// views in the Panel correctly display 0.
|
||||||
if state == environment.ProcessOfflineState {
|
if st == environment.ProcessOfflineState {
|
||||||
s.resources.mu.Lock()
|
s.resources.mu.Lock()
|
||||||
s.resources.Empty()
|
s.resources.Empty()
|
||||||
s.resources.mu.Unlock()
|
s.resources.mu.Unlock()
|
||||||
@@ -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
|
// 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
|
// separate thread as to not block any actions currently taking place in the flow
|
||||||
// that called this function.
|
// 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")
|
s.Log().Info("detected server as entering a crashed state; running crash handler")
|
||||||
|
|
||||||
go func(server *Server) {
|
go func(server *Server) {
|
||||||
@@ -122,25 +114,26 @@ func (s *Server) SetState(state string) error {
|
|||||||
if IsTooFrequentCrashError(err) {
|
if IsTooFrequentCrashError(err) {
|
||||||
server.Log().Info("did not restart server after crash; occurred too soon after the last")
|
server.Log().Info("did not restart server after crash; occurred too soon after the last")
|
||||||
} else {
|
} 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")
|
server.Log().WithField("error", err).Error("failed to handle server crash")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}(s)
|
}(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the current state of the server in a race-safe manner.
|
// Returns the current state of the server in a race-safe manner.
|
||||||
|
// Deprecated
|
||||||
|
// use Environment.State()
|
||||||
func (s *Server) GetState() string {
|
func (s *Server) GetState() string {
|
||||||
return s.Proc().getInternalState()
|
return s.Environment.State()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determines if the server state is running or not. This is different than the
|
// Determines if the server state is running or not. This is different than the
|
||||||
// environment state, it is simply the tracked state from this daemon instance, and
|
// environment state, it is simply the tracked state from this daemon instance, and
|
||||||
// not the response from Docker.
|
// not the response from Docker.
|
||||||
func (s *Server) IsRunning() bool {
|
func (s *Server) IsRunning() bool {
|
||||||
st := s.GetState()
|
st := s.Environment.State()
|
||||||
|
|
||||||
return st == environment.ProcessRunningState || st == environment.ProcessStartingState
|
return st == environment.ProcessRunningState || st == environment.ProcessStartingState
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
func (s *Server) UpdateDataStructure(data []byte) error {
|
func (s *Server) UpdateDataStructure(data []byte) error {
|
||||||
src := new(Configuration)
|
src := new(Configuration)
|
||||||
if err := json.Unmarshal(data, src); err != nil {
|
if err := json.Unmarshal(data, src); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't allow obviously corrupted data to pass through into this function. If the UUID
|
// Don't allow obviously corrupted data to pass through into this function. If the UUID
|
||||||
@@ -47,7 +47,7 @@ func (s *Server) UpdateDataStructure(data []byte) error {
|
|||||||
// Merge the new data object that we have received with the existing server data object
|
// Merge the new data object that we have received with the existing server data object
|
||||||
// and then save it to the disk so it is persistent.
|
// and then save it to the disk so it is persistent.
|
||||||
if err := mergo.Merge(&c, src, mergo.WithOverride); err != nil {
|
if err := mergo.Merge(&c, src, mergo.WithOverride); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't explode if we're setting CPU limits to 0. Mergo sees that as an empty value
|
// Don't explode if we're setting CPU limits to 0. Mergo sees that as an empty value
|
||||||
@@ -65,7 +65,7 @@ func (s *Server) UpdateDataStructure(data []byte) error {
|
|||||||
// request is going to be boolean. Allegedly.
|
// request is going to be boolean. Allegedly.
|
||||||
if v, err := jsonparser.GetBoolean(data, "container", "oom_disabled"); err != nil {
|
if v, err := jsonparser.GetBoolean(data, "container", "oom_disabled"); err != nil {
|
||||||
if err != jsonparser.KeyPathNotFoundError {
|
if err != jsonparser.KeyPathNotFoundError {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
c.Build.OOMDisabled = v
|
c.Build.OOMDisabled = v
|
||||||
@@ -74,7 +74,7 @@ func (s *Server) UpdateDataStructure(data []byte) error {
|
|||||||
// Mergo also cannot handle this boolean value.
|
// Mergo also cannot handle this boolean value.
|
||||||
if v, err := jsonparser.GetBoolean(data, "suspended"); err != nil {
|
if v, err := jsonparser.GetBoolean(data, "suspended"); err != nil {
|
||||||
if err != jsonparser.KeyPathNotFoundError {
|
if err != jsonparser.KeyPathNotFoundError {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
c.Suspended = v
|
c.Suspended = v
|
||||||
@@ -82,7 +82,7 @@ func (s *Server) UpdateDataStructure(data []byte) error {
|
|||||||
|
|
||||||
if v, err := jsonparser.GetBoolean(data, "skip_egg_scripts"); err != nil {
|
if v, err := jsonparser.GetBoolean(data, "skip_egg_scripts"); err != nil {
|
||||||
if err != jsonparser.KeyPathNotFoundError {
|
if err != jsonparser.KeyPathNotFoundError {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
c.SkipEggScripts = v
|
c.SkipEggScripts = v
|
||||||
@@ -143,7 +143,7 @@ func (s *Server) SyncWithEnvironment() {
|
|||||||
} else {
|
} else {
|
||||||
// Checks if the server is now in a suspended state. If so and a server process is currently running it
|
// 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).
|
// 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")
|
s.Log().Info("server suspended with running process state, terminating now")
|
||||||
|
|
||||||
go func(s *Server) {
|
go func(s *Server) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type WebsocketBag struct {
|
type WebsocketBag struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
conns map[uuid.UUID]*context.CancelFunc
|
conns map[uuid.UUID]*context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,4 +58,4 @@ func (w *WebsocketBag) CancelAll() {
|
|||||||
|
|
||||||
// Reset the connections.
|
// Reset the connections.
|
||||||
w.conns = make(map[uuid.UUID]*context.CancelFunc)
|
w.conns = make(map[uuid.UUID]*context.CancelFunc)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package sftp
|
|||||||
import (
|
import (
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/patrickmn/go-cache"
|
"github.com/patrickmn/go-cache"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/pkg/sftp"
|
"github.com/pkg/sftp"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@@ -58,14 +57,14 @@ func (fs FileSystem) Fileread(request *sftp.Request) (io.ReaderAt, error) {
|
|||||||
if _, err := os.Stat(p); os.IsNotExist(err) {
|
if _, err := os.Stat(p); os.IsNotExist(err) {
|
||||||
return nil, sftp.ErrSshFxNoSuchFile
|
return nil, sftp.ErrSshFxNoSuchFile
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
fs.logger.WithField("error", errors.WithStack(err)).Error("error while processing file stat")
|
fs.logger.WithField("error", err).Error("error while processing file stat")
|
||||||
|
|
||||||
return nil, sftp.ErrSshFxFailure
|
return nil, sftp.ErrSshFxFailure
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Open(p)
|
file, err := os.Open(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.logger.WithField("source", p).WithField("error", errors.WithStack(err)).Error("could not open file for reading")
|
fs.logger.WithField("source", p).WithField("error", err).Error("could not open file for reading")
|
||||||
return nil, sftp.ErrSshFxFailure
|
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 {
|
if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
|
||||||
l.WithFields(log.Fields{
|
l.WithFields(log.Fields{
|
||||||
"path": filepath.Dir(p),
|
"path": filepath.Dir(p),
|
||||||
"error": errors.WithStack(err),
|
"error": err,
|
||||||
}).Error("error making path for file")
|
}).Error("error making path for file")
|
||||||
|
|
||||||
return nil, sftp.ErrSshFxFailure
|
return nil, sftp.ErrSshFxFailure
|
||||||
@@ -116,7 +115,7 @@ func (fs FileSystem) Filewrite(request *sftp.Request) (io.WriterAt, error) {
|
|||||||
|
|
||||||
file, err := os.Create(p)
|
file, err := os.Create(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to create file")
|
l.WithField("error", err).Error("failed to create file")
|
||||||
|
|
||||||
return nil, sftp.ErrSshFxFailure
|
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
|
// Not failing here is intentional. We still made the file, it is just owned incorrectly
|
||||||
// and will likely cause some issues.
|
// and will likely cause some issues.
|
||||||
if err := os.Chown(p, fs.User.Uid, fs.User.Gid); err != nil {
|
if err := os.Chown(p, fs.User.Uid, fs.User.Gid); err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).Warn("failed to set permissions on file")
|
l.WithField("error", err).Warn("failed to set permissions on file")
|
||||||
}
|
}
|
||||||
|
|
||||||
return file, nil
|
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
|
// If the stat error isn't about the file not existing, there is some other issue
|
||||||
// at play and we need to go ahead and bail out of the process.
|
// at play and we need to go ahead and bail out of the process.
|
||||||
if statErr != nil {
|
if statErr != nil {
|
||||||
l.WithField("error", errors.WithStack(statErr)).Error("encountered error performing file stat")
|
l.WithField("error", statErr).Error("encountered error performing file stat")
|
||||||
|
|
||||||
return nil, sftp.ErrSshFxFailure
|
return nil, sftp.ErrSshFxFailure
|
||||||
}
|
}
|
||||||
@@ -159,14 +158,14 @@ func (fs FileSystem) Filewrite(request *sftp.Request) (io.WriterAt, error) {
|
|||||||
return nil, sftp.ErrSSHFxNoSuchFile
|
return nil, sftp.ErrSSHFxNoSuchFile
|
||||||
}
|
}
|
||||||
|
|
||||||
l.WithField("flags", request.Flags).WithField("error", errors.WithStack(err)).Error("failed to open existing file on system")
|
l.WithField("flags", request.Flags).WithField("error", err).Error("failed to open existing file on system")
|
||||||
return nil, sftp.ErrSshFxFailure
|
return nil, sftp.ErrSshFxFailure
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not failing here is intentional. We still made the file, it is just owned incorrectly
|
// Not failing here is intentional. We still made the file, it is just owned incorrectly
|
||||||
// and will likely cause some issues.
|
// and will likely cause some issues.
|
||||||
if err := os.Chown(p, fs.User.Uid, fs.User.Gid); err != nil {
|
if err := os.Chown(p, fs.User.Uid, fs.User.Gid); err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).Warn("error chowning file")
|
l.WithField("error", err).Warn("error chowning file")
|
||||||
}
|
}
|
||||||
|
|
||||||
return file, nil
|
return file, nil
|
||||||
@@ -220,7 +219,7 @@ func (fs FileSystem) Filecmd(request *sftp.Request) error {
|
|||||||
return sftp.ErrSSHFxNoSuchFile
|
return sftp.ErrSSHFxNoSuchFile
|
||||||
}
|
}
|
||||||
|
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to perform setstat on item")
|
l.WithField("error", err).Error("failed to perform setstat on item")
|
||||||
return sftp.ErrSSHFxFailure
|
return sftp.ErrSSHFxFailure
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -234,7 +233,7 @@ func (fs FileSystem) Filecmd(request *sftp.Request) error {
|
|||||||
return sftp.ErrSSHFxNoSuchFile
|
return sftp.ErrSSHFxNoSuchFile
|
||||||
}
|
}
|
||||||
|
|
||||||
l.WithField("target", target).WithField("error", errors.WithStack(err)).Error("failed to rename file")
|
l.WithField("target", target).WithField("error", err).Error("failed to rename file")
|
||||||
|
|
||||||
return sftp.ErrSshFxFailure
|
return sftp.ErrSshFxFailure
|
||||||
}
|
}
|
||||||
@@ -246,7 +245,7 @@ func (fs FileSystem) Filecmd(request *sftp.Request) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := os.RemoveAll(p); err != nil {
|
if err := os.RemoveAll(p); err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to remove directory")
|
l.WithField("error", err).Error("failed to remove directory")
|
||||||
|
|
||||||
return sftp.ErrSshFxFailure
|
return sftp.ErrSshFxFailure
|
||||||
}
|
}
|
||||||
@@ -258,7 +257,7 @@ func (fs FileSystem) Filecmd(request *sftp.Request) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(p, 0755); err != nil {
|
if err := os.MkdirAll(p, 0755); err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to create directory")
|
l.WithField("error", err).Error("failed to create directory")
|
||||||
|
|
||||||
return sftp.ErrSshFxFailure
|
return sftp.ErrSshFxFailure
|
||||||
}
|
}
|
||||||
@@ -270,7 +269,7 @@ func (fs FileSystem) Filecmd(request *sftp.Request) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := os.Symlink(p, target); err != nil {
|
if err := os.Symlink(p, target); err != nil {
|
||||||
l.WithField("target", target).WithField("error", errors.WithStack(err)).Error("failed to create symlink")
|
l.WithField("target", target).WithField("error", err).Error("failed to create symlink")
|
||||||
|
|
||||||
return sftp.ErrSshFxFailure
|
return sftp.ErrSshFxFailure
|
||||||
}
|
}
|
||||||
@@ -286,7 +285,7 @@ func (fs FileSystem) Filecmd(request *sftp.Request) error {
|
|||||||
return sftp.ErrSSHFxNoSuchFile
|
return sftp.ErrSSHFxNoSuchFile
|
||||||
}
|
}
|
||||||
|
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to remove a file")
|
l.WithField("error", err).Error("failed to remove a file")
|
||||||
|
|
||||||
return sftp.ErrSshFxFailure
|
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
|
// and will likely cause some issues. There is no logical check for if the file was removed
|
||||||
// because both of those cases (Rmdir, Remove) have an explicit return rather than break.
|
// because both of those cases (Rmdir, Remove) have an explicit return rather than break.
|
||||||
if err := os.Chown(fileLocation, fs.User.Uid, fs.User.Gid); err != nil {
|
if err := os.Chown(fileLocation, fs.User.Uid, fs.User.Gid); err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).Warn("error chowning file")
|
l.WithField("error", err).Warn("error chowning file")
|
||||||
}
|
}
|
||||||
|
|
||||||
return sftp.ErrSshFxOk
|
return sftp.ErrSshFxOk
|
||||||
@@ -327,7 +326,7 @@ func (fs FileSystem) Filelist(request *sftp.Request) (sftp.ListerAt, error) {
|
|||||||
|
|
||||||
files, err := ioutil.ReadDir(p)
|
files, err := ioutil.ReadDir(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.logger.WithField("error", errors.WithStack(err)).Error("error while listing directory")
|
fs.logger.WithField("error", err).Error("error while listing directory")
|
||||||
|
|
||||||
return nil, sftp.ErrSshFxFailure
|
return nil, sftp.ErrSshFxFailure
|
||||||
}
|
}
|
||||||
@@ -342,7 +341,7 @@ func (fs FileSystem) Filelist(request *sftp.Request) (sftp.ListerAt, error) {
|
|||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return nil, sftp.ErrSshFxNoSuchFile
|
return nil, sftp.ErrSshFxNoSuchFile
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
fs.logger.WithField("source", p).WithField("error", errors.WithStack(err)).Error("error performing stat on file")
|
fs.logger.WithField("source", p).WithField("error", err).Error("error performing stat on file")
|
||||||
|
|
||||||
return nil, sftp.ErrSshFxFailure
|
return nil, sftp.ErrSshFxFailure
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,14 +28,14 @@ func Initialize(config config.SystemConfiguration) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := New(s); err != nil {
|
if err := New(s); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the SFTP server in a background thread since this is
|
// Initialize the SFTP server in a background thread since this is
|
||||||
// a long running operation.
|
// a long running operation.
|
||||||
go func(s *Server) {
|
go func(s *Server) {
|
||||||
if err := s.Initialize(); err != nil {
|
if err := s.Initialize(); err != nil {
|
||||||
log.WithField("subsystem", "sftp").WithField("error", errors.WithStack(err)).Error("failed to initialize SFTP subsystem")
|
log.WithField("subsystem", "sftp").WithField("error", err).Error("failed to initialize SFTP subsystem")
|
||||||
}
|
}
|
||||||
}(s)
|
}(s)
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ func validateCredentials(c api.SftpAuthRequest) (*api.SftpAuthResponse, error) {
|
|||||||
f := log.Fields{"subsystem": "sftp", "username": c.User, "ip": c.IP}
|
f := log.Fields{"subsystem": "sftp", "username": c.User, "ip": c.IP}
|
||||||
|
|
||||||
log.WithFields(f).Debug("validating credentials for SFTP connection")
|
log.WithFields(f).Debug("validating credentials for SFTP connection")
|
||||||
resp, err := api.NewRequester().ValidateSftpCredentials(c)
|
resp, err := api.New().ValidateSftpCredentials(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if api.IsInvalidCredentialsError(err) {
|
if api.IsInvalidCredentialsError(err) {
|
||||||
log.WithFields(f).Warn("failed to validate user credentials (invalid username or password)")
|
log.WithFields(f).Warn("failed to validate user credentials (invalid username or password)")
|
||||||
|
|||||||
@@ -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