From 314a5ad5467fe82f0937794bda0857c8f1351835 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 6 Apr 2019 16:53:22 -0700 Subject: [PATCH] Add FS logic for determining folder size as well as safe path resolution --- go.mod | 1 + go.sum | 2 + server/filesystem.go | 158 +++++++++++++++++++++++++++++++++++++++++-- server/server.go | 12 +++- 4 files changed, 167 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 56c3262..de8b1d4 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/onsi/gomega v1.5.0 // indirect github.com/opencontainers/go-digest v1.0.0-rc1 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.8.0 github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce diff --git a/go.sum b/go.sum index 7db1f02..b349387 100644 --- a/go.sum +++ b/go.sum @@ -44,6 +44,8 @@ github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2i github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/server/filesystem.go b/server/filesystem.go index 29fa4c7..bfd47de 100644 --- a/server/filesystem.go +++ b/server/filesystem.go @@ -1,6 +1,18 @@ package server -import "path" +import ( + "errors" + "go.uber.org/zap" + "io/ioutil" + "os" + "path/filepath" + "strings" + "sync" + "time" +) + +// Error returned when there is a bad path provided to one of the FS calls. +var InvalidPathResolution = errors.New("invalid path resolution") type Filesystem struct { // The root directory where all of the server data is contained. By default @@ -13,10 +25,146 @@ type Filesystem struct { // Returns the root path that contains all of a server's data. func (fs *Filesystem) Path() string { - return path.Join(fs.Root, fs.Server.Uuid) + return filepath.Join(fs.Root, fs.Server.Uuid) } -// Returns a safe path for a server object. -func (fs *Filesystem) SafePath(p string) string { - return fs.Path() +// Normalizes a directory being passed in to ensure the user is not able to escape +// from their data directory. After normalization if the directory is still within their home +// path it is returned. If they managed to "escape" an error will be returned. +// +// This logic is actually copied over from the SFTP server code. Ideally that eventually +// either gets ported into this application, or is able to make use of this package. +func (fs *Filesystem) SafePath(p string) (string, error) { + var nonExistentPathResolution string + + // Calling filpath.Clean on the joined directory will resolve it to the absolute path, + // removing any ../ type of resolution arguments, and leaving us with a direct path link. + r := filepath.Clean(filepath.Join(fs.Path(), p)) + + // At the same time, evaluate the symlink status and determine where this file or folder + // is truly pointing to. + p, err := filepath.EvalSymlinks(r) + if err != nil && !os.IsNotExist(err) { + return "", err + } else if os.IsNotExist(err) { + // The requested directory doesn't exist, so at this point we need to iterate up the + // path chain until we hit a directory that _does_ exist and can be validated. + parts := strings.Split(filepath.Dir(r), "/") + + var try string + // Range over all of the path parts and form directory pathings from the end + // moving up until we have a valid resolution or we run out of paths to try. + for k := range parts { + try = strings.Join(parts[:(len(parts) - k)], "/") + + if !strings.HasPrefix(try, fs.Path()) { + break + } + + t, err := filepath.EvalSymlinks(try) + if err == nil { + nonExistentPathResolution = t + break + } + } + } + + // If the new path doesn't start with their root directory there is clearly an escape + // attempt going on, and we should NOT resolve this path for them. + if nonExistentPathResolution != "" { + if !strings.HasPrefix(nonExistentPathResolution, fs.Path()) { + return "", InvalidPathResolution + } + + // If the nonExistentPathResoltion variable is not empty then the initial path requested + // did not exist and we looped through the pathway until we found a match. At this point + // we've confirmed the first matched pathway exists in the root server directory, so we + // can go ahead and just return the path that was requested initially. + return r, nil + } + + // If the requested directory from EvalSymlinks begins with the server root directory go + // ahead and return it. If not we'll return an error which will block any further action + // on the file. + if strings.HasPrefix(p, fs.Path()) { + return p, nil + } + + return "", InvalidPathResolution +} + +// Determines if the directory a file is trying to be added to has enough space available +// for the file to be written to. +// +// Because determining the amount of space being used by a server is a taxing operation we +// will load it all up into a cache and pull from that as long as the key is not expired. +func (fs *Filesystem) HasSpaceAvailable() bool { + var space = fs.Server.Build.DiskSpace + + // If space is -1 or 0 just return true, means they're allowed unlimited. + if space <= 0 { + return true + } + + var size int64 + if x, exists := fs.Server.Cache().Get("disk_used"); exists { + size = x.(int64) + } + + // If there is no size its either because there is no data (in which case running this function + // will have effectively no impact), or there is nothing in the cache, in which case we need to + // grab the size of their data directory. This is a taxing operation, so we want to store it in + // the cache once we've gotten it. + if size == 0 { + if size, err := fs.DirectorySize("/"); err != nil { + zap.S().Warnw("failed to determine directory size", zap.String("server", fs.Server.Uuid), zap.Error(err)) + } else { + fs.Server.Cache().Set("disk_used", size, time.Minute * 5) + } + } + + // Determine if their folder size, in bytes, is smaller than the amount of space they've + // been allocated. + return (size / 1024.0 / 1024.0) <= space +} + +// Determines the directory size of a given location by running parallel tasks to iterate +// through all of the folders. Returns the size in bytes. This can be a fairly taxing operation +// on locations with tons of files, so it is recommended that you cache the output. +func (fs *Filesystem) DirectorySize(dir string) (int64, error) { + var size int64 + var wg sync.WaitGroup + + cleaned, err := fs.SafePath(dir) + if err != nil { + return 0, err + } + + files, err := ioutil.ReadDir(cleaned) + if err != nil { + return 0, err + } + + // Iterate over all of the files and directories. If it is a file, immediately add its size + // to the total size being returned. If we're dealing with a directory, call this function + // on a seperate thread until we have gotten the size of everything nested within the given + // directory. + for _, f := range files { + if f.IsDir() { + wg.Add(1) + + go func(p string) { + defer wg.Done() + + s, _ := fs.DirectorySize(p) + size += s + }(filepath.Join(cleaned, f.Name())) + } else { + size += f.Size() + } + } + + wg.Wait() + + return size, nil } \ No newline at end of file diff --git a/server/server.go b/server/server.go index 989c689..ee32a6c 100644 --- a/server/server.go +++ b/server/server.go @@ -1,6 +1,7 @@ package server import ( + "github.com/patrickmn/go-cache" "github.com/pterodactyl/wings/config" "github.com/remeh/sizedwaitgroup" "go.uber.org/zap" @@ -9,6 +10,7 @@ import ( "os" "path" "strings" + "time" ) // High level definition for a server instance being controlled by Wings. @@ -43,6 +45,10 @@ type Server struct { environment Environment fs *Filesystem + + // Server cache used to store frequently requested information in memory and make + // certain long operations return faster. For example, FS disk space usage. + cache *cache.Cache } // The build settings for a given server that impact docker container creation and @@ -187,7 +193,7 @@ func FromConfiguration(data []byte, cfg *config.SystemConfiguration) (*Server, e } s.environment = env - + s.cache = cache.New(time.Minute * 10, time.Minute * 15) s.fs = &Filesystem{ Root: cfg.Data, Server: s, @@ -209,6 +215,10 @@ func (s *Server) Filesystem() *Filesystem { return s.fs } +func (s *Server) Cache() *cache.Cache { + return s.cache +} + // Determine if the server is bootable in it's current state or not. This will not // indicate why a server is not bootable, only if it is. func (s *Server) IsBootable() bool {