Compare commits
1 Commits
v1.11.3
...
release/v1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fda42596c9 |
38
.github/workflows/docker.yaml
vendored
38
.github/workflows/docker.yaml
vendored
@@ -4,9 +4,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
@@ -14,26 +13,21 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
# Always run against a tag, even if the commit into the tag has [docker skip] within the commit message.
|
||||
if: "!contains(github.ref, 'develop') || (!contains(github.event.head_commit.message, 'skip docker') && !contains(github.event.head_commit.message, 'docker skip'))"
|
||||
|
||||
steps:
|
||||
- name: Code checkout
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Docker metadata
|
||||
- name: Docker Meta
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: crazy-max/ghaction-docker-meta@v1
|
||||
with:
|
||||
images: ghcr.io/pterodactyl/wings
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.action == 'published' && github.event.release.prerelease == false }}
|
||||
type=ref,event=tag
|
||||
type=ref,event=branch
|
||||
|
||||
- name: Setup QEMU
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Setup Docker buildx
|
||||
- name: Install buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
@@ -46,12 +40,12 @@ jobs:
|
||||
- name: Get Build Information
|
||||
id: build_info
|
||||
run: |
|
||||
echo "version_tag=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_OUTPUT
|
||||
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
echo "::set-output name=version_tag::${GITHUB_REF/refs\/tags\/v/}"
|
||||
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
|
||||
|
||||
- name: Build and Push (tag)
|
||||
uses: docker/build-push-action@v3
|
||||
if: "github.event_name == 'release' && github.event.action == 'published'"
|
||||
- name: Build and push (latest)
|
||||
uses: docker/build-push-action@v2
|
||||
if: "!contains(github.ref, 'develop')"
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
@@ -62,9 +56,9 @@ jobs:
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
|
||||
- name: Build and Push (develop)
|
||||
uses: docker/build-push-action@v3
|
||||
if: "github.event_name == 'push' && contains(github.ref, 'develop')"
|
||||
- name: Build and push (develop)
|
||||
uses: docker/build-push-action@v2
|
||||
if: "contains(github.ref, 'develop')"
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
|
||||
6
.github/workflows/push.yaml
vendored
6
.github/workflows/push.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-20.04]
|
||||
go: ["1.18.10", "1.19.5"]
|
||||
go: ["1.18.8", "1.19.3"]
|
||||
goos: [linux]
|
||||
goarch: [amd64, arm64]
|
||||
|
||||
@@ -86,14 +86,14 @@ jobs:
|
||||
go test -race $(go list ./...)
|
||||
|
||||
- name: Upload Release Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v2
|
||||
if: ${{ github.ref == 'refs/heads/develop' || github.event_name == 'pull_request' }}
|
||||
with:
|
||||
name: wings_linux_${{ matrix.goarch }}
|
||||
path: dist/wings
|
||||
|
||||
- name: Upload Debug Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v2
|
||||
if: ${{ github.ref == 'refs/heads/develop' || github.event_name == 'pull_request' }}
|
||||
with:
|
||||
name: wings_linux_${{ matrix.goarch }}_debug
|
||||
|
||||
11
.github/workflows/release.yaml
vendored
11
.github/workflows/release.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: "1.18.10"
|
||||
go-version: "1.18.8"
|
||||
|
||||
- name: Build release binaries
|
||||
env:
|
||||
@@ -34,6 +34,7 @@ jobs:
|
||||
REF: ${{ github.ref }}
|
||||
run: |
|
||||
sed -n "/^## ${REF:10}/,/^## /{/^## /b;p}" CHANGELOG.md > ./RELEASE_CHANGELOG
|
||||
echo "version_name=`sed -nr "s/^## (${REF:10} .*)$/\1/p" CHANGELOG.md`" > $GITHUB_OUTPUT
|
||||
|
||||
- name: Create checksum and add to changelog
|
||||
run: |
|
||||
@@ -58,13 +59,15 @@ jobs:
|
||||
|
||||
- name: Create release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ contains(github.ref, 'rc') || contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
|
||||
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
|
||||
|
||||
57
CHANGELOG.md
57
CHANGELOG.md
@@ -1,58 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## v1.11.3
|
||||
### Fixed
|
||||
* CVE-2023-25152
|
||||
|
||||
## v1.11.2
|
||||
### Fixed
|
||||
* Backups being restored from remote storage (s3) erroring out due to a closed stream.
|
||||
* Fix IP validation logic for activity logs filtering out valid IPs instead of invalid IPs
|
||||
|
||||
## v1.11.1
|
||||
### Changed
|
||||
* Release binaries are now built with Go 1.18.10
|
||||
* Timeout when stopping a server before a transfer begins has been reduced to 15 seconds from 1 minute
|
||||
* Removed insecure SSH protocols for use with the SFTP server
|
||||
|
||||
### Fixed
|
||||
* Unnecessary Docker client connections being left open, causing a slow leak of file descriptors
|
||||
* Files being left open in parts of the server's filesystem, causing a leak of file descriptors
|
||||
* IPv6 addresses being corrupted by flawed port stripping logic for activity logs, old entries with malformed IPs will be deleted from the local SQLite database automatically
|
||||
* A server that times out while being stopped at the beginning of a transfer no longer causes the server to become stuck in a transferring state
|
||||
|
||||
## v1.11.0
|
||||
### Added (since 1.7.2)
|
||||
* More detailed information returned by the `/api/system` endpoint when using the `?v=2` query parameter.
|
||||
|
||||
### Changed (since 1.7.2)
|
||||
* Send re-installation status separately from installation status.
|
||||
* Wings release versions will now follow the major and minor version of the Panel.
|
||||
* Transfers no longer buffer to disk, instead they are fully streamed with only a small amount of memory used for buffering.
|
||||
* Release binaries are no longer compressed with UPX.
|
||||
* Use `POST` instead of `GET` for sending the status of a transfer to the Panel.
|
||||
|
||||
### Fixed (since 1.7.2)
|
||||
* Fixed servers outgoing IP not being updated whenever a server's primary allocation is changed when using the Force Outgoing IP option.
|
||||
* Fixed servers being terminated rather than gracefully stopped when a signal is used to stop the container rather than a command.
|
||||
* Fixed file not found errors being treated as an internal error, they are now treated as a 404.
|
||||
* Wings can be run with Podman instead of Docker, this is still experimental and not recommended for production use.
|
||||
* Archive progress is now reported correctly.
|
||||
* Labels for containers can now be set by the Panel.
|
||||
* Fixed servers becoming deadlocked when the target node of a transfer goes offline.
|
||||
|
||||
## v1.11.0-rc.2
|
||||
### Added
|
||||
* More detailed information returned by the `/api/system` endpoint when using the `?v=2` query parameter.
|
||||
|
||||
### Changed
|
||||
* Send reinstallation status separately from installation status.
|
||||
|
||||
### Fixed
|
||||
* Fixed servers outgoing IP not being updated whenever a server's primary allocation is changed when using the Force Outgoing IP option.
|
||||
* Fixed servers being terminated rather than gracefully stopped when a signal is used to stop the container rather than a command.
|
||||
* Fixed file not found errors being treated as an internal error, they are now treated as a 404.
|
||||
|
||||
## v1.11.0-rc.1
|
||||
### Changed
|
||||
* Wings release versions will now follow the major and minor version of the panel.
|
||||
@@ -64,10 +11,6 @@
|
||||
* Archive progress is now reported correctly.
|
||||
* Labels for containers can now be set by the Panel.
|
||||
|
||||
## v1.7.3
|
||||
### Fixed
|
||||
* CVE-2023-25152
|
||||
|
||||
## v1.7.2
|
||||
### Fixed
|
||||
* The S3 backup driver now supports Cloudflare R2
|
||||
|
||||
31
README.md
31
README.md
@@ -5,7 +5,6 @@
|
||||
[](https://goreportcard.com/report/github.com/pterodactyl/wings)
|
||||
|
||||
# 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.
|
||||
@@ -14,31 +13,31 @@ In addition, Wings ships with a built-in SFTP server allowing your system to rem
|
||||
dependencies, and allowing users to authenticate with the same credentials they would normally use to access the Panel.
|
||||
|
||||
## Sponsors
|
||||
|
||||
I would like to extend my sincere thanks to the following sponsors for helping find Pterodactyl's development.
|
||||
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/matthewpi)
|
||||
|
||||
| Company | About |
|
||||
|-----------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [**WISP**](https://wisp.gg) | Extra features. |
|
||||
| [**Aussie Server Hosts**](https://aussieserverhosts.com/) | No frills Australian Owned and operated High Performance Server hosting for some of the most demanding games serving Australia and New Zealand. |
|
||||
| [**BisectHosting**](https://www.bisecthosting.com/) | BisectHosting provides Minecraft, Valheim and other server hosting services with the highest reliability and lightning fast support since 2012. |
|
||||
| [**MineStrator**](https://minestrator.com/) | Looking for the most highend French hosting company for your minecraft server? More than 24,000 members on our discord trust us. Give us a try! |
|
||||
| [**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! |
|
||||
| [**VibeGAMES**](https://vibegames.net/) | VibeGAMES is a game server provider that specializes in DDOS protection for the games we offer. We have multiple locations in the US, Brazil, France, Germany, Singapore, Australia and South Africa. |
|
||||
| [**Pterodactyl Market**](https://pterodactylmarket.com/) | Pterodactyl Market is a one-and-stop shop for Pterodactyl. In our market, you can find Add-ons, Themes, Eggs, and more for Pterodactyl. |
|
||||
| [**UltraServers**](https://ultraservers.com/) | Deploy premium games hosting with the click of a button. Manage and swap games with ease and let us take care of the rest. We currently support Minecraft, Rust, ARK, 7 Days to Die, Garys MOD, CS:GO, Satisfactory and others. |
|
||||
| [**Realms Hosting**](https://realmshosting.com/) | Want to build your Gaming Empire? Use Realms Hosting today to kick start your game server hosting with outstanding DDOS Protection, 24/7 Support, Cheap Prices and a Custom Control Panel. | |
|
||||
| Company | About |
|
||||
| ------- | ----- |
|
||||
| [**WISP**](https://wisp.gg) | Extra features. |
|
||||
| [**BisectHosting**](https://www.bisecthosting.com/) | BisectHosting provides Minecraft, Valheim and other server hosting services with the highest reliability and lightning fast support since 2012. |
|
||||
| [**Fragnet**](https://fragnet.net) | Providing low latency, high-end game hosting solutions to gamers, game studios and eSports platforms. |
|
||||
| [**Tempest**](https://tempest.net/) | Tempest Hosting is a subsidiary of Path Network, Inc. offering unmetered DDoS protected 10Gbps dedicated servers, starting at just $80/month. Full anycast, tons of filters. |
|
||||
| [**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 the most highend French hosting company for your minecraft server? More than 24,000 members on our discord trust us. Give us a try! |
|
||||
| [**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! |
|
||||
| [**DeinServerHost**](https://deinserverhost.de/) | DeinServerHost offers Dedicated, vps and Gameservers for many popular Games like Minecraft and Rust in Germany since 2013. |
|
||||
| [**Aussie Server Hosts**](https://aussieserverhosts.com/) | No frills Australian Owned and operated High Performance Server hosting for some of the most demanding games serving Australia and New Zealand. |
|
||||
| [**HostEZ**](https://hostez.io) | Providing North America Valheim, Minecraft and other popular games with low latency, high uptime and maximum availability. EZ! |
|
||||
| [**VibeGAMES**](https://vibegames.net/) | VibeGAMES is a game server provider that specializes in DDOS protection for the games we offer. We have multiple locations in the US, Brazil, France, Germany, Singapore, Australia and South Africa.|
|
||||
| [**Gamenodes**](https://gamenodes.nl) | Gamenodes love quality. For Minecraft, Discord Bots and other services, among others. With our own programmers, we provide just that little bit of extra service! |
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -58,7 +58,7 @@ func (e *Environment) Attach(ctx context.Context) error {
|
||||
|
||||
// Set the stream again with the container.
|
||||
if st, err := e.client.ContainerAttach(ctx, e.Id, opts); err != nil {
|
||||
return errors.WrapIf(err, "environment/docker: error while attaching to container")
|
||||
return err
|
||||
} else {
|
||||
e.SetStream(&st)
|
||||
}
|
||||
@@ -143,7 +143,7 @@ func (e *Environment) Create() error {
|
||||
if _, err := e.ContainerInspect(ctx); err == nil {
|
||||
return nil
|
||||
} else if !client.IsErrNotFound(err) {
|
||||
return errors.WrapIf(err, "environment/docker: failed to inspect container")
|
||||
return errors.Wrap(err, "environment/docker: failed to inspect container")
|
||||
}
|
||||
|
||||
// Try to pull the requested image before creating the container.
|
||||
@@ -196,7 +196,7 @@ func (e *Environment) Create() error {
|
||||
networkMode := container.NetworkMode(cfg.Docker.Network.Mode)
|
||||
if a.ForceOutgoingIP {
|
||||
e.log().Debug("environment/docker: forcing outgoing IP address")
|
||||
networkName := "ip-" + strings.ReplaceAll(strings.ReplaceAll(a.DefaultMapping.Ip, ".", "-"), ":", "-")
|
||||
networkName := strings.ReplaceAll(e.Id, "-", "")
|
||||
networkMode = container.NetworkMode(networkName)
|
||||
|
||||
if _, err := e.client.NetworkInspect(ctx, networkName, types.NetworkInspectOptions{}); err != nil {
|
||||
|
||||
@@ -161,7 +161,7 @@ func (e *Environment) ExitState() (uint32, bool, error) {
|
||||
if client.IsErrNotFound(err) {
|
||||
return 1, false, nil
|
||||
}
|
||||
return 0, false, errors.WrapIf(err, "environment/docker: failed to inspect container")
|
||||
return 0, false, err
|
||||
}
|
||||
return uint32(c.State.ExitCode), c.State.OOMKilled, nil
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ func (e *Environment) Start(ctx context.Context) error {
|
||||
// exists on the system, and rebuild the container if that is required for server booting to
|
||||
// occur.
|
||||
if err := e.OnBeforeStart(ctx); err != nil {
|
||||
return errors.WrapIf(err, "environment/docker: failed to run pre-boot process")
|
||||
return errors.WithStackIf(err)
|
||||
}
|
||||
|
||||
// If we cannot start & attach to the container in 30 seconds something has gone
|
||||
@@ -119,7 +119,7 @@ func (e *Environment) Start(ctx context.Context) error {
|
||||
// By explicitly attaching to the instance before we start it, we can immediately
|
||||
// react to errors/output stopping/etc. when starting.
|
||||
if err := e.Attach(actx); err != nil {
|
||||
return errors.WrapIf(err, "environment/docker: failed to attach to container")
|
||||
return err
|
||||
}
|
||||
|
||||
if err := e.client.ContainerStart(actx, e.Id, types.ContainerStartOptions{}); err != nil {
|
||||
@@ -179,12 +179,8 @@ func (e *Environment) Stop(ctx context.Context) error {
|
||||
|
||||
// Allow the stop action to run for however long it takes, similar to executing a command
|
||||
// and using a different logic pathway to wait for the container to stop successfully.
|
||||
//
|
||||
// Using a negative timeout here will allow the container to stop gracefully,
|
||||
// rather than forcefully terminating it, this value MUST be at least 1
|
||||
// second, otherwise it will be ignored.
|
||||
timeout := -1 * time.Second
|
||||
if err := e.client.ContainerStop(ctx, e.Id, &timeout); err != nil {
|
||||
t := time.Duration(-1)
|
||||
if err := e.client.ContainerStop(ctx, e.Id, &t); err != nil {
|
||||
// If the container does not exist just mark the process as stopped and return without
|
||||
// an error.
|
||||
if client.IsErrNotFound(err) {
|
||||
|
||||
@@ -2,7 +2,6 @@ package cron
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"emperror.dev/errors"
|
||||
|
||||
@@ -18,9 +17,9 @@ type activityCron struct {
|
||||
max int
|
||||
}
|
||||
|
||||
// Run executes the cronjob and ensures we fetch and send all the stored activity to the
|
||||
// Run executes the cronjob and ensures we fetch and send all of the stored activity to the
|
||||
// Panel instance. Once activity is sent it is deleted from the local database instance. Any
|
||||
// SFTP specific events are not handled in this cron, they're handled separately to account
|
||||
// SFTP specific events are not handled in this cron, they're handled seperately to account
|
||||
// for de-duplication and event merging.
|
||||
func (ac *activityCron) Run(ctx context.Context) error {
|
||||
// Don't execute this cron if there is currently one running. Once this task is completed
|
||||
@@ -35,6 +34,7 @@ func (ac *activityCron) Run(ctx context.Context) error {
|
||||
Where("event NOT LIKE ?", "server:sftp.%").
|
||||
Limit(ac.max).
|
||||
Find(&activity)
|
||||
|
||||
if tx.Error != nil {
|
||||
return errors.WithStack(tx.Error)
|
||||
}
|
||||
@@ -42,42 +42,15 @@ func (ac *activityCron) Run(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ids to delete from the database.
|
||||
ids := make([]int, 0, len(activity))
|
||||
// activities to send to the panel.
|
||||
activities := make([]models.Activity, 0, len(activity))
|
||||
for _, v := range activity {
|
||||
// Delete any activity that has an invalid IP address. This is a fix for
|
||||
// a bug that truncated the last octet of an IPv6 address in the database.
|
||||
if ip := net.ParseIP(v.IP); ip == nil {
|
||||
ids = append(ids, v.ID)
|
||||
continue
|
||||
}
|
||||
activities = append(activities, v)
|
||||
}
|
||||
|
||||
if len(ids) > 0 {
|
||||
tx = database.Instance().WithContext(ctx).Where("id IN ?", ids).Delete(&models.Activity{})
|
||||
if tx.Error != nil {
|
||||
return errors.WithStack(tx.Error)
|
||||
}
|
||||
}
|
||||
|
||||
if len(activities) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := ac.manager.Client().SendActivityLogs(ctx, activities); err != nil {
|
||||
if err := ac.manager.Client().SendActivityLogs(ctx, activity); err != nil {
|
||||
return errors.WrapIf(err, "cron: failed to send activity events to Panel")
|
||||
}
|
||||
|
||||
// Add all the successful activities to the list of IDs to delete.
|
||||
ids = make([]int, len(activities))
|
||||
for i, v := range activities {
|
||||
ids[i] = v.ID
|
||||
var ids []int
|
||||
for _, v := range activity {
|
||||
ids = append(ids, v.ID)
|
||||
}
|
||||
|
||||
// Delete all the activities that were sent to the Panel (or that were invalid).
|
||||
tx = database.Instance().WithContext(ctx).Where("id IN ?", ids).Delete(&models.Activity{})
|
||||
if tx.Error != nil {
|
||||
return errors.WithStack(tx.Error)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/pterodactyl/wings/system"
|
||||
)
|
||||
|
||||
type Event string
|
||||
@@ -57,9 +57,7 @@ func (a Activity) SetUser(u string) *Activity {
|
||||
// is trimmed down to remove any extraneous data, and the timestamp is set to the current
|
||||
// system time and then stored as UTC.
|
||||
func (a *Activity) BeforeCreate(_ *gorm.DB) error {
|
||||
if ip, _, err := net.SplitHostPort(strings.TrimSpace(a.IP)); err == nil {
|
||||
a.IP = ip
|
||||
}
|
||||
a.IP = system.TrimIPSuffix(a.IP)
|
||||
if a.Timestamp.IsZero() {
|
||||
a.Timestamp = time.Now()
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ type Client interface {
|
||||
SetArchiveStatus(ctx context.Context, uuid string, successful bool) error
|
||||
SetBackupStatus(ctx context.Context, backup string, data BackupRequest) error
|
||||
SendRestorationStatus(ctx context.Context, backup string, successful bool) error
|
||||
SetInstallationStatus(ctx context.Context, uuid string, data InstallStatusRequest) error
|
||||
SetInstallationStatus(ctx context.Context, uuid string, successful bool) error
|
||||
SetTransferStatus(ctx context.Context, uuid string, successful bool) error
|
||||
ValidateSftpCredentials(ctx context.Context, request SftpAuthRequest) (SftpAuthResponse, error)
|
||||
SendActivityLogs(ctx context.Context, activity []models.Activity) error
|
||||
|
||||
@@ -19,7 +19,7 @@ const (
|
||||
ProcessStopNativeStop = "stop"
|
||||
)
|
||||
|
||||
// GetServers returns all the servers that are present on the Panel making
|
||||
// GetServers returns all of the servers that are present on the Panel making
|
||||
// parallel API calls to the endpoint if more than one page of servers is
|
||||
// returned.
|
||||
func (c *client) GetServers(ctx context.Context, limit int) ([]RawServerData, error) {
|
||||
@@ -58,7 +58,7 @@ func (c *client) GetServers(ctx context.Context, limit int) ([]RawServerData, er
|
||||
//
|
||||
// This handles Wings exiting during either of these processes which will leave
|
||||
// things in a bad state within the Panel. This API call is executed once Wings
|
||||
// has fully booted all the servers.
|
||||
// has fully booted all of the servers.
|
||||
func (c *client) ResetServersState(ctx context.Context) error {
|
||||
res, err := c.Post(ctx, "/servers/reset", nil)
|
||||
if err != nil {
|
||||
@@ -92,8 +92,8 @@ func (c *client) GetInstallationScript(ctx context.Context, uuid string) (Instal
|
||||
return config, err
|
||||
}
|
||||
|
||||
func (c *client) SetInstallationStatus(ctx context.Context, uuid string, data InstallStatusRequest) error {
|
||||
resp, err := c.Post(ctx, fmt.Sprintf("/servers/%s/install", uuid), data)
|
||||
func (c *client) SetInstallationStatus(ctx context.Context, uuid string, successful bool) error {
|
||||
resp, err := c.Post(ctx, fmt.Sprintf("/servers/%s/install", uuid), d{"successful": successful})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -115,7 +115,7 @@ func (c *client) SetTransferStatus(ctx context.Context, uuid string, successful
|
||||
if successful {
|
||||
state = "success"
|
||||
}
|
||||
resp, err := c.Post(ctx, fmt.Sprintf("/servers/%s/transfer/%s", uuid, state), nil)
|
||||
resp, err := c.Get(ctx, fmt.Sprintf("/servers/%s/transfer/%s", uuid, state), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -127,7 +127,7 @@ func (c *client) SetTransferStatus(ctx context.Context, uuid string, successful
|
||||
// password combination provided is associated with a valid server on the instance
|
||||
// using the Panel's authentication control mechanisms. This will get itself
|
||||
// throttled if too many requests are made, allowing us to completely offload
|
||||
// all the authorization security logic to the Panel.
|
||||
// all of the authorization security logic to the Panel.
|
||||
func (c *client) ValidateSftpCredentials(ctx context.Context, request SftpAuthRequest) (SftpAuthResponse, error) {
|
||||
var auth SftpAuthResponse
|
||||
res, err := c.Post(ctx, "/sftp/auth", request)
|
||||
|
||||
@@ -92,8 +92,8 @@ type SftpAuthResponse struct {
|
||||
}
|
||||
|
||||
type OutputLineMatcher struct {
|
||||
// raw string to match against. This may or may not be prefixed with
|
||||
// `regex:` which indicates we want to match against the regex expression.
|
||||
// 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 []byte
|
||||
reg *regexp.Regexp
|
||||
}
|
||||
@@ -139,9 +139,9 @@ type ProcessStopConfiguration struct {
|
||||
}
|
||||
|
||||
// ProcessConfiguration defines the process configuration for a given server
|
||||
// instance. This sets what Wings 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.
|
||||
// instance. This sets what Wings 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"`
|
||||
@@ -169,8 +169,3 @@ type BackupRequest struct {
|
||||
Successful bool `json:"successful"`
|
||||
Parts []BackupPart `json:"parts"`
|
||||
}
|
||||
|
||||
type InstallStatusRequest struct {
|
||||
Successful bool `json:"successful"`
|
||||
Reinstall bool `json:"reinstall"`
|
||||
}
|
||||
|
||||
157
router/error.go
Normal file
157
router/error.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"emperror.dev/errors"
|
||||
"github.com/apex/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/pterodactyl/wings/server"
|
||||
"github.com/pterodactyl/wings/server/filesystem"
|
||||
)
|
||||
|
||||
type RequestError struct {
|
||||
err error
|
||||
uuid string
|
||||
message string
|
||||
server *server.Server
|
||||
}
|
||||
|
||||
// Attaches an error to the gin.Context object for the request and ensures that it
|
||||
// has a proper stacktrace associated with it when doing so.
|
||||
//
|
||||
// If you just call c.Error(err) without using this function you'll likely end up
|
||||
// with an error that has no annotated stack on it.
|
||||
func WithError(c *gin.Context, err error) error {
|
||||
return c.Error(errors.WithStackDepthIf(err, 1))
|
||||
}
|
||||
|
||||
// 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 NewTrackedError(err error) *RequestError {
|
||||
return &RequestError{
|
||||
err: err,
|
||||
uuid: uuid.Must(uuid.NewRandom()).String(),
|
||||
}
|
||||
}
|
||||
|
||||
// Same as NewTrackedError, except this will also attach the server instance that
|
||||
// generated this server for the purposes of logging.
|
||||
func NewServerError(err error, s *server.Server) *RequestError {
|
||||
return &RequestError{
|
||||
err: err,
|
||||
uuid: uuid.Must(uuid.NewRandom()).String(),
|
||||
server: s,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *RequestError) logger() *log.Entry {
|
||||
if e.server != nil {
|
||||
return e.server.Log().WithField("error_id", e.uuid).WithField("error", e.err)
|
||||
}
|
||||
return log.WithField("error_id", e.uuid).WithField("error", e.err)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// In instances where the status has already been set just use that existing status
|
||||
// since we cannot change it at this point, and trying to do so will emit a gin warning
|
||||
// into the program output.
|
||||
if c.Writer.Status() != 200 {
|
||||
status = c.Writer.Status()
|
||||
}
|
||||
|
||||
// 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 errors.Is(e.err, os.ErrNotExist) {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
||||
"error": "The requested resource was not found on the system.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(e.err.Error(), "invalid URL escape") {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Some of the data provided in the request appears to be escaped improperly.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// If this is a Filesystem error just return it without all of the tracking code nonsense
|
||||
// since we don't need to be logging it into the logs or anything, its just a normal error
|
||||
// that the user can solve on their end.
|
||||
if st, msg := e.getAsFilesystemError(); st != 0 {
|
||||
c.AbortWithStatusJSON(st, gin.H{"error": msg})
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, log the error to zap, and then report the error back to the user.
|
||||
if status >= 500 {
|
||||
e.logger().Error("unexpected error while handling HTTP request")
|
||||
} else {
|
||||
e.logger().Debug("non-server error encountered while handling HTTP request")
|
||||
}
|
||||
|
||||
if e.message == "" {
|
||||
e.message = "An unexpected error was encountered while processing this request."
|
||||
}
|
||||
|
||||
c.AbortWithStatusJSON(status, gin.H{"error": e.message, "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) Abort(c *gin.Context) {
|
||||
e.AbortWithStatus(http.StatusInternalServerError, c)
|
||||
}
|
||||
|
||||
// Looks at the given RequestError and determines if it is a specific filesystem error that
|
||||
// we can process and return differently for the user.
|
||||
func (e *RequestError) getAsFilesystemError() (int, string) {
|
||||
// Some external things end up calling fmt.Errorf() on our filesystem errors
|
||||
// which ends up just unleashing chaos on the system. For the sake of this
|
||||
// fallback to using text checks...
|
||||
if filesystem.IsErrorCode(e.err, filesystem.ErrCodeDenylistFile) || strings.Contains(e.err.Error(), "filesystem: file access prohibited") {
|
||||
return http.StatusForbidden, "This file cannot be modified: present in egg denylist."
|
||||
}
|
||||
if filesystem.IsErrorCode(e.err, filesystem.ErrCodePathResolution) || strings.Contains(e.err.Error(), "resolves to a location outside the server root") {
|
||||
return http.StatusNotFound, "The requested resource was not found on the system."
|
||||
}
|
||||
if filesystem.IsErrorCode(e.err, filesystem.ErrCodeIsDirectory) || strings.Contains(e.err.Error(), "filesystem: is a directory") {
|
||||
return http.StatusBadRequest, "Cannot perform that action: file is a directory."
|
||||
}
|
||||
if filesystem.IsErrorCode(e.err, filesystem.ErrCodeDiskSpace) || strings.Contains(e.err.Error(), "filesystem: not enough disk space") {
|
||||
return http.StatusBadRequest, "Cannot perform that action: not enough disk space available."
|
||||
}
|
||||
if strings.HasSuffix(e.err.Error(), "file name too long") {
|
||||
return http.StatusBadRequest, "Cannot perform that action: file name is too long."
|
||||
}
|
||||
if e, ok := e.err.(*os.SyscallError); ok && e.Syscall == "readdirent" {
|
||||
return http.StatusNotFound, "The requested directory does not exist."
|
||||
}
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
// Handle specific filesystem errors for a server.
|
||||
func (e *RequestError) AbortFilesystemError(c *gin.Context) {
|
||||
e.Abort(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)
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"emperror.dev/errors"
|
||||
@@ -14,8 +16,133 @@ import (
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/remote"
|
||||
"github.com/pterodactyl/wings/server"
|
||||
"github.com/pterodactyl/wings/server/filesystem"
|
||||
)
|
||||
|
||||
// RequestError is a custom error type returned when something goes wrong with
|
||||
// any of the HTTP endpoints.
|
||||
type RequestError struct {
|
||||
err error
|
||||
status int
|
||||
msg string
|
||||
}
|
||||
|
||||
// NewError returns a new RequestError for the provided error.
|
||||
func NewError(err error) *RequestError {
|
||||
return &RequestError{
|
||||
// Attach a stacktrace to the error if it is missing at this point and mark it
|
||||
// as originating from the location where NewError was called, rather than this
|
||||
// specific point in the code.
|
||||
err: errors.WithStackDepthIf(err, 1),
|
||||
}
|
||||
}
|
||||
|
||||
// SetMessage allows for a custom error message to be set on an existing
|
||||
// RequestError instance.
|
||||
func (re *RequestError) SetMessage(m string) {
|
||||
re.msg = m
|
||||
}
|
||||
|
||||
// SetStatus sets the HTTP status code for the error response. By default this
|
||||
// is a HTTP-500 error.
|
||||
func (re *RequestError) SetStatus(s int) {
|
||||
re.status = s
|
||||
}
|
||||
|
||||
// Abort aborts the given HTTP request with the specified status code and then
|
||||
// logs the event into the logs. The error that is output will include the unique
|
||||
// request ID if it is present.
|
||||
func (re *RequestError) Abort(c *gin.Context, status int) {
|
||||
reqId := c.Writer.Header().Get("X-Request-Id")
|
||||
|
||||
// Generate the base logger instance, attaching the unique request ID and
|
||||
// the URL that was requested.
|
||||
event := log.WithField("request_id", reqId).WithField("url", c.Request.URL.String())
|
||||
// If there is a server present in the gin.Context stack go ahead and pull it
|
||||
// and attach that server UUID to the logs as well so that we can see what specific
|
||||
// server triggered this error.
|
||||
if s, ok := c.Get("server"); ok {
|
||||
if s, ok := s.(*server.Server); ok {
|
||||
event = event.WithField("server_id", s.ID())
|
||||
}
|
||||
}
|
||||
|
||||
if c.Writer.Status() == 200 {
|
||||
// Handle context deadlines being exceeded a little differently since we want
|
||||
// to report a more user-friendly error and a proper error code. The "context
|
||||
// canceled" error is generally when a request is terminated before all of the
|
||||
// logic is finished running.
|
||||
if errors.Is(re.err, context.DeadlineExceeded) {
|
||||
re.SetStatus(http.StatusGatewayTimeout)
|
||||
re.SetMessage("The server could not process this request in time, please try again.")
|
||||
} else if strings.Contains(re.Cause().Error(), "context canceled") {
|
||||
re.SetStatus(http.StatusBadRequest)
|
||||
re.SetMessage("Request aborted by client.")
|
||||
}
|
||||
}
|
||||
|
||||
// c.Writer.Status() will be a non-200 value if the headers have already been sent
|
||||
// to the requester but an error is encountered. This can happen if there is an issue
|
||||
// marshaling a struct placed into a c.JSON() call (or c.AbortWithJSON() call).
|
||||
if status >= 500 || c.Writer.Status() != 200 {
|
||||
event.WithField("status", status).WithField("error", re.err).Error("error while handling HTTP request")
|
||||
} else {
|
||||
event.WithField("status", status).WithField("error", re.err).Debug("error handling HTTP request (not a server error)")
|
||||
}
|
||||
if re.msg == "" {
|
||||
re.msg = "An unexpected error was encountered while processing this request"
|
||||
}
|
||||
// Now abort the request with the error message and include the unique request
|
||||
// ID that was present to make things super easy on people who don't know how
|
||||
// or cannot view the response headers (where X-Request-Id would be present).
|
||||
c.AbortWithStatusJSON(status, gin.H{"error": re.msg, "request_id": reqId})
|
||||
}
|
||||
|
||||
// Cause returns the underlying error.
|
||||
func (re *RequestError) Cause() error {
|
||||
return re.err
|
||||
}
|
||||
|
||||
// Error returns the underlying error message for this request.
|
||||
func (re *RequestError) Error() string {
|
||||
return re.err.Error()
|
||||
}
|
||||
|
||||
// Looks at the given RequestError and determines if it is a specific filesystem
|
||||
// error that we can process and return differently for the user.
|
||||
//
|
||||
// Some external things end up calling fmt.Errorf() on our filesystem errors
|
||||
// which ends up just unleashing chaos on the system. For the sake of this,
|
||||
// fallback to using text checks.
|
||||
//
|
||||
// If the error passed into this call is nil or does not match empty values will
|
||||
// be returned to the caller.
|
||||
func (re *RequestError) asFilesystemError() (int, string) {
|
||||
err := re.Cause()
|
||||
if err == nil {
|
||||
return 0, ""
|
||||
}
|
||||
if filesystem.IsErrorCode(err, filesystem.ErrCodeDenylistFile) || strings.Contains(err.Error(), "filesystem: file access prohibited") {
|
||||
return http.StatusForbidden, "This file cannot be modified: present in egg denylist."
|
||||
}
|
||||
if filesystem.IsErrorCode(err, filesystem.ErrCodePathResolution) || strings.Contains(err.Error(), "resolves to a location outside the server root") {
|
||||
return http.StatusNotFound, "The requested resource was not found on the system."
|
||||
}
|
||||
if filesystem.IsErrorCode(err, filesystem.ErrCodeIsDirectory) || strings.Contains(err.Error(), "filesystem: is a directory") {
|
||||
return http.StatusBadRequest, "Cannot perform that action: file is a directory."
|
||||
}
|
||||
if filesystem.IsErrorCode(err, filesystem.ErrCodeDiskSpace) || strings.Contains(err.Error(), "filesystem: not enough disk space") {
|
||||
return http.StatusBadRequest, "There is not enough disk space available to perform that action."
|
||||
}
|
||||
if strings.HasSuffix(err.Error(), "file name too long") {
|
||||
return http.StatusBadRequest, "Cannot perform that action: file name is too long."
|
||||
}
|
||||
if e, ok := err.(*os.SyscallError); ok && e.Syscall == "readdirent" {
|
||||
return http.StatusNotFound, "The requested directory does not exist."
|
||||
}
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
// AttachRequestID attaches a unique ID to the incoming HTTP request so that any
|
||||
// errors that are generated or returned to the client will include this reference
|
||||
// allowing for an easier time identifying the specific request that failed for
|
||||
@@ -53,7 +180,7 @@ func AttachApiClient(client remote.Client) gin.HandlerFunc {
|
||||
}
|
||||
|
||||
// CaptureAndAbort aborts the request and attaches the provided error to the gin
|
||||
// context, so it can be reported properly. If the error is missing a stacktrace
|
||||
// context so it can be reported properly. If the error is missing a stacktrace
|
||||
// at the time it is called the stack will be attached.
|
||||
func CaptureAndAbort(c *gin.Context, err error) {
|
||||
c.Abort()
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"emperror.dev/errors"
|
||||
"github.com/apex/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/pterodactyl/wings/server"
|
||||
"github.com/pterodactyl/wings/server/filesystem"
|
||||
)
|
||||
|
||||
// RequestError is a custom error type returned when something goes wrong with
|
||||
// any of the HTTP endpoints.
|
||||
type RequestError struct {
|
||||
err error
|
||||
status int
|
||||
msg string
|
||||
}
|
||||
|
||||
// NewError returns a new RequestError for the provided error.
|
||||
func NewError(err error) *RequestError {
|
||||
return &RequestError{
|
||||
// Attach a stacktrace to the error if it is missing at this point and mark it
|
||||
// as originating from the location where NewError was called, rather than this
|
||||
// specific point in the code.
|
||||
err: errors.WithStackDepthIf(err, 1),
|
||||
}
|
||||
}
|
||||
|
||||
// SetMessage allows for a custom error message to be set on an existing
|
||||
// RequestError instance.
|
||||
func (re *RequestError) SetMessage(m string) {
|
||||
re.msg = m
|
||||
}
|
||||
|
||||
// SetStatus sets the HTTP status code for the error response. By default this
|
||||
// is a HTTP-500 error.
|
||||
func (re *RequestError) SetStatus(s int) {
|
||||
re.status = s
|
||||
}
|
||||
|
||||
// Abort aborts the given HTTP request with the specified status code and then
|
||||
// logs the event into the logs. The error that is output will include the unique
|
||||
// request ID if it is present.
|
||||
func (re *RequestError) Abort(c *gin.Context, status int) {
|
||||
reqId := c.Writer.Header().Get("X-Request-Id")
|
||||
|
||||
// Generate the base logger instance, attaching the unique request ID and
|
||||
// the URL that was requested.
|
||||
event := log.WithField("request_id", reqId).WithField("url", c.Request.URL.String())
|
||||
// If there is a server present in the gin.Context stack go ahead and pull it
|
||||
// and attach that server UUID to the logs as well so that we can see what specific
|
||||
// server triggered this error.
|
||||
if s, ok := c.Get("server"); ok {
|
||||
if s, ok := s.(*server.Server); ok {
|
||||
event = event.WithField("server_id", s.ID())
|
||||
}
|
||||
}
|
||||
|
||||
if c.Writer.Status() == 200 {
|
||||
// Handle context deadlines being exceeded a little differently since we want
|
||||
// to report a more user-friendly error and a proper error code. The "context
|
||||
// canceled" error is generally when a request is terminated before all of the
|
||||
// logic is finished running.
|
||||
if errors.Is(re.err, context.DeadlineExceeded) {
|
||||
re.SetStatus(http.StatusGatewayTimeout)
|
||||
re.SetMessage("The server could not process this request in time, please try again.")
|
||||
} else if strings.Contains(re.Cause().Error(), "context canceled") {
|
||||
re.SetStatus(http.StatusBadRequest)
|
||||
re.SetMessage("Request aborted by client.")
|
||||
}
|
||||
}
|
||||
|
||||
// c.Writer.Status() will be a non-200 value if the headers have already been sent
|
||||
// to the requester but an error is encountered. This can happen if there is an issue
|
||||
// marshaling a struct placed into a c.JSON() call (or c.AbortWithJSON() call).
|
||||
if status >= 500 || c.Writer.Status() != 200 {
|
||||
event.WithField("status", status).WithField("error", re.err).Error("error while handling HTTP request")
|
||||
} else {
|
||||
event.WithField("status", status).WithField("error", re.err).Debug("error handling HTTP request (not a server error)")
|
||||
}
|
||||
if re.msg == "" {
|
||||
re.msg = "An unexpected error was encountered while processing this request"
|
||||
}
|
||||
// Now abort the request with the error message and include the unique request
|
||||
// ID that was present to make things super easy on people who don't know how
|
||||
// or cannot view the response headers (where X-Request-Id would be present).
|
||||
c.AbortWithStatusJSON(status, gin.H{"error": re.msg, "request_id": reqId})
|
||||
}
|
||||
|
||||
// Cause returns the underlying error.
|
||||
func (re *RequestError) Cause() error {
|
||||
return re.err
|
||||
}
|
||||
|
||||
// Error returns the underlying error message for this request.
|
||||
func (re *RequestError) Error() string {
|
||||
return re.err.Error()
|
||||
}
|
||||
|
||||
// Looks at the given RequestError and determines if it is a specific filesystem
|
||||
// error that we can process and return differently for the user.
|
||||
//
|
||||
// Some external things end up calling fmt.Errorf() on our filesystem errors
|
||||
// which ends up just unleashing chaos on the system. For the sake of this,
|
||||
// fallback to using text checks.
|
||||
//
|
||||
// If the error passed into this call is nil or does not match empty values will
|
||||
// be returned to the caller.
|
||||
func (re *RequestError) asFilesystemError() (int, string) {
|
||||
err := re.Cause()
|
||||
if err == nil {
|
||||
return 0, ""
|
||||
}
|
||||
if filesystem.IsErrorCode(err, filesystem.ErrNotExist) ||
|
||||
filesystem.IsErrorCode(err, filesystem.ErrCodePathResolution) ||
|
||||
strings.Contains(err.Error(), "resolves to a location outside the server root") {
|
||||
return http.StatusNotFound, "The requested resources was not found on the system."
|
||||
}
|
||||
if filesystem.IsErrorCode(err, filesystem.ErrCodeDenylistFile) || strings.Contains(err.Error(), "filesystem: file access prohibited") {
|
||||
return http.StatusForbidden, "This file cannot be modified: present in egg denylist."
|
||||
}
|
||||
if filesystem.IsErrorCode(err, filesystem.ErrCodeIsDirectory) || strings.Contains(err.Error(), "filesystem: is a directory") {
|
||||
return http.StatusBadRequest, "Cannot perform that action: file is a directory."
|
||||
}
|
||||
if filesystem.IsErrorCode(err, filesystem.ErrCodeDiskSpace) || strings.Contains(err.Error(), "filesystem: not enough disk space") {
|
||||
return http.StatusBadRequest, "There is not enough disk space available to perform that action."
|
||||
}
|
||||
if strings.HasSuffix(err.Error(), "file name too long") {
|
||||
return http.StatusBadRequest, "Cannot perform that action: file name is too long."
|
||||
}
|
||||
if e, ok := err.(*os.SyscallError); ok && e.Syscall == "readdirent" {
|
||||
return http.StatusNotFound, "The requested directory does not exist."
|
||||
}
|
||||
return 0, ""
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"emperror.dev/errors"
|
||||
"github.com/apex/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -17,10 +16,7 @@ func Configure(m *wserver.Manager, client remote.Client) *gin.Engine {
|
||||
|
||||
router := gin.New()
|
||||
router.Use(gin.Recovery())
|
||||
if err := router.SetTrustedProxies(config.Get().Api.TrustedProxies); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
return nil
|
||||
}
|
||||
_ = router.SetTrustedProxies(config.Get().Api.TrustedProxies)
|
||||
router.Use(middleware.AttachRequestID(), middleware.CaptureErrors(), middleware.SetAccessControlHeaders())
|
||||
router.Use(middleware.AttachServerManager(m), middleware.AttachApiClient(client))
|
||||
// @todo log this into a different file so you can setup IP blocking for abusive requests and such.
|
||||
|
||||
@@ -21,11 +21,12 @@ func getDownloadBackup(c *gin.Context) {
|
||||
|
||||
token := tokens.BackupPayload{}
|
||||
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewTrackedError(err).Abort(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := manager.Get(token.ServerUuid); !ok || !token.IsUniqueRequest() {
|
||||
s, ok := manager.Get(token.ServerUuid)
|
||||
if !ok || !token.IsUniqueRequest() {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
||||
"error": "The requested resource was not found on this server.",
|
||||
})
|
||||
@@ -41,13 +42,13 @@ func getDownloadBackup(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewServerError(err, s).Abort(c)
|
||||
return
|
||||
}
|
||||
|
||||
f, err := os.Open(b.Path())
|
||||
if err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewServerError(err, s).Abort(c)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
@@ -56,7 +57,7 @@ func getDownloadBackup(c *gin.Context) {
|
||||
c.Header("Content-Disposition", "attachment; filename="+strconv.Quote(st.Name()))
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
|
||||
_, _ = bufio.NewReader(f).WriteTo(c.Writer)
|
||||
bufio.NewReader(f).WriteTo(c.Writer)
|
||||
}
|
||||
|
||||
// Handles downloading a specific file for a server.
|
||||
@@ -64,7 +65,7 @@ func getDownloadFile(c *gin.Context) {
|
||||
manager := middleware.ExtractManager(c)
|
||||
token := tokens.FilePayload{}
|
||||
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewTrackedError(err).Abort(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -81,7 +82,7 @@ func getDownloadFile(c *gin.Context) {
|
||||
// If there is an error or we're somehow trying to download a directory, just
|
||||
// respond with the appropriate error.
|
||||
if err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewServerError(err, s).Abort(c)
|
||||
return
|
||||
} else if st.IsDir() {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
||||
@@ -92,14 +93,13 @@ func getDownloadFile(c *gin.Context) {
|
||||
|
||||
f, err := os.Open(p)
|
||||
if err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewServerError(err, s).Abort(c)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
c.Header("Content-Length", strconv.Itoa(int(st.Size())))
|
||||
c.Header("Content-Disposition", "attachment; filename="+strconv.Quote(st.Name()))
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
|
||||
_, _ = bufio.NewReader(f).WriteTo(c.Writer)
|
||||
bufio.NewReader(f).WriteTo(c.Writer)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func getServerLogs(c *gin.Context) {
|
||||
|
||||
out, err := s.ReadLogfile(l)
|
||||
if err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewServerError(err, s).Abort(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ func postServerCommands(c *gin.Context) {
|
||||
s := ExtractServer(c)
|
||||
|
||||
if running, err := s.Environment.IsRunning(c.Request.Context()); err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewServerError(err, s).Abort(c)
|
||||
return
|
||||
} else if !running {
|
||||
c.AbortWithStatusJSON(http.StatusBadGateway, gin.H{
|
||||
@@ -143,7 +143,7 @@ func postServerSync(c *gin.Context) {
|
||||
s := ExtractServer(c)
|
||||
|
||||
if err := s.Sync(); err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
WithError(c, err)
|
||||
} else {
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
@@ -153,15 +153,9 @@ func postServerSync(c *gin.Context) {
|
||||
func postServerInstall(c *gin.Context) {
|
||||
s := ExtractServer(c)
|
||||
|
||||
go func(s *server.Server) {
|
||||
s.Log().Info("syncing server state with remote source before executing installation process")
|
||||
if err := s.Sync(); err != nil {
|
||||
s.Log().WithField("error", err).Error("failed to sync server state with Panel")
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.Install(); err != nil {
|
||||
s.Log().WithField("error", err).Error("failed to execute server installation process")
|
||||
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)
|
||||
|
||||
@@ -217,7 +211,7 @@ func deleteServer(c *gin.Context) {
|
||||
// forcibly terminate it before removing the container, so we do not need to handle
|
||||
// that here.
|
||||
if err := s.Environment.Destroy(); err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
_ = WithError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ func getServerListDirectory(c *gin.Context) {
|
||||
s := ExtractServer(c)
|
||||
dir := c.Query("directory")
|
||||
if stats, err := s.Filesystem().ListDirectory(dir); err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
WithError(c, err)
|
||||
} else {
|
||||
c.JSON(http.StatusOK, stats)
|
||||
}
|
||||
@@ -152,7 +152,7 @@ func putServerRenameFiles(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewServerError(err, s).AbortFilesystemError(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -172,11 +172,11 @@ func postServerCopyFile(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := s.Filesystem().IsIgnored(data.Location); err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewServerError(err, s).Abort(c)
|
||||
return
|
||||
}
|
||||
if err := s.Filesystem().Copy(data.Location); err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewServerError(err, s).AbortFilesystemError(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ func postServerDeleteFiles(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewServerError(err, s).Abort(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -236,7 +236,7 @@ func postServerWriteFile(c *gin.Context) {
|
||||
f = "/" + strings.TrimLeft(f, "/")
|
||||
|
||||
if err := s.Filesystem().IsIgnored(f); err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewServerError(err, s).Abort(c)
|
||||
return
|
||||
}
|
||||
if err := s.Filesystem().Writefile(f, c.Request.Body); err != nil {
|
||||
@@ -247,7 +247,7 @@ func postServerWriteFile(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewServerError(err, s).AbortFilesystemError(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -294,12 +294,12 @@ func postServerPullRemoteFile(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
WithError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.Filesystem().HasSpaceErr(true); err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
WithError(c, err)
|
||||
return
|
||||
}
|
||||
// Do not allow more than three simultaneous remote file downloads at one time.
|
||||
@@ -338,13 +338,13 @@ func postServerPullRemoteFile(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := download(); err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewServerError(err, s).Abort(c)
|
||||
return
|
||||
}
|
||||
|
||||
st, err := s.Filesystem().Stat(dl.Path())
|
||||
if err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewServerError(err, s).AbortFilesystemError(c)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, &st)
|
||||
@@ -380,7 +380,7 @@ func postServerCreateDirectory(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewServerError(err, s).Abort(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -415,7 +415,7 @@ func postServerCompressFiles(c *gin.Context) {
|
||||
|
||||
f, err := s.Filesystem().CompressFiles(data.RootPath, data.Files)
|
||||
if err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewServerError(err, s).AbortFilesystemError(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -533,7 +533,7 @@ func postServerChmodFile(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewServerError(err, s).AbortFilesystemError(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -545,7 +545,7 @@ func postServerUploadFiles(c *gin.Context) {
|
||||
|
||||
token := tokens.UploadPayload{}
|
||||
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewTrackedError(err).Abort(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -591,14 +591,14 @@ func postServerUploadFiles(c *gin.Context) {
|
||||
for _, header := range headers {
|
||||
p, err := s.Filesystem().SafePath(filepath.Join(directory, header.Filename))
|
||||
if err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewServerError(err, s).Abort(c)
|
||||
return
|
||||
}
|
||||
|
||||
// We run this in a different method so I can use defer without any of
|
||||
// the consequences caused by calling it in a loop.
|
||||
if err := handleFileUpload(p, s, header); err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewServerError(err, s).Abort(c)
|
||||
return
|
||||
} else {
|
||||
s.SaveActivity(s.NewRequestActivity(token.UserUuid, c.ClientIP()), server.ActivityFileUploaded, models.ActivityMeta{
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"emperror.dev/errors"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
@@ -64,11 +63,11 @@ func postServerTransfer(c *gin.Context) {
|
||||
if s.Environment.State() != environment.ProcessOfflineState {
|
||||
if err := s.Environment.WaitForStop(
|
||||
s.Context(),
|
||||
time.Second*15,
|
||||
time.Minute,
|
||||
false,
|
||||
); err != nil && !strings.Contains(strings.ToLower(err.Error()), "no such container") {
|
||||
s.SetTransferring(false)
|
||||
middleware.CaptureAndAbort(c, errors.Wrap(err, "failed to stop server for transfer"))
|
||||
notifyPanelOfFailure()
|
||||
s.Log().WithError(err).Error("failed to stop server for transfer")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func getServerWebsocket(c *gin.Context) {
|
||||
|
||||
handler, err := websocket.GetHandler(s, c.Writer, c.Request, c)
|
||||
if err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewServerError(err, s).Abort(c)
|
||||
return
|
||||
}
|
||||
defer handler.Connection.Close()
|
||||
@@ -58,7 +58,7 @@ func getServerWebsocket(c *gin.Context) {
|
||||
case <-ctx.Done():
|
||||
break
|
||||
case <-s.Context().Done():
|
||||
_ = handler.Connection.WriteControl(ws.CloseMessage, ws.FormatCloseMessage(ws.CloseGoingAway, "server deleted"), time.Now().Add(time.Second*5))
|
||||
handler.Connection.WriteControl(ws.CloseMessage, ws.FormatCloseMessage(ws.CloseGoingAway, "server deleted"), time.Now().Add(time.Second*5))
|
||||
break
|
||||
}
|
||||
}()
|
||||
@@ -83,7 +83,7 @@ func getServerWebsocket(c *gin.Context) {
|
||||
|
||||
go func(msg websocket.Message) {
|
||||
if err := handler.HandleInbound(ctx, msg); err != nil {
|
||||
_ = handler.SendErrorJson(msg, err)
|
||||
handler.SendErrorJson(msg, err)
|
||||
}
|
||||
}(j)
|
||||
}
|
||||
|
||||
@@ -20,28 +20,12 @@ import (
|
||||
func getSystemInformation(c *gin.Context) {
|
||||
i, err := system.GetSystemInformation()
|
||||
if err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewTrackedError(err).Abort(c)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if c.Query("v") == "2" {
|
||||
c.JSON(http.StatusOK, i)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, struct {
|
||||
Architecture string `json:"architecture"`
|
||||
CPUCount int `json:"cpu_count"`
|
||||
KernelVersion string `json:"kernel_version"`
|
||||
OS string `json:"os"`
|
||||
Version string `json:"version"`
|
||||
}{
|
||||
Architecture: i.System.Architecture,
|
||||
CPUCount: i.System.CPUThreads,
|
||||
KernelVersion: i.System.KernelVersion,
|
||||
OS: i.System.OSType,
|
||||
Version: i.Version,
|
||||
})
|
||||
c.JSON(http.StatusOK, i)
|
||||
}
|
||||
|
||||
// Returns all the servers that are registered and configured correctly on
|
||||
@@ -91,7 +75,7 @@ func postCreateServer(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := i.Server().Install(); err != nil {
|
||||
if err := i.Server().Install(false); err != nil {
|
||||
log.WithFields(log.Fields{"server": i.Server().ID(), "error": err}).Error("failed to run install process for server")
|
||||
return
|
||||
}
|
||||
@@ -133,7 +117,7 @@ func postUpdateConfiguration(c *gin.Context) {
|
||||
// Try to write this new configuration to the disk before updating our global
|
||||
// state with it.
|
||||
if err := config.WriteToDisk(cfg); err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
_ = WithError(c, err)
|
||||
return
|
||||
}
|
||||
// Since we wrote it to the disk successfully now update the global configuration
|
||||
|
||||
@@ -38,14 +38,14 @@ func postTransfers(c *gin.Context) {
|
||||
|
||||
token := tokens.TransferPayload{}
|
||||
if err := tokens.ParseToken([]byte(auth[1]), &token); err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewTrackedError(err).Abort(c)
|
||||
return
|
||||
}
|
||||
|
||||
manager := middleware.ExtractManager(c)
|
||||
u, err := uuid.Parse(token.Subject)
|
||||
if err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewTrackedError(err).Abort(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ func postTransfers(c *gin.Context) {
|
||||
if err := manager.Client().SetTransferStatus(context.Background(), trnsfr.Server.ID(), false); err != nil {
|
||||
trnsfr.Log().WithField("status", false).WithError(err).Error("failed to set transfer status")
|
||||
}
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewTrackedError(err).Abort(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -123,13 +123,13 @@ func postTransfers(c *gin.Context) {
|
||||
mediaType, params, err := mime.ParseMediaType(c.GetHeader("Content-Type"))
|
||||
if err != nil {
|
||||
trnsfr.Log().Debug("failed to parse content type header")
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewTrackedError(err).Abort(c)
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(mediaType, "multipart/") {
|
||||
trnsfr.Log().Debug("invalid content type")
|
||||
middleware.CaptureAndAbort(c, fmt.Errorf("invalid content type \"%s\", expected \"multipart/form-data\"", mediaType))
|
||||
NewTrackedError(fmt.Errorf("invalid content type \"%s\", expected \"multipart/form-data\"", mediaType)).Abort(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ out:
|
||||
break out
|
||||
}
|
||||
if err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewTrackedError(err).Abort(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -166,13 +166,13 @@ out:
|
||||
trnsfr.Log().Debug("received archive")
|
||||
|
||||
if err := trnsfr.Server.EnsureDataDirectoryExists(); err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewTrackedError(err).Abort(c)
|
||||
return
|
||||
}
|
||||
|
||||
tee := io.TeeReader(p, h)
|
||||
if err := trnsfr.Server.Filesystem().ExtractStreamUnsafe(ctx, "/", tee); err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewTrackedError(err).Abort(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ out:
|
||||
trnsfr.Log().Debug("received checksum")
|
||||
|
||||
if !hasArchive {
|
||||
middleware.CaptureAndAbort(c, errors.New("archive must be sent before the checksum"))
|
||||
NewTrackedError(errors.New("archive must be sent before the checksum")).Abort(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -189,14 +189,14 @@ out:
|
||||
|
||||
v, err := io.ReadAll(p)
|
||||
if err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewTrackedError(err).Abort(c)
|
||||
return
|
||||
}
|
||||
|
||||
expected := make([]byte, hex.DecodedLen(len(v)))
|
||||
n, err := hex.Decode(expected, v)
|
||||
if err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewTrackedError(err).Abort(c)
|
||||
return
|
||||
}
|
||||
actual := h.Sum(nil)
|
||||
@@ -207,7 +207,7 @@ out:
|
||||
}).Debug("checksums")
|
||||
|
||||
if !bytes.Equal(expected[:n], actual) {
|
||||
middleware.CaptureAndAbort(c, errors.New("checksums don't match"))
|
||||
NewTrackedError(errors.New("checksums don't match")).Abort(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -220,12 +220,12 @@ out:
|
||||
}
|
||||
|
||||
if !hasArchive || !hasChecksum {
|
||||
middleware.CaptureAndAbort(c, errors.New("missing archive or checksum"))
|
||||
NewTrackedError(errors.New("missing archive or checksum")).Abort(c)
|
||||
return
|
||||
}
|
||||
|
||||
if !checksumVerified {
|
||||
middleware.CaptureAndAbort(c, errors.New("checksums don't match"))
|
||||
NewTrackedError(errors.New("checksums don't match")).Abort(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ out:
|
||||
|
||||
// Ensure the server environment gets configured.
|
||||
if err := trnsfr.Server.CreateEnvironment(); err != nil {
|
||||
middleware.CaptureAndAbort(c, err)
|
||||
NewTrackedError(err).Abort(c)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,6 @@ func (b *LocalBackup) Restore(ctx context.Context, _ io.Reader, callback Restore
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var reader io.Reader = f
|
||||
// Steal the logic we use for making backups which will be applied when restoring
|
||||
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"emperror.dev/errors"
|
||||
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
)
|
||||
@@ -59,7 +57,7 @@ func (s *Server) handleServerCrash() error {
|
||||
|
||||
exitCode, oomKilled, err := s.Environment.ExitState()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get exit state for server process")
|
||||
return err
|
||||
}
|
||||
|
||||
// If the system is not configured to detect a clean exit code as a crash, and the
|
||||
@@ -87,5 +85,5 @@ func (s *Server) handleServerCrash() error {
|
||||
|
||||
s.crasher.SetLastCrash(time.Now())
|
||||
|
||||
return errors.Wrap(s.HandlePowerAction(PowerActionStart), "failed to start server after crash detection")
|
||||
return s.HandlePowerAction(PowerActionStart)
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ func (fs *Filesystem) DecompressFileUnsafe(ctx context.Context, dir string, file
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
// TODO: defer file close?
|
||||
|
||||
// Identify the type of archive we are dealing with.
|
||||
format, input, err := archiver.Identify(filepath.Base(file), f)
|
||||
|
||||
@@ -183,7 +183,7 @@ func (fs *Filesystem) DirectorySize(dir string) (int64, error) {
|
||||
}
|
||||
|
||||
if !e.IsDir() {
|
||||
_ = syscall.Lstat(p, &st)
|
||||
syscall.Lstat(p, &st)
|
||||
atomic.AddInt64(&size, st.Size)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ const (
|
||||
ErrCodePathResolution ErrorCode = "E_BADPATH"
|
||||
ErrCodeDenylistFile ErrorCode = "E_DENYLIST"
|
||||
ErrCodeUnknownError ErrorCode = "E_UNKNOWN"
|
||||
ErrNotExist ErrorCode = "E_NOTEXIST"
|
||||
)
|
||||
|
||||
type Error struct {
|
||||
@@ -69,8 +68,6 @@ func (e *Error) Error() string {
|
||||
r = "<empty>"
|
||||
}
|
||||
return fmt.Sprintf("filesystem: server path [%s] resolves to a location outside the server root: %s", e.path, r)
|
||||
case ErrNotExist:
|
||||
return "filesystem: does not exist"
|
||||
case ErrCodeUnknownError:
|
||||
fallthrough
|
||||
default:
|
||||
|
||||
@@ -61,28 +61,25 @@ func (fs *Filesystem) Path() string {
|
||||
func (fs *Filesystem) File(p string) (*os.File, Stat, error) {
|
||||
cleaned, err := fs.SafePath(p)
|
||||
if err != nil {
|
||||
return nil, Stat{}, errors.WithStackIf(err)
|
||||
return nil, Stat{}, err
|
||||
}
|
||||
st, err := fs.Stat(cleaned)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, Stat{}, newFilesystemError(ErrNotExist, err)
|
||||
}
|
||||
return nil, Stat{}, errors.WithStackIf(err)
|
||||
return nil, Stat{}, err
|
||||
}
|
||||
if st.IsDir() {
|
||||
return nil, Stat{}, newFilesystemError(ErrCodeIsDirectory, nil)
|
||||
}
|
||||
f, err := os.Open(cleaned)
|
||||
if err != nil {
|
||||
return nil, Stat{}, errors.WithStackIf(err)
|
||||
return nil, Stat{}, err
|
||||
}
|
||||
return f, st, nil
|
||||
}
|
||||
|
||||
// Touch acts by creating the given file and path on the disk if it is not present
|
||||
// already. If it is present, the file is opened using the defaults which will truncate
|
||||
// the contents. The opened file is then returned to the caller.
|
||||
// Acts by creating the given file and path on the disk if it is not present already. If
|
||||
// it is present, the file is opened using the defaults which will truncate the contents.
|
||||
// The opened file is then returned to the caller.
|
||||
func (fs *Filesystem) Touch(p string, flag int) (*os.File, error) {
|
||||
cleaned, err := fs.SafePath(p)
|
||||
if err != nil {
|
||||
@@ -92,9 +89,6 @@ func (fs *Filesystem) Touch(p string, flag int) (*os.File, error) {
|
||||
if err == nil {
|
||||
return f, nil
|
||||
}
|
||||
if f != nil {
|
||||
_ = f.Close()
|
||||
}
|
||||
// If the error is not because it doesn't exist then we just need to bail at this point.
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, errors.Wrap(err, "server/filesystem: touch: failed to open file handle")
|
||||
@@ -165,7 +159,7 @@ func (fs *Filesystem) Writefile(p string, r io.Reader) error {
|
||||
// Adjust the disk usage to account for the old size and the new size of the file.
|
||||
fs.addDisk(sz - currentSize)
|
||||
|
||||
return fs.unsafeChown(cleaned)
|
||||
return fs.Chown(cleaned)
|
||||
}
|
||||
|
||||
// Creates a new directory (name) at a specified path (p) for the server.
|
||||
@@ -223,12 +217,7 @@ func (fs *Filesystem) Chown(path string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fs.unsafeChown(cleaned)
|
||||
}
|
||||
|
||||
// unsafeChown chowns the given path, without checking if the path is safe. This should only be used
|
||||
// when the path has already been checked.
|
||||
func (fs *Filesystem) unsafeChown(path string) error {
|
||||
if fs.isTest {
|
||||
return nil
|
||||
}
|
||||
@@ -237,19 +226,19 @@ func (fs *Filesystem) unsafeChown(path string) error {
|
||||
gid := config.Get().System.User.Gid
|
||||
|
||||
// Start by just chowning the initial path that we received.
|
||||
if err := os.Chown(path, uid, gid); err != nil {
|
||||
if err := os.Chown(cleaned, uid, gid); err != nil {
|
||||
return errors.Wrap(err, "server/filesystem: chown: failed to chown path")
|
||||
}
|
||||
|
||||
// If this is not a directory we can now return from the function, there is nothing
|
||||
// left that we need to do.
|
||||
if st, err := os.Stat(path); err != nil || !st.IsDir() {
|
||||
if st, err := os.Stat(cleaned); err != nil || !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.
|
||||
err := godirwalk.Walk(path, &godirwalk.Options{
|
||||
err = godirwalk.Walk(cleaned, &godirwalk.Options{
|
||||
Unsorted: true,
|
||||
Callback: func(p string, e *godirwalk.Dirent) error {
|
||||
// Do not attempt to chown a symlink. Go's os.Chown function will affect the symlink
|
||||
@@ -266,6 +255,7 @@ func (fs *Filesystem) unsafeChown(path string) error {
|
||||
return os.Chown(p, uid, gid)
|
||||
},
|
||||
})
|
||||
|
||||
return errors.Wrap(err, "server/filesystem: chown: failed to chown during walk function")
|
||||
}
|
||||
|
||||
|
||||
@@ -84,35 +84,6 @@ func (rfs *rootFs) reset() {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilesystem_Openfile(t *testing.T) {
|
||||
g := Goblin(t)
|
||||
fs, rfs := NewFs()
|
||||
|
||||
g.Describe("File", func() {
|
||||
g.It("returns custom error when file does not exist", func() {
|
||||
_, _, err := fs.File("foo/bar.txt")
|
||||
|
||||
g.Assert(err).IsNotNil()
|
||||
g.Assert(IsErrorCode(err, ErrNotExist)).IsTrue()
|
||||
})
|
||||
|
||||
g.It("returns file stat information", func() {
|
||||
_ = rfs.CreateServerFile("foo.txt", []byte("hello world"))
|
||||
|
||||
f, st, err := fs.File("foo.txt")
|
||||
g.Assert(err).IsNil()
|
||||
|
||||
g.Assert(st.Name()).Equal("foo.txt")
|
||||
g.Assert(f).IsNotNil()
|
||||
_ = f.Close()
|
||||
})
|
||||
|
||||
g.AfterEach(func() {
|
||||
rfs.reset()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilesystem_Writefile(t *testing.T) {
|
||||
g := Goblin(t)
|
||||
fs, rfs := NewFs()
|
||||
|
||||
@@ -2,7 +2,6 @@ package filesystem
|
||||
|
||||
import (
|
||||
"context"
|
||||
iofs "io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -34,6 +33,8 @@ func (fs *Filesystem) IsIgnored(paths ...string) error {
|
||||
// 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)
|
||||
|
||||
@@ -43,24 +44,47 @@ func (fs *Filesystem) SafePath(p string) (string, error) {
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return "", errors.Wrap(err, "server/filesystem: failed to evaluate symlink")
|
||||
} else if os.IsNotExist(err) {
|
||||
// The target of one of the symlinks (EvalSymlinks is recursive) does not exist.
|
||||
// So we get what target path does not exist and check if it's within the data
|
||||
// directory. If it is, we return the original path, otherwise we return an error.
|
||||
pErr, ok := err.(*iofs.PathError)
|
||||
if !ok {
|
||||
return "", errors.Wrap(err, "server/filesystem: failed to evaluate symlink")
|
||||
// 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
|
||||
}
|
||||
}
|
||||
ep = pErr.Path
|
||||
}
|
||||
|
||||
// If the new path doesn't start with their root directory there is clearly an escape
|
||||
// attempt going on, and we should NOT resolve this path for them.
|
||||
if nonExistentPathResolution != "" {
|
||||
if !fs.unsafeIsInDataDirectory(nonExistentPathResolution) {
|
||||
return "", NewBadPathResolution(p, nonExistentPathResolution)
|
||||
}
|
||||
|
||||
// If the nonExistentPathResolution variable is not empty then the initial path requested
|
||||
// did not exist and we looped through the pathway until we found a match. At this point
|
||||
// we've confirmed the first matched pathway exists in the root server directory, so we
|
||||
// can go ahead and just return the path that was requested initially.
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// If the requested directory from EvalSymlinks begins with the server root directory go
|
||||
// ahead and return it. If not we'll return an error which will block any further action
|
||||
// on the file.
|
||||
if fs.unsafeIsInDataDirectory(ep) {
|
||||
// Returning the original path here instead of the resolved path ensures that
|
||||
// whatever the user is trying to do will work as expected. If we returned the
|
||||
// resolved path, the user would be unable to know that it is in fact a symlink.
|
||||
return r, nil
|
||||
return ep, nil
|
||||
}
|
||||
|
||||
return "", NewBadPathResolution(p, r)
|
||||
|
||||
@@ -115,14 +115,6 @@ func TestFilesystem_Blocks_Symlinks(t *testing.T) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := os.Symlink(filepath.Join(rfs.root, "malicious_does_not_exist.txt"), filepath.Join(rfs.root, "/server/symlinked_does_not_exist.txt")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := os.Symlink(filepath.Join(rfs.root, "/server/symlinked_does_not_exist.txt"), filepath.Join(rfs.root, "/server/symlinked_does_not_exist2.txt")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := os.Symlink(filepath.Join(rfs.root, "/malicious_dir"), filepath.Join(rfs.root, "/server/external_dir")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -136,22 +128,6 @@ func TestFilesystem_Blocks_Symlinks(t *testing.T) {
|
||||
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
|
||||
})
|
||||
|
||||
g.It("cannot write to a non-existent file symlinked outside the root", func() {
|
||||
r := bytes.NewReader([]byte("testing what the fuck"))
|
||||
|
||||
err := fs.Writefile("symlinked_does_not_exist.txt", r)
|
||||
g.Assert(err).IsNotNil()
|
||||
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
|
||||
})
|
||||
|
||||
g.It("cannot write to chained symlinks with target that does not exist outside the root", func() {
|
||||
r := bytes.NewReader([]byte("testing what the fuck"))
|
||||
|
||||
err := fs.Writefile("symlinked_does_not_exist2.txt", r)
|
||||
g.Assert(err).IsNotNil()
|
||||
g.Assert(IsErrorCode(err, ErrCodePathResolution)).IsTrue()
|
||||
})
|
||||
|
||||
g.It("cannot write a file to a directory symlinked outside the root", func() {
|
||||
r := bytes.NewReader([]byte("testing"))
|
||||
|
||||
|
||||
@@ -32,17 +32,19 @@ import (
|
||||
//
|
||||
// Pass true as the first argument in order to execute a server sync before the
|
||||
// process to ensure the latest information is used.
|
||||
func (s *Server) Install() error {
|
||||
return s.install(false)
|
||||
}
|
||||
func (s *Server) Install(sync bool) error {
|
||||
if sync {
|
||||
s.Log().Info("syncing server state with remote source before executing installation process")
|
||||
if err := s.Sync(); err != nil {
|
||||
return errors.WrapIf(err, "install: failed to sync server state with Panel")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) install(reinstall bool) error {
|
||||
var err error
|
||||
if !s.Config().SkipEggScripts {
|
||||
// Send the start event so the Panel can automatically update. We don't
|
||||
// send this unless the process is actually going to run, otherwise all
|
||||
// sorts of weird rapid UI behavior happens since there isn't an actual
|
||||
// install process being executed.
|
||||
// Send the start event so the Panel can automatically update. We don't send this unless the process
|
||||
// is actually going to run, otherwise all sorts of weird rapid UI behavior happens since there isn't
|
||||
// an actual install process being executed.
|
||||
s.Events().Publish(InstallStartedEvent, "")
|
||||
|
||||
err = s.internalInstall()
|
||||
@@ -51,13 +53,12 @@ func (s *Server) install(reinstall bool) error {
|
||||
}
|
||||
|
||||
s.Log().WithField("was_successful", err == nil).Debug("notifying panel of server install state")
|
||||
if serr := s.SyncInstallState(err == nil, reinstall); serr != nil {
|
||||
if serr := s.SyncInstallState(err == nil); serr != nil {
|
||||
l := s.Log().WithField("was_successful", err == nil)
|
||||
|
||||
// If the request was successful but there was an error with this request,
|
||||
// attach the error to this log entry. Otherwise, ignore it in this log
|
||||
// since whatever is calling this function should handle the error and
|
||||
// will end up logging the same one.
|
||||
// If the request was successful but there was an error with this request, attach the
|
||||
// error to this log entry. Otherwise ignore it in this log since whatever is calling
|
||||
// this function should handle the error and will end up logging the same one.
|
||||
if err == nil {
|
||||
l.WithField("error", err)
|
||||
}
|
||||
@@ -65,20 +66,19 @@ func (s *Server) install(reinstall bool) error {
|
||||
l.Warn("failed to notify panel of server install state")
|
||||
}
|
||||
|
||||
// Ensure that the server is marked as offline at this point, otherwise you
|
||||
// end up with a blank value which is a bit confusing.
|
||||
// Ensure that the server is marked as offline at this point, otherwise you end up
|
||||
// with a blank value which is a bit confusing.
|
||||
s.Environment.SetState(environment.ProcessOfflineState)
|
||||
|
||||
// Push an event to the websocket, so we can auto-refresh the information in
|
||||
// the panel once the installation is completed.
|
||||
// Push an event to the websocket so we can auto-refresh the information in the panel once
|
||||
// the install is completed.
|
||||
s.Events().Publish(InstallCompletedEvent, "")
|
||||
|
||||
return errors.WithStackIf(err)
|
||||
}
|
||||
|
||||
// Reinstall reinstalls a server's software by utilizing the installation script
|
||||
// for the server egg. This does not touch any existing files for the server,
|
||||
// other than what the script modifies.
|
||||
// Reinstalls a server's software by utilizing the install script for the server egg. This
|
||||
// does not touch any existing files for the server, other than what the script modifies.
|
||||
func (s *Server) Reinstall() error {
|
||||
if s.Environment.State() != environment.ProcessOfflineState {
|
||||
s.Log().Debug("waiting for server instance to enter a stopped state")
|
||||
@@ -87,12 +87,7 @@ func (s *Server) Reinstall() error {
|
||||
}
|
||||
}
|
||||
|
||||
s.Log().Info("syncing server state with remote source before executing re-installation process")
|
||||
if err := s.Sync(); err != nil {
|
||||
return errors.WrapIf(err, "install: failed to sync server state with Panel")
|
||||
}
|
||||
|
||||
return s.install(true)
|
||||
return s.Install(true)
|
||||
}
|
||||
|
||||
// Internal installation function used to simplify reporting back to the Panel.
|
||||
@@ -121,9 +116,8 @@ type InstallationProcess struct {
|
||||
client *client.Client
|
||||
}
|
||||
|
||||
// NewInstallationProcess returns a new installation process struct that will be
|
||||
// used to create containers and otherwise perform installation commands for a
|
||||
// server.
|
||||
// Generates a new installation process struct that will be used to create containers,
|
||||
// and otherwise perform installation commands for a server.
|
||||
func NewInstallationProcess(s *Server, script *remote.InstallationScript) (*InstallationProcess, error) {
|
||||
proc := &InstallationProcess{
|
||||
Script: script,
|
||||
@@ -139,8 +133,8 @@ func NewInstallationProcess(s *Server, script *remote.InstallationScript) (*Inst
|
||||
return proc, nil
|
||||
}
|
||||
|
||||
// IsInstalling returns if the server is actively running the installation
|
||||
// process by checking the status of the installer lock.
|
||||
// Determines if the server is actively running the installation process by checking the status
|
||||
// of the installer lock.
|
||||
func (s *Server) IsInstalling() bool {
|
||||
return s.installing.Load()
|
||||
}
|
||||
@@ -161,7 +155,7 @@ func (s *Server) SetRestoring(state bool) {
|
||||
s.restoring.Store(state)
|
||||
}
|
||||
|
||||
// RemoveContainer removes the installation container for the server.
|
||||
// Removes the installer container for the server.
|
||||
func (ip *InstallationProcess) RemoveContainer() error {
|
||||
err := ip.client.ContainerRemove(ip.Server.Context(), ip.Server.ID()+"_installer", types.ContainerRemoveOptions{
|
||||
RemoveVolumes: true,
|
||||
@@ -334,14 +328,14 @@ func (ip *InstallationProcess) BeforeExecute() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLogPath returns the log path for the installation process.
|
||||
// Returns the log path for the installation process.
|
||||
func (ip *InstallationProcess) GetLogPath() string {
|
||||
return filepath.Join(config.Get().System.LogDirectory, "/install", ip.Server.ID()+".log")
|
||||
}
|
||||
|
||||
// AfterExecute cleans up after the execution of the installation process.
|
||||
// This grabs the logs from the process to store in the server configuration
|
||||
// directory, and then destroys the associated installation container.
|
||||
// Cleans up after the execution of the installation process. This grabs the logs from the
|
||||
// process to store in the server configuration directory, and then destroys the associated
|
||||
// installation container.
|
||||
func (ip *InstallationProcess) AfterExecute(containerId string) error {
|
||||
defer ip.RemoveContainer()
|
||||
|
||||
@@ -531,7 +525,7 @@ func (ip *InstallationProcess) StreamOutput(ctx context.Context, id string) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
// resourceLimits returns resource limits for the installation container. This
|
||||
// resourceLimits returns the install container specific resource limits. This
|
||||
// looks at the globally defined install container limits and attempts to use
|
||||
// the higher of the two (defined limits & server limits). This allows for servers
|
||||
// with super low limits (e.g. Discord bots with 128Mb of memory) to perform more
|
||||
@@ -543,8 +537,8 @@ func (ip *InstallationProcess) StreamOutput(ctx context.Context, id string) erro
|
||||
func (ip *InstallationProcess) resourceLimits() container.Resources {
|
||||
limits := config.Get().Docker.InstallerLimits
|
||||
|
||||
// Create a copy of the configuration, so we're not accidentally making
|
||||
// changes to the underlying server build data.
|
||||
// Create a copy of the configuration so we're not accidentally making changes
|
||||
// to the underlying server build data.
|
||||
c := *ip.Server.Config()
|
||||
cfg := c.Build
|
||||
if cfg.MemoryLimit < limits.Memory {
|
||||
@@ -568,12 +562,10 @@ func (ip *InstallationProcess) resourceLimits() container.Resources {
|
||||
return resources
|
||||
}
|
||||
|
||||
// SyncInstallState makes an HTTP request to the Panel instance notifying it that
|
||||
// SyncInstallState makes a HTTP request to the Panel instance notifying it that
|
||||
// the server has completed the installation process, and what the state of the
|
||||
// server is.
|
||||
func (s *Server) SyncInstallState(successful, reinstall bool) error {
|
||||
return s.client.SetInstallationStatus(s.Context(), s.ID(), remote.InstallStatusRequest{
|
||||
Successful: successful,
|
||||
Reinstall: reinstall,
|
||||
})
|
||||
// server is. A boolean value of "true" means everything was successful, "false"
|
||||
// means something went wrong and the server must be deleted and re-created.
|
||||
func (s *Server) SyncInstallState(successful bool) error {
|
||||
return s.client.SetInstallationStatus(s.Context(), s.ID(), successful)
|
||||
}
|
||||
|
||||
@@ -147,7 +147,6 @@ func (s *Server) Context() context.Context {
|
||||
// server instance.
|
||||
func (s *Server) GetEnvironmentVariables() []string {
|
||||
out := []string{
|
||||
// TODO: allow this to be overridden by the user.
|
||||
fmt.Sprintf("TZ=%s", config.Get().System.Timezone),
|
||||
fmt.Sprintf("STARTUP=%s", s.Config().Invocation),
|
||||
fmt.Sprintf("SERVER_MEMORY=%d", s.MemoryLimit()),
|
||||
|
||||
@@ -128,13 +128,6 @@ func (t *Transfer) PushArchiveToTarget(url, token string) ([]byte, error) {
|
||||
t.Log().Debug("sending archive to destination")
|
||||
client := http.Client{Timeout: 0}
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Log().Debug("error while sending archive to destination")
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("unexpected status code from destination: %d", res.StatusCode)
|
||||
}
|
||||
t.Log().Debug("waiting for stream to complete")
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
||||
@@ -68,21 +68,6 @@ func (c *SFTPServer) Run() error {
|
||||
}
|
||||
|
||||
conf := &ssh.ServerConfig{
|
||||
Config: ssh.Config{
|
||||
KeyExchanges: []string{
|
||||
"curve25519-sha256", "curve25519-sha256@libssh.org",
|
||||
"ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521",
|
||||
"diffie-hellman-group14-sha256",
|
||||
},
|
||||
Ciphers: []string{
|
||||
"aes128-gcm@openssh.com",
|
||||
"chacha20-poly1305@openssh.com",
|
||||
"aes128-ctr", "aes192-ctr", "aes256-ctr",
|
||||
},
|
||||
MACs: []string{
|
||||
"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256",
|
||||
},
|
||||
},
|
||||
NoClientAuth: false,
|
||||
MaxAuthTries: 6,
|
||||
PasswordCallback: func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
package system
|
||||
|
||||
var Version = "develop"
|
||||
var Version = "1.11.0-rc.1"
|
||||
|
||||
29
system/strings.go
Normal file
29
system/strings.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var ipTrimRegex = regexp.MustCompile(`(:\d*)?$`)
|
||||
|
||||
const characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
|
||||
|
||||
// RandomString generates a random string of alpha-numeric characters using a
|
||||
// pseudo-random number generator. The output of this function IS NOT cryptographically
|
||||
// secure, it is used solely for generating random strings outside a security context.
|
||||
func RandomString(n int) string {
|
||||
var b strings.Builder
|
||||
b.Grow(n)
|
||||
for i := 0; i < n; i++ {
|
||||
b.WriteByte(characters[rand.Intn(len(characters))])
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// TrimIPSuffix removes the internal port value from an IP address to ensure we're only
|
||||
// ever working directly with the IP address.
|
||||
func TrimIPSuffix(s string) string {
|
||||
return ipTrimRegex.ReplaceAllString(s, "")
|
||||
}
|
||||
131
system/system.go
131
system/system.go
@@ -1,57 +1,17 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
|
||||
"github.com/acobaugh/osrelease"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/parsers/kernel"
|
||||
)
|
||||
|
||||
type Information struct {
|
||||
Version string `json:"version"`
|
||||
Docker DockerInformation `json:"docker"`
|
||||
System System `json:"system"`
|
||||
}
|
||||
|
||||
type DockerInformation struct {
|
||||
Version string `json:"version"`
|
||||
Cgroups DockerCgroups `json:"cgroups"`
|
||||
Containers DockerContainers `json:"containers"`
|
||||
Storage DockerStorage `json:"storage"`
|
||||
Runc DockerRunc `json:"runc"`
|
||||
}
|
||||
|
||||
type DockerCgroups struct {
|
||||
Driver string `json:"driver"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type DockerContainers struct {
|
||||
Total int `json:"total"`
|
||||
Running int `json:"running"`
|
||||
Paused int `json:"paused"`
|
||||
Stopped int `json:"stopped"`
|
||||
}
|
||||
|
||||
type DockerStorage struct {
|
||||
Driver string `json:"driver"`
|
||||
Filesystem string `json:"filesystem"`
|
||||
}
|
||||
|
||||
type DockerRunc struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type System struct {
|
||||
Architecture string `json:"architecture"`
|
||||
CPUThreads int `json:"cpu_threads"`
|
||||
MemoryBytes int64 `json:"memory_bytes"`
|
||||
Version string `json:"version"`
|
||||
KernelVersion string `json:"kernel_version"`
|
||||
Architecture string `json:"architecture"`
|
||||
OS string `json:"os"`
|
||||
OSType string `json:"os_type"`
|
||||
CpuCount int `json:"cpu_count"`
|
||||
}
|
||||
|
||||
func GetSystemInformation() (*Information, error) {
|
||||
@@ -60,84 +20,13 @@ func GetSystemInformation() (*Information, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
version, info, err := GetDockerInfo(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
s := &Information{
|
||||
Version: Version,
|
||||
KernelVersion: k.String(),
|
||||
Architecture: runtime.GOARCH,
|
||||
OS: runtime.GOOS,
|
||||
CpuCount: runtime.NumCPU(),
|
||||
}
|
||||
|
||||
release, err := osrelease.Read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var os string
|
||||
if release["PRETTY_NAME"] != "" {
|
||||
os = release["PRETTY_NAME"]
|
||||
} else if release["NAME"] != "" {
|
||||
os = release["NAME"]
|
||||
} else {
|
||||
os = info.OperatingSystem
|
||||
}
|
||||
|
||||
var filesystem string
|
||||
for _, v := range info.DriverStatus {
|
||||
if v[0] != "Backing Filesystem" {
|
||||
continue
|
||||
}
|
||||
filesystem = v[1]
|
||||
break
|
||||
}
|
||||
|
||||
return &Information{
|
||||
Version: Version,
|
||||
Docker: DockerInformation{
|
||||
Version: version.Version,
|
||||
Cgroups: DockerCgroups{
|
||||
Driver: info.CgroupDriver,
|
||||
Version: info.CgroupVersion,
|
||||
},
|
||||
Containers: DockerContainers{
|
||||
Total: info.Containers,
|
||||
Running: info.ContainersRunning,
|
||||
Paused: info.ContainersPaused,
|
||||
Stopped: info.ContainersStopped,
|
||||
},
|
||||
Storage: DockerStorage{
|
||||
Driver: info.Driver,
|
||||
Filesystem: filesystem,
|
||||
},
|
||||
Runc: DockerRunc{
|
||||
Version: info.RuncCommit.ID,
|
||||
},
|
||||
},
|
||||
System: System{
|
||||
Architecture: runtime.GOARCH,
|
||||
CPUThreads: runtime.NumCPU(),
|
||||
MemoryBytes: info.MemTotal,
|
||||
KernelVersion: k.String(),
|
||||
OS: os,
|
||||
OSType: runtime.GOOS,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetDockerInfo(ctx context.Context) (types.Version, types.Info, error) {
|
||||
// TODO: find a way to re-use the client from the docker environment.
|
||||
c, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return types.Version{}, types.Info{}, err
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
dockerVersion, err := c.ServerVersion(ctx)
|
||||
if err != nil {
|
||||
return types.Version{}, types.Info{}, err
|
||||
}
|
||||
|
||||
dockerInfo, err := c.Info(ctx)
|
||||
if err != nil {
|
||||
return types.Version{}, types.Info{}, err
|
||||
}
|
||||
|
||||
return dockerVersion, dockerInfo, nil
|
||||
return s, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user