Compare commits

..

7 Commits

Author SHA1 Message Date
Matthew Penner
105f0150f6 Update CHANGELOG.md 2022-11-22 13:49:10 -07:00
Matthew Penner
aeec51632e Update README.md 2022-11-22 13:48:56 -07:00
Dane Everitt
ff50d0e5bd Cleanup request error handling; properly handle os.ErrNotExist errors (#150) 2022-11-22 11:18:27 -07:00
Matthew Penner
9226ccae31 system: more detailed system information 2022-11-21 16:01:14 -07:00
Matthew Penner
2fd0edbff9 environment(docker): fix timeout when sending a stop signal
Previously, Docker would terminate the container when it's stop
configuration was configured to send a signal to the container.
This was due to Docker's API wanting the value as a duration string
(`1s`) rather than a number, so our value of `-1` was being formatted
to `0s` rather than `-1s` like we needed.

Closes https://github.com/pterodactyl/panel/issues/4555
2022-11-21 15:06:38 -07:00
Matthew Penner
1457470fff environment(docker): fix outgoing ip not changing
Fixes an issue where when a server has it's primary allocation
changed, the outgoing ip is not updated on the Docker network.

The only downside of this change is old networks are not cleaned up.

Closes https://github.com/pterodactyl/panel/issues/4547
2022-11-21 14:59:53 -07:00
Matthew Penner
da94f750ad server(install): update installation status request
The new request includes a `reinstall` option to denote
whether the installation failed for the first time, or
during a reinstall.

ref https://github.com/pterodactyl/panel/issues/1994
2022-11-21 14:57:44 -07:00
22 changed files with 481 additions and 423 deletions

View File

@@ -1,5 +1,17 @@
# Changelog
## v1.11.0-rc.2
### Added
* More detailed information returned by the `/api/system` endpoint when using the `?v=2` query parameter.
### Changed
* Send reinstallation status separately from installation status.
### Fixed
* Fixed servers outgoing IP not being updated whenever a server's primary allocation is changed when using the Force Outgoing IP option.
* Fixed servers being terminated rather than gracefully stopped when a signal is used to stop the container rather than a command.
* Fixed file not found errors being treated as an internal error, they are now treated as a 404.
## v1.11.0-rc.1
### Changed
* Wings release versions will now follow the major and minor version of the panel.

View File

@@ -5,6 +5,7 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/pterodactyl/wings)](https://goreportcard.com/report/github.com/pterodactyl/wings)
# Pterodactyl Wings
Wings is Pterodactyl's server control plane, built for the rapidly changing gaming industry and designed to be
highly performant and secure. Wings provides an HTTP API allowing you to interface directly with running server
instances, fetch server logs, generate backups, and control all aspects of the server lifecycle.
@@ -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.
## Sponsors
I would like to extend my sincere thanks to the following sponsors for helping find Pterodactyl's developement.
[Interested in becoming a sponsor?](https://github.com/sponsors/matthewpi)
| Company | About |
| ------- | ----- |
|-----------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [**WISP**](https://wisp.gg) | Extra features. |
| [**BisectHosting**](https://www.bisecthosting.com/) | BisectHosting provides Minecraft, Valheim and other server hosting services with the highest reliability and lightning fast support since 2012. |
| [**Fragnet**](https://fragnet.net) | Providing low latency, high-end game hosting solutions to gamers, game studios and eSports platforms. |
| [**Tempest**](https://tempest.net/) | Tempest Hosting is a subsidiary of Path Network, Inc. offering unmetered DDoS protected 10Gbps dedicated servers, starting at just $80/month. Full anycast, tons of filters. |
| [**Bloom.host**](https://bloom.host) | Bloom.host offers dedicated core VPS and Minecraft hosting with Ryzen 9 processors. With owned-hardware, we offer truly unbeatable prices on high-performance hosting. |
| [**RocketNode**](https://rocketnode.com/) | Innovative game server hosting combined with a straightforward control panel, affordable prices, and Rocket-Fast support. |
| [**Aussie Server Hosts**](https://aussieserverhosts.com/) | No frills Australian Owned and operated High Performance Server hosting for some of the most demanding games serving Australia and New Zealand. |
| [**BisectHosting**](https://www.bisecthosting.com/) | BisectHosting provides Minecraft, Valheim and other server hosting services with the highest reliability and lightning fast support since 2012. |
| [**MineStrator**](https://minestrator.com/) | Looking for the most highend French hosting company for your minecraft server? More than 24,000 members on our discord trust us. Give us a try! |
| [**Skynode**](https://www.skynode.pro/) | Skynode provides blazing fast game servers along with a top-notch user experience. Whatever our clients are looking for, we're able to provide it! |
| [**DeinServerHost**](https://deinserverhost.de/) | DeinServerHost offers Dedicated, vps and Gameservers for many popular Games like Minecraft and Rust in Germany since 2013. |
| [**Aussie Server Hosts**](https://aussieserverhosts.com/) | No frills Australian Owned and operated High Performance Server hosting for some of the most demanding games serving Australia and New Zealand. |
| [**HostEZ**](https://hostez.io) | Providing North America Valheim, Minecraft and other popular games with low latency, high uptime and maximum availability. EZ! |
| [**VibeGAMES**](https://vibegames.net/) | VibeGAMES is a game server provider that specializes in DDOS protection for the games we offer. We have multiple locations in the US, Brazil, France, Germany, Singapore, Australia and South Africa.|
| [**Gamenodes**](https://gamenodes.nl) | Gamenodes love quality. For Minecraft, Discord Bots and other services, among others. With our own programmers, we provide just that little bit of extra service! |
| [**VibeGAMES**](https://vibegames.net/) | VibeGAMES is a game server provider that specializes in DDOS protection for the games we offer. We have multiple locations in the US, Brazil, France, Germany, Singapore, Australia and South Africa. |
| [**Pterodactyl Market**](https://pterodactylmarket.com/) | Pterodactyl Market is a one-and-stop shop for Pterodactyl. In our market, you can find Add-ons, Themes, Eggs, and more for Pterodactyl. |
| [**UltraServers**](https://ultraservers.com/) | Deploy premium games hosting with the click of a button. Manage and swap games with ease and let us take care of the rest. We currently support Minecraft, Rust, ARK, 7 Days to Die, Garys MOD, CS:GO, Satisfactory and others. |
## Documentation
* [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html)
* [Wings Documentation](https://pterodactyl.io/wings/1.0/installing.html)
* [Community Guides](https://pterodactyl.io/community/about.html)
* Or, get additional help [via Discord](https://discord.gg/pterodactyl)
## Reporting Issues
Please use the [pterodactyl/panel](https://github.com/pterodactyl/panel) repository to report any issues or make
feature requests for Wings. In addition, the [security policy](https://github.com/pterodactyl/panel/security/policy) listed
within that repository also applies to Wings.

View File

@@ -196,7 +196,7 @@ func (e *Environment) Create() error {
networkMode := container.NetworkMode(cfg.Docker.Network.Mode)
if a.ForceOutgoingIP {
e.log().Debug("environment/docker: forcing outgoing IP address")
networkName := strings.ReplaceAll(e.Id, "-", "")
networkName := "ip-" + strings.ReplaceAll(strings.ReplaceAll(a.DefaultMapping.Ip, ".", "-"), ":", "-")
networkMode = container.NetworkMode(networkName)
if _, err := e.client.NetworkInspect(ctx, networkName, types.NetworkInspectOptions{}); err != nil {

View File

@@ -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
// 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
// an error.
if client.IsErrNotFound(err) {

View File

@@ -29,7 +29,7 @@ type Client interface {
SetArchiveStatus(ctx context.Context, uuid string, successful bool) error
SetBackupStatus(ctx context.Context, backup string, data BackupRequest) error
SendRestorationStatus(ctx context.Context, backup string, successful bool) error
SetInstallationStatus(ctx context.Context, uuid string, successful bool) error
SetInstallationStatus(ctx context.Context, uuid string, data InstallStatusRequest) error
SetTransferStatus(ctx context.Context, uuid string, successful bool) error
ValidateSftpCredentials(ctx context.Context, request SftpAuthRequest) (SftpAuthResponse, error)
SendActivityLogs(ctx context.Context, activity []models.Activity) error

View File

@@ -19,7 +19,7 @@ const (
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
// returned.
func (c *client) GetServers(ctx context.Context, limit int) ([]RawServerData, error) {
@@ -58,7 +58,7 @@ func (c *client) GetServers(ctx context.Context, limit int) ([]RawServerData, er
//
// This handles Wings exiting during either of these processes which will leave
// things in a bad state within the Panel. This API call is executed once Wings
// has fully booted all of the servers.
// has fully booted all the servers.
func (c *client) ResetServersState(ctx context.Context) error {
res, err := c.Post(ctx, "/servers/reset", nil)
if err != nil {
@@ -92,8 +92,8 @@ func (c *client) GetInstallationScript(ctx context.Context, uuid string) (Instal
return config, err
}
func (c *client) SetInstallationStatus(ctx context.Context, uuid string, successful bool) error {
resp, err := c.Post(ctx, fmt.Sprintf("/servers/%s/install", uuid), d{"successful": successful})
func (c *client) SetInstallationStatus(ctx context.Context, uuid string, data InstallStatusRequest) error {
resp, err := c.Post(ctx, fmt.Sprintf("/servers/%s/install", uuid), data)
if err != nil {
return err
}
@@ -127,7 +127,7 @@ func (c *client) SetTransferStatus(ctx context.Context, uuid string, successful
// password combination provided is associated with a valid server on the instance
// using the Panel's authentication control mechanisms. This will get itself
// throttled if too many requests are made, allowing us to completely offload
// all 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) {
var auth SftpAuthResponse
res, err := c.Post(ctx, "/sftp/auth", request)

View File

@@ -92,8 +92,8 @@ type SftpAuthResponse struct {
}
type OutputLineMatcher struct {
// The raw string to match against. This may or may not be prefixed with
// regex: which indicates we want to match against the regex expression.
// raw string to match against. This may or may not be prefixed with
// `regex:` which indicates we want to match against the regex expression.
raw []byte
reg *regexp.Regexp
}
@@ -139,9 +139,9 @@ type ProcessStopConfiguration struct {
}
// ProcessConfiguration defines the process configuration for a given server
// instance. This sets what Wings is looking for to mark a server as done starting
// what to do when stopping, and what changes to make to the configuration file
// for a server.
// instance. This sets what Wings is looking for to mark a server as done
// starting what to do when stopping, and what changes to make to the
// configuration file for a server.
type ProcessConfiguration struct {
Startup struct {
Done []*OutputLineMatcher `json:"done"`
@@ -169,3 +169,8 @@ type BackupRequest struct {
Successful bool `json:"successful"`
Parts []BackupPart `json:"parts"`
}
type InstallStatusRequest struct {
Successful bool `json:"successful"`
Reinstall bool `json:"reinstall"`
}

View File

@@ -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)
}

View File

@@ -1,11 +1,9 @@
package middleware
import (
"context"
"crypto/subtle"
"io"
"net/http"
"os"
"strings"
"emperror.dev/errors"
@@ -16,133 +14,8 @@ import (
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/remote"
"github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/server/filesystem"
)
// RequestError is a custom error type returned when something goes wrong with
// any of the HTTP endpoints.
type RequestError struct {
err error
status int
msg string
}
// NewError returns a new RequestError for the provided error.
func NewError(err error) *RequestError {
return &RequestError{
// Attach a stacktrace to the error if it is missing at this point and mark it
// as originating from the location where NewError was called, rather than this
// specific point in the code.
err: errors.WithStackDepthIf(err, 1),
}
}
// SetMessage allows for a custom error message to be set on an existing
// RequestError instance.
func (re *RequestError) SetMessage(m string) {
re.msg = m
}
// SetStatus sets the HTTP status code for the error response. By default this
// is a HTTP-500 error.
func (re *RequestError) SetStatus(s int) {
re.status = s
}
// Abort aborts the given HTTP request with the specified status code and then
// logs the event into the logs. The error that is output will include the unique
// request ID if it is present.
func (re *RequestError) Abort(c *gin.Context, status int) {
reqId := c.Writer.Header().Get("X-Request-Id")
// Generate the base logger instance, attaching the unique request ID and
// the URL that was requested.
event := log.WithField("request_id", reqId).WithField("url", c.Request.URL.String())
// If there is a server present in the gin.Context stack go ahead and pull it
// and attach that server UUID to the logs as well so that we can see what specific
// server triggered this error.
if s, ok := c.Get("server"); ok {
if s, ok := s.(*server.Server); ok {
event = event.WithField("server_id", s.ID())
}
}
if c.Writer.Status() == 200 {
// Handle context deadlines being exceeded a little differently since we want
// to report a more user-friendly error and a proper error code. The "context
// canceled" error is generally when a request is terminated before all of the
// logic is finished running.
if errors.Is(re.err, context.DeadlineExceeded) {
re.SetStatus(http.StatusGatewayTimeout)
re.SetMessage("The server could not process this request in time, please try again.")
} else if strings.Contains(re.Cause().Error(), "context canceled") {
re.SetStatus(http.StatusBadRequest)
re.SetMessage("Request aborted by client.")
}
}
// c.Writer.Status() will be a non-200 value if the headers have already been sent
// to the requester but an error is encountered. This can happen if there is an issue
// marshaling a struct placed into a c.JSON() call (or c.AbortWithJSON() call).
if status >= 500 || c.Writer.Status() != 200 {
event.WithField("status", status).WithField("error", re.err).Error("error while handling HTTP request")
} else {
event.WithField("status", status).WithField("error", re.err).Debug("error handling HTTP request (not a server error)")
}
if re.msg == "" {
re.msg = "An unexpected error was encountered while processing this request"
}
// Now abort the request with the error message and include the unique request
// ID that was present to make things super easy on people who don't know how
// or cannot view the response headers (where X-Request-Id would be present).
c.AbortWithStatusJSON(status, gin.H{"error": re.msg, "request_id": reqId})
}
// Cause returns the underlying error.
func (re *RequestError) Cause() error {
return re.err
}
// Error returns the underlying error message for this request.
func (re *RequestError) Error() string {
return re.err.Error()
}
// Looks at the given RequestError and determines if it is a specific filesystem
// error that we can process and return differently for the user.
//
// Some external things end up calling fmt.Errorf() on our filesystem errors
// which ends up just unleashing chaos on the system. For the sake of this,
// fallback to using text checks.
//
// If the error passed into this call is nil or does not match empty values will
// be returned to the caller.
func (re *RequestError) asFilesystemError() (int, string) {
err := re.Cause()
if err == nil {
return 0, ""
}
if filesystem.IsErrorCode(err, filesystem.ErrCodeDenylistFile) || strings.Contains(err.Error(), "filesystem: file access prohibited") {
return http.StatusForbidden, "This file cannot be modified: present in egg denylist."
}
if filesystem.IsErrorCode(err, filesystem.ErrCodePathResolution) || strings.Contains(err.Error(), "resolves to a location outside the server root") {
return http.StatusNotFound, "The requested resource was not found on the system."
}
if filesystem.IsErrorCode(err, filesystem.ErrCodeIsDirectory) || strings.Contains(err.Error(), "filesystem: is a directory") {
return http.StatusBadRequest, "Cannot perform that action: file is a directory."
}
if filesystem.IsErrorCode(err, filesystem.ErrCodeDiskSpace) || strings.Contains(err.Error(), "filesystem: not enough disk space") {
return http.StatusBadRequest, "There is not enough disk space available to perform that action."
}
if strings.HasSuffix(err.Error(), "file name too long") {
return http.StatusBadRequest, "Cannot perform that action: file name is too long."
}
if e, ok := err.(*os.SyscallError); ok && e.Syscall == "readdirent" {
return http.StatusNotFound, "The requested directory does not exist."
}
return 0, ""
}
// AttachRequestID attaches a unique ID to the incoming HTTP request so that any
// errors that are generated or returned to the client will include this reference
// allowing for an easier time identifying the specific request that failed for
@@ -180,7 +53,7 @@ func AttachApiClient(client remote.Client) gin.HandlerFunc {
}
// CaptureAndAbort aborts the request and attaches the provided error to the gin
// context so it can be reported properly. If the error is missing a stacktrace
// context, so it can be reported properly. If the error is missing a stacktrace
// at the time it is called the stack will be attached.
func CaptureAndAbort(c *gin.Context, err error) {
c.Abort()

View 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, ""
}

View File

@@ -1,6 +1,7 @@
package router
import (
"emperror.dev/errors"
"github.com/apex/log"
"github.com/gin-gonic/gin"
@@ -16,7 +17,10 @@ func Configure(m *wserver.Manager, client remote.Client) *gin.Engine {
router := gin.New()
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.AttachServerManager(m), middleware.AttachApiClient(client))
// @todo log this into a different file so you can setup IP blocking for abusive requests and such.

View File

@@ -21,12 +21,11 @@ func getDownloadBackup(c *gin.Context) {
token := tokens.BackupPayload{}
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
NewTrackedError(err).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
s, ok := manager.Get(token.ServerUuid)
if !ok || !token.IsUniqueRequest() {
if _, ok := manager.Get(token.ServerUuid); !ok || !token.IsUniqueRequest() {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested resource was not found on this server.",
})
@@ -42,13 +41,13 @@ func getDownloadBackup(c *gin.Context) {
return
}
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
f, err := os.Open(b.Path())
if err != nil {
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
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-Type", "application/octet-stream")
bufio.NewReader(f).WriteTo(c.Writer)
_, _ = bufio.NewReader(f).WriteTo(c.Writer)
}
// Handles downloading a specific file for a server.
@@ -65,7 +64,7 @@ func getDownloadFile(c *gin.Context) {
manager := middleware.ExtractManager(c)
token := tokens.FilePayload{}
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
NewTrackedError(err).Abort(c)
middleware.CaptureAndAbort(c, err)
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
// respond with the appropriate error.
if err != nil {
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
} else if st.IsDir() {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
@@ -93,7 +92,7 @@ func getDownloadFile(c *gin.Context) {
f, err := os.Open(p)
if err != nil {
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
@@ -101,5 +100,5 @@ func getDownloadFile(c *gin.Context) {
c.Header("Content-Disposition", "attachment; filename="+strconv.Quote(st.Name()))
c.Header("Content-Type", "application/octet-stream")
bufio.NewReader(f).WriteTo(c.Writer)
_, _ = bufio.NewReader(f).WriteTo(c.Writer)
}

View File

@@ -35,7 +35,7 @@ func getServerLogs(c *gin.Context) {
out, err := s.ReadLogfile(l)
if err != nil {
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
@@ -109,7 +109,7 @@ func postServerCommands(c *gin.Context) {
s := ExtractServer(c)
if running, err := s.Environment.IsRunning(c.Request.Context()); err != nil {
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
} else if !running {
c.AbortWithStatusJSON(http.StatusBadGateway, gin.H{
@@ -143,7 +143,7 @@ func postServerSync(c *gin.Context) {
s := ExtractServer(c)
if err := s.Sync(); err != nil {
WithError(c, err)
middleware.CaptureAndAbort(c, err)
} else {
c.Status(http.StatusNoContent)
}
@@ -153,9 +153,15 @@ func postServerSync(c *gin.Context) {
func postServerInstall(c *gin.Context) {
s := ExtractServer(c)
go func(serv *server.Server) {
if err := serv.Install(true); err != nil {
serv.Log().WithField("error", err).Error("failed to execute server installation process")
go func(s *server.Server) {
s.Log().Info("syncing server state with remote source before executing installation process")
if err := s.Sync(); err != nil {
s.Log().WithField("error", err).Error("failed to sync server state with Panel")
return
}
if err := s.Install(); err != nil {
s.Log().WithField("error", err).Error("failed to execute server installation process")
}
}(s)
@@ -211,7 +217,7 @@ func deleteServer(c *gin.Context) {
// forcibly terminate it before removing the container, so we do not need to handle
// that here.
if err := s.Environment.Destroy(); err != nil {
_ = WithError(c, err)
middleware.CaptureAndAbort(c, err)
return
}

View File

@@ -79,7 +79,7 @@ func getServerListDirectory(c *gin.Context) {
s := ExtractServer(c)
dir := c.Query("directory")
if stats, err := s.Filesystem().ListDirectory(dir); err != nil {
WithError(c, err)
middleware.CaptureAndAbort(c, err)
} else {
c.JSON(http.StatusOK, stats)
}
@@ -152,7 +152,7 @@ func putServerRenameFiles(c *gin.Context) {
return
}
NewServerError(err, s).AbortFilesystemError(c)
middleware.CaptureAndAbort(c, err)
return
}
@@ -172,11 +172,11 @@ func postServerCopyFile(c *gin.Context) {
}
if err := s.Filesystem().IsIgnored(data.Location); err != nil {
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
if err := s.Filesystem().Copy(data.Location); err != nil {
NewServerError(err, s).AbortFilesystemError(c)
middleware.CaptureAndAbort(c, err)
return
}
@@ -221,7 +221,7 @@ func postServerDeleteFiles(c *gin.Context) {
}
if err := g.Wait(); err != nil {
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
@@ -236,7 +236,7 @@ func postServerWriteFile(c *gin.Context) {
f = "/" + strings.TrimLeft(f, "/")
if err := s.Filesystem().IsIgnored(f); err != nil {
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
if err := s.Filesystem().Writefile(f, c.Request.Body); err != nil {
@@ -247,7 +247,7 @@ func postServerWriteFile(c *gin.Context) {
return
}
NewServerError(err, s).AbortFilesystemError(c)
middleware.CaptureAndAbort(c, err)
return
}
@@ -294,12 +294,12 @@ func postServerPullRemoteFile(c *gin.Context) {
})
return
}
WithError(c, err)
middleware.CaptureAndAbort(c, err)
return
}
if err := s.Filesystem().HasSpaceErr(true); err != nil {
WithError(c, err)
middleware.CaptureAndAbort(c, err)
return
}
// Do not allow more than three simultaneous remote file downloads at one time.
@@ -338,13 +338,13 @@ func postServerPullRemoteFile(c *gin.Context) {
}
if err := download(); err != nil {
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
st, err := s.Filesystem().Stat(dl.Path())
if err != nil {
NewServerError(err, s).AbortFilesystemError(c)
middleware.CaptureAndAbort(c, err)
return
}
c.JSON(http.StatusOK, &st)
@@ -380,7 +380,7 @@ func postServerCreateDirectory(c *gin.Context) {
return
}
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
@@ -415,7 +415,7 @@ func postServerCompressFiles(c *gin.Context) {
f, err := s.Filesystem().CompressFiles(data.RootPath, data.Files)
if err != nil {
NewServerError(err, s).AbortFilesystemError(c)
middleware.CaptureAndAbort(c, err)
return
}
@@ -533,7 +533,7 @@ func postServerChmodFile(c *gin.Context) {
return
}
NewServerError(err, s).AbortFilesystemError(c)
middleware.CaptureAndAbort(c, err)
return
}
@@ -545,7 +545,7 @@ func postServerUploadFiles(c *gin.Context) {
token := tokens.UploadPayload{}
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
NewTrackedError(err).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
@@ -591,14 +591,14 @@ func postServerUploadFiles(c *gin.Context) {
for _, header := range headers {
p, err := s.Filesystem().SafePath(filepath.Join(directory, header.Filename))
if err != nil {
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
// We run this in a different method so I can use defer without any of
// the consequences caused by calling it in a loop.
if err := handleFileUpload(p, s, header); err != nil {
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
} else {
s.SaveActivity(s.NewRequestActivity(token.UserUuid, c.ClientIP()), server.ActivityFileUploaded, models.ActivityMeta{

View File

@@ -34,7 +34,7 @@ func getServerWebsocket(c *gin.Context) {
handler, err := websocket.GetHandler(s, c.Writer, c.Request, c)
if err != nil {
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
defer handler.Connection.Close()

View File

@@ -20,12 +20,28 @@ import (
func getSystemInformation(c *gin.Context) {
i, err := system.GetSystemInformation()
if err != nil {
NewTrackedError(err).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
if c.Query("v") == "2" {
c.JSON(http.StatusOK, i)
return
}
c.JSON(http.StatusOK, struct {
Architecture string `json:"architecture"`
CPUCount int `json:"cpu_count"`
KernelVersion string `json:"kernel_version"`
OS string `json:"os"`
Version string `json:"version"`
}{
Architecture: i.System.Architecture,
CPUCount: i.System.CPUThreads,
KernelVersion: i.System.KernelVersion,
OS: i.System.OSType,
Version: i.Version,
})
}
// Returns all the servers that are registered and configured correctly on
@@ -75,7 +91,7 @@ func postCreateServer(c *gin.Context) {
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")
return
}
@@ -117,7 +133,7 @@ func postUpdateConfiguration(c *gin.Context) {
// Try to write this new configuration to the disk before updating our global
// state with it.
if err := config.WriteToDisk(cfg); err != nil {
_ = WithError(c, err)
middleware.CaptureAndAbort(c, err)
return
}
// Since we wrote it to the disk successfully now update the global configuration

View File

@@ -38,14 +38,14 @@ func postTransfers(c *gin.Context) {
token := tokens.TransferPayload{}
if err := tokens.ParseToken([]byte(auth[1]), &token); err != nil {
NewTrackedError(err).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
manager := middleware.ExtractManager(c)
u, err := uuid.Parse(token.Subject)
if err != nil {
NewTrackedError(err).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
@@ -70,7 +70,7 @@ func postTransfers(c *gin.Context) {
if err := manager.Client().SetTransferStatus(context.Background(), trnsfr.Server.ID(), false); err != nil {
trnsfr.Log().WithField("status", false).WithError(err).Error("failed to set transfer status")
}
NewTrackedError(err).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
@@ -123,13 +123,13 @@ func postTransfers(c *gin.Context) {
mediaType, params, err := mime.ParseMediaType(c.GetHeader("Content-Type"))
if err != nil {
trnsfr.Log().Debug("failed to parse content type header")
NewTrackedError(err).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
if !strings.HasPrefix(mediaType, "multipart/") {
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
}
@@ -156,7 +156,7 @@ out:
break out
}
if err != nil {
NewTrackedError(err).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
@@ -166,13 +166,13 @@ out:
trnsfr.Log().Debug("received archive")
if err := trnsfr.Server.EnsureDataDirectoryExists(); err != nil {
NewTrackedError(err).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
tee := io.TeeReader(p, h)
if err := trnsfr.Server.Filesystem().ExtractStreamUnsafe(ctx, "/", tee); err != nil {
NewTrackedError(err).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
@@ -181,7 +181,7 @@ out:
trnsfr.Log().Debug("received checksum")
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
}
@@ -189,14 +189,14 @@ out:
v, err := io.ReadAll(p)
if err != nil {
NewTrackedError(err).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
expected := make([]byte, hex.DecodedLen(len(v)))
n, err := hex.Decode(expected, v)
if err != nil {
NewTrackedError(err).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
actual := h.Sum(nil)
@@ -207,7 +207,7 @@ out:
}).Debug("checksums")
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
}
@@ -220,12 +220,12 @@ out:
}
if !hasArchive || !hasChecksum {
NewTrackedError(errors.New("missing archive or checksum")).Abort(c)
middleware.CaptureAndAbort(c, errors.New("missing archive or checksum"))
return
}
if !checksumVerified {
NewTrackedError(errors.New("checksums don't match")).Abort(c)
middleware.CaptureAndAbort(c, errors.New("checksums don't match"))
return
}
@@ -235,7 +235,7 @@ out:
// Ensure the server environment gets configured.
if err := trnsfr.Server.CreateEnvironment(); err != nil {
NewTrackedError(err).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}

View File

@@ -18,6 +18,7 @@ const (
ErrCodePathResolution ErrorCode = "E_BADPATH"
ErrCodeDenylistFile ErrorCode = "E_DENYLIST"
ErrCodeUnknownError ErrorCode = "E_UNKNOWN"
ErrNotExist ErrorCode = "E_NOTEXIST"
)
type Error struct {
@@ -68,6 +69,8 @@ func (e *Error) Error() string {
r = "<empty>"
}
return fmt.Sprintf("filesystem: server path [%s] resolves to a location outside the server root: %s", e.path, r)
case ErrNotExist:
return "filesystem: does not exist"
case ErrCodeUnknownError:
fallthrough
default:

View File

@@ -61,25 +61,28 @@ func (fs *Filesystem) Path() string {
func (fs *Filesystem) File(p string) (*os.File, Stat, error) {
cleaned, err := fs.SafePath(p)
if err != nil {
return nil, Stat{}, err
return nil, Stat{}, errors.WithStackIf(err)
}
st, err := fs.Stat(cleaned)
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() {
return nil, Stat{}, newFilesystemError(ErrCodeIsDirectory, nil)
}
f, err := os.Open(cleaned)
if err != nil {
return nil, Stat{}, err
return nil, Stat{}, errors.WithStackIf(err)
}
return f, st, nil
}
// Acts by creating the given file and path on the disk if it is not present already. If
// it is present, the file is opened using the defaults which will truncate the contents.
// The opened file is then returned to the caller.
// Touch acts by creating the given file and path on the disk if it is not present
// already. If it is present, the file is opened using the defaults which will truncate
// the contents. The opened file is then returned to the caller.
func (fs *Filesystem) Touch(p string, flag int) (*os.File, error) {
cleaned, err := fs.SafePath(p)
if err != nil {

View File

@@ -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) {
g := Goblin(t)
fs, rfs := NewFs()

View File

@@ -32,19 +32,17 @@ import (
//
// Pass true as the first argument in order to execute a server sync before the
// process to ensure the latest information is used.
func (s *Server) Install(sync bool) error {
if sync {
s.Log().Info("syncing server state with remote source before executing installation process")
if err := s.Sync(); err != nil {
return errors.WrapIf(err, "install: failed to sync server state with Panel")
}
}
func (s *Server) Install() error {
return s.install(false)
}
func (s *Server) install(reinstall bool) error {
var err error
if !s.Config().SkipEggScripts {
// Send the start event so the Panel can automatically update. We don't send this unless the process
// is actually going to run, otherwise all sorts of weird rapid UI behavior happens since there isn't
// an actual install process being executed.
// Send the start event so the Panel can automatically update. We don't
// send this unless the process is actually going to run, otherwise all
// sorts of weird rapid UI behavior happens since there isn't an actual
// install process being executed.
s.Events().Publish(InstallStartedEvent, "")
err = s.internalInstall()
@@ -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")
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)
// If the request was successful but there was an error with this request, attach the
// error to this log entry. Otherwise ignore it in this log since whatever is calling
// this function should handle the error and will end up logging the same one.
// If the request was successful but there was an error with this request,
// attach the error to this log entry. Otherwise, ignore it in this log
// since whatever is calling this function should handle the error and
// will end up logging the same one.
if err == nil {
l.WithField("error", err)
}
@@ -66,19 +65,20 @@ func (s *Server) Install(sync bool) error {
l.Warn("failed to notify panel of server install state")
}
// Ensure that the server is marked as offline at this point, otherwise you end up
// with a blank value which is a bit confusing.
// Ensure that the server is marked as offline at this point, otherwise you
// end up with a blank value which is a bit confusing.
s.Environment.SetState(environment.ProcessOfflineState)
// Push an event to the websocket so we can auto-refresh the information in the panel once
// the install is completed.
// Push an event to the websocket, so we can auto-refresh the information in
// the panel once the installation is completed.
s.Events().Publish(InstallCompletedEvent, "")
return errors.WithStackIf(err)
}
// Reinstalls a server's software by utilizing the install script for the server egg. This
// does not touch any existing files for the server, other than what the script modifies.
// Reinstall reinstalls a server's software by utilizing the installation script
// for the server egg. This does not touch any existing files for the server,
// other than what the script modifies.
func (s *Server) Reinstall() error {
if s.Environment.State() != environment.ProcessOfflineState {
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.
@@ -116,8 +121,9 @@ type InstallationProcess struct {
client *client.Client
}
// Generates a new installation process struct that will be used to create containers,
// and otherwise perform installation commands for a server.
// NewInstallationProcess returns a new installation process struct that will be
// used to create containers and otherwise perform installation commands for a
// server.
func NewInstallationProcess(s *Server, script *remote.InstallationScript) (*InstallationProcess, error) {
proc := &InstallationProcess{
Script: script,
@@ -133,8 +139,8 @@ func NewInstallationProcess(s *Server, script *remote.InstallationScript) (*Inst
return proc, nil
}
// Determines if the server is actively running the installation process by checking the status
// of the installer lock.
// IsInstalling returns if the server is actively running the installation
// process by checking the status of the installer lock.
func (s *Server) IsInstalling() bool {
return s.installing.Load()
}
@@ -155,7 +161,7 @@ func (s *Server) SetRestoring(state bool) {
s.restoring.Store(state)
}
// Removes the installer container for the server.
// RemoveContainer removes the installation container for the server.
func (ip *InstallationProcess) RemoveContainer() error {
err := ip.client.ContainerRemove(ip.Server.Context(), ip.Server.ID()+"_installer", types.ContainerRemoveOptions{
RemoveVolumes: true,
@@ -328,14 +334,14 @@ func (ip *InstallationProcess) BeforeExecute() error {
return nil
}
// Returns the log path for the installation process.
// GetLogPath returns the log path for the installation process.
func (ip *InstallationProcess) GetLogPath() string {
return filepath.Join(config.Get().System.LogDirectory, "/install", ip.Server.ID()+".log")
}
// Cleans up after the execution of the installation process. This grabs the logs from the
// process to store in the server configuration directory, and then destroys the associated
// installation container.
// AfterExecute cleans up after the execution of the installation process.
// This grabs the logs from the process to store in the server configuration
// directory, and then destroys the associated installation container.
func (ip *InstallationProcess) AfterExecute(containerId string) error {
defer ip.RemoveContainer()
@@ -525,7 +531,7 @@ func (ip *InstallationProcess) StreamOutput(ctx context.Context, id string) erro
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
// 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
@@ -537,8 +543,8 @@ func (ip *InstallationProcess) StreamOutput(ctx context.Context, id string) erro
func (ip *InstallationProcess) resourceLimits() container.Resources {
limits := config.Get().Docker.InstallerLimits
// Create a copy of the configuration so we're not accidentally making changes
// to the underlying server build data.
// Create a copy of the configuration, so we're not accidentally making
// changes to the underlying server build data.
c := *ip.Server.Config()
cfg := c.Build
if cfg.MemoryLimit < limits.Memory {
@@ -562,10 +568,12 @@ func (ip *InstallationProcess) resourceLimits() container.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
// server is. A boolean value of "true" means everything was successful, "false"
// means something went wrong and the server must be deleted and re-created.
func (s *Server) SyncInstallState(successful bool) error {
return s.client.SetInstallationStatus(s.Context(), s.ID(), successful)
// server is.
func (s *Server) SyncInstallState(successful, reinstall bool) error {
return s.client.SetInstallationStatus(s.Context(), s.ID(), remote.InstallStatusRequest{
Successful: successful,
Reinstall: reinstall,
})
}

View File

@@ -1,17 +1,57 @@
package system
import (
"context"
"runtime"
"github.com/acobaugh/osrelease"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/parsers/kernel"
)
type Information struct {
Version string `json:"version"`
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"`
CPUThreads int `json:"cpu_threads"`
MemoryBytes int64 `json:"memory_bytes"`
KernelVersion string `json:"kernel_version"`
OS string `json:"os"`
CpuCount int `json:"cpu_count"`
OSType string `json:"os_type"`
}
func GetSystemInformation() (*Information, error) {
@@ -20,13 +60,83 @@ func GetSystemInformation() (*Information, error) {
return nil, err
}
s := &Information{
Version: Version,
KernelVersion: k.String(),
Architecture: runtime.GOARCH,
OS: runtime.GOOS,
CpuCount: runtime.NumCPU(),
version, info, err := GetDockerInfo(context.Background())
if err != nil {
return nil, err
}
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
}