From bf1233def4b7ca5c78dad191b7817242a2e8e29b Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 24 Sep 2020 21:18:10 -0700 Subject: [PATCH] Don't ignore disk space limits when copying/archiving; closes pterodactyl/panel#2400 --- router/error.go | 34 +++++++++++++++++++++++++++++++++ router/router_server_files.go | 28 +++------------------------ server/configuration.go | 3 ++- server/filesystem.go | 35 ++++++++++++++++++++++++++++------ server/filesystem_unarchive.go | 2 +- 5 files changed, 69 insertions(+), 33 deletions(-) diff --git a/router/error.go b/router/error.go index 5755b7f..091edda 100644 --- a/router/error.go +++ b/router/error.go @@ -9,6 +9,7 @@ import ( "github.com/pterodactyl/wings/server" "net/http" "os" + "strings" ) type RequestError struct { @@ -95,6 +96,39 @@ func (e *RequestError) AbortWithServerError(c *gin.Context) { e.AbortWithStatus(http.StatusInternalServerError, c) } +// Handle specific filesystem errors for a server. +func (e *RequestError) AbortFilesystemError(c *gin.Context) { + if errors.Is(e.Err, os.ErrNotExist) { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{ + "error": "The requested resource was not found.", + }) + return + } + + if errors.Is(e.Err, server.ErrNotEnoughDiskSpace) { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ + "error": server.ErrNotEnoughDiskSpace.Error(), + }) + return + } + + if strings.HasSuffix(e.Err.Error(), "file name too long") { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ + "error": "File name is too long.", + }) + return + } + + if e, ok := e.Err.(*os.SyscallError); ok && e.Syscall == "readdirent" { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{ + "error": "The requested directory does not exist.", + }) + return + } + + e.AbortWithServerError(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/router_server_files.go b/router/router_server_files.go index 640e6dd..878d835 100644 --- a/router/router_server_files.go +++ b/router/router_server_files.go @@ -82,14 +82,7 @@ func getServerListDirectory(c *gin.Context) { stats, err := s.Filesystem.ListDirectory(d) if err != nil { - if e, ok := err.(*os.SyscallError); ok && e.Syscall == "readdirent" { - c.AbortWithStatusJSON(http.StatusNotFound, gin.H{ - "error": "The requested directory does not exist.", - }) - return - } - - TrackedServerError(err, s).AbortWithServerError(c) + TrackedServerError(err, s).AbortFilesystemError(c) return } @@ -156,14 +149,7 @@ func putServerRenameFiles(c *gin.Context) { return } - if strings.HasSuffix(err.Error(), "file name too long") { - c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ - "error": "Cannot move or rename file, name is too long.", - }) - return - } - - TrackedServerError(err, s).AbortWithServerError(c) + TrackedServerError(err, s).AbortFilesystemError(c) return } @@ -183,15 +169,7 @@ func postServerCopyFile(c *gin.Context) { } if err := s.Filesystem.Copy(data.Location); err != nil { - // Check if the file does not exist. - // NOTE: os.IsNotExist() does not work if the error is wrapped. - if errors.Is(err, os.ErrNotExist) { - c.Status(http.StatusNotFound) - return - } - - TrackedServerError(err, s).AbortWithServerError(c) - return + TrackedServerError(err, s).AbortFilesystemError(c) } c.Status(http.StatusNoContent) diff --git a/server/configuration.go b/server/configuration.go index 22f558d..3f82294 100644 --- a/server/configuration.go +++ b/server/configuration.go @@ -47,11 +47,12 @@ func (s *Server) Config() *Configuration { return &s.cfg } +// Returns the amount of disk space available to a server in bytes. func (s *Server) DiskSpace() int64 { s.cfg.mu.RLock() defer s.cfg.mu.RUnlock() - return s.cfg.Build.DiskSpace + return s.cfg.Build.DiskSpace * 1000.0 * 1000.0 } func (s *Server) MemoryLimit() int64 { diff --git a/server/filesystem.go b/server/filesystem.go index 398cd3e..0246478 100644 --- a/server/filesystem.go +++ b/server/filesystem.go @@ -243,7 +243,7 @@ func (fs *Filesystem) HasSpaceAvailable(allowStaleValue bool) bool { return true } - return (size / 1000.0 / 1000.0) <= space + return size <= space } // Internal helper function to allow other parts of the codebase to check the total used disk space @@ -581,15 +581,14 @@ func (fs *Filesystem) Chown(path string) error { // Copies a given file to the same location and appends a suffix to the file to indicate that // it has been copied. -// -// @todo need to get an exclusive lock on the file. func (fs *Filesystem) Copy(p string) error { cleaned, err := fs.SafePath(p) if err != nil { return errors.WithStack(err) } - if s, err := os.Stat(cleaned); err != nil { + s, err := os.Stat(cleaned); + if err != nil { return errors.WithStack(err) } else if s.IsDir() || !s.Mode().IsRegular() { // If this is a directory or not a regular file, just throw a not-exist error @@ -597,6 +596,12 @@ func (fs *Filesystem) Copy(p string) error { return os.ErrNotExist } + // Check that copying this file wouldn't put the server over its limit. + curSize := atomic.LoadInt64(&fs.disk) + if (curSize + s.Size()) > fs.Server.DiskSpace() { + return ErrNotEnoughDiskSpace + } + base := filepath.Base(cleaned) relative := strings.TrimSuffix(strings.TrimPrefix(cleaned, fs.Path()), base) extension := filepath.Ext(base) @@ -665,6 +670,9 @@ func (fs *Filesystem) Copy(p string) error { return errors.WithStack(err) } + // Once everything is done, increment the disk space used. + atomic.AddInt64(&fs.disk, s.Size()) + return nil } @@ -927,10 +935,25 @@ func (fs *Filesystem) CompressFiles(dir string, paths []string) (os.FileInfo, er } a := &backup.Archive{TrimPrefix: fs.Path(), Files: inc} - d := path.Join(cleanedRootDir, fmt.Sprintf("archive-%s.tar.gz", strings.ReplaceAll(time.Now().Format(time.RFC3339), ":", ""))) - return a.Create(d, context.Background()) + f, err := a.Create(d, context.Background()) + if err != nil { + return nil, errors.WithStack(err) + } + + curSize := atomic.LoadInt64(&fs.disk) + if (curSize + f.Size()) > fs.Server.DiskSpace() { + // Exceeding space limits, delete archive and return error. Kind of a waste of resources + // I suppose, but oh well. + _ = os.Remove(d) + + return nil, ErrNotEnoughDiskSpace + } + + atomic.AddInt64(&fs.disk, f.Size()) + + return f, nil } // Handle errors encountered when walking through directories. diff --git a/server/filesystem_unarchive.go b/server/filesystem_unarchive.go index 6173e19..128a1d1 100644 --- a/server/filesystem_unarchive.go +++ b/server/filesystem_unarchive.go @@ -35,7 +35,7 @@ func (fs *Filesystem) SpaceAvailableForDecompression(dir string, file string) (b dirSize, err := fs.DiskUsage(false) var size int64 - var max = fs.Server.DiskSpace() * 1000.0 * 1000.0 + var max = fs.Server.DiskSpace() // Walk over the archive and figure out just how large the final output would be from unarchiving it. err = archiver.Walk(source, func(f archiver.File) error { if atomic.AddInt64(&size, f.Size())+dirSize > max {