Compare commits
13 Commits
v1.11.0-rc
...
release/v1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1304e03a06 | ||
|
|
e4d790ea40 | ||
|
|
5641e45059 | ||
|
|
9a718b699f | ||
|
|
92efdb1981 | ||
|
|
43227bf24d | ||
|
|
105f0150f6 | ||
|
|
aeec51632e | ||
|
|
ff50d0e5bd | ||
|
|
9226ccae31 | ||
|
|
2fd0edbff9 | ||
|
|
1457470fff | ||
|
|
da94f750ad |
32
.github/workflows/docker.yaml
vendored
32
.github/workflows/docker.yaml
vendored
@@ -4,8 +4,9 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- develop
|
- develop
|
||||||
tags:
|
release:
|
||||||
- "v*"
|
types:
|
||||||
|
- published
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push:
|
build-and-push:
|
||||||
@@ -13,18 +14,21 @@ jobs:
|
|||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
# Always run against a tag, even if the commit into the tag has [docker skip] within the commit message.
|
# 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'))"
|
if: "!contains(github.ref, 'develop') || (!contains(github.event.head_commit.message, 'skip docker') && !contains(github.event.head_commit.message, 'docker skip'))"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Code Checkout
|
- name: Code checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Docker Meta
|
- name: Docker Meta
|
||||||
id: docker_meta
|
id: docker_meta
|
||||||
uses: crazy-max/ghaction-docker-meta@v1
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/pterodactyl/wings
|
images: ghcr.io/pterodactyl/wings
|
||||||
|
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: Set up QEMU
|
- name: Setup QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
- name: Install buildx
|
- name: Install buildx
|
||||||
@@ -40,12 +44,12 @@ jobs:
|
|||||||
- name: Get Build Information
|
- name: Get Build Information
|
||||||
id: build_info
|
id: build_info
|
||||||
run: |
|
run: |
|
||||||
echo "::set-output name=version_tag::${GITHUB_REF/refs\/tags\/v/}"
|
echo "version_tag=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_OUTPUT
|
||||||
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
|
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Build and push (latest)
|
- name: Build and Push (tag)
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v3
|
||||||
if: "!contains(github.ref, 'develop')"
|
if: "github.event_name == 'release' && github.event.action == 'published'"
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
@@ -56,9 +60,9 @@ jobs:
|
|||||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||||
|
|
||||||
- name: Build and push (develop)
|
- name: Build and Push (develop)
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v3
|
||||||
if: "contains(github.ref, 'develop')"
|
if: "github.event_name == 'push' && contains(github.ref, 'develop')"
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
|||||||
9
.github/workflows/release.yaml
vendored
9
.github/workflows/release.yaml
vendored
@@ -34,7 +34,6 @@ jobs:
|
|||||||
REF: ${{ github.ref }}
|
REF: ${{ github.ref }}
|
||||||
run: |
|
run: |
|
||||||
sed -n "/^## ${REF:10}/,/^## /{/^## /b;p}" CHANGELOG.md > ./RELEASE_CHANGELOG
|
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
|
- name: Create checksum and add to changelog
|
||||||
run: |
|
run: |
|
||||||
@@ -59,15 +58,15 @@ jobs:
|
|||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
id: create_release
|
id: create_release
|
||||||
uses: actions/create-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
name: ${{ github.ref }}
|
||||||
tag_name: ${{ github.ref }}
|
tag_name: ${{ github.ref }}
|
||||||
release_name: ${{ steps.extract_changelog.outputs.version_name }}
|
|
||||||
body_path: ./RELEASE_CHANGELOG
|
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: ${{ contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
|
prerelease: ${{ contains(github.ref, 'rc') || contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
|
||||||
|
body_path: ./RELEASE_CHANGELOG
|
||||||
|
|
||||||
- name: Upload amd64 binary
|
- name: Upload amd64 binary
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
|
|||||||
32
CHANGELOG.md
32
CHANGELOG.md
@@ -1,5 +1,37 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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
|
## v1.11.0-rc.1
|
||||||
### Changed
|
### Changed
|
||||||
* Wings release versions will now follow the major and minor version of the panel.
|
* Wings release versions will now follow the major and minor version of the panel.
|
||||||
|
|||||||
30
README.md
30
README.md
@@ -5,6 +5,7 @@
|
|||||||
[](https://goreportcard.com/report/github.com/pterodactyl/wings)
|
[](https://goreportcard.com/report/github.com/pterodactyl/wings)
|
||||||
|
|
||||||
# Pterodactyl Wings
|
# Pterodactyl Wings
|
||||||
|
|
||||||
Wings is Pterodactyl's server control plane, built for the rapidly changing gaming industry and designed to be
|
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
|
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.
|
instances, fetch server logs, generate backups, and control all aspects of the server lifecycle.
|
||||||
@@ -13,31 +14,32 @@ 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.
|
dependencies, and allowing users to authenticate with the same credentials they would normally use to access the Panel.
|
||||||
|
|
||||||
## Sponsors
|
## Sponsors
|
||||||
|
|
||||||
I would like to extend my sincere thanks to the following sponsors for helping find Pterodactyl's developement.
|
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)
|
[Interested in becoming a sponsor?](https://github.com/sponsors/matthewpi)
|
||||||
|
|
||||||
| Company | About |
|
| Company | About |
|
||||||
| ------- | ----- |
|
|-----------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| [**WISP**](https://wisp.gg) | Extra features. |
|
| [**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. |
|
||||||
| [**Fragnet**](https://fragnet.net) | Providing low latency, high-end game hosting solutions to gamers, game studios and eSports platforms. |
|
| [**RocketNode**](https://rocketnode.com/) | Innovative game server hosting combined with a straightforward control panel, affordable prices, and Rocket-Fast support. |
|
||||||
| [**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. |
|
| [**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. |
|
||||||
| [**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. |
|
| [**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! |
|
| [**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! |
|
| [**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. |
|
| [**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. |
|
||||||
| [**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. |
|
| [**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. |
|
||||||
| [**HostEZ**](https://hostez.io) | Providing North America Valheim, Minecraft and other popular games with low latency, high uptime and maximum availability. EZ! |
|
| [**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. |
|
||||||
| [**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
|
## Documentation
|
||||||
|
|
||||||
* [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html)
|
* [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html)
|
||||||
* [Wings Documentation](https://pterodactyl.io/wings/1.0/installing.html)
|
* [Wings Documentation](https://pterodactyl.io/wings/1.0/installing.html)
|
||||||
* [Community Guides](https://pterodactyl.io/community/about.html)
|
* [Community Guides](https://pterodactyl.io/community/about.html)
|
||||||
* Or, get additional help [via Discord](https://discord.gg/pterodactyl)
|
* Or, get additional help [via Discord](https://discord.gg/pterodactyl)
|
||||||
|
|
||||||
## Reporting Issues
|
## Reporting Issues
|
||||||
|
|
||||||
Please use the [pterodactyl/panel](https://github.com/pterodactyl/panel) repository to report any issues or make
|
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
|
feature requests for Wings. In addition, the [security policy](https://github.com/pterodactyl/panel/security/policy) listed
|
||||||
within that repository also applies to Wings.
|
within that repository also applies to Wings.
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ func (e *Environment) Create() error {
|
|||||||
networkMode := container.NetworkMode(cfg.Docker.Network.Mode)
|
networkMode := container.NetworkMode(cfg.Docker.Network.Mode)
|
||||||
if a.ForceOutgoingIP {
|
if a.ForceOutgoingIP {
|
||||||
e.log().Debug("environment/docker: forcing outgoing IP address")
|
e.log().Debug("environment/docker: forcing outgoing IP address")
|
||||||
networkName := strings.ReplaceAll(e.Id, "-", "")
|
networkName := "ip-" + strings.ReplaceAll(strings.ReplaceAll(a.DefaultMapping.Ip, ".", "-"), ":", "-")
|
||||||
networkMode = container.NetworkMode(networkName)
|
networkMode = container.NetworkMode(networkName)
|
||||||
|
|
||||||
if _, err := e.client.NetworkInspect(ctx, networkName, types.NetworkInspectOptions{}); err != nil {
|
if _, err := e.client.NetworkInspect(ctx, networkName, types.NetworkInspectOptions{}); err != nil {
|
||||||
|
|||||||
@@ -179,8 +179,12 @@ func (e *Environment) Stop(ctx context.Context) error {
|
|||||||
|
|
||||||
// Allow the stop action to run for however long it takes, similar to executing a command
|
// 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.
|
// and using a different logic pathway to wait for the container to stop successfully.
|
||||||
t := time.Duration(-1)
|
//
|
||||||
if err := e.client.ContainerStop(ctx, e.Id, &t); err != nil {
|
// 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 {
|
||||||
// If the container does not exist just mark the process as stopped and return without
|
// If the container does not exist just mark the process as stopped and return without
|
||||||
// an error.
|
// an error.
|
||||||
if client.IsErrNotFound(err) {
|
if client.IsErrNotFound(err) {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ type Client interface {
|
|||||||
SetArchiveStatus(ctx context.Context, uuid string, successful bool) error
|
SetArchiveStatus(ctx context.Context, uuid string, successful bool) error
|
||||||
SetBackupStatus(ctx context.Context, backup string, data BackupRequest) error
|
SetBackupStatus(ctx context.Context, backup string, data BackupRequest) error
|
||||||
SendRestorationStatus(ctx context.Context, backup string, successful bool) error
|
SendRestorationStatus(ctx context.Context, backup string, successful bool) error
|
||||||
SetInstallationStatus(ctx context.Context, uuid string, successful bool) error
|
SetInstallationStatus(ctx context.Context, uuid string, data InstallStatusRequest) error
|
||||||
SetTransferStatus(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)
|
ValidateSftpCredentials(ctx context.Context, request SftpAuthRequest) (SftpAuthResponse, error)
|
||||||
SendActivityLogs(ctx context.Context, activity []models.Activity) error
|
SendActivityLogs(ctx context.Context, activity []models.Activity) error
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const (
|
|||||||
ProcessStopNativeStop = "stop"
|
ProcessStopNativeStop = "stop"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetServers returns all of the servers that are present on the Panel making
|
// GetServers returns all the servers that are present on the Panel making
|
||||||
// parallel API calls to the endpoint if more than one page of servers is
|
// parallel API calls to the endpoint if more than one page of servers is
|
||||||
// returned.
|
// returned.
|
||||||
func (c *client) GetServers(ctx context.Context, limit int) ([]RawServerData, error) {
|
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
|
// 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
|
// things in a bad state within the Panel. This API call is executed once Wings
|
||||||
// has fully booted all of the servers.
|
// has fully booted all the servers.
|
||||||
func (c *client) ResetServersState(ctx context.Context) error {
|
func (c *client) ResetServersState(ctx context.Context) error {
|
||||||
res, err := c.Post(ctx, "/servers/reset", nil)
|
res, err := c.Post(ctx, "/servers/reset", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -92,8 +92,8 @@ func (c *client) GetInstallationScript(ctx context.Context, uuid string) (Instal
|
|||||||
return config, err
|
return config, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) SetInstallationStatus(ctx context.Context, uuid string, successful bool) error {
|
func (c *client) SetInstallationStatus(ctx context.Context, uuid string, data InstallStatusRequest) error {
|
||||||
resp, err := c.Post(ctx, fmt.Sprintf("/servers/%s/install", uuid), d{"successful": successful})
|
resp, err := c.Post(ctx, fmt.Sprintf("/servers/%s/install", uuid), data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -115,7 +115,7 @@ func (c *client) SetTransferStatus(ctx context.Context, uuid string, successful
|
|||||||
if successful {
|
if successful {
|
||||||
state = "success"
|
state = "success"
|
||||||
}
|
}
|
||||||
resp, err := c.Get(ctx, fmt.Sprintf("/servers/%s/transfer/%s", uuid, state), nil)
|
resp, err := c.Post(ctx, fmt.Sprintf("/servers/%s/transfer/%s", uuid, state), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
// password combination provided is associated with a valid server on the instance
|
||||||
// using the Panel's authentication control mechanisms. This will get itself
|
// using the Panel's authentication control mechanisms. This will get itself
|
||||||
// throttled if too many requests are made, allowing us to completely offload
|
// throttled if too many requests are made, allowing us to completely offload
|
||||||
// all of the authorization security logic to the Panel.
|
// all the authorization security logic to the Panel.
|
||||||
func (c *client) ValidateSftpCredentials(ctx context.Context, request SftpAuthRequest) (SftpAuthResponse, error) {
|
func (c *client) ValidateSftpCredentials(ctx context.Context, request SftpAuthRequest) (SftpAuthResponse, error) {
|
||||||
var auth SftpAuthResponse
|
var auth SftpAuthResponse
|
||||||
res, err := c.Post(ctx, "/sftp/auth", request)
|
res, err := c.Post(ctx, "/sftp/auth", request)
|
||||||
|
|||||||
@@ -92,8 +92,8 @@ type SftpAuthResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OutputLineMatcher struct {
|
type OutputLineMatcher struct {
|
||||||
// The raw string to match against. This may or may not be prefixed with
|
// raw string to match against. This may or may not be prefixed with
|
||||||
// regex: which indicates we want to match against the regex expression.
|
// `regex:` which indicates we want to match against the regex expression.
|
||||||
raw []byte
|
raw []byte
|
||||||
reg *regexp.Regexp
|
reg *regexp.Regexp
|
||||||
}
|
}
|
||||||
@@ -139,9 +139,9 @@ type ProcessStopConfiguration struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ProcessConfiguration defines the process configuration for a given server
|
// ProcessConfiguration defines the process configuration for a given server
|
||||||
// instance. This sets what Wings is looking for to mark a server as done starting
|
// instance. This sets what Wings is looking for to mark a server as done
|
||||||
// what to do when stopping, and what changes to make to the configuration file
|
// starting what to do when stopping, and what changes to make to the
|
||||||
// for a server.
|
// configuration file for a server.
|
||||||
type ProcessConfiguration struct {
|
type ProcessConfiguration struct {
|
||||||
Startup struct {
|
Startup struct {
|
||||||
Done []*OutputLineMatcher `json:"done"`
|
Done []*OutputLineMatcher `json:"done"`
|
||||||
@@ -169,3 +169,8 @@ type BackupRequest struct {
|
|||||||
Successful bool `json:"successful"`
|
Successful bool `json:"successful"`
|
||||||
Parts []BackupPart `json:"parts"`
|
Parts []BackupPart `json:"parts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InstallStatusRequest struct {
|
||||||
|
Successful bool `json:"successful"`
|
||||||
|
Reinstall bool `json:"reinstall"`
|
||||||
|
}
|
||||||
|
|||||||
157
router/error.go
157
router/error.go
@@ -1,157 +0,0 @@
|
|||||||
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,11 +1,9 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"emperror.dev/errors"
|
"emperror.dev/errors"
|
||||||
@@ -16,133 +14,8 @@ import (
|
|||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"github.com/pterodactyl/wings/remote"
|
"github.com/pterodactyl/wings/remote"
|
||||||
"github.com/pterodactyl/wings/server"
|
"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
|
// 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
|
// 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
|
// allowing for an easier time identifying the specific request that failed for
|
||||||
@@ -180,7 +53,7 @@ func AttachApiClient(client remote.Client) gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CaptureAndAbort aborts the request and attaches the provided error to the gin
|
// 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.
|
// at the time it is called the stack will be attached.
|
||||||
func CaptureAndAbort(c *gin.Context, err error) {
|
func CaptureAndAbort(c *gin.Context, err error) {
|
||||||
c.Abort()
|
c.Abort()
|
||||||
|
|||||||
141
router/middleware/request_error.go
Normal file
141
router/middleware/request_error.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
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,6 +1,7 @@
|
|||||||
package router
|
package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"emperror.dev/errors"
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
@@ -16,7 +17,10 @@ func Configure(m *wserver.Manager, client remote.Client) *gin.Engine {
|
|||||||
|
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
router.Use(gin.Recovery())
|
router.Use(gin.Recovery())
|
||||||
_ = router.SetTrustedProxies(config.Get().Api.TrustedProxies)
|
if err := router.SetTrustedProxies(config.Get().Api.TrustedProxies); err != nil {
|
||||||
|
panic(errors.WithStack(err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
router.Use(middleware.AttachRequestID(), middleware.CaptureErrors(), middleware.SetAccessControlHeaders())
|
router.Use(middleware.AttachRequestID(), middleware.CaptureErrors(), middleware.SetAccessControlHeaders())
|
||||||
router.Use(middleware.AttachServerManager(m), middleware.AttachApiClient(client))
|
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.
|
// @todo log this into a different file so you can setup IP blocking for abusive requests and such.
|
||||||
|
|||||||
@@ -21,12 +21,11 @@ func getDownloadBackup(c *gin.Context) {
|
|||||||
|
|
||||||
token := tokens.BackupPayload{}
|
token := tokens.BackupPayload{}
|
||||||
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
|
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
|
||||||
NewTrackedError(err).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s, ok := manager.Get(token.ServerUuid)
|
if _, ok := manager.Get(token.ServerUuid); !ok || !token.IsUniqueRequest() {
|
||||||
if !ok || !token.IsUniqueRequest() {
|
|
||||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
||||||
"error": "The requested resource was not found on this server.",
|
"error": "The requested resource was not found on this server.",
|
||||||
})
|
})
|
||||||
@@ -42,13 +41,13 @@ func getDownloadBackup(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
NewServerError(err, s).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.Open(b.Path())
|
f, err := os.Open(b.Path())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
NewServerError(err, s).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
@@ -57,7 +56,7 @@ func getDownloadBackup(c *gin.Context) {
|
|||||||
c.Header("Content-Disposition", "attachment; filename="+strconv.Quote(st.Name()))
|
c.Header("Content-Disposition", "attachment; filename="+strconv.Quote(st.Name()))
|
||||||
c.Header("Content-Type", "application/octet-stream")
|
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.
|
// Handles downloading a specific file for a server.
|
||||||
@@ -65,7 +64,7 @@ func getDownloadFile(c *gin.Context) {
|
|||||||
manager := middleware.ExtractManager(c)
|
manager := middleware.ExtractManager(c)
|
||||||
token := tokens.FilePayload{}
|
token := tokens.FilePayload{}
|
||||||
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
|
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
|
||||||
NewTrackedError(err).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +81,7 @@ func getDownloadFile(c *gin.Context) {
|
|||||||
// If there is an error or we're somehow trying to download a directory, just
|
// If there is an error or we're somehow trying to download a directory, just
|
||||||
// respond with the appropriate error.
|
// respond with the appropriate error.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
NewServerError(err, s).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
} else if st.IsDir() {
|
} else if st.IsDir() {
|
||||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
||||||
@@ -93,7 +92,7 @@ func getDownloadFile(c *gin.Context) {
|
|||||||
|
|
||||||
f, err := os.Open(p)
|
f, err := os.Open(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
NewServerError(err, s).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,5 +100,5 @@ func getDownloadFile(c *gin.Context) {
|
|||||||
c.Header("Content-Disposition", "attachment; filename="+strconv.Quote(st.Name()))
|
c.Header("Content-Disposition", "attachment; filename="+strconv.Quote(st.Name()))
|
||||||
c.Header("Content-Type", "application/octet-stream")
|
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)
|
out, err := s.ReadLogfile(l)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
NewServerError(err, s).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ func postServerCommands(c *gin.Context) {
|
|||||||
s := ExtractServer(c)
|
s := ExtractServer(c)
|
||||||
|
|
||||||
if running, err := s.Environment.IsRunning(c.Request.Context()); err != nil {
|
if running, err := s.Environment.IsRunning(c.Request.Context()); err != nil {
|
||||||
NewServerError(err, s).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
} else if !running {
|
} else if !running {
|
||||||
c.AbortWithStatusJSON(http.StatusBadGateway, gin.H{
|
c.AbortWithStatusJSON(http.StatusBadGateway, gin.H{
|
||||||
@@ -143,7 +143,7 @@ func postServerSync(c *gin.Context) {
|
|||||||
s := ExtractServer(c)
|
s := ExtractServer(c)
|
||||||
|
|
||||||
if err := s.Sync(); err != nil {
|
if err := s.Sync(); err != nil {
|
||||||
WithError(c, err)
|
middleware.CaptureAndAbort(c, err)
|
||||||
} else {
|
} else {
|
||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
@@ -153,9 +153,15 @@ func postServerSync(c *gin.Context) {
|
|||||||
func postServerInstall(c *gin.Context) {
|
func postServerInstall(c *gin.Context) {
|
||||||
s := ExtractServer(c)
|
s := ExtractServer(c)
|
||||||
|
|
||||||
go func(serv *server.Server) {
|
go func(s *server.Server) {
|
||||||
if err := serv.Install(true); err != nil {
|
s.Log().Info("syncing server state with remote source before executing installation process")
|
||||||
serv.Log().WithField("error", err).Error("failed to execute server 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")
|
||||||
}
|
}
|
||||||
}(s)
|
}(s)
|
||||||
|
|
||||||
@@ -211,7 +217,7 @@ func deleteServer(c *gin.Context) {
|
|||||||
// forcibly terminate it before removing the container, so we do not need to handle
|
// forcibly terminate it before removing the container, so we do not need to handle
|
||||||
// that here.
|
// that here.
|
||||||
if err := s.Environment.Destroy(); err != nil {
|
if err := s.Environment.Destroy(); err != nil {
|
||||||
_ = WithError(c, err)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ func getServerListDirectory(c *gin.Context) {
|
|||||||
s := ExtractServer(c)
|
s := ExtractServer(c)
|
||||||
dir := c.Query("directory")
|
dir := c.Query("directory")
|
||||||
if stats, err := s.Filesystem().ListDirectory(dir); err != nil {
|
if stats, err := s.Filesystem().ListDirectory(dir); err != nil {
|
||||||
WithError(c, err)
|
middleware.CaptureAndAbort(c, err)
|
||||||
} else {
|
} else {
|
||||||
c.JSON(http.StatusOK, stats)
|
c.JSON(http.StatusOK, stats)
|
||||||
}
|
}
|
||||||
@@ -152,7 +152,7 @@ func putServerRenameFiles(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
NewServerError(err, s).AbortFilesystemError(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,11 +172,11 @@ func postServerCopyFile(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Filesystem().IsIgnored(data.Location); err != nil {
|
if err := s.Filesystem().IsIgnored(data.Location); err != nil {
|
||||||
NewServerError(err, s).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := s.Filesystem().Copy(data.Location); err != nil {
|
if err := s.Filesystem().Copy(data.Location); err != nil {
|
||||||
NewServerError(err, s).AbortFilesystemError(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +221,7 @@ func postServerDeleteFiles(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := g.Wait(); err != nil {
|
if err := g.Wait(); err != nil {
|
||||||
NewServerError(err, s).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,7 +236,7 @@ func postServerWriteFile(c *gin.Context) {
|
|||||||
f = "/" + strings.TrimLeft(f, "/")
|
f = "/" + strings.TrimLeft(f, "/")
|
||||||
|
|
||||||
if err := s.Filesystem().IsIgnored(f); err != nil {
|
if err := s.Filesystem().IsIgnored(f); err != nil {
|
||||||
NewServerError(err, s).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := s.Filesystem().Writefile(f, c.Request.Body); err != nil {
|
if err := s.Filesystem().Writefile(f, c.Request.Body); err != nil {
|
||||||
@@ -247,7 +247,7 @@ func postServerWriteFile(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
NewServerError(err, s).AbortFilesystemError(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,12 +294,12 @@ func postServerPullRemoteFile(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
WithError(c, err)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Filesystem().HasSpaceErr(true); err != nil {
|
if err := s.Filesystem().HasSpaceErr(true); err != nil {
|
||||||
WithError(c, err)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Do not allow more than three simultaneous remote file downloads at one time.
|
// 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 {
|
if err := download(); err != nil {
|
||||||
NewServerError(err, s).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
st, err := s.Filesystem().Stat(dl.Path())
|
st, err := s.Filesystem().Stat(dl.Path())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
NewServerError(err, s).AbortFilesystemError(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, &st)
|
c.JSON(http.StatusOK, &st)
|
||||||
@@ -380,7 +380,7 @@ func postServerCreateDirectory(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
NewServerError(err, s).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,7 +415,7 @@ func postServerCompressFiles(c *gin.Context) {
|
|||||||
|
|
||||||
f, err := s.Filesystem().CompressFiles(data.RootPath, data.Files)
|
f, err := s.Filesystem().CompressFiles(data.RootPath, data.Files)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
NewServerError(err, s).AbortFilesystemError(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -533,7 +533,7 @@ func postServerChmodFile(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
NewServerError(err, s).AbortFilesystemError(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,7 +545,7 @@ func postServerUploadFiles(c *gin.Context) {
|
|||||||
|
|
||||||
token := tokens.UploadPayload{}
|
token := tokens.UploadPayload{}
|
||||||
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
|
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
|
||||||
NewTrackedError(err).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,14 +591,14 @@ func postServerUploadFiles(c *gin.Context) {
|
|||||||
for _, header := range headers {
|
for _, header := range headers {
|
||||||
p, err := s.Filesystem().SafePath(filepath.Join(directory, header.Filename))
|
p, err := s.Filesystem().SafePath(filepath.Join(directory, header.Filename))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
NewServerError(err, s).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// We run this in a different method so I can use defer without any of
|
// We run this in a different method so I can use defer without any of
|
||||||
// the consequences caused by calling it in a loop.
|
// the consequences caused by calling it in a loop.
|
||||||
if err := handleFileUpload(p, s, header); err != nil {
|
if err := handleFileUpload(p, s, header); err != nil {
|
||||||
NewServerError(err, s).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
s.SaveActivity(s.NewRequestActivity(token.UserUuid, c.ClientIP()), server.ActivityFileUploaded, models.ActivityMeta{
|
s.SaveActivity(s.NewRequestActivity(token.UserUuid, c.ClientIP()), server.ActivityFileUploaded, models.ActivityMeta{
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func getServerWebsocket(c *gin.Context) {
|
|||||||
|
|
||||||
handler, err := websocket.GetHandler(s, c.Writer, c.Request, c)
|
handler, err := websocket.GetHandler(s, c.Writer, c.Request, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
NewServerError(err, s).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer handler.Connection.Close()
|
defer handler.Connection.Close()
|
||||||
|
|||||||
@@ -20,12 +20,28 @@ import (
|
|||||||
func getSystemInformation(c *gin.Context) {
|
func getSystemInformation(c *gin.Context) {
|
||||||
i, err := system.GetSystemInformation()
|
i, err := system.GetSystemInformation()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
NewTrackedError(err).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, i)
|
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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns all the servers that are registered and configured correctly on
|
// Returns all the servers that are registered and configured correctly on
|
||||||
@@ -75,7 +91,7 @@ func postCreateServer(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := i.Server().Install(false); err != nil {
|
if err := i.Server().Install(); err != nil {
|
||||||
log.WithFields(log.Fields{"server": i.Server().ID(), "error": err}).Error("failed to run install process for server")
|
log.WithFields(log.Fields{"server": i.Server().ID(), "error": err}).Error("failed to run install process for server")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -117,7 +133,7 @@ func postUpdateConfiguration(c *gin.Context) {
|
|||||||
// Try to write this new configuration to the disk before updating our global
|
// Try to write this new configuration to the disk before updating our global
|
||||||
// state with it.
|
// state with it.
|
||||||
if err := config.WriteToDisk(cfg); err != nil {
|
if err := config.WriteToDisk(cfg); err != nil {
|
||||||
_ = WithError(c, err)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Since we wrote it to the disk successfully now update the global configuration
|
// 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{}
|
token := tokens.TransferPayload{}
|
||||||
if err := tokens.ParseToken([]byte(auth[1]), &token); err != nil {
|
if err := tokens.ParseToken([]byte(auth[1]), &token); err != nil {
|
||||||
NewTrackedError(err).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
manager := middleware.ExtractManager(c)
|
manager := middleware.ExtractManager(c)
|
||||||
u, err := uuid.Parse(token.Subject)
|
u, err := uuid.Parse(token.Subject)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
NewTrackedError(err).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ func postTransfers(c *gin.Context) {
|
|||||||
if err := manager.Client().SetTransferStatus(context.Background(), trnsfr.Server.ID(), false); err != nil {
|
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")
|
trnsfr.Log().WithField("status", false).WithError(err).Error("failed to set transfer status")
|
||||||
}
|
}
|
||||||
NewTrackedError(err).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,13 +123,13 @@ func postTransfers(c *gin.Context) {
|
|||||||
mediaType, params, err := mime.ParseMediaType(c.GetHeader("Content-Type"))
|
mediaType, params, err := mime.ParseMediaType(c.GetHeader("Content-Type"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
trnsfr.Log().Debug("failed to parse content type header")
|
trnsfr.Log().Debug("failed to parse content type header")
|
||||||
NewTrackedError(err).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(mediaType, "multipart/") {
|
if !strings.HasPrefix(mediaType, "multipart/") {
|
||||||
trnsfr.Log().Debug("invalid content type")
|
trnsfr.Log().Debug("invalid content type")
|
||||||
NewTrackedError(fmt.Errorf("invalid content type \"%s\", expected \"multipart/form-data\"", mediaType)).Abort(c)
|
middleware.CaptureAndAbort(c, fmt.Errorf("invalid content type \"%s\", expected \"multipart/form-data\"", mediaType))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@ out:
|
|||||||
break out
|
break out
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
NewTrackedError(err).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,13 +166,13 @@ out:
|
|||||||
trnsfr.Log().Debug("received archive")
|
trnsfr.Log().Debug("received archive")
|
||||||
|
|
||||||
if err := trnsfr.Server.EnsureDataDirectoryExists(); err != nil {
|
if err := trnsfr.Server.EnsureDataDirectoryExists(); err != nil {
|
||||||
NewTrackedError(err).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tee := io.TeeReader(p, h)
|
tee := io.TeeReader(p, h)
|
||||||
if err := trnsfr.Server.Filesystem().ExtractStreamUnsafe(ctx, "/", tee); err != nil {
|
if err := trnsfr.Server.Filesystem().ExtractStreamUnsafe(ctx, "/", tee); err != nil {
|
||||||
NewTrackedError(err).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +181,7 @@ out:
|
|||||||
trnsfr.Log().Debug("received checksum")
|
trnsfr.Log().Debug("received checksum")
|
||||||
|
|
||||||
if !hasArchive {
|
if !hasArchive {
|
||||||
NewTrackedError(errors.New("archive must be sent before the checksum")).Abort(c)
|
middleware.CaptureAndAbort(c, errors.New("archive must be sent before the checksum"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,14 +189,14 @@ out:
|
|||||||
|
|
||||||
v, err := io.ReadAll(p)
|
v, err := io.ReadAll(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
NewTrackedError(err).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
expected := make([]byte, hex.DecodedLen(len(v)))
|
expected := make([]byte, hex.DecodedLen(len(v)))
|
||||||
n, err := hex.Decode(expected, v)
|
n, err := hex.Decode(expected, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
NewTrackedError(err).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
actual := h.Sum(nil)
|
actual := h.Sum(nil)
|
||||||
@@ -207,7 +207,7 @@ out:
|
|||||||
}).Debug("checksums")
|
}).Debug("checksums")
|
||||||
|
|
||||||
if !bytes.Equal(expected[:n], actual) {
|
if !bytes.Equal(expected[:n], actual) {
|
||||||
NewTrackedError(errors.New("checksums don't match")).Abort(c)
|
middleware.CaptureAndAbort(c, errors.New("checksums don't match"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,12 +220,12 @@ out:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !hasArchive || !hasChecksum {
|
if !hasArchive || !hasChecksum {
|
||||||
NewTrackedError(errors.New("missing archive or checksum")).Abort(c)
|
middleware.CaptureAndAbort(c, errors.New("missing archive or checksum"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !checksumVerified {
|
if !checksumVerified {
|
||||||
NewTrackedError(errors.New("checksums don't match")).Abort(c)
|
middleware.CaptureAndAbort(c, errors.New("checksums don't match"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +235,7 @@ out:
|
|||||||
|
|
||||||
// Ensure the server environment gets configured.
|
// Ensure the server environment gets configured.
|
||||||
if err := trnsfr.Server.CreateEnvironment(); err != nil {
|
if err := trnsfr.Server.CreateEnvironment(); err != nil {
|
||||||
NewTrackedError(err).Abort(c)
|
middleware.CaptureAndAbort(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const (
|
|||||||
ErrCodePathResolution ErrorCode = "E_BADPATH"
|
ErrCodePathResolution ErrorCode = "E_BADPATH"
|
||||||
ErrCodeDenylistFile ErrorCode = "E_DENYLIST"
|
ErrCodeDenylistFile ErrorCode = "E_DENYLIST"
|
||||||
ErrCodeUnknownError ErrorCode = "E_UNKNOWN"
|
ErrCodeUnknownError ErrorCode = "E_UNKNOWN"
|
||||||
|
ErrNotExist ErrorCode = "E_NOTEXIST"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Error struct {
|
type Error struct {
|
||||||
@@ -68,6 +69,8 @@ func (e *Error) Error() string {
|
|||||||
r = "<empty>"
|
r = "<empty>"
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("filesystem: server path [%s] resolves to a location outside the server root: %s", e.path, r)
|
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:
|
case ErrCodeUnknownError:
|
||||||
fallthrough
|
fallthrough
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -61,25 +61,28 @@ func (fs *Filesystem) Path() string {
|
|||||||
func (fs *Filesystem) File(p string) (*os.File, Stat, error) {
|
func (fs *Filesystem) File(p string) (*os.File, Stat, error) {
|
||||||
cleaned, err := fs.SafePath(p)
|
cleaned, err := fs.SafePath(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, Stat{}, err
|
return nil, Stat{}, errors.WithStackIf(err)
|
||||||
}
|
}
|
||||||
st, err := fs.Stat(cleaned)
|
st, err := fs.Stat(cleaned)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, Stat{}, err
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil, Stat{}, newFilesystemError(ErrNotExist, err)
|
||||||
|
}
|
||||||
|
return nil, Stat{}, errors.WithStackIf(err)
|
||||||
}
|
}
|
||||||
if st.IsDir() {
|
if st.IsDir() {
|
||||||
return nil, Stat{}, newFilesystemError(ErrCodeIsDirectory, nil)
|
return nil, Stat{}, newFilesystemError(ErrCodeIsDirectory, nil)
|
||||||
}
|
}
|
||||||
f, err := os.Open(cleaned)
|
f, err := os.Open(cleaned)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, Stat{}, err
|
return nil, Stat{}, errors.WithStackIf(err)
|
||||||
}
|
}
|
||||||
return f, st, nil
|
return f, st, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Acts by creating the given file and path on the disk if it is not present already. If
|
// Touch acts by creating the given file and path on the disk if it is not present
|
||||||
// it is present, the file is opened using the defaults which will truncate the contents.
|
// already. If it is present, the file is opened using the defaults which will truncate
|
||||||
// The opened file is then returned to the caller.
|
// the contents. The opened file is then returned to the caller.
|
||||||
func (fs *Filesystem) Touch(p string, flag int) (*os.File, error) {
|
func (fs *Filesystem) Touch(p string, flag int) (*os.File, error) {
|
||||||
cleaned, err := fs.SafePath(p)
|
cleaned, err := fs.SafePath(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -84,6 +84,35 @@ 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) {
|
func TestFilesystem_Writefile(t *testing.T) {
|
||||||
g := Goblin(t)
|
g := Goblin(t)
|
||||||
fs, rfs := NewFs()
|
fs, rfs := NewFs()
|
||||||
|
|||||||
@@ -32,19 +32,17 @@ import (
|
|||||||
//
|
//
|
||||||
// Pass true as the first argument in order to execute a server sync before the
|
// Pass true as the first argument in order to execute a server sync before the
|
||||||
// process to ensure the latest information is used.
|
// process to ensure the latest information is used.
|
||||||
func (s *Server) Install(sync bool) error {
|
func (s *Server) Install() error {
|
||||||
if sync {
|
return s.install(false)
|
||||||
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
|
var err error
|
||||||
if !s.Config().SkipEggScripts {
|
if !s.Config().SkipEggScripts {
|
||||||
// Send the start event so the Panel can automatically update. We don't send this unless the process
|
// Send the start event so the Panel can automatically update. We don't
|
||||||
// is actually going to run, otherwise all sorts of weird rapid UI behavior happens since there isn't
|
// send this unless the process is actually going to run, otherwise all
|
||||||
// an actual install process being executed.
|
// sorts of weird rapid UI behavior happens since there isn't an actual
|
||||||
|
// install process being executed.
|
||||||
s.Events().Publish(InstallStartedEvent, "")
|
s.Events().Publish(InstallStartedEvent, "")
|
||||||
|
|
||||||
err = s.internalInstall()
|
err = s.internalInstall()
|
||||||
@@ -53,12 +51,13 @@ func (s *Server) Install(sync bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.Log().WithField("was_successful", err == nil).Debug("notifying panel of server install state")
|
s.Log().WithField("was_successful", err == nil).Debug("notifying panel of server install state")
|
||||||
if serr := s.SyncInstallState(err == nil); serr != nil {
|
if serr := s.SyncInstallState(err == nil, reinstall); serr != nil {
|
||||||
l := s.Log().WithField("was_successful", err == nil)
|
l := s.Log().WithField("was_successful", err == nil)
|
||||||
|
|
||||||
// If the request was successful but there was an error with this request, attach the
|
// If the request was successful but there was an error with this request,
|
||||||
// error to this log entry. Otherwise ignore it in this log since whatever is calling
|
// attach the error to this log entry. Otherwise, ignore it in this log
|
||||||
// this function should handle the error and will end up logging the same one.
|
// since whatever is calling this function should handle the error and
|
||||||
|
// will end up logging the same one.
|
||||||
if err == nil {
|
if err == nil {
|
||||||
l.WithField("error", err)
|
l.WithField("error", err)
|
||||||
}
|
}
|
||||||
@@ -66,19 +65,20 @@ func (s *Server) Install(sync bool) error {
|
|||||||
l.Warn("failed to notify panel of server install state")
|
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
|
// Ensure that the server is marked as offline at this point, otherwise you
|
||||||
// with a blank value which is a bit confusing.
|
// end up with a blank value which is a bit confusing.
|
||||||
s.Environment.SetState(environment.ProcessOfflineState)
|
s.Environment.SetState(environment.ProcessOfflineState)
|
||||||
|
|
||||||
// Push an event to the websocket so we can auto-refresh the information in the panel once
|
// Push an event to the websocket, so we can auto-refresh the information in
|
||||||
// the install is completed.
|
// the panel once the installation is completed.
|
||||||
s.Events().Publish(InstallCompletedEvent, "")
|
s.Events().Publish(InstallCompletedEvent, "")
|
||||||
|
|
||||||
return errors.WithStackIf(err)
|
return errors.WithStackIf(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reinstalls a server's software by utilizing the install script for the server egg. This
|
// Reinstall reinstalls a server's software by utilizing the installation script
|
||||||
// does not touch any existing files for the server, other than what the script modifies.
|
// 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 {
|
func (s *Server) Reinstall() error {
|
||||||
if s.Environment.State() != environment.ProcessOfflineState {
|
if s.Environment.State() != environment.ProcessOfflineState {
|
||||||
s.Log().Debug("waiting for server instance to enter a stopped state")
|
s.Log().Debug("waiting for server instance to enter a stopped state")
|
||||||
@@ -87,7 +87,12 @@ func (s *Server) Reinstall() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.Install(true)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal installation function used to simplify reporting back to the Panel.
|
// Internal installation function used to simplify reporting back to the Panel.
|
||||||
@@ -116,8 +121,9 @@ type InstallationProcess struct {
|
|||||||
client *client.Client
|
client *client.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generates a new installation process struct that will be used to create containers,
|
// NewInstallationProcess returns a new installation process struct that will be
|
||||||
// and otherwise perform installation commands for a server.
|
// used to create containers and otherwise perform installation commands for a
|
||||||
|
// server.
|
||||||
func NewInstallationProcess(s *Server, script *remote.InstallationScript) (*InstallationProcess, error) {
|
func NewInstallationProcess(s *Server, script *remote.InstallationScript) (*InstallationProcess, error) {
|
||||||
proc := &InstallationProcess{
|
proc := &InstallationProcess{
|
||||||
Script: script,
|
Script: script,
|
||||||
@@ -133,8 +139,8 @@ func NewInstallationProcess(s *Server, script *remote.InstallationScript) (*Inst
|
|||||||
return proc, nil
|
return proc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determines if the server is actively running the installation process by checking the status
|
// IsInstalling returns if the server is actively running the installation
|
||||||
// of the installer lock.
|
// process by checking the status of the installer lock.
|
||||||
func (s *Server) IsInstalling() bool {
|
func (s *Server) IsInstalling() bool {
|
||||||
return s.installing.Load()
|
return s.installing.Load()
|
||||||
}
|
}
|
||||||
@@ -155,7 +161,7 @@ func (s *Server) SetRestoring(state bool) {
|
|||||||
s.restoring.Store(state)
|
s.restoring.Store(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removes the installer container for the server.
|
// RemoveContainer removes the installation container for the server.
|
||||||
func (ip *InstallationProcess) RemoveContainer() error {
|
func (ip *InstallationProcess) RemoveContainer() error {
|
||||||
err := ip.client.ContainerRemove(ip.Server.Context(), ip.Server.ID()+"_installer", types.ContainerRemoveOptions{
|
err := ip.client.ContainerRemove(ip.Server.Context(), ip.Server.ID()+"_installer", types.ContainerRemoveOptions{
|
||||||
RemoveVolumes: true,
|
RemoveVolumes: true,
|
||||||
@@ -328,14 +334,14 @@ func (ip *InstallationProcess) BeforeExecute() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the log path for the installation process.
|
// GetLogPath returns the log path for the installation process.
|
||||||
func (ip *InstallationProcess) GetLogPath() string {
|
func (ip *InstallationProcess) GetLogPath() string {
|
||||||
return filepath.Join(config.Get().System.LogDirectory, "/install", ip.Server.ID()+".log")
|
return filepath.Join(config.Get().System.LogDirectory, "/install", ip.Server.ID()+".log")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleans up after the execution of the installation process. This grabs the logs from the
|
// AfterExecute cleans up after the execution of the installation process.
|
||||||
// process to store in the server configuration directory, and then destroys the associated
|
// This grabs the logs from the process to store in the server configuration
|
||||||
// installation container.
|
// directory, and then destroys the associated installation container.
|
||||||
func (ip *InstallationProcess) AfterExecute(containerId string) error {
|
func (ip *InstallationProcess) AfterExecute(containerId string) error {
|
||||||
defer ip.RemoveContainer()
|
defer ip.RemoveContainer()
|
||||||
|
|
||||||
@@ -525,7 +531,7 @@ func (ip *InstallationProcess) StreamOutput(ctx context.Context, id string) erro
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// resourceLimits returns the install container specific resource limits. This
|
// resourceLimits returns resource limits for the installation container. This
|
||||||
// looks at the globally defined install container limits and attempts to use
|
// 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
|
// 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
|
// with super low limits (e.g. Discord bots with 128Mb of memory) to perform more
|
||||||
@@ -537,8 +543,8 @@ func (ip *InstallationProcess) StreamOutput(ctx context.Context, id string) erro
|
|||||||
func (ip *InstallationProcess) resourceLimits() container.Resources {
|
func (ip *InstallationProcess) resourceLimits() container.Resources {
|
||||||
limits := config.Get().Docker.InstallerLimits
|
limits := config.Get().Docker.InstallerLimits
|
||||||
|
|
||||||
// Create a copy of the configuration so we're not accidentally making changes
|
// Create a copy of the configuration, so we're not accidentally making
|
||||||
// to the underlying server build data.
|
// changes to the underlying server build data.
|
||||||
c := *ip.Server.Config()
|
c := *ip.Server.Config()
|
||||||
cfg := c.Build
|
cfg := c.Build
|
||||||
if cfg.MemoryLimit < limits.Memory {
|
if cfg.MemoryLimit < limits.Memory {
|
||||||
@@ -562,10 +568,12 @@ func (ip *InstallationProcess) resourceLimits() container.Resources {
|
|||||||
return resources
|
return resources
|
||||||
}
|
}
|
||||||
|
|
||||||
// SyncInstallState makes a HTTP request to the Panel instance notifying it that
|
// SyncInstallState makes an HTTP request to the Panel instance notifying it that
|
||||||
// the server has completed the installation process, and what the state of the
|
// the server has completed the installation process, and what the state of the
|
||||||
// server is. A boolean value of "true" means everything was successful, "false"
|
// server is.
|
||||||
// means something went wrong and the server must be deleted and re-created.
|
func (s *Server) SyncInstallState(successful, reinstall bool) error {
|
||||||
func (s *Server) SyncInstallState(successful bool) error {
|
return s.client.SetInstallationStatus(s.Context(), s.ID(), remote.InstallStatusRequest{
|
||||||
return s.client.SetInstallationStatus(s.Context(), s.ID(), successful)
|
Successful: successful,
|
||||||
|
Reinstall: reinstall,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,6 +128,13 @@ func (t *Transfer) PushArchiveToTarget(url, token string) ([]byte, error) {
|
|||||||
t.Log().Debug("sending archive to destination")
|
t.Log().Debug("sending archive to destination")
|
||||||
client := http.Client{Timeout: 0}
|
client := http.Client{Timeout: 0}
|
||||||
res, err := client.Do(req)
|
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")
|
t.Log().Debug("waiting for stream to complete")
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
package system
|
package system
|
||||||
|
|
||||||
var Version = "develop"
|
var Version = "1.11.0"
|
||||||
|
|||||||
130
system/system.go
130
system/system.go
@@ -1,17 +1,57 @@
|
|||||||
package system
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/acobaugh/osrelease"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
"github.com/docker/docker/pkg/parsers/kernel"
|
"github.com/docker/docker/pkg/parsers/kernel"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Information struct {
|
type Information struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
KernelVersion string `json:"kernel_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"`
|
Architecture string `json:"architecture"`
|
||||||
|
CPUThreads int `json:"cpu_threads"`
|
||||||
|
MemoryBytes int64 `json:"memory_bytes"`
|
||||||
|
KernelVersion string `json:"kernel_version"`
|
||||||
OS string `json:"os"`
|
OS string `json:"os"`
|
||||||
CpuCount int `json:"cpu_count"`
|
OSType string `json:"os_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetSystemInformation() (*Information, error) {
|
func GetSystemInformation() (*Information, error) {
|
||||||
@@ -20,13 +60,83 @@ func GetSystemInformation() (*Information, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s := &Information{
|
version, info, err := GetDockerInfo(context.Background())
|
||||||
Version: Version,
|
if err != nil {
|
||||||
KernelVersion: k.String(),
|
return nil, err
|
||||||
Architecture: runtime.GOARCH,
|
|
||||||
OS: runtime.GOOS,
|
|
||||||
CpuCount: runtime.NumCPU(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return s, nil
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user