Compare commits

..

1 Commits

Author SHA1 Message Date
Dane Everitt
3f9edee212 Bump for release 2019-12-22 21:45:47 -08:00
120 changed files with 3851 additions and 13153 deletions

View File

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

View File

@@ -1,35 +0,0 @@
name: "Code scanning - action"
on:
push:
pull_request:
schedule:
- cron: '0 21 * * 6'
jobs:
CodeQL-Build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
# Override language selection by uncommenting this and choosing your languages
with:
languages: go
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -1,115 +0,0 @@
name: "Release"
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: '1.15.2'
- name: Build
env:
REF: ${{ github.ref }}
run: |
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X github.com/pterodactyl/wings/system.Version=${REF:11}" -o build/wings_linux_amd64 -v wings.go
GOOS=linux GOARCH=arm64 go build -ldflags="-s -w -X github.com/pterodactyl/wings/system.Version=${REF:11}" -o build/wings_linux_arm64 -v wings.go
GOOS=linux GOARCH=arm go build -ldflags="-s -w -X github.com/pterodactyl/wings/system.Version=${REF:11}" -o build/wings_linux_arm -v wings.go
- name: Test
run: go test ./...
- name: Compress binary and make it executable
run: |
upx --brute build/wings_linux_amd64 && chmod +x build/wings_linux_amd64
upx build/wings_linux_arm64 && chmod +x build/wings_linux_arm64
upx build/wings_linux_arm && chmod +x build/wings_linux_arm
- name: Extract changelog
env:
REF: ${{ github.ref }}
run: |
sed -n "/^## ${REF:10}/,/^## /{/^## /b;p}" CHANGELOG.md > ./RELEASE_CHANGELOG
echo ::set-output name=version_name::`sed -nr "s/^## (${REF:10} .*)$/\1/p" CHANGELOG.md`
- name: Create checksum and add to changelog
run: |
SUM=`cd build && sha256sum wings_linux_amd64`
SUM2=`cd build && sha256sum wings_linux_arm64`
SUM3=`cd build && sha256sum wings_linux_arm`
echo -e "\n#### SHA256 Checksum\n\`\`\`\n$SUM\n$SUM2\n$SUM3\n\`\`\`\n" >> ./RELEASE_CHANGELOG
echo -e "$SUM\n$SUM2\n$SUM3" > checksums.txt
- name: Create release branch
env:
REF: ${{ github.ref }}
run: |
BRANCH=release/${REF:10}
git config --local user.email "ci@pterodactyl.io"
git config --local user.name "Pterodactyl CI"
git checkout -b $BRANCH
git push -u origin $BRANCH
sed -i "s/ Version = \".*\"/ Version = \"${REF:11}\"/" system/const.go
git add system/const.go
git commit -m "bump version for release"
git push
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ steps.extract_changelog.outputs.version_name }}
body_path: ./RELEASE_CHANGELOG
draft: true
prerelease: ${{ contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
- name: Upload amd64 Binary
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: build/wings_linux_amd64
asset_name: wings_linux_amd64
asset_content_type: application/octet-stream
- name: Upload arm64 Binary
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: build/wings_linux_arm64
asset_name: wings_linux_arm64
asset_content_type: application/octet-stream
- name: Upload arm Binary
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: build/wings_linux_arm
asset_name: wings_linux_arm
asset_content_type: application/octet-stream
- name: Upload checksum
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./checksums.txt
asset_name: checksums.txt
asset_content_type: text/plain

2
.gitignore vendored
View File

@@ -45,5 +45,3 @@ test_*/
# Keep all gitkeep files (This needs to stay at the bottom)
!.gitkeep
debug
data/.states.json
.DS_Store

View File

@@ -1,24 +1,22 @@
os: linux
dist: xenial
language: go
go:
- 1.13.x
go_import_path: "github.com/pterodactyl/wings"
- 1.9.x
services:
- docker
- docker
install:
- mkdir -p $GOPATH/bin
# Install used tools
- go get github.com/golang/dep/cmd/dep
- go get github.com/mitchellh/gox
- go get github.com/haya14busa/goverage
- go get github.com/schrej/godacov
- go mod download
# Install project dependencies with dep
- dep ensure
script:
- make cross-build

View File

@@ -1,305 +0,0 @@
# Changelog
## next
### Added
* Adds support for ARM to build outputs for wings.
### Fixed
* Fixed a few docker clients not having version negotiation enabled.
* Fixes local images prefixed with `~` getting pulled from remote sources rather than just using the local copy.
* Fixes console output breaking with certain games when excessive line length was output.
* Fixes an error when console lines were too long that would cause the console to stop updating until the server was restarted,
### Changed
* Simplified timezone logic for containers by properly grabbing the system timezone and then passing that through to containers with the `TZ=` environment variable.
## v1.0.0
This is the first official stable release of Wings! Please be aware that while this specific version changelog is very short,
it technically includes all of the previous beta and alpha releases within it. For the sake of version history and following
along though, I've only included the differences between this version and the previous RC build.
### Fixed
* Fixes file parser not properly appending newline character to modified lines.
* Fixes server disk usage not being properly reported back to the API and websocket.
### Changed
* Changes diagnostics endpoint URL to be `ptero.co` rather than `hastebin.com`.
* Diagnostics report now includes system time for easier debugging of logs and container issues.
## v1.0.0-rc.7
### Fixed
* **[Security]** Prevents the `allowed_mounts` configuration value from being set by a remote API call.
* Fixes an unexpected error when attempting to make a copy of an archive file.
* Fixes certain expected filesystems errors being returned to the API as a 500 error rather than a 4XX series error.
* Fixes a panic crash when there is no text on a line and the parser is attempting to determine if the line is a comment.
* Fixes multiple filesystem operations to correctly check, increment, and decrement disk usage for a server.
* Fixes a negative disk space usage issue when deleting a file due to a bad int swap.
* Errors from a websocket connection being closed will no longer spam the console.
* Fixes an extraneous `.` in the install script for servers causing errors in some scenarios.
* Fixes unexpected error spam due to a change in how `os.ErrNotExist` is returned from some functions.
### Changed
* Stacktrace is no longer emitted when warning that an image exists locally.
* Configuration parser now attempts to create the directory structure leading to a configuration file if it is missing.
* If a file name is too long for the system a nice error is returned to the caller.
* Enables client version negotiation for Docker to support more versions.
* Disk space errors are no longer logged into the wings logs.
* Servers can no longer be reinstalled while another power action is currently running. This avoids data collisions and wings running into issues with the container state.
* Wings now uses `1024` rather than `1000` bytes when calculating the disk space usage for a server to match how the Panel reports usage.
* JWT errors in the websocket are now sent back to the connection as a specific event type allowing them to be handled even if the timer fails to execute or executes but is not being listened for.
* A server struct is no longer embedded in the virtual filesystem allowing for easier testing and modularization of the codebase.
* Server websockets are now closed when a server is deleted, disconnecting any currently connected clients.
### Removed
* **[Security]** Removes the `SafeJoin` function which could wrongly assume the state of a file and allow a user to escape the root if the developer implementing the call calls `Stat` rather than `Lstat`.
## v1.0.0-rc.6
### Fixed
* Fixes race condition when checking if the running state of a server has changed.
* Fixes files mistakenly unpacking themselves to the root directory rather than the directory they are told to unpack to.
* Fixes console output not being sent to the websocket in the same order that it was received.
* Fixes a file busy error causing a 500 error response when unpacking an archive rather than a 400 error with a message indicating what is wrong to the user.
* Fixes docker image not properly updating when a server is started.
### Changed
* Replaces fragile event bus logic with a more robust and easier to understand system. This fixes all remaining console and stats output issues that have been reported.
* Cleans up API response error messaging to avoid empty errors being logged which cannot be acted upon.
* Adds support for retrying a file write a few times with a back-off if the file is busy when the write occurs.
### Added
* Docker image pull data is shown in the console when an admin is connected to the websocket.
* Adds console throttling to stop a server if too much data is being piped out of the console. This logic mimics the logic present in the old Nodejs daemon, but with a 2x line count limit (1000 -> 2000) per period.
## v1.0.0-rc.5
### Fixed
* Fixes long standing bug with console output not properly sending back to the client with server stats in certain edge case scenarios. This was "fixed" in `rc.4` but ended up breaking many servers in significantly more painful manners, but did allow us to better track down the issue.
* Fixes build flags during release process to correctly strip unnessary code allowing the final binary size to be reduced back down to `~5MB`
* Fixes Wings returning the last `16384` lines of the log file when connecting to the websocket.
* Fixes pre-boot actions always running for a server start event even if the server is already running.
### Added
* Adds support for configuring the amount of time that can elapse between server disk size checking before the value is considered stale.
## v1.0.0-rc.4
### Fixed
* Fixes server files being inaccessible if the root data path is a symlink to another location on the machine.
* Fixes some console output being written to the logs accidentally truncating other lines due to special ANSI sequences being output.
* Fixes `server.properties` files getting mangled by the automatic configuration editor when booting a server.
* Fixes a missed stream close when stopping resource polling that would lead to memory leaks.
* Fixes port bindings being incorrectly re-assigned when using `127.0.0.1` with Docker. These are now properly re-mapped to the `pterodactyl0` interface so that networking operates as expected for the server.
* Fixes handling of values within arrays in `yaml` and `json` configuration files.
* Fixes a 304 error being returned rather than a 400 error when a file upload is invalid.
* Fixes deadlocks and response delays when processing server console and stat events. This was causing server consoles to become completely unresponsive after random periods of time and was only resolvable via a Wings restart.
### Changed
* Allows a stale value to be present when starting a server if the server is allowed an unlimited amount of disk space.
* Removed all remaining traces of the `zap` logging library from the codebase.
* Servers no longer auto-restart as if they crashed when a stop command is manually sent via the server console to the instance.
* Changes the CORS checks to allow `*` as a remote origin.
### Added
* Added an auto-generated logrotate file which is written to the normal logrotate directory when wings is first started.
* Added additional debug logging within the internals of the resource polling for servers to better trace unexpected behavior.
* Adds additional logic check to avoid even trying to stop a suspended server if it is already stopped.
## v1.0.0-rc.3
### Fixed
* Errors during the backup process are now correctly reported to the Panel and logged into the output correctly.
* Empty directories are no longer pushed into the backup file list (which was causing errors to occur previously).
* Covered an edge case to prevent errors if a file gets deleted while a backup is in progress.
* Fixed a bug causing Wings to panic and crash if an invalid environment variable value was passed through. These invalid values are now logged to the output to better detect and an empty string is returned in their place.
* Fixed startup variables and other server information not properly being updated when a server is restarted.
* Suspension state of a server is now properly returned by the API.
* Fixes an error being thrown if a backup does not exist on the local machine. A 404 is now properly returned and handled by the Panel when this occurs.
* Fixes an error when attempting to move or rename a folder due to the target being created accidentally before the rename occurred.
* Fixes install scripts running even when the checkbox to not run them on installation was selected in the Panel.
### Changed
* Disk space checking modified to not block in as many parts of the codebase and allow returning a stale cache value where that is appropriate. Thanks @cyberkitsune!
* SFTP package code is now merged into the codebase to make keeping up with all of the code simpler, and reduce the complexity of the SFTP server.
### Added
* Added the ability for a node to be configured to skip file permissions checking when starting a server.
* Added console output message to indicate that server disk space checking is occurring rather than silently sitting there.
## v1.0.0-rc.2
### Fixed
* Fixes significant performance degradation due to excessive `syscall` actions when determining directory sizes on large servers. This was previously causing CPU & I/O lockups on servers and should be significantly more performant and less impactful on the system now.
* Fixes panic crash when booting wings with no log directory created.
### Changed
* Changed default interval for disk space calculation from every 60 seconds to every 2.5 minutes.
## v1.0.0-rc.1
### Fixed
* Servers are no longer incorrectly marked as stopping when they are, in fact, offline.
* Release build version is now correctly output when starting wings.
* Termination signals can now always be sent to a server instance even if the instance is currently starting/stopping.
* Removed the file chown on wings boot to avoid slowing down the boot process unnecessarily when working with hundreds of servers on a node.
* Fixed a multitude of race conditions throughout the code that cropped up during testing and made power handling even more robust for server instances.
* In general multiple fragile areas of the codebase have been improved and are more likely to return useful errors should they break.
* Addressed serious CPU usage issues when generating backups, and additionally decreased the amount of time they take to generate.
### Changed
* Internal server now uses more secure and recommended TLS settings.
* Environment handling is now completely separate from the server package itself, allowing the environments to no longer be tightly coupled to the server.
* `/tmp` directory mounted into containers can now be programmatically managed and uses better defaults to avoid people even needing to edit it.
### Added
* Wings logs are now properly persisted to the disk.
* Adds the ability for an egg to use ANSI-stripped matching when determining if a server is done booting.
## v1.0.0-beta.9
### Fixed
* Fixes server resource usage stats not being returned correctly by the API endpoint.
* Fixes an exception thrown when attempting to write server installation logs.
* Fixes error handling to provide a more accurate stack-trace in more scenarios where one is missing initially.
* Fixes a memory leak and zombie event listeners when disconnecting from a server's websocket.
* Fixes a race condition when wings is attempting to register/de-register event subscribers.
* Server data directories now correctly have their permissions set recursively when booting Wings.
* Fixes a race condition when a server's console stream was not fully closed before the next power action was started.
### Changed
* Server power handling is now handled in a synchronous manner. This avoids endless bugs and race conditions that would crop up if someone triggered two restart processes back to back. The new logic prevents performing any additional power actions until the currently executing action is completed.
* Server disk usage is now correctly calculated when restarting the daemon as long as the server data directory exists.
* Multiple code pathways within the boot process were cleaned up and modified to be less IO intensive and overall easier to reason about as a developer.
* Additional timezone data is now mounted into containers to better improve the ability for instances running to be using the correct timezone.
### Added
* Adds basic internal file upload endpoints (these are currently not exposed anywhere in the Panel).
* Added additional process events for installation start and completion.
* Additional CORS allowed origins for the websocket can now be defined in the configuration file.
* Adds the ability to authenticate with a docker registry when pulling images.
## v1.0.0-beta.8
### Fixed
* Server state is synced with the Panel before performing a reinstall to ensure that the latest information is used.
* Wings no longer crashes when a non-string environment variable value is passed through.
* SFTP server authentication no longer attempts to contact the Panel to validate credentials if the format is known to already be incorrect.
* Some previously missing error stacks are now properly returned when encountered.
* Renaming a file no longer triggers an error if the base path does not exist.
* Disk space is now properly calculated for a server even if it is assigned unlimited space.
* **[Security]** Prevent symlink files from unintentionally chmodding their target file when a server is booted when the target file resides outside the server data directory.
* **[Security]** Cleans up multiple code pathways that may unintentionally allow a malicious user to impact files outside their home directory.
* Fixes numerous race conditions during a server's boot process that may lead to unintended data states.
* Fixes an error thrown when attempting to delete a file that points to a symlink outside the server data directory.
* Deleting a symlink will no longer attempt to remove the source file, only the symlink itself.
* Websocket no longer blocks when handling a long running process for a user.
### Changed
* Install log for servers is now more detailed and more useful for debugging what might be going wrong.
* Certain file management API endpoints now support passing through multiple paths at once to make mass actions easier to perform.
* Re-worked file walking implementation to not cause a server crash when working with very large directories, and avoid race conditions when recursively walking directories.
* Server configuration structure re-worked to be more manageable in the code base and avoid additional race conditions and complexity while making on-the-fly changes to it.
### Added
* Support for configuring additional file mount points in a container via the Panel.
* Support for automated SSL certificate generation when booting the Daemon.
* Added wings diagnostics command.
* New API endpoints to compress and decompress files on a server.
## v1.0.0-beta.7
### Fixed
* Stacktraces are now displayed once in the error output, rather than twice when certain errors are encountered.
* Additional errors that did not previously output a stack trace have been fixed to output.
* Fixes a bug where server space available would be triggered when creating a new server from a remote configuration before that file location existed on the disk, leading to an error.
* Fixes context timeouts while pulling server docker images. Time was upped from 10 seconds to 15 minutes.
* Configuration file replacement values are now properly de-escaped when writing to the disk. `\/no\/more\/slashes`
* `.properties` files are now saved correctly to the disk with newlines, rather than shoved on a single line.
* `./wings configure` command can now properly save the configuration to the disk.
* Custom SSL locations are no longer obliterated when making changes to the Node's configuration via the Panel.
### Changed
* Exclusive lock is now acquired when performing a server installation to avoid two install processes being triggered at the same time. This also allows an install to be properly canceled if the server is deleted before it is completed.
## v1.0.0-beta.6
### Fixed
* Server status is no longer sent to all connected websocket clients when a new client connects to the socket.
* Server disk usage is sent back over the socket when connecting.
* Default configuration for the SFTP server is now properly returned to `on` rather than `off`
* Server boot is no longer blocked if there is an error fetching the Docker image as long as that image exists on the host.
* The websocket no longer gets locked up when Wings attempts to send an error to the client.
* Fixed a crash loop when an error is thrown during pre-boot phase of server startup.
* Errors with `BindJSON` in API endpoints are now properly handled and returned.
* Fixed warning about Gin running in non-release mode, even when the binary is running in release mode.
### Changed
* Switched logging libraries to output data in a clearer format more suited to the CLI where this application runs.
* Cleaner debug messaging in debug mode from the router.
## v1.0.0-beta.5
### Fixed
* Default config location settled on `/etc/pterodactyl/config.yml`; wings will now check all of the previous locations for the configuration and move it automatically to the new location.
* Deleting a server no longer fails the process if the container cannot be found.
* Fixes permissions checking for subusers connecting to the SFTP instance.
* S3 backups now properly send back hash data to the panel.
* Server installation containers are now always deleted, even if the installer process fails for the instance.
* Files and folders with special characters and spaces in them no longer return a 404 error.
* Servers using eggs with bad configurations will no longer cause the daemon to fail booting; these bad configurations are simply skipped over and a warning is emitted into the logs.
* Environment variables passed to containers no longer contain improper quotes around them.
* Matching on array indexes in configurations now works correctly; `foo[0]` is quietly transformed into the proper `foo.0` syntax.
### Added
* New banner error message when the daemon is unable to locate the configuration file. This should better clarify what the user needs to do in order to resolve the issue.
* Adds ability to configure the default networking driver used by docker.
## v1.0.0-beta.4
### Fixed
* Fixes unexpected nil-pointer panic when attempting to start some newly created servers, or any server that was missing a container on the system.
* Fixes memory usage of process being reported differently than the `docker stats` output leading to some confusion. These numbers should be more correct now.
* Fixes possible nil-pointer panic when detecting a deleted container as being crashed.
## v1.0.0-beta.3
### Fixed
* Daemon will no longer crash if someone requests a websocket for a deleted server.
* Temporary directories are now created properly if missing during the server installation process.
### Added
* Added support for using Amazon S3 as a backup location for archives.
### Changed
* Memory overhead for containers is now 5/10/15% higher than the passed limit to account for JVM heap and prevent crashing.
## v1.0.0-beta.2
### Changed
* Backup functionality made significantly more modular to ease adding additional methods in the future.
* Websocket permissions changed to use same name as in panel.
* Container memory hard-limits are now adjusted up by 15% (< 2G memory), 10% (< 4G memory), or 5% to avoid unexpected OOM crashes for memory heavy games.
* Wings executable is now 80% smaller thanks to better compilation arguments.
### Added
* Adds support for ignoring files and directories when generating a new backup.
* Added internal directory walker with callback-continue support.
### Fixed
* Fixed linux distro matching when booting the daemon.
* Fixes DNS to be configurable for docker containers that are made for servers.
* Fixes incorrect file truncation when making modifications to a server's configuration files.
## v1.0.0-beta.1
### Added
* Added support for passing specific threads to the docker environment when running a server.
* Added support for reinstalling an existing server.
* Added support for restarting a server.
* Adds support for transferring servers between daemon instances.
* Added auto-deploy command to fetch configuration from Panel automatically.
### Changed
* Server file and backup downloads now hit a direct endpoint with a one-time JWT to avoid proxying large downloads through the panel.
### Fixed
* Fixes a go routine causing a memory and CPU leak.
* Fixed improper chown of server directories on boot.
* Miscellaneous fixes to CPU usage, abandoned go-routines, and race conditions.
* Fixes support for Alpine based systems.
## v1.0.0-alpha.2
### Added
* Ability to run an installation process for a server and notify the panel when completed.
* Output from the installation process is now emitted over the server console for administrative users to view.
### Fixed
* Fixed bugs caused when unmarshaling data into a struct with default values.
* Timezone is properly set in containers by using the `TZ=` environment variable rather than mounting timezone data into the filesystem.
* Server data directly is now properly created when running the install process.
* Time drift caused in the socket JWT is now handled in a better manner and less likely to unexpectedly error out.

View File

@@ -1,14 +0,0 @@
# ----------------------------------
# Pterodactyl Panel Dockerfile
# ----------------------------------
FROM golang:1.15-alpine
COPY . /go/wings/
WORKDIR /go/wings/
RUN apk add --no-cache upx \
&& go build -ldflags="-s -w" \
&& upx --brute wings
FROM alpine:latest
COPY --from=0 /go/wings/wings /usr/bin/
CMD ["wings","--config", "/etc/pterodactyl/config.yml"]

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019 Dane Everitt <dane@daneeveritt.com>
Copyright (c) 2017 Jakob Schrettenbrunner <dev@schrej.net>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,13 +1,28 @@
build:
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -gcflags "all=-trimpath=$(pwd)" -o build/wings_linux_amd64 -v wings.go
GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -gcflags "all=-trimpath=$(pwd)" -o build/wings_linux_arm64 -v wings.go
BINARY = "build/wings"
OSARCHLIST = "darwin/386 darwin/amd64 linux/386 linux/amd64 linux/arm linux/arm64 windows/386 windows/amd64"
compress:
upx --brute build/wings_*
all: $(BINARY)
cross-build: clean build compress
$(BINARY):
go build -o $(BINARY)
clean:
rm -rf build/wings_*
cross-build:
gox -osarch $(OSARCHLIST) -output "build/{{.Dir}}_{{.OS}}_{{.Arch}}"
.PHONY: all build compress clean
.PHONY: install
install:
go install
test:
go test `go list ./... | grep -v "/vendor/"`
coverage:
goverage -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
dependencies:
glide install
install-tools:
go get -u github.com/mitchellh/gox
go get -u github.com/haya14busa/goverage

View File

@@ -1,40 +1,16 @@
[![Logo Image](https://cdn.pterodactyl.io/logos/new/pterodactyl_logo.png)](https://pterodactyl.io)
# Alpha Project
Please refrain from opening PRs or Issues at this time. This project is still under heavy development, and until we have a solid foundation and plan for how everything will connect, we will not be accepting PRs or feature suggestions.
![Discord](https://img.shields.io/discord/122900397965705216?label=Discord&logo=Discord&logoColor=white)
![GitHub Releases](https://img.shields.io/github/downloads/pterodactyl/wings/latest/total)
[![Go Report Card](https://goreportcard.com/badge/github.com/pterodactyl/wings)](https://goreportcard.com/report/github.com/pterodactyl/wings)
# Pterodactyl wings [![travis](https://img.shields.io/travis/pterodactyl/wings.svg?style=flat-square)](https://travis-ci.org/pterodactyl/wings) [![codacy quality](https://img.shields.io/codacy/grade/27a1576bda86450f853b1052b12fa570.svg?style=flat-square)](https://www.codacy.com/app/schrej/wings/dashboard) [![codacy coverage](https://img.shields.io/codacy/coverage/27a1576bda86450f853b1052b12fa570.svg?style=flat-square)](https://www.codacy.com/app/schrej/wings/files)
# Pterodactyl Wings
Wings is Pterodactyl's server control plane, built for the rapidly changing gaming industry and designed to be
highly performant and secure. Wings provides an HTTP API allowing you to interface directly with running server
instances, fetch server logs, generate backups, and control all aspects of the server lifecycle.
```
____
__ Pterodactyl _____/___/_______ _______ ______
\_____\ \/\/ / / / __ / ___/
\___\ / / / / /_/ /___ /
\___/\___/___/___/___/___ /______/
/_______/ alpha
```
In addition, Wings ships with a built-in SFTP server allowing your system to remain free of Pterodactyl specific
dependencies, and allowing users to authenticate with the same credentials they would normally use to access the Panel.
A new generation of the Pterodactyl daemon, written in go.
## Sponsors
I would like to extend my sincere thanks to the following sponsors for helping find Pterodactyl's developement.
[Interested in becoming a sponsor?](https://github.com/sponsors/DaneEveritt)
| Company | About |
| ------- | ----- |
| [**WISP**](https://wisp.gg) | Extra features. |
| [**Bloom.host**](https://bloom.host) | Bloom.host offers dedicated core VPS and Minecraft hosting with Ryzen 9 processors. With owned-hardware, we offer truly unbeatable prices on high-performance hosting. |
| [**MineStrator**](https://minestrator.com/) | Looking for a French highend hosting company for you minecraft server? More than 14,000 members on our discord, trust us. |
| [**DedicatedMC**](https://dedicatedmc.io/) | DedicatedMC provides Raw Power hosting at affordable pricing, making sure to never compromise on your performance and giving you the best performance money can buy. |
| [**Skynode**](https://www.skynode.pro/) | Skynode provides blazing fast game servers along with a top-notch user experience. Whatever our clients are looking for, we're able to provide it! |
| [**XCORE-SERVER.de**](https://xcore-server.de/) | XCORE-SERVER.de offers High-End Servers for hosting and gaming since 2012. Fast, excellent and well-known for eSports Gaming. |
| [**RoyaleHosting**](https://royalehosting.net/) | Build your dreams and deploy them with RoyaleHostings reliable servers and network. Easy to use, provisioned in a couple of minutes. |
| [**Spill Hosting**](https://spillhosting.no/) | Spill Hosting is a Norwegian hosting service, which aims to cheap services on quality servers. Premium i9-9900K processors will run your game like a dream. |
| [**DeinServerHost**](https://deinserverhost.de/) | DeinServerHost offers Dedicated, vps and Gameservers for many popular Games like Minecraft and Rust in Germany since 2013. |
## Documentation
* [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html)
* [Wings Documentation](https://pterodactyl.io/wings/1.0/installing.html)
* [Community Guides](https://pterodactyl.io/community/about.html)
* Or, get additional help [via Discord](https://discord.gg/pterodactyl)
## Reporting Issues
Please use the [pterodactyl/panel](https://github.com/pterodactyl/panel) repository to report any issues or make
feature requests for Wings. In addition, the [security policy](https://github.com/pterodactyl/panel/security/policy) listed
within that repository also applies to Wings.

View File

@@ -4,16 +4,16 @@ import (
"bytes"
"encoding/json"
"fmt"
"github.com/apex/log"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config"
"go.uber.org/zap"
"io/ioutil"
"net/http"
"strings"
"time"
)
// Initializes the requester instance.
// Initalizes the requester instance.
func NewRequester() *PanelRequest {
return &PanelRequest{
Response: nil,
@@ -32,7 +32,7 @@ func (r *PanelRequest) GetClient() *http.Client {
func (r *PanelRequest) SetHeaders(req *http.Request) *http.Request {
req.Header.Set("Accept", "application/vnd.pterodactyl.v1+json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s.%s", config.Get().AuthenticationTokenId, config.Get().AuthenticationToken))
req.Header.Set("Authorization", "Bearer "+config.Get().AuthenticationToken)
return req
}
@@ -45,26 +45,6 @@ func (r *PanelRequest) GetEndpoint(endpoint string) string {
)
}
// Logs the request into the debug log with all of the important request bits.
// The authorization key will be cleaned up before being output.
func (r *PanelRequest) logDebug(req *http.Request) {
headers := make(map[string][]string)
for k, v := range req.Header {
if k != "Authorization" || len(v) == 0 {
headers[k] = v
continue
}
headers[k] = []string{v[0][0:15] + "(redacted)"}
}
log.WithFields(log.Fields{
"method": req.Method,
"endpoint": req.URL.String(),
"headers": headers,
}).Debug("making request to external HTTP endpoint")
}
func (r *PanelRequest) Get(url string) (*http.Response, error) {
c := r.GetClient()
@@ -75,7 +55,7 @@ func (r *PanelRequest) Get(url string) (*http.Response, error) {
return nil, err
}
r.logDebug(req)
zap.S().Debugw("GET request to endpoint", zap.String("endpoint", r.GetEndpoint(url)), zap.Any("headers", req.Header))
return c.Do(req)
}
@@ -90,7 +70,7 @@ func (r *PanelRequest) Post(url string, data []byte) (*http.Response, error) {
return nil, err
}
r.logDebug(req)
zap.S().Debugw("POST request to endpoint", zap.String("endpoint", r.GetEndpoint(url)), zap.Any("headers", req.Header))
return c.Do(req)
}
@@ -130,26 +110,15 @@ func (r *PanelRequest) HttpResponseCode() int {
return r.Response.StatusCode
}
func IsRequestError(err error) bool {
_, ok := err.(*RequestError)
return ok
}
type RequestError struct {
response *http.Response
Code string `json:"code"`
Status string `json:"status"`
Detail string `json:"detail"`
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()
return fmt.Sprintf("%s: %s (HTTP/%s)", re.Code, re.Detail, re.Status)
}
type RequestErrorBag struct {
@@ -166,12 +135,9 @@ func (r *PanelRequest) Error() *RequestError {
bag := RequestErrorBag{}
json.Unmarshal(body, &bag)
e := new(RequestError)
if len(bag.Errors) > 0 {
e = &bag.Errors[0]
if len(bag.Errors) == 0 {
return new(RequestError)
}
e.response = r.Response
return e
return &bag.Errors[0]
}

View File

@@ -1,36 +0,0 @@
package api
import (
"encoding/json"
"fmt"
"github.com/pkg/errors"
)
type BackupRequest struct {
Checksum string `json:"checksum"`
ChecksumType string `json:"checksum_type"`
Size int64 `json:"size"`
Successful bool `json:"successful"`
}
// Notifies the panel that a specific backup has been completed and is now
// available for a user to view and download.
func (r *PanelRequest) SendBackupStatus(backup string, data BackupRequest) (*RequestError, error) {
b, err := json.Marshal(data)
if err != nil {
return nil, errors.WithStack(err)
}
resp, err := r.Post(fmt.Sprintf("/backups/%s", backup), b)
if err != nil {
return nil, errors.WithStack(err)
}
defer resp.Body.Close()
r.Response = resp
if r.HasError() {
return r.Error(), nil
}
return nil, nil
}

View File

@@ -1,68 +0,0 @@
package api
import (
"encoding/json"
"github.com/apex/log"
"github.com/pterodactyl/wings/parser"
"regexp"
"strings"
)
type OutputLineMatcher struct {
// The raw string to match against. This may or may not be prefixed with
// regex: which indicates we want to match against the regex expression.
raw string
reg *regexp.Regexp
}
// Determine if a given string "s" matches the given line.
func (olm *OutputLineMatcher) Matches(s string) bool {
if olm.reg == nil {
return strings.Contains(s, olm.raw)
}
return olm.reg.MatchString(s)
}
// Return the matcher's raw comparison string.
func (olm *OutputLineMatcher) String() string {
return olm.raw
}
// Unmarshal the startup lines into individual structs for easier matching abilities.
func (olm *OutputLineMatcher) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &olm.raw); err != nil {
return err
}
if strings.HasPrefix(olm.raw, "regex:") && len(olm.raw) > 6 {
r, err := regexp.Compile(strings.TrimPrefix(olm.raw, "regex:"))
if err != nil {
log.WithField("error", err).WithField("raw", olm.raw).Warn("failed to compile output line marked as being regex")
}
olm.reg = r
}
return nil
}
type ProcessStopConfiguration struct {
Type string `json:"type"`
Value string `json:"value"`
}
// Defines the process configuration for a given server instance. This sets what the
// daemon is looking for to mark a server as done starting, what to do when stopping,
// and what changes to make to the configuration file for a server.
type ProcessConfiguration struct {
Startup struct {
Done []*OutputLineMatcher `json:"done"`
UserInteraction []string `json:"user_interaction"`
StripAnsi bool `json:"strip_ansi"`
} `json:"startup"`
Stop ProcessStopConfiguration `json:"stop"`
ConfigurationFiles []parser.ConfigurationFile `json:"configs"`
}

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/parser"
)
const (
@@ -25,17 +26,24 @@ type ServerConfigurationResponse struct {
ProcessConfiguration *ProcessConfiguration `json:"process_configuration"`
}
// Defines installation script information for a server process. This is used when
// a server is installed for the first time, and when a server is marked for re-installation.
type InstallationScript struct {
ContainerImage string `json:"container_image"`
Entrypoint string `json:"entrypoint"`
Script string `json:"script"`
// Defines the process configuration for a given server instance. This sets what the
// daemon is looking for to mark a server as done starting, what to do when stopping,
// and what changes to make to the configuration file for a server.
type ProcessConfiguration struct {
Startup struct {
Done string `json:"done"`
UserInteraction []string `json:"userInteraction"`
} `json:"startup"`
Stop struct {
Type string `json:"type"`
Value string `json:"value"`
} `json:"stop"`
ConfigurationFiles []parser.ConfigurationFile `json:"configs"`
}
// GetAllServerConfigurations fetches configurations for all servers assigned to this node.
func (r *PanelRequest) GetAllServerConfigurations() (map[string]json.RawMessage, *RequestError, error) {
resp, err := r.Get("/servers")
// Fetches the server configuration and returns the struct for it.
func (r *PanelRequest) GetServerConfiguration(uuid string) (*ServerConfigurationResponse, *RequestError, error) {
resp, err := r.Get(fmt.Sprintf("/servers/%s", uuid))
if err != nil {
return nil, nil, errors.WithStack(err)
}
@@ -47,142 +55,12 @@ func (r *PanelRequest) GetAllServerConfigurations() (map[string]json.RawMessage,
return nil, r.Error(), nil
}
res := &ServerConfigurationResponse{}
b, _ := r.ReadBody()
res := map[string]json.RawMessage{}
if len(b) == 2 {
return res, nil, nil
}
if err := json.Unmarshal(b, &res); err != nil {
if err := json.Unmarshal(b, res); err != nil {
return nil, nil, errors.WithStack(err)
}
return res, nil, nil
}
// Fetches the server configuration and returns the struct for it.
func (r *PanelRequest) GetServerConfiguration(uuid string) (ServerConfigurationResponse, *RequestError, error) {
res := ServerConfigurationResponse{}
resp, err := r.Get(fmt.Sprintf("/servers/%s", uuid))
if err != nil {
return res, nil, errors.WithStack(err)
}
defer resp.Body.Close()
r.Response = resp
if r.HasError() {
return res, r.Error(), nil
}
b, _ := r.ReadBody()
if err := json.Unmarshal(b, &res); err != nil {
return res, nil, errors.WithStack(err)
}
return res, nil, nil
}
// Fetches installation information for the server process.
func (r *PanelRequest) GetInstallationScript(uuid string) (InstallationScript, *RequestError, error) {
res := InstallationScript{}
resp, err := r.Get(fmt.Sprintf("/servers/%s/install", uuid))
if err != nil {
return res, nil, errors.WithStack(err)
}
defer resp.Body.Close()
r.Response = resp
if r.HasError() {
return res, r.Error(), nil
}
b, _ := r.ReadBody()
if err := json.Unmarshal(b, &res); err != nil {
return res, nil, errors.WithStack(err)
}
return res, nil, nil
}
type installRequest struct {
Successful bool `json:"successful"`
}
// Marks a server as being installed successfully or unsuccessfully on the panel.
func (r *PanelRequest) SendInstallationStatus(uuid string, successful bool) (*RequestError, error) {
b, err := json.Marshal(installRequest{Successful: successful})
if err != nil {
return nil, errors.WithStack(err)
}
resp, err := r.Post(fmt.Sprintf("/servers/%s/install", uuid), b)
if err != nil {
return nil, errors.WithStack(err)
}
defer resp.Body.Close()
r.Response = resp
if r.HasError() {
return r.Error(), nil
}
return nil, nil
}
type archiveRequest struct {
Successful bool `json:"successful"`
}
func (r *PanelRequest) SendArchiveStatus(uuid string, successful bool) (*RequestError, error) {
b, err := json.Marshal(archiveRequest{Successful: successful})
if err != nil {
return nil, errors.WithStack(err)
}
resp, err := r.Post(fmt.Sprintf("/servers/%s/archive", uuid), b)
if err != nil {
return nil, errors.WithStack(err)
}
defer resp.Body.Close()
r.Response = resp
if r.HasError() {
return r.Error(), nil
}
return nil, nil
}
func (r *PanelRequest) SendTransferFailure(uuid string) (*RequestError, error) {
resp, err := r.Get(fmt.Sprintf("/servers/%s/transfer/failure", uuid))
if err != nil {
return nil, errors.WithStack(err)
}
defer resp.Body.Close()
r.Response = resp
if r.HasError() {
return r.Error(), nil
}
return nil, nil
}
func (r *PanelRequest) SendTransferSuccess(uuid string) (*RequestError, error) {
resp, err := r.Get(fmt.Sprintf("/servers/%s/transfer/success", uuid))
if err != nil {
return nil, errors.WithStack(err)
}
defer resp.Body.Close()
r.Response = resp
if r.HasError() {
return r.Error(), nil
}
return nil, nil
}

View File

@@ -2,57 +2,11 @@ package api
import (
"encoding/json"
"github.com/apex/log"
"github.com/pkg/errors"
"regexp"
"github.com/pterodactyl/sftp-server"
)
type SftpAuthRequest struct {
User string `json:"username"`
Pass string `json:"password"`
IP string `json:"ip"`
SessionID []byte `json:"session_id"`
ClientVersion []byte `json:"client_version"`
}
type SftpAuthResponse struct {
Server string `json:"server"`
Token string `json:"token"`
Permissions []string `json:"permissions"`
}
type sftpInvalidCredentialsError struct {
}
func (ice sftpInvalidCredentialsError) Error() string {
return "the credentials provided were invalid"
}
func IsInvalidCredentialsError(err error) bool {
_, ok := err.(*sftpInvalidCredentialsError)
return ok
}
// Usernames all follow the same format, so don't even bother hitting the API if the username is not
// at least in the expected format. This is very basic protection against random bots finding the SFTP
// server and sending a flood of usernames.
var validUsernameRegexp = regexp.MustCompile(`^(?i)(.+)\.([a-z0-9]{8})$`)
func (r *PanelRequest) ValidateSftpCredentials(request SftpAuthRequest) (*SftpAuthResponse, error) {
// If the username doesn't meet the expected format that the Panel would even recognize just go ahead
// and bail out of the process here to avoid accidentally brute forcing the panel if a bot decides
// to connect to spam username attempts.
if !validUsernameRegexp.MatchString(request.User) {
log.WithFields(log.Fields{
"subsystem": "sftp",
"username": request.User,
"ip": request.IP,
}).Warn("failed to validate user credentials (invalid format)")
return nil, new(sftpInvalidCredentialsError)
}
func (r *PanelRequest) ValidateSftpCredentials(request sftp_server.AuthenticationRequest) (*sftp_server.AuthenticationResponse, error) {
b, err := json.Marshal(request)
if err != nil {
return nil, err
@@ -67,22 +21,14 @@ func (r *PanelRequest) ValidateSftpCredentials(request SftpAuthRequest) (*SftpAu
r.Response = resp
if r.HasError() {
if r.HttpResponseCode() >= 400 && r.HttpResponseCode() < 500 {
log.WithFields(log.Fields{
"subsystem": "sftp",
"username": request.User,
"ip": request.IP,
}).Warn(r.Error().String())
return nil, new(sftpInvalidCredentialsError)
if r.HttpResponseCode() == 403 {
return nil, sftp_server.InvalidCredentialsError{}
}
rerr := errors.New(r.Error().String())
return nil, rerr
return nil, errors.WithStack(errors.New(r.Error().String()))
}
response := new(SftpAuthResponse)
response := new(sftp_server.AuthenticationResponse)
body, _ := r.ReadBody()
if err := json.Unmarshal(body, response); err != nil {
@@ -90,4 +36,4 @@ func (r *PanelRequest) ValidateSftpCredentials(request SftpAuthRequest) (*SftpAu
}
return response, nil
}
}

View File

@@ -1,60 +0,0 @@
package cmd
import (
"github.com/pterodactyl/wings/config"
"os"
"path/filepath"
)
// We've gone through a couple of iterations of where the configuration is stored. This
// helpful little function will look through the three areas it might have ended up, and
// return it.
//
// We only run this if the configuration flag for the instance is not actually passed in
// via the command line. Once found, the configuration is moved into the expected default
// location. Only errors are returned from this function, you can safely assume that after
// running this the configuration can be found in the correct default location.
func RelocateConfiguration() error {
var match string
check := []string{
config.DefaultLocation,
"/var/lib/pterodactyl/config.yml",
"/etc/wings/config.yml",
}
// Loop over all of the configuration paths, and return which one we found, if
// any.
for _, p := range check {
if s, err := os.Stat(p); err != nil {
if !os.IsNotExist(err) {
return err
}
} else if !s.IsDir() {
match = p
break
}
}
// Just return a generic not exist error at this point if we didn't have a match, this
// will allow the caller to handle displaying a more friendly error to the user. If we
// did match in the default location, go ahead and return successfully.
if match == "" {
return os.ErrNotExist
} else if match == config.DefaultLocation {
return nil
}
// The rest of this function simply creates the new default location and moves the
// old configuration file over to the new location, then sets the permissions on the
// file correctly so that only the user running this process can read it.
p, _ := filepath.Split(config.DefaultLocation)
if err := os.MkdirAll(p, 0755); err != nil {
return err
}
if err := os.Rename(match, config.DefaultLocation); err != nil {
return err
}
return os.Chmod(config.DefaultLocation, 0600)
}

View File

@@ -1,183 +0,0 @@
package cmd
import (
"crypto/tls"
"encoding/json"
"fmt"
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/pterodactyl/wings/config"
"github.com/spf13/cobra"
"io/ioutil"
"net/http"
"net/url"
"os"
"path"
"regexp"
"time"
)
var (
configureArgs struct {
PanelURL string
Token string
ConfigPath string
Node string
Override bool
AllowInsecure bool
}
)
var nodeIdRegex = regexp.MustCompile(`^(\d+)$`)
var configureCmd = &cobra.Command{
Use: "configure",
Short: "Use a token to configure wings automatically",
Run: configureCmdRun,
}
func init() {
configureCmd.PersistentFlags().StringVarP(&configureArgs.PanelURL, "panel-url", "p", "", "The base URL for this daemon's panel")
configureCmd.PersistentFlags().StringVarP(&configureArgs.Token, "token", "t", "", "The API key to use for fetching node information")
configureCmd.PersistentFlags().StringVarP(&configureArgs.Node, "node", "n", "", "The ID of the node which will be connected to this daemon")
configureCmd.PersistentFlags().StringVarP(&configureArgs.ConfigPath, "config-path", "c", config.DefaultLocation, "The path where the configuration file should be made")
configureCmd.PersistentFlags().BoolVar(&configureArgs.Override, "override", false, "Set to true to override an existing configuration for this node")
configureCmd.PersistentFlags().BoolVar(&configureArgs.AllowInsecure, "allow-insecure", false, "Set to true to disable certificate checking")
}
func configureCmdRun(cmd *cobra.Command, args []string) {
if configureArgs.AllowInsecure {
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
InsecureSkipVerify: true,
}
}
if _, err := os.Stat(configureArgs.ConfigPath); err == nil && !configureArgs.Override {
survey.AskOne(&survey.Confirm{Message: "Override existing configuration file"}, &configureArgs.Override)
if !configureArgs.Override {
fmt.Println("Aborting process; a configuration file already exists for this node.")
os.Exit(1)
}
} else if err != nil && !os.IsNotExist(err) {
panic(err)
}
var questions []*survey.Question
if configureArgs.PanelURL == "" {
questions = append(questions, &survey.Question{
Name: "PanelURL",
Prompt: &survey.Input{Message: "Panel URL: "},
Validate: func(ans interface{}) error {
if str, ok := ans.(string); ok {
_, err := url.ParseRequestURI(str)
return err
}
return nil
},
})
}
if configureArgs.Token == "" {
questions = append(questions, &survey.Question{
Name: "Token",
Prompt: &survey.Input{Message: "API Token: "},
Validate: func(ans interface{}) error {
if str, ok := ans.(string); ok {
if len(str) == 0 {
return fmt.Errorf("please provide a valid authentication token")
}
}
return nil
},
})
}
if configureArgs.Node == "" {
questions = append(questions, &survey.Question{
Name: "Node",
Prompt: &survey.Input{Message: "Node ID: "},
Validate: func(ans interface{}) error {
if str, ok := ans.(string); ok {
if !nodeIdRegex.Match([]byte(str)) {
return fmt.Errorf("please provide a valid authentication token")
}
}
return nil
},
})
}
if err := survey.Ask(questions, &configureArgs); err != nil {
if err == terminal.InterruptErr {
return
}
panic(err)
}
c := &http.Client{
Timeout: time.Second * 30,
}
req, err := getRequest()
if err != nil {
panic(err)
}
fmt.Printf("%+v", req.Header)
fmt.Printf(req.URL.String())
res, err := c.Do(req)
if err != nil {
fmt.Println("Failed to fetch configuration from the panel.\n", err.Error())
os.Exit(1)
}
defer res.Body.Close()
if res.StatusCode == http.StatusForbidden || res.StatusCode == http.StatusUnauthorized {
fmt.Println("The authentication credentials provided were not valid.")
os.Exit(1)
} else if res.StatusCode != http.StatusOK {
b, _ := ioutil.ReadAll(res.Body)
fmt.Println("An error occurred while processing this request.\n", string(b))
os.Exit(1)
}
b, err := ioutil.ReadAll(res.Body)
cfg, err := config.NewFromPath(configPath)
if err != nil {
panic(err)
}
if err := json.Unmarshal(b, cfg); err != nil {
panic(err)
}
if err = cfg.WriteToDisk(); err != nil {
panic(err)
}
fmt.Println("Successfully configured wings.")
}
func getRequest() (*http.Request, error) {
u, err := url.Parse(configureArgs.PanelURL)
if err != nil {
panic(err)
}
u.Path = path.Join(u.Path, fmt.Sprintf("api/application/nodes/%s/configuration", configureArgs.Node))
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
r.Header.Set("Accept", "application/vnd.pterodactyl.v1+json")
r.Header.Set("Content-Type", "application/json")
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", configureArgs.Token))
return r, nil
}

View File

@@ -1,242 +0,0 @@
package cmd
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/pterodactyl/wings/environment"
"io"
"io/ioutil"
"net/http"
"net/url"
"os/exec"
"path"
"strconv"
"strings"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/docker/cli/components/engine/pkg/parsers/operatingsystem"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/parsers/kernel"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/system"
"github.com/spf13/cobra"
)
const DefaultHastebinUrl = "https://ptero.co"
const DefaultLogLines = 200
var (
diagnosticsArgs struct {
IncludeEndpoints bool
IncludeLogs bool
ReviewBeforeUpload bool
HastebinURL string
LogLines int
}
)
var diagnosticsCmd = &cobra.Command{
Use: "diagnostics",
Short: "Collect diagnostics information.",
Run: diagnosticsCmdRun,
}
func init() {
diagnosticsCmd.PersistentFlags().StringVar(&diagnosticsArgs.HastebinURL, "hastebin-url", DefaultHastebinUrl, "The url of the hastebin instance to use.")
diagnosticsCmd.PersistentFlags().IntVar(&diagnosticsArgs.LogLines, "log-lines", DefaultLogLines, "The number of log lines to include in the report")
}
// diagnosticsCmdRun collects diagnostics about wings, it's configuration and the node.
// We collect:
// - wings and docker versions
// - relevant parts of daemon configuration
// - the docker debug output
// - running docker containers
// - logs
func diagnosticsCmdRun(cmd *cobra.Command, args []string) {
questions := []*survey.Question{
{
Name: "IncludeEndpoints",
Prompt: &survey.Confirm{Message: "Do you want to include endpoints (i.e. the FQDN/IP of your panel)?", Default: false},
},
{
Name: "IncludeLogs",
Prompt: &survey.Confirm{Message: "Do you want to include the latest logs?", Default: true},
},
{
Name: "ReviewBeforeUpload",
Prompt: &survey.Confirm{
Message: "Do you want to review the collected data before uploading to hastebin.com?",
Help: "The data, especially the logs, might contain sensitive information, so you should review it. You will be asked again if you want to upload.",
Default: true,
},
},
}
if err := survey.Ask(questions, &diagnosticsArgs); err != nil {
if err == terminal.InterruptErr {
return
}
panic(err)
}
dockerVersion, dockerInfo, dockerErr := getDockerInfo()
_ = dockerInfo
output := &strings.Builder{}
fmt.Fprintln(output, "Pterodactyl Wings - Diagnostics Report")
printHeader(output, "Versions")
fmt.Fprintln(output, " wings:", system.Version)
if dockerErr == nil {
fmt.Fprintln(output, "Docker:", dockerVersion.Version)
}
if v, err := kernel.GetKernelVersion(); err == nil {
fmt.Fprintln(output, "Kernel:", v)
}
if os, err := operatingsystem.GetOperatingSystem(); err == nil {
fmt.Fprintln(output, " OS:", os)
}
printHeader(output, "Wings Configuration")
cfg, err := config.ReadConfiguration(config.DefaultLocation)
if cfg != nil {
fmt.Fprintln(output, " Panel Location:", redact(cfg.PanelLocation))
fmt.Fprintln(output, "")
fmt.Fprintln(output, " Internal Webserver:", redact(cfg.Api.Host), ":", cfg.Api.Port)
fmt.Fprintln(output, " SSL Enabled:", cfg.Api.Ssl.Enabled)
fmt.Fprintln(output, " SSL Certificate:", redact(cfg.Api.Ssl.CertificateFile))
fmt.Fprintln(output, " SSL Key:", redact(cfg.Api.Ssl.KeyFile))
fmt.Fprintln(output, "")
fmt.Fprintln(output, " SFTP Server:", redact(cfg.System.Sftp.Address), ":", cfg.System.Sftp.Port)
fmt.Fprintln(output, " SFTP Read-Only:", cfg.System.Sftp.ReadOnly)
fmt.Fprintln(output, "")
fmt.Fprintln(output, " Root Directory:", cfg.System.RootDirectory)
fmt.Fprintln(output, " Logs Directory:", cfg.System.LogDirectory)
fmt.Fprintln(output, " Data Directory:", cfg.System.Data)
fmt.Fprintln(output, " Archive Directory:", cfg.System.ArchiveDirectory)
fmt.Fprintln(output, " Backup Directory:", cfg.System.BackupDirectory)
fmt.Fprintln(output, "")
fmt.Fprintln(output, " Username:", cfg.System.Username)
fmt.Fprintln(output, " Server Time:", time.Now().Format(time.RFC1123Z))
fmt.Fprintln(output, " Debug Mode:", cfg.Debug)
} else {
fmt.Println("Failed to load configuration.", err)
}
printHeader(output, "Docker: Info")
fmt.Fprintln(output, "Server Version:", dockerInfo.ServerVersion)
fmt.Fprintln(output, "Storage Driver:", dockerInfo.Driver)
if dockerInfo.DriverStatus != nil {
for _, pair := range dockerInfo.DriverStatus {
fmt.Fprintf(output, " %s: %s\n", pair[0], pair[1])
}
}
if dockerInfo.SystemStatus != nil {
for _, pair := range dockerInfo.SystemStatus {
fmt.Fprintf(output, " %s: %s\n", pair[0], pair[1])
}
}
fmt.Fprintln(output, "LoggingDriver:", dockerInfo.LoggingDriver)
fmt.Fprintln(output, " CgroupDriver:", dockerInfo.CgroupDriver)
if len(dockerInfo.Warnings) > 0 {
for _, w := range dockerInfo.Warnings {
fmt.Fprintln(output, w)
}
}
printHeader(output, "Docker: Running Containers")
c := exec.Command("docker", "ps")
if co, err := c.Output(); err == nil {
output.Write(co)
} else {
fmt.Fprint(output, "Couldn't list containers: ", err)
}
printHeader(output, "Latest Wings Logs")
if diagnosticsArgs.IncludeLogs {
p := "/var/log/pterodactyl/wings.log"
if cfg != nil {
p = path.Join(cfg.System.LogDirectory, "wings.log")
}
if c, err := exec.Command("tail", "-n", strconv.Itoa(diagnosticsArgs.LogLines), p).Output(); err != nil {
fmt.Fprintln(output, "No logs found or an error occurred.")
} else {
fmt.Fprintf(output, "%s\n", string(c))
}
} else {
fmt.Fprintln(output, "Logs redacted.")
}
fmt.Println("\n--------------- generated report ---------------")
fmt.Println(output.String())
fmt.Print("--------------- end of report ---------------\n\n")
upload := !diagnosticsArgs.ReviewBeforeUpload
if !upload {
survey.AskOne(&survey.Confirm{Message: "Upload to " + diagnosticsArgs.HastebinURL + "?", Default: false}, &upload)
}
if upload {
url, err := uploadToHastebin(diagnosticsArgs.HastebinURL, output.String())
if err == nil {
fmt.Println("Your report is available here: ", url)
}
}
}
func getDockerInfo() (types.Version, types.Info, error) {
cli, err := environment.DockerClient()
if err != nil {
return types.Version{}, types.Info{}, err
}
dockerVersion, err := cli.ServerVersion(context.Background())
if err != nil {
return types.Version{}, types.Info{}, err
}
dockerInfo, err := cli.Info(context.Background())
if err != nil {
return types.Version{}, types.Info{}, err
}
return dockerVersion, dockerInfo, nil
}
func uploadToHastebin(hbUrl, content string) (string, error) {
r := strings.NewReader(content)
u, err := url.Parse(hbUrl)
if err != nil {
return "", err
}
u.Path = path.Join(u.Path, "documents")
res, err := http.Post(u.String(), "plain/text", r)
if err != nil || res.StatusCode != 200 {
fmt.Println("Failed to upload report to ", u.String(), err)
return "", err
}
pres := make(map[string]interface{})
body, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println("Failed to parse response.", err)
return "", err
}
json.Unmarshal(body, &pres)
if key, ok := pres["key"].(string); ok {
u, _ := url.Parse(hbUrl)
u.Path = path.Join(u.Path, key)
return u.String(), nil
}
return "", errors.New("failed to find key in response")
}
func redact(s string) string {
if !diagnosticsArgs.IncludeEndpoints {
return "{redacted}"
}
return s
}
func printHeader(w io.Writer, title string) {
fmt.Fprintln(w, "\n|\n|", title)
fmt.Fprintln(w, "| ------------------------------")
}

View File

@@ -1,433 +0,0 @@
package cmd
import (
"crypto/tls"
"fmt"
"github.com/NYTimes/logrotate"
"github.com/apex/log/handlers/multi"
"github.com/docker/docker/client"
"github.com/gammazero/workerpool"
"golang.org/x/crypto/acme"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"github.com/apex/log"
"github.com/mitchellh/colorstring"
"github.com/pterodactyl/wings/loggers/cli"
"golang.org/x/crypto/acme/autocert"
"github.com/pkg/errors"
"github.com/pkg/profile"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/router"
"github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/sftp"
"github.com/pterodactyl/wings/system"
"github.com/spf13/cobra"
)
var configPath = config.DefaultLocation
var debug = false
var shouldRunProfiler = false
var useAutomaticTls = false
var tlsHostname = ""
var showVersion = false
var root = &cobra.Command{
Use: "wings",
Short: "The wings of the pterodactyl game management panel",
Long: ``,
PreRun: func(cmd *cobra.Command, args []string) {
if useAutomaticTls && len(tlsHostname) == 0 {
fmt.Println("A TLS hostname must be provided when running wings with automatic TLS, e.g.:\n\n ./wings --auto-tls --tls-hostname my.example.com")
os.Exit(1)
}
},
Run: rootCmdRun,
}
func init() {
root.PersistentFlags().BoolVar(&showVersion, "version", false, "show the version and exit")
root.PersistentFlags().StringVar(&configPath, "config", config.DefaultLocation, "set the location for the configuration file")
root.PersistentFlags().BoolVar(&debug, "debug", false, "pass in order to run wings in debug mode")
root.PersistentFlags().BoolVar(&shouldRunProfiler, "profile", false, "pass in order to profile wings")
root.PersistentFlags().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.AddCommand(configureCmd)
root.AddCommand(diagnosticsCmd)
}
// Get the configuration path based on the arguments provided.
func readConfiguration() (*config.Configuration, error) {
var p = configPath
if !strings.HasPrefix(p, "/") {
d, err := os.Getwd()
if err != nil {
return nil, err
}
p = path.Clean(path.Join(d, configPath))
}
if s, err := os.Stat(p); err != nil {
return nil, errors.WithStack(err)
} else if s.IsDir() {
return nil, errors.New("cannot use directory as configuration file path")
}
return config.ReadConfiguration(p)
}
func rootCmdRun(*cobra.Command, []string) {
if showVersion {
fmt.Println(system.Version)
os.Exit(0)
}
if shouldRunProfiler {
defer profile.Start().Stop()
}
// Only attempt configuration file relocation if a custom location has not
// been specified in the command startup.
if configPath == config.DefaultLocation {
if err := RelocateConfiguration(); err != nil {
if os.IsNotExist(err) {
exitWithConfigurationNotice()
}
panic(err)
}
}
c, err := readConfiguration()
if err != nil {
panic(err)
}
if debug {
c.Debug = true
}
printLogo()
if err := configureLogging(c.System.LogDirectory, c.Debug); err != nil {
panic(err)
}
log.WithField("path", c.GetPath()).Info("loading configuration from path")
if c.Debug {
log.Debug("running in debug mode")
log.Warn("certificate checking is disabled")
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
InsecureSkipVerify: true,
}
}
config.Set(c)
config.SetDebugViaFlag(debug)
if err := c.System.ConfigureTimezone(); err != nil {
log.WithField("error", err).Fatal("failed to detect system timezone or use supplied configuration value")
return
}
log.WithField("timezone", c.System.Timezone).Info("configured wings with system timezone")
if err := c.System.ConfigureDirectories(); err != nil {
log.WithField("error", err).Fatal("failed to configure system directories for pterodactyl")
return
}
if err := c.System.EnableLogRotation(); err != nil {
log.WithField("error", err).Fatal("failed to configure log rotation on the system")
return
}
log.WithField("username", c.System.Username).Info("checking for pterodactyl system user")
if su, err := c.EnsurePterodactylUser(); err != nil {
log.WithField("error", err).Fatal("failed to create pterodactyl system user")
return
} else {
log.WithFields(log.Fields{
"username": su.Username,
"uid": su.Uid,
"gid": su.Gid,
}).Info("configured system user successfully")
}
if err := server.LoadDirectory(); err != nil {
log.WithField("error", err).Fatal("failed to load server configurations")
return
}
if err := environment.ConfigureDocker(&c.Docker); err != nil {
log.WithField("error", err).Fatal("failed to configure docker environment")
return
}
if err := c.WriteToDisk(); err != nil {
log.WithField("error", err).Error("failed to save configuration to disk")
}
// Just for some nice log output.
for _, s := range server.GetServers().All() {
log.WithField("server", s.Id()).Info("loaded configuration for server")
}
states, err := server.CachedServerStates()
if err != nil {
log.WithField("error", errors.WithStack(err)).Error("failed to retrieve locally cached server states from disk, assuming all servers in offline state")
}
// Create a new workerpool that limits us to 4 servers being bootstrapped at a time
// on Wings. This allows us to ensure the environment exists, write configurations,
// and reboot processes without causing a slow-down due to sequential booting.
pool := workerpool.New(4)
for _, serv := range server.GetServers().All() {
s := serv
pool.Submit(func() {
s.Log().Info("configuring server environment and restoring to previous state")
var st string
if state, exists := states[s.Id()]; exists {
st = state
}
r, err := s.Environment.IsRunning()
// We ignore missing containers because we don't want to actually block booting of wings at this
// point. If we didn't do this and you pruned all of the images and then started wings you could
// end up waiting a long period of time for all of the images to be re-pulled on Wings boot rather
// than when the server itself is started.
if err != nil && !client.IsErrNotFound(err) {
s.Log().WithField("error", err).Error("error checking server environment status")
}
// Check if the server was previously running. If so, attempt to start the server now so that Wings
// can pick up where it left off. If the environment does not exist at all, just create it and then allow
// the normal flow to execute.
//
// This does mean that booting wings after a catastrophic machine crash and wiping out the Docker images
// as a result will result in a slow boot.
if !r && (st == environment.ProcessRunningState || st == environment.ProcessStartingState) {
if err := s.HandlePowerAction(server.PowerActionStart); err != nil {
s.Log().WithField("error", errors.WithStack(err)).Warn("failed to return server to running state")
}
} else if r || (!r && s.IsRunning()) {
// If the server is currently running on Docker, mark the process as being in that state.
// We never want to stop an instance that is currently running external from Wings since
// that is a good way of keeping things running even if Wings gets in a very corrupted state.
//
// This will also validate that a server process is running if the last tracked state we have
// is that it was running, but we see that the container process is not currently running.
s.Log().Info("detected server is running, re-attaching to process...")
s.SetState(environment.ProcessRunningState)
if err := s.Environment.Attach(); err != nil {
s.Log().WithField("error", errors.WithStack(err)).Warn("failed to attach to running server environment")
}
return
}
// 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)
})
}
// Wait until all of the servers are ready to go before we fire up the SFTP and HTTP servers.
pool.StopWait()
// Initialize the SFTP server.
if err := sftp.Initialize(c.System); err != nil {
log.WithError(err).Fatal("failed to initialize the sftp server")
return
}
// Ensure the archive directory exists.
if err := os.MkdirAll(c.System.ArchiveDirectory, 0755); err != nil {
log.WithField("error", err).Error("failed to create archive directory")
}
// Ensure the backup directory exists.
if err := os.MkdirAll(c.System.BackupDirectory, 0755); err != nil {
log.WithField("error", err).Error("failed to create backup directory")
}
log.WithFields(log.Fields{
"use_ssl": c.Api.Ssl.Enabled,
"use_auto_tls": useAutomaticTls && len(tlsHostname) > 0,
"host_address": c.Api.Host,
"host_port": c.Api.Port,
}).Info("configuring internal webserver")
// Configure the router.
r := router.Configure()
s := &http.Server{
Addr: fmt.Sprintf("%s:%d", c.Api.Host, c.Api.Port),
Handler: r,
TLSConfig: &tls.Config{
NextProtos: []string{
"h2", // enable HTTP/2
"http/1.1",
},
// https://blog.cloudflare.com/exposing-go-on-the-internet
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
},
PreferServerCipherSuites: true,
MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS13,
CurvePreferences: []tls.CurveID{
tls.X25519,
tls.CurveP256,
},
// END https://blog.cloudflare.com/exposing-go-on-the-internet
},
}
// Check if the server should run with TLS but using autocert.
if useAutomaticTls && len(tlsHostname) > 0 {
m := autocert.Manager{
Prompt: autocert.AcceptTOS,
Cache: autocert.DirCache(path.Join(c.System.RootDirectory, "/.tls-cache")),
HostPolicy: autocert.HostWhitelist(tlsHostname),
}
log.WithField("hostname", tlsHostname).
Info("webserver is now listening with auto-TLS enabled; certificates will be automatically generated by Let's Encrypt")
// Hook autocert into the main http server.
s.TLSConfig.GetCertificate = m.GetCertificate
s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, acme.ALPNProto) // enable tls-alpn ACME challenges
// Start the autocert server.
go func() {
if err := http.ListenAndServe(":http", m.HTTPHandler(nil)); err != nil {
log.WithError(err).Error("failed to serve autocert http server")
}
}()
// Start the main http server with TLS using autocert.
if err := s.ListenAndServeTLS("", ""); err != nil {
log.WithFields(log.Fields{"auto_tls": true, "tls_hostname": tlsHostname, "error": err}).
Fatal("failed to configure HTTP server using auto-tls")
os.Exit(1)
}
return
}
// Check if main http server should run with TLS.
if c.Api.Ssl.Enabled {
if err := s.ListenAndServeTLS(c.Api.Ssl.CertificateFile, c.Api.Ssl.KeyFile); err != nil {
log.WithFields(log.Fields{"auto_tls": false, "error": err}).Fatal("failed to configure HTTPS server")
os.Exit(1)
}
return
}
// Run the main http server without TLS.
s.TLSConfig = nil
if err := s.ListenAndServe(); err != nil {
log.WithField("error", err).Fatal("failed to configure HTTP server")
os.Exit(1)
}
}
// Execute calls cobra to handle cli commands
func Execute() error {
return root.Execute()
}
// Configures the global logger for Zap so that we can call it from any location
// in the code without having to pass around a logger instance.
func configureLogging(logDir string, debug bool) error {
if err := os.MkdirAll(path.Join(logDir, "/install"), 0700); err != nil {
return errors.WithStack(err)
}
p := filepath.Join(logDir, "/wings.log")
w, err := logrotate.NewFile(p)
if err != nil {
panic(errors.Wrap(err, "failed to open process log file"))
}
if debug {
log.SetLevel(log.DebugLevel)
} else {
log.SetLevel(log.InfoLevel)
}
log.SetHandler(multi.New(
cli.Default,
cli.New(w.File, false),
))
log.WithField("path", p).Info("writing log files to disk")
return nil
}
// Prints the wings logo, nothing special here!
func printLogo() {
fmt.Printf(colorstring.Color(`
____
__ [blue][bold]Pterodactyl[reset] _____/___/_______ _______ ______
\_____\ \/\/ / / / __ / ___/
\___\ / / / / /_/ /___ /
\___/\___/___/___/___/___ /______/
/_______/ [bold]v%s[reset]
Copyright © 2018 - 2020 Dane Everitt & Contributors
Website: https://pterodactyl.io
Source: https://github.com/pterodactyl/wings
License: https://github.com/pterodactyl/wings/blob/develop/LICENSE
This software is made available under the terms of the MIT license.
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.%s`), system.Version, "\n\n")
}
func exitWithConfigurationNotice() {
fmt.Print(colorstring.Color(`
[_red_][white][bold]Error: Configuration File Not Found[reset]
Wings was not able to locate your configuration file, and therefore is not
able to complete its boot process.
Please ensure you have copied your instance configuration file into
the default location, or have provided the --config flag to use a
custom location.
Default Location: /etc/pterodactyl/config.yml
[yellow]This is not a bug with this software. Please do not make a bug report
for this issue, it will be closed.[reset]
`))
os.Exit(1)
}

View File

@@ -2,50 +2,28 @@ package config
import (
"fmt"
"github.com/cobaugh/osrelease"
"github.com/creasty/defaults"
"github.com/gbrlsnchs/jwt/v3"
"github.com/pkg/errors"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
"io/ioutil"
"os"
"os/exec"
"os/user"
"path"
"regexp"
"strconv"
"strings"
"sync"
)
const DefaultLocation = "/etc/pterodactyl/config.yml"
type Configuration struct {
sync.RWMutex `json:"-" yaml:"-"`
// The location from which this configuration instance was instantiated.
path string
// Locker specific to writing the configuration to the disk, this happens
// in areas that might already be locked so we don't want to crash the process.
writeLock sync.Mutex
// Determines if wings should be running in debug mode. This value is ignored
// if the debug flag is passed through the command line arguments.
Debug bool
// A unique identifier for this node in the Panel.
Uuid string
// An identifier for the token which must be included in any requests to the panel
// so that the token can be looked up correctly.
AuthenticationTokenId string `json:"token_id" yaml:"token_id"`
// The token used when performing operations. Requests to this instance must
// validate against it.
AuthenticationToken string `json:"token" yaml:"token"`
Api ApiConfiguration `json:"api" yaml:"api"`
System SystemConfiguration `json:"system" yaml:"system"`
Docker DockerConfiguration `json:"docker" yaml:"docker"`
Api ApiConfiguration
System SystemConfiguration
Docker DockerConfiguration
// 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
@@ -55,32 +33,142 @@ type Configuration struct {
// Defines internal throttling configurations for server processes to prevent
// someone from running an endless loop that spams data to logs.
Throttles ConsoleThrottles
Throttles struct {
// The number of data overage warnings (inclusive) that can accumulate
// before a process is terminated.
KillAtCount int `default:"5" yaml:"kill_at_count"`
// The number of seconds that must elapse before the internal counter
// begins decrementing warnings assigned to a process that is outputting
// too much data.
DecaySeconds int `default:"10" yaml:"decay"`
// The total number of bytes allowed to be output by a server process
// per interval.
BytesPerInterval int `default:"4096" yaml:"bytes"`
// The amount of time that should lapse between data output throttle
// checks. This should be defined in milliseconds.
CheckInterval int `defauly:"100" yaml:"check_interval"`
}
// The location where the panel is running that this daemon should connect to
// to collect data and send events.
PanelLocation string `json:"remote" yaml:"remote"`
PanelLocation string `yaml:"remote"`
// AllowedMounts is a list of allowed host-system mount points.
// This is required to have the "Server Mounts" feature work properly.
AllowedMounts []string `json:"-" yaml:"allowed_mounts"`
// The token used when performing operations. Requests to this instance must
// validate aganist it.
AuthenticationToken string `yaml:"token"`
}
// AllowedOrigins is a list of allowed request origins.
// The Panel URL is automatically allowed, this is only needed for adding
// additional origins.
AllowedOrigins []string `json:"allowed_origins" yaml:"allowed_origins"`
// Defines basic system configuration settings.
type SystemConfiguration struct {
// Directory where the server data is stored at.
Data string `default:"/srv/daemon-data" yaml:"data"`
// The user that should own all of the server files, and be used for containers.
Username string `default:"pterodactyl" yaml:"username"`
// Definitions for the user that gets created to ensure that we can quickly access
// this information without constantly having to do a system lookup.
User struct {
Uid int
Gid int
}
// Determines wether or not server data should be synced when the Daemon is started.
// If set to false, data will only be synced when a server process is started, or
// detected as started when booting.
SyncServersOnBoot bool `default:"true" yaml:"sync_servers_on_boot"`
// The path to the system's timezone file that will be mounted into running Docker containers.
TimezonePath string `yaml:"timezone_path"`
// Determines if permissions for a server should be set automatically on
// daemon boot. This can take a long time on systems with many servers, or on
// systems with servers containing thousands of files.
//
// Setting this to true by default helps us avoid a lot of support requests
// from people that keep trying to move files around as a root user leading
// to server permission issues.
//
// In production and heavy use environments where boot speed is essential,
// this should be set to false as servers will self-correct permissions on
// boot anyways.
SetPermissionsOnBoot bool `default:"true" yaml:"set_permissions_on_boot"`
// 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"`
Sftp *SftpConfiguration `yaml:"sftp"`
}
// Defines the configuration of the internal SFTP server.
type SftpConfiguration struct {
// If set to false, the internal SFTP server will not be booted and you will need
// to run the SFTP server independent of this program.
UseInternalSystem bool `default:"true" yaml:"use_internal"`
// If set to true disk checking will not be performed. This will prevent the SFTP
// server from checking the total size of a directory when uploading files.
DisableDiskChecking bool `default:"false" yaml:"disable_disk_checking"`
// The bind address of the SFTP server.
Address string `default:"0.0.0.0" json:"bind_address" yaml:"bind_address"`
Address string `default:"0.0.0.0" yaml:"bind_address"`
// The bind port of the SFTP server.
Port int `default:"2022" json:"bind_port" yaml:"bind_port"`
Port int `default:"2022" yaml:"bind_port"`
// If set to true, no write actions will be allowed on the SFTP server.
ReadOnly bool `default:"false" yaml:"read_only"`
}
type dockerNetworkInterfaces struct {
V4 struct {
Subnet string `default:"172.18.0.0/16"`
Gateway string `default:"172.18.0.1"`
}
V6 struct {
Subnet string `default:"fdba:17c8:6c94::/64"`
Gateway string `default:"fdba:17c8:6c94::1011"`
}
}
type DockerNetworkConfiguration struct {
// The interface that should be used to create the network. Must not conflict
// with any other interfaces in use by Docker or on the system.
Interface string `default:"172.18.0.1"`
// The name of the network to use. If this network already exists it will not
// be created. If it is not found, a new network will be created using the interface
// defined.
Name string `default:"pterodactyl_nw"`
ISPN bool `default:"false" yaml:"ispn"`
Driver string `default:"bridge"`
IsInternal bool `default:"false" yaml:"is_internal"`
EnableICC bool `default:"true" yaml:"enable_icc"`
Interfaces dockerNetworkInterfaces `yaml:"interfaces"`
}
// Defines the docker configuration used by the daemon when interacting with
// containers and networks on the system.
type DockerConfiguration struct {
// Network configuration that should be used when creating a new network
// for containers run through the daemon.
Network DockerNetworkConfiguration `yaml:"network"`
// If true, container images will be updated when a server starts if there
// is an update available. If false the daemon will not attempt updates and will
// defer to the host system to manage image updates.
UpdateImages bool `default:"true" yaml:"update_images"`
// The location of the Docker socket.
Socket string `default:"/var/run/docker.sock"`
// Defines the location of the timezone file on the host system that should
// be mounted into the created containers so that they all use the same time.
TimezonePath string `yaml:"timezone_path"`
}
// Defines the configuration for the internal API that is exposed by the
// daemon webserver.
type ApiConfiguration struct {
@@ -93,12 +181,12 @@ type ApiConfiguration struct {
// SSL configuration for the daemon.
Ssl struct {
Enabled bool `default:"false"`
CertificateFile string `json:"cert" yaml:"cert"`
KeyFile string `json:"key" yaml:"key"`
CertificateFile string `yaml:"cert"`
KeyFile string `yaml:"key"`
}
// The maximum size for files uploaded through the Panel in bytes.
UploadLimit int `default:"100" json:"upload_limit" yaml:"upload_limit"`
UploadLimit int `default:"100" yaml:"upload_limit"`
}
// Reads the configuration from the provided file and returns the configuration
@@ -111,15 +199,12 @@ func ReadConfiguration(path string) (*Configuration, error) {
c := new(Configuration)
// Configures the default values for many of the configuration options present
// in the structs. Values set in the configuration file take priority over the
// in the structs. Valkues set in the configuration file take priority over the
// default values.
if err := defaults.Set(c); err != nil {
return nil, err
}
// Track the location where we created this configuration.
c.unsafeSetPath(path)
// Replace environment variables within the configuration file with their
// values from the host system.
b = []byte(os.ExpandEnv(string(b)))
@@ -131,76 +216,23 @@ func ReadConfiguration(path string) (*Configuration, error) {
return c, nil
}
var mu sync.RWMutex
var _config *Configuration
var _jwtAlgo *jwt.HMACSHA
var _debugViaFlag bool
// Set the global configuration instance. This is a blocking operation such that
// anything trying to set a different configuration value, or read the configuration
// will be paused until it is complete.
// Set the global configuration instance.
func Set(c *Configuration) {
mu.Lock()
if _config == nil || _config.AuthenticationToken != c.AuthenticationToken {
_jwtAlgo = jwt.NewHS256([]byte(c.AuthenticationToken))
}
_config = c
mu.Unlock()
}
func SetDebugViaFlag(d bool) {
_debugViaFlag = d
}
// Get the global configuration instance. This is a read-safe operation that will block
// if the configuration is presently being modified.
// Get the global configuration instance.
func Get() *Configuration {
mu.RLock()
defer mu.RUnlock()
return _config
}
// Returns the in-memory JWT algorithm.
func GetJwtAlgorithm() *jwt.HMACSHA {
mu.RLock()
defer mu.RUnlock()
return _jwtAlgo
}
// Create a new struct and set the path where it should be stored.
func NewFromPath(path string) (*Configuration, error) {
c := new(Configuration)
if err := defaults.Set(c); err != nil {
return c, errors.WithStack(err)
}
c.unsafeSetPath(path)
return c, nil
}
// Sets the path where the configuration file is located on the server. This function should
// not be called except by processes that are generating the configuration such as the configuration
// command shipped with this software.
func (c *Configuration) unsafeSetPath(path string) {
c.Lock()
c.path = path
c.Unlock()
}
// Returns the path for this configuration file.
func (c *Configuration) GetPath() string {
c.RLock()
defer c.RUnlock()
return c.path
}
// Ensures that the Pterodactyl core user exists on the system. This user will be the
// owner of all data in the root data directory and is used as the user within containers.
//
@@ -214,35 +246,35 @@ func (c *Configuration) EnsurePterodactylUser() (*user.User, error) {
if err == nil {
return u, c.setSystemUser(u)
} else if _, ok := err.(user.UnknownUserError); !ok {
return nil, errors.WithStack(err)
return nil, err
}
sysName, err := getSystemName()
if err != nil {
return nil, errors.WithStack(err)
return nil, err
}
var command = fmt.Sprintf("useradd --system --no-create-home --shell /bin/false %s", c.System.Username)
// Alpine Linux is the only OS we currently support that doesn't work with the useradd command, so
// in those cases we just modify the command a bit to work as expected.
if strings.HasPrefix(sysName, "alpine") {
if strings.HasPrefix(sysName, "Alpine") {
command = fmt.Sprintf("adduser -S -D -H -G %[1]s -s /bin/false %[1]s", c.System.Username)
// We have to create the group first on Alpine, so do that here before continuing on
// to the user creation process.
if _, err := exec.Command("addgroup", "-S", c.System.Username).Output(); err != nil {
return nil, errors.WithStack(err)
if _, err := exec.Command("addgroup", "-s", c.System.Username).Output(); err != nil {
return nil, err
}
}
split := strings.Split(command, " ")
if _, err := exec.Command(split[0], split[1:]...).Output(); err != nil {
return nil, errors.WithStack(err)
return nil, err
}
if u, err := user.Lookup(c.System.Username); err != nil {
return nil, errors.WithStack(err)
return nil, err
} else {
return u, c.setSystemUser(u)
}
@@ -254,22 +286,74 @@ func (c *Configuration) setSystemUser(u *user.User) error {
uid, _ := strconv.Atoi(u.Uid)
gid, _ := strconv.Atoi(u.Gid)
c.Lock()
c.System.Username = u.Username
c.System.User.Uid = uid
c.System.User.Gid = gid
c.Unlock()
return c.WriteToDisk()
}
// Ensures that the configured data directory has the correct permissions assigned to
// all of the files and folders within.
func (c *Configuration) EnsureFilePermissions() error {
// Don't run this unless it is configured to be run. On large system this can often slow
// things down dramatically during the boot process.
if !c.System.SetPermissionsOnBoot {
return nil
}
r := regexp.MustCompile("^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$")
files, err := ioutil.ReadDir(c.System.Data)
if err != nil {
return err
}
su, err := user.Lookup(c.System.Username)
if err != nil {
return err
}
wg := new(sync.WaitGroup)
for _, file := range files {
wg.Add(1)
// Asynchronously run through the list of files and folders in the data directory. If
// the item is not a folder, or is not a folder that matches the expected UUIDv4 format
// skip over it.
//
// If we do have a positive match, run a chown aganist the directory.
go func(f os.FileInfo) {
defer wg.Done()
if !f.IsDir() || !r.MatchString(f.Name()) {
return
}
uid, _ := strconv.Atoi(su.Uid)
gid, _ := strconv.Atoi(su.Gid)
if err := os.Chown(path.Join(c.System.Data, f.Name()), uid, gid); err != nil {
zap.S().Warnw("failed to chown server directory", zap.String("directory", f.Name()), zap.Error(err))
}
}(file)
}
wg.Wait()
return nil
}
// Writes the configuration to the disk as a blocking operation by obtaining an exclusive
// lock on the file. This prevents something else from writing at the exact same time and
// leading to bad data conditions.
func (c *Configuration) WriteToDisk() error {
// Obtain an exclusive write against the configuration file.
c.writeLock.Lock()
defer c.writeLock.Unlock()
f, err := os.OpenFile("config.yml", os.O_WRONLY, os.ModeExclusive)
if err != nil {
return err
}
defer f.Close()
ccopy := *c
// If debugging is set with the flag, don't save that to the configuration file, otherwise
@@ -278,17 +362,13 @@ func (c *Configuration) WriteToDisk() error {
ccopy.Debug = false
}
if c.path == "" {
return errors.New("cannot write configuration, no path defined in struct")
}
b, err := yaml.Marshal(&ccopy)
if err != nil {
return errors.WithStack(err)
return err
}
if err := ioutil.WriteFile(c.GetPath(), b, 0644); err != nil {
return errors.WithStack(err)
if _, err := f.Write(b); err != nil {
return err
}
return nil
@@ -296,10 +376,12 @@ func (c *Configuration) WriteToDisk() error {
// Gets the system release name.
func getSystemName() (string, error) {
// use osrelease to get release version and ID
if release, err := osrelease.Read(); err != nil {
return "", errors.WithStack(err)
} else {
return release["ID"], nil
cmd := exec.Command("lsb_release", "-is")
b, err := cmd.Output()
if err != nil {
return "", err
}
return string(b), nil
}

View File

@@ -1,80 +0,0 @@
package config
import (
"encoding/base64"
"encoding/json"
"github.com/docker/docker/api/types"
"github.com/pkg/errors"
)
type dockerNetworkInterfaces struct {
V4 struct {
Subnet string `default:"172.18.0.0/16"`
Gateway string `default:"172.18.0.1"`
}
V6 struct {
Subnet string `default:"fdba:17c8:6c94::/64"`
Gateway string `default:"fdba:17c8:6c94::1011"`
}
}
type DockerNetworkConfiguration struct {
// The interface that should be used to create the network. Must not conflict
// with any other interfaces in use by Docker or on the system.
Interface string `default:"172.18.0.1" json:"interface" yaml:"interface"`
// The DNS settings for containers.
Dns []string `default:"[\"1.1.1.1\", \"1.0.0.1\"]"`
// The name of the network to use. If this network already exists it will not
// be created. If it is not found, a new network will be created using the interface
// defined.
Name string `default:"pterodactyl_nw"`
ISPN bool `default:"false" yaml:"ispn"`
Driver string `default:"bridge"`
Mode string `default:"pterodactyl_nw" yaml:"network_mode"`
IsInternal bool `default:"false" yaml:"is_internal"`
EnableICC bool `default:"true" yaml:"enable_icc"`
Interfaces dockerNetworkInterfaces `yaml:"interfaces"`
}
// Defines the docker configuration used by the daemon when interacting with
// containers and networks on the system.
type DockerConfiguration struct {
// Network configuration that should be used when creating a new network
// for containers run through the daemon.
Network DockerNetworkConfiguration `json:"network" yaml:"network"`
// Domainname is the Docker domainname for all containers.
Domainname string `default:"" json:"domainname" yaml:"domainname"`
// Registries .
Registries map[string]RegistryConfiguration `json:"registries" yaml:"registries"`
// The size of the /tmp directory when mounted into a container. Please be aware that Docker
// utilizes host memory for this value, and that we do not keep track of the space used here
// so avoid allocating too much to a server.
TmpfsSize uint `default:"100" json:"tmpfs_size" yaml:"tmpfs_size"`
}
// RegistryConfiguration .
type RegistryConfiguration struct {
Username string `yaml:"username"`
Password string `yaml:"password"`
}
// Base64 .
func (c RegistryConfiguration) Base64() (string, error) {
authConfig := types.AuthConfig{
Username: c.Username,
Password: c.Password,
}
b, err := json.Marshal(authConfig)
if err != nil {
return "", errors.WithStack(err)
}
return base64.URLEncoding.EncodeToString(b), nil
}

View File

@@ -1,225 +0,0 @@
package config
import (
"context"
"fmt"
"github.com/apex/log"
"github.com/pkg/errors"
"html/template"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"time"
)
// Defines basic system configuration settings.
type SystemConfiguration struct {
// The root directory where all of the pterodactyl data is stored at.
RootDirectory string `default:"/var/lib/pterodactyl" yaml:"root_directory"`
// Directory where logs for server installations and other wings events are logged.
LogDirectory string `default:"/var/log/pterodactyl" yaml:"log_directory"`
// Directory where the server data is stored at.
Data string `default:"/var/lib/pterodactyl/volumes" yaml:"data"`
// Directory where server archives for transferring will be stored.
ArchiveDirectory string `default:"/var/lib/pterodactyl/archives" yaml:"archive_directory"`
// Directory where local backups will be stored on the machine.
BackupDirectory string `default:"/var/lib/pterodactyl/backups" yaml:"backup_directory"`
// The user that should own all of the server files, and be used for containers.
Username string `default:"pterodactyl" yaml:"username"`
// The timezone for this Wings instance. This is detected by Wings automatically if possible,
// and falls back to UTC if not able to be detected. If you need to set this manually, that
// can also be done.
//
// This timezone value is passed into all containers created by Wings.
Timezone string `yaml:"timezone"`
// Definitions for the user that gets created to ensure that we can quickly access
// this information without constantly having to do a system lookup.
User struct {
Uid int
Gid int
}
// The amount of time in seconds that can elapse before a server's disk space calculation is
// considered stale and a re-check should occur. DANGER: setting this value too low can seriously
// impact system performance and cause massive I/O bottlenecks and high CPU usage for the Wings
// process.
DiskCheckInterval int64 `default:"150" yaml:"disk_check_interval"`
// Determines if Wings should detect a server that stops with a normal exit code of
// "0" as being crashed if the process stopped without any Wings interaction. E.g.
// the user did not press the stop button, but the process stopped cleanly.
DetectCleanExitAsCrash bool `default:"true" yaml:"detect_clean_exit_as_crash"`
// If set to true, file permissions for a server will be checked when the process is
// booted. This can cause boot delays if the server has a large amount of files. In most
// cases disabling this should not have any major impact unless external processes are
// frequently modifying a servers' files.
CheckPermissionsOnBoot bool `default:"true" yaml:"check_permissions_on_boot"`
// If set to false Wings will not attempt to write a log rotate configuration to the disk
// when it boots and one is not detected.
EnableLogRotate bool `default:"true" yaml:"enable_log_rotate"`
Sftp SftpConfiguration `yaml:"sftp"`
}
// Ensures that all of the system directories exist on the system. These directories are
// created so that only the owner can read the data, and no other users.
func (sc *SystemConfiguration) ConfigureDirectories() error {
log.WithField("path", sc.RootDirectory).Debug("ensuring root data directory exists")
if err := os.MkdirAll(sc.RootDirectory, 0700); err != nil {
return err
}
// There are a non-trivial number of users out there whose data directories are actually a
// symlink to another location on the disk. If we do not resolve that final destination at this
// point things will appear to work, but endless errors will be encountered when we try to
// verify accessed paths since they will all end up resolving outside the expected data directory.
//
// For the sake of automating away as much of this as possible, see if the data directory is a
// symlink, and if so resolve to its final real path, and then update the configuration to use
// that.
if d, err := filepath.EvalSymlinks(sc.Data); err != nil {
if !os.IsNotExist(err) {
return errors.WithStack(err)
}
} else if d != sc.Data {
sc.Data = d
}
log.WithField("path", sc.Data).Debug("ensuring server data directory exists")
if err := os.MkdirAll(sc.Data, 0700); err != nil {
return err
}
log.WithField("path", sc.ArchiveDirectory).Debug("ensuring archive data directory exists")
if err := os.MkdirAll(sc.ArchiveDirectory, 0700); err != nil {
return err
}
log.WithField("path", sc.BackupDirectory).Debug("ensuring backup data directory exists")
if err := os.MkdirAll(sc.BackupDirectory, 0700); err != nil {
return err
}
return nil
}
// Writes a logrotate file for wings to the system logrotate configuration directory if one
// exists and a logrotate file is not found. This allows us to basically automate away the log
// rotation for most installs, but also enable users to make modifications on their own.
func (sc *SystemConfiguration) EnableLogRotation() error {
// Do nothing if not enabled.
if sc.EnableLogRotate == false {
log.Info("skipping log rotate configuration, disabled in wings config file")
return nil
}
if st, err := os.Stat("/etc/logrotate.d"); err != nil && !os.IsNotExist(err) {
return errors.WithStack(err)
} else if (err != nil && os.IsNotExist(err)) || !st.IsDir() {
return nil
}
if _, err := os.Stat("/etc/logrotate.d/wings"); err != nil && !os.IsNotExist(err) {
return errors.WithStack(err)
} else if err == nil {
return nil
}
log.Info("no log rotation configuration found, system is configured to support it, adding file now")
// If we've gotten to this point it means the logrotate directory exists on the system
// but there is not a file for wings already. In that case, let us write a new file to
// it so files can be rotated easily.
f, err := os.Create("/etc/logrotate.d/wings")
if err != nil {
return errors.WithStack(err)
}
defer f.Close()
t, err := template.New("logrotate").Parse(`
{{.LogDirectory}}/wings.log {
size 10M
compress
delaycompress
dateext
maxage 7
missingok
notifempty
create 0640 {{.User.Uid}} {{.User.Gid}}
postrotate
killall -SIGHUP wings
endscript
}`)
if err != nil {
return errors.WithStack(err)
}
return errors.Wrap(t.Execute(f, sc), "failed to write logrotate file to disk")
}
// Returns the location of the JSON file that tracks server states.
func (sc *SystemConfiguration) GetStatesPath() string {
return path.Join(sc.RootDirectory, "states.json")
}
// Returns the location of the JSON file that tracks server states.
func (sc *SystemConfiguration) GetInstallLogPath() string {
return path.Join(sc.LogDirectory, "install/")
}
// Configures the timezone data for the configuration if it is currently missing. If
// a value has been set, this functionality will only run to validate that the timezone
// being used is valid.
func (sc *SystemConfiguration) ConfigureTimezone() error {
if sc.Timezone == "" {
if b, err := ioutil.ReadFile("/etc/timezone"); err != nil {
if !os.IsNotExist(err) {
return errors.Wrap(err, "failed to open /etc/timezone for automatic server timezone calibration")
}
ctx, _ := context.WithTimeout(context.Background(), time.Second * 5)
// Okay, file isn't found on this OS, we will try using timedatectl to handle this. If this
// command fails, exit, but if it returns a value use that. If no value is returned we will
// fall through to UTC to get Wings booted at least.
out, err := exec.CommandContext(ctx, "timedatectl").Output()
if err != nil {
log.WithField("error", err).Warn("failed to execute \"timedatectl\" to determine system timezone, falling back to UTC")
sc.Timezone = "UTC"
return nil
}
r := regexp.MustCompile(`Time zone: ([\w/]+)`)
matches := r.FindSubmatch(out)
if len(matches) != 2 || string(matches[1]) == "" {
log.Warn("failed to parse timezone from \"timedatectl\" output, falling back to UTC")
sc.Timezone = "UTC"
return nil
}
sc.Timezone = string(matches[1])
} else {
sc.Timezone = string(b)
}
}
sc.Timezone = regexp.MustCompile(`(?i)[^a-z_/]+`).ReplaceAllString(sc.Timezone, "")
_, err := time.LoadLocation(sc.Timezone)
return errors.Wrap(err, fmt.Sprintf("the supplied timezone %s is invalid", sc.Timezone))
}

View File

@@ -1,27 +0,0 @@
package config
type ConsoleThrottles struct {
// Whether or not the throttler is enabled for this instance.
Enabled bool `json:"enabled" yaml:"enabled" default:"true"`
// The total number of lines that can be output in a given LineResetInterval period before
// a warning is triggered and counted against the server.
Lines uint64 `json:"lines" yaml:"lines" default:"2000"`
// The total number of throttle activations that can accumulate before a server is considered
// to be breaching and will be stopped. This value is decremented by one every DecayInterval.
MaximumTriggerCount uint64 `json:"maximum_trigger_count" yaml:"maximum_trigger_count" default:"5"`
// The amount of time after which the number of lines processed is reset to 0. This runs in
// a constant loop and is not affected by the current console output volumes. By default, this
// will reset the processed line count back to 0 every 100ms.
LineResetInterval uint64 `json:"line_reset_interval" yaml:"line_reset_interval" default:"100"`
// The amount of time in milliseconds that must pass without an output warning being triggered
// before a throttle activation is decremented.
DecayInterval uint64 `json:"decay_interval" yaml:"decay_interval" default:"10000"`
// The amount of time that a server is allowed to be stopping for before it is terminated
// forfully if it triggers output throttles.
StopGracePeriod uint `json:"stop_grace_period" yaml:"stop_grace_period" default:"15"`
}

29
const.go Normal file
View File

@@ -0,0 +1,29 @@
package main
import "os"
const (
Version = "1.0.0-alpha.1"
// DefaultFilePerms are the file perms used for created files.
DefaultFilePerms os.FileMode = 0644
// DefaultFolderPerms are the file perms used for created folders.
DefaultFolderPerms os.FileMode = 0755
// ServersPath is the path of the servers within the configured DataPath.
ServersPath = "servers"
// ServerConfigFile is the filename of the server config file.
ServerConfigFile = "server.json"
// ServerDataPath is the path of the data of a single server.
ServerDataPath = "data"
// DockerContainerPrefix is the prefix used for naming Docker containers.
// It's also used to prefix the hostnames of the docker containers.
DockerContainerPrefix = "ptdl-"
// WSMaxMessages is the maximum number of messages that are sent in one transfer.
WSMaxMessages = 10
)

1
data/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
servers/*.yml

0
data/servers/.gitkeep Normal file
View File

View File

@@ -1,35 +0,0 @@
version: '3.5'
services:
daemon:
build: .
restart: always
networks:
- daemon0
ports:
- "8080:8080"
- "2022:2022"
tty: true
environment:
- "DEBUG=false"
- "TZ=UTC" # change to the three letter timezone of your choosing
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
- "/var/lib/docker/containers/:/var/lib/docker/containers/"
- "/etc/pterodactyl/:/etc/pterodactyl/"
- "/var/lib/pterodactyl/:/var/lib/pterodactyl/"
- "/var/log/pterodactyl/:/var/log/pterodactyl/"
- "/tmp/pterodactyl/:/tmp/pterodactyl/"
## you may need /srv/daemon-data if you are upgrading from an old daemon
## - "/srv/daemon-data/:/srv/daemon-data/"
## Required for ssl if you user let's encrypt. uncomment to use.
## - "/etc/letsencrypt/:/etc/letsencrypt/"
networks:
daemon0:
name: daemon0
driver: bridge
ipam:
config:
- subnet: "172.21.0.0/16"
driver_opts:
com.docker.network.bridge.name: daemon0

View File

@@ -1,49 +1,28 @@
package environment
package main
import (
"context"
"github.com/apex/log"
"strconv"
"sync"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/pterodactyl/wings/config"
"go.uber.org/zap"
)
var _cmu sync.Mutex
var _client *client.Client
// Return a Docker client to be used throughout the codebase. Once a client has been created it
// will be returned for all subsequent calls to this function.
func DockerClient() (*client.Client, error) {
_cmu.Lock()
defer _cmu.Unlock()
if _client != nil {
return _client, nil
}
_client, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
return _client, err
}
// Configures the required network for the docker environment.
func ConfigureDocker(c *config.DockerConfiguration) error {
func ConfigureDockerEnvironment(c *config.DockerConfiguration) error {
// Ensure the required docker network exists on the system.
cli, err := DockerClient()
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return err
}
resource, err := cli.NetworkInspect(context.Background(), c.Network.Name, types.NetworkInspectOptions{})
if err != nil && client.IsErrNotFound(err) {
log.Info("creating missing pterodactyl0 interface, this could take a few seconds...")
zap.S().Infow("creating missing pterodactyl0 interface, this could take a few seconds...")
return createDockerNetwork(cli, c)
} else if err != nil {
log.WithField("error", err).Fatal("failed to create required docker network for containers")
zap.S().Fatalw("failed to create required docker network for containers", zap.Error(err))
}
switch resource.Driver {
@@ -84,7 +63,7 @@ func createDockerNetwork(cli *client.Client, c *config.DockerConfiguration) erro
Options: map[string]string{
"encryption": "false",
"com.docker.network.bridge.default_bridge": "false",
"com.docker.network.bridge.enable_icc": strconv.FormatBool(c.Network.EnableICC),
"com.docker.network.bridge.enable_icc": "true",
"com.docker.network.bridge.enable_ip_masquerade": "true",
"com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
"com.docker.network.bridge.name": "pterodactyl0",

View File

@@ -1,101 +0,0 @@
package environment
import (
"fmt"
"github.com/docker/go-connections/nat"
"github.com/pterodactyl/wings/config"
"strconv"
)
// Defines the allocations available for a given server. When using the Docker environment
// driver these correspond to mappings for the container that allow external connections.
type Allocations struct {
// Defines the default allocation that should be used for this server. This is
// what will be used for {SERVER_IP} and {SERVER_PORT} when modifying configuration
// files or the startup arguments for a server.
DefaultMapping struct {
Ip string `json:"ip"`
Port int `json:"port"`
} `json:"default"`
// Mappings contains all of the ports that should be assigned to a given server
// attached to the IP they correspond to.
Mappings map[string][]int `json:"mappings"`
}
// Converts the server allocation mappings into a format that can be understood by Docker. While
// we do strive to support multiple environments, using Docker's standardized format for the
// bindings certainly makes life a little easier for managing things.
//
// You'll want to use DockerBindings() if you need to re-map 127.0.0.1 to the Docker interface.
func (a *Allocations) Bindings() nat.PortMap {
var out = nat.PortMap{}
for ip, ports := range a.Mappings {
for _, port := range ports {
// Skip over invalid ports.
if port < 1 || port > 65535 {
continue
}
binding := []nat.PortBinding{
{
HostIP: ip,
HostPort: strconv.Itoa(port),
},
}
out[nat.Port(fmt.Sprintf("%d/tcp", port))] = binding
out[nat.Port(fmt.Sprintf("%d/udp", port))] = binding
}
}
return out
}
// Returns the bindings for the server in a way that is supported correctly by Docker. This replaces
// any reference to 127.0.0.1 with the IP of the pterodactyl0 network interface which will allow the
// server to operate on a local address while still being accessible by other containers.
func (a *Allocations) DockerBindings() nat.PortMap {
iface := config.Get().Docker.Network.Interface
out := a.Bindings()
// Loop over all of the bindings for this container, and convert any that reference 127.0.0.1
// to use the pterodactyl0 network interface IP, as that is the true local for what people are
// trying to do when creating servers.
for p, binds := range out {
for i, alloc := range binds {
if alloc.HostIP != "127.0.0.1" {
continue
}
// If using ISPN just delete the local allocation from the server.
if config.Get().Docker.Network.ISPN {
out[p] = append(out[p][:i], out[p][i+1:]...)
} else {
out[p][i] = nat.PortBinding{
HostIP: iface,
HostPort: alloc.HostPort,
}
}
}
}
return out
}
// Converts the server allocation mappings into a PortSet that can be understood
// by Docker. This formatting is slightly different than "Bindings" as it should
// return an empty struct rather than a binding.
//
// To accomplish this, we'll just get the values from "DockerBindings" and then set them
// to empty structs. Because why not.
func (a *Allocations) Exposed() nat.PortSet {
var out = nat.PortSet{}
for port := range a.DockerBindings() {
out[port] = struct{}{}
}
return out
}

View File

@@ -1,77 +0,0 @@
package environment
import (
"sync"
)
type Settings struct {
Mounts []Mount
Allocations Allocations
Limits Limits
}
// Defines the actual configuration struct for the environment with all of the settings
// defined within it.
type Configuration struct {
mu sync.RWMutex
environmentVariables []string
settings Settings
}
// Returns a new environment configuration with the given settings and environment variables
// defined within it.
func NewConfiguration(s Settings, envVars []string) *Configuration {
return &Configuration{
environmentVariables: envVars,
settings: s,
}
}
// Updates the settings struct for this environment on the fly. This allows modified servers to
// automatically push those changes to the environment.
func (c *Configuration) SetSettings(s Settings) {
c.mu.Lock()
c.settings = s
c.mu.Unlock()
}
// Updates the environment variables associated with this environment by replacing the entire
// array of them with a new one.
func (c *Configuration) SetEnvironmentVariables(ev []string) {
c.mu.Lock()
c.environmentVariables = ev
c.mu.Unlock()
}
// Returns the limits assigned to this environment.
func (c *Configuration) Limits() Limits {
c.mu.RLock()
defer c.mu.RUnlock()
return c.settings.Limits
}
// Returns the allocations associated with this environment.
func (c *Configuration) Allocations() Allocations {
c.mu.RLock()
defer c.mu.RUnlock()
return c.settings.Allocations
}
// Returns all of the mounts associated with this environment.
func (c *Configuration) Mounts() []Mount {
c.mu.RLock()
defer c.mu.RUnlock()
return c.settings.Mounts
}
// Returns the environment variables associated with this instance.
func (c *Configuration) EnvironmentVariables() []string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.environmentVariables
}

View File

@@ -1,20 +0,0 @@
package docker
import "io"
type Console struct {
HandlerFunc *func(string)
}
var _ io.Writer = Console{}
func (c Console) Write(b []byte) (int, error) {
if c.HandlerFunc != nil {
l := make([]byte, len(b))
copy(l, b)
(*c.HandlerFunc)(string(l))
}
return len(b), nil
}

View File

@@ -1,442 +0,0 @@
package docker
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/apex/log"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/client"
"github.com/docker/docker/daemon/logger/jsonfilelog"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment"
"io"
"strconv"
"strings"
"time"
)
type imagePullStatus struct {
Status string `json:"status"`
Progress string `json:"progress"`
}
// Attaches to the docker container itself and ensures that we can pipe data in and out
// of the process stream. This should not be used for reading console data as you *will*
// miss important output at the beginning because of the time delay with attaching to the
// output.
func (e *Environment) Attach() error {
if e.IsAttached() {
return nil
}
if err := e.followOutput(); err != nil {
return errors.WithStack(err)
}
opts := types.ContainerAttachOptions{
Stdin: true,
Stdout: true,
Stderr: true,
Stream: true,
}
// Set the stream again with the container.
if st, err := e.client.ContainerAttach(context.Background(), e.Id, opts); err != nil {
return errors.WithStack(err)
} else {
e.SetStream(&st)
}
c := new(Console)
go func(console *Console) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
defer e.stream.Close()
defer func() {
e.setState(environment.ProcessOfflineState)
e.SetStream(nil)
}()
// Poll resources in a separate thread since this will block the copy call below
// from being reached until it is completed if not run in a separate process. However,
// we still want it to be stopped when the copy operation below is finished running which
// indicates that the container is no longer running.
go func(ctx context.Context) {
if err := e.pollResources(ctx); err != nil {
log.WithField("environment_id", e.Id).WithField("error", errors.WithStack(err)).Error("error during environment resource polling")
}
}(ctx)
// Stream the reader output to the console which will then fire off events and handle console
// throttling and sending the output to the user.
if _, err := io.Copy(console, e.stream.Reader); err != nil {
log.WithField("environment_id", e.Id).WithField("error", errors.WithStack(err)).Error("error while copying environment output to console")
}
}(c)
return nil
}
func (e *Environment) resources() container.Resources {
l := e.Configuration.Limits()
return container.Resources{
Memory: l.BoundedMemoryLimit(),
MemoryReservation: l.MemoryLimit * 1_000_000,
MemorySwap: l.ConvertedSwap(),
CPUQuota: l.ConvertedCpuLimit(),
CPUPeriod: 100_000,
CPUShares: 1024,
BlkioWeight: l.IoWeight,
OomKillDisable: &l.OOMDisabled,
CpusetCpus: l.Threads,
}
}
// Performs an in-place update of the Docker container's resource limits without actually
// making any changes to the operational state of the container. This allows memory, cpu,
// and IO limitations to be adjusted on the fly for individual instances.
func (e *Environment) InSituUpdate() error {
if _, err := e.client.ContainerInspect(context.Background(), e.Id); err != nil {
// If the container doesn't exist for some reason there really isn't anything
// we can do to fix that in this process (it doesn't make sense at least). In those
// cases just return without doing anything since we still want to save the configuration
// to the disk.
//
// We'll let a boot process make modifications to the container if needed at this point.
if client.IsErrNotFound(err) {
return nil
}
return errors.WithStack(err)
}
u := container.UpdateConfig{
Resources: e.resources(),
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
if _, err := e.client.ContainerUpdate(ctx, e.Id, u); err != nil {
return errors.WithStack(err)
}
return nil
}
// Creates a new container for the server using all of the data that is currently
// available for it. If the container already exists it will be returnee.
func (e *Environment) Create() error {
// If the container already exists don't hit the user with an error, just return
// the current information about it which is what we would do when creating the
// container anyways.
if _, err := e.client.ContainerInspect(context.Background(), e.Id); err == nil {
return nil
} else if !client.IsErrNotFound(err) {
return errors.WithStack(err)
}
// Try to pull the requested image before creating the container.
if err := e.ensureImageExists(e.meta.Image); err != nil {
return errors.WithStack(err)
}
a := e.Configuration.Allocations()
evs := e.Configuration.EnvironmentVariables()
for i, v := range evs {
// Convert 127.0.0.1 to the pterodactyl0 network interface if the environment is Docker
// so that the server operates as expected.
if v == "SERVER_IP=127.0.0.1" {
evs[i] = "SERVER_IP=" + config.Get().Docker.Network.Interface
}
}
conf := &container.Config{
Hostname: e.Id,
Domainname: config.Get().Docker.Domainname,
User: strconv.Itoa(config.Get().System.User.Uid),
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
OpenStdin: true,
Tty: true,
ExposedPorts: a.Exposed(),
Image: e.meta.Image,
Env: e.Configuration.EnvironmentVariables(),
Labels: map[string]string{
"Service": "Pterodactyl",
"ContainerType": "server_process",
},
}
tmpfsSize := strconv.Itoa(int(config.Get().Docker.TmpfsSize))
hostConf := &container.HostConfig{
PortBindings: a.DockerBindings(),
// Configure the mounts for this container. First mount the server data directory
// into the container as a r/w bind.
Mounts: e.convertMounts(),
// Configure the /tmp folder mapping in containers. This is necessary for some
// games that need to make use of it for downloads and other installation processes.
Tmpfs: map[string]string{
"/tmp": "rw,exec,nosuid,size=" + tmpfsSize + "M",
},
// Define resource limits for the container based on the data passed through
// from the Panel.
Resources: e.resources(),
DNS: config.Get().Docker.Network.Dns,
// Configure logging for the container to make it easier on the Daemon to grab
// the server output. Ensure that we don't use too much space on the host machine
// since we only need it for the last few hundred lines of output and don't care
// about anything else in it.
LogConfig: container.LogConfig{
Type: jsonfilelog.Name,
Config: map[string]string{
"max-size": "5m",
"max-file": "1",
},
},
SecurityOpt: []string{"no-new-privileges"},
ReadonlyRootfs: true,
CapDrop: []string{
"setpcap", "mknod", "audit_write", "net_raw", "dac_override",
"fowner", "fsetid", "net_bind_service", "sys_chroot", "setfcap",
},
NetworkMode: container.NetworkMode(config.Get().Docker.Network.Mode),
}
if _, err := e.client.ContainerCreate(context.Background(), conf, hostConf, nil, e.Id); err != nil {
return errors.WithStack(err)
}
return nil
}
func (e *Environment) convertMounts() []mount.Mount {
var out []mount.Mount
for _, m := range e.Configuration.Mounts() {
out = append(out, mount.Mount{
Type: mount.TypeBind,
Source: m.Source,
Target: m.Target,
ReadOnly: m.ReadOnly,
})
}
return out
}
// Remove the Docker container from the machine. If the container is currently running
// it will be forcibly stopped by Docker.
func (e *Environment) Destroy() error {
// We set it to stopping than offline to prevent crash detection from being triggered.
e.setState(environment.ProcessStoppingState)
err := e.client.ContainerRemove(context.Background(), e.Id, types.ContainerRemoveOptions{
RemoveVolumes: true,
RemoveLinks: false,
Force: true,
})
// Don't trigger a destroy failure if we try to delete a container that does not
// exist on the system. We're just a step ahead of ourselves in that case.
//
// @see https://github.com/pterodactyl/panel/issues/2001
if err != nil && client.IsErrNotFound(err) {
return nil
}
e.setState(environment.ProcessOfflineState)
return err
}
// Attaches to the log for the container. This avoids us missing crucial output that
// happens in the split seconds before the code moves from 'Starting' to 'Attaching'
// on the process.
func (e *Environment) followOutput() error {
if exists, err := e.Exists(); !exists {
if err != nil {
return errors.WithStack(err)
}
return errors.New(fmt.Sprintf("no such container: %s", e.Id))
}
opts := types.ContainerLogsOptions{
ShowStderr: true,
ShowStdout: true,
Follow: true,
Since: time.Now().Format(time.RFC3339),
}
reader, err := e.client.ContainerLogs(context.Background(), e.Id, opts)
go func(reader io.ReadCloser) {
defer reader.Close()
r := bufio.NewReader(reader)
ParentLoop:
for {
var b bytes.Buffer
var line []byte
var isPrefix bool
for {
// Read the line and write it to the buffer.
line, isPrefix, err = r.ReadLine()
// Certain games like Minecraft output absolutely random carriage returns in the output seemingly
// in line with that it thinks is the terminal size. Those returns break a lot of output handling,
// so we'll just replace them with proper new-lines and then split it later and send each line as
// its own event in the response.
b.Write(bytes.ReplaceAll(line, []byte(" \r"), []byte("\r\n")))
// Finish this loop and begin outputting the line if there is no prefix (the line fit into
// the default buffer), or if we hit the end of the line.
if !isPrefix || err == io.EOF {
break
}
// If we encountered an error with something in ReadLine that was not an EOF just abort
// the entire process here.
if err != nil {
break ParentLoop
}
}
// Publish the line for this loop. Break on new-line characters so every line is sent as a single
// output event, otherwise you get funky handling in the browser console.
for _, line := range strings.Split(b.String(), "\r\n") {
e.Events().Publish(environment.ConsoleOutputEvent, line)
}
// If the error we got previously that lead to the line being output is an io.EOF we want to
// exit the entire looping process.
if err == io.EOF {
break
}
}
if err != nil && err != io.EOF {
log.WithField("error", err).WithField("container_id", e.Id).Warn("error processing scanner line in console output")
}
}(reader)
return errors.WithStack(err)
}
// Pulls the image from Docker. If there is an error while pulling the image from the source
// but the image already exists locally, we will report that error to the logger but continue
// with the process.
//
// The reasoning behind this is that Quay has had some serious outages as of late, and we don't
// need to block all of the servers from booting just because of that. I'd imagine in a lot of
// cases an outage shouldn't affect users too badly. It'll at least keep existing servers working
// correctly if anything.
func (e *Environment) ensureImageExists(image string) error {
e.Events().Publish(environment.DockerImagePullStarted, "")
defer e.Events().Publish(environment.DockerImagePullCompleted, "")
// Images prefixed with a ~ are local images that we do not need to try and pull.
if strings.HasPrefix(image, "~") {
return nil
}
// Give it up to 15 minutes to pull the image. I think this should cover 99.8% of cases where an
// image pull might fail. I can't imagine it will ever take more than 15 minutes to fully pull
// an image. Let me know when I am inevitably wrong here...
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*15)
defer cancel()
// Get a registry auth configuration from the config.
var registryAuth *config.RegistryConfiguration
for registry, c := range config.Get().Docker.Registries {
if !strings.HasPrefix(image, 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
}
out, err := e.client.ImagePull(ctx, image, imagePullOptions)
if err != nil {
images, ierr := e.client.ImageList(ctx, 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 != image {
continue
}
log.WithFields(log.Fields{
"image": image,
"container_id": e.Id,
"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 out.Close()
log.WithField("image", image).Debug("pulling docker image... this could take a bit of time")
// I'm not sure what the best approach here is, but this will block execution until the image
// is done being pulled, which is what we need.
scanner := bufio.NewScanner(out)
for scanner.Scan() {
s := imagePullStatus{}
fmt.Println(scanner.Text())
if err := json.Unmarshal(scanner.Bytes(), &s); err == nil {
e.Events().Publish(environment.DockerImagePullStatus, s.Status+" "+s.Progress)
}
}
if err := scanner.Err(); err != nil {
return err
}
log.WithField("image", image).Debug("completed docker image pull")
return nil
}

View File

@@ -1,184 +0,0 @@
package docker
import (
"context"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/events"
"io"
"sync"
)
type Metadata struct {
Image string
Stop api.ProcessStopConfiguration
}
// Ensure that the Docker environment is always implementing all of the methods
// from the base environment interface.
var _ environment.ProcessEnvironment = (*Environment)(nil)
type Environment struct {
mu sync.RWMutex
eventMu sync.Mutex
// The public identifier for this environment. In this case it is the Docker container
// name that will be used for all instances created under it.
Id string
// The environment configuration.
Configuration *environment.Configuration
meta *Metadata
// The Docker client being used for this instance.
client *client.Client
// Controls the hijacked response stream which exists only when we're attached to
// the running container instance.
stream *types.HijackedResponse
// Holds the stats stream used by the polling commands so that we can easily close it out.
stats io.ReadCloser
emitter *events.EventBus
// Tracks the environment state.
st string
stMu sync.RWMutex
}
// Creates a new base Docker environment. The ID passed through will be the ID that is used to
// reference the container from here on out. This should be unique per-server (we use the UUID
// by default). The container does not need to exist at this point.
func New(id string, m *Metadata, c *environment.Configuration) (*Environment, error) {
cli, err := environment.DockerClient()
if err != nil {
return nil, err
}
e := &Environment{
Id: id,
Configuration: c,
meta: m,
client: cli,
}
return e, nil
}
func (e *Environment) Type() string {
return "docker"
}
// Set if this process is currently attached to the process.
func (e *Environment) SetStream(s *types.HijackedResponse) {
e.mu.Lock()
e.stream = s
e.mu.Unlock()
}
// Determine if the this process is currently attached to the container.
func (e *Environment) IsAttached() bool {
e.mu.RLock()
defer e.mu.RUnlock()
return e.stream != nil
}
func (e *Environment) Events() *events.EventBus {
e.eventMu.Lock()
defer e.eventMu.Unlock()
if e.emitter == nil {
e.emitter = events.New()
}
return e.emitter
}
// Determines if the container exists in this environment. The ID passed through should be the
// server UUID since containers are created utilizing the server UUID as the name and docker
// will work fine when using the container name as the lookup parameter in addition to the longer
// ID auto-assigned when the container is created.
func (e *Environment) Exists() (bool, error) {
_, err := e.client.ContainerInspect(context.Background(), e.Id)
if err != nil {
// If this error is because the container instance wasn't found via Docker we
// can safely ignore the error and just return false.
if client.IsErrNotFound(err) {
return false, nil
}
return false, err
}
return true, nil
}
// Determines if the server's docker container is currently running. If there is no container
// present, an error will be raised (since this shouldn't be a case that ever happens under
// correctly developed circumstances).
//
// You can confirm if the instance wasn't found by using client.IsErrNotFound from the Docker
// API.
//
// @see docker/client/errors.go
func (e *Environment) IsRunning() (bool, error) {
c, err := e.client.ContainerInspect(context.Background(), e.Id)
if err != nil {
return false, err
}
return c.State.Running, nil
}
// Determine the container exit state and return the exit code and whether or not
// the container was killed by the OOM killer.
func (e *Environment) ExitState() (uint32, bool, error) {
c, err := e.client.ContainerInspect(context.Background(), e.Id)
if err != nil {
// I'm not entirely sure how this can happen to be honest. I tried deleting a
// container _while_ a server was running and wings gracefully saw the crash and
// created a new container for it.
//
// However, someone reported an error in Discord about this scenario happening,
// so I guess this should prevent it? They didn't tell me how they caused it though
// so that's a mystery that will have to go unsolved.
//
// @see https://github.com/pterodactyl/panel/issues/2003
if client.IsErrNotFound(err) {
return 1, false, nil
}
return 0, false, errors.WithStack(err)
}
return uint32(c.State.ExitCode), c.State.OOMKilled, nil
}
// Returns the environment configuration allowing a process to make modifications of the
// environment on the fly.
func (e *Environment) Config() *environment.Configuration {
e.mu.RLock()
defer e.mu.RUnlock()
return e.Configuration
}
// Sets the stop configuration for the environment.
func (e *Environment) SetStopConfiguration(c api.ProcessStopConfiguration) {
e.mu.Lock()
e.meta.Stop = c
e.mu.Unlock()
}
func (e *Environment) SetImage(i string) {
e.mu.Lock()
e.meta.Image = i
e.mu.Unlock()
}

View File

@@ -1,240 +0,0 @@
package docker
import (
"context"
"github.com/apex/log"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/environment"
"os"
"strings"
"time"
)
// Run before the container starts and get the process configuration from the Panel.
// This is important since we use this to check configuration files as well as ensure
// we always have the latest version of an egg available for server processes.
//
// This process will also confirm that the server environment exists and is in a bootable
// state. This ensures that unexpected container deletion while Wings is running does
// not result in the server becoming unbootable.
func (e *Environment) OnBeforeStart() error {
// Always destroy and re-create the server container to ensure that synced data from
// the Panel is usee.
if err := e.client.ContainerRemove(context.Background(), e.Id, types.ContainerRemoveOptions{RemoveVolumes: true}); err != nil {
if !client.IsErrNotFound(err) {
return errors.Wrap(err, "failed to remove server docker container during pre-boot")
}
}
// The Create() function will check if the container exists in the first place, and if
// so just silently return without an error. Otherwise, it will try to create the necessary
// container and data storage directory.
//
// This won't actually run an installation process however, it is just here to ensure the
// environment gets created properly if it is missing and the server is started. We're making
// an assumption that all of the files will still exist at this point.
if err := e.Create(); err != nil {
return err
}
return nil
}
// Starts the server environment and begins piping output to the event listeners for the
// console. If a container does not exist, or needs to be rebuilt that will happen in the
// call to OnBeforeStart().
func (e *Environment) Start() error {
sawError := false
// If sawError is set to true there was an error somewhere in the pipeline that
// got passed up, but we also want to ensure we set the server to be offline at
// that point.
defer func() {
if sawError {
// If we don't set it to stopping first, you'll trigger crash detection which
// we don't want to do at this point since it'll just immediately try to do the
// exact same action that lead to it crashing in the first place...
e.setState(environment.ProcessStoppingState)
e.setState(environment.ProcessOfflineState)
}
}()
if c, err := e.client.ContainerInspect(context.Background(), e.Id); err != nil {
// Do nothing if the container is not found, we just don't want to continue
// to the next block of code here. This check was inlined here to guard against
// a nil-pointer when checking c.State below.
//
// @see https://github.com/pterodactyl/panel/issues/2000
if !client.IsErrNotFound(err) {
return errors.WithStack(err)
}
} else {
// If the server is running update our internal state and continue on with the attach.
if c.State.Running {
e.setState(environment.ProcessRunningState)
return e.Attach()
}
// Truncate the log file so we don't end up outputting a bunch of useless log information
// to the websocket and whatnot. Check first that the path and file exist before trying
// to truncate them.
if _, err := os.Stat(c.LogPath); err == nil {
if err := os.Truncate(c.LogPath, 0); err != nil {
return errors.WithStack(err)
}
}
}
e.setState(environment.ProcessStartingState)
// Set this to true for now, we will set it to false once we reach the
// end of this chain.
sawError = true
// Run the before start function and wait for it to finish. This will validate that the container
// exists on the system, and rebuild the container if that is required for server booting to
// occur.
if err := e.OnBeforeStart(); err != nil {
return errors.WithStack(err)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
if err := e.client.ContainerStart(ctx, e.Id, types.ContainerStartOptions{}); err != nil {
return errors.WithStack(err)
}
// No errors, good to continue through.
sawError = false
return e.Attach()
}
// Stops the container that the server is running in. This will allow up to 30 seconds to pass
// before the container is forcefully terminated if we are trying to stop it without using a command
// sent into the instance.
//
// You most likely want to be using WaitForStop() rather than this function, since this will return
// as soon as the command is sent, rather than waiting for the process to be completed stopped.
func (e *Environment) Stop() error {
e.mu.RLock()
s := e.meta.Stop
e.mu.RUnlock()
if s.Type == "" || s.Type == api.ProcessStopSignal {
if s.Type == "" {
log.WithField("container_id", e.Id).Warn("no stop configuration detected for environment, using termination procedure")
}
return e.Terminate(os.Kill)
}
// If the process is already offline don't switch it back to stopping. Just leave it how
// it is and continue through to the stop handling for the process.
if e.State() != environment.ProcessOfflineState {
e.setState(environment.ProcessStoppingState)
}
// Only attempt to send the stop command to the instance if we are actually attached to
// the instance. If we are not for some reason, just send the container stop event.
if e.IsAttached() && s.Type == api.ProcessStopCommand {
return e.SendCommand(s.Value)
}
t := time.Second * 30
err := e.client.ContainerStop(context.Background(), e.Id, &t)
if err != nil {
// If the container does not exist just mark the process as stopped and return without
// an error.
if client.IsErrNotFound(err) {
e.SetStream(nil)
e.setState(environment.ProcessOfflineState)
return nil
}
return err
}
return nil
}
// Attempts to gracefully stop a server using the defined stop command. If the server
// does not stop after seconds have passed, an error will be returned, or the instance
// will be terminated forcefully depending on the value of the second argument.
func (e *Environment) WaitForStop(seconds uint, terminate bool) error {
if err := e.Stop(); err != nil {
return errors.WithStack(err)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(seconds)*time.Second)
defer cancel()
// Block the return of this function until the container as been marked as no
// longer running. If this wait does not end by the time seconds have passed,
// attempt to terminate the container, or return an error.
ok, errChan := e.client.ContainerWait(ctx, e.Id, container.WaitConditionNotRunning)
select {
case <-ctx.Done():
if ctxErr := ctx.Err(); ctxErr != nil {
if terminate {
log.WithField("container_id", e.Id).Debug("server did not stop in time, executing process termination")
return errors.WithStack(e.Terminate(os.Kill))
}
return errors.WithStack(ctxErr)
}
case err := <-errChan:
if err != nil {
if terminate {
log.WithField("container_id", e.Id).WithField("error", errors.WithStack(err)).Warn("error while waiting for container stop, attempting process termination")
return errors.WithStack(e.Terminate(os.Kill))
}
return errors.WithStack(err)
}
case <-ok:
}
return nil
}
// Forcefully terminates the container using the signal passed through.
func (e *Environment) Terminate(signal os.Signal) error {
c, err := e.client.ContainerInspect(context.Background(), e.Id)
if err != nil {
return errors.WithStack(err)
}
if !c.State.Running {
// If the container is not running but we're not already in a stopped state go ahead
// and update things to indicate we should be completely stopped now. Set to stopping
// first so crash detection is not triggered.
if e.State() != environment.ProcessOfflineState {
e.setState(environment.ProcessStoppingState)
e.setState(environment.ProcessOfflineState)
}
return nil
}
// We set it to stopping than offline to prevent crash detection from being triggered.
e.setState(environment.ProcessStoppingState)
sig := strings.TrimSuffix(strings.TrimPrefix(signal.String(), "signal "), "ed")
if err := e.client.ContainerKill(context.Background(), e.Id, sig); err != nil {
return err
}
e.setState(environment.ProcessOfflineState)
return nil
}

View File

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

View File

@@ -1,131 +0,0 @@
package docker
import (
"context"
"encoding/json"
"github.com/apex/log"
"github.com/docker/docker/api/types"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/environment"
"io"
"math"
"sync/atomic"
)
// Attach to the instance and then automatically emit an event whenever the resource usage for the
// server process changes.
func (e *Environment) pollResources(ctx context.Context) error {
l := log.WithField("container_id", e.Id)
l.Debug("starting resource polling for container")
defer l.Debug("stopped resource polling for container")
if e.State() == environment.ProcessOfflineState {
return errors.New("cannot enable resource polling on a stopped server")
}
stats, err := e.client.ContainerStats(context.Background(), e.Id, true)
if err != nil {
return errors.WithStack(err)
}
defer stats.Body.Close()
dec := json.NewDecoder(stats.Body)
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
var v *types.StatsJSON
if err := dec.Decode(&v); err != nil {
if err != io.EOF {
l.WithField("error", errors.WithStack(err)).Warn("error while processing Docker stats output for container")
} else {
l.Debug("io.EOF encountered during stats decode, stopping polling...")
}
return nil
}
// Disable collection if the server is in an offline state and this process is still running.
if e.State() == environment.ProcessOfflineState {
l.Debug("process in offline state while resource polling is still active; stopping poll")
return nil
}
var rx uint64
var tx uint64
for _, nw := range v.Networks {
atomic.AddUint64(&rx, nw.RxBytes)
atomic.AddUint64(&tx, nw.RxBytes)
}
st := &environment.Stats{
Memory: calculateDockerMemory(v.MemoryStats),
MemoryLimit: v.MemoryStats.Limit,
CpuAbsolute: calculateDockerAbsoluteCpu(&v.PreCPUStats, &v.CPUStats),
Network: struct {
RxBytes uint64 `json:"rx_bytes"`
TxBytes uint64 `json:"tx_bytes"`
}{
RxBytes: rx,
TxBytes: tx,
},
}
if b, err := json.Marshal(st); err != nil {
l.WithField("error", errors.WithStack(err)).Warn("error while marshaling stats object for environment")
} else {
e.Events().Publish(environment.ResourceEvent, string(b))
}
}
}
}
// The "docker stats" CLI call does not return the same value as the types.MemoryStats.Usage
// value which can be rather confusing to people trying to compare panel usage to
// their stats output.
//
// This math is straight up lifted from their CLI repository in order to show the same
// values to avoid people bothering me about it. It should also reflect a slightly more
// correct memory value anyways.
//
// @see https://github.com/docker/cli/blob/96e1d1d6/cli/command/container/stats_helpers.go#L227-L249
func calculateDockerMemory(stats types.MemoryStats) uint64 {
if v, ok := stats.Stats["total_inactive_file"]; ok && v < stats.Usage {
return stats.Usage - v
}
if v := stats.Stats["inactive_file"]; v < stats.Usage {
return stats.Usage - v
}
return stats.Usage
}
// Calculates the absolute CPU usage used by the server process on the system, not constrained
// by the defined CPU limits on the container.
//
// @see https://github.com/docker/cli/blob/aa097cf1aa19099da70930460250797c8920b709/cli/command/container/stats_helpers.go#L166
func calculateDockerAbsoluteCpu(pStats *types.CPUStats, stats *types.CPUStats) float64 {
// Calculate the change in CPU usage between the current and previous reading.
cpuDelta := float64(stats.CPUUsage.TotalUsage) - float64(pStats.CPUUsage.TotalUsage)
// Calculate the change for the entire system's CPU usage between current and previous reading.
systemDelta := float64(stats.SystemUsage) - float64(pStats.SystemUsage)
// Calculate the total number of CPU cores being used.
cpus := float64(stats.OnlineCPUs)
if cpus == 0.0 {
cpus = float64(len(stats.CPUUsage.PercpuUsage))
}
percent := 0.0
if systemDelta > 0.0 && cpuDelta > 0.0 {
percent = (cpuDelta / systemDelta) * cpus * 100.0
}
return math.Round(percent*1000) / 1000
}

View File

@@ -1,99 +0,0 @@
package docker
import (
"bufio"
"bytes"
"context"
"encoding/json"
"github.com/docker/docker/api/types"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/environment"
"strconv"
)
type dockerLogLine struct {
Log string `json:"log"`
}
var ErrNotAttached = errors.New("not attached to instance")
func (e *Environment) setStream(s *types.HijackedResponse) {
e.mu.Lock()
e.stream = s
e.mu.Unlock()
}
// Sends the specified command to the stdin of the running container instance. There is no
// confirmation that this data is sent successfully, only that it gets pushed into the stdin.
func (e *Environment) SendCommand(c string) error {
if !e.IsAttached() {
return ErrNotAttached
}
e.mu.RLock()
defer e.mu.RUnlock()
// If the command being processed is the same as the process stop command then we want to mark
// the server as entering the stopping state otherwise the process will stop and Wings will think
// it has crashed and attempt to restart it.
if e.meta.Stop.Type == "command" && c == e.meta.Stop.Value {
e.Events().Publish(environment.StateChangeEvent, environment.ProcessStoppingState)
}
_, err := e.stream.Conn.Write([]byte(c + "\n"))
return errors.WithStack(err)
}
// Reads the log file for the server. This does not care if the server is running or not, it will
// simply try to read the last X bytes of the file and return them.
func (e *Environment) Readlog(lines int) ([]string, error) {
r, err := e.client.ContainerLogs(context.Background(), e.Id, types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Tail: strconv.Itoa(lines),
})
if err != nil {
return nil, errors.WithStack(err)
}
defer r.Close()
var out []string
scanner := bufio.NewScanner(r)
for scanner.Scan() {
out = append(out, scanner.Text())
}
return out, nil
}
// Docker stores the logs for server output in a JSON format. This function will iterate over the JSON
// that was read from the log file and parse it into a more human readable format.
func (e *Environment) parseLogToStrings(b []byte) ([]string, error) {
var hasError = false
var out []string
scanner := bufio.NewScanner(bytes.NewReader(b))
for scanner.Scan() {
var l dockerLogLine
// Unmarshal the contents and allow up to a single error before bailing out of the process. We
// do this because if you're arbitrarily reading a length of the file you'll likely end up
// with the first line in the output being improperly formatted JSON. In those cases we want to
// just skip over it. However if we see another error we're going to bail out because that is an
// abnormal situation.
if err := json.Unmarshal([]byte(scanner.Text()), &l); err != nil {
if hasError {
return nil, err
}
hasError = true
continue
}
out = append(out, l.Log)
}
return out, nil
}

View File

@@ -1,131 +0,0 @@
package environment
import (
"fmt"
"github.com/apex/log"
"math"
"strconv"
)
type Mount struct {
// In Docker environments this makes no difference, however in a non-Docker environment you
// should treat the "Default" mount as the root directory for the server. All other mounts
// are just in addition to that one, and generally things like shared maps or timezone data.
Default bool `json:"-"`
// The target path on the system. This is "/home/container" for all server's Default mount
// but in non-container environments you can likely ignore the target and just work with the
// source.
Target string `json:"target"`
// The directory from which the files will be read. In Docker environments this is the directory
// that we're mounting into the container at the Target location.
Source string `json:"source"`
// Whether or not the directory is being mounted as read-only. It is up to the environment to
// handle this value correctly and ensure security expectations are met with its usage.
ReadOnly bool `json:"read_only"`
}
// The build settings for a given server that impact docker container creation and
// resource limits for a server instance.
type Limits struct {
// The total amount of memory in megabytes that this server is allowed to
// use on the host system.
MemoryLimit int64 `json:"memory_limit"`
// The amount of additional swap space to be provided to a container instance.
Swap int64 `json:"swap"`
// The relative weight for IO operations in a container. This is relative to other
// containers on the system and should be a value between 10 and 1000.
IoWeight uint16 `json:"io_weight"`
// The percentage of CPU that this instance is allowed to consume relative to
// the host. A value of 200% represents complete utilization of two cores. This
// should be a value between 1 and THREAD_COUNT * 100.
CpuLimit int64 `json:"cpu_limit"`
// The amount of disk space in megabytes that a server is allowed to use.
DiskSpace int64 `json:"disk_space"`
// Sets which CPU threads can be used by the docker instance.
Threads string `json:"threads"`
OOMDisabled bool `json:"oom_disabled"`
}
// Converts the CPU limit for a server build into a number that can be better understood
// by the Docker environment. If there is no limit set, return -1 which will indicate to
// Docker that it has unlimited CPU quota.
func (r *Limits) ConvertedCpuLimit() int64 {
if r.CpuLimit == 0 {
return -1
}
return r.CpuLimit * 1000
}
// Set the hard limit for memory usage to be 5% more than the amount of memory assigned to
// the server. If the memory limit for the server is < 4G, use 10%, if less than 2G use
// 15%. This avoids unexpected crashes from processes like Java which run over the limit.
func (r *Limits) MemoryOverheadMultiplier() float64 {
if r.MemoryLimit <= 2048 {
return 1.15
} else if r.MemoryLimit <= 4096 {
return 1.10
}
return 1.05
}
func (r *Limits) BoundedMemoryLimit() int64 {
return int64(math.Round(float64(r.MemoryLimit) * r.MemoryOverheadMultiplier() * 1_000_000))
}
// Returns the amount of swap available as a total in bytes. This is returned as the amount
// of memory available to the server initially, PLUS the amount of additional swap to include
// which is the format used by Docker.
func (r *Limits) ConvertedSwap() int64 {
if r.Swap < 0 {
return -1
}
return (r.Swap * 1_000_000) + r.BoundedMemoryLimit()
}
type Variables map[string]interface{}
// Ugly hacky function to handle environment variables that get passed through as not-a-string
// from the Panel. Ideally we'd just say only pass strings, but that is a fragile idea and if a
// string wasn't passed through you'd cause a crash or the server to become unavailable. For now
// try to handle the most likely values from the JSON and hope for the best.
func (v Variables) Get(key string) string {
val, ok := v[key]
if !ok {
return ""
}
switch val.(type) {
case int:
return strconv.Itoa(val.(int))
case int32:
return strconv.FormatInt(val.(int64), 10)
case int64:
return strconv.FormatInt(val.(int64), 10)
case float32:
return fmt.Sprintf("%f", val.(float32))
case float64:
return fmt.Sprintf("%f", val.(float64))
case bool:
return strconv.FormatBool(val.(bool))
case string:
return val.(string)
}
// TODO: I think we can add a check for val == nil and return an empty string for those
// and this warning should theoretically never happen?
log.Warn(fmt.Sprintf("failed to marshal environment variable \"%s\" of type %+v into string", key, val))
return ""
}

View File

@@ -1,47 +0,0 @@
package environment
import "sync"
// Defines the current resource usage for a given server instance. If a server is offline you
// should obviously expect memory and CPU usage to be 0. However, disk will always be returned
// since that is not dependent on the server being running to collect that data.
type Stats struct {
mu sync.RWMutex
// The total amount of memory, in bytes, that this server instance is consuming. This is
// calculated slightly differently than just using the raw Memory field that the stats
// return from the container, so please check the code setting this value for how that
// is calculated.
Memory uint64 `json:"memory_bytes"`
// The total amount of memory this container or resource can use. Inside Docker this is
// going to be higher than you'd expect because we're automatically allocating overhead
// abilities for the container, so its not going to be a perfect match.
MemoryLimit uint64 `json:"memory_limit_bytes"`
// The absolute CPU usage is the amount of CPU used in relation to the entire system and
// does not take into account any limits on the server process itself.
CpuAbsolute float64 `json:"cpu_absolute"`
// The current disk space being used by the server. This is cached to prevent slow lookup
// issues on frequent refreshes.
// Disk int64 `json:"disk_bytes"`
// Current network transmit in & out for a container.
Network struct {
RxBytes uint64 `json:"rx_bytes"`
TxBytes uint64 `json:"tx_bytes"`
} `json:"network"`
}
// Resets the usages values to zero, used when a server is stopped to ensure we don't hold
// onto any values incorrectly.
func (s *Stats) Empty() {
s.mu.Lock()
defer s.mu.Unlock()
s.Memory = 0
s.CpuAbsolute = 0
s.Network.TxBytes = 0
s.Network.RxBytes = 0
}

View File

@@ -1,122 +0,0 @@
package events
import (
"encoding/json"
"github.com/gammazero/workerpool"
"github.com/pkg/errors"
"strings"
"sync"
)
type Event struct {
Data string
Topic string
}
type EventBus struct {
mu sync.RWMutex
pools map[string]*CallbackPool
}
func New() *EventBus {
return &EventBus{
pools: make(map[string]*CallbackPool),
}
}
// Publish data to a given topic.
func (e *EventBus) Publish(topic string, data string) {
t := topic
// Some of our topics for the socket support passing a more specific namespace,
// such as "backup completed:1234" to indicate which specific backup was completed.
//
// In these cases, we still need to the send the event using the standard listener
// name of "backup completed".
if strings.Contains(topic, ":") {
parts := strings.SplitN(topic, ":", 2)
if len(parts) == 2 {
t = parts[0]
}
}
e.mu.RLock()
defer e.mu.RUnlock()
// Acquire a read lock and loop over all of the channels registered for the topic. This
// avoids a panic crash if the process tries to unregister the channel while this routine
// is running.
if cp, ok := e.pools[t]; ok {
for _, callback := range cp.callbacks {
c := *callback
evt := Event{Data: data, Topic: topic}
// Using the workerpool with one worker allows us to execute events in a FIFO manner. Running
// this using goroutines would cause things such as console output to just output in random order
// if more than one event is fired at the same time.
//
// However, the pool submission does not block the execution of this function itself, allowing
// us to call publish without blocking any of the other pathways.
//
// @see https://github.com/pterodactyl/panel/issues/2303
cp.pool.Submit(func() {
c(evt)
})
}
}
}
// Publishes a JSON message to a given topic.
func (e *EventBus) PublishJson(topic string, data interface{}) error {
b, err := json.Marshal(data)
if err != nil {
return errors.WithStack(err)
}
e.Publish(topic, string(b))
return nil
}
// Register a callback function that will be executed each time one of the events using the topic
// name is called.
func (e *EventBus) On(topic string, callback *func(Event)) {
e.mu.Lock()
defer e.mu.Unlock()
// Check if this topic has been registered at least once for the event listener, and if
// not create an empty struct for the topic.
if _, exists := e.pools[topic]; !exists {
e.pools[topic] = &CallbackPool{
callbacks: make([]*func(Event), 0),
pool: workerpool.New(1),
}
}
// If this callback is not already registered as an event listener, go ahead and append
// it to the array of callbacks for this topic.
e.pools[topic].Add(callback)
}
// Removes an event listener from the bus.
func (e *EventBus) Off(topic string, callback *func(Event)) {
e.mu.Lock()
defer e.mu.Unlock()
if cp, ok := e.pools[topic]; ok {
cp.Remove(callback)
}
}
// Removes all of the event listeners that have been registered for any topic. Also stops the worker
// pool to close that routine.
func (e *EventBus) Destroy() {
e.mu.Lock()
defer e.mu.Unlock()
// Stop every pool that exists for a given callback topic.
for _, cp := range e.pools {
cp.pool.Stop()
}
e.pools = make(map[string]*CallbackPool)
}

View File

@@ -1,49 +0,0 @@
package events
import (
"github.com/gammazero/workerpool"
"reflect"
)
type CallbackPool struct {
callbacks []*func(Event)
pool *workerpool.WorkerPool
}
// Pushes a new callback into the array of listeners for the pool.
func (cp *CallbackPool) Add(callback *func(Event)) {
if cp.index(reflect.ValueOf(callback)) < 0 {
cp.callbacks = append(cp.callbacks, callback)
}
}
// Removes a callback from the array of registered callbacks if it exists.
func (cp *CallbackPool) Remove(callback *func(Event)) {
i := cp.index(reflect.ValueOf(callback))
// If i < 0 it means there was no index found for the given callback, meaning it was
// never registered or was already unregistered from the listeners. Also double check
// that we didn't somehow escape the length of the topic callback (not sure how that
// would happen, but lets avoid a panic condition).
if i < 0 || i >= len(cp.callbacks) {
return
}
// We can assume that the topic still exists at this point since we acquire an exclusive
// lock on the process, and the "e.index" function cannot return a value >= 0 if there is
// no topic already existing.
cp.callbacks = append(cp.callbacks[:i], cp.callbacks[i+1:]...)
}
// Finds the index of a given callback in the topic by comparing all of the registered callback
// pointers to the passed function. This function does not aquire a lock as it should only be called
// within the confines of a function that has already acquired a lock for the duration of the lookup.
func (cp *CallbackPool) index(v reflect.Value) int {
for i, handler := range cp.callbacks {
if reflect.ValueOf(handler).Pointer() == v.Pointer() {
return i
}
}
return -1
}

115
go.mod
View File

@@ -1,82 +1,67 @@
module github.com/pterodactyl/wings
go 1.13
go 1.12
// Uncomment this in development environments to make changes to the core SFTP
// server software. This assumes you're using the official Pterodactyl Environment
// otherwise this path will not work.
//
// @see https://github.com/pterodactyl/development
//
// replace github.com/pterodactyl/sftp-server => ../sftp-server
require (
github.com/AlecAivazis/survey/v2 v2.1.0
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
github.com/Jeffail/gabs/v2 v2.5.1
github.com/Microsoft/go-winio v0.4.14 // indirect
github.com/NYTimes/logrotate v1.0.0
github.com/andybalholm/brotli v1.0.0 // indirect
github.com/apex/log v1.8.0
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
github.com/Jeffail/gabs/v2 v2.2.0
github.com/Microsoft/go-winio v0.4.7 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a
github.com/beevik/etree v1.1.0
github.com/buger/jsonparser v1.0.0
github.com/cobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249
github.com/containerd/containerd v1.3.7 // indirect
github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b // indirect
github.com/creasty/defaults v1.5.0
github.com/docker/cli v17.12.1-ce-rc2+incompatible
github.com/buger/jsonparser v0.0.0-20191204142016-1a29609e0929
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448 // indirect
github.com/creasty/defaults v1.3.0
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 v0.0.0-20180422163414-57142e89befe
github.com/docker/go-connections v0.4.0
github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/fatih/color v1.9.0
github.com/franela/goblin v0.0.0-20200825194134-80c0062ed6cd
github.com/frankban/quicktest v1.10.2 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/gabriel-vasile/mimetype v1.1.1
github.com/gammazero/deque v0.0.0-20200721202602-07291166fe33 // indirect
github.com/gammazero/workerpool v1.0.0
github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.2
github.com/gin-gonic/gin v1.6.3
github.com/go-playground/validator/v10 v10.3.0 // indirect
github.com/gogo/protobuf v1.3.1 // indirect
github.com/docker/go-units v0.3.3 // indirect
github.com/gabriel-vasile/mimetype v0.1.4
github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.0
github.com/ghodss/yaml v1.0.0
github.com/gogo/protobuf v1.2.1 // indirect
github.com/google/uuid v1.1.1
github.com/gorilla/mux v1.7.4 // indirect
github.com/gorilla/websocket v1.4.2
github.com/gorilla/websocket v1.4.0
github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334
github.com/icza/dyno v0.0.0-20200205103839-49cb13720835
github.com/imdario/mergo v0.3.8
github.com/karrick/godirwalk v1.16.1
github.com/klauspost/compress v1.10.10 // indirect
github.com/klauspost/pgzip v1.2.4
github.com/magefile/mage v1.10.0 // indirect
github.com/julienschmidt/httprouter v1.2.0
github.com/magiconair/properties v1.8.1
github.com/mattn/go-colorable v0.1.7
github.com/mattn/go-shellwords v1.0.10 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/archiver/v3 v3.3.0
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db
github.com/morikuni/aec v1.0.0 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/nwaples/rardecode v1.1.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/olebedev/emitter v0.0.0-20190110104742-e8d1457e6aee
github.com/onsi/ginkgo v1.8.0 // indirect
github.com/onsi/gomega v1.5.0 // indirect
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pierrec/lz4 v2.5.2+incompatible // indirect
github.com/pkg/errors v0.9.1
github.com/pkg/profile v1.5.0
github.com/pkg/sftp v1.11.0
github.com/prometheus/common v0.11.1 // indirect
github.com/remeh/sizedwaitgroup v1.0.0
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94
github.com/spf13/cobra v1.0.0
github.com/spf13/pflag v1.0.5 // indirect
github.com/ulikunitz/xz v0.5.7 // indirect
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
golang.org/x/text v0.3.3 // indirect
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98 // indirect
google.golang.org/grpc v1.31.0 // indirect
google.golang.org/protobuf v1.25.0 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/ini.v1 v1.57.0
gopkg.in/yaml.v2 v2.3.0
github.com/pkg/errors v0.8.1
github.com/pkg/sftp v1.10.1 // indirect
github.com/pterodactyl/sftp-server v1.1.0
github.com/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce
github.com/sirupsen/logrus v1.0.5 // indirect
go.uber.org/atomic v1.5.1 // indirect
go.uber.org/multierr v1.4.0 // indirect
go.uber.org/zap v1.13.0
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f // indirect
golang.org/x/net v0.0.0-20190923162816-aa69164e4478
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect
golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab // indirect
golang.org/x/text v0.3.2 // indirect
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
golang.org/x/tools v0.0.0-20191206204035-259af5ff87bd // indirect
golang.org/x/tools/gopls v0.1.7 // indirect
gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect
gopkg.in/ini.v1 v1.51.0
gopkg.in/yaml.v2 v2.2.2
gotest.tools v2.2.0+incompatible // indirect
)

742
go.sum
View File

@@ -1,766 +1,212 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/AlecAivazis/survey/v2 v2.1.0 h1:AT4+23hOFopXYZaNGugbk7MWItkz0SfTmH/Hk92KeeE=
github.com/AlecAivazis/survey/v2 v2.1.0/go.mod h1:9FJRdMdDm8rnT+zHVbvQT2RTSTLq0Ttd6q3Vl2fahjk=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Jeffail/gabs/v2 v2.5.1 h1:ANfZYjpMlfTTKebycu4X1AgkVWumFVDYQl7JwOr4mDk=
github.com/Jeffail/gabs/v2 v2.5.1/go.mod h1:xCn81vdHKxFUuWWAaD5jCTQDNPBMh5pPs9IJ+NcziBI=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/NYTimes/logrotate v1.0.0 h1:6jFGbon6jOtpy3t3kwZZKS4Gdmf1C/Wv5J4ll4Xn5yk=
github.com/NYTimes/logrotate v1.0.0/go.mod h1:GxNz1cSw1c6t99PXoZlw+nm90H6cyQyrH66pjVv7x88=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/andybalholm/brotli v0.0.0-20190621154722-5f990b63d2d6 h1:bZ28Hqta7TFAK3Q08CMvv8y3/8ATaEqv2nGoc6yff6c=
github.com/andybalholm/brotli v0.0.0-20190621154722-5f990b63d2d6/go.mod h1:+lx6/Aqd1kLJ1GQfkvOnaZ1WGmLpMpbprPuIOOZX30U=
github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4=
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apex/log v1.8.0 h1:+W4j+dttibFvynPLlctdnYFUn1eLKT37BZWWW2iMfEM=
github.com/apex/log v1.8.0/go.mod h1:m82fZlWIuiWzWP04XCTXmnX0xRkYYbCdYn8jbJeLBEA=
github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo=
github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE=
github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY=
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 h1:WWB576BN5zNSZc/M9d/10pqEx5VHNhaQ/yOVAkmj5Yo=
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
github.com/Jeffail/gabs/v2 v2.2.0 h1:7touC+WzbQ7LO5+mwgxT44miyTqAVCOlIWLA6PiIB5w=
github.com/Jeffail/gabs/v2 v2.2.0/go.mod h1:xCn81vdHKxFUuWWAaD5jCTQDNPBMh5pPs9IJ+NcziBI=
github.com/Microsoft/go-winio v0.4.7 h1:vOvDiY/F1avSWlCWiKJjdYKz2jVjTK3pWPHndeG4OAY=
github.com/Microsoft/go-winio v0.4.7/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/buger/jsonparser v1.0.0 h1:etJTGF5ESxjI0Ic2UaLQs2LQQpa8G9ykQScukbh4L8A=
github.com/buger/jsonparser v1.0.0/go.mod h1:tgcrVJ81GPSF0mz+0nu1Xaz0fazGPrmmJfJtxjbHhUQ=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249 h1:R0IDH8daQ3lODvu8YtxnIqqth5qMGCJyADoUQvmLx4o=
github.com/cobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249/go.mod h1:EHKW9yNEYSBpTKzuu7Y9oOrft/UlzH57rMIB03oev6M=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/containerd/containerd v1.3.7 h1:eFSOChY8TTcxvkzp8g+Ov1RL0MYww7XEeK0y+zqGpVc=
github.com/containerd/containerd v1.3.7/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b h1:qUtCegLdOUVfVJOw+KDg6eJyE1TGvLlkGEd1091kSSQ=
github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creasty/defaults v1.5.0 h1:DW6NAGGaKuNSKkntc8BCBrR2KOUAcXVnfcwu/LmJhaQ=
github.com/creasty/defaults v1.5.0/go.mod h1:FPZ+Y0WNrbqOVw+c6av63eyHUAl6pMHZwqLPvXUZGfY=
github.com/buger/jsonparser v0.0.0-20181023193515-52c6e1462ebd/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23 h1:D21IyuvjDCshj1/qq+pCNd3VZOAEI9jy6Bi131YlXgI=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/buger/jsonparser v0.0.0-20191204142016-1a29609e0929 h1:MW/JDk68Rny52yI0M0N+P8lySNgB+NhpI/uAmhgOhUM=
github.com/buger/jsonparser v0.0.0-20191204142016-1a29609e0929/go.mod h1:tgcrVJ81GPSF0mz+0nu1Xaz0fazGPrmmJfJtxjbHhUQ=
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448 h1:PUD50EuOMkXVcpBIA/R95d56duJR9VxhwncsFbNnxW4=
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
github.com/creasty/defaults v1.3.0 h1:uG+RAxYbJgOPCOdKEcec9ZJXeva7Y6mj/8egdzwmLtw=
github.com/creasty/defaults v1.3.0/go.mod h1:CIEEvs7oIVZm30R8VxtFJs+4k201gReYyuYHJxZc68I=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/docker/cli v17.12.1-ce-rc2+incompatible h1:ESUycEAqvFuLglAHkUW66rCc2djYtd3i1x231svLq9o=
github.com/docker/cli v17.12.1-ce-rc2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible h1:iWPIG7pWIsCwT6ZtHnTUpoVMnete7O/pzd9HFE3+tn8=
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v0.0.0-20180422163414-57142e89befe h1:VW8TnWi0CZgg7oCv0wH6evNwkzcJg/emnw4HrVIWws4=
github.com/docker/docker v0.0.0-20180422163414-57142e89befe/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
github.com/franela/goblin v0.0.0-20200825194134-80c0062ed6cd h1:b/30UOB56Rhfe185ZfgvZT0/HOql0OzxuiNOxRKXRXc=
github.com/franela/goblin v0.0.0-20200825194134-80c0062ed6cd/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/frankban/quicktest v1.10.2 h1:19ARM85nVi4xH7xPXuc5eM/udya5ieh7b/Sv+d844Tk=
github.com/frankban/quicktest v1.10.2/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P6txr3mVT54s=
github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk=
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gabriel-vasile/mimetype v1.1.1 h1:qbN9MPuRf3bstHu9zkI9jDWNfH//9+9kHxr9oRBBBOA=
github.com/gabriel-vasile/mimetype v1.1.1/go.mod h1:6CDPel/o/3/s4+bp6kIbsWATq8pmgOisOPG40CJa6To=
github.com/gammazero/deque v0.0.0-20200227231300-1e9af0e52b46 h1:iX4+rD9Fjdx8SkmSO/O5WAIX/j79ll3kuqv5VdYt9J8=
github.com/gammazero/deque v0.0.0-20200227231300-1e9af0e52b46/go.mod h1:D90+MBHVc9Sk1lJAbEVgws0eYEurY4mv2TDso3Nxh3w=
github.com/gammazero/deque v0.0.0-20200721202602-07291166fe33 h1:UG4wNrJX9xSKnm/Gck5yTbxnOhpNleuE4MQRdmcGySo=
github.com/gammazero/deque v0.0.0-20200721202602-07291166fe33/go.mod h1:D90+MBHVc9Sk1lJAbEVgws0eYEurY4mv2TDso3Nxh3w=
github.com/gammazero/workerpool v1.0.0 h1:MfkJc6KL0tAmjrRDS203AZz3F+84Uod9YbL8KjpcQ00=
github.com/gammazero/workerpool v1.0.0/go.mod h1:/XWO2YAUUpPi3smDlFBl0vpX0JHwUomDM/oRMwRmnSs=
github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.2 h1:3t7jvTkeQfk1FdP0noXSNiM6AdBokLz7QmZDmnCHAAA=
github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.2/go.mod h1:AncDcjXz18xetI3A6STfXq2w+LuTx8pQ8bGEwRN8zVM=
github.com/gabriel-vasile/mimetype v0.1.4 h1:5mcsq3+DXypREUkW+1juhjeKmE/XnWgs+paHMJn7lf8=
github.com/gabriel-vasile/mimetype v0.1.4/go.mod h1:kMJbg3SlWZCsj4R73F1WDzbT9AyGCOVmUtIxxwO5pmI=
github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.0 h1:7KeiSrO5puFH1+vdAdbpiie2TrNnkvFc/eOQzT60Z2k=
github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.0/go.mod h1:D1+3UtCYAJ1os1PI+zhTVEj6Tb+IHJvXjXKz83OstmM=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-playground/validator/v10 v10.3.0 h1:nZU+7q+yJoFmwvNgv/LnPUkwPal62+b2xXj0AU1Es7o=
github.com/go-playground/validator/v10 v10.3.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721 h1:KRMr9A3qfbVM7iV/WcLY/rL5LICqwMHLhwRXKu99fXw=
github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/gotestyourself/gotestyourself v2.2.0+incompatible h1:AQwinXlbQR2HvPjQZOmDhRqsv5mZf+Jb1RnSLxcqZcI=
github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 h1:VHgatEHNcBFEB7inlalqfNqw65aNkM1lGX2yt3NmbS8=
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
github.com/icza/dyno v0.0.0-20200205103839-49cb13720835 h1:f1irK5f03uGGj+FjgQfZ5VhdKNVQVJ4skHsedzVohQ4=
github.com/icza/dyno v0.0.0-20200205103839-49cb13720835/go.mod h1:c1tRKs5Tx7E2+uHGSyyncziFjvGpgv4H2HrqXeUQ/Uk=
github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw=
github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.9.2 h1:LfVyl+ZlLlLDeQ/d2AqfGIIH4qEDu0Ed2S5GyhCWIWY=
github.com/klauspost/compress v1.9.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I=
github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.1 h1:oIPZROsWuPHpOdMVWLuJZXwgjhrW8r1yEX8UqMyeNHM=
github.com/klauspost/pgzip v1.2.1/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/klauspost/pgzip v1.2.4 h1:TQ7CNpYKovDOmqzRHKxJh0BeaBI7UdQZYc6p7pMQh1A=
github.com/klauspost/pgzip v1.2.4/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ=
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/magefile/mage v1.9.0 h1:t3AU2wNwehMCW97vuqQLtw6puppWXHO+O2MHo5a50XE=
github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/magefile/mage v1.10.0 h1:3HiXzCUY12kh9bIuyXShaVe529fJfyqoVM42o/uom2g=
github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw=
github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mholt/archiver/v3 v3.3.0 h1:vWjhY8SQp5yzM9P6OJ/eZEkmi3UAbRrxCq48MxjAzig=
github.com/mholt/archiver/v3 v3.3.0/go.mod h1:YnQtqsp+94Rwd0D/rk5cnLrxusUBUXg+08Ebtr1Mqao=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nwaples/rardecode v1.0.0 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs=
github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olebedev/emitter v0.0.0-20190110104742-e8d1457e6aee h1:IquUs3fIykn10zWDIyddanhpTqBvAHMaPnFhQuyYw5U=
github.com/olebedev/emitter v0.0.0-20190110104742-e8d1457e6aee/go.mod h1:eT2/Pcsim3XBjbvldGiJBvvgiqZkAFyiOJJsDKXs/ts=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI=
github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pkg/profile v1.5.0 h1:042Buzk+NhDI+DeSAA62RwJL8VAuZUMQZUjCsRz1Mug=
github.com/pkg/profile v1.5.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
github.com/pkg/sftp v1.11.0 h1:4Zv0OGbpkg4yNuUtH0s8rvoYxRCNyT29NVUo6pgPmxI=
github.com/pkg/sftp v1.11.0/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pkg/sftp v1.8.3 h1:9jSe2SxTM8/3bXZjtqnkgTBW+lA8db0knZJyns7gpBA=
github.com/pkg/sftp v1.8.3/go.mod h1:NxmoDg/QLVWluQDUYG7XBZTLUpKeFa8e3aMf1BfjyHk=
github.com/pkg/sftp v1.10.1 h1:VasscCm72135zRysgrJDKsntdmPN+OuU3+nnHYA9wyc=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.11.1 h1:0ZISXCMRuCZcxF77aT1BXY5m74mX2vrGYl1dSwBI0Jo=
github.com/prometheus/common v0.11.1/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3 h1:CTwfnzjQ+8dS6MhHHu4YswVAD99sL2wjPqP+VkURmKE=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E=
github.com/remeh/sizedwaitgroup v1.0.0/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/pterodactyl/sftp-server v1.0.4 h1:hPUaUQvA6U/R8/bybQFDMBDcZaqqj+kufGBQZ3esP5M=
github.com/pterodactyl/sftp-server v1.0.4/go.mod h1:0LKDl+f1qY2TH9+B5jxdROktW0+10UM1qJ472iWbyvQ=
github.com/pterodactyl/sftp-server v1.1.0 h1:NcYh+UqEH8pfvFsee6yt7eb08RLLidw6q+cNOCdh/V0=
github.com/pterodactyl/sftp-server v1.1.0/go.mod h1:b1VVWYv0RF9rxSZQqaD/rYXriiRMNPsbV//CKMXR4ag=
github.com/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce h1:aP+C+YbHZfOQlutA4p4soHi7rVUqHQdWEVMSkHfDTqY=
github.com/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94 h1:G04eS0JkAIVZfaJLjla9dNxkJCPiKIGZlw9AfOhzOD0=
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94/go.mod h1:b18R55ulyQ/h3RaWyloPyER7fWQVZvimKKhnI5OfrJQ=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8=
github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
github.com/sirupsen/logrus v1.0.5 h1:8c8b5uO0zS4X6RPl/sd1ENwSkIc0/H2PaHxE3udaE8I=
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tj/assert v0.0.0-20171129193455-018094318fb0 h1:Rw8kxzWo1mr6FSaYXjQELRe88y2KdfynXdnK72rdjtA=
github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk=
github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=
github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj52Uc=
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
github.com/tj/go-spin v1.1.0 h1:lhdWZsvImxvZ3q1C5OIB7d72DuOwP4O2NdBg9PyzNds=
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/ulikunitz/xz v0.5.7 h1:YvTNdFzX6+W5m9msiYg/zpkSURPPtOlzbqYjrFn7Yt4=
github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
github.com/uber-go/zap v1.9.1/go.mod h1:GY+83l3yxBcBw2kmHu/sAWwItnTn+ynxHCRo+WiIQOY=
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.5.1 h1:rsqfU5vBkVknbhUGbAUwQKR2H4ItV8tjJ+6kJX4cxHM=
go.uber.org/atomic v1.5.1/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.4.0 h1:f3WCSC2KzAcBXGATIxAB1E2XuCpNU255wNKZ505qi3E=
go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480 h1:O5YqonU5IWby+w98jVUG9h7zlCWCcH4RHyPVReBmhzk=
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e h1:nFYrTHrdrAOpShe27kaFHjsqYSEQ0KWqdWLu3xuZJts=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab h1:FvshnhkKW+LO3HWHodML8kuVX8rnJTxKm9dFPuI68UM=
golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s=
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190710153321-831012c29e42/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
golang.org/x/tools v0.0.0-20190918214516-5a1a30219888/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190925020647-22afafe3322a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191206204035-259af5ff87bd h1:Zc7EU2PqpsNeIfOoVA7hvQX4cS3YDJEs5KlfatT3hLo=
golang.org/x/tools v0.0.0-20191206204035-259af5ff87bd/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools/gopls v0.1.3/go.mod h1:vrCQzOKxvuiZLjCKSmbbov04oeBQQOb4VQqwYK2PWIY=
golang.org/x/tools/gopls v0.1.7/go.mod h1:PE3vTwT0ejw3a2L2fFgSJkxlEbA8Slbk+Lsy9hTmbG8=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98 h1:LCO0fg4kb6WwkXQXRQQgUYsFeFb5taTX5WAx5O/Vt28=
google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=

559
http.go Normal file
View File

@@ -0,0 +1,559 @@
package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"github.com/buger/jsonparser"
"github.com/gorilla/websocket"
"github.com/julienschmidt/httprouter"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/installer"
"github.com/pterodactyl/wings/server"
"go.uber.org/zap"
"io"
"net/http"
"os"
"strconv"
"strings"
)
// Retrieves a server out of the collection by UUID.
func (rt *Router) GetServer(uuid string) *server.Server {
return server.GetServers().Find(func(i *server.Server) bool {
return i.Uuid == uuid
})
}
type Router struct {
upgrader websocket.Upgrader
// The authentication token defined in the config.yml file that allows
// a request to perform any action aganist the daemon.
token string
}
func (rt *Router) AuthenticateRequest(h httprouter.Handle) httprouter.Handle {
return rt.AuthenticateToken(rt.AuthenticateServer(h))
}
// Middleware to protect server specific routes. This will ensure that the server exists and
// is in a state that allows it to be exposed to the API.
func (rt *Router) AuthenticateServer(h httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
if rt.GetServer(ps.ByName("server")) != nil {
h(w, r, ps)
return
}
http.NotFound(w, r)
}
}
// Attaches required access control headers to all of the requests.
func (rt *Router) AttachAccessControlHeaders(w http.ResponseWriter, r *http.Request, ps httprouter.Params) (http.ResponseWriter, *http.Request, httprouter.Params) {
w.Header().Set("Access-Control-Allow-Origin", config.Get().PanelLocation)
w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
return w, r, ps
}
// Authenticates the request token aganist the given permission string, ensuring that
// if it is a server permission, the token has control over that server. If it is a global
// token, this will ensure that the request is using a properly signed global token.
func (rt *Router) AuthenticateToken(h httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// Adds support for using this middleware on the websocket routes for servers. Those
// routes don't support Authorization headers, per the spec, so we abuse the socket
// protocol header and use that to pass the authorization token along to Wings without
// exposing the token in the URL directly. Neat. 📸
auth := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
if len(auth) != 2 || auth[0] != "Bearer" {
w.Header().Set("WWW-Authenticate", "Bearer")
http.Error(w, "authorization failed", http.StatusUnauthorized)
return
}
// Try to match the request aganist the global token for the Daemon, regardless
// of the permission type. If nothing is matched we will fall through to the Panel
// API to try and validate permissions for a server.
if auth[1] == rt.token {
h(rt.AttachAccessControlHeaders(w, r, ps))
return
}
// Happens because we don't have any of the server handling code here.
http.Error(w, "not implemented", http.StatusNotImplemented)
return
}
}
// Returns the basic Wings index page without anything else.
func (rt *Router) routeIndex(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Welcome!\n")
}
// Returns all of the servers that exist on the Daemon. This route is only accessible to
// requests that include an administrative control key, otherwise a 404 is returned. This
// authentication is handled by a middleware.
func (rt *Router) routeAllServers(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
json.NewEncoder(w).Encode(server.GetServers().All())
}
// Returns basic information about a single server found on the Daemon.
func (rt *Router) routeServer(w http.ResponseWriter, _ *http.Request, ps httprouter.Params) {
s := rt.GetServer(ps.ByName("server"))
json.NewEncoder(w).Encode(s)
}
type PowerActionRequest struct {
Action string `json:"action"`
}
type CreateDirectoryRequest struct {
Name string `json:"name"`
Path string `json:"path"`
}
func (pr *PowerActionRequest) IsValid() bool {
return pr.Action == "start" || pr.Action == "stop" || pr.Action == "kill" || pr.Action == "restart"
}
// Handles a request to control the power state of a server. If the action being passed
// through is invalid a 404 is returned. Otherwise, a HTTP/202 Accepted response is returned
// and the actual power action is run asynchronously so that we don't have to block the
// request until a potentially slow operation completes.
//
// This is done because for the most part the Panel is using websockets to determine when
// things are happening, so theres no reason to sit and wait for a request to finish. We'll
// just see over the socket if something isn't working correctly.
func (rt *Router) routeServerPower(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
s := rt.GetServer(ps.ByName("server"))
defer r.Body.Close()
dec := json.NewDecoder(r.Body)
var action PowerActionRequest
if err := dec.Decode(&action); err != nil {
// Don't flood the logs with error messages if someone sends through bad
// JSON data. We don't really care.
if err != io.EOF && err != io.ErrUnexpectedEOF {
zap.S().Errorw("failed to decode power action", zap.Error(err))
}
http.Error(w, "could not parse power action from request", http.StatusInternalServerError)
return
}
if !action.IsValid() {
http.NotFound(w, r)
return
}
// Because we route all of the actual bootup process to a seperate thread we need to
// check the suspension status here, otherwise the user will hit the endpoint and then
// just sit there wondering why it returns a success but nothing actually happens.
//
// We don't really care about any of the other actions at this point, they'll all result
// in the process being stopped, which should have happened anyways if the server is suspended.
if action.Action == "start" && s.Suspended {
http.Error(w, "server is suspended", http.StatusBadRequest)
return
}
// Pass the actual heavy processing off to a seperate thread to handle so that
// we can immediately return a response from the server.
go func(a string, s *server.Server) {
switch a {
case "start":
if err := s.Environment.Start(); err != nil {
zap.S().Errorw(
"encountered unexpected error starting server process",
zap.Error(err),
zap.String("server", s.Uuid),
zap.String("action", "start"),
)
}
break
case "stop":
if err := s.Environment.Stop(); err != nil {
zap.S().Errorw(
"encountered unexpected error stopping server process",
zap.Error(err),
zap.String("server", s.Uuid),
zap.String("action", "stop"),
)
}
break
case "restart":
break
case "kill":
if err := s.Environment.Terminate(os.Kill); err != nil {
zap.S().Errorw(
"encountered unexpected error killing server process",
zap.Error(err),
zap.String("server", s.Uuid),
zap.String("action", "kill"),
)
}
}
}(action.Action, s)
w.WriteHeader(http.StatusAccepted)
}
// Return the last 1Kb of the server log file.
func (rt *Router) routeServerLogs(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
s := rt.GetServer(ps.ByName("server"))
l, _ := strconv.ParseInt(r.URL.Query().Get("size"), 10, 64)
if l <= 0 {
l = 2048
}
out, err := s.ReadLogfile(l)
if err != nil {
zap.S().Errorw("failed to read server log file", zap.Error(err))
http.Error(w, "failed to read log", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(struct{ Data []string `json:"data"` }{Data: out})
}
// Handle a request to get the contents of a file on the server.
func (rt *Router) routeServerFileRead(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
s := rt.GetServer(ps.ByName("server"))
cleaned, err := s.Filesystem.SafePath(r.URL.Query().Get("file"))
if err != nil {
http.NotFound(w, r)
return
}
st, err := s.Filesystem.Stat(cleaned)
if err != nil {
if !os.IsNotExist(err) {
zap.S().Errorw("failed to stat file for reading", zap.String("path", ps.ByName("path")), zap.String("server", s.Uuid), zap.Error(err))
http.Error(w, "failed to stat file", http.StatusInternalServerError)
return
}
http.NotFound(w, r)
return
}
f, err := os.OpenFile(cleaned, os.O_RDONLY, 0)
if err != nil {
if !os.IsNotExist(err) {
zap.S().Errorw("failed to open file for reading", zap.String("path", ps.ByName("path")), zap.String("server", s.Uuid), zap.Error(err))
}
http.Error(w, "failed to open file", http.StatusInternalServerError)
return
}
defer f.Close()
w.Header().Set("X-Mime-Type", st.Mimetype)
w.Header().Set("Content-Length", strconv.Itoa(int(st.Info.Size())))
// If a download parameter is included in the URL go ahead and attach the necessary headers
// so that the file can be downloaded.
if r.URL.Query().Get("download") != "" {
w.Header().Set("Content-Disposition", "attachment; filename="+st.Info.Name())
w.Header().Set("Content-Type", "application/octet-stream")
}
bufio.NewReader(f).WriteTo(w)
}
// Lists the contents of a directory.
func (rt *Router) routeServerListDirectory(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
s := rt.GetServer(ps.ByName("server"))
stats, err := s.Filesystem.ListDirectory(r.URL.Query().Get("directory"))
if os.IsNotExist(err) {
http.NotFound(w, r)
return
} else if err != nil {
zap.S().Errorw("failed to list contents of directory", zap.String("server", s.Uuid), zap.String("path", ps.ByName("path")), zap.Error(err))
http.Error(w, "failed to list directory", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(stats)
}
// Writes a file to the system for the server.
func (rt *Router) routeServerWriteFile(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
s := rt.GetServer(ps.ByName("server"))
p := r.URL.Query().Get("file")
defer r.Body.Close()
err := s.Filesystem.Writefile(p, r.Body)
if err != nil {
zap.S().Errorw("failed to write file to directory", zap.String("server", s.Uuid), zap.String("path", p), zap.Error(err))
http.Error(w, "failed to write file to directory", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
// Creates a new directory for the server.
func (rt *Router) routeServerCreateDirectory(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
s := rt.GetServer(ps.ByName("server"))
defer r.Body.Close()
dec := json.NewDecoder(r.Body)
var data CreateDirectoryRequest
if err := dec.Decode(&data); err != nil {
// Don't flood the logs with error messages if someone sends through bad
// JSON data. We don't really care.
if err != io.EOF && err != io.ErrUnexpectedEOF {
zap.S().Errorw("failed to decode directory creation data", zap.Error(err))
}
http.Error(w, "could not parse data in request", http.StatusUnprocessableEntity)
return
}
if err := s.Filesystem.CreateDirectory(data.Name, data.Path); err != nil {
zap.S().Errorw("failed to create directory for server", zap.String("server", s.Uuid), zap.Error(err))
http.Error(w, "an error was encountered while creating the directory", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (rt *Router) routeServerRenameFile(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
s := rt.GetServer(ps.ByName("server"))
defer r.Body.Close()
data := rt.ReaderToBytes(r.Body)
oldPath, _ := jsonparser.GetString(data, "rename_from")
newPath, _ := jsonparser.GetString(data, "rename_to")
if oldPath == "" || newPath == "" {
http.Error(w, "invalid paths provided; did you forget to provide an old path and new path?", http.StatusUnprocessableEntity)
return
}
if err := s.Filesystem.Rename(oldPath, newPath); err != nil {
zap.S().Errorw("failed to rename file on server", zap.String("server", s.Uuid), zap.Error(err))
http.Error(w, "an error occurred while renaming the file", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (rt *Router) routeServerCopyFile(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
s := rt.GetServer(ps.ByName("server"))
defer r.Body.Close()
data := rt.ReaderToBytes(r.Body)
loc, _ := jsonparser.GetString(data, "location")
if err := s.Filesystem.Copy(loc); err != nil {
zap.S().Errorw("error copying file for server", zap.String("server", s.Uuid), zap.Error(err))
http.Error(w, "an error occurred while copying the file", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (rt *Router) routeServerDeleteFile(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
s := rt.GetServer(ps.ByName("server"))
defer r.Body.Close()
data := rt.ReaderToBytes(r.Body)
loc, _ := jsonparser.GetString(data, "location")
if err := s.Filesystem.Delete(loc); err != nil {
zap.S().Errorw("failed to delete a file or directory for server", zap.String("server", s.Uuid), zap.Error(err))
http.Error(w, "an error occurred while trying to delete a file or directory", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (rt *Router) routeServerSendCommand(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
s := rt.GetServer(ps.ByName("server"))
defer r.Body.Close()
if running, err := s.Environment.IsRunning(); !running || err != nil {
http.Error(w, "cannot send commands to a stopped instance", http.StatusBadGateway)
return
}
data := rt.ReaderToBytes(r.Body)
commands, dt, _, _ := jsonparser.Get(data, "commands")
if dt != jsonparser.Array {
http.Error(w, "commands must be an array of strings", http.StatusUnprocessableEntity)
return
}
for _, command := range commands {
if err := s.Environment.SendCommand(string(command)); err != nil {
zap.S().Warnw("failed to send command to server", zap.Any("command", command), zap.Error(err))
return
}
}
w.WriteHeader(http.StatusNoContent)
}
func (rt *Router) routeServerUpdate(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
s := rt.GetServer(ps.ByName("server"))
defer r.Body.Close()
data := rt.ReaderToBytes(r.Body)
if err := s.UpdateDataStructure(data, true); err != nil {
zap.S().Errorw("failed to update a server's data structure", zap.String("server", s.Uuid), zap.Error(err))
http.Error(w, "failed to update data structure", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (rt *Router) routeCreateServer(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
defer r.Body.Close()
inst, err := installer.New(rt.ReaderToBytes(r.Body))
if err != nil {
zap.S().Warnw("failed to validate the received data", zap.Error(err))
http.Error(w, "failed to validate data", http.StatusUnprocessableEntity)
return
}
// Plop that server instance onto the request so that it can be referenced in
// requests from here-on out.
server.GetServers().Add(inst.Server())
// Begin the installation process in the background to not block the request
// cycle. If there are any errors they will be logged and communicated back
// to the Panel where a reinstall may take place.
zap.S().Infow("beginning installation process for server", zap.String("server", inst.Uuid()))
go inst.Execute()
w.WriteHeader(http.StatusAccepted)
}
func (rt *Router) routeSystemInformation(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
defer r.Body.Close()
s, err := GetSystemInformation()
if err != nil {
zap.S().Errorw("failed to retrieve system information", zap.Error(errors.WithStack(err)))
http.Error(w, "failed to retrieve information", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(s)
}
func (rt *Router) routeServerDelete(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
s := rt.GetServer(ps.ByName("server"))
defer r.Body.Close()
// Immediately suspend the server to prevent a user from attempting
// to start it while this process is running.
s.Suspended = true
// Destroy the environment; in Docker this will handle a running container and
// forcibly terminate it before removing the container, so we do not need to handle
// that here.
if err := s.Environment.Destroy(); err != nil {
zap.S().Errorw("failed to destroy server environment", zap.Error(errors.WithStack(err)))
http.Error(w, "failed to destroy server environment", http.StatusInternalServerError)
return
}
// Once the environment is terminated, remove the server files from the system. This is
// done in a seperate process since failure is not the end of the world and can be
// manually cleaned up after the fact.
//
// In addition, servers with large amounts of files can take some time to finish deleting
// so we don't want to block the HTTP call while waiting on this.
go func(p string) {
if err := os.RemoveAll(p); err != nil {
zap.S().Warnw("failed to remove server files on deletion", zap.String("path", p), zap.Error(errors.WithStack(err)))
}
}(s.Filesystem.Path())
var uuid = s.Uuid
server.GetServers().Remove(func(s2 *server.Server) bool {
return s2.Uuid == uuid
})
s = nil
// Remove the configuration file stored on the Daemon for this server.
go func(u string) {
if err := os.Remove("data/servers/" + u + ".yml"); err != nil {
zap.S().Warnw("failed to delete server configuration file on deletion", zap.String("server", u), zap.Error(errors.WithStack(err)))
}
}(uuid)
w.WriteHeader(http.StatusAccepted)
}
func (rt *Router) ReaderToBytes(r io.Reader) []byte {
buf := bytes.Buffer{}
buf.ReadFrom(r)
return buf.Bytes()
}
// Configures the router and all of the associated routes.
func (rt *Router) ConfigureRouter() *httprouter.Router {
router := httprouter.New()
router.OPTIONS("/api/system", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
rt.AttachAccessControlHeaders(w, r, ps)
})
router.GET("/", rt.routeIndex)
router.GET("/api/system", rt.AuthenticateToken(rt.routeSystemInformation))
router.GET("/api/servers", rt.AuthenticateToken(rt.routeAllServers))
router.GET("/api/servers/:server", rt.AuthenticateRequest(rt.routeServer))
router.GET("/api/servers/:server/ws", rt.AuthenticateServer(rt.routeWebsocket))
router.GET("/api/servers/:server/logs", rt.AuthenticateRequest(rt.routeServerLogs))
router.GET("/api/servers/:server/files/contents", rt.AuthenticateRequest(rt.routeServerFileRead))
router.GET("/api/servers/:server/files/list-directory", rt.AuthenticateRequest(rt.routeServerListDirectory))
router.PUT("/api/servers/:server/files/rename", rt.AuthenticateRequest(rt.routeServerRenameFile))
router.POST("/api/servers", rt.AuthenticateToken(rt.routeCreateServer));
router.POST("/api/servers/:server/files/copy", rt.AuthenticateRequest(rt.routeServerCopyFile))
router.POST("/api/servers/:server/files/write", rt.AuthenticateRequest(rt.routeServerWriteFile))
router.POST("/api/servers/:server/files/create-directory", rt.AuthenticateRequest(rt.routeServerCreateDirectory))
router.POST("/api/servers/:server/files/delete", rt.AuthenticateRequest(rt.routeServerDeleteFile))
router.POST("/api/servers/:server/power", rt.AuthenticateRequest(rt.routeServerPower))
router.POST("/api/servers/:server/commands", rt.AuthenticateRequest(rt.routeServerSendCommand))
router.PATCH("/api/servers/:server", rt.AuthenticateRequest(rt.routeServerUpdate))
router.DELETE("/api/servers/:server", rt.AuthenticateRequest(rt.routeServerDelete))
return router
}

View File

@@ -1,19 +0,0 @@
package installer
type validationError struct {
msg string
}
func (e *validationError) Error() string {
return e.msg
}
func IsValidationError(err error) bool {
_, ok := err.(*validationError)
return ok
}
func NewValidationError(msg string) error {
return &validationError{msg: msg}
}

View File

@@ -5,9 +5,9 @@ import (
"github.com/asaskevich/govalidator"
"github.com/buger/jsonparser"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/server"
"go.uber.org/zap"
)
type Installer struct {
@@ -19,34 +19,42 @@ type Installer struct {
// calling Execute().
func New(data []byte) (*Installer, error) {
if !govalidator.IsUUIDv4(getString(data, "uuid")) {
return nil, NewValidationError("uuid provided was not in a valid format")
return nil, errors.New("uuid provided was not in a valid format")
}
cfg := &server.Configuration{
Uuid: getString(data, "uuid"),
Suspended: false,
Invocation: getString(data, "invocation"),
SkipEggScripts: getBoolean(data, "skip_egg_scripts"),
Build: environment.Limits{
if !govalidator.IsUUIDv4(getString(data, "service", "egg")) {
return nil, errors.New("service egg provided was not in a valid format")
}
s := &server.Server{
Uuid: getString(data, "uuid"),
Suspended: false,
State: server.ProcessOfflineState,
Invocation: getString(data, "invocation"),
EnvVars: make(map[string]string),
Build: server.BuildSettings{
MemoryLimit: getInt(data, "build", "memory"),
Swap: getInt(data, "build", "swap"),
IoWeight: uint16(getInt(data, "build", "io")),
CpuLimit: getInt(data, "build", "cpu"),
DiskSpace: getInt(data, "build", "disk"),
Threads: getString(data, "build", "threads"),
},
CrashDetectionEnabled: true,
Allocations: server.Allocations{
Mappings: make(map[string][]int),
},
}
cfg.Allocations.DefaultMapping.Ip = getString(data, "allocations", "default", "ip")
cfg.Allocations.DefaultMapping.Port = int(getInt(data, "allocations", "default", "port"))
s.Init()
s.Allocations.DefaultMapping.Ip = getString(data, "allocations", "default", "ip")
s.Allocations.DefaultMapping.Port = int(getInt(data, "allocations", "default", "port"))
// Unmarshal the environment variables from the request into the server struct.
if b, _, _, err := jsonparser.Get(data, "environment"); err != nil {
return nil, errors.WithStack(err)
} else {
cfg.EnvVars = make(environment.Variables)
if err := json.Unmarshal(b, &cfg.EnvVars); err != nil {
s.EnvVars = make(map[string]string)
if err := json.Unmarshal(b, &s.EnvVars); err != nil {
return nil, errors.WithStack(err)
}
}
@@ -55,35 +63,34 @@ func New(data []byte) (*Installer, error) {
if b, _, _, err := jsonparser.Get(data, "allocations", "mappings"); err != nil {
return nil, errors.WithStack(err)
} else {
cfg.Allocations.Mappings = make(map[string][]int)
if err := json.Unmarshal(b, &cfg.Allocations.Mappings); err != nil {
s.Allocations.Mappings = make(map[string][]int)
if err := json.Unmarshal(b, &s.Allocations.Mappings); err != nil {
return nil, errors.WithStack(err)
}
}
cfg.Container.Image = getString(data, "container", "image")
s.Container.Image = getString(data, "container", "image")
c, rerr, err := api.NewRequester().GetServerConfiguration(cfg.Uuid)
if err != nil || rerr != nil {
if err != nil {
return nil, errors.WithStack(err)
}
return nil, errors.New(rerr.String())
b, err := s.WriteConfigurationToDisk()
if err != nil {
return nil, err
}
// Destroy the temporary server instance.
s = nil
// Create a new server instance using the configuration we wrote to the disk
// so that everything gets instantiated correctly on the struct.
s, err := server.FromConfiguration(c)
s2, err := server.FromConfiguration(b, &config.Get().System)
return &Installer{
server: s,
server: s2,
}, err
}
// Returns the UUID associated with this installer instance.
func (i *Installer) Uuid() string {
return i.server.Id()
return i.server.Uuid
}
// Return the server instance.
@@ -91,6 +98,19 @@ func (i *Installer) Server() *server.Server {
return i.server
}
// Executes the installer process, creating the server and running through the
// associated installation process based on the parameters passed through for
// the server instance.
func (i *Installer) Execute() {
zap.S().Debugw("creating required environment for server instance", zap.String("server", i.Uuid()))
if err := i.server.Environment.Create(); err != nil {
zap.S().Errorw("failed to create environment for server", zap.String("server", i.Uuid()), zap.Error(err))
return
}
zap.S().Debugw("created environment for server during install process", zap.String("server", i.Uuid()))
}
// Returns a string value from the JSON data provided.
func getString(data []byte, key ...string) string {
value, _ := jsonparser.GetString(data, key...)
@@ -104,9 +124,3 @@ func getInt(data []byte, key ...string) int64 {
return value
}
func getBoolean(data []byte, key ...string) bool {
value, _ := jsonparser.GetBoolean(data, key...)
return value
}

View File

@@ -1,114 +0,0 @@
package cli
import (
"fmt"
"github.com/apex/log"
"github.com/apex/log/handlers/cli"
color2 "github.com/fatih/color"
"github.com/mattn/go-colorable"
"github.com/pkg/errors"
"io"
"os"
"sync"
"time"
)
var Default = New(os.Stderr, true)
var bold = color2.New(color2.Bold)
var Strings = [...]string{
log.DebugLevel: "DEBUG",
log.InfoLevel: " INFO",
log.WarnLevel: " WARN",
log.ErrorLevel: "ERROR",
log.FatalLevel: "FATAL",
}
type Handler struct {
mu sync.Mutex
Writer io.Writer
Padding int
}
func New(w io.Writer, useColors bool) *Handler {
if f, ok := w.(*os.File); ok {
if useColors {
return &Handler{Writer: colorable.NewColorable(f), Padding: 2}
}
}
return &Handler{Writer: colorable.NewNonColorable(w), Padding: 2}
}
type tracer interface {
StackTrace() errors.StackTrace
}
// HandleLog implements log.Handler.
func (h *Handler) HandleLog(e *log.Entry) error {
color := cli.Colors[e.Level]
level := Strings[e.Level]
names := e.Fields.Names()
h.mu.Lock()
defer h.mu.Unlock()
color.Fprintf(h.Writer, "%s: [%s] %-25s", bold.Sprintf("%*s", h.Padding+1, level), time.Now().Format(time.StampMilli), e.Message)
for _, name := range names {
if name == "source" {
continue
}
fmt.Fprintf(h.Writer, " %s=%v", color.Sprint(name), e.Fields.Get(name))
}
fmt.Fprintln(h.Writer)
for _, name := range names {
if name != "error" {
continue
}
var br = color2.New(color2.Bold, color2.FgRed)
if err, ok := e.Fields.Get("error").(error); ok {
fmt.Fprintf(h.Writer, "\n%s%+v\n\n", br.Sprintf("Stacktrace:"), getErrorStack(err, false))
} else {
fmt.Fprintf(h.Writer, "\n%s%+v\n\n", br.Sprintf("Invalid Error:"), err)
}
}
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]
}

View File

@@ -1,9 +1,7 @@
package parser
import (
"bytes"
"github.com/Jeffail/gabs/v2"
"github.com/apex/log"
"github.com/buger/jsonparser"
"github.com/iancoleman/strcase"
"github.com/pkg/errors"
@@ -46,20 +44,32 @@ func readFileBytes(path string) ([]byte, error) {
return ioutil.ReadAll(file)
}
// Helper function to set the value of the JSON key item based on the jsonparser value
// type returned.
func setPathway(c *gabs.Container, path string, value []byte, vt jsonparser.ValueType) error {
v := getKeyValue(value, vt)
_, err := c.SetP(v, path)
return err
}
// Gets the value of a key based on the value type defined.
func (cfr *ConfigurationFileReplacement) getKeyValue(value []byte) interface{} {
if cfr.ReplaceWith.Type() == jsonparser.Boolean {
v, _ := strconv.ParseBool(string(value))
return v
func getKeyValue(value []byte, vt jsonparser.ValueType) interface{} {
switch vt {
case jsonparser.Number:
{
v, _ := strconv.Atoi(string(value))
return v
}
case jsonparser.Boolean:
{
v, _ := strconv.ParseBool(string(value))
return v
}
default:
return string(value)
}
// Try to parse into an int, if this fails just ignore the error and continue
// through, returning the string.
if v, err := strconv.Atoi(string(value)); err == nil {
return v
}
return string(value)
}
// Iterate over an unstructured JSON/YAML/etc. interface and set all of the required
@@ -70,19 +80,17 @@ func (cfr *ConfigurationFileReplacement) getKeyValue(value []byte) interface{} {
// configurations per-world (such as Spigot and Bungeecord) where we'll need to make
// adjustments to the bind address for the user.
//
// This does not currently support nested wildcard matches. For example, foo.*.bar
// will work, however foo.*.bar.*.baz will not, since we'll only be splitting at the
// first wildcard, and not subsequent ones.
// This does not currently support nested matches. container.*.foo.*.bar will not work.
func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error) {
parsed, err := gabs.ParseJSON(data)
if err != nil {
return nil, errors.WithStack(err)
return nil, err
}
for _, v := range f.Replace {
value, err := f.LookupConfigurationValue(v)
value, dt, err := f.LookupConfigurationValue(v)
if err != nil {
return nil, errors.WithStack(err)
return nil, err
}
// Check for a wildcard character, and if found split the key on that value to
@@ -96,21 +104,13 @@ func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error
// If the child is a null value, nothing will happen. Seems reasonable as of the
// time this code is being written.
for _, child := range parsed.Path(strings.Trim(parts[0], ".")).Children() {
if err := v.SetAtPathway(child, strings.Trim(parts[1], "."), []byte(value)); err != nil {
if errors.Is(err, gabs.ErrNotFound) {
continue
}
return nil, errors.Wrap(err, "failed to set config value of array child")
if err := setPathway(child, strings.Trim(parts[1], "."), value, dt); err != nil {
return nil, err
}
}
} else {
if err = v.SetAtPathway(parsed, v.Match, []byte(value)); err != nil {
if errors.Is(err, gabs.ErrNotFound) {
continue
}
return nil, errors.Wrap(err, "unable to set config value at pathway: "+v.Match)
if err = setPathway(parsed, v.Match, value, dt); err != nil {
return nil, err
}
}
}
@@ -118,150 +118,41 @@ func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error
return parsed, nil
}
// Regex used to check if there is an array element present in the given pathway by looking for something
// along the lines of "something[1]" or "something[1].nestedvalue" as the path.
var checkForArrayElement = regexp.MustCompile(`^([^\[\]]+)\[([\d]+)](\..+)?$`)
// Attempt to set the value of the path depending on if it is an array or not. Gabs cannot handle array
// values as "something[1]" but can parse them just fine. This is basically just overly complex code
// to handle that edge case and ensure the value gets set correctly.
//
// Bless thee who has to touch these most unholy waters.
func setValueAtPath(c *gabs.Container, path string, value interface{}) error {
var err error
matches := checkForArrayElement.FindStringSubmatch(path)
if len(matches) < 3 {
// Only update the value if the pathway actually exists in the configuration, otherwise
// do nothing.
if c.ExistsP(path) {
_, err = c.SetP(value, path)
}
return errors.WithStack(err)
}
i, _ := strconv.Atoi(matches[2])
// Find the array element "i" or try to create it if "i" is equal to 0 and is not found
// at the given path.
ct, err := c.ArrayElementP(i, matches[1])
if err != nil {
if i != 0 || (!errors.Is(err, gabs.ErrNotArray) && !errors.Is(err, gabs.ErrNotFound)) {
return errors.Wrap(err, "error while parsing array element at path")
}
var t = make([]interface{}, 1)
// If the length of matches is 4 it means we're trying to access an object down in this array
// key, so make sure we generate the array as an array of objects, and not just a generic nil
// array.
if len(matches) == 4 {
t = []interface{}{map[string]interface{}{}}
}
// If the error is because this isn't an array or isn't found go ahead and create the array with
// an empty object if we have additional things to set on the array, or just an empty array type
// if there is not an object structure detected (no matches[3] available).
if _, err = c.SetP(t, matches[1]); err != nil {
return errors.Wrap(err, "failed to create empty array for missing element")
}
// Set our cursor to be the array element we expect, which in this case is just the first element
// since we won't run this code unless the array element is 0. There is too much complexity in trying
// to match additional elements. In those cases the server will just have to be rebooted or something.
ct, err = c.ArrayElementP(0, matches[1])
if err != nil {
return errors.Wrap(err, "failed to find array element at path")
}
}
// Try to set the value. If the path does not exist an error will be raised to the caller which will
// then check if the error is because the path is missing. In those cases we just ignore the error since
// we don't want to do anything specifically when that happens.
//
// If there are four matches in the regex it means that we managed to also match a trailing pathway
// for the key, which should be found in the given array key item and modified further.
if len(matches) == 4 {
_, err = ct.SetP(value, strings.TrimPrefix(matches[3], "."))
} else {
_, err = ct.Set(value)
}
if err != nil {
return errors.Wrap(err, "failed to set value at config path: "+path)
}
return nil
}
// Sets the value at a specific pathway, but checks if we were looking for a specific
// value or not before doing it.
func (cfr *ConfigurationFileReplacement) SetAtPathway(c *gabs.Container, path string, value []byte) error {
if cfr.IfValue == "" {
return setValueAtPath(c, path, cfr.getKeyValue(value))
}
// If this is a regex based matching, we need to get a little more creative since
// we're only going to replacing part of the string, and not the whole thing.
if c.ExistsP(path) && strings.HasPrefix(cfr.IfValue, "regex:") {
// We're doing some regex here.
r, err := regexp.Compile(strings.TrimPrefix(cfr.IfValue, "regex:"))
if err != nil {
log.WithFields(log.Fields{"if_value": strings.TrimPrefix(cfr.IfValue, "regex:"), "error": err}).
Warn("configuration if_value using invalid regexp, cannot perform replacement")
return nil
}
// If the path exists and there is a regex match, go ahead and attempt the replacement
// using the value we got from the key. This will only replace the one match.
v := strings.Trim(string(c.Path(path).Bytes()), "\"")
if r.Match([]byte(v)) {
return setValueAtPath(c, path, r.ReplaceAllString(v, string(value)))
}
return nil
} else if !c.ExistsP(path) || (c.ExistsP(path) && !bytes.Equal(c.Bytes(), []byte(cfr.IfValue))) {
return nil
}
return setValueAtPath(c, path, cfr.getKeyValue(value))
}
// Looks up a configuration value on the Daemon given a dot-notated syntax.
func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplacement) (string, error) {
// If this is not something that we can do a regex lookup on then just continue
// on our merry way. If the value isn't a string, we're not going to be doing anything
// with it anyways.
if cfr.ReplaceWith.Type() != jsonparser.String || !configMatchRegex.Match(cfr.ReplaceWith.Value()) {
return cfr.ReplaceWith.String(), nil
func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplacement) ([]byte, jsonparser.ValueType, error) {
if !configMatchRegex.Match([]byte(cfr.Value)) {
return []byte(cfr.Value), cfr.ValueType, nil
}
// If there is a match, lookup the value in the configuration for the Daemon. If no key
// is found, just return the string representation, otherwise use the value from the
// daemon configuration here.
huntPath := configMatchRegex.ReplaceAllString(
configMatchRegex.FindString(cfr.ReplaceWith.String()), "$1",
configMatchRegex.FindString(cfr.Value), "$1",
)
var path []string
// The camel casing is important here, the configuration for the Daemon does not use
// JSON, and as such all of the keys will be generated in CamelCase format, rather than
// the expected snake_case from the old Daemon.
for _, value := range strings.Split(huntPath, ".") {
path = append(path, strcase.ToSnake(value))
path = append(path, strcase.ToCamel(value))
}
// Look for the key in the configuration file, and if found return that value to the
// calling function.
match, _, _, err := jsonparser.Get(f.configuration, path...)
match, dt, _, err := jsonparser.Get(f.configuration, path...)
if err != nil {
if err != jsonparser.KeyPathNotFoundError {
return string(match), errors.WithStack(err)
return match, dt, errors.WithStack(err)
}
log.WithFields(log.Fields{"path": path, "filename": f.FileName}).Debug("attempted to load a configuration value that does not exist")
// If there is no key, keep the original value intact, that way it is obvious there
// is a replace issue at play.
return string(match), nil
return []byte(cfr.Value), cfr.ValueType, nil
} else {
return configMatchRegex.ReplaceAllString(cfr.ReplaceWith.String(), string(match)), nil
replaced := []byte(configMatchRegex.ReplaceAllString(cfr.Value, string(match)))
return replaced, cfr.ValueType, nil
}
}

View File

@@ -3,20 +3,17 @@ package parser
import (
"bufio"
"encoding/json"
"github.com/apex/log"
"fmt"
"github.com/beevik/etree"
"github.com/buger/jsonparser"
"github.com/icza/dyno"
"github.com/ghodss/yaml"
"github.com/magiconair/properties"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config"
"go.uber.org/zap"
"gopkg.in/ini.v1"
"gopkg.in/yaml.v2"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
)
@@ -32,10 +29,6 @@ const (
type ConfigurationParser string
func (cp ConfigurationParser) String() string {
return string(cp)
}
// Defines a configuration file for the server startup. These will be looped over
// and modified before the server finishes booting.
type ConfigurationFile struct {
@@ -48,82 +41,31 @@ type ConfigurationFile struct {
configuration []byte
}
// Custom unmarshaler for configuration files. If there is an error while parsing out the
// replacements, don't fail the entire operation, just log a global warning so someone can
// find the issue, and return an empty array of replacements.
//
// I imagine people will notice configuration replacement isn't working correctly and then
// the logs should help better expose that issue.
func (f *ConfigurationFile) UnmarshalJSON(data []byte) error {
var m map[string]*json.RawMessage
if err := json.Unmarshal(data, &m); err != nil {
return err
}
if err := json.Unmarshal(*m["file"], &f.FileName); err != nil {
return err
}
if err := json.Unmarshal(*m["parser"], &f.Parser); err != nil {
return err
}
if err := json.Unmarshal(*m["replace"], &f.Replace); err != nil {
log.WithField("file", f.FileName).WithField("error", err).Warn("failed to unmarshal configuration file replacement")
f.Replace = []ConfigurationFileReplacement{}
}
return nil
}
// Regex to match paths such as foo[1].bar[2] and convert them into a format that
// gabs can work with, such as foo.1.bar.2 in this case. This is applied when creating
// the struct for the configuration file replacements.
var cfrMatchReplacement = regexp.MustCompile(`\[(\d+)]`)
// Defines a single find/replace instance for a given server configuration file.
type ConfigurationFileReplacement struct {
Match string `json:"match"`
IfValue string `json:"if_value"`
ReplaceWith ReplaceValue `json:"replace_with"`
Match string `json:"match"`
Value string `json:"value"`
ValueType jsonparser.ValueType `json:"-"`
}
// Handles unmarshaling the JSON representation into a struct that provides more useful
// data to this functionality.
func (cfr *ConfigurationFileReplacement) UnmarshalJSON(data []byte) error {
m, err := jsonparser.GetString(data, "match")
if err != nil {
if m, err := jsonparser.GetString(data, "match"); err != nil {
return err
} else {
cfr.Match = m
}
cfr.Match = m
iv, err := jsonparser.GetString(data, "if_value")
// We only check keypath here since match & replace_with should be present on all of
// them, however if_value is optional.
if err != nil && err != jsonparser.KeyPathNotFoundError {
if v, dt, _, err := jsonparser.Get(data, "value"); err != nil {
return err
}
cfr.IfValue = iv
rw, dt, _, err := jsonparser.Get(data, "replace_with")
if err != nil {
if err != jsonparser.KeyPathNotFoundError {
return err
} else {
if dt != jsonparser.String && dt != jsonparser.Number && dt != jsonparser.Boolean {
return errors.New(
fmt.Sprintf("cannot parse JSON: received unexpected replacement value type: %d", dt),
)
}
// Okay, likely dealing with someone who forgot to upgrade their eggs, so in
// that case, fallback to using the old key which was "value".
rw, dt, _, err = jsonparser.Get(data, "value")
if err != nil {
return err
}
}
cfr.ReplaceWith = ReplaceValue{
value: rw,
valueType: dt,
cfr.Value = string(v)
cfr.ValueType = dt
}
return nil
@@ -132,13 +74,10 @@ func (cfr *ConfigurationFileReplacement) UnmarshalJSON(data []byte) error {
// Parses a given configuration file and updates all of the values within as defined
// in the API response from the Panel.
func (f *ConfigurationFile) Parse(path string, internal bool) error {
log.WithField("path", path).WithField("parser", f.Parser.String()).Debug("parsing server configuration file")
zap.S().Debugw("parsing configuration file", zap.String("path", path), zap.String("parser", string(f.Parser)))
if mb, err := json.Marshal(config.Get()); err != nil {
return err
} else {
f.configuration = mb
}
mb, _ := json.Marshal(config.Get())
f.configuration = mb
var err error
@@ -163,20 +102,15 @@ func (f *ConfigurationFile) Parse(path string, internal bool) error {
break
}
if errors.Is(err, os.ErrNotExist) {
if os.IsNotExist(err) {
// File doesn't exist, we tried creating it, and same error is returned? Pretty
// sure this pathway is impossible, but if not, abort here.
if internal {
return nil
}
b := strings.TrimSuffix(path, filepath.Base(path))
if err := os.MkdirAll(b, 0755); err != nil {
return errors.Wrap(err, "failed to create base directory for missing configuration file")
} else {
if _, err := os.Create(path); err != nil {
return errors.Wrap(err, "failed to create missing configuration file")
}
if _, err := os.Create(path); err != nil {
return errors.WithStack(err)
}
return f.Parse(path, true)
@@ -205,7 +139,7 @@ func (f *ConfigurationFile) parseXmlFile(path string) error {
}
for i, replacement := range f.Replace {
value, err := f.LookupConfigurationValue(replacement)
value, _, err := f.LookupConfigurationValue(replacement)
if err != nil {
return err
}
@@ -242,13 +176,13 @@ func (f *ConfigurationFile) parseXmlFile(path string) error {
// Iterate over the elements we found and update their values.
for _, element := range doc.FindElements(path) {
if xmlValueMatchRegex.MatchString(value) {
k := xmlValueMatchRegex.ReplaceAllString(value, "$1")
v := xmlValueMatchRegex.ReplaceAllString(value, "$2")
if xmlValueMatchRegex.Match(value) {
k := xmlValueMatchRegex.ReplaceAllString(string(value), "$1")
v := xmlValueMatchRegex.ReplaceAllString(string(value), "$2")
element.CreateAttr(k, v)
} else {
element.SetText(value)
element.SetText(string(value))
}
}
}
@@ -265,11 +199,6 @@ func (f *ConfigurationFile) parseXmlFile(path string) error {
// Ensure the XML is indented properly.
doc.Indent(2)
// Truncate the file before attempting to write the changes.
if err := os.Truncate(path, 0); err != nil {
return err
}
// Write the XML to the file.
_, err = doc.WriteTo(file)
@@ -279,13 +208,12 @@ func (f *ConfigurationFile) parseXmlFile(path string) error {
// Parses an ini file.
func (f *ConfigurationFile) parseIniFile(path string) error {
// Ini package can't handle a non-existent file, so handle that automatically here
// by creating it if not exists. Then, immediately close the file since we will use
// other methods to write the new contents.
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0644)
// by creating it if not exists.
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0644);
if err != nil {
return err
}
file.Close()
defer file.Close()
cfg, err := ini.Load(path)
if err != nil {
@@ -295,7 +223,7 @@ func (f *ConfigurationFile) parseIniFile(path string) error {
for _, replacement := range f.Replace {
path := strings.SplitN(replacement.Match, ".", 2)
value, err := f.LookupConfigurationValue(replacement)
value, _, err := f.LookupConfigurationValue(replacement)
if err != nil {
return err
}
@@ -320,15 +248,19 @@ func (f *ConfigurationFile) parseIniFile(path string) error {
// If the key exists in the file go ahead and set the value, otherwise try to
// create it in the section.
if s.HasKey(k) {
s.Key(k).SetValue(value)
s.Key(k).SetValue(string(value))
} else {
if _, err := s.NewKey(k, value); err != nil {
if _, err := s.NewKey(k, string(value)); err != nil {
return err
}
}
}
return cfg.SaveTo(path)
if _, err := cfg.WriteTo(file); err != nil {
return err
}
return nil
}
// Parses a json file updating any matching key/value pairs. If a match is not found, the
@@ -354,33 +286,28 @@ func (f *ConfigurationFile) parseJsonFile(path string) error {
func (f *ConfigurationFile) parseYamlFile(path string) error {
b, err := readFileBytes(path)
if err != nil {
return errors.WithStack(err)
}
i := make(map[string]interface{})
if err := yaml.Unmarshal(b, &i); err != nil {
return errors.WithStack(err)
return err
}
// Unmarshal the yaml data into a JSON interface such that we can work with
// any arbitrary data structure. If we don't do this, I can't use gabs which
// makes working with unknown JSON significantly easier.
jsonBytes, err := json.Marshal(dyno.ConvertMapI2MapS(i))
// makes working with unknown JSON signficiantly easier.
jsonBytes, err := yaml.YAMLToJSON(b)
if err != nil {
return errors.WithStack(err)
return err
}
// Now that the data is converted, treat it just like JSON and pass it to the
// iterator function to update values as necessary.
data, err := f.IterateOverJson(jsonBytes)
if err != nil {
return errors.WithStack(err)
return err
}
// Remarshal the JSON into YAML format before saving it back to the disk.
marshaled, err := yaml.Marshal(data.Data())
marshaled, err := yaml.JSONToYAML(data.Bytes())
if err != nil {
return errors.WithStack(err)
return err
}
return ioutil.WriteFile(path, marshaled, 0644)
@@ -390,26 +317,39 @@ func (f *ConfigurationFile) parseYamlFile(path string) error {
// scanning a file and performing a replacement. You should attempt to use anything other
// than this function where possible.
func (f *ConfigurationFile) parseTextFile(path string) error {
input, err := ioutil.ReadFile(path)
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0644)
if err != nil {
return errors.WithStack(err)
return err
}
defer file.Close()
lines := strings.Split(string(input), "\n")
for i, line := range lines {
scanner := bufio.NewScanner(file)
for scanner.Scan() {
hasReplaced := false
t := scanner.Text()
// Iterate over the potential replacements for the line and check if there are
// any matches.
for _, replace := range f.Replace {
// If this line doesn't match what we expect for the replacement, move on to the next
// line. Otherwise, update the line to have the replacement value.
if !strings.HasPrefix(line, replace.Match) {
if !strings.HasPrefix(t, replace.Match) {
continue
}
lines[i] = replace.ReplaceWith.String()
hasReplaced = true
t = strings.Replace(t, replace.Match, replace.Value, 1)
}
// If there was a replacement that occurred on this specific line, do a write to the file
// immediately to write that modified content to the disk.
if hasReplaced {
if _, err := file.WriteAt([]byte(t), int64(len(scanner.Bytes()))); err != nil {
return err
}
}
}
if err := ioutil.WriteFile(path, []byte(strings.Join(lines, "\n")), 0644); err != nil {
return errors.WithStack(err)
if err := scanner.Err(); err != nil {
return err
}
return nil
@@ -418,84 +358,28 @@ func (f *ConfigurationFile) parseTextFile(path string) error {
// Parses a properties file and updates the values within it to match those that
// are passed. Writes the file once completed.
func (f *ConfigurationFile) parsePropertiesFile(path string) error {
// Open the file.
f2, err := os.Open(path)
if err != nil {
return errors.WithStack(err)
}
var s strings.Builder
// Get any header comments from the file.
scanner := bufio.NewScanner(f2)
for scanner.Scan() {
text := scanner.Text()
if len(text) > 0 && text[0] != '#' {
break
}
s.WriteString(text)
s.WriteString("\n")
}
// Close the file.
_ = f2.Close()
// Handle any scanner errors.
if err := scanner.Err(); err != nil {
return errors.WithStack(err)
}
// Decode the properties file.
p, err := properties.LoadFile(path, properties.UTF8)
if err != nil {
return errors.WithStack(err)
}
// Replace any values that need to be replaced.
for _, replace := range f.Replace {
data, err := f.LookupConfigurationValue(replace)
if err != nil {
return errors.WithStack(err)
}
v, ok := p.Get(replace.Match)
// Don't attempt to replace the value if we're looking for a specific value and
// it does not match. If there was no match at all in the file for this key but
// we're doing an IfValue match, do nothing.
if replace.IfValue != "" && (!ok || (ok && v != replace.IfValue)) {
continue
}
if _, _, err := p.Set(replace.Match, data); err != nil {
return errors.WithStack(err)
}
}
// Add the new file content to the string builder.
for _, key := range p.Keys() {
value, ok := p.Get(key)
if !ok {
continue
}
s.WriteString(key)
s.WriteByte('=')
s.WriteString(strings.Trim(strconv.QuoteToASCII(value), `"`))
s.WriteString("\n")
}
// Open the file for writing.
w, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return errors.WithStack(err)
}
defer w.Close()
// Write the data to the file.
if _, err := w.Write([]byte(s.String())); err != nil {
return err
}
return nil
for _, replace := range f.Replace {
data, _, err := f.LookupConfigurationValue(replace)
if err != nil {
return err
}
if _, _, err := p.Set(replace.Match, string(data)); err != nil {
return err
}
}
w, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
_, err = p.Write(w, properties.UTF8)
return err
}

View File

@@ -1,24 +0,0 @@
package parser
import (
"github.com/buger/jsonparser"
)
type ReplaceValue struct {
value []byte
valueType jsonparser.ValueType `json:"-"`
}
func (cv *ReplaceValue) Value() []byte {
return cv.value
}
func (cv *ReplaceValue) String() string {
str, _ := jsonparser.ParseString(cv.value)
return str
}
func (cv *ReplaceValue) Type() jsonparser.ValueType {
return cv.valueType
}

View File

@@ -1,143 +0,0 @@
package router
import (
"fmt"
"github.com/apex/log"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/server/filesystem"
"net/http"
"os"
"strings"
)
type RequestError struct {
Err error
Uuid string
Message string
server *server.Server
}
// Generates a new tracked error, which simply tracks the specific error that
// is being passed in, and also assigned a UUID to the error so that it can be
// cross referenced in the logs.
func TrackedError(err error) *RequestError {
return &RequestError{
Err: err,
Uuid: uuid.Must(uuid.NewRandom()).String(),
Message: "",
}
}
// Same as TrackedError, except this will also attach the server instance that
// generated this server for the purposes of logging.
func TrackedServerError(err error, s *server.Server) *RequestError {
return &RequestError{
Err: err,
Uuid: uuid.Must(uuid.NewRandom()).String(),
Message: "",
server: s,
}
}
func (e *RequestError) logger() *log.Entry {
if e.server != nil {
return e.server.Log().WithField("error_id", e.Uuid)
}
return log.WithField("error_id", e.Uuid)
}
// Sets the output message to display to the user in the error.
func (e *RequestError) SetMessage(msg string) *RequestError {
e.Message = msg
return e
}
// Aborts the request with the given status code, and responds with the error. This
// will also include the error UUID in the output so that the user can report that
// and link the response to a specific error in the logs.
func (e *RequestError) AbortWithStatus(status int, c *gin.Context) {
// If this error is because the resource does not exist, we likely do not need to log
// the error anywhere, just return a 404 and move on with our lives.
if os.IsNotExist(e.Err) {
e.logger().WithField("error", e.Err).Debug("encountered os.IsNotExist error while handling request")
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested resource was not found on the system.",
})
return
}
// Otherwise, log the error to zap, and then report the error back to the user.
if status >= 500 {
e.logger().WithField("error", e.Err).Error("encountered HTTP/500 error while handling request")
c.Error(errors.WithStack(e))
} else {
e.logger().WithField("error", e.Err).Debug("encountered non-HTTP/500 error while handling request")
}
msg := "An unexpected error was encountered while processing this request."
if e.Message != "" {
msg = e.Message
}
c.AbortWithStatusJSON(status, gin.H{
"error": msg,
"error_id": e.Uuid,
})
}
// Helper function to just abort with an internal server error. This is generally the response
// from most errors encountered by the API.
func (e *RequestError) AbortWithServerError(c *gin.Context) {
e.AbortWithStatus(http.StatusInternalServerError, c)
}
// Handle specific filesystem errors for a server.
func (e *RequestError) AbortFilesystemError(c *gin.Context) {
if errors.Is(e.Err, os.ErrNotExist) {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested resource was not found.",
})
return
}
if errors.Is(e.Err, filesystem.ErrNotEnoughDiskSpace) {
c.AbortWithStatusJSON(http.StatusConflict, gin.H{
"error": "There is not enough disk space available to perform that action.",
})
return
}
if strings.HasSuffix(e.Err.Error(), "file name too long") {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "File name is too long.",
})
return
}
if e, ok := e.Err.(*os.SyscallError); ok && e.Syscall == "readdirent" {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested directory does not exist.",
})
return
}
if strings.HasSuffix(e.Err.Error(), "file name too long") {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "Cannot perform that action: file name is too long.",
})
return
}
e.AbortWithServerError(c)
}
// Format the error to a string and include the UUID.
func (e *RequestError) Error() string {
return fmt.Sprintf("%v (uuid: %s)", e.Err, e.Uuid)
}

View File

@@ -1,81 +0,0 @@
package router
import (
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/server"
"net/http"
"strings"
)
// Set the access request control headers on all of the requests.
func SetAccessControlHeaders(c *gin.Context) {
c.Header("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
o := c.GetHeader("Origin")
if o != config.Get().PanelLocation {
for _, origin := range config.Get().AllowedOrigins {
if origin != "*" && o != origin {
continue
}
c.Header("Access-Control-Allow-Origin", origin)
c.Next()
return
}
}
c.Header("Access-Control-Allow-Origin", config.Get().PanelLocation)
c.Next()
}
// Authenticates the request token against the given permission string, ensuring that
// if it is a server permission, the token has control over that server. If it is a global
// token, this will ensure that the request is using a properly signed global token.
func AuthorizationMiddleware(c *gin.Context) {
auth := strings.SplitN(c.GetHeader("Authorization"), " ", 2)
if len(auth) != 2 || auth[0] != "Bearer" {
c.Header("WWW-Authenticate", "Bearer")
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "The required authorization heads were not present in the request.",
})
return
}
// Try to match the request against the global token for the Daemon, regardless
// of the permission type. If nothing is matched we will fall through to the Panel
// API to try and validate permissions for a server.
if auth[1] == config.Get().AuthenticationToken {
c.Next()
return
}
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "You are not authorized to access this endpoint.",
})
}
// Helper function to fetch a server out of the servers collection stored in memory.
func GetServer(uuid string) *server.Server {
return server.GetServers().Find(func(s *server.Server) bool {
return uuid == s.Id()
})
}
// Ensure that the requested server exists in this setup. Returns a 404 if we cannot
// locate it.
func ServerExists(c *gin.Context) {
u, err := uuid.Parse(c.Param("server"))
if err != nil || GetServer(u.String()) == nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The resource you requested does not exist.",
})
return
}
c.Next()
}

View File

@@ -1,99 +0,0 @@
package router
import (
"github.com/apex/log"
"github.com/gin-gonic/gin"
)
// Configures the routing infrastructure for this daemon instance.
func Configure() *gin.Engine {
gin.SetMode("release")
router := gin.New()
router.Use(gin.Recovery())
router.Use(SetAccessControlHeaders)
// @todo log this into a different file so you can setup IP blocking for abusive requests and such.
// This should still dump requests in debug mode since it does help with understanding the request
// lifecycle and quickly seeing what was called leading to the logs. However, it isn't feasible to mix
// this output in production and still get meaningful logs from it since they'll likely just be a huge
// spamfest.
router.Use(gin.LoggerWithFormatter(func(params gin.LogFormatterParams) string {
log.WithFields(log.Fields{
"client_ip": params.ClientIP,
"status": params.StatusCode,
"latency": params.Latency,
}).Debugf("%s %s", params.MethodColor()+params.Method+params.ResetColor(), params.Path)
return ""
}))
router.OPTIONS("/api/system", func(c *gin.Context) {
c.Status(200)
})
// These routes use signed URLs to validate access to the resource being requested.
router.GET("/download/backup", getDownloadBackup)
router.GET("/download/file", getDownloadFile)
router.POST("/upload/file", postServerUploadFiles)
// This route is special it sits above all of the other requests because we are
// using a JWT to authorize access to it, therefore it needs to be publicly
// accessible.
router.GET("/api/servers/:server/ws", ServerExists, getServerWebsocket)
// This request is called by another daemon when a server is going to be transferred out.
// This request does not need the AuthorizationMiddleware as the panel should never call it
// and requests are authenticated through a JWT the panel issues to the other daemon.
router.GET("/api/servers/:server/archive", ServerExists, getServerArchive)
// All of the routes beyond this mount will use an authorization middleware
// and will not be accessible without the correct Authorization header provided.
protected := router.Use(AuthorizationMiddleware)
protected.POST("/api/update", postUpdateConfiguration)
protected.GET("/api/system", getSystemInformation)
protected.GET("/api/servers", getAllServers)
protected.POST("/api/servers", postCreateServer)
protected.POST("/api/transfer", postTransfer)
// These are server specific routes, and require that the request be authorized, and
// that the server exist on the Daemon.
server := router.Group("/api/servers/:server")
server.Use(AuthorizationMiddleware, ServerExists)
{
server.GET("", getServer)
server.PATCH("", patchServer)
server.DELETE("", deleteServer)
server.GET("/logs", getServerLogs)
server.POST("/power", postServerPower)
server.POST("/commands", postServerCommands)
server.POST("/install", postServerInstall)
server.POST("/reinstall", postServerReinstall)
// This archive request causes the archive to start being created
// this should only be triggered by the panel.
server.POST("/archive", postServerArchive)
files := server.Group("/files")
{
files.GET("/contents", getServerFileContents)
files.GET("/list-directory", getServerListDirectory)
files.PUT("/rename", putServerRenameFiles)
files.POST("/copy", postServerCopyFile)
files.POST("/write", postServerWriteFile)
files.POST("/create-directory", postServerCreateDirectory)
files.POST("/delete", postServerDeleteFiles)
files.POST("/compress", postServerCompressFiles)
files.POST("/decompress", postServerDecompressFiles)
}
backup := server.Group("/backup")
{
backup.POST("", postServerBackup)
backup.DELETE("/:backup", deleteServerBackup)
}
}
return router
}

View File

@@ -1,98 +0,0 @@
package router
import (
"bufio"
"errors"
"github.com/gin-gonic/gin"
"github.com/pterodactyl/wings/router/tokens"
"github.com/pterodactyl/wings/server/backup"
"net/http"
"os"
"strconv"
)
// Handle a download request for a server backup.
func getDownloadBackup(c *gin.Context) {
token := tokens.BackupPayload{}
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
TrackedError(err).AbortWithServerError(c)
return
}
s := GetServer(token.ServerUuid)
if s == nil || !token.IsUniqueRequest() {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested resource was not found on this server.",
})
return
}
b, st, err := backup.LocateLocal(token.BackupUuid)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested backup was not found on this server.",
})
return
}
TrackedServerError(err, s).AbortWithServerError(c)
return
}
f, err := os.Open(b.Path())
if err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
return
}
defer f.Close()
c.Header("Content-Length", strconv.Itoa(int(st.Size())))
c.Header("Content-Disposition", "attachment; filename="+st.Name())
c.Header("Content-Type", "application/octet-stream")
bufio.NewReader(f).WriteTo(c.Writer)
}
// Handles downloading a specific file for a server.
func getDownloadFile(c *gin.Context) {
token := tokens.FilePayload{}
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
TrackedError(err).AbortWithServerError(c)
return
}
s := GetServer(token.ServerUuid)
if s == nil || !token.IsUniqueRequest() {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested resource was not found on this server.",
})
return
}
p, _ := s.Filesystem().SafePath(token.FilePath)
st, err := os.Stat(p)
// If there is an error or we're somehow trying to download a directory, just
// respond with the appropriate error.
if err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
return
} else if st.IsDir() {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested resource was not found on this server.",
})
return
}
f, err := os.Open(p)
if err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
return
}
c.Header("Content-Length", strconv.Itoa(int(st.Size())))
c.Header("Content-Disposition", "attachment; filename="+st.Name())
c.Header("Content-Type", "application/octet-stream")
bufio.NewReader(f).WriteTo(c.Writer)
}

View File

@@ -1,243 +0,0 @@
package router
import (
"bytes"
"context"
"github.com/apex/log"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/server"
"net/http"
"os"
"strconv"
)
type serverProcData struct {
server.ResourceUsage
Suspended bool `json:"suspended"`
}
// Returns a single server from the collection of servers.
func getServer(c *gin.Context) {
s := GetServer(c.Param("server"))
c.JSON(http.StatusOK, serverProcData{
ResourceUsage: *s.Proc(),
Suspended: s.IsSuspended(),
})
}
// Returns the logs for a given server instance.
func getServerLogs(c *gin.Context) {
s := GetServer(c.Param("server"))
l, _ := strconv.Atoi(c.DefaultQuery("size", "100"))
if l <= 0 {
l = 100
} else if l > 100 {
l = 100
}
out, err := s.ReadLogfile(l)
if err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
return
}
c.JSON(http.StatusOK, gin.H{"data": out})
}
// Handles a request to control the power state of a server. If the action being passed
// through is invalid a 404 is returned. Otherwise, a HTTP/202 Accepted response is returned
// and the actual power action is run asynchronously so that we don't have to block the
// request until a potentially slow operation completes.
//
// This is done because for the most part the Panel is using websockets to determine when
// things are happening, so theres no reason to sit and wait for a request to finish. We'll
// just see over the socket if something isn't working correctly.
func postServerPower(c *gin.Context) {
s := GetServer(c.Param("server"))
var data struct {
Action server.PowerAction `json:"action"`
}
if err := c.BindJSON(&data); err != nil {
return
}
if !data.Action.IsValid() {
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
"error": "The power action provided was not valid, should be one of \"stop\", \"start\", \"restart\", \"kill\"",
})
return
}
// Because we route all of the actual bootup process to a separate thread we need to
// check the suspension status here, otherwise the user will hit the endpoint and then
// just sit there wondering why it returns a success but nothing actually happens.
//
// We don't really care about any of the other actions at this point, they'll all result
// in the process being stopped, which should have happened anyways if the server is suspended.
if (data.Action == server.PowerActionStart || data.Action == server.PowerActionRestart) && s.IsSuspended() {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "Cannot start or restart a server that is suspended.",
})
return
}
// Pass the actual heavy processing off to a separate thread to handle so that
// we can immediately return a response from the server. Some of these actions
// can take quite some time, especially stopping or restarting.
go func(s *server.Server) {
if err := s.HandlePowerAction(data.Action, 30); err != nil {
if errors.Is(err, context.DeadlineExceeded) {
s.Log().WithField("action", data.Action).
Warn("could not acquire a lock while attempting to perform a power action")
} else {
s.Log().WithFields(log.Fields{"action": data, "error": err}).
Error("encountered error processing a server power action in the background")
}
}
}(s)
c.Status(http.StatusAccepted)
}
// Sends an array of commands to a running server instance.
func postServerCommands(c *gin.Context) {
s := GetServer(c.Param("server"))
if running, err := s.Environment.IsRunning(); err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
return
} else if !running {
c.AbortWithStatusJSON(http.StatusBadGateway, gin.H{
"error": "Cannot send commands to a stopped server instance.",
})
return
}
var data struct {
Commands []string `json:"commands"`
}
// BindJSON sends 400 if the request fails, all we need to do is return
if err := c.BindJSON(&data); err != nil {
return
}
for _, command := range data.Commands {
if err := s.Environment.SendCommand(command); err != nil {
s.Log().WithFields(log.Fields{"command": command, "error": err}).Warn("failed to send command to server instance")
}
}
c.Status(http.StatusNoContent)
}
// Updates information about a server internally.
func patchServer(c *gin.Context) {
s := GetServer(c.Param("server"))
buf := bytes.Buffer{}
buf.ReadFrom(c.Request.Body)
if err := s.UpdateDataStructure(buf.Bytes()); err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
return
}
s.SyncWithEnvironment()
c.Status(http.StatusNoContent)
}
// Performs a server installation in a background thread.
func postServerInstall(c *gin.Context) {
s := GetServer(c.Param("server"))
go func(serv *server.Server) {
if err := serv.Install(true); err != nil {
serv.Log().WithField("error", err).Error("failed to execute server installation process")
}
}(s)
c.Status(http.StatusAccepted)
}
// Reinstalls a server.
func postServerReinstall(c *gin.Context) {
s := GetServer(c.Param("server"))
if s.ExecutingPowerAction() {
c.AbortWithStatusJSON(http.StatusConflict, gin.H{
"error": "Cannot execute server reinstall event while another power action is running.",
})
return
}
go func(s *server.Server) {
if err := s.Reinstall(); err != nil {
s.Log().WithField("error", err).Error("failed to complete server re-install process")
}
}(s)
c.Status(http.StatusAccepted)
}
// Deletes a server from the wings daemon and dissociate it's objects.
func deleteServer(c *gin.Context) {
s := GetServer(c.Param("server"))
// Immediately suspend the server to prevent a user from attempting
// to start it while this process is running.
s.Config().SetSuspended(true)
// If the server is currently installing, abort it.
if s.IsInstalling() {
s.AbortInstallation()
}
// Delete the server's archive if it exists. We intentionally don't return
// here, if the archive fails to delete, the server can still be removed.
if err := s.Archiver.DeleteIfExists(); err != nil {
s.Log().WithField("error", err).Warn("failed to delete server archive during deletion process")
}
// Unsubscribe all of the event listeners.
s.Events().Destroy()
s.Throttler().StopTimer()
s.Websockets().CancelAll()
// Destroy the environment; in Docker this will handle a running container and
// forcibly terminate it before removing the container, so we do not need to handle
// that here.
if err := s.Environment.Destroy(); err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
}
// Once the environment is terminated, remove the server files from the system. This is
// done in a separate process since failure is not the end of the world and can be
// manually cleaned up after the fact.
//
// In addition, servers with large amounts of files can take some time to finish deleting
// so we don't want to block the HTTP call while waiting on this.
go func(p string) {
if err := os.RemoveAll(p); err != nil {
log.WithFields(log.Fields{
"path": p,
"error": errors.WithStack(err),
}).Warn("failed to remove server files during deletion process")
}
}(s.Filesystem().Path())
var uuid = s.Id()
server.GetServers().Remove(func(s2 *server.Server) bool {
return s2.Id() == uuid
})
// Deallocate the reference to this server.
s = nil
c.Status(http.StatusNoContent)
}

View File

@@ -1,81 +0,0 @@
package router
import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/server/backup"
"net/http"
"os"
)
// Backs up a server.
func postServerBackup(c *gin.Context) {
s := GetServer(c.Param("server"))
data := &backup.Request{}
// BindJSON sends 400 if the request fails, all we need to do is return
if err := c.BindJSON(&data); err != nil {
return
}
var adapter backup.BackupInterface
var err error
switch data.Adapter {
case backup.LocalBackupAdapter:
adapter, err = data.NewLocalBackup()
case backup.S3BackupAdapter:
adapter, err = data.NewS3Backup()
default:
err = errors.New(fmt.Sprintf("unknown backup adapter [%s] provided", data.Adapter))
return
}
if err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
return
}
go func(b backup.BackupInterface, serv *server.Server) {
if err := serv.Backup(b); err != nil {
serv.Log().WithField("error", err).Error("failed to generate backup for server")
}
}(adapter, s)
c.Status(http.StatusAccepted)
}
// Deletes a local backup of a server. If the backup is not found on the machine just return
// a 404 error. The service calling this endpoint can make its own decisions as to how it wants
// to handle that response.
func deleteServerBackup(c *gin.Context) {
s := GetServer(c.Param("server"))
b, _, err := backup.LocateLocal(c.Param("backup"))
if err != nil {
// Just return from the function at this point if the backup was not located.
if errors.Is(err, os.ErrNotExist) {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested backup was not found on this server.",
})
return
}
TrackedServerError(err, s).AbortWithServerError(c)
return
}
if err := b.Remove(); err != nil {
// I'm not entirely sure how likely this is to happen, however if we did manage to locate
// the backup previously and it is now missing when we go to delete, just treat it as having
// been successful, rather than returning a 404.
if !errors.Is(err, os.ErrNotExist) {
TrackedServerError(err, s).AbortWithServerError(c)
return
}
}
c.Status(http.StatusNoContent)
}

View File

@@ -1,425 +0,0 @@
package router
import (
"context"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/router/tokens"
"github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/server/filesystem"
"golang.org/x/sync/errgroup"
"mime/multipart"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"strings"
)
// Returns the contents of a file on the server.
func getServerFileContents(c *gin.Context) {
s := GetServer(c.Param("server"))
p, err := url.QueryUnescape(c.Query("file"))
if err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
return
}
p = "/" + strings.TrimLeft(p, "/")
st, err := s.Filesystem().Stat(p)
if err != nil {
TrackedServerError(err, s).AbortFilesystemError(c)
return
}
c.Header("X-Mime-Type", st.Mimetype)
c.Header("Content-Length", strconv.Itoa(int(st.Info.Size())))
// If a download parameter is included in the URL go ahead and attach the necessary headers
// so that the file can be downloaded.
if c.Query("download") != "" {
c.Header("Content-Disposition", "attachment; filename="+st.Info.Name())
c.Header("Content-Type", "application/octet-stream")
}
if err := s.Filesystem().Readfile(p, c.Writer); err != nil {
TrackedServerError(err, s).AbortFilesystemError(c)
return
}
}
// Returns the contents of a directory for a server.
func getServerListDirectory(c *gin.Context) {
s := GetServer(c.Param("server"))
d, err := url.QueryUnescape(c.Query("directory"))
if err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
return
}
stats, err := s.Filesystem().ListDirectory(d)
if err != nil {
TrackedServerError(err, s).AbortFilesystemError(c)
return
}
c.JSON(http.StatusOK, stats)
}
type renameFile struct {
To string `json:"to"`
From string `json:"from"`
}
// Renames (or moves) files for a server.
func putServerRenameFiles(c *gin.Context) {
s := GetServer(c.Param("server"))
var data struct {
Root string `json:"root"`
Files []renameFile `json:"files"`
}
// BindJSON sends 400 if the request fails, all we need to do is return
if err := c.BindJSON(&data); err != nil {
return
}
if len(data.Files) == 0 {
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
"error": "No files to move or rename 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 {
pf := path.Join(data.Root, p.From)
pt := path.Join(data.Root, p.To)
g.Go(func() error {
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := s.Filesystem().Rename(pf, pt); 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 {
if errors.Is(err, os.ErrExist) {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "Cannot move or rename file, destination already exists.",
})
return
}
TrackedServerError(err, s).AbortFilesystemError(c)
return
}
c.Status(http.StatusNoContent)
}
// Copies a server file.
func postServerCopyFile(c *gin.Context) {
s := GetServer(c.Param("server"))
var data struct {
Location string `json:"location"`
}
// BindJSON sends 400 if the request fails, all we need to do is return
if err := c.BindJSON(&data); err != nil {
return
}
if err := s.Filesystem().Copy(data.Location); err != nil {
TrackedServerError(err, s).AbortFilesystemError(c)
return
}
c.Status(http.StatusNoContent)
}
// Deletes files from a server.
func postServerDeleteFiles(c *gin.Context) {
s := GetServer(c.Param("server"))
var data struct {
Root string `json:"root"`
Files []string `json:"files"`
}
if err := c.BindJSON(&data); err != nil {
return
}
if len(data.Files) == 0 {
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
"error": "No files were specified for deletion.",
})
return
}
g, ctx := errgroup.WithContext(context.Background())
// Loop over the array of files passed in and delete them. If any of the file deletions
// fail just abort the process entirely.
for _, p := range data.Files {
pi := path.Join(data.Root, p)
g.Go(func() error {
select {
case <-ctx.Done():
return ctx.Err()
default:
return s.Filesystem().Delete(pi)
}
})
}
if err := g.Wait(); err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
return
}
c.Status(http.StatusNoContent)
}
// Writes the contents of the request to a file on a server.
func postServerWriteFile(c *gin.Context) {
s := GetServer(c.Param("server"))
f, err := url.QueryUnescape(c.Query("file"))
if err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
return
}
f = "/" + strings.TrimLeft(f, "/")
if err := s.Filesystem().Writefile(f, c.Request.Body); err != nil {
if errors.Is(err, filesystem.ErrIsDirectory) {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "Cannot write file, name conflicts with an existing directory by the same name.",
})
return
}
TrackedServerError(err, s).AbortFilesystemError(c)
return
}
c.Status(http.StatusNoContent)
}
// Create a directory on a server.
func postServerCreateDirectory(c *gin.Context) {
s := GetServer(c.Param("server"))
var data struct {
Name string `json:"name"`
Path string `json:"path"`
}
// BindJSON sends 400 if the request fails, all we need to do is return
if err := c.BindJSON(&data); err != nil {
return
}
if err := s.Filesystem().CreateDirectory(data.Name, data.Path); err != nil {
if err.Error() == "not a directory" {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "Part of the path being created is not a directory (ENOTDIR).",
})
return
}
TrackedServerError(err, s).AbortWithServerError(c)
return
}
c.Status(http.StatusNoContent)
}
func postServerCompressFiles(c *gin.Context) {
s := GetServer(c.Param("server"))
var data struct {
RootPath string `json:"root"`
Files []string `json:"files"`
}
if err := c.BindJSON(&data); err != nil {
return
}
if len(data.Files) == 0 {
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
"error": "No files were passed through to be compressed.",
})
return
}
if !s.Filesystem().HasSpaceAvailable(true) {
c.AbortWithStatusJSON(http.StatusConflict, gin.H{
"error": "This server does not have enough available disk space to generate a compressed archive.",
})
return
}
f, err := s.Filesystem().CompressFiles(data.RootPath, data.Files)
if err != nil {
TrackedServerError(err, s).AbortFilesystemError(c)
return
}
c.JSON(http.StatusOK, &filesystem.Stat{
Info: f,
Mimetype: "application/tar+gzip",
})
}
func postServerDecompressFiles(c *gin.Context) {
s := GetServer(c.Param("server"))
var data struct {
RootPath string `json:"root"`
File string `json:"file"`
}
if err := c.BindJSON(&data); err != nil {
return
}
hasSpace, err := s.Filesystem().SpaceAvailableForDecompression(data.RootPath, data.File)
if err != nil {
// Handle an unknown format error.
if errors.Is(err, filesystem.ErrUnknownArchiveFormat) {
s.Log().WithField("error", err).Warn("failed to decompress file due to unknown format")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "unknown archive format",
})
return
}
TrackedServerError(err, s).AbortWithServerError(c)
return
}
if !hasSpace {
c.AbortWithStatusJSON(http.StatusConflict, gin.H{
"error": "This server does not have enough available disk space to decompress this archive.",
})
return
}
if err := s.Filesystem().DecompressFile(data.RootPath, data.File); err != nil {
if errors.Is(err, os.ErrNotExist) {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested archive was not found.",
})
return
}
// If the file is busy for some reason just return a nicer error to the user since there is not
// much we specifically can do. They'll need to stop the running server process in order to overwrite
// a file like this.
if strings.Contains(err.Error(), "text file busy") {
s.Log().WithField("error", err).Warn("failed to decompress file due to busy text file")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "One or more files this archive is attempting to overwrite are currently in use by another process. Please try again.",
})
return
}
TrackedServerError(err, s).AbortFilesystemError(c)
return
}
c.Status(http.StatusNoContent)
}
func postServerUploadFiles(c *gin.Context) {
token := tokens.UploadPayload{}
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
TrackedError(err).AbortWithServerError(c)
return
}
s := GetServer(token.ServerUuid)
if s == nil || !token.IsUniqueRequest() {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested resource was not found on this server.",
})
return
}
form, err := c.MultipartForm()
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "Failed to get multipart form data from request.",
})
return
}
headers, ok := form.File["files"]
if !ok {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "No files were found on the request body.",
})
return
}
directory := c.Query("directory")
var totalSize int64
for _, header := range headers {
totalSize += header.Size
}
for _, header := range headers {
p, err := s.Filesystem().SafePath(filepath.Join(directory, header.Filename))
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
// We run this in a different method so I can use defer without any of
// the consequences caused by calling it in a loop.
if err := handleFileUpload(p, s, header); err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
}
}
func handleFileUpload(p string, s *server.Server, header *multipart.FileHeader) error {
file, err := header.Open()
if err != nil {
return errors.WithStack(err)
}
defer file.Close()
if err := s.Filesystem().Writefile(p, file); err != nil {
return errors.WithStack(err)
}
return nil
}

View File

@@ -1,83 +0,0 @@
package router
import (
"context"
"encoding/json"
"github.com/gin-gonic/gin"
ws "github.com/gorilla/websocket"
"github.com/pterodactyl/wings/router/websocket"
"time"
)
// Upgrades a connection to a websocket and passes events along between.
func getServerWebsocket(c *gin.Context) {
s := GetServer(c.Param("server"))
handler, err := websocket.GetHandler(s, c.Writer, c.Request)
if err != nil {
TrackedServerError(err, s).AbortWithServerError(c)
return
}
defer handler.Connection.Close()
// Create a context that can be canceled when the user disconnects from this
// socket that will also cancel listeners running in separate threads.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Track this open connection on the server so that we can close them all programtically
// if the server is deleted.
s.Websockets().Push(handler.Uuid(), &cancel)
defer s.Websockets().Remove(handler.Uuid())
// Listen for the context being canceled and then close the websocket connection. This normally
// just happens because you're disconnecting from the socket in the browser, however in some
// cases we close the connections programatically (e.g. deleting the server) and need to send
// a close message to the websocket so it disconnects.
go func(ctx context.Context, c *ws.Conn) {
ListenerLoop:
for {
select {
case <-ctx.Done():
handler.Connection.WriteControl(ws.CloseMessage, ws.FormatCloseMessage(ws.CloseGoingAway, "server deleted"), time.Now().Add(time.Second*5))
// A break right here without defining the specific loop would only break the select
// and not actually break the for loop, thus causing this routine to stick around forever.
break ListenerLoop
}
}
}(ctx, handler.Connection)
go handler.ListenForServerEvents(ctx)
go handler.ListenForExpiration(ctx)
for {
j := websocket.Message{}
_, p, err := handler.Connection.ReadMessage()
if err != nil {
if !ws.IsCloseError(
err,
ws.CloseNormalClosure,
ws.CloseGoingAway,
ws.CloseNoStatusReceived,
ws.CloseServiceRestart,
ws.CloseAbnormalClosure,
) {
s.Log().WithField("error", err).Warn("error handling websocket message for server")
}
break
}
// Discard and JSON parse errors into the void and don't continue processing this
// specific socket request. If we did a break here the client would get disconnected
// from the socket, which is NOT what we want to do.
if err := json.Unmarshal(p, &j); err != nil {
continue
}
go func(msg websocket.Message) {
if err := handler.HandleInbound(msg); err != nil {
handler.SendErrorJson(msg, err)
}
}(j)
}
}

View File

@@ -1,107 +0,0 @@
package router
import (
"bytes"
"github.com/apex/log"
"github.com/gin-gonic/gin"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/installer"
"github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/system"
"net/http"
"strings"
)
// Returns information about the system that wings is running on.
func getSystemInformation(c *gin.Context) {
i, err := system.GetSystemInformation()
if err != nil {
TrackedError(err).AbortWithServerError(c)
return
}
c.JSON(http.StatusOK, i)
}
// Returns all of the servers that are registered and configured correctly on
// this wings instance.
func getAllServers(c *gin.Context) {
c.JSON(http.StatusOK, server.GetServers().All())
}
// Creates a new server on the wings daemon and begins the installation process
// for it.
func postCreateServer(c *gin.Context) {
buf := bytes.Buffer{}
buf.ReadFrom(c.Request.Body)
install, err := installer.New(buf.Bytes())
if err != nil {
if installer.IsValidationError(err) {
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
"error": "The data provided in the request could not be validated.",
})
return
}
TrackedError(err).AbortWithServerError(c)
return
}
// Plop that server instance onto the request so that it can be referenced in
// requests from here-on out.
server.GetServers().Add(install.Server())
// Begin the installation process in the background to not block the request
// cycle. If there are any errors they will be logged and communicated back
// to the Panel where a reinstall may take place.
go func(i *installer.Installer) {
err := i.Server().CreateEnvironment()
if err != nil {
i.Server().Log().WithField("error", err).Error("failed to create server environment during install process")
return
}
if err := i.Server().Install(false); err != nil {
log.WithFields(log.Fields{"server": i.Uuid(), "error": err}).Error("failed to run install process for server")
}
}(install)
c.Status(http.StatusAccepted)
}
// Updates the running configuration for this daemon instance.
func postUpdateConfiguration(c *gin.Context) {
// A backup of the configuration for error purposes.
ccopy := *config.Get()
// A copy of the configuration we're using to bind the data received into.
cfg := *config.Get()
// BindJSON sends 400 if the request fails, all we need to do is return
if err := c.BindJSON(&cfg); err != nil {
return
}
// Keep the SSL certificates the same since the Panel will send through Lets Encrypt
// default locations. However, if we picked a different location manually we don't
// want to override that.
//
// If you pass through manual locations in the API call this logic will be skipped.
if strings.HasPrefix(cfg.Api.Ssl.KeyFile, "/etc/letsencrypt/live/") {
cfg.Api.Ssl.KeyFile = ccopy.Api.Ssl.KeyFile
cfg.Api.Ssl.CertificateFile = ccopy.Api.Ssl.CertificateFile
}
config.Set(&cfg)
if err := config.Get().WriteToDisk(); err != nil {
// If there was an error writing to the disk, revert back to the configuration we had
// before this code was run.
config.Set(&ccopy)
TrackedError(err).AbortWithServerError(c)
return
}
c.Status(http.StatusNoContent)
}

View File

@@ -1,315 +0,0 @@
package router
import (
"bufio"
"bytes"
"crypto/sha256"
"encoding/hex"
"github.com/apex/log"
"github.com/buger/jsonparser"
"github.com/gin-gonic/gin"
"github.com/mholt/archiver/v3"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/installer"
"github.com/pterodactyl/wings/router/tokens"
"github.com/pterodactyl/wings/server"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
)
func getServerArchive(c *gin.Context) {
auth := strings.SplitN(c.GetHeader("Authorization"), " ", 2)
if len(auth) != 2 || auth[0] != "Bearer" {
c.Header("WWW-Authenticate", "Bearer")
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "The required authorization heads were not present in the request.",
})
return
}
token := tokens.TransferPayload{}
if err := tokens.ParseToken([]byte(auth[1]), &token); err != nil {
TrackedError(err).AbortWithServerError(c)
return
}
if token.Subject != c.Param("server") {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "( .. •˘___˘• .. )",
})
return
}
s := GetServer(c.Param("server"))
st, err := s.Archiver.Stat()
if err != nil {
if !os.IsNotExist(err) {
TrackedServerError(err, s).SetMessage("failed to stat archive").AbortWithServerError(c)
return
}
c.AbortWithStatus(http.StatusNotFound)
return
}
checksum, err := s.Archiver.Checksum()
if err != nil {
TrackedServerError(err, s).SetMessage("failed to calculate checksum").AbortWithServerError(c)
return
}
file, err := os.Open(s.Archiver.Path())
if err != nil {
tserr := TrackedServerError(err, s)
if !os.IsNotExist(err) {
tserr.SetMessage("failed to open archive for reading")
} else {
tserr.SetMessage("failed to open archive")
}
tserr.AbortWithServerError(c)
return
}
defer file.Close()
c.Header("X-Checksum", checksum)
c.Header("X-Mime-Type", st.Mimetype)
c.Header("Content-Length", strconv.Itoa(int(st.Info.Size())))
c.Header("Content-Disposition", "attachment; filename="+s.Archiver.Name())
c.Header("Content-Type", "application/octet-stream")
bufio.NewReader(file).WriteTo(c.Writer)
}
func postServerArchive(c *gin.Context) {
s := GetServer(c.Param("server"))
go func(s *server.Server) {
if err := s.Archiver.Archive(); err != nil {
s.Log().WithField("error", err).Error("failed to get archive for server")
return
}
s.Log().Debug("successfully created server archive, notifying panel")
r := api.NewRequester()
rerr, err := r.SendArchiveStatus(s.Id(), true)
if rerr != nil || err != nil {
if err != nil {
s.Log().WithField("error", err).Error("failed to notify panel of archive status")
return
}
s.Log().WithField("error", rerr.String()).Error("panel returned an error when sending the archive status")
return
}
s.Log().Debug("successfully notified panel of archive status")
}(s)
c.Status(http.StatusAccepted)
}
func postTransfer(c *gin.Context) {
buf := bytes.Buffer{}
buf.ReadFrom(c.Request.Body)
go func(data []byte) {
serverID, _ := jsonparser.GetString(data, "server_id")
url, _ := jsonparser.GetString(data, "url")
token, _ := jsonparser.GetString(data, "token")
l := log.WithField("server", serverID)
// Create an http client with no timeout.
client := &http.Client{Timeout: 0}
hasError := true
defer func() {
if !hasError {
return
}
l.Info("server transfer failed, notifying panel")
rerr, err := api.NewRequester().SendTransferFailure(serverID)
if rerr != nil || err != nil {
if err != nil {
l.WithField("error", err).Error("failed to notify panel with transfer failure")
return
}
l.WithField("error", errors.WithStack(rerr)).Error("received error response from panel while notifying of transfer failure")
return
}
l.Debug("notified panel of transfer failure")
}()
// Make a new GET request to the URL the panel gave us.
req, err := http.NewRequest("GET", url, nil)
if err != nil {
log.WithField("error", errors.WithStack(err)).Error("failed to create http request for archive transfer")
return
}
// Add the authorization header.
req.Header.Set("Authorization", token)
// Execute the http request.
res, err := client.Do(req)
if err != nil {
l.WithField("error", errors.WithStack(err)).Error("failed to send archive http request")
return
}
defer res.Body.Close()
// Handle non-200 status codes.
if res.StatusCode != 200 {
_, err := ioutil.ReadAll(res.Body)
if err != nil {
l.WithField("error", errors.WithStack(err)).WithField("status", res.StatusCode).Error("failed read transfer response body")
return
}
l.WithField("error", errors.WithStack(err)).WithField("status", res.StatusCode).Error("failed to request server archive")
return
}
// Get the path to the archive.
archivePath := filepath.Join(config.Get().System.ArchiveDirectory, serverID+".tar.gz")
// Check if the archive already exists and delete it if it does.
_, err = os.Stat(archivePath)
if err != nil {
if !os.IsNotExist(err) {
l.WithField("error", errors.WithStack(err)).Error("failed to stat archive file")
return
}
} else {
if err := os.Remove(archivePath); err != nil {
l.WithField("error", errors.WithStack(err)).Warn("failed to remove old archive file")
return
}
}
// Create the file.
file, err := os.Create(archivePath)
if err != nil {
l.WithField("error", errors.WithStack(err)).Error("failed to open archive on disk")
return
}
// Copy the file.
buf := make([]byte, 1024*4)
_, err = io.CopyBuffer(file, res.Body, buf)
if err != nil {
l.WithField("error", errors.WithStack(err)).Error("failed to copy archive file to disk")
return
}
// Close the file so it can be opened to verify the checksum.
if err := file.Close(); err != nil {
l.WithField("error", errors.WithStack(err)).Error("failed to close archive file")
return
}
l.WithField("server", serverID).Debug("server archive downloaded, computing checksum...")
// Open the archive file for computing a checksum.
file, err = os.Open(archivePath)
if err != nil {
l.WithField("error", errors.WithStack(err)).Error("failed to open archive on disk")
return
}
// Compute the sha256 checksum of the file.
hash := sha256.New()
buf = make([]byte, 1024*4)
if _, err := io.CopyBuffer(hash, file, buf); err != nil {
l.WithField("error", errors.WithStack(err)).Error("failed to copy archive file for checksum verification")
return
}
// Verify the two checksums.
if hex.EncodeToString(hash.Sum(nil)) != res.Header.Get("X-Checksum") {
l.Error("checksum verification failed for archive")
return
}
// Close the file.
if err := file.Close(); err != nil {
l.WithField("error", errors.WithStack(err)).Error("failed to close archive file after calculating checksum")
return
}
l.Info("server archive transfer was successful")
// Get the server data from the request.
serverData, t, _, _ := jsonparser.Get(data, "server")
if t != jsonparser.Object {
l.Error("invalid server data passed in request")
return
}
// Create a new server installer (note this does not execute the install script)
i, err := installer.New(serverData)
if err != nil {
l.WithField("error", errors.WithStack(err)).Error("failed to validate received server data")
return
}
// Add the server to the collection.
server.GetServers().Add(i.Server())
// Create the server's environment (note this does not execute the install script)
if err := i.Server().CreateEnvironment(); err != nil {
l.WithField("error", err).Error("failed to create server environment")
return
}
// Un-archive the archive. That sounds weird..
if err := archiver.NewTarGz().Unarchive(archivePath, i.Server().Filesystem().Path()); err != nil {
l.WithField("error", errors.WithStack(err)).Error("failed to extract server archive")
return
}
// We mark the process as being successful here as if we fail to send a transfer success,
// then a transfer failure won't probably be successful either.
//
// It may be useful to retry sending the transfer success every so often just in case of a small
// hiccup or the fix of whatever error causing the success request to fail.
hasError = false
// Notify the panel that the transfer succeeded.
rerr, err := api.NewRequester().SendTransferSuccess(serverID)
if rerr != nil || err != nil {
if err != nil {
l.WithField("error", errors.WithStack(err)).Error("failed to notify panel of transfer success")
return
}
l.WithField("error", errors.WithStack(rerr)).Error("panel responded with error after transfer success")
return
}
l.Info("successfully notified panel of transfer success")
}(buf.Bytes())
c.Status(http.StatusAccepted)
}

View File

@@ -1,26 +0,0 @@
package tokens
import (
"github.com/gbrlsnchs/jwt/v3"
)
type BackupPayload struct {
jwt.Payload
ServerUuid string `json:"server_uuid"`
BackupUuid string `json:"backup_uuid"`
UniqueId string `json:"unique_id"`
}
// Returns the JWT payload.
func (p *BackupPayload) GetPayload() *jwt.Payload {
return &p.Payload
}
// Determines if this JWT is valid for the given request cycle. If the
// unique ID passed in the token has already been seen before this will
// return false. This allows us to use this JWT as a one-time token that
// validates all of the request.
func (p *BackupPayload) IsUniqueRequest() bool {
return getTokenStore().IsValidToken(p.UniqueId)
}

View File

@@ -1,25 +0,0 @@
package tokens
import (
"github.com/gbrlsnchs/jwt/v3"
)
type FilePayload struct {
jwt.Payload
FilePath string `json:"file_path"`
ServerUuid string `json:"server_uuid"`
UniqueId string `json:"unique_id"`
}
// Returns the JWT payload.
func (p *FilePayload) GetPayload() *jwt.Payload {
return &p.Payload
}
// Determines if this JWT is valid for the given request cycle. If the
// unique ID passed in the token has already been seen before this will
// return false. This allows us to use this JWT as a one-time token that
// validates all of the request.
func (p *FilePayload) IsUniqueRequest() bool {
return getTokenStore().IsValidToken(p.UniqueId)
}

View File

@@ -1,27 +0,0 @@
package tokens
import (
"github.com/gbrlsnchs/jwt/v3"
"github.com/pterodactyl/wings/config"
"time"
)
type TokenData interface {
GetPayload() *jwt.Payload
}
// Validates the provided JWT against the known secret for the Daemon and returns the
// parsed data. This function DOES NOT validate that the token is valid for the connected
// server, nor does it ensure that the user providing the token is able to actually do things.
//
// This simply returns a parsed token.
func ParseToken(token []byte, data TokenData) error {
verifyOptions := jwt.ValidatePayload(
data.GetPayload(),
jwt.ExpirationTimeValidator(time.Now()),
)
_, err := jwt.Verify(token, config.GetJwtAlgorithm(), &data, verifyOptions)
return err
}

View File

@@ -1,41 +0,0 @@
package tokens
import (
"github.com/patrickmn/go-cache"
"sync"
"time"
)
type TokenStore struct {
sync.Mutex
cache *cache.Cache
}
var _tokens *TokenStore
// Returns the global unique token store cache. This is used to validate
// one time token usage by storing any received tokens in a local memory
// cache until they are ready to expire.
func getTokenStore() *TokenStore {
if _tokens == nil {
_tokens = &TokenStore{
cache: cache.New(time.Minute*60, time.Minute*5),
}
}
return _tokens
}
// Checks if a token is valid or not.
func (t *TokenStore) IsValidToken(token string) bool {
t.Lock()
defer t.Unlock()
_, exists := t.cache.Get(token)
if !exists {
t.cache.Add(token, "", time.Minute*60)
}
return !exists
}

View File

@@ -1,14 +0,0 @@
package tokens
import (
"github.com/gbrlsnchs/jwt/v3"
)
type TransferPayload struct {
jwt.Payload
}
// GetPayload returns the JWT payload.
func (p *TransferPayload) GetPayload() *jwt.Payload {
return &p.Payload
}

View File

@@ -1,25 +0,0 @@
package tokens
import (
"github.com/gbrlsnchs/jwt/v3"
)
type UploadPayload struct {
jwt.Payload
ServerUuid string `json:"server_uuid"`
UniqueId string `json:"unique_id"`
}
// Returns the JWT payload.
func (p *UploadPayload) GetPayload() *jwt.Payload {
return &p.Payload
}
// Determines if this JWT is valid for the given request cycle. If the
// unique ID passed in the token has already been seen before this will
// return false. This allows us to use this JWT as a one-time token that
// validates all of the request.
func (p *UploadPayload) IsUniqueRequest() bool {
return getTokenStore().IsValidToken(p.UniqueId)
}

View File

@@ -1,46 +0,0 @@
package tokens
import (
"encoding/json"
"github.com/gbrlsnchs/jwt/v3"
"strings"
"sync"
)
type WebsocketPayload struct {
jwt.Payload
sync.RWMutex
UserID json.Number `json:"user_id"`
ServerUUID string `json:"server_uuid"`
Permissions []string `json:"permissions"`
}
// Returns the JWT payload.
func (p *WebsocketPayload) GetPayload() *jwt.Payload {
p.RLock()
defer p.RUnlock()
return &p.Payload
}
func (p *WebsocketPayload) GetServerUuid() string {
p.RLock()
defer p.RUnlock()
return p.ServerUUID
}
// Checks if the given token payload has a permission string.
func (p *WebsocketPayload) HasPermission(permission string) bool {
p.RLock()
defer p.RUnlock()
for _, k := range p.Permissions {
if k == permission || (!strings.HasPrefix(permission, "admin") && k == "*") {
return true
}
}
return false
}

View File

@@ -1,75 +0,0 @@
package websocket
import (
"context"
"github.com/pterodactyl/wings/events"
"github.com/pterodactyl/wings/server"
"time"
)
// Checks the time to expiration on the JWT every 30 seconds until the token has
// expired. If we are within 3 minutes of the token expiring, send a notice over
// the socket that it is expiring soon. If it has expired, send that notice as well.
func (h *Handler) ListenForExpiration(ctx context.Context) {
// Make a ticker and completion channel that is used to continuously poll the
// JWT stored in the session to send events to the socket when it is expiring.
ticker := time.NewTicker(time.Second * 30)
// Whenever this function is complete, end the ticker, close out the channel,
// and then close the websocket connection.
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
jwt := h.GetJwt()
if jwt != nil {
if jwt.ExpirationTime.Unix()-time.Now().Unix() <= 0 {
_ = h.SendJson(&Message{Event: TokenExpiredEvent})
} else if jwt.ExpirationTime.Unix()-time.Now().Unix() <= 60 {
_ = h.SendJson(&Message{Event: TokenExpiringEvent})
}
}
}
}
}
var e = []string{
server.StatsEvent,
server.StatusEvent,
server.ConsoleOutputEvent,
server.InstallOutputEvent,
server.InstallStartedEvent,
server.InstallCompletedEvent,
server.DaemonMessageEvent,
server.BackupCompletedEvent,
}
// Listens for different events happening on a server and sends them along
// to the connected websocket.
func (h *Handler) ListenForServerEvents(ctx context.Context) {
h.server.Log().Debug("listening for server events over websocket")
callback := func(e events.Event) {
if err := h.SendJson(&Message{Event: e.Topic, Args: []string{e.Data}}); err != nil {
h.server.Log().WithField("error", err).Warn("error while sending server data over websocket")
}
}
// Subscribe to all of the events with the same callback that will push the data out over the
// websocket for the server.
for _, evt := range e {
h.server.Events().On(evt, &callback)
}
go func(ctx context.Context) {
select {
case <-ctx.Done():
// Once this context is stopped, de-register all of the listeners that have been registered.
for _, evt := range e {
h.server.Events().Off(evt, &callback)
}
}
}(ctx)
}

View File

@@ -1,23 +0,0 @@
package websocket
const (
AuthenticationSuccessEvent = "auth success"
TokenExpiringEvent = "token expiring"
TokenExpiredEvent = "token expired"
AuthenticationEvent = "auth"
SetStateEvent = "set state"
SendServerLogsEvent = "send logs"
SendCommandEvent = "send command"
SendStatsEvent = "send stats"
ErrorEvent = "daemon error"
JwtErrorEvent = "jwt error"
)
type Message struct {
// The event to perform.
Event string `json:"event"`
// The data to pass along, only used by power/command currently. Other requests
// should either omit the field or pass an empty value as it is ignored.
Args []string `json:"args,omitempty"`
}

View File

@@ -1,415 +0,0 @@
package websocket
import (
"context"
"encoding/json"
"fmt"
"github.com/apex/log"
"github.com/gbrlsnchs/jwt/v3"
"github.com/google/uuid"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/environment/docker"
"github.com/pterodactyl/wings/router/tokens"
"github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/server/filesystem"
"net/http"
"strings"
"sync"
"time"
)
const (
PermissionConnect = "websocket.connect"
PermissionSendCommand = "control.console"
PermissionSendPowerStart = "control.start"
PermissionSendPowerStop = "control.stop"
PermissionSendPowerRestart = "control.restart"
PermissionReceiveErrors = "admin.websocket.errors"
PermissionReceiveInstall = "admin.websocket.install"
PermissionReceiveBackups = "backup.read"
)
type Handler struct {
sync.RWMutex
Connection *websocket.Conn
jwt *tokens.WebsocketPayload `json:"-"`
server *server.Server
uuid uuid.UUID
}
var (
ErrJwtNotPresent = errors.New("jwt: no jwt present")
ErrJwtNoConnectPerm = errors.New("jwt: missing connect permission")
ErrJwtUuidMismatch = errors.New("jwt: server uuid mismatch")
)
func IsJwtError(err error) bool {
return errors.Is(err, ErrJwtNotPresent) ||
errors.Is(err, ErrJwtNoConnectPerm) ||
errors.Is(err, ErrJwtUuidMismatch) ||
errors.Is(err, jwt.ErrExpValidation)
}
// Parses a JWT into a websocket token payload.
func NewTokenPayload(token []byte) (*tokens.WebsocketPayload, error) {
payload := tokens.WebsocketPayload{}
err := tokens.ParseToken(token, &payload)
if err != nil {
return nil, err
}
if !payload.HasPermission(PermissionConnect) {
return nil, errors.New("not authorized to connect to this socket")
}
return &payload, nil
}
// Returns a new websocket handler using the context provided.
func GetHandler(s *server.Server, w http.ResponseWriter, r *http.Request) (*Handler, error) {
upgrader := websocket.Upgrader{
// Ensure that the websocket request is originating from the Panel itself,
// and not some other location.
CheckOrigin: func(r *http.Request) bool {
o := r.Header.Get("Origin")
if o == config.Get().PanelLocation {
return true
}
for _, origin := range config.Get().AllowedOrigins {
if origin == "*" {
return true
}
if o != origin {
continue
}
return true
}
return false
},
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return nil, err
}
u, err := uuid.NewRandom()
if err != nil {
return nil, errors.WithStack(err)
}
return &Handler{
Connection: conn,
jwt: nil,
server: s,
uuid: u,
}, nil
}
func (h *Handler) Uuid() uuid.UUID {
return h.uuid
}
func (h *Handler) SendJson(v *Message) error {
// Do not send JSON down the line if the JWT on the connection is not valid!
if err := h.TokenValid(); err != nil {
h.unsafeSendJson(Message{
Event: JwtErrorEvent,
Args: []string{err.Error()},
})
return nil
}
j := h.GetJwt()
if j != nil {
// If we're sending installation output but the user does not have the required
// permissions to see the output, don't send it down the line.
if v.Event == server.InstallOutputEvent {
if !j.HasPermission(PermissionReceiveInstall) {
return nil
}
}
// If the user does not have permission to see backup events, do not emit
// them over the socket.
if strings.HasPrefix(v.Event, server.BackupCompletedEvent) {
if !j.HasPermission(PermissionReceiveBackups) {
return nil
}
}
}
if err := h.unsafeSendJson(v); err != nil {
// Not entirely sure how this happens (likely just when there is a ton of console spam)
// but I don't care to fix it right now, so just mask the error and throw a warning into
// the logs for us to look into later.
if errors.Is(err, websocket.ErrCloseSent) {
if h.server != nil {
h.server.Log().WithField("subsystem", "websocket").
WithField("event", v.Event).
Warn("failed to send event to websocket: close already sent")
}
return nil
}
return err
}
return nil
}
// Sends JSON over the websocket connection, ignoring the authentication state of the
// socket user. Do not call this directly unless you are positive a response should be
// sent back to the client!
func (h *Handler) unsafeSendJson(v interface{}) error {
h.Lock()
defer h.Unlock()
return h.Connection.WriteJSON(v)
}
// Checks if the JWT is still valid.
func (h *Handler) TokenValid() error {
j := h.GetJwt()
if j == nil {
return ErrJwtNotPresent
}
if err := jwt.ExpirationTimeValidator(time.Now())(&j.Payload); err != nil {
return err
}
if !j.HasPermission(PermissionConnect) {
return ErrJwtNoConnectPerm
}
if h.server.Id() != j.GetServerUuid() {
return ErrJwtUuidMismatch
}
return nil
}
// Sends an error back to the connected websocket instance by checking the permissions
// of the token. If the user has the "receive-errors" grant we will send back the actual
// error message, otherwise we just send back a standard error message.
func (h *Handler) SendErrorJson(msg Message, err error, shouldLog ...bool) error {
j := h.GetJwt()
expected := errors.Is(err, server.ErrSuspended) ||
errors.Is(err, server.ErrIsRunning) ||
errors.Is(err, filesystem.ErrNotEnoughDiskSpace)
message := "an unexpected error was encountered while handling this request"
if expected || (j != nil && j.HasPermission(PermissionReceiveErrors)) {
message = err.Error()
}
m, u := h.GetErrorMessage(message)
wsm := Message{Event: ErrorEvent}
wsm.Args = []string{m}
if len(shouldLog) == 0 || (len(shouldLog) == 1 && shouldLog[0] == true) {
if !expected && !IsJwtError(err) {
h.server.Log().WithFields(log.Fields{"event": msg.Event, "error_identifier": u.String(), "error": err}).
Error("failed to handle websocket process; an error was encountered processing an event")
}
}
return h.unsafeSendJson(wsm)
}
// Converts an error message into a more readable representation and returns a UUID
// that can be cross-referenced to find the specific error that triggered.
func (h *Handler) GetErrorMessage(msg string) (string, uuid.UUID) {
u := uuid.Must(uuid.NewRandom())
m := fmt.Sprintf("Error Event [%s]: %s", u.String(), msg)
return m, u
}
// Sets the JWT for the websocket in a race-safe manner.
func (h *Handler) setJwt(token *tokens.WebsocketPayload) {
h.Lock()
h.jwt = token
h.Unlock()
}
func (h *Handler) GetJwt() *tokens.WebsocketPayload {
h.RLock()
defer h.RUnlock()
return h.jwt
}
// Handle the inbound socket request and route it to the proper server action.
func (h *Handler) HandleInbound(m Message) error {
if m.Event != AuthenticationEvent {
if err := h.TokenValid(); err != nil {
h.unsafeSendJson(Message{
Event: JwtErrorEvent,
Args: []string{err.Error()},
})
return nil
}
}
switch m.Event {
case AuthenticationEvent:
{
token, err := NewTokenPayload([]byte(strings.Join(m.Args, "")))
if err != nil {
// If the error says the JWT expired, send a token expired
// event and hopefully the client renews the token.
if err == jwt.ErrExpValidation {
h.SendJson(&Message{Event: TokenExpiredEvent})
return nil
}
return err
}
// Check if the user has previously authenticated successfully.
newConnection := h.GetJwt() == nil
// Previously there was a HasPermission(PermissionConnect) check around this,
// however NewTokenPayload will return an error if it doesn't have the connect
// permission meaning that it was a redundant function call.
h.setJwt(token)
// Tell the client they authenticated successfully.
h.unsafeSendJson(Message{
Event: AuthenticationSuccessEvent,
Args: []string{},
})
// Check if the client was refreshing their authentication token
// instead of authenticating for the first time.
if !newConnection {
// This prevents duplicate status messages as outlined in
// https://github.com/pterodactyl/panel/issues/2077
return nil
}
// On every authentication event, send the current server status back
// to the client. :)
state := h.server.GetState()
h.SendJson(&Message{
Event: server.StatusEvent,
Args: []string{state},
})
// Only send the current disk usage if the server is offline, if docker container is running,
// Environment#EnableResourcePolling() will send this data to all clients.
if state == environment.ProcessOfflineState {
_ = h.server.Filesystem().HasSpaceAvailable(false)
b, _ := json.Marshal(h.server.Proc())
h.SendJson(&Message{
Event: server.StatsEvent,
Args: []string{string(b)},
})
}
return nil
}
case SetStateEvent:
{
action := server.PowerAction(strings.Join(m.Args, ""))
actions := make(map[server.PowerAction]string)
actions[server.PowerActionStart] = PermissionSendPowerStart
actions[server.PowerActionStop] = PermissionSendPowerStop
actions[server.PowerActionRestart] = PermissionSendPowerRestart
actions[server.PowerActionTerminate] = PermissionSendPowerStop
// Check that they have permission to perform this action if it is needed.
if permission, exists := actions[action]; exists {
if !h.GetJwt().HasPermission(permission) {
return nil
}
}
err := h.server.HandlePowerAction(action)
if errors.Is(err, context.DeadlineExceeded) {
m, _ := h.GetErrorMessage("another power action is currently being processed for this server, please try again later")
h.SendJson(&Message{
Event: ErrorEvent,
Args: []string{m},
})
return nil
}
return err
}
case SendServerLogsEvent:
{
if running, _ := h.server.Environment.IsRunning(); !running {
return nil
}
logs, err := h.server.Environment.Readlog(100)
if err != nil {
return err
}
for _, line := range logs {
h.SendJson(&Message{
Event: server.ConsoleOutputEvent,
Args: []string{line},
})
}
return nil
}
case SendStatsEvent:
{
b, _ := json.Marshal(h.server.Proc())
h.SendJson(&Message{
Event: server.StatsEvent,
Args: []string{string(b)},
})
return nil
}
case SendCommandEvent:
{
if !h.GetJwt().HasPermission(PermissionSendCommand) {
return nil
}
if h.server.GetState() == environment.ProcessOfflineState {
return nil
}
// TODO(dane): should probably add a new process state that is "booting environment" or something
// so that we can better handle this and only set the environment to booted once we're attached.
//
// Or maybe just an IsBooted function?
if h.server.GetState() == environment.ProcessStartingState {
if e, ok := h.server.Environment.(*docker.Environment); ok {
if !e.IsAttached() {
return nil
}
}
}
return h.server.Environment.SendCommand(strings.Join(m.Args, ""))
}
}
return nil
}

View File

@@ -1,124 +0,0 @@
package server
import (
"crypto/sha256"
"encoding/hex"
"github.com/mholt/archiver/v3"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/server/filesystem"
"io"
"io/ioutil"
"os"
"path/filepath"
)
// Archiver represents a Server Archiver.
type Archiver struct {
Server *Server
}
// Path returns the path to the server's archive.
func (a *Archiver) Path() string {
return filepath.Join(config.Get().System.ArchiveDirectory, a.Name())
}
// Name returns the name of the server's archive.
func (a *Archiver) Name() string {
return a.Server.Id() + ".tar.gz"
}
// Exists returns a boolean based off if the archive exists.
func (a *Archiver) Exists() bool {
if _, err := os.Stat(a.Path()); os.IsNotExist(err) {
return false
}
return true
}
// Stat stats the archive file.
func (a *Archiver) Stat() (*filesystem.Stat, error) {
s, err := os.Stat(a.Path())
if err != nil {
return nil, errors.WithStack(err)
}
return &filesystem.Stat{
Info: s,
Mimetype: "application/tar+gzip",
}, nil
}
// Archive creates an archive of the server and deletes the previous one.
func (a *Archiver) Archive() error {
path := a.Server.Filesystem().Path()
// Get the list of root files and directories to archive.
var files []string
fileInfo, err := ioutil.ReadDir(path)
if err != nil {
return err
}
for _, file := range fileInfo {
f := filepath.Join(path, file.Name())
// If the file is a symlink we cannot safely assume that the result of a filepath.Join() will be
// a safe destination. We need to check if the file is a symlink, and if so pass off to the SafePath
// function to resolve it to the final destination.
//
// ioutil.ReadDir() calls Lstat, so this will work correctly. If it did not call Lstat, but rather
// just did a normal Stat call, this would fail since that would be looking at the symlink destination
// and not the actual file in this listing.
if file.Mode()&os.ModeSymlink != 0 {
f, err = a.Server.Filesystem().SafePath(filepath.Join(path, file.Name()))
if err != nil {
return err
}
}
files = append(files, f)
}
if err := a.DeleteIfExists(); err != nil {
return err
}
return archiver.NewTarGz().Archive(files, a.Path())
}
// DeleteIfExists deletes the archive if it exists.
func (a *Archiver) DeleteIfExists() error {
if _, err := a.Stat(); err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil
}
return err
}
if err := os.Remove(a.Path()); err != nil {
return errors.WithStack(err)
}
return nil
}
// Checksum computes a SHA256 checksum of the server's archive.
func (a *Archiver) Checksum() (string, error) {
file, err := os.Open(a.Path())
if err != nil {
return "", err
}
defer file.Close()
hash := sha256.New()
buf := make([]byte, 1024*4)
if _, err := io.CopyBuffer(hash, file, buf); err != nil {
return "", err
}
return hex.EncodeToString(hash.Sum(nil)), nil
}

View File

@@ -1,125 +0,0 @@
package server
import (
"bufio"
"github.com/apex/log"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/server/backup"
"os"
"path"
)
// Notifies the panel of a backup's state and returns an error if one is encountered
// while performing this action.
func (s *Server) notifyPanelOfBackup(uuid string, ad *backup.ArchiveDetails, successful bool) error {
r := api.NewRequester()
rerr, err := r.SendBackupStatus(uuid, ad.ToRequest(successful))
if rerr != nil || err != nil {
if err != nil {
s.Log().WithFields(log.Fields{
"backup": uuid,
"error": err,
}).Error("failed to notify panel of backup status due to wings error")
return err
}
return errors.New(rerr.String())
}
return nil
}
// Get all of the ignored files for a server based on its .pteroignore file in the root.
func (s *Server) getServerwideIgnoredFiles() ([]string, error) {
var ignored []string
f, err := os.Open(path.Join(s.Filesystem().Path(), ".pteroignore"))
if err != nil {
if !os.IsNotExist(err) {
return nil, err
}
} else {
scanner := bufio.NewScanner(f)
for scanner.Scan() {
// Only include non-empty lines, for the sake of clarity...
if t := scanner.Text(); t != "" {
ignored = append(ignored, t)
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
}
return ignored, nil
}
// Get the backup files to include when generating it.
func (s *Server) GetIncludedBackupFiles(ignored []string) (*backup.IncludedFiles, error) {
// If no ignored files are present in the request, check for a .pteroignore file in the root
// of the server files directory, and use that to generate the backup.
if len(ignored) == 0 {
if i, err := s.getServerwideIgnoredFiles(); err != nil {
s.Log().WithField("error", err).Warn("failed to retrieve ignored files listing for server")
} else {
ignored = i
}
}
// Get the included files based on the root path and the ignored files provided.
return s.Filesystem().GetIncludedFiles(s.Filesystem().Path(), ignored)
}
// Performs a server backup and then emits the event over the server websocket. We
// let the actual backup system handle notifying the panel of the status, but that
// won't emit a websocket event.
func (s *Server) Backup(b backup.BackupInterface) error {
// Get the included files based on the root path and the ignored files provided.
inc, err := s.GetIncludedBackupFiles(b.Ignored())
if err != nil {
return errors.WithStack(err)
}
ad, err := b.Generate(inc, s.Filesystem().Path())
if err != nil {
if notifyError := s.notifyPanelOfBackup(b.Identifier(), &backup.ArchiveDetails{}, false); notifyError != nil {
s.Log().WithFields(log.Fields{
"backup": b.Identifier(),
"error": notifyError,
}).Warn("failed to notify panel of failed backup state")
}
s.Events().PublishJson(BackupCompletedEvent+":"+b.Identifier(), map[string]interface{}{
"uuid": b.Identifier(),
"is_successful": false,
"checksum": "",
"checksum_type": "sha1",
"file_size": 0,
})
return errors.Wrap(err, "error while generating server backup")
}
// Try to notify the panel about the status of this backup. If for some reason this request
// fails, delete the archive from the daemon and return that error up the chain to the caller.
if notifyError := s.notifyPanelOfBackup(b.Identifier(), ad, true); notifyError != nil {
b.Remove()
return notifyError
}
// Emit an event over the socket so we can update the backup in realtime on
// the frontend for the server.
s.Events().PublishJson(BackupCompletedEvent+":"+b.Identifier(), map[string]interface{}{
"uuid": b.Identifier(),
"is_successful": true,
"checksum": ad.Checksum,
"checksum_type": "sha1",
"file_size": ad.Size,
})
return nil
}

View File

@@ -1,132 +0,0 @@
package backup
import (
"archive/tar"
"context"
"github.com/apex/log"
gzip "github.com/klauspost/pgzip"
"github.com/pkg/errors"
"github.com/remeh/sizedwaitgroup"
"golang.org/x/sync/errgroup"
"io"
"os"
"runtime"
"strings"
"sync"
)
type Archive struct {
sync.Mutex
TrimPrefix string
Files *IncludedFiles
}
// Creates an archive at dst with all of the files defined in the included files struct.
func (a *Archive) Create(dst string, ctx context.Context) error {
f, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return errors.WithStack(err)
}
defer f.Close()
maxCpu := runtime.NumCPU() / 2
if maxCpu > 4 {
maxCpu = 4
}
gzw, _ := gzip.NewWriterLevel(f, gzip.BestSpeed)
_ = gzw.SetConcurrency(1<<20, maxCpu)
defer gzw.Flush()
defer gzw.Close()
tw := tar.NewWriter(gzw)
defer tw.Flush()
defer tw.Close()
wg := sizedwaitgroup.New(10)
g, ctx := errgroup.WithContext(ctx)
// Iterate over all of the files to be included and put them into the archive. This is
// done as a concurrent goroutine to speed things along. If an error is encountered at
// any step, the entire process is aborted.
for _, p := range a.Files.All() {
p := p
g.Go(func() error {
wg.Add()
defer wg.Done()
select {
case <-ctx.Done():
return errors.WithStack(ctx.Err())
default:
return a.addToArchive(p, tw)
}
})
}
// Block until the entire routine is completed.
if err := g.Wait(); err != nil {
f.Close()
// Attempt to remove the archive if there is an error, report that error to
// the logger if it fails.
if rerr := os.Remove(dst); rerr != nil && !os.IsNotExist(rerr) {
log.WithField("location", dst).Warn("failed to delete corrupted backup archive")
}
return errors.WithStack(err)
}
return nil
}
// Adds a single file to the existing tar archive writer.
func (a *Archive) addToArchive(p string, w *tar.Writer) error {
f, err := os.Open(p)
if err != nil {
// If you try to backup something that no longer exists (got deleted somewhere during the process
// but not by this process), just skip over it and don't kill the entire backup.
if os.IsNotExist(err) {
return nil
}
return errors.WithStack(err)
}
defer f.Close()
s, err := f.Stat()
if err != nil {
// Same as above, don't kill the process just because the file no longer exists.
if os.IsNotExist(err) {
return nil
}
return errors.WithStack(err)
}
header := &tar.Header{
// Trim the long server path from the name of the file so that the resulting
// archive is exactly how the user would see it in the panel file manager.
Name: strings.TrimPrefix(p, a.TrimPrefix),
Size: s.Size(),
Mode: int64(s.Mode()),
ModTime: s.ModTime(),
}
// These actions must occur sequentially, even if this function is called multiple
// in parallel. You'll get some nasty panic's otherwise.
a.Lock()
defer a.Unlock()
if err := w.WriteHeader(header); err != nil {
return errors.WithStack(err)
}
buf := make([]byte, 4*1024)
if _, err := io.CopyBuffer(w, f, buf); err != nil {
return errors.WithStack(err)
}
return nil
}

View File

@@ -1,158 +0,0 @@
package backup
import (
"crypto/sha1"
"encoding/hex"
"github.com/apex/log"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/config"
"io"
"os"
"path"
"sync"
)
const (
LocalBackupAdapter = "wings"
S3BackupAdapter = "s3"
)
type ArchiveDetails struct {
Checksum string `json:"checksum"`
ChecksumType string `json:"checksum_type"`
Size int64 `json:"size"`
}
// Returns a request object.
func (ad *ArchiveDetails) ToRequest(successful bool) api.BackupRequest {
return api.BackupRequest{
Checksum: ad.Checksum,
ChecksumType: ad.ChecksumType,
Size: ad.Size,
Successful: successful,
}
}
type Backup struct {
// The UUID of this backup object. This must line up with a backup from
// the panel instance.
Uuid string `json:"uuid"`
// An array of files to ignore when generating this backup. This should be
// compatible with a standard .gitignore structure.
IgnoredFiles []string `json:"ignored_files"`
}
// noinspection GoNameStartsWithPackageName
type BackupInterface interface {
// Returns the UUID of this backup as tracked by the panel instance.
Identifier() string
// Generates a backup in whatever the configured source for the specific
// implementation is.
Generate(*IncludedFiles, string) (*ArchiveDetails, error)
// Returns the ignored files for this backup instance.
Ignored() []string
// Returns a SHA256 checksum for the generated backup.
Checksum() ([]byte, error)
// Returns the size of the generated backup.
Size() (int64, error)
// Returns the path to the backup on the machine. This is not always the final
// storage location of the backup, simply the location we're using to store
// it until it is moved to the final spot.
Path() string
// Returns details about the archive.
Details() *ArchiveDetails
// Removes a backup file.
Remove() error
}
func (b *Backup) Identifier() string {
return b.Uuid
}
// Returns the path for this specific backup.
func (b *Backup) Path() string {
return path.Join(config.Get().System.BackupDirectory, b.Identifier()+".tar.gz")
}
// Return the size of the generated backup.
func (b *Backup) Size() (int64, error) {
st, err := os.Stat(b.Path())
if err != nil {
return 0, errors.WithStack(err)
}
return st.Size(), nil
}
// Returns the SHA256 checksum of a backup.
func (b *Backup) Checksum() ([]byte, error) {
h := sha1.New()
f, err := os.Open(b.Path())
if err != nil {
return nil, errors.WithStack(err)
}
defer f.Close()
buf := make([]byte, 1024*4)
if _, err := io.CopyBuffer(h, f, buf); err != nil {
return nil, err
}
return h.Sum(nil), nil
}
// Returns details of the archive by utilizing two go-routines to get the checksum and
// the size of the archive.
func (b *Backup) Details() *ArchiveDetails {
wg := sync.WaitGroup{}
wg.Add(2)
var checksum string
// Calculate the checksum for the file.
go func() {
defer wg.Done()
resp, err := b.Checksum()
if err != nil {
log.WithFields(log.Fields{
"backup": b.Identifier(),
"error": err,
}).Error("failed to calculate checksum for backup")
}
checksum = hex.EncodeToString(resp)
}()
var sz int64
go func() {
defer wg.Done()
if s, err := b.Size(); err != nil {
return
} else {
sz = s
}
}()
wg.Wait()
return &ArchiveDetails{
Checksum: checksum,
ChecksumType: "sha1",
Size: sz,
}
}
func (b *Backup) Ignored() []string {
return b.IgnoredFiles
}

View File

@@ -1,55 +0,0 @@
package backup
import (
"context"
"github.com/pkg/errors"
"os"
)
type LocalBackup struct {
Backup
}
var _ BackupInterface = (*LocalBackup)(nil)
// Locates the backup for a server and returns the local path. This will obviously only
// work if the backup was created as a local backup.
func LocateLocal(uuid string) (*LocalBackup, os.FileInfo, error) {
b := &LocalBackup{
Backup{
Uuid: uuid,
IgnoredFiles: nil,
},
}
st, err := os.Stat(b.Path())
if err != nil {
return nil, nil, errors.WithStack(err)
}
if st.IsDir() {
return nil, nil, errors.New("invalid archive, is directory")
}
return b, st, nil
}
// Removes a backup from the system.
func (b *LocalBackup) Remove() error {
return os.Remove(b.Path())
}
// Generates a backup of the selected files and pushes it to the defined location
// for this instance.
func (b *LocalBackup) Generate(included *IncludedFiles, prefix string) (*ArchiveDetails, error) {
a := &Archive{
TrimPrefix: prefix,
Files: included,
}
if err := a.Create(b.Path(), context.Background()); err != nil {
return nil, errors.WithStack(err)
}
return b.Details(), nil
}

View File

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

View File

@@ -1,87 +0,0 @@
package backup
import (
"context"
"fmt"
"github.com/apex/log"
"github.com/pkg/errors"
"io"
"net/http"
"os"
"strconv"
)
type S3Backup struct {
Backup
// The pre-signed upload endpoint for the generated backup. This must be
// provided otherwise this request will fail. This allows us to keep all
// of the keys off the daemon instances and the panel can handle generating
// the credentials for us.
PresignedUrl string
}
var _ BackupInterface = (*S3Backup)(nil)
// Generates a new backup on the disk, moves it into the S3 bucket via the provided
// presigned URL, and then deletes the backup from the disk.
func (s *S3Backup) Generate(included *IncludedFiles, prefix string) (*ArchiveDetails, error) {
defer s.Remove()
a := &Archive{
TrimPrefix: prefix,
Files: included,
}
if err := a.Create(s.Path(), context.Background()); err != nil {
return nil, errors.WithStack(err)
}
rc, err := os.Open(s.Path())
if err != nil {
return nil, errors.WithStack(err)
}
defer rc.Close()
if resp, err := s.generateRemoteRequest(rc); err != nil {
return nil, errors.WithStack(err)
} else {
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to put S3 object, %d:%s", resp.StatusCode, resp.Status)
}
}
return s.Details(), err
}
// Removes a backup from the system.
func (s *S3Backup) Remove() error {
return os.Remove(s.Path())
}
// Generates the remote S3 request and begins the upload.
func (s *S3Backup) generateRemoteRequest(rc io.ReadCloser) (*http.Response, error) {
r, err := http.NewRequest(http.MethodPut, s.PresignedUrl, nil)
if err != nil {
return nil, err
}
if sz, err := s.Size(); err != nil {
return nil, err
} else {
r.ContentLength = sz
r.Header.Add("Content-Length", strconv.Itoa(int(sz)))
r.Header.Add("Content-Type", "application/x-gzip")
}
r.Body = rc
log.WithFields(log.Fields{
"endpoint": s.PresignedUrl,
"headers": r.Header,
}).Debug("uploading backup to remote S3 endpoint")
return http.DefaultClient.Do(r)
}

View File

@@ -1,25 +0,0 @@
package backup
import (
"sync"
)
type IncludedFiles struct {
sync.RWMutex
files []string
}
// Pushes an additional file or folder onto the struct.
func (i *IncludedFiles) Push(p string) {
i.Lock()
i.files = append(i.files, p) // ~~
i.Unlock()
}
// Returns all of the files that were marked as being included.
func (i *IncludedFiles) All() []string {
i.RLock()
defer i.RUnlock()
return i.files
}

View File

@@ -4,35 +4,34 @@ import "sync"
type Collection struct {
items []*Server
sync.RWMutex
mutex *sync.Mutex
}
// Create a new collection from a slice of servers.
func NewCollection(servers []*Server) *Collection {
return &Collection{
items: servers,
mutex: &sync.Mutex{},
}
}
// Return all of the items in the collection.
func (c *Collection) All() []*Server {
c.RLock()
defer c.RUnlock()
return c.items
}
// Adds an item to the collection store.
func (c *Collection) Add(s *Server) {
c.Lock()
c.mutex.Lock()
defer c.mutex.Unlock()
c.items = append(c.items, s)
c.Unlock()
}
// Returns only those items matching the filter criteria.
func (c *Collection) Filter(filter func(*Server) bool) []*Server {
c.RLock()
defer c.RUnlock()
c.mutex.Lock()
defer c.mutex.Unlock()
r := make([]*Server, 0)
for _, v := range c.items {
@@ -47,9 +46,6 @@ func (c *Collection) Filter(filter func(*Server) bool) []*Server {
// Returns a single element from the collection matching the filter. If nothing is
// found a nil result is returned.
func (c *Collection) Find(filter func(*Server) bool) *Server {
c.RLock()
defer c.RUnlock()
for _, v := range c.items {
if filter(v) {
return v
@@ -61,8 +57,8 @@ func (c *Collection) Find(filter func(*Server) bool) *Server {
// Removes all items from the collection that match the filter function.
func (c *Collection) Remove(filter func(*Server) bool) {
c.Lock()
defer c.Unlock()
c.mutex.Lock()
defer c.mutex.Unlock()
r := make([]*Server, 0)
for _, v := range c.items {

View File

@@ -1,32 +1,34 @@
package server
import (
"github.com/gammazero/workerpool"
"runtime"
"github.com/pterodactyl/wings/parser"
"go.uber.org/zap"
"sync"
)
// Parent function that will update all of the defined configuration files for a server
// automatically to ensure that they always use the specified values.
func (s *Server) UpdateConfigurationFiles() {
pool := workerpool.New(runtime.NumCPU())
wg := new(sync.WaitGroup)
files := s.ProcessConfiguration().ConfigurationFiles
for _, cf := range files {
f := cf
for _, v := range s.processConfiguration.ConfigurationFiles {
wg.Add(1)
pool.Submit(func() {
p, err := s.Filesystem().SafePath(f.FileName)
go func(f parser.ConfigurationFile, server *Server) {
defer wg.Done()
p, err := s.Filesystem.SafePath(f.FileName)
if err != nil {
s.Log().WithField("error", err).Error("failed to generate safe path for configuration file")
zap.S().Errorw("failed to generate safe path for configuration file", zap.String("server", server.Uuid), zap.Error(err))
return
}
if err := f.Parse(p, false); err != nil {
s.Log().WithField("error", err).Error("failed to parse and update server configuration file")
zap.S().Errorw("failed to parse and update server configuration file", zap.String("server", server.Uuid), zap.Error(err))
}
})
}(v, s)
}
pool.StopWait()
}
wg.Wait()
}

View File

@@ -1,76 +1,31 @@
package server
import (
"github.com/pterodactyl/wings/environment"
"sync"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
"os"
)
type Configuration struct {
mu sync.RWMutex
// Writes the server configuration to the disk. The saved configuration will be returned
// back to the calling function to use if desired.
func (s *Server) WriteConfigurationToDisk() ([]byte, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
// The unique identifier for the server that should be used when referencing
// it against the Panel API (and internally). This will be used when naming
// docker containers as well as in log output.
Uuid string `json:"uuid"`
f, err := os.Create("data/servers/" + s.Uuid + ".yml")
if err != nil {
return nil, errors.WithStack(err)
}
defer f.Close()
// Whether or not the server is in a suspended state. Suspended servers cannot
// be started or modified except in certain scenarios by an admin user.
Suspended bool `json:"suspended"`
b, err := yaml.Marshal(&s)
if err != nil {
return nil, errors.WithStack(err)
}
// The command that should be used when booting up the server instance.
Invocation string `json:"invocation"`
if _, err := f.Write(b); err != nil {
return nil, errors.WithStack(err)
}
// By default this is false, however if selected within the Panel while installing or re-installing a
// server, specific installation scripts will be skipped for the server process.
SkipEggScripts bool `default:"false" json:"skip_egg_scripts"`
// An array of environment variables that should be passed along to the running
// server process.
EnvVars environment.Variables `json:"environment"`
Allocations environment.Allocations `json:"allocations"`
Build environment.Limits `json:"build"`
CrashDetectionEnabled bool `default:"true" json:"enabled" yaml:"enabled"`
Mounts []Mount `json:"mounts"`
Resources ResourceUsage `json:"resources"`
Container struct {
// Defines the Docker image that will be used for this server
Image string `json:"image,omitempty"`
} `json:"container,omitempty"`
}
func (s *Server) Config() *Configuration {
s.cfg.mu.RLock()
defer s.cfg.mu.RUnlock()
return &s.cfg
}
// Returns the amount of disk space available to a server in bytes.
func (s *Server) DiskSpace() int64 {
s.cfg.mu.RLock()
defer s.cfg.mu.RUnlock()
return s.cfg.Build.DiskSpace * 1024.0 * 1024.0
}
func (s *Server) MemoryLimit() int64 {
s.cfg.mu.RLock()
defer s.cfg.mu.RUnlock()
return s.cfg.Build.MemoryLimit
}
func (c *Configuration) GetUuid() string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.Uuid
}
func (c *Configuration) SetSuspended(s bool) {
c.mu.Lock()
c.Suspended = s
c.mu.Unlock()
}
return b, nil
}

View File

@@ -1,168 +1,21 @@
package server
import (
"context"
"fmt"
"github.com/mitchellh/colorstring"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/system"
"sync"
"sync/atomic"
"time"
)
import "io"
var ErrTooMuchConsoleData = errors.New("console is outputting too much data")
type ConsoleThrottler struct {
mu sync.Mutex
config.ConsoleThrottles
// The total number of activations that have occurred thus far.
activations uint64
// The total number of lines that have been sent since the last reset timer period.
count uint64
// Wether or not the console output is being throttled. It is up to calling code to
// determine what to do if it is.
isThrottled system.AtomicBool
// The total number of lines processed so far during the given time period.
timerCancel *context.CancelFunc
type Console struct {
Server *Server
HandlerFunc *func(string)
}
// Resets the state of the throttler.
func (ct *ConsoleThrottler) Reset() {
atomic.StoreUint64(&ct.count, 0)
atomic.StoreUint64(&ct.activations, 0)
ct.isThrottled.Set(false)
}
var _ io.Writer = Console{}
// Triggers an activation for a server. You can also decrement the number of activations
// by passing a negative number.
func (ct *ConsoleThrottler) markActivation(increment bool) uint64 {
if !increment {
if atomic.LoadUint64(&ct.activations) == 0 {
return 0
}
func (c Console) Write(b []byte) (int, error) {
if c.HandlerFunc != nil {
l := make([]byte, len(b))
copy(l, b)
// This weird dohicky subtracts 1 from the activation count.
return atomic.AddUint64(&ct.activations, ^uint64(0))
(*c.HandlerFunc)(string(l))
}
return atomic.AddUint64(&ct.activations, 1)
}
// Determines if the console is currently being throttled. Calls to this function can be used to
// determine if output should be funneled along to the websocket processes.
func (ct *ConsoleThrottler) Throttled() bool {
return ct.isThrottled.Get()
}
// Starts a timer that runs in a seperate thread and will continually decrement the lines processed
// and number of activations, regardless of the current console message volume.
func (ct *ConsoleThrottler) StartTimer() {
ctx, cancel := context.WithCancel(context.Background())
reset := time.NewTicker(time.Duration(int64(ct.LineResetInterval)) * time.Millisecond)
decay := time.NewTicker(time.Duration(int64(ct.DecayInterval)) * time.Millisecond)
go func() {
for {
select {
case <-ctx.Done():
reset.Stop()
return
case <-reset.C:
ct.isThrottled.Set(false)
atomic.StoreUint64(&ct.count, 0)
}
}
}()
go func() {
for {
select {
case <-ctx.Done():
decay.Stop()
return
case <-decay.C:
ct.markActivation(false)
}
}
}()
ct.timerCancel = &cancel
}
// Stops a running timer processes if one exists. This is only called when the server is deleted since
// we want this to always be running. If there is no process currently running nothing will really happen.
func (ct *ConsoleThrottler) StopTimer() {
ct.mu.Lock()
defer ct.mu.Unlock()
if ct.timerCancel != nil {
c := *ct.timerCancel
c()
ct.timerCancel = nil
}
}
// Handles output from a server's console. This code ensures that a server is not outputting
// an excessive amount of data to the console that could indicate a malicious or run-away process
// and lead to performance issues for other users.
//
// This was much more of a problem for the NodeJS version of the daemon which struggled to handle
// large volumes of output. However, this code is much more performant so I generally feel a lot
// better about it's abilities.
//
// However, extreme output is still somewhat of a DoS attack vector against this software since we
// are still logging it to the disk temporarily and will want to avoid dumping a huge amount of
// data all at once. These values are all configurable via the wings configuration file, however the
// defaults have been in the wild for almost two years at the time of this writing, so I feel quite
// confident in them.
//
// This function returns an error if the server should be stopped due to violating throttle constraints
// and a boolean value indicating if a throttle is being violated when it is checked.
func (ct *ConsoleThrottler) Increment(onTrigger func()) error {
if !ct.Enabled {
return nil
}
// Increment the line count and if we have now output more lines than are allowed, trigger a throttle
// activation. Once the throttle is triggered and has passed the kill at value we will trigger a server
// stop automatically.
if atomic.AddUint64(&ct.count, 1) >= ct.Lines && !ct.Throttled() {
ct.isThrottled.Set(true)
if ct.markActivation(true) >= ct.MaximumTriggerCount {
return ErrTooMuchConsoleData
}
onTrigger()
}
return nil
}
// Returns the throttler instance for the server or creates a new one.
func (s *Server) Throttler() *ConsoleThrottler {
s.throttleLock.Lock()
defer s.throttleLock.Unlock()
if s.throttler == nil {
s.throttler = &ConsoleThrottler{
ConsoleThrottles: config.Get().Throttles,
}
}
return s.throttler
}
// Sends output to the server console formatted to appear correctly as being sent
// from Wings.
func (s *Server) PublishConsoleOutputFromDaemon(data string) {
s.Events().Publish(
ConsoleOutputEvent,
colorstring.Color(fmt.Sprintf("[yellow][bold][Pterodactyl Daemon]:[default] %s", data)),
)
}
return len(b), nil
}

View File

@@ -4,52 +4,40 @@ import (
"fmt"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment"
"sync"
"go.uber.org/zap"
"time"
)
type CrashHandler struct {
mu sync.RWMutex
type CrashDetection struct {
// If set to false, the system will not listen for crash detection events that
// can indicate that the server stopped unexpectedly.
Enabled bool `default:"true" json:"enabled" yaml:"enabled"`
// Tracks the time of the last server crash event.
lastCrash time.Time
}
// Returns the time of the last crash for this server instance.
func (cd *CrashHandler) LastCrashTime() time.Time {
cd.mu.RLock()
defer cd.mu.RUnlock()
return cd.lastCrash
}
// Sets the last crash time for a server.
func (cd *CrashHandler) SetLastCrash(t time.Time) {
cd.mu.Lock()
cd.lastCrash = t
cd.mu.Unlock()
}
// Looks at the environment exit state to determine if the process exited cleanly or
// if it was the result of an event that we should try to recover from.
//
// This function assumes it is called under circumstances where a crash is suspected
// of occurring. It will not do anything to determine if it was actually a crash, just
// of occuring. It will not do anything to determine if it was actually a crash, just
// look at the exit state and check if it meets the criteria of being called a crash
// by Wings.
//
// If the server is determined to have crashed, the process will be restarted and the
// counter for the server will be incremented.
//
// @todo output event to server console
func (s *Server) handleServerCrash() error {
// No point in doing anything here if the server isn't currently offline, there
// is no reason to do a crash detection event. If the server crash detection is
// disabled we want to skip anything after this as well.
if s.GetState() != environment.ProcessOfflineState || !s.Config().CrashDetectionEnabled {
if !s.Config().CrashDetectionEnabled {
s.Log().Debug("server triggered crash detection but handler is disabled for server process")
if s.State != ProcessOfflineState || !s.CrashDetection.Enabled {
if !s.CrashDetection.Enabled {
zap.S().Debugw("server triggered crash detection but handler is disabled for server process", zap.String("server", s.Uuid))
s.PublishConsoleOutputFromDaemon("Server detected as crashed; crash detection is disabled for this instance.")
s.SendConsoleOutputFromDaemon("Server detected as crashed; crash detection is disabled for this instance.")
}
return nil
@@ -63,25 +51,25 @@ func (s *Server) handleServerCrash() error {
// If the system is not configured to detect a clean exit code as a crash, and the
// crash is not the result of the program running out of memory, do nothing.
if exitCode == 0 && !oomKilled && !config.Get().System.DetectCleanExitAsCrash {
s.Log().Debug("server exited with successful exit code; system is configured to not detect this as a crash")
zap.S().Debugw("server exited with successful code; system configured to not detect as crash", zap.String("server", s.Uuid))
return nil
}
s.PublishConsoleOutputFromDaemon("---------- Detected server process in a crashed state! ----------")
s.PublishConsoleOutputFromDaemon(fmt.Sprintf("Exit code: %d", exitCode))
s.PublishConsoleOutputFromDaemon(fmt.Sprintf("Out of memory: %t", oomKilled))
s.SendConsoleOutputFromDaemon("---------- Detected server process in a crashed state! ----------")
s.SendConsoleOutputFromDaemon(fmt.Sprintf("Exit code: %d", exitCode))
s.SendConsoleOutputFromDaemon(fmt.Sprintf("Out of memory: %t", oomKilled))
c := s.crasher.LastCrashTime()
c := s.CrashDetection.lastCrash
// If the last crash time was within the last 60 seconds we do not want to perform
// an automatic reboot of the process. Return an error that can be handled.
if !c.IsZero() && c.Add(time.Second*60).After(time.Now()) {
s.PublishConsoleOutputFromDaemon("Aborting automatic reboot: last crash occurred less than 60 seconds ago.")
if !c.IsZero() && c.Add(time.Second * 60).After(time.Now()) {
s.SendConsoleOutputFromDaemon("Aborting automatic reboot: last crash occurred less than 60 seconds ago.")
return &crashTooFrequent{}
}
s.crasher.SetLastCrash(time.Now())
s.CrashDetection.lastCrash = time.Now()
return s.HandlePowerAction(PowerActionStart)
}
return s.Environment.Start()
}

View File

@@ -1,45 +1,15 @@
package environment
package server
import (
"github.com/pterodactyl/wings/events"
"os"
)
const (
ConsoleOutputEvent = "console output"
StateChangeEvent = "state change"
ResourceEvent = "resources"
DockerImagePullStarted = "docker image pull started"
DockerImagePullStatus = "docker image pull status"
DockerImagePullCompleted = "docker image pull completed"
)
const (
ProcessOfflineState = "offline"
ProcessStartingState = "starting"
ProcessRunningState = "running"
ProcessStoppingState = "stopping"
)
// Defines the basic interface that all environments need to implement so that
// a server can be properly controlled.
type ProcessEnvironment interface {
type Environment interface {
// Returns the name of the environment.
Type() string
// Returns the environment configuration to the caller.
Config() *Configuration
// Returns an event emitter instance that can be hooked into to listen for different
// events that are fired by the environment. This should not allow someone to publish
// events, only subscribe to them.
Events() *events.EventBus
// Determines if the server instance exists. For example, in a docker environment
// this should confirm that the container is created and in a bootable state. In
// a basic CLI environment this can probably just return true right away.
Exists() (bool, error)
// Determines if the environment is currently active and running a server process
// for this specific server instance.
IsRunning() (bool, error)
@@ -61,10 +31,10 @@ type ProcessEnvironment interface {
// not be returned.
Stop() error
// Waits for a server instance to stop gracefully. If the server is still detected
// as running after seconds, an error will be returned, or the server will be terminated
// depending on the value of the second argument.
WaitForStop(seconds uint, terminate bool) error
// Determines if the server instance exists. For example, in a docker environment
// this should confirm that the container is created and in a bootable state. In
// a basic CLI environment this can probably just return true right away.
Exists() (bool, error)
// Terminates a running server instance using the provided signal. If the server
// is not running no error should be returned.
@@ -88,10 +58,22 @@ type ProcessEnvironment interface {
// send data into the environment's stdin.
Attach() error
// Follows the output from the server console and will begin piping the output to
// the server's emitter.
FollowConsoleOutput() error
// Sends the provided command to the running server instance.
SendCommand(string) error
// Reads the log file for the process from the end backwards until the provided
// number of lines is met.
Readlog(int) ([]string, error)
// number of bytes is met.
Readlog(int64) ([]string, error)
// Polls the given environment for resource usage of the server when the process
// is running.
EnableResourcePolling() error
// Disables the polling operation for resource usage and sets the required values
// to 0 in the server resource usage struct.
DisableResourcePolling() error
}

View File

@@ -0,0 +1,793 @@
package server
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/client"
"github.com/docker/docker/daemon/logger/jsonfilelog"
"github.com/docker/go-connections/nat"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api"
"go.uber.org/zap"
"io"
"os"
"strconv"
"strings"
"time"
)
// Defines the base environment for Docker instances running through Wings.
type DockerEnvironment struct {
Server *Server
// The user ID that containers should be running as.
User int
// Defines the configuration for the Docker instance that will allow us to connect
// and create and modify containers.
TimezonePath string
// The Docker client being used for this instance.
Client *client.Client
// Tracks if we are currently attached to the server container. This allows us to attach
// once and then just use that attachment to stream logs out of the server and also stream
// commands back into it without constantly attaching and detaching.
attached bool
// Controls the hijacked response stream which exists only when we're attached to
// the running container instance.
stream types.HijackedResponse
// Holds the stats stream used by the polling commands so that we can easily close
// it out.
stats io.ReadCloser
}
// Creates a new base Docker environment. A server must still be attached to it.
func NewDockerEnvironment(opts ...func(*DockerEnvironment)) (*DockerEnvironment, error) {
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return nil, err
}
env := &DockerEnvironment{
User: 1000,
Client: cli,
}
for _, opt := range opts {
opt(env)
}
return env, nil
}
// Ensure that the Docker environment is always implementing all of the methods
// from the base environment interface.
var _ Environment = (*DockerEnvironment)(nil)
// Returns the name of the environment.
func (d *DockerEnvironment) Type() string {
return "docker"
}
// Determines if the container exists in this environment.
func (d *DockerEnvironment) Exists() (bool, error) {
_, err := d.Client.ContainerInspect(context.Background(), d.Server.Uuid)
if err != nil {
// If this error is because the container instance wasn't found via Docker we
// can safely ignore the error and just return false.
if client.IsErrNotFound(err) {
return false, nil
}
return false, err
}
return true, nil
}
// Determines if the server's docker container is currently running. If there is no container
// present, an error will be raised (since this shouldn't be a case that ever happens under
// correctly developed circumstances).
//
// You can confirm if the instance wasn't found by using client.IsErrNotFound from the Docker
// API.
//
// @see docker/client/errors.go
func (d *DockerEnvironment) IsRunning() (bool, error) {
ctx := context.Background()
c, err := d.Client.ContainerInspect(ctx, d.Server.Uuid)
if err != nil {
return false, err
}
return c.State.Running, nil
}
// Performs an in-place update of the Docker container's resource limits without actually
// making any changes to the operational state of the container. This allows memory, cpu,
// and IO limitations to be adjusted on the fly for individual instances.
func (d *DockerEnvironment) InSituUpdate() error {
if _, err := d.Client.ContainerInspect(context.Background(), d.Server.Uuid); err != nil {
// If the container doesn't exist for some reason there really isn't anything
// we can do to fix that in this process (it doesn't make sense at least). In those
// cases just return without doing anything since we still want to save the configuration
// to the disk.
//
// We'll let a boot process make modifications to the container if needed at this point.
if client.IsErrNotFound(err) {
return nil
}
return errors.WithStack(err)
}
u := container.UpdateConfig{
Resources: d.getResourcesForServer(),
}
if _, err := d.Client.ContainerUpdate(context.Background(), d.Server.Uuid, u); err != nil {
return errors.WithStack(err)
}
return nil
}
// Run before the container starts and get the process configuration from the Panel.
// This is important since we use this to check configuration files as well as ensure
// we always have the latest version of an egg available for server processes.
//
// This process will also confirm that the server environment exists and is in a bootable
// state. This ensures that unexpected container deletion while Wings is running does
// not result in the server becoming unbootable.
func (d *DockerEnvironment) OnBeforeStart() error {
zap.S().Infow("syncing server configuration with Panel", zap.String("server", d.Server.Uuid))
if err := d.Server.Sync(); err != nil {
return err
}
// Always destroy and re-create the server container to ensure that synced data from
// the Panel is used.
if err := d.Client.ContainerRemove(context.Background(), d.Server.Uuid, types.ContainerRemoveOptions{RemoveVolumes: true}); err != nil {
if !client.IsErrNotFound(err) {
return err
}
}
// The Create() function will check if the container exists in the first place, and if
// so just silently return without an error. Otherwise, it will try to create the necessary
// container and data storage directory.
//
// This won't actually run an installation process however, it is just here to ensure the
// environment gets created properly if it is missing and the server is started. We're making
// an assumption that all of the files will still exist at this point.
if err := d.Create(); err != nil {
return err
}
return nil
}
// Starts the server environment and begins piping output to the event listeners for the
// console. If a container does not exist, or needs to be rebuilt that will happen in the
// call to OnBeforeStart().
func (d *DockerEnvironment) Start() error {
sawError := false
// If sawError is set to true there was an error somewhere in the pipeline that
// got passed up, but we also want to ensure we set the server to be offline at
// that point.
defer func() {
if sawError {
d.Server.SetState(ProcessOfflineState)
}
}()
// If the server is suspended the user shouldn't be able to boot it, in those cases
// return a suspension error and let the calling area handle the issue.
//
// Theoretically you'd have the Panel handle all of this logic, but we cannot do that
// because we allow the websocket to control the server power state as well, so we'll
// need to handle that action in here.
if d.Server.Suspended {
return &suspendedError{}
}
c, err := d.Client.ContainerInspect(context.Background(), d.Server.Uuid)
if err != nil && !client.IsErrNotFound(err) {
return errors.WithStack(err)
}
// No reason to try starting a container that is already running.
if c.State.Running {
d.Server.SetState(ProcessRunningState)
if !d.attached {
return d.Attach()
}
return nil
}
d.Server.SetState(ProcessStartingState)
// Set this to true for now, we will set it to false once we reach the
// end of this chain.
sawError = true
// Run the before start function and wait for it to finish. This will validate that the container
// exists on the system, and rebuild the container if that is required for server booting to
// occur.
if err := d.OnBeforeStart(); err != nil {
return errors.WithStack(err)
}
// Truncate the log file so we don't end up outputting a bunch of useless log information
// to the websocket and whatnot. Check first that the path and file exist before trying
// to truncate them.
if _, err := os.Stat(c.LogPath); err == nil {
if err := os.Truncate(c.LogPath, 0); err != nil {
return errors.WithStack(err)
}
}
// Update the configuration files defined for the server before beginning the boot process.
// This process executes a bunch of parallel updates, so we just block until that process
// is completed. Any errors as a result of this will just be bubbled out in the logger,
// we don't need to actively do anything about it at this point, worst comes to worst the
// server starts in a weird state and the user can manually adjust.
d.Server.UpdateConfigurationFiles()
// Reset the permissions on files for the server before actually trying
// to start it.
if err := d.Server.Filesystem.Chown("/"); err != nil {
return errors.WithStack(err)
}
opts := types.ContainerStartOptions{}
if err := d.Client.ContainerStart(context.Background(), d.Server.Uuid, opts); err != nil {
return errors.WithStack(err)
}
// No errors, good to continue through.
sawError = false
return d.Attach()
}
// Stops the container that the server is running in. This will allow up to 10
// seconds to pass before a failure occurs.
func (d *DockerEnvironment) Stop() error {
stop := d.Server.processConfiguration.Stop
if stop.Type == api.ProcessStopSignal {
return d.Terminate(os.Kill)
}
d.Server.SetState(ProcessStoppingState)
if stop.Type == api.ProcessStopCommand {
return d.SendCommand(stop.Value)
}
t := time.Second * 10
return d.Client.ContainerStop(context.Background(), d.Server.Uuid, &t)
}
// Forcefully terminates the container using the signal passed through.
func (d *DockerEnvironment) Terminate(signal os.Signal) error {
ctx := context.Background()
c, err := d.Client.ContainerInspect(ctx, d.Server.Uuid)
if err != nil {
return errors.WithStack(err)
}
if !c.State.Running {
return nil
}
d.Server.SetState(ProcessStoppingState)
return d.Client.ContainerKill(
ctx, d.Server.Uuid, strings.TrimSuffix(strings.TrimPrefix(signal.String(), "signal "), "ed"),
)
}
// Remove the Docker container from the machine. If the container is currently running
// it will be forcibly stopped by Docker.
func (d *DockerEnvironment) Destroy() error {
ctx := context.Background()
return d.Client.ContainerRemove(ctx, d.Server.Uuid, types.ContainerRemoveOptions{
RemoveVolumes: true,
RemoveLinks: false,
Force: true,
})
}
// Determine the container exit state and return the exit code and wether or not
// the container was killed by the OOM killer.
func (d *DockerEnvironment) ExitState() (uint32, bool, error) {
c, err := d.Client.ContainerInspect(context.Background(), d.Server.Uuid)
if err != nil {
return 0, false, errors.WithStack(err)
}
return uint32(c.State.ExitCode), c.State.OOMKilled, nil
}
// Attaches to the docker container itself and ensures that we can pipe data in and out
// of the process stream. This should not be used for reading console data as you *will*
// miss important output at the beginning because of the time delay with attaching to the
// output.
func (d *DockerEnvironment) Attach() error {
if d.attached {
return nil
}
if err := d.FollowConsoleOutput(); err != nil {
return errors.WithStack(err)
}
ctx := context.Background()
var err error
d.stream, err = d.Client.ContainerAttach(ctx, d.Server.Uuid, types.ContainerAttachOptions{
Stdin: true,
Stdout: true,
Stderr: true,
Stream: true,
})
if err != nil {
return errors.WithStack(err)
}
console := Console{
Server: d.Server,
}
d.EnableResourcePolling()
d.attached = true
go func() {
defer d.stream.Close()
defer func() {
d.Server.SetState(ProcessOfflineState)
d.attached = false
}()
io.Copy(console, d.stream.Reader)
}()
return nil
}
// Attaches to the log for the container. This avoids us missing cruicial output that
// happens in the split seconds before the code moves from 'Starting' to 'Attaching'
// on the process.
func (d *DockerEnvironment) FollowConsoleOutput() error {
if exists, err := d.Exists(); !exists {
if err != nil {
return errors.WithStack(err)
}
return errors.New(fmt.Sprintf("no such container: %s", d.Server.Uuid))
}
ctx := context.Background()
opts := types.ContainerLogsOptions{
ShowStderr: true,
ShowStdout: true,
Follow: true,
Since: time.Now().Format(time.RFC3339),
}
reader, err := d.Client.ContainerLogs(ctx, d.Server.Uuid, opts)
go func(r io.ReadCloser) {
defer r.Close()
s := bufio.NewScanner(r)
for s.Scan() {
d.Server.Emit(ConsoleOutputEvent, s.Text())
}
if err := s.Err(); err != nil {
zap.S().Warnw("error processing scanner line in console output", zap.String("server", d.Server.Uuid), zap.Error(err))
}
}(reader)
return errors.WithStack(err)
}
// Enables resource polling on the docker instance. Except we aren't actually polling Docker for this
// information, instead just sit there with an async process that lets Docker stream all of this data
// to us automatically.
func (d *DockerEnvironment) EnableResourcePolling() error {
if d.Server.State == ProcessOfflineState {
return errors.New("cannot enable resource polling on a server that is not running")
}
ctx := context.Background()
stats, err := d.Client.ContainerStats(ctx, d.Server.Uuid, true)
if err != nil {
return errors.WithStack(err)
}
d.stats = stats.Body
dec := json.NewDecoder(d.stats)
go func(s *Server) {
for {
var v *types.StatsJSON
if err := dec.Decode(&v); err != nil {
zap.S().Warnw("encountered error processing server stats; stopping collection", zap.Error(err))
d.DisableResourcePolling()
return
}
// Disable collection if the server is in an offline state and this process is
// still running.
if s.State == ProcessOfflineState {
d.DisableResourcePolling()
return
}
s.Resources.CpuAbsolute = s.Resources.CalculateAbsoluteCpu(&v.PreCPUStats, &v.CPUStats)
s.Resources.Memory = v.MemoryStats.Usage
s.Resources.MemoryLimit = v.MemoryStats.Limit
// Why you ask? This already has the logic for caching disk space in use and then
// also handles pushing that value to the resources object automatically.
s.Filesystem.HasSpaceAvailable()
for _, nw := range v.Networks {
s.Resources.Network.RxBytes += nw.RxBytes
s.Resources.Network.TxBytes += nw.TxBytes
}
b, _ := json.Marshal(s.Resources)
s.Emit(StatsEvent, string(b))
}
}(d.Server)
return nil
}
// Closes the stats stream for a server process.
func (d *DockerEnvironment) DisableResourcePolling() error {
if d.stats == nil {
return nil
}
err := d.stats.Close()
d.Server.Resources.CpuAbsolute = 0
d.Server.Resources.Memory = 0
d.Server.Resources.Network.TxBytes = 0
d.Server.Resources.Network.RxBytes = 0
return errors.WithStack(err)
}
// Pulls the image from Docker.
//
// @todo handle authorization & local images
func (d *DockerEnvironment) ensureImageExists(c *client.Client) error {
out, err := c.ImagePull(context.Background(), d.Server.Container.Image, types.ImagePullOptions{All: false})
if err != nil {
return err
}
defer out.Close()
zap.S().Debugw("pulling docker image... this could take a bit of time", zap.String("image", d.Server.Container.Image))
// I'm not sure what the best approach here is, but this will block execution until the image
// is done being pulled, which is what we need.
scanner := bufio.NewScanner(out)
for scanner.Scan() {
continue
}
if err := scanner.Err(); err != nil {
return err
}
return nil
}
// Creates a new container for the server using all of the data that is currently
// available for it. If the container already exists it will be returned.
//
// @todo pull the image being requested if it doesn't exist currently.
func (d *DockerEnvironment) Create() error {
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return errors.WithStack(err)
}
// Ensure the data directory exists before getting too far through this process.
if err := d.Server.Filesystem.EnsureDataDirectory(); err != nil {
return errors.WithStack(err)
}
// If the container already exists don't hit the user with an error, just return
// the current information about it which is what we would do when creating the
// container anyways.
if _, err := cli.ContainerInspect(ctx, d.Server.Uuid); err == nil {
return nil
} else if !client.IsErrNotFound(err) {
return errors.WithStack(err)
}
// Try to pull the requested image before creating the container.
if err := d.ensureImageExists(cli); err != nil {
return errors.WithStack(err)
}
conf := &container.Config{
Hostname: "container",
User: strconv.Itoa(d.User),
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
OpenStdin: true,
Tty: true,
ExposedPorts: d.exposedPorts(),
Image: d.Server.Container.Image,
Env: d.environmentVariables(),
Labels: map[string]string{
"Service": "Pterodactyl",
},
}
hostConf := &container.HostConfig{
PortBindings: d.portBindings(),
// Configure the mounts for this container. First mount the server data directory
// into the container as a r/w bind. Additionally mount the host timezone data into
// the container as a readonly bind so that software running in the container uses
// the same time as the host system.
Mounts: []mount.Mount{
{
Target: "/home/container",
Source: d.Server.Filesystem.Path(),
Type: mount.TypeBind,
ReadOnly: false,
},
{
Target: d.TimezonePath,
Source: d.TimezonePath,
Type: mount.TypeBind,
ReadOnly: true,
},
},
// Configure the /tmp folder mapping in containers. This is necessary for some
// games that need to make use of it for downloads and other installation processes.
Tmpfs: map[string]string{
"/tmp": "rw,exec,nosuid,size=50M",
},
// Define resource limits for the container based on the data passed through
// from the Panel.
Resources: d.getResourcesForServer(),
// @todo make this configurable again
DNS: []string{"1.1.1.1", "8.8.8.8"},
// Configure logging for the container to make it easier on the Daemon to grab
// the server output. Ensure that we don't use too much space on the host machine
// since we only need it for the last few hundred lines of output and don't care
// about anything else in it.
LogConfig: container.LogConfig{
Type: jsonfilelog.Name,
Config: map[string]string{
"max-size": "5m",
"max-file": "1",
},
},
SecurityOpt: []string{"no-new-privileges"},
ReadonlyRootfs: true,
CapDrop: []string{
"setpcap", "mknod", "audit_write", "net_raw", "dac_override",
"fowner", "fsetid", "net_bind_service", "sys_chroot", "setfcap",
},
NetworkMode: "pterodactyl_nw",
}
if _, err := cli.ContainerCreate(ctx, conf, hostConf, nil, d.Server.Uuid); err != nil {
return errors.WithStack(err)
}
return nil
}
// Sends the specified command to the stdin of the running container instance. There is no
// confirmation that this data is sent successfully, only that it gets pushed into the stdin.
func (d *DockerEnvironment) SendCommand(c string) error {
if !d.attached {
return errors.New("attempting to send command to non-attached instance")
}
_, err := d.stream.Conn.Write([]byte(c + "\n"))
return errors.WithStack(err)
}
// Reads the log file for the server. This does not care if the server is running or not, it will
// simply try to read the last X bytes of the file and return them.
func (d *DockerEnvironment) Readlog(len int64) ([]string, error) {
ctx := context.Background()
j, err := d.Client.ContainerInspect(ctx, d.Server.Uuid)
if err != nil {
return nil, err
}
if j.LogPath == "" {
return nil, errors.New("empty log path defined for server")
}
f, err := os.Open(j.LogPath)
if err != nil {
return nil, err
}
defer f.Close()
// Check if the length of the file is smaller than the amount of data that was requested
// for reading. If so, adjust the length to be the total length of the file. If this is not
// done an error is thrown since we're reading backwards, and not forwards.
if stat, err := os.Stat(j.LogPath); err != nil {
return nil, err
} else if stat.Size() < len {
len = stat.Size()
}
// Seed to the end of the file and then move backwards until the length is met to avoid
// reading the entirety of the file into memory.
if _, err := f.Seek(-len, io.SeekEnd); err != nil {
return nil, err
}
b := make([]byte, len)
if _, err := f.Read(b); err != nil && err != io.EOF {
return nil, err
}
return d.parseLogToStrings(b)
}
type dockerLogLine struct {
Log string `json:"log"`
}
// Docker stores the logs for server output in a JSON format. This function will iterate over the JSON
// that was read from the log file and parse it into a more human readable format.
func (d *DockerEnvironment) parseLogToStrings(b []byte) ([]string, error) {
var hasError = false
var out []string
scanner := bufio.NewScanner(bytes.NewReader(b))
for scanner.Scan() {
var l dockerLogLine
// Unmarshal the contents and allow up to a single error before bailing out of the process. We
// do this because if you're arbitrarily reading a length of the file you'll likely end up
// with the first line in the output being improperly formatted JSON. In those cases we want to
// just skip over it. However if we see another error we're going to bail out because that is an
// abnormal situation.
if err := json.Unmarshal([]byte(scanner.Text()), &l); err != nil {
if hasError {
return nil, err
}
hasError = true
continue
}
out = append(out, l.Log)
}
return out, nil
}
// Returns the environment variables for a server in KEY="VALUE" form.
func (d *DockerEnvironment) environmentVariables() []string {
var out = []string{
fmt.Sprintf("STARTUP=%s", d.Server.Invocation),
fmt.Sprintf("SERVER_MEMORY=%d", d.Server.Build.MemoryLimit),
fmt.Sprintf("SERVER_IP=%s", d.Server.Allocations.DefaultMapping.Ip),
fmt.Sprintf("SERVER_PORT=%d", d.Server.Allocations.DefaultMapping.Port),
}
eloop:
for k, v := range d.Server.EnvVars {
for _, e := range out {
if strings.HasPrefix(e, strings.ToUpper(k)) {
continue eloop
}
}
out = append(out, fmt.Sprintf("%s=\"%s\"", strings.ToUpper(k), v))
}
return out
}
func (d *DockerEnvironment) volumes() map[string]struct{} {
return nil
}
// Converts the server allocation mappings into a format that can be understood
// by Docker.
func (d *DockerEnvironment) portBindings() nat.PortMap {
var out = nat.PortMap{}
for ip, ports := range d.Server.Allocations.Mappings {
for _, port := range ports {
// Skip over invalid ports.
if port < 0 || port > 65535 {
continue
}
binding := []nat.PortBinding{
{
HostIP: ip,
HostPort: strconv.Itoa(port),
},
}
out[nat.Port(fmt.Sprintf("%d/tcp", port))] = binding
out[nat.Port(fmt.Sprintf("%d/udp", port))] = binding
}
}
return out
}
// Converts the server allocation mappings into a PortSet that can be understood
// by Docker. This formatting is slightly different than portBindings as it should
// return an empty struct rather than a binding.
//
// To accomplish this, we'll just get the values from portBindings and then set them
// to empty structs. Because why not.
func (d *DockerEnvironment) exposedPorts() nat.PortSet {
var out = nat.PortSet{}
for port := range d.portBindings() {
out[port] = struct{}{}
}
return out
}
// Formats the resources available to a server instance in such as way that Docker will
// generate a matching environment in the container.
func (d *DockerEnvironment) getResourcesForServer() container.Resources {
return container.Resources{
// @todo memory limit should be slightly higher than the reservation
Memory: d.Server.Build.MemoryLimit * 1000000,
MemoryReservation: d.Server.Build.MemoryLimit * 1000000,
MemorySwap: d.Server.Build.ConvertedSwap(),
CPUQuota: d.Server.Build.ConvertedCpuLimit(),
CPUPeriod: 100000,
CPUShares: 1024,
BlkioWeight: d.Server.Build.IoWeight,
OomKillDisable: &d.Server.Container.OomDisabled,
}
}

View File

@@ -1,9 +1,17 @@
package server
import "github.com/pkg/errors"
type suspendedError struct {
}
var ErrIsRunning = errors.New("server is running")
var ErrSuspended = errors.New("server is currently in a suspended state")
func (e *suspendedError) Error() string {
return "server is currently in a suspended state"
}
func IsSuspendedError(err error) bool {
_, ok := err.(*suspendedError)
return ok
}
type crashTooFrequent struct {
}
@@ -29,4 +37,4 @@ func IsServerDoesNotExistError(err error) bool {
_, ok := err.(*serverDoesNotExist)
return ok
}
}

View File

@@ -1,30 +1,63 @@
package server
import (
"github.com/pterodactyl/wings/events"
"fmt"
"github.com/mitchellh/colorstring"
)
type EventListeners map[string][]EventListenerFunction
type EventListenerFunction *func(string)
// Defines all of the possible output events for a server.
// noinspection GoNameStartsWithPackageName
const (
DaemonMessageEvent = "daemon message"
InstallOutputEvent = "install output"
InstallStartedEvent = "install started"
InstallCompletedEvent = "install completed"
ConsoleOutputEvent = "console output"
StatusEvent = "status"
StatsEvent = "stats"
BackupCompletedEvent = "backup completed"
ConsoleOutputEvent = "console output"
StatusEvent = "status"
StatsEvent = "stats"
)
// Returns the server's emitter instance.
func (s *Server) Events() *events.EventBus {
s.emitterLock.Lock()
defer s.emitterLock.Unlock()
if s.emitter == nil {
s.emitter = events.New()
// Adds an event listener for the server instance.
func (s *Server) AddListener(event string, f EventListenerFunction) {
if s.listeners == nil {
s.listeners = make(map[string][]EventListenerFunction)
}
return s.emitter
if _, ok := s.listeners[event]; ok {
s.listeners[event] = append(s.listeners[event], f)
} else {
s.listeners[event] = []EventListenerFunction{f}
}
}
// Removes the event listener for the server instance.
func (s *Server) RemoveListener(event string, f EventListenerFunction) {
if _, ok := s.listeners[event]; ok {
for i := range s.listeners[event] {
if s.listeners[event][i] == f {
s.listeners[event] = append(s.listeners[event][:i], s.listeners[event][i+1:]...)
break
}
}
}
}
// Emits an event to all of the active listeners for a server.
func (s *Server) Emit(event string, data string) {
if _, ok := s.listeners[event]; ok {
for _, handler := range s.listeners[event] {
go func(f EventListenerFunction, d string) {
(*f)(d)
}(handler, data)
}
}
}
// Sends output to the server console formatted to appear correctly as being sent
// from Wings.
func (s *Server) SendConsoleOutputFromDaemon(data string) {
s.Emit(
ConsoleOutputEvent,
colorstring.Color(fmt.Sprintf("[yellow][bold][Pterodactyl Daemon]:[default] %s", data)),
)
}

View File

@@ -1,30 +1,570 @@
package server
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"github.com/gabriel-vasile/mimetype"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/server/filesystem"
"github.com/pterodactyl/wings/config"
"go.uber.org/zap"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"time"
)
func (s *Server) Filesystem() *filesystem.Filesystem {
return s.fs
// Error returned when there is a bad path provided to one of the FS calls.
var InvalidPathResolution = errors.New("invalid path resolution")
type Filesystem struct {
// The server object associated with this Filesystem.
Server *Server
Configuration *config.SystemConfiguration
}
// Ensures that the data directory for the server instance exists.
func (s *Server) EnsureDataDirectoryExists() error {
if _, err := os.Stat(s.fs.Path()); err != nil && !os.IsNotExist(err) {
// Returns the root path that contains all of a server's data.
func (fs *Filesystem) Path() string {
return filepath.Join(fs.Configuration.Data, fs.Server.Uuid)
}
// Normalizes a directory being passed in to ensure the user is not able to escape
// from their data directory. After normalization if the directory is still within their home
// path it is returned. If they managed to "escape" an error will be returned.
//
// This logic is actually copied over from the SFTP server code. Ideally that eventually
// either gets ported into this application, or is able to make use of this package.
func (fs *Filesystem) SafePath(p string) (string, error) {
var nonExistentPathResolution string
// Calling filpath.Clean on the joined directory will resolve it to the absolute path,
// removing any ../ type of resolution arguments, and leaving us with a direct path link.
//
// This will also trim the existing root path off the beginning of the path passed to
// the function since that can get a bit messy.
r := filepath.Clean(filepath.Join(fs.Path(), strings.TrimPrefix(p, fs.Path())))
// At the same time, evaluate the symlink status and determine where this file or folder
// is truly pointing to.
p, err := filepath.EvalSymlinks(r)
if err != nil && !os.IsNotExist(err) {
return "", err
} else if os.IsNotExist(err) {
// The requested directory doesn't exist, so at this point we need to iterate up the
// path chain until we hit a directory that _does_ exist and can be validated.
parts := strings.Split(filepath.Dir(r), "/")
var try string
// Range over all of the path parts and form directory pathings from the end
// moving up until we have a valid resolution or we run out of paths to try.
for k := range parts {
try = strings.Join(parts[:(len(parts) - k)], "/")
if !strings.HasPrefix(try, fs.Path()) {
break
}
t, err := filepath.EvalSymlinks(try)
if err == nil {
nonExistentPathResolution = t
break
}
}
}
// If the new path doesn't start with their root directory there is clearly an escape
// attempt going on, and we should NOT resolve this path for them.
if nonExistentPathResolution != "" {
if !strings.HasPrefix(nonExistentPathResolution, fs.Path()) {
return "", InvalidPathResolution
}
// If the nonExistentPathResoltion variable is not empty then the initial path requested
// did not exist and we looped through the pathway until we found a match. At this point
// we've confirmed the first matched pathway exists in the root server directory, so we
// can go ahead and just return the path that was requested initially.
return r, nil
}
// If the requested directory from EvalSymlinks begins with the server root directory go
// ahead and return it. If not we'll return an error which will block any further action
// on the file.
if strings.HasPrefix(p, fs.Path()) {
return p, nil
}
return "", InvalidPathResolution
}
// Determines if the directory a file is trying to be added to has enough space available
// for the file to be written to.
//
// Because determining the amount of space being used by a server is a taxing operation we
// will load it all up into a cache and pull from that as long as the key is not expired.
func (fs *Filesystem) HasSpaceAvailable() bool {
var space = fs.Server.Build.DiskSpace
// If space is -1 or 0 just return true, means they're allowed unlimited.
if space <= 0 {
return true
}
var size int64
if x, exists := fs.Server.Cache.Get("disk_used"); exists {
size = x.(int64)
}
// If there is no size its either because there is no data (in which case running this function
// will have effectively no impact), or there is nothing in the cache, in which case we need to
// grab the size of their data directory. This is a taxing operation, so we want to store it in
// the cache once we've gotten it.
if size == 0 {
if size, err := fs.DirectorySize("/"); err != nil {
zap.S().Warnw("failed to determine directory size", zap.String("server", fs.Server.Uuid), zap.Error(err))
} else {
fs.Server.Cache.Set("disk_used", size, time.Second * 60)
}
}
// Determine if their folder size, in bytes, is smaller than the amount of space they've
// been allocated.
fs.Server.Resources.Disk = size
return (size / 1000.0 / 1000.0) <= space
}
// Determines the directory size of a given location by running parallel tasks to iterate
// through all of the folders. Returns the size in bytes. This can be a fairly taxing operation
// on locations with tons of files, so it is recommended that you cache the output.
func (fs *Filesystem) DirectorySize(dir string) (int64, error) {
var size int64
var wg sync.WaitGroup
cleaned, err := fs.SafePath(dir)
if err != nil {
return 0, err
}
files, err := ioutil.ReadDir(cleaned)
if err != nil {
return 0, err
}
// Iterate over all of the files and directories. If it is a file, immediately add its size
// to the total size being returned. If we're dealing with a directory, call this function
// on a seperate thread until we have gotten the size of everything nested within the given
// directory.
for _, f := range files {
if f.IsDir() {
wg.Add(1)
go func(p string) {
defer wg.Done()
s, _ := fs.DirectorySize(p)
size += s
}(filepath.Join(cleaned, f.Name()))
} else {
size += f.Size()
}
}
wg.Wait()
return size, nil
}
// Reads a file on the system and returns it as a byte representation in a file
// reader. This is not the most memory efficient usage since it will be reading the
// entirety of the file into memory.
func (fs *Filesystem) Readfile(p string) (io.Reader, error) {
cleaned, err := fs.SafePath(p)
if err != nil {
return nil, err
}
b, err := ioutil.ReadFile(cleaned)
if err != nil {
return nil, err
}
return bytes.NewReader(b), nil
}
// Writes a file to the system. If the file does not already exist one will be created.
//
// @todo should probably have a write lock here so we don't write twice at once.
func (fs *Filesystem) Writefile(p string, r io.Reader) error {
cleaned, err := fs.SafePath(p)
if err != nil {
return errors.WithStack(err)
} else if err != nil {
// Create the server data directory because it does not currently exist
// on the system.
if err := os.MkdirAll(s.fs.Path(), 0700); err != nil {
}
// If the file does not exist on the system already go ahead and create the pathway
// 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 && os.IsNotExist(err) {
if err := os.MkdirAll(filepath.Dir(cleaned), 0755); err != nil {
return errors.WithStack(err)
}
if err := s.fs.Chown("/"); err != nil {
s.Log().WithField("error", err).Warn("failed to chown server data directory")
if err := fs.Chown(filepath.Dir(cleaned)); err != nil {
return errors.WithStack(err)
}
} else if err != nil {
return errors.WithStack(err)
} else if stat.IsDir() {
return errors.New("cannot use a directory as a file for writing")
}
// This will either create the file if it does not already exist, or open and
// truncate the existing file.
file, err := os.OpenFile(cleaned, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return errors.WithStack(err)
}
defer file.Close()
// Create a new buffered writer that will write to the file we just opened
// and stream in the contents from the reader.
w := bufio.NewWriter(file)
buf := make([]byte, 1024)
for {
n, err := r.Read(buf)
if err != nil && err != io.EOF {
return errors.WithStack(err)
}
if n == 0 {
break
}
if _, err := w.Write(buf[:n]); err != nil {
return errors.WithStack(err)
}
}
if err := w.Flush(); err != nil {
return errors.WithStack(err)
}
// Finally, chown the file to ensure the permissions don't end up out-of-whack
// if we had just created it.
return fs.Chown(p)
}
// Defines the stat struct object.
type Stat struct {
Info os.FileInfo
Mimetype string
}
func (s *Stat) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Name string `json:"name"`
Created string `json:"created"`
Modified string `json:"modified"`
Mode string `json:"mode"`
Size int64 `json:"size"`
Directory bool `json:"directory"`
File bool `json:"file"`
Symlink bool `json:"symlink"`
Mime string `json:"mime"`
}{
Name: s.Info.Name(),
Created: s.CTime().Format(time.RFC3339),
Modified: s.Info.ModTime().Format(time.RFC3339),
Mode: s.Info.Mode().String(),
Size: s.Info.Size(),
Directory: s.Info.IsDir(),
File: !s.Info.IsDir(),
Symlink: s.Info.Mode().Perm()&os.ModeSymlink != 0,
Mime: s.Mimetype,
})
}
// Stats a file or folder and returns the base stat object from go along with the
// MIME data that can be used for editing files.
func (fs *Filesystem) Stat(p string) (*Stat, error) {
cleaned, err := fs.SafePath(p)
if err != nil {
return nil, err
}
s, err := os.Stat(cleaned)
if err != nil {
return nil, err
}
var m = "inode/directory"
if !s.IsDir() {
m, _, err = mimetype.DetectFile(cleaned)
if err != nil {
return nil, err
}
}
st := &Stat{
Info: s,
Mimetype: m,
}
return st, nil
}
// Creates a new directory (name) at a specificied path (p) for the server.
func (fs *Filesystem) CreateDirectory(name string, p string) error {
cleaned, err := fs.SafePath(path.Join(p, name))
if err != nil {
return errors.WithStack(err)
}
return os.MkdirAll(cleaned, 0755)
}
// Moves (or renames) a file or directory.
func (fs *Filesystem) Rename(from string, to string) error {
cleanedFrom, err := fs.SafePath(from)
if err != nil {
return errors.WithStack(err)
}
cleanedTo, err := fs.SafePath(to)
if err != nil {
return errors.WithStack(err)
}
return os.Rename(cleanedFrom, cleanedTo)
}
// Recursively iterates over a directory and sets the permissions on all of the
// underlying files.
func (fs *Filesystem) Chown(path string) error {
cleaned, err := fs.SafePath(path)
if err != nil {
return errors.WithStack(err)
}
if s, err := os.Stat(cleaned); err != nil {
return errors.WithStack(err)
} else if !s.IsDir() {
return os.Chown(cleaned, fs.Configuration.User.Uid, fs.Configuration.User.Gid)
}
return fs.chownDirectory(cleaned)
}
// Iterate over all of the files and directories. If it is a file just go ahead and perform
// the chown operation. Otherwise dig deeper into the directory until we've run out of
// directories to dig into.
func (fs *Filesystem) chownDirectory(path string) error {
var wg sync.WaitGroup
cleaned, err := fs.SafePath(path)
if err != nil {
return errors.WithStack(err)
}
files, err := ioutil.ReadDir(cleaned)
if err != nil {
return errors.WithStack(err)
}
for _, f := range files {
if f.IsDir() {
wg.Add(1)
go func(p string) {
defer wg.Done()
fs.chownDirectory(p)
}(filepath.Join(cleaned, f.Name()))
} else {
os.Chown(filepath.Join(cleaned, f.Name()), fs.Configuration.User.Uid, fs.Configuration.User.Gid)
}
}
wg.Wait()
return nil
}
// Copies a given file to the same location and appends a suffix to the file to indicate that
// it has been copied.
//
// @todo need to get an exclusive lock on the file.
func (fs *Filesystem) Copy(p string) error {
cleaned, err := fs.SafePath(p)
if err != nil {
return errors.WithStack(err)
}
if s, err := os.Stat(cleaned); (err != nil && os.IsNotExist(err)) || s.IsDir() || !s.Mode().IsRegular() {
// For now I think I am okay just returning a nil response if the thing
// we're trying to copy doesn't exist. Probably will want to come back and
// re-evaluate if this is a smart decision (I'm guessing not).
return nil
} else if err != nil {
return errors.WithStack(err)
}
base := filepath.Base(cleaned)
relative := strings.TrimSuffix(strings.TrimPrefix(cleaned, fs.Path()), base)
extension := filepath.Ext(base)
name := strings.TrimSuffix(base, filepath.Ext(base))
// Begin looping up to 50 times to try and create a unique copy file name. This will take
// an input of "file.txt" and generate "file copy.txt". If that name is already taken, it will
// then try to write "file copy 2.txt" and so on, until reaching 50 loops. At that point we
// won't waste anymore time, just use the current timestamp and make that copy.
//
// Could probably make this more efficient by checking if there are any files matching the copy
// pattern, and trying to find the highest number and then incrementing it by one rather than
// looping endlessly.
var i int
copySuffix := " copy"
for i = 0; i < 51; i++ {
if i > 0 {
copySuffix = " copy " + strconv.Itoa(i)
}
tryName := fmt.Sprintf("%s%s%s", name, copySuffix, extension)
tryLocation, err := fs.SafePath(path.Join(relative, tryName))
if err != nil {
return errors.WithStack(err)
}
// If the file exists, continue to the next loop, otherwise we're good to start a copy.
if _, err := os.Stat(tryLocation); err != nil && !os.IsNotExist(err) {
return errors.WithStack(err)
} else if os.IsNotExist(err) {
break
}
if i == 50 {
copySuffix = "." + time.Now().Format(time.RFC3339)
}
}
finalPath, err := fs.SafePath(path.Join(relative, fmt.Sprintf("%s%s%s", name, copySuffix, extension)))
if err != nil {
return errors.WithStack(err)
}
source, err := os.Open(cleaned)
if err != nil {
return errors.WithStack(err)
}
defer source.Close()
dest, err := os.Create(finalPath)
if err != nil {
return errors.WithStack(err)
}
defer dest.Close()
if _, err := io.Copy(dest, source); err != nil {
return errors.WithStack(err)
}
return nil
}
// Deletes a file or folder from the system. Prevents the user from accidentally
// (or maliciously) removing their root server data directory.
func (fs *Filesystem) Delete(p string) error {
cleaned, err := fs.SafePath(p)
if err != nil {
return errors.WithStack(err)
}
// Block any whoopsies.
if cleaned == fs.Path() {
return errors.New("cannot delete root server directory")
}
return os.RemoveAll(cleaned)
}
// Lists the contents of a given directory and returns stat information about each
// file and folder within it.
func (fs *Filesystem) ListDirectory(p string) ([]*Stat, error) {
cleaned, err := fs.SafePath(p)
if err != nil {
return nil, err
}
files, err := ioutil.ReadDir(cleaned)
if err != nil {
return nil, err
}
var wg sync.WaitGroup
// You must initialize the output of this directory as a non-nil value otherwise
// when it is marshaled into a JSON object you'll just get 'null' back, which will
// break the panel badly.
out := make([]*Stat, 0)
// Iterate over all of the files and directories returned and perform an async process
// to get the mime-type for them all.
for _, file := range files {
wg.Add(1)
go func(f os.FileInfo) {
defer wg.Done()
var m = "inode/directory"
if !f.IsDir() {
m, _, _ = mimetype.DetectFile(filepath.Join(cleaned, f.Name()))
}
out = append(out, &Stat{
Info: f,
Mimetype: m,
})
}(file)
}
wg.Wait()
// Sort the output alphabetically to begin with since we've run the output
// through an asynchronous process and the order is gonna be very random.
sort.SliceStable(out, func(i, j int) bool {
if out[i].Info.Name() == out[j].Info.Name() || out[i].Info.Name() > out[j].Info.Name() {
return true
}
return false
})
// Then, sort it so that directories are listed first in the output. Everything
// will continue to be alphabetized at this point.
sort.SliceStable(out, func(i, j int) bool {
return out[i].Info.IsDir()
})
return out, nil
}
// Ensures that the data directory for the server instance exists.
func (fs *Filesystem) EnsureDataDirectory() error {
if _, err := os.Stat(fs.Path()); err != nil && !os.IsNotExist(err) {
return errors.WithStack(err)
} else if err != nil {
// Create the server data directory because it does not currently exist
// on the system.
if err := os.MkdirAll(fs.Path(), 0700); err != nil {
return errors.WithStack(err)
}
}
return nil
}

View File

@@ -1,163 +0,0 @@
package filesystem
import (
"context"
"fmt"
"github.com/karrick/godirwalk"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/server/backup"
ignore "github.com/sabhiram/go-gitignore"
"os"
"path"
"path/filepath"
"strings"
"time"
)
// Given a directory, iterate through all of the files and folders within it and determine
// if they should be included in the output based on an array of ignored matches. This uses
// standard .gitignore formatting to make that determination.
//
// If no ignored files are passed through you'll get the entire directory listing.
func (fs *Filesystem) GetIncludedFiles(dir string, ignored []string) (*backup.IncludedFiles, error) {
cleaned, err := fs.SafePath(dir)
if err != nil {
return nil, err
}
i, err := ignore.CompileIgnoreLines(ignored...)
if err != nil {
return nil, err
}
// Walk through all of the files and directories on a server. This callback only returns
// files found, and will keep walking deeper and deeper into directories.
inc := new(backup.IncludedFiles)
err = godirwalk.Walk(cleaned, &godirwalk.Options{
Unsorted: true,
Callback: func(p string, e *godirwalk.Dirent) error {
sp := p
if e.IsSymlink() {
sp, err = fs.SafePath(p)
if err != nil {
if errors.Is(err, ErrBadPathResolution) {
return godirwalk.SkipThis
}
return err
}
}
// Only push files into the result array since archives can't create an empty directory within them.
if !e.IsDir() {
// Avoid unnecessary parsing if there are no ignored files, nothing will match anyways
// so no reason to call the function.
if len(ignored) == 0 || !i.MatchesPath(strings.TrimPrefix(sp, fs.Path()+"/")) {
inc.Push(sp)
}
}
// We can't just abort if the path is technically ignored. It is possible there is a nested
// file or folder that should not be excluded, so in this case we need to just keep going
// until we get to a final state.
return nil
},
})
return inc, errors.WithStack(err)
}
// Compresses all of the files matching the given paths in the specified directory. This function
// also supports passing nested paths to only compress certain files and folders when working in
// a larger directory. This effectively creates a local backup, but rather than ignoring specific
// files and folders, it takes an allow-list of files and folders.
//
// All paths are relative to the dir that is passed in as the first argument, and the compressed
// file will be placed at that location named `archive-{date}.tar.gz`.
func (fs *Filesystem) CompressFiles(dir string, paths []string) (os.FileInfo, error) {
cleanedRootDir, err := fs.SafePath(dir)
if err != nil {
return nil, err
}
// Take all of the paths passed in and merge them together with the root directory we've gotten.
for i, p := range paths {
paths[i] = filepath.Join(cleanedRootDir, p)
}
cleaned, err := fs.ParallelSafePath(paths)
if err != nil {
return nil, err
}
inc := new(backup.IncludedFiles)
// Iterate over all of the cleaned paths and merge them into a large object of final file
// paths to pass into the archiver. As directories are encountered this will drop into them
// and look for all of the files.
for _, p := range cleaned {
f, err := os.Stat(p)
if err != nil {
fs.error(err).WithField("path", p).Debug("failed to stat file or directory for compression")
continue
}
if !f.IsDir() {
inc.Push(p)
} else {
err := godirwalk.Walk(p, &godirwalk.Options{
Unsorted: true,
Callback: func(p string, e *godirwalk.Dirent) error {
sp := p
if e.IsSymlink() {
// Ensure that any symlinks are properly resolved to their final destination. If
// that destination is outside the server directory skip over this entire item, otherwise
// use the resolved location for the rest of this function.
sp, err = fs.SafePath(p)
if err != nil {
if errors.Is(err, ErrBadPathResolution) {
return godirwalk.SkipThis
}
return err
}
}
if !e.IsDir() {
inc.Push(sp)
}
return nil
},
})
if err != nil {
return nil, err
}
}
}
a := &backup.Archive{TrimPrefix: fs.Path(), Files: inc}
d := path.Join(cleanedRootDir, fmt.Sprintf("archive-%s.tar.gz", strings.ReplaceAll(time.Now().Format(time.RFC3339), ":", "")))
if err := a.Create(d, context.Background()); err != nil {
return nil, errors.WithStack(err)
}
f, err := os.Stat(d)
if err != nil {
_ = os.Remove(d)
return nil, err
}
if err := fs.hasSpaceFor(f.Size()); err != nil {
_ = os.Remove(d)
return nil, err
}
fs.addDisk(f.Size())
return f, nil
}

View File

@@ -1,110 +0,0 @@
package filesystem
import (
"archive/tar"
"archive/zip"
"compress/gzip"
"fmt"
"github.com/mholt/archiver/v3"
"github.com/pkg/errors"
"os"
"path/filepath"
"reflect"
"strings"
"sync/atomic"
)
// Look through a given archive and determine if decompressing it would put the server over
// its allocated disk space limit.
func (fs *Filesystem) SpaceAvailableForDecompression(dir string, file string) (bool, error) {
// Don't waste time trying to determine this if we know the server will have the space for
// it since there is no limit.
if fs.MaxDisk() <= 0 {
return true, nil
}
source, err := fs.SafePath(filepath.Join(dir, file))
if err != nil {
return false, err
}
// Get the cached size in a parallel process so that if it is not cached we are not
// waiting an unnecessary amount of time on this call.
dirSize, err := fs.DiskUsage(false)
var size int64
// Walk over the archive and figure out just how large the final output would be from unarchiving it.
err = archiver.Walk(source, func(f archiver.File) error {
if atomic.AddInt64(&size, f.Size())+dirSize > fs.MaxDisk() {
return ErrNotEnoughDiskSpace
}
return nil
})
if err != nil {
if strings.HasPrefix(err.Error(), "format ") {
return false, ErrUnknownArchiveFormat
}
return false, errors.WithStack(err)
}
return true, errors.WithStack(err)
}
// Decompress a file in a given directory by using the archiver tool to infer the file
// type and go from there. This will walk over all of the files within the given archive
// and ensure that there is not a zip-slip attack being attempted by validating that the
// final path is within the server data directory.
func (fs *Filesystem) DecompressFile(dir string, file string) error {
source, err := fs.SafePath(filepath.Join(dir, file))
if err != nil {
return errors.WithStack(err)
}
// Make sure the file exists basically.
if _, err := os.Stat(source); err != nil {
return errors.WithStack(err)
}
// Walk over all of the files spinning up an additional go-routine for each file we've encountered
// and then extract that file from the archive and write it to the disk. If any part of this process
// encounters an error the entire process will be stopped.
err = archiver.Walk(source, func(f archiver.File) error {
// Don't waste time with directories, we don't need to create them if they have no contents, and
// we will ensure the directory exists when opening the file for writing anyways.
if f.IsDir() {
return nil
}
var name string
switch s := f.Sys().(type) {
case *tar.Header:
name = s.Name
case *gzip.Header:
name = s.Name
case *zip.FileHeader:
name = s.Name
default:
return errors.New(fmt.Sprintf("could not parse underlying data source with type %s", reflect.TypeOf(s).String()))
}
p, err := fs.SafePath(filepath.Join(dir, name))
if err != nil {
return errors.Wrap(err, "failed to generate a safe path to server file")
}
return errors.Wrap(fs.Writefile(p, f), "could not extract file from archive")
})
if err != nil {
if strings.HasPrefix(err.Error(), "format ") {
return errors.WithStack(ErrUnknownArchiveFormat)
}
return errors.WithStack(err)
}
return nil
}

View File

@@ -1,232 +0,0 @@
package filesystem
import (
"github.com/apex/log"
"github.com/karrick/godirwalk"
"github.com/pkg/errors"
"sync"
"sync/atomic"
"syscall"
"time"
)
type SpaceCheckingOpts struct {
AllowStaleResponse bool
}
type usageLookupTime struct {
sync.RWMutex
value time.Time
}
// Update the last time that a disk space lookup was performed.
func (ult *usageLookupTime) Set(t time.Time) {
ult.Lock()
ult.value = t
ult.Unlock()
}
// Get the last time that we performed a disk space usage lookup.
func (ult *usageLookupTime) Get() time.Time {
ult.RLock()
defer ult.RUnlock()
return ult.value
}
// Returns the maximum amount of disk space that this Filesystem instance is allowed to use.
func (fs *Filesystem) MaxDisk() int64 {
fs.mu.RLock()
defer fs.mu.RUnlock()
return fs.diskLimit
}
// Sets the disk space limit for this Filesystem instance.
func (fs *Filesystem) SetDiskLimit(i int64) {
fs.mu.Lock()
fs.diskLimit = i
fs.mu.Unlock()
}
// Determines if the directory a file is trying to be added to has enough space available
// for the file to be written to.
//
// Because determining the amount of space being used by a server is a taxing operation we
// will load it all up into a cache and pull from that as long as the key is not expired.
//
// This operation will potentially block unless allowStaleValue is set to true. See the
// documentation on DiskUsage for how this affects the call.
func (fs *Filesystem) HasSpaceAvailable(allowStaleValue bool) bool {
size, err := fs.DiskUsage(allowStaleValue)
if err != nil {
log.WithField("root", fs.root).WithField("error", err).Warn("failed to determine root fs directory size")
}
// If space is -1 or 0 just return true, means they're allowed unlimited.
//
// Technically we could skip disk space calculation because we don't need to check if the
// server exceeds it's limit but because this method caches the disk usage it would be best
// to calculate the disk usage and always return true.
if fs.MaxDisk() == 0 {
return true
}
return size <= fs.MaxDisk()
}
// Returns the cached value for the amount of disk space used by the filesystem. Do not rely on this
// function for critical logical checks. It should only be used in areas where the actual disk usage
// does not need to be perfect, e.g. API responses for server resource usage.
func (fs *Filesystem) CachedUsage() int64 {
fs.mu.RLock()
defer fs.mu.RUnlock()
return fs.diskUsed
}
// Internal helper function to allow other parts of the codebase to check the total used disk space
// as needed without overly taxing the system. This will prioritize the value from the cache to avoid
// excessive IO usage. We will only walk the filesystem and determine the size of the directory if there
// is no longer a cached value.
//
// If "allowStaleValue" is set to true, a stale value MAY be returned to the caller if there is an
// expired cache value AND there is currently another lookup in progress. If there is no cached value but
// no other lookup is in progress, a fresh disk space response will be returned to the caller.
//
// This is primarily to avoid a bunch of I/O operations from piling up on the server, especially on servers
// with a large amount of files.
func (fs *Filesystem) DiskUsage(allowStaleValue bool) (int64, error) {
if !fs.lastLookupTime.Get().After(time.Now().Add(time.Second * fs.diskCheckInterval * -1)) {
// If we are now allowing a stale response go ahead and perform the lookup and return the fresh
// value. This is a blocking operation to the calling process.
if !allowStaleValue {
return fs.updateCachedDiskUsage()
} else if !fs.lookupInProgress.Get() {
// Otherwise, if we allow a stale value and there isn't a valid item in the cache and we aren't
// currently performing a lookup, just do the disk usage calculation in the background.
go func(fs *Filesystem) {
if _, err := fs.updateCachedDiskUsage(); err != nil {
log.WithField("root", fs.root).WithField("error", err).Warn("failed to update fs disk usage from within routine")
}
}(fs)
}
}
// Return the currently cached value back to the calling function.
return atomic.LoadInt64(&fs.diskUsed), nil
}
// Updates the currently used disk space for a server.
func (fs *Filesystem) updateCachedDiskUsage() (int64, error) {
// Obtain an exclusive lock on this process so that we don't unintentionally run it at the same
// time as another running process. Once the lock is available it'll read from the cache for the
// second call rather than hitting the disk in parallel.
fs.mu.Lock()
defer fs.mu.Unlock()
// Signal that we're currently updating the disk size so that other calls to the disk checking
// functions can determine if they should queue up additional calls to this function. Ensure that
// we always set this back to "false" when this process is done executing.
fs.lookupInProgress.Set(true)
defer fs.lookupInProgress.Set(false)
// If there is no size its either because there is no data (in which case running this function
// will have effectively no impact), or there is nothing in the cache, in which case we need to
// grab the size of their data directory. This is a taxing operation, so we want to store it in
// the cache once we've gotten it.
size, err := fs.DirectorySize("/")
// Always cache the size, even if there is an error. We want to always return that value
// so that we don't cause an endless loop of determining the disk size if there is a temporary
// error encountered.
fs.lastLookupTime.Set(time.Now())
atomic.StoreInt64(&fs.diskUsed, size)
return size, err
}
// Determines the directory size of a given location by running parallel tasks to iterate
// through all of the folders. Returns the size in bytes. This can be a fairly taxing operation
// on locations with tons of files, so it is recommended that you cache the output.
func (fs *Filesystem) DirectorySize(dir string) (int64, error) {
d, err := fs.SafePath(dir)
if err != nil {
return 0, errors.WithStack(err)
}
var size int64
var st syscall.Stat_t
err = godirwalk.Walk(d, &godirwalk.Options{
Unsorted: true,
Callback: func(p string, e *godirwalk.Dirent) error {
// If this is a symlink then resolve the final destination of it before trying to continue walking
// over its contents. If it resolves outside the server data directory just skip everything else for
// it. Otherwise, allow it to continue.
if e.IsSymlink() {
if _, err := fs.SafePath(p); err != nil {
if errors.Is(err, ErrBadPathResolution) {
return godirwalk.SkipThis
}
return err
}
}
if !e.IsDir() {
syscall.Lstat(p, &st)
atomic.AddInt64(&size, st.Size)
}
return nil
},
})
return size, errors.WithStack(err)
}
// Helper function to determine if a server has space available for a file of a given size.
// If space is available, no error will be returned, otherwise an ErrNotEnoughSpace error
// will be raised.
func (fs *Filesystem) hasSpaceFor(size int64) error {
if fs.MaxDisk() == 0 {
return nil
}
s, err := fs.DiskUsage(true)
if err != nil {
return err
}
if (s + size) > fs.MaxDisk() {
return ErrNotEnoughDiskSpace
}
return nil
}
// Updates the disk usage for the Filesystem instance.
func (fs *Filesystem) addDisk(i int64) int64 {
size := atomic.LoadInt64(&fs.diskUsed)
// Sorry go gods. This is ugly but the best approach I can come up with for right
// now without completely re-evaluating the logic we use for determining disk space.
//
// Normally I would just be using the atomic load right below, but I'm not sure about
// the scenarios where it is 0 because nothing has run that would trigger a disk size
// calculation?
//
// Perhaps that isn't even a concern for the sake of this?
if !fs.isTest {
size, _ = fs.DiskUsage(true)
}
// If we're dropping below 0 somehow just cap it to 0.
if (size + i) < 0 {
return atomic.SwapInt64(&fs.diskUsed, 0)
}
return atomic.AddInt64(&fs.diskUsed, i)
}

View File

@@ -1,35 +0,0 @@
package filesystem
import (
"github.com/apex/log"
"github.com/pkg/errors"
"os"
"path/filepath"
)
var ErrIsDirectory = errors.New("filesystem: is a directory")
var ErrNotEnoughDiskSpace = errors.New("filesystem: not enough disk space")
var ErrBadPathResolution = errors.New("filesystem: invalid path resolution")
var ErrUnknownArchiveFormat = errors.New("filesystem: unknown archive format")
// Generates an error logger instance with some basic information.
func (fs *Filesystem) error(err error) *log.Entry {
return log.WithField("subsystem", "filesystem").WithField("root", fs.root).WithField("error", err)
}
// Handle errors encountered when walking through directories.
//
// If there is a path resolution error just skip the item entirely. Only return this for a
// directory, otherwise return nil. Returning this error for a file will stop the walking
// for the remainder of the directory. This is assuming an os.FileInfo struct was even returned.
func (fs *Filesystem) handleWalkerError(err error, f os.FileInfo) error {
if !errors.Is(err, ErrBadPathResolution) {
return err
}
if f != nil && f.IsDir() {
return filepath.SkipDir
}
return nil
}

View File

@@ -1,461 +0,0 @@
package filesystem
import (
"bufio"
"github.com/gabriel-vasile/mimetype"
"github.com/karrick/godirwalk"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/system"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"time"
)
type Filesystem struct {
mu sync.RWMutex
lastLookupTime *usageLookupTime
lookupInProgress system.AtomicBool
diskUsed int64
diskCheckInterval time.Duration
// The maximum amount of disk space (in bytes) that this Filesystem instance can use.
diskLimit int64
// The root data directory path for this Filesystem instance.
root string
isTest bool
}
// Creates a new Filesystem instance for a given server.
func New(root string, size int64) *Filesystem {
return &Filesystem{
root: root,
diskLimit: size,
diskCheckInterval: time.Duration(config.Get().System.DiskCheckInterval),
lastLookupTime: &usageLookupTime{},
}
}
// Returns the root path for the Filesystem instance.
func (fs *Filesystem) Path() string {
return fs.root
}
// Reads a file on the system and returns it as a byte representation in a file
// reader. This is not the most memory efficient usage since it will be reading the
// entirety of the file into memory.
func (fs *Filesystem) Readfile(p string, w io.Writer) error {
cleaned, err := fs.SafePath(p)
if err != nil {
return err
}
if st, err := os.Stat(cleaned); err != nil {
return err
} else if st.IsDir() {
return ErrIsDirectory
}
f, err := os.Open(cleaned)
if err != nil {
return err
}
defer f.Close()
_, err = bufio.NewReader(f).WriteTo(w)
return err
}
// Writes a file to the system. If the file does not already exist one will be created.
func (fs *Filesystem) Writefile(p string, r io.Reader) error {
cleaned, err := fs.SafePath(p)
if err != nil {
return errors.WithStack(err)
}
var currentSize int64
// If the file does not exist on the system already go ahead and create the pathway
// to it and an empty file. We'll then write to it later on after this completes.
if stat, err := os.Stat(cleaned); err != nil {
if !os.IsNotExist(err) {
return errors.WithStack(err)
}
if err := os.MkdirAll(filepath.Dir(cleaned), 0755); err != nil {
return errors.WithStack(err)
}
if err := fs.Chown(filepath.Dir(cleaned)); err != nil {
return errors.WithStack(err)
}
} else {
if stat.IsDir() {
return ErrIsDirectory
}
currentSize = stat.Size()
}
br := bufio.NewReader(r)
// Check that the new size we're writing to the disk can fit. If there is currently a file
// we'll subtract that current file size from the size of the buffer to determine the amount
// of new data we're writing (or amount we're removing if smaller).
if err := fs.hasSpaceFor(int64(br.Size()) - currentSize); err != nil {
return err
}
o := &fileOpener{}
// This will either create the file if it does not already exist, or open and
// truncate the existing file.
file, err := o.open(cleaned, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return errors.WithStack(err)
}
defer file.Close()
buf := make([]byte, 1024*4)
sz, err := io.CopyBuffer(file, r, buf)
// Adjust the disk usage to account for the old size and the new size of the file.
fs.addDisk(sz - currentSize)
// Finally, chown the file to ensure the permissions don't end up out-of-whack
// if we had just created it.
return fs.Chown(cleaned)
}
// Creates a new directory (name) at a specified path (p) for the server.
func (fs *Filesystem) CreateDirectory(name string, p string) error {
cleaned, err := fs.SafePath(path.Join(p, name))
if err != nil {
return errors.WithStack(err)
}
return os.MkdirAll(cleaned, 0755)
}
// Moves (or renames) a file or directory.
func (fs *Filesystem) Rename(from string, to string) error {
cleanedFrom, err := fs.SafePath(from)
if err != nil {
return errors.WithStack(err)
}
cleanedTo, err := fs.SafePath(to)
if err != nil {
return errors.WithStack(err)
}
// If the target file or directory already exists the rename function will fail, so just
// bail out now.
if _, err := os.Stat(cleanedTo); err == nil {
return os.ErrExist
}
if cleanedTo == fs.Path() {
return errors.New("attempting to rename into an invalid directory space")
}
d := strings.TrimSuffix(cleanedTo, path.Base(cleanedTo))
// Ensure that the directory we're moving into exists correctly on the system. Only do this if
// we're not at the root directory level.
if d != fs.Path() {
if mkerr := os.MkdirAll(d, 0755); mkerr != nil {
return errors.Wrap(mkerr, "failed to create directory structure for file rename")
}
}
return os.Rename(cleanedFrom, cleanedTo)
}
// Recursively iterates over a file or directory and sets the permissions on all of the
// underlying files. Iterate over all of the files and directories. If it is a file just
// go ahead and perform the chown operation. Otherwise dig deeper into the directory until
// we've run out of directories to dig into.
func (fs *Filesystem) Chown(path string) error {
cleaned, err := fs.SafePath(path)
if err != nil {
return errors.WithStack(err)
}
if fs.isTest {
return nil
}
uid := config.Get().System.User.Uid
gid := config.Get().System.User.Gid
// Start by just chowning the initial path that we received.
if err := os.Chown(cleaned, uid, gid); err != nil {
return errors.WithStack(err)
}
// If this is not a directory we can now return from the function, there is nothing
// left that we need to do.
if st, _ := os.Stat(cleaned); !st.IsDir() {
return nil
}
// If this was a directory, begin walking over its contents recursively and ensure that all
// of the subfiles and directories get their permissions updated as well.
return godirwalk.Walk(cleaned, &godirwalk.Options{
Unsorted: true,
Callback: func(p string, e *godirwalk.Dirent) error {
// Do not attempt to chmod a symlink. Go's os.Chown function will affect the symlink
// so if it points to a location outside the data directory the user would be able to
// (un)intentionally modify that files permissions.
if e.IsSymlink() {
if e.IsDir() {
return godirwalk.SkipThis
}
return nil
}
return os.Chown(p, uid, gid)
},
})
}
// Begin looping up to 50 times to try and create a unique copy file name. This will take
// an input of "file.txt" and generate "file copy.txt". If that name is already taken, it will
// then try to write "file copy 2.txt" and so on, until reaching 50 loops. At that point we
// won't waste anymore time, just use the current timestamp and make that copy.
//
// Could probably make this more efficient by checking if there are any files matching the copy
// pattern, and trying to find the highest number and then incrementing it by one rather than
// looping endlessly.
func (fs *Filesystem) findCopySuffix(dir string, name string, extension string) (string, error) {
var i int
var suffix = " copy"
for i = 0; i < 51; i++ {
if i > 0 {
suffix = " copy " + strconv.Itoa(i)
}
n := name + suffix + extension
// If we stat the file and it does not exist that means we're good to create the copy. If it
// does exist, we'll just continue to the next loop and try again.
if _, err := fs.Stat(path.Join(dir, n)); err != nil {
if !os.IsNotExist(err) {
return "", err
}
break
}
if i == 50 {
suffix = "copy." + time.Now().Format(time.RFC3339)
}
}
return name + suffix + extension, nil
}
// Copies a given file to the same location and appends a suffix to the file to indicate that
// it has been copied.
func (fs *Filesystem) Copy(p string) error {
cleaned, err := fs.SafePath(p)
if err != nil {
return errors.WithStack(err)
}
s, err := os.Stat(cleaned)
if err != nil {
return errors.WithStack(err)
} else if s.IsDir() || !s.Mode().IsRegular() {
// If this is a directory or not a regular file, just throw a not-exist error
// since anything calling this function should understand what that means.
return os.ErrNotExist
}
// Check that copying this file wouldn't put the server over its limit.
if err := fs.hasSpaceFor(s.Size()); err != nil {
return err
}
base := filepath.Base(cleaned)
relative := strings.TrimSuffix(strings.TrimPrefix(cleaned, fs.Path()), base)
extension := filepath.Ext(base)
name := strings.TrimSuffix(base, extension)
// Ensure that ".tar" is also counted as apart of the file extension.
// There might be a better way to handle this for other double file extensions,
// but this is a good workaround for now.
if strings.HasSuffix(name, ".tar") {
extension = ".tar" + extension
name = strings.TrimSuffix(name, ".tar")
}
source, err := os.Open(cleaned)
if err != nil {
return errors.WithStack(err)
}
defer source.Close()
n, err := fs.findCopySuffix(relative, name, extension)
return fs.Writefile(path.Join(relative, n), source)
}
// Deletes a file or folder from the system. Prevents the user from accidentally
// (or maliciously) removing their root server data directory.
func (fs *Filesystem) Delete(p string) error {
wg := sync.WaitGroup{}
// This is one of the few (only?) places in the codebase where we're explicitly not using
// the SafePath functionality when working with user provided input. If we did, you would
// not be able to delete a file that is a symlink pointing to a location outside of the data
// directory.
//
// We also want to avoid resolving a symlink that points _within_ the data directory and thus
// deleting the actual source file for the symlink rather than the symlink itself. For these
// purposes just resolve the actual file path using filepath.Join() and confirm that the path
// exists within the data directory.
resolved := fs.unsafeFilePath(p)
if !fs.unsafeIsInDataDirectory(resolved) {
return ErrBadPathResolution
}
// Block any whoopsies.
if resolved == fs.Path() {
return errors.New("cannot delete root server directory")
}
if st, err := os.Lstat(resolved); err != nil {
if !os.IsNotExist(err) {
fs.error(err).Warn("error while attempting to stat file before deletion")
}
} else {
if !st.IsDir() {
fs.addDisk(-st.Size())
} else {
wg.Add(1)
go func(wg *sync.WaitGroup, st os.FileInfo, resolved string) {
defer wg.Done()
if s, err := fs.DirectorySize(resolved); err == nil {
fs.addDisk(-s)
}
}(&wg, st, resolved)
}
}
wg.Wait()
return os.RemoveAll(resolved)
}
type fileOpener struct {
busy uint
}
// Attempts to open a given file up to "attempts" number of times, using a backoff. If the file
// cannot be opened because of a "text file busy" error, we will attempt until the number of attempts
// has been exhaused, at which point we will abort with an error.
func (fo *fileOpener) open(path string, flags int, perm os.FileMode) (*os.File, error) {
for {
f, err := os.OpenFile(path, flags, perm)
// If there is an error because the text file is busy, go ahead and sleep for a few
// hundred milliseconds and then try again up to three times before just returning the
// error back to the caller.
//
// Based on code from: https://github.com/golang/go/issues/22220#issuecomment-336458122
if err != nil && fo.busy < 3 && strings.Contains(err.Error(), "text file busy") {
time.Sleep(100 * time.Millisecond << fo.busy)
fo.busy++
continue
}
return f, err
}
}
// Lists the contents of a given directory and returns stat information about each
// file and folder within it.
func (fs *Filesystem) ListDirectory(p string) ([]*Stat, error) {
cleaned, err := fs.SafePath(p)
if err != nil {
return nil, err
}
files, err := ioutil.ReadDir(cleaned)
if err != nil {
return nil, err
}
var wg sync.WaitGroup
// You must initialize the output of this directory as a non-nil value otherwise
// when it is marshaled into a JSON object you'll just get 'null' back, which will
// break the panel badly.
out := make([]*Stat, len(files))
// Iterate over all of the files and directories returned and perform an async process
// to get the mime-type for them all.
for i, file := range files {
wg.Add(1)
go func(idx int, f os.FileInfo) {
defer wg.Done()
var m *mimetype.MIME
var d = "inode/directory"
if !f.IsDir() {
cleanedp := filepath.Join(cleaned, f.Name())
if f.Mode()&os.ModeSymlink != 0 {
cleanedp, _ = fs.SafePath(filepath.Join(cleaned, f.Name()))
}
if cleanedp != "" {
m, _ = mimetype.DetectFile(filepath.Join(cleaned, f.Name()))
} else {
// Just pass this for an unknown type because the file could not safely be resolved within
// the server data path.
d = "application/octet-stream"
}
}
st := &Stat{
Info: f,
Mimetype: d,
}
if m != nil {
st.Mimetype = m.String()
}
out[idx] = st
}(i, file)
}
wg.Wait()
// Sort the output alphabetically to begin with since we've run the output
// through an asynchronous process and the order is gonna be very random.
sort.SliceStable(out, func(i, j int) bool {
if out[i].Info.Name() == out[j].Info.Name() || out[i].Info.Name() > out[j].Info.Name() {
return true
}
return false
})
// Then, sort it so that directories are listed first in the output. Everything
// will continue to be alphabetized at this point.
sort.SliceStable(out, func(i, j int) bool {
return out[i].Info.IsDir()
})
return out, nil
}

View File

@@ -1,788 +0,0 @@
package filesystem
import (
"bytes"
"errors"
. "github.com/franela/goblin"
"github.com/pterodactyl/wings/config"
"io/ioutil"
"math/rand"
"os"
"path/filepath"
"sync/atomic"
"testing"
"unicode/utf8"
)
func NewFs() (*Filesystem, *rootFs) {
config.Set(&config.Configuration{
AuthenticationToken: "abc",
System: config.SystemConfiguration{
RootDirectory: "/server",
DiskCheckInterval: 150,
},
})
tmpDir, err := ioutil.TempDir(os.TempDir(), "pterodactyl")
if err != nil {
panic(err)
}
// defer os.RemoveAll(tmpDir)
rfs := rootFs{root: tmpDir}
rfs.reset()
fs := New(filepath.Join(tmpDir, "/server"), 0)
fs.isTest = true
return fs, &rfs
}
type rootFs struct {
root string
}
func (rfs *rootFs) CreateServerFile(p string, c string) error {
f, err := os.Create(filepath.Join(rfs.root, "/server", p))
if err == nil {
f.Write([]byte(c))
f.Close()
}
return err
}
func (rfs *rootFs) StatServerFile(p string) (os.FileInfo, error) {
return os.Stat(filepath.Join(rfs.root, "/server", p))
}
func (rfs *rootFs) reset() {
if err := os.RemoveAll(filepath.Join(rfs.root, "/server")); err != nil {
if !os.IsNotExist(err) {
panic(err)
}
}
if err := os.Mkdir(filepath.Join(rfs.root, "/server"), 0755); err != nil {
panic(err)
}
}
func TestFilesystem_Path(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()
g.Describe("Path", func() {
g.It("returns the root path for the instance", func() {
g.Assert(fs.Path()).Equal(filepath.Join(rfs.root, "/server"))
})
})
}
func TestFilesystem_SafePath(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()
prefix := filepath.Join(rfs.root, "/server")
g.Describe("SafePath", func() {
g.It("returns a cleaned path to a given file", func() {
p, err := fs.SafePath("test.txt")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/test.txt")
p, err = fs.SafePath("/test.txt")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/test.txt")
p, err = fs.SafePath("./test.txt")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/test.txt")
p, err = fs.SafePath("/foo/../test.txt")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/test.txt")
p, err = fs.SafePath("/foo/bar")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/foo/bar")
})
g.It("handles root directory access", func() {
p, err := fs.SafePath("/")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix)
p, err = fs.SafePath("")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix)
})
g.It("removes trailing slashes from paths", func() {
p, err := fs.SafePath("/foo/bar/")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/foo/bar")
})
g.It("handles deeply nested directories that do not exist", func() {
p, err := fs.SafePath("/foo/bar/baz/quaz/../../ducks/testing.txt")
g.Assert(err).IsNil()
g.Assert(p).Equal(prefix + "/foo/bar/ducks/testing.txt")
})
g.It("blocks access to files outside the root directory", func() {
p, err := fs.SafePath("../test.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
g.Assert(p).Equal("")
p, err = fs.SafePath("/../test.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
g.Assert(p).Equal("")
p, err = fs.SafePath("./foo/../../test.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
g.Assert(p).Equal("")
p, err = fs.SafePath("..")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
g.Assert(p).Equal("")
})
})
}
// We test against accessing files outside the root directory in the tests, however it
// is still possible for someone to mess up and not properly use this safe path call. In
// order to truly confirm this, we'll try to pass in a symlinked malicious file to all of
// the calls and ensure they all fail with the same reason.
func TestFilesystem_Blocks_Symlinks(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()
if err := rfs.CreateServerFile("/../malicious.txt", "external content"); err != nil {
panic(err)
}
if err := os.Mkdir(filepath.Join(rfs.root, "/malicious_dir"), 0777); err != nil {
panic(err)
}
if err := os.Symlink(filepath.Join(rfs.root, "malicious.txt"), filepath.Join(rfs.root, "/server/symlinked.txt")); err != nil {
panic(err)
}
if err := os.Symlink(filepath.Join(rfs.root, "/malicious_dir"), filepath.Join(rfs.root, "/server/external_dir")); err != nil {
panic(err)
}
g.Describe("Readfile", func() {
g.It("cannot read a file symlinked outside the root", func() {
b := bytes.Buffer{}
err := fs.Readfile("symlinked.txt", &b)
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
})
g.Describe("Writefile", func() {
g.It("cannot write to a file symlinked outside the root", func() {
r := bytes.NewReader([]byte("testing"))
err := fs.Writefile("symlinked.txt", r)
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
g.It("cannot write a file to a directory symlinked outside the root", func() {
r := bytes.NewReader([]byte("testing"))
err := fs.Writefile("external_dir/foo.txt", r)
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
})
g.Describe("CreateDirectory", func() {
g.It("cannot create a directory outside the root", func() {
err := fs.CreateDirectory("my_dir", "external_dir")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
g.It("cannot create a nested directory outside the root", func() {
err := fs.CreateDirectory("my/nested/dir", "external_dir/foo/bar")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
g.It("cannot create a nested directory outside the root", func() {
err := fs.CreateDirectory("my/nested/dir", "external_dir/server")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
})
g.Describe("Rename", func() {
g.It("cannot rename a file symlinked outside the directory root", func() {
err := fs.Rename("symlinked.txt", "foo.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
g.It("cannot rename a symlinked directory outside the root", func() {
err := fs.Rename("external_dir", "foo")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
g.It("cannot rename a file to a location outside the directory root", func() {
rfs.CreateServerFile("my_file.txt", "internal content")
err := fs.Rename("my_file.txt", "external_dir/my_file.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
})
g.Describe("Chown", func() {
g.It("cannot chown a file symlinked outside the directory root", func() {
err := fs.Chown("symlinked.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
g.It("cannot chown a directory symlinked outside the directory root", func() {
err := fs.Chown("external_dir")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
})
g.Describe("Copy", func() {
g.It("cannot copy a file symlinked outside the directory root", func() {
err := fs.Copy("symlinked.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
})
g.Describe("Delete", func() {
g.It("deletes the symlinked file but leaves the source", func() {
err := fs.Delete("symlinked.txt")
g.Assert(err).IsNil()
_, err = os.Stat(filepath.Join(rfs.root, "malicious.txt"))
g.Assert(err).IsNil()
_, err = rfs.StatServerFile("symlinked.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrNotExist)).IsTrue()
})
})
rfs.reset()
}
func TestFilesystem_Readfile(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()
g.Describe("Readfile", func() {
buf := &bytes.Buffer{}
g.It("opens a file if it exists on the system", func() {
err := rfs.CreateServerFile("test.txt", "testing")
g.Assert(err).IsNil()
err = fs.Readfile("test.txt", buf)
g.Assert(err).IsNil()
g.Assert(buf.String()).Equal("testing")
})
g.It("returns an error if the file does not exist", func() {
err := fs.Readfile("test.txt", buf)
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrNotExist)).IsTrue()
})
g.It("returns an error if the \"file\" is a directory", func() {
err := os.Mkdir(filepath.Join(rfs.root, "/server/test.txt"), 0755)
g.Assert(err).IsNil()
err = fs.Readfile("test.txt", buf)
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrIsDirectory)).IsTrue()
})
g.It("cannot open a file outside the root directory", func() {
err := rfs.CreateServerFile("/../test.txt", "testing")
g.Assert(err).IsNil()
err = fs.Readfile("/../test.txt", buf)
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
g.AfterEach(func() {
buf.Truncate(0)
atomic.StoreInt64(&fs.diskUsed, 0)
rfs.reset()
})
})
}
func TestFilesystem_Writefile(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()
g.Describe("Open and WriteFile", func() {
buf := &bytes.Buffer{}
// Test that a file can be written to the disk and that the disk space used as a result
// is updated correctly in the end.
g.It("can create a new file", func() {
r := bytes.NewReader([]byte("test file content"))
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(0))
err := fs.Writefile("test.txt", r)
g.Assert(err).IsNil()
err = fs.Readfile("test.txt", buf)
g.Assert(err).IsNil()
g.Assert(buf.String()).Equal("test file content")
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(r.Size())
})
g.It("can create a new file inside a nested directory with leading slash", func() {
r := bytes.NewReader([]byte("test file content"))
err := fs.Writefile("/some/nested/test.txt", r)
g.Assert(err).IsNil()
err = fs.Readfile("/some/nested/test.txt", buf)
g.Assert(err).IsNil()
g.Assert(buf.String()).Equal("test file content")
})
g.It("can create a new file inside a nested directory without a trailing slash", func() {
r := bytes.NewReader([]byte("test file content"))
err := fs.Writefile("some/../foo/bar/test.txt", r)
g.Assert(err).IsNil()
err = fs.Readfile("foo/bar/test.txt", buf)
g.Assert(err).IsNil()
g.Assert(buf.String()).Equal("test file content")
})
g.It("cannot create a file outside the root directory", func() {
r := bytes.NewReader([]byte("test file content"))
err := fs.Writefile("/some/../foo/../../test.txt", r)
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
g.It("cannot write a file that exceeds the disk limits", func() {
atomic.StoreInt64(&fs.diskLimit, 1024)
b := make([]byte, 1025)
_, err := rand.Read(b)
g.Assert(err).IsNil()
g.Assert(len(b)).Equal(1025)
r := bytes.NewReader(b)
err = fs.Writefile("test.txt", r)
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrNotEnoughDiskSpace)).IsTrue()
})
/*g.It("updates the total space used when a file is appended to", func() {
atomic.StoreInt64(&fs.diskUsed, 100)
b := make([]byte, 100)
_, _ = rand.Read(b)
r := bytes.NewReader(b)
err := fs.Writefile("test.txt", r)
g.Assert(err).IsNil()
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(200))
// If we write less data than already exists, we should expect the total
// disk used to be decremented.
b = make([]byte, 50)
_, _ = rand.Read(b)
r = bytes.NewReader(b)
err = fs.Writefile("test.txt", r)
g.Assert(err).IsNil()
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(150))
})*/
g.It("truncates the file when writing new contents", func() {
r := bytes.NewReader([]byte("original data"))
err := fs.Writefile("test.txt", r)
g.Assert(err).IsNil()
r = bytes.NewReader([]byte("new data"))
err = fs.Writefile("test.txt", r)
g.Assert(err).IsNil()
err = fs.Readfile("test.txt", buf)
g.Assert(err).IsNil()
g.Assert(buf.String()).Equal("new data")
})
g.AfterEach(func() {
buf.Truncate(0)
rfs.reset()
atomic.StoreInt64(&fs.diskUsed, 0)
atomic.StoreInt64(&fs.diskLimit, 0)
})
})
}
func TestFilesystem_CreateDirectory(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()
g.Describe("CreateDirectory", func() {
g.It("should create missing directories automatically", func() {
err := fs.CreateDirectory("test", "foo/bar/baz")
g.Assert(err).IsNil()
st, err := rfs.StatServerFile("foo/bar/baz/test")
g.Assert(err).IsNil()
g.Assert(st.IsDir()).IsTrue()
g.Assert(st.Name()).Equal("test")
})
g.It("should work with leading and trailing slashes", func() {
err := fs.CreateDirectory("test", "/foozie/barzie/bazzy/")
g.Assert(err).IsNil()
st, err := rfs.StatServerFile("foozie/barzie/bazzy/test")
g.Assert(err).IsNil()
g.Assert(st.IsDir()).IsTrue()
g.Assert(st.Name()).Equal("test")
})
g.It("should not allow the creation of directories outside the root", func() {
err := fs.CreateDirectory("test", "e/../../something")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
g.It("should not increment the disk usage", func() {
err := fs.CreateDirectory("test", "/")
g.Assert(err).IsNil()
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(0))
})
g.AfterEach(func() {
rfs.reset()
})
})
}
func TestFilesystem_Rename(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()
g.Describe("Rename", func() {
g.BeforeEach(func() {
if err := rfs.CreateServerFile("source.txt", "text content"); err != nil {
panic(err)
}
})
g.It("returns an error if the target already exists", func() {
err := rfs.CreateServerFile("target.txt", "taget content")
g.Assert(err).IsNil()
err = fs.Rename("source.txt", "target.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrExist)).IsTrue()
})
g.It("returns an error if the final destination is the root directory", func() {
err := fs.Rename("source.txt", "/")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrExist)).IsTrue()
})
g.It("returns an error if the source destination is the root directory", func() {
err := fs.Rename("source.txt", "/")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrExist)).IsTrue()
})
g.It("does not allow renaming to a location outside the root", func() {
err := fs.Rename("source.txt", "../target.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
g.It("does not allow renaming from a location outside the root", func() {
err := rfs.CreateServerFile("/../ext-source.txt", "taget content")
err = fs.Rename("/../ext-source.txt", "target.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
g.It("allows a file to be renamed", func() {
err := fs.Rename("source.txt", "target.txt")
g.Assert(err).IsNil()
_, err = rfs.StatServerFile("source.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrNotExist)).IsTrue()
st, err := rfs.StatServerFile("target.txt")
g.Assert(err).IsNil()
g.Assert(st.Name()).Equal("target.txt")
g.Assert(st.Size()).IsNotZero()
})
g.It("allows a folder to be renamed", func() {
err := os.Mkdir(filepath.Join(rfs.root, "/server/source_dir"), 0755)
g.Assert(err).IsNil()
err = fs.Rename("source_dir", "target_dir")
g.Assert(err).IsNil()
_, err = rfs.StatServerFile("source_dir")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrNotExist)).IsTrue()
st, err := rfs.StatServerFile("target_dir")
g.Assert(err).IsNil()
g.Assert(st.IsDir()).IsTrue()
})
g.It("returns an error if the source does not exist", func() {
err := fs.Rename("missing.txt", "target.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrNotExist)).IsTrue()
})
g.It("creates directories if they are missing", func() {
err := fs.Rename("source.txt", "nested/folder/target.txt")
g.Assert(err).IsNil()
st, err := rfs.StatServerFile("nested/folder/target.txt")
g.Assert(err).IsNil()
g.Assert(st.Name()).Equal("target.txt")
})
g.AfterEach(func() {
rfs.reset()
})
})
}
func TestFilesystem_Copy(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()
g.Describe("Copy", func() {
g.BeforeEach(func() {
if err := rfs.CreateServerFile("source.txt", "text content"); err != nil {
panic(err)
}
atomic.StoreInt64(&fs.diskUsed, int64(utf8.RuneCountInString("test content")))
})
g.It("should return an error if the source does not exist", func() {
err := fs.Copy("foo.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrNotExist)).IsTrue()
})
g.It("should return an error if the source is outside the root", func() {
err := rfs.CreateServerFile("/../ext-source.txt", "text content")
err = fs.Copy("../ext-source.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
g.It("should return an error if the source directory is outside the root", func() {
err := os.MkdirAll(filepath.Join(rfs.root, "/nested/in/dir"), 0755)
g.Assert(err).IsNil()
err = rfs.CreateServerFile("/../nested/in/dir/ext-source.txt", "external content")
g.Assert(err).IsNil()
err = fs.Copy("../nested/in/dir/ext-source.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
err = fs.Copy("nested/in/../../../nested/in/dir/ext-source.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
g.It("should return an error if the source is a directory", func() {
err := os.Mkdir(filepath.Join(rfs.root, "/server/dir"), 0755)
g.Assert(err).IsNil()
err = fs.Copy("dir")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrNotExist)).IsTrue()
})
g.It("should return an error if there is not space to copy the file", func() {
atomic.StoreInt64(&fs.diskLimit, 2)
err := fs.Copy("source.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrNotEnoughDiskSpace)).IsTrue()
})
g.It("should create a copy of the file and increment the disk used", func() {
err := fs.Copy("source.txt")
g.Assert(err).IsNil()
_, err = rfs.StatServerFile("source.txt")
g.Assert(err).IsNil()
_, err = rfs.StatServerFile("source copy.txt")
g.Assert(err).IsNil()
})
g.It("should create a copy of the file with a suffix if a copy already exists", func() {
err := fs.Copy("source.txt")
g.Assert(err).IsNil()
err = fs.Copy("source.txt")
g.Assert(err).IsNil()
r := []string{"source.txt", "source copy.txt", "source copy 1.txt"}
for _, name := range r {
_, err = rfs.StatServerFile(name)
g.Assert(err).IsNil()
}
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(utf8.RuneCountInString("test content")) * 3)
})
g.It("should create a copy inside of a directory", func() {
err := os.MkdirAll(filepath.Join(rfs.root, "/server/nested/in/dir"), 0755)
g.Assert(err).IsNil()
err = rfs.CreateServerFile("nested/in/dir/source.txt", "test content")
g.Assert(err).IsNil()
err = fs.Copy("nested/in/dir/source.txt")
g.Assert(err).IsNil()
_, err = rfs.StatServerFile("nested/in/dir/source.txt")
g.Assert(err).IsNil()
_, err = rfs.StatServerFile("nested/in/dir/source copy.txt")
g.Assert(err).IsNil()
})
g.AfterEach(func() {
rfs.reset()
atomic.StoreInt64(&fs.diskUsed, 0)
atomic.StoreInt64(&fs.diskLimit, 0)
})
})
}
func TestFilesystem_Delete(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()
g.Describe("Delete", func() {
g.BeforeEach(func() {
if err := rfs.CreateServerFile("source.txt", "test content"); err != nil {
panic(err)
}
atomic.StoreInt64(&fs.diskUsed, int64(utf8.RuneCountInString("test content")))
})
g.It("does not delete files outside the root directory", func() {
err := rfs.CreateServerFile("/../ext-source.txt", "external content")
err = fs.Delete("../ext-source.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
g.It("does not allow the deletion of the root directory", func() {
err := fs.Delete("/")
g.Assert(err).IsNotNil()
g.Assert(err.Error()).Equal("cannot delete root server directory")
})
g.It("does not return an error if the target does not exist", func() {
err := fs.Delete("missing.txt")
g.Assert(err).IsNil()
st, err := rfs.StatServerFile("source.txt")
g.Assert(err).IsNil()
g.Assert(st.Name()).Equal("source.txt")
})
g.It("deletes files and subtracts their size from the disk usage", func() {
err := fs.Delete("source.txt")
g.Assert(err).IsNil()
_, err = rfs.StatServerFile("source.txt")
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrNotExist)).IsTrue()
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(0))
})
g.It("deletes all items inside a directory if the directory is deleted", func() {
sources := []string{
"foo/source.txt",
"foo/bar/source.txt",
"foo/bar/baz/source.txt",
}
err := os.MkdirAll(filepath.Join(rfs.root, "/server/foo/bar/baz"), 0755)
g.Assert(err).IsNil()
for _, s := range sources {
err = rfs.CreateServerFile(s, "test content")
g.Assert(err).IsNil()
}
atomic.StoreInt64(&fs.diskUsed, int64(utf8.RuneCountInString("test content")*3))
err = fs.Delete("foo")
g.Assert(err).IsNil()
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(0))
for _, s := range sources {
_, err = rfs.StatServerFile(s)
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrNotExist)).IsTrue()
}
})
g.AfterEach(func() {
rfs.reset()
atomic.StoreInt64(&fs.diskUsed, 0)
atomic.StoreInt64(&fs.diskLimit, 0)
})
})
}

View File

@@ -1,141 +0,0 @@
package filesystem
import (
"context"
"golang.org/x/sync/errgroup"
"os"
"path/filepath"
"strings"
"sync"
)
// Normalizes a directory being passed in to ensure the user is not able to escape
// from their data directory. After normalization if the directory is still within their home
// path it is returned. If they managed to "escape" an error will be returned.
//
// This logic is actually copied over from the SFTP server code. Ideally that eventually
// either gets ported into this application, or is able to make use of this package.
func (fs *Filesystem) SafePath(p string) (string, error) {
var nonExistentPathResolution string
// Start with a cleaned up path before checking the more complex bits.
r := fs.unsafeFilePath(p)
// At the same time, evaluate the symlink status and determine where this file or folder
// is truly pointing to.
p, err := filepath.EvalSymlinks(r)
if err != nil && !os.IsNotExist(err) {
return "", err
} else if os.IsNotExist(err) {
// The requested directory doesn't exist, so at this point we need to iterate up the
// path chain until we hit a directory that _does_ exist and can be validated.
parts := strings.Split(filepath.Dir(r), "/")
var try string
// Range over all of the path parts and form directory pathings from the end
// moving up until we have a valid resolution or we run out of paths to try.
for k := range parts {
try = strings.Join(parts[:(len(parts)-k)], "/")
if !fs.unsafeIsInDataDirectory(try) {
break
}
t, err := filepath.EvalSymlinks(try)
if err == nil {
nonExistentPathResolution = t
break
}
}
}
// If the new path doesn't start with their root directory there is clearly an escape
// attempt going on, and we should NOT resolve this path for them.
if nonExistentPathResolution != "" {
if !fs.unsafeIsInDataDirectory(nonExistentPathResolution) {
return "", ErrBadPathResolution
}
// If the nonExistentPathResolution variable is not empty then the initial path requested
// did not exist and we looped through the pathway until we found a match. At this point
// we've confirmed the first matched pathway exists in the root server directory, so we
// can go ahead and just return the path that was requested initially.
return r, nil
}
// If the requested directory from EvalSymlinks begins with the server root directory go
// ahead and return it. If not we'll return an error which will block any further action
// on the file.
if fs.unsafeIsInDataDirectory(p) {
return p, nil
}
return "", ErrBadPathResolution
}
// Generate a path to the file by cleaning it up and appending the root server path to it. This
// DOES NOT guarantee that the file resolves within the server data directory. You'll want to use
// the fs.unsafeIsInDataDirectory(p) function to confirm.
func (fs *Filesystem) unsafeFilePath(p string) string {
// Calling filepath.Clean on the joined directory will resolve it to the absolute path,
// removing any ../ type of resolution arguments, and leaving us with a direct path link.
//
// This will also trim the existing root path off the beginning of the path passed to
// the function since that can get a bit messy.
return filepath.Clean(filepath.Join(fs.Path(), strings.TrimPrefix(p, fs.Path())))
}
// Check that that path string starts with the server data directory path. This function DOES NOT
// validate that the rest of the path does not end up resolving out of this directory, or that the
// targeted file or folder is not a symlink doing the same thing.
func (fs *Filesystem) unsafeIsInDataDirectory(p string) bool {
return strings.HasPrefix(strings.TrimSuffix(p, "/")+"/", strings.TrimSuffix(fs.Path(), "/")+"/")
}
// Executes the fs.SafePath function in parallel against an array of paths. If any of the calls
// fails an error will be returned.
func (fs *Filesystem) ParallelSafePath(paths []string) ([]string, error) {
var cleaned []string
// Simple locker function to avoid racy appends to the array of cleaned paths.
var m = new(sync.Mutex)
var push = func(c string) {
m.Lock()
cleaned = append(cleaned, c)
m.Unlock()
}
// Create an error group that we can use to run processes in parallel while retaining
// the ability to cancel the entire process immediately should any of it fail.
g, ctx := errgroup.WithContext(context.Background())
// Iterate over all of the paths and generate a cleaned path, if there is an error for any
// of the files, abort the process.
for _, p := range paths {
// Create copy so we can use it within the goroutine correctly.
pi := p
// Recursively call this function to continue digging through the directory tree within
// a separate goroutine. If the context is canceled abort this process.
g.Go(func() error {
select {
case <-ctx.Done():
return ctx.Err()
default:
// If the callback returns true, go ahead and keep walking deeper. This allows
// us to programmatically continue deeper into directories, or stop digging
// if that pathway knows it needs nothing else.
if c, err := fs.SafePath(pi); err != nil {
return err
} else {
push(c)
}
return nil
}
})
}
// Block until all of the routines finish and have returned a value.
return cleaned, g.Wait()
}

View File

@@ -1,74 +0,0 @@
package filesystem
import (
"encoding/json"
"github.com/gabriel-vasile/mimetype"
"os"
"time"
)
type Stat struct {
Info os.FileInfo
Mimetype string
}
func (s *Stat) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Name string `json:"name"`
Created string `json:"created"`
Modified string `json:"modified"`
Mode string `json:"mode"`
Size int64 `json:"size"`
Directory bool `json:"directory"`
File bool `json:"file"`
Symlink bool `json:"symlink"`
Mime string `json:"mime"`
}{
Name: s.Info.Name(),
Created: s.CTime().Format(time.RFC3339),
Modified: s.Info.ModTime().Format(time.RFC3339),
Mode: s.Info.Mode().String(),
Size: s.Info.Size(),
Directory: s.Info.IsDir(),
File: !s.Info.IsDir(),
Symlink: s.Info.Mode().Perm()&os.ModeSymlink != 0,
Mime: s.Mimetype,
})
}
// Stats a file or folder and returns the base stat object from go along with the
// MIME data that can be used for editing files.
func (fs *Filesystem) Stat(p string) (*Stat, error) {
cleaned, err := fs.SafePath(p)
if err != nil {
return nil, err
}
return fs.unsafeStat(cleaned)
}
func (fs *Filesystem) unsafeStat(p string) (*Stat, error) {
s, err := os.Stat(p)
if err != nil {
return nil, err
}
var m *mimetype.MIME
if !s.IsDir() {
m, err = mimetype.DetectFile(p)
if err != nil {
return nil, err
}
}
st := &Stat{
Info: s,
Mimetype: "inode/directory",
}
if m != nil {
st.Mimetype = m.String()
}
return st, nil
}

View File

@@ -1,14 +0,0 @@
package filesystem
import (
"syscall"
"time"
)
// Returns the time that the file/folder was created.
func (s *Stat) CTime() time.Time {
st := s.Info.Sys().(*syscall.Stat_t)
// Do not remove these "redundant" type-casts, they are required for 32-bit builds to work.
return time.Unix(int64(st.Ctim.Sec), int64(st.Ctim.Nsec))
}

View File

@@ -0,0 +1,13 @@
package server
import (
"syscall"
"time"
)
// Returns the time that the file/folder was created.
func (s *Stat) CTime() time.Time {
st := s.Info.Sys().(*syscall.Stat_t)
return time.Unix(int64(st.Ctimespec.Sec), int64(st.Ctimespec.Nsec))
}

View File

@@ -1,4 +1,4 @@
package filesystem
package server
import (
"syscall"
@@ -9,5 +9,5 @@ import (
func (s *Stat) CTime() time.Time {
st := s.Info.Sys().(*syscall.Stat_t)
return time.Unix(st.Ctimespec.Sec, st.Ctimespec.Nsec)
}
return time.Unix(int64(st.Ctim.Sec), int64(st.Ctim.Nsec))
}

View File

@@ -1,4 +1,4 @@
package filesystem
package server
import (
"time"
@@ -9,4 +9,4 @@ import (
// for right now.
func (s *Stat) CTime() time.Time {
return s.Info.ModTime()
}
}

Some files were not shown because too many files have changed in this diff Show More