wings/router/error.go

157 lines
5.7 KiB
Go
Raw Normal View History

2020-04-06 01:00:33 +00:00
package router
import (
"fmt"
2021-01-10 01:22:39 +00:00
"net/http"
"os"
"strings"
"emperror.dev/errors"
2020-06-13 17:26:35 +00:00
"github.com/apex/log"
2020-04-06 01:00:33 +00:00
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/server/filesystem"
2020-04-06 01:00:33 +00:00
)
type RequestError struct {
2020-12-16 05:08:00 +00:00
err error
uuid string
message string
2020-04-06 01:00:33 +00:00
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))
}
2020-04-06 01:00:33 +00:00
// 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.
2020-12-16 05:08:00 +00:00
func NewTrackedError(err error) *RequestError {
2020-04-06 01:00:33 +00:00
return &RequestError{
2020-12-16 05:08:00 +00:00
err: err,
uuid: uuid.Must(uuid.NewRandom()).String(),
2020-04-06 01:00:33 +00:00
}
}
2020-12-16 05:08:00 +00:00
// Same as NewTrackedError, except this will also attach the server instance that
2020-04-06 01:00:33 +00:00
// generated this server for the purposes of logging.
2020-12-16 05:08:00 +00:00
func NewServerError(err error, s *server.Server) *RequestError {
2020-04-06 01:00:33 +00:00
return &RequestError{
2020-12-16 05:08:00 +00:00
err: err,
uuid: uuid.Must(uuid.NewRandom()).String(),
server: s,
2020-04-06 01:00:33 +00:00
}
}
2020-06-13 17:26:35 +00:00
func (e *RequestError) logger() *log.Entry {
if e.server != nil {
return e.server.Log().WithField("error_id", e.uuid).WithField("error", e.err)
2020-06-13 17:26:35 +00:00
}
return log.WithField("error_id", e.uuid).WithField("error", e.err)
2020-06-13 17:26:35 +00:00
}
2020-04-06 01:00:33 +00:00
// Sets the output message to display to the user in the error.
func (e *RequestError) SetMessage(msg string) *RequestError {
2020-12-16 05:08:00 +00:00
e.message = msg
2020-04-06 01:00:33 +00:00
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) {
2020-12-16 05:08:00 +00:00
// 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()
}
2020-04-06 01:00:33 +00:00
// 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.
2020-12-16 05:08:00 +00:00
if errors.Is(e.err, os.ErrNotExist) {
2020-04-06 01:00:33 +00:00
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested resource was not found on the system.",
})
return
}
2020-12-17 05:38:56 +00:00
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
}
2020-12-16 05:08:00 +00:00
// 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
}
2020-04-06 01:00:33 +00:00
// 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")
2020-04-12 00:48:57 +00:00
} else {
e.logger().Debug("non-server error encountered while handling HTTP request")
2020-04-06 01:00:33 +00:00
}
2020-12-16 05:08:00 +00:00
if e.message == "" {
e.message = "An unexpected error was encountered while processing this request."
2020-04-06 01:00:33 +00:00
}
2020-12-16 05:08:00 +00:00
c.AbortWithStatusJSON(status, gin.H{"error": e.message, "error_id": e.uuid})
2020-04-06 01:00:33 +00:00
}
// Helper function to just abort with an internal server error. This is generally the response
// from most errors encountered by the API.
2020-12-16 04:19:09 +00:00
func (e *RequestError) Abort(c *gin.Context) {
2020-04-06 01:00:33 +00:00
e.AbortWithStatus(http.StatusInternalServerError, c)
}
2020-12-16 05:08:00 +00:00
// 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: file is a directory."
}
if strings.HasSuffix(e.err.Error(), "file name too long") {
2020-12-16 05:08:00 +00:00
return http.StatusBadRequest, "Cannot perform that action: file name is too long."
}
if e, ok := e.err.(*os.SyscallError); ok && e.Syscall == "readdirent" {
2020-12-16 05:08:00 +00:00
return http.StatusNotFound, "The requested directory does not exist."
}
2020-12-16 05:08:00 +00:00
return 0, ""
}
2020-12-16 05:08:00 +00:00
// Handle specific filesystem errors for a server.
func (e *RequestError) AbortFilesystemError(c *gin.Context) {
2020-12-16 04:19:09 +00:00
e.Abort(c)
}
2020-04-06 01:00:33 +00:00
// Format the error to a string and include the UUID.
func (e *RequestError) Error() string {
2020-12-16 05:08:00 +00:00
return fmt.Sprintf("%v (uuid: %s)", e.err, e.uuid)
2020-04-06 01:00:33 +00:00
}