Cleanup request error handling; properly handle os.ErrNotExist errors (#150)

This commit is contained in:
Dane Everitt 2022-11-22 10:18:27 -08:00 committed by GitHub
parent 9226ccae31
commit ff50d0e5bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 236 additions and 341 deletions

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)
}
@ -217,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,7 +20,7 @@ import (
func getSystemInformation(c *gin.Context) {
i, err := system.GetSystemInformation()
if err != nil {
NewTrackedError(err).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
@ -133,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()