diff --git a/router/error.go b/router/error.go deleted file mode 100644 index 4dd2b2a..0000000 --- a/router/error.go +++ /dev/null @@ -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) -} diff --git a/router/middleware/middleware.go b/router/middleware/middleware.go index d102174..cd37a9e 100644 --- a/router/middleware/middleware.go +++ b/router/middleware/middleware.go @@ -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() diff --git a/router/middleware/request_error.go b/router/middleware/request_error.go new file mode 100644 index 0000000..9ac6fd2 --- /dev/null +++ b/router/middleware/request_error.go @@ -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, "" +} diff --git a/router/router.go b/router/router.go index c978118..d748e27 100644 --- a/router/router.go +++ b/router/router.go @@ -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. diff --git a/router/router_download.go b/router/router_download.go index 6e55ec0..2ebc646 100644 --- a/router/router_download.go +++ b/router/router_download.go @@ -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) } diff --git a/router/router_server.go b/router/router_server.go index df721cf..1cd4a98 100644 --- a/router/router_server.go +++ b/router/router_server.go @@ -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 } diff --git a/router/router_server_files.go b/router/router_server_files.go index dc48a14..4c5ec1e 100644 --- a/router/router_server_files.go +++ b/router/router_server_files.go @@ -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{ diff --git a/router/router_server_ws.go b/router/router_server_ws.go index d71635f..bca88d8 100644 --- a/router/router_server_ws.go +++ b/router/router_server_ws.go @@ -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() diff --git a/router/router_system.go b/router/router_system.go index 013abfc..b40e1d1 100644 --- a/router/router_system.go +++ b/router/router_system.go @@ -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 diff --git a/router/router_transfer.go b/router/router_transfer.go index b57b304..1e78a39 100644 --- a/router/router_transfer.go +++ b/router/router_transfer.go @@ -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 } diff --git a/server/filesystem/errors.go b/server/filesystem/errors.go index ad05da0..afae74a 100644 --- a/server/filesystem/errors.go +++ b/server/filesystem/errors.go @@ -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 = "" } 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: diff --git a/server/filesystem/filesystem.go b/server/filesystem/filesystem.go index cb70b44..cdfe506 100644 --- a/server/filesystem/filesystem.go +++ b/server/filesystem/filesystem.go @@ -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 { diff --git a/server/filesystem/filesystem_test.go b/server/filesystem/filesystem_test.go index b56ecec..7a175c0 100644 --- a/server/filesystem/filesystem_test.go +++ b/server/filesystem/filesystem_test.go @@ -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()