From a33ac304cae2a99f7f95a88e34835b0134c395e4 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 2 Aug 2021 20:02:27 -0700 Subject: [PATCH 01/17] Perhaps don't break _everything_ on people. --- server/server.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/server.go b/server/server.go index 876a3d8..3526faa 100644 --- a/server/server.go +++ b/server/server.go @@ -99,6 +99,14 @@ func (s *Server) ID() string { return s.Config().GetUuid() } +// Id returns the UUID for the server instance. This function is deprecated +// in favor of Server.ID(). +// +// Deprecated +func (s *Server) Id() string { + return s.ID() +} + // Cancels the context assigned to this server instance. Assuming background tasks // are using this server's context for things, all of the background tasks will be // stopped as a result. From ec57f43dd46699170300506f9cf3f33d0ff24f29 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 2 Aug 2021 20:15:25 -0700 Subject: [PATCH 02/17] Add deprecation flag on the directory, don't remove it entirely --- router/router_server_files.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/router/router_server_files.go b/router/router_server_files.go index 1b70f89..b8202c3 100644 --- a/router/router_server_files.go +++ b/router/router_server_files.go @@ -254,13 +254,20 @@ func getServerPullingFiles(c *gin.Context) { func postServerPullRemoteFile(c *gin.Context) { s := ExtractServer(c) var data struct { - RootPath string `binding:"required,omitempty" json:"root"` - URL string `binding:"required" json:"url"` + // Deprecated + Directory string `binding:"required_without=RootPath,omitempty" json:"directory"` + RootPath string `binding:"required_without=Directory,omitempty" json:"root"` + URL string `binding:"required" json:"url"` } if err := c.BindJSON(&data); err != nil { return } + // Handle the deprecated Directory field in the struct until it is removed. + if data.Directory != "" && data.RootPath == "" { + data.RootPath = data.Directory + } + u, err := url.Parse(data.URL) if err != nil { if e, ok := err.(*url.Error); ok { From 930abfb4a7c6665812cb6937a104750e618edb57 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 2 Aug 2021 20:17:02 -0700 Subject: [PATCH 03/17] Update CHANGELOG.md --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70cd158..515e9b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## v1.4.6 +### Fixed +* Environment variable starting with the same prefix no longer get merged into a single environment variable value (skipping all but the first). +* The `start_on_completion` flag for server installs will now properly start the server. +* Fixes socket files unintentionally causing backups to be aborted. +* Files extracted from a backup now have their preior mode properly set on the restored files, rather than defaulting to 0644. +* Fixes logrotate issues due to a bad user configuration on some systems. + +### Updated +* The minimum Go version required to compile Wings is now `go1.16`. + +### Deprecated +> Both of these deprecations will be removed in `Wings@2.0.0`. + +* The `Server.Id()` method has been deprecated in favor of `Server.ID()`. +* The `directory` field on the `/api/servers/:server/files/pull` endpoint is deprecated and should be updated to use `root` instead for consistency with other endpoints. + ## v1.4.5 ### Changed * Upped the process limit for a container from `256` to `512` in order to address edge-cases for some games that spawn a lot of processes. From 25f3cb60cbbc37e6692a7b6b88ba35314c4f7283 Mon Sep 17 00:00:00 2001 From: Matthew Penner Date: Tue, 3 Aug 2021 16:26:25 -0600 Subject: [PATCH 04/17] server: actually use StartOnCompletion and CrashDetectionEnabled --- server/update.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/server/update.go b/server/update.go index cfbdfed..7a1eb2e 100644 --- a/server/update.go +++ b/server/update.go @@ -86,6 +86,24 @@ func (s *Server) UpdateDataStructure(data []byte) error { c.SkipEggScripts = v } + if v, err := jsonparser.GetBoolean(data, "start_on_completion"); err != nil { + if err != jsonparser.KeyPathNotFoundError { + return errors.WithStack(err) + } + } else { + c.StartOnCompletion = v + } + + if v, err := jsonparser.GetBoolean(data, "crash_detection_enabled"); err != nil { + if err != jsonparser.KeyPathNotFoundError { + return errors.WithStack(err) + } + // Enable crash detection by default. + c.CrashDetectionEnabled = true + } else { + c.CrashDetectionEnabled = v + } + // Environment and Mappings should be treated as a full update at all times, never a // true patch, otherwise we can't know what we're passing along. if src.EnvVars != nil && len(src.EnvVars) > 0 { From ca25ba5fab2a482f5f673495d9365b5a8fda17db Mon Sep 17 00:00:00 2001 From: Matthew Penner Date: Tue, 3 Aug 2021 20:56:02 -0600 Subject: [PATCH 05/17] sftp: deny access if server is suspended (#100) --- sftp/handler.go | 16 ++++++++++++---- sftp/server.go | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/sftp/handler.go b/sftp/handler.go index ba904f1..2f3f0b9 100644 --- a/sftp/handler.go +++ b/sftp/handler.go @@ -14,6 +14,7 @@ import ( "golang.org/x/crypto/ssh" "github.com/pterodactyl/wings/config" + "github.com/pterodactyl/wings/server" "github.com/pterodactyl/wings/server/filesystem" ) @@ -26,8 +27,10 @@ const ( ) type Handler struct { + mu sync.Mutex + permissions []string - mu sync.Mutex + server *server.Server fs *filesystem.Filesystem logger *log.Entry ro bool @@ -35,11 +38,12 @@ type Handler struct { // Returns a new connection handler for the SFTP server. This allows a given user // to access the underlying filesystem. -func NewHandler(sc *ssh.ServerConn, fs *filesystem.Filesystem) *Handler { +func NewHandler(sc *ssh.ServerConn, srv *server.Server) *Handler { return &Handler{ - fs: fs, - ro: config.Get().System.Sftp.ReadOnly, permissions: strings.Split(sc.Permissions.Extensions["permissions"], ","), + server: srv, + fs: srv.Filesystem(), + ro: config.Get().System.Sftp.ReadOnly, logger: log.WithFields(log.Fields{ "subsystem": "sftp", "username": sc.User(), @@ -278,6 +282,10 @@ func (h *Handler) Filelist(request *sftp.Request) (sftp.ListerAt, error) { // Determines if a user has permission to perform a specific action on the SFTP server. These // permissions are defined and returned by the Panel API. func (h *Handler) can(permission string) bool { + if h.server.IsSuspended() { + return false + } + // SFTPServer owners and super admins have their permissions returned as '[*]' via the Panel // API, so for the sake of speed do an initial check for that before iterating over the // entire array of permissions. diff --git a/sftp/server.go b/sftp/server.go index fda47b7..a2d03f0 100644 --- a/sftp/server.go +++ b/sftp/server.go @@ -141,7 +141,7 @@ func (c *SFTPServer) AcceptInbound(conn net.Conn, config *ssh.ServerConfig) { // Spin up a SFTP server instance for the authenticated user's server allowing // them access to the underlying filesystem. - handler := sftp.NewRequestServer(channel, NewHandler(sconn, srv.Filesystem()).Handlers()) + handler := sftp.NewRequestServer(channel, NewHandler(sconn, srv).Handlers()) if err := handler.Serve(); err == io.EOF { handler.Close() } From a2a02906ea90950fa91f4d42a3f78446de26e27d Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 3 Aug 2021 20:04:00 -0700 Subject: [PATCH 06/17] Update CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 515e9b7..a7231d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## v1.4.7 +### Fixed +* SFTP access is now properly denied if a server is suspended. +* Correctly uses `start_on_completion` and `crash_detection_enabled` for servers. + ## v1.4.6 ### Fixed * Environment variable starting with the same prefix no longer get merged into a single environment variable value (skipping all but the first). From f7c8571f468e87aeb8fa4658ac8129d8517ee778 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 15 Aug 2021 16:46:55 -0700 Subject: [PATCH 07/17] Fix race condition when setting app name in console output --- server/console.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/console.go b/server/console.go index 908673e..d5babaa 100644 --- a/server/console.go +++ b/server/console.go @@ -19,6 +19,8 @@ import ( // a server. var appName string +var appNameSync sync.Once + var ErrTooMuchConsoleData = errors.New("console is outputting too much data") type ConsoleThrottler struct { @@ -131,9 +133,9 @@ func (s *Server) Throttler() *ConsoleThrottler { // PublishConsoleOutputFromDaemon sends output to the server console formatted // to appear correctly as being sent from Wings. func (s *Server) PublishConsoleOutputFromDaemon(data string) { - if appName == "" { + appNameSync.Do(func() { appName = config.Get().AppName - } + }) s.Events().Publish( ConsoleOutputEvent, colorstring.Color(fmt.Sprintf("[yellow][bold][%s Daemon]:[default] %s", appName, data)), From c279d28c5d88bf30744c8a13d3785eb70a391b26 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 15 Aug 2021 17:53:54 -0700 Subject: [PATCH 08/17] Correctly set the egg values to avoid allowing blocked files to be edited; closes pterodactyl/panel#3536 --- server/update.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/update.go b/server/update.go index 7a1eb2e..f43b415 100644 --- a/server/update.go +++ b/server/update.go @@ -58,6 +58,9 @@ func (s *Server) UpdateDataStructure(data []byte) error { // backfiring at some point, but until then... c.Build = src.Build + // Yee haw. + c.Egg = src.Egg + // Mergo can't quite handle this boolean value correctly, so for now we'll just // handle this edge case manually since none of the other data passed through in this // request is going to be boolean. Allegedly. From 4ee7f367e7d8573a8de52506cad0743f093eb386 Mon Sep 17 00:00:00 2001 From: Johannes Date: Mon, 16 Aug 2021 03:31:11 +0200 Subject: [PATCH 09/17] =?UTF-8?q?Expose=208080=20so=20that=20reverse-proxi?= =?UTF-8?q?es=20like=20jwilder/nginx-proxy=20can=20pick=E2=80=A6=20(#97)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Expose 8080 so that reverse-proxies like jwilder/nginx-proxy can pick up on it. * Now actually patching the right image.... Co-authored-by: Dane Everitt --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index 9374502..2b3ca02 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,5 +19,8 @@ RUN echo "ID=\"distroless\"" > /etc/os-release # Stage 2 (Final) FROM gcr.io/distroless/static:latest COPY --from=builder /etc/os-release /etc/os-release + COPY --from=builder /app/wings /usr/bin/ CMD [ "/usr/bin/wings", "--config", "/etc/pterodactyl/config.yml" ] + +EXPOSE 8080 From 88caafa3f55aed466a1a0a75852802d7c745ed93 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 15 Aug 2021 18:32:54 -0700 Subject: [PATCH 10/17] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 6a7c771..ceefad5 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,9 @@ I would like to extend my sincere thanks to the following sponsors for helping f | [**HostBend**](https://hostbend.com/) | HostBend offers a variety of solutions for developers, students, and others who have a tight budget but don't want to compromise quality and support. | | [**Capitol Hosting Solutions**](https://capitolsolutions.cloud/) | CHS is *the* budget friendly hosting company for Australian and American gamers, offering a variety of plans from Web Hosting to Game Servers; Custom Solutions too! | | [**ByteAnia**](https://byteania.com/?utm_source=pterodactyl) | ByteAnia offers the best performing and most affordable **Ryzen 5000 Series hosting** on the market for *unbeatable prices*! | +| [**Aussie Server Hosts**](https://aussieserverhosts.com/) | No frills Australian Owned and operated High Performance Server hosting for some of the most demanding games serving Australia and New Zealand. | +| [**VibeGAMES**](https://vibegames.net/) | VibeGAMES is a game server provider that specializes in DDOS protection for the games we offer. We have multiple locations in the US, Brazil, France, Germany, Singapore, Australia and South Africa.| +| [**RocketNode**](https://rocketnode.net) | RocketNode is a VPS and Game Server provider that offers the best performing VPS and Game hosting Solutions at affordable prices! | ## Documentation * [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html) From a0a54749d7e11fcbc3b2d2f32d338944f24afb70 Mon Sep 17 00:00:00 2001 From: Matthew Penner Date: Tue, 24 Aug 2021 13:28:17 -0600 Subject: [PATCH 11/17] upgrade to go1.17 --- .github/workflows/build-test.yml | 2 +- .github/workflows/release.yml | 2 +- Dockerfile | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index bf33b90..2297509 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -12,7 +12,7 @@ jobs: fail-fast: false matrix: os: [ ubuntu-20.04 ] - go: [ '^1.16' ] + go: [ '^1.17' ] goos: [ linux ] goarch: [ amd64, arm64 ] runs-on: ${{ matrix.os }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 658be6b..a11dc02 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 - uses: actions/setup-go@v2 with: - go-version: '^1.16' + go-version: '^1.17' - name: Build env: REF: ${{ github.ref }} diff --git a/Dockerfile b/Dockerfile index 2b3ca02..f5273ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Stage 1 (Build) -FROM --platform=$BUILDPLATFORM golang:1.16-alpine AS builder +FROM --platform=$BUILDPLATFORM golang:1.17-alpine AS builder ARG VERSION RUN apk add --update --no-cache git make upx From d4a8f25cc68f98215044394890b4110408edf147 Mon Sep 17 00:00:00 2001 From: Matthew Penner Date: Tue, 24 Aug 2021 16:05:02 -0700 Subject: [PATCH 12/17] parser: bug fixes (#102) * parser: remove unnecessary type convertions * parser: properly pass number and boolean values * parser: set values if they don't exist --- parser/helpers.go | 58 +++++++++++++++++++++++------------------------ parser/parser.go | 21 ++++++++++------- 2 files changed, 41 insertions(+), 38 deletions(-) diff --git a/parser/helpers.go b/parser/helpers.go index ef60e44..b5feb52 100644 --- a/parser/helpers.go +++ b/parser/helpers.go @@ -48,19 +48,19 @@ func readFileBytes(path string) ([]byte, error) { } // Gets the value of a key based on the value type defined. -func (cfr *ConfigurationFileReplacement) getKeyValue(value []byte) interface{} { +func (cfr *ConfigurationFileReplacement) getKeyValue(value string) interface{} { if cfr.ReplaceWith.Type() == jsonparser.Boolean { - v, _ := strconv.ParseBool(string(value)) + v, _ := strconv.ParseBool(value) return v } // Try to parse into an int, if this fails just ignore the error and continue // through, returning the string. - if v, err := strconv.Atoi(string(value)); err == nil { + if v, err := strconv.Atoi(value); err == nil { return v } - return string(value) + return value } // Iterate over an unstructured JSON/YAML/etc. interface and set all of the required @@ -97,22 +97,21 @@ func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error // If the child is a null value, nothing will happen. Seems reasonable as of the // time this code is being written. for _, child := range parsed.Path(strings.Trim(parts[0], ".")).Children() { - if err := v.SetAtPathway(child, strings.Trim(parts[1], "."), []byte(value)); err != nil { + if err := v.SetAtPathway(child, strings.Trim(parts[1], "."), value); err != nil { if errors.Is(err, gabs.ErrNotFound) { continue } - return nil, errors.WithMessage(err, "failed to set config value of array child") } } - } else { - if err = v.SetAtPathway(parsed, v.Match, []byte(value)); err != nil { - if errors.Is(err, gabs.ErrNotFound) { - continue - } + continue + } - return nil, errors.WithMessage(err, "unable to set config value at pathway: "+v.Match) + if err := v.SetAtPathway(parsed, v.Match, value); err != nil { + if errors.Is(err, gabs.ErrNotFound) { + continue } + return nil, errors.WithMessage(err, "unable to set config value at pathway: "+v.Match) } } @@ -132,13 +131,10 @@ func setValueAtPath(c *gabs.Container, path string, value interface{}) error { var err error matches := checkForArrayElement.FindStringSubmatch(path) - if len(matches) < 3 { - // Only update the value if the pathway actually exists in the configuration, otherwise - // do nothing. - if c.ExistsP(path) { - _, err = c.SetP(value, path) - } + // Check if we are **NOT** updating an array element. + if len(matches) < 3 { + _, err = c.SetP(value, path) return err } @@ -196,32 +192,34 @@ func setValueAtPath(c *gabs.Container, path string, value interface{}) error { // Sets the value at a specific pathway, but checks if we were looking for a specific // value or not before doing it. -func (cfr *ConfigurationFileReplacement) SetAtPathway(c *gabs.Container, path string, value []byte) error { +func (cfr *ConfigurationFileReplacement) SetAtPathway(c *gabs.Container, path string, value string) error { if cfr.IfValue == "" { return setValueAtPath(c, path, cfr.getKeyValue(value)) } - // If this is a regex based matching, we need to get a little more creative since - // we're only going to replacing part of the string, and not the whole thing. - if c.ExistsP(path) && strings.HasPrefix(cfr.IfValue, "regex:") { - // We're doing some regex here. + // Check if we are replacing instead of overwriting. + if strings.HasPrefix(cfr.IfValue, "regex:") { + // Doing a regex replacement requires an existing value. + // TODO: Do we try passing an empty string to the regex? + if c.ExistsP(path) { + return gabs.ErrNotFound + } + r, err := regexp.Compile(strings.TrimPrefix(cfr.IfValue, "regex:")) if err != nil { log.WithFields(log.Fields{"if_value": strings.TrimPrefix(cfr.IfValue, "regex:"), "error": err}). Warn("configuration if_value using invalid regexp, cannot perform replacement") - return nil } - // If the path exists and there is a regex match, go ahead and attempt the replacement - // using the value we got from the key. This will only replace the one match. - v := strings.Trim(string(c.Path(path).Bytes()), "\"") + v := strings.Trim(c.Path(path).String(), "\"") if r.Match([]byte(v)) { - return setValueAtPath(c, path, r.ReplaceAllString(v, string(value))) + return setValueAtPath(c, path, r.ReplaceAllString(v, value)) } - return nil - } else if !c.ExistsP(path) || (c.ExistsP(path) && !bytes.Equal(c.Bytes(), []byte(cfr.IfValue))) { + } + + if c.ExistsP(path) && !bytes.Equal(c.Bytes(), []byte(cfr.IfValue)) { return nil } diff --git a/parser/parser.go b/parser/parser.go index af01bef..78c7608 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -57,17 +57,22 @@ func (cv *ReplaceValue) Type() jsonparser.ValueType { // handle casting the UTF-8 sequence into the expected value, switching something // like "\u00a7Foo" into "§Foo". func (cv *ReplaceValue) String() string { - if cv.Type() != jsonparser.String { - if cv.Type() == jsonparser.Null { - return "" + switch cv.Type() { + case jsonparser.String: + str, err := jsonparser.ParseString(cv.value) + if err != nil { + panic(errors.Wrap(err, "parser: could not parse value")) } + return str + case jsonparser.Null: + return "" + case jsonparser.Boolean: + return string(cv.value) + case jsonparser.Number: + return string(cv.value) + default: return "" } - str, err := jsonparser.ParseString(cv.value) - if err != nil { - panic(errors.Wrap(err, "parser: could not parse value")) - } - return str } type ConfigurationParser string From 5764894a5ec40171ad5277bc560a863cc9fd8d68 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 29 Aug 2021 13:37:18 -0700 Subject: [PATCH 13/17] Cleanup server sync logic to work in a single consistent format (#101) * Cleanup server sync logic to work in a single consistent format Previously we had a mess of a function trying to update server details from a patch request. This change just centralizes everything to a single Sync() call when a server needs to update itself. We can also eventually update the panel (in V2) to not hit the patch endpoint, rather it can just be a generic endpoint that is hit after a server is updated on the Panel that tells Wings to re-sync the data to get the environment changes on the fly. The changes I made to the patch function currently act like that, with a slightly fragile 2 second wait to let the panel persist the changes since I don't want this to be a breaking change on that end. * Remove legacy server patch endpoint; replace with simpler sync endpoint --- router/router.go | 6 +- router/router_server.go | 23 +++--- server/manager.go | 12 ++-- server/server.go | 51 +++++++++----- server/update.go | 152 ++++++---------------------------------- 5 files changed, 73 insertions(+), 171 deletions(-) diff --git a/router/router.go b/router/router.go index 461a991..4162cff 100644 --- a/router/router.go +++ b/router/router.go @@ -6,11 +6,11 @@ import ( "github.com/pterodactyl/wings/remote" "github.com/pterodactyl/wings/router/middleware" - "github.com/pterodactyl/wings/server" + wserver "github.com/pterodactyl/wings/server" ) // Configure configures the routing infrastructure for this daemon instance. -func Configure(m *server.Manager, client remote.Client) *gin.Engine { +func Configure(m *wserver.Manager, client remote.Client) *gin.Engine { gin.SetMode("release") router := gin.New() @@ -63,7 +63,6 @@ func Configure(m *server.Manager, client remote.Client) *gin.Engine { server.Use(middleware.RequireAuthorization(), middleware.ServerExists()) { server.GET("", getServer) - server.PATCH("", patchServer) server.DELETE("", deleteServer) server.GET("/logs", getServerLogs) @@ -71,6 +70,7 @@ func Configure(m *server.Manager, client remote.Client) *gin.Engine { server.POST("/commands", postServerCommands) server.POST("/install", postServerInstall) server.POST("/reinstall", postServerReinstall) + server.POST("/sync", postServerSync) server.POST("/ws/deny", postServerDenyWSTokens) // This archive request causes the archive to start being created diff --git a/router/router_server.go b/router/router_server.go index 7deb1c4..1125e78 100644 --- a/router/router_server.go +++ b/router/router_server.go @@ -1,7 +1,6 @@ package router import ( - "bytes" "context" "net/http" "os" @@ -10,7 +9,6 @@ import ( "emperror.dev/errors" "github.com/apex/log" "github.com/gin-gonic/gin" - "github.com/pterodactyl/wings/router/downloader" "github.com/pterodactyl/wings/router/middleware" "github.com/pterodactyl/wings/router/tokens" @@ -130,21 +128,18 @@ func postServerCommands(c *gin.Context) { c.Status(http.StatusNoContent) } -// Updates information about a server internally. -func patchServer(c *gin.Context) { +// postServerSync will accept a POST request and trigger a re-sync of the given +// server against the Panel. This can be manually triggered when needed by an +// external system, or triggered by the Panel itself when modifications are made +// to the build of a server internally. +func postServerSync(c *gin.Context) { s := ExtractServer(c) - buf := bytes.Buffer{} - buf.ReadFrom(c.Request.Body) - - if err := s.UpdateDataStructure(buf.Bytes()); err != nil { - NewServerError(err, s).Abort(c) - return + if err := s.Sync(); err != nil { + WithError(c, err) + } else { + c.Status(http.StatusNoContent) } - - s.SyncWithEnvironment() - - c.Status(http.StatusNoContent) } // Performs a server installation in a background thread. diff --git a/server/manager.go b/server/manager.go index 2b5249b..6df09ab 100644 --- a/server/manager.go +++ b/server/manager.go @@ -172,8 +172,11 @@ func (m *Manager) InitServer(data remote.ServerConfigurationResponse) (*Server, if err != nil { return nil, err } - if err := s.UpdateDataStructure(data.Settings); err != nil { - return nil, err + + // Setup the base server configuration data which will be used for all of the + // remaining functionality in this call. + if err := s.SyncWithConfiguration(data); err != nil { + return nil, errors.WithStackIf(err) } s.fs = filesystem.New(filepath.Join(config.Get().System.Data, s.ID()), s.DiskSpace(), s.Config().Egg.FileDenylist) @@ -200,11 +203,6 @@ func (m *Manager) InitServer(data remote.ServerConfigurationResponse) (*Server, s.Throttler().StartTimer(s.Context()) } - // Forces the configuration to be synced with the panel. - if err := s.SyncWithConfiguration(data); err != nil { - return nil, err - } - // If the server's data directory exists, force disk usage calculation. if _, err := os.Stat(s.Filesystem().Path()); err == nil { s.Filesystem().HasSpaceAvailable(true) diff --git a/server/server.go b/server/server.go index 3526faa..6ffc4bc 100644 --- a/server/server.go +++ b/server/server.go @@ -2,6 +2,7 @@ package server import ( "context" + "encoding/json" "fmt" "net/http" "os" @@ -15,7 +16,6 @@ import ( "github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/environment" - "github.com/pterodactyl/wings/environment/docker" "github.com/pterodactyl/wings/events" "github.com/pterodactyl/wings/remote" "github.com/pterodactyl/wings/server/filesystem" @@ -167,31 +167,48 @@ func (s *Server) Sync() error { } return errors.WithStackIf(err) } - return s.SyncWithConfiguration(cfg) + + if err := s.SyncWithConfiguration(cfg); err != nil { + return errors.WithStackIf(err) + } + + // Update the disk space limits for the server whenever the configuration for + // it changes. + s.fs.SetDiskLimit(s.DiskSpace()) + + s.SyncWithEnvironment() + + return nil } +// SyncWithConfiguration accepts a configuration object for a server and will +// sync all of the values with the existing server state. This only replaces the +// existing configuration and process configuration for the server. The +// underlying environment will not be affected. This is because this function +// can be called from scoped where the server may not be fully initialized, +// therefore other things like the filesystem and environment may not exist yet. func (s *Server) SyncWithConfiguration(cfg remote.ServerConfigurationResponse) error { - // Update the data structure and persist it to the disk. - if err := s.UpdateDataStructure(cfg.Settings); err != nil { - return err + c := Configuration{} + if err := json.Unmarshal(cfg.Settings, &c); err != nil { + return errors.WithStackIf(err) } + s.cfg.mu.Lock() + defer s.cfg.mu.Unlock() + + // Lock the new configuration. Since we have the defered Unlock above we need + // to make sure that the NEW configuration object is already locked since that + // defer is running on the memory address for "s.cfg.mu" which we're explcitly + // changing on the next line. + c.mu.Lock() + + //goland:noinspection GoVetCopyLock + s.cfg = c + s.Lock() s.procConfig = cfg.ProcessConfiguration s.Unlock() - // Update the disk space limits for the server whenever the configuration - // for it changes. - s.fs.SetDiskLimit(s.DiskSpace()) - - // If this is a Docker environment we need to sync the stop configuration with it so that - // the process isn't just terminated when a user requests it be stopped. - if e, ok := s.Environment.(*docker.Environment); ok { - s.Log().Debug("syncing stop configuration with configured docker environment") - e.SetImage(s.Config().Container.Image) - e.SetStopConfiguration(cfg.ProcessConfiguration.Stop) - } - return nil } diff --git a/server/update.go b/server/update.go index f43b415..3334b59 100644 --- a/server/update.go +++ b/server/update.go @@ -1,149 +1,41 @@ package server import ( - "encoding/json" - - "emperror.dev/errors" - "github.com/buger/jsonparser" - "github.com/imdario/mergo" + "github.com/pterodactyl/wings/environment/docker" "github.com/pterodactyl/wings/environment" ) -// UpdateDataStructure merges data passed through in JSON form into the existing -// server object. Any changes to the build settings will apply immediately in -// the environment if the environment supports it. +// SyncWithEnvironment updates the environment for the server to match any of +// the changed data. This pushes new settings and environment variables to the +// environment. In addition, the in-situ update method is called on the +// environment which will allow environments that make use of it (such as Docker) +// to immediately apply some settings without having to wait on a server to +// restart. // -// The server will be marked as requiring a rebuild on the next boot sequence, -// it is up to the specific environment to determine what needs to happen when -// that is the case. -func (s *Server) UpdateDataStructure(data []byte) error { - src := new(Configuration) - if err := json.Unmarshal(data, src); err != nil { - return errors.Wrap(err, "server/update: could not unmarshal source data into Configuration struct") - } - - // Don't allow obviously corrupted data to pass through into this function. If the UUID - // doesn't match something has gone wrong and the API is attempting to meld this server - // instance into a totally different one, which would be bad. - if src.Uuid != "" && s.ID() != "" && src.Uuid != s.ID() { - return errors.New("server/update: attempting to merge a data stack with an invalid UUID") - } - - // Grab a copy of the configuration to work on. - c := *s.Config() - - // Lock our copy of the configuration since the deferred unlock will end up acting upon this - // new memory address rather than the old one. If we don't lock this, the deferred unlock will - // cause a panic when it goes to run. However, since we only update s.cfg at the end, if there - // is an error before that point we'll still properly unlock the original configuration for the - // server. - c.mu.Lock() - - // Lock the server configuration while we're doing this merge to avoid anything - // trying to overwrite it or make modifications while we're sorting out what we - // need to do. - s.cfg.mu.Lock() - defer s.cfg.mu.Unlock() - - // Merge the new data object that we have received with the existing server data object - // and then save it to the disk so it is persistent. - if err := mergo.Merge(&c, src, mergo.WithOverride); err != nil { - return errors.WithStack(err) - } - - // Don't explode if we're setting CPU limits to 0. Mergo sees that as an empty value - // so it won't override the value we've passed through in the API call. However, we can - // safely assume that we're passing through valid data structures here. I foresee this - // backfiring at some point, but until then... - c.Build = src.Build - - // Yee haw. - c.Egg = src.Egg - - // Mergo can't quite handle this boolean value correctly, so for now we'll just - // handle this edge case manually since none of the other data passed through in this - // request is going to be boolean. Allegedly. - if v, err := jsonparser.GetBoolean(data, "container", "oom_disabled"); err != nil { - if err != jsonparser.KeyPathNotFoundError { - return errors.WithStack(err) - } - } else { - c.Build.OOMDisabled = v - } - - // Mergo also cannot handle this boolean value. - if v, err := jsonparser.GetBoolean(data, "suspended"); err != nil { - if err != jsonparser.KeyPathNotFoundError { - return errors.WithStack(err) - } - } else { - c.Suspended = v - } - - if v, err := jsonparser.GetBoolean(data, "skip_egg_scripts"); err != nil { - if err != jsonparser.KeyPathNotFoundError { - return errors.WithStack(err) - } - } else { - c.SkipEggScripts = v - } - - if v, err := jsonparser.GetBoolean(data, "start_on_completion"); err != nil { - if err != jsonparser.KeyPathNotFoundError { - return errors.WithStack(err) - } - } else { - c.StartOnCompletion = v - } - - if v, err := jsonparser.GetBoolean(data, "crash_detection_enabled"); err != nil { - if err != jsonparser.KeyPathNotFoundError { - return errors.WithStack(err) - } - // Enable crash detection by default. - c.CrashDetectionEnabled = true - } else { - c.CrashDetectionEnabled = v - } - - // Environment and Mappings should be treated as a full update at all times, never a - // true patch, otherwise we can't know what we're passing along. - if src.EnvVars != nil && len(src.EnvVars) > 0 { - c.EnvVars = src.EnvVars - } - - if src.Allocations.Mappings != nil && len(src.Allocations.Mappings) > 0 { - c.Allocations.Mappings = src.Allocations.Mappings - } - - if src.Mounts != nil && len(src.Mounts) > 0 { - c.Mounts = src.Mounts - } - - // Update the configuration once we have a lock on the configuration object. - s.cfg = c - - return nil -} - -// Updates the environment for the server to match any of the changed data. This pushes new settings and -// environment variables to the environment. In addition, the in-situ update method is called on the -// environment which will allow environments that make use of it (such as Docker) to immediately apply -// some settings without having to wait on a server to restart. -// -// This functionality allows a server's resources limits to be modified on the fly and have them apply -// right away allowing for dynamic resource allocation and responses to abusive server processes. +// This functionality allows a server's resources limits to be modified on the +// fly and have them apply right away allowing for dynamic resource allocation +// and responses to abusive server processes. func (s *Server) SyncWithEnvironment() { s.Log().Debug("syncing server settings with environment") + cfg := s.Config() + // Update the environment settings using the new information from this server. s.Environment.Config().SetSettings(environment.Settings{ Mounts: s.Mounts(), - Allocations: s.Config().Allocations, - Limits: s.Config().Build, + Allocations: cfg.Allocations, + Limits: cfg.Build, }) + // For Docker specific environments we also want to update the configured image + // and stop configuration. + if e, ok := s.Environment.(*docker.Environment); ok { + s.Log().Debug("syncing stop configuration with configured docker environment") + e.SetImage(cfg.Container.Image) + e.SetStopConfiguration(s.ProcessConfiguration().Stop) + } + // If build limits are changed, environment variables also change. Plus, any modifications to // the startup command also need to be properly propagated to this environment. // From d2cfa6cd51715708d39588c8e21104910ab204cd Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 29 Aug 2021 13:49:34 -0700 Subject: [PATCH 14/17] Update CHANGELOG.md --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7231d5..77f9fa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## v1.5.0 +### Fixed +* Fixes a race condition when setting the application name in the console output for a server. +* Fixes a server being reinstalled causing the `file_denylist` parameter for an Egg to be ignored until Wings is restarted. +* Fixes YAML file parser not correctly setting boolean values. + +### Added +* Exposes `8080` in the default Docker setup to better support proxy tools. + +### Changed +* Releases are now built using `Go 1.17` — the minimum version required to build Wings remains `Go 1.16`. +* Simplifed the logic powering server updates to only pull information from the Panel rather than trying to accept updated values. All parts of Wings needing the most up-to-date server details should call `Server#Sync()` to fetch the latest stored build information. + +### Removed +* Removes the `PATCH /api/servers/:server` endpoint — if you were previously using this API call it should be replaced with `POST /api/servers/:server/sync`. + ## v1.4.7 ### Fixed * SFTP access is now properly denied if a server is suspended. From 7321c6aa45f7019d41fdc411bc38549ecca99fd6 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 29 Aug 2021 13:52:19 -0700 Subject: [PATCH 15/17] Remove unused and complicated installer logic --- CHANGELOG.md | 1 + installer/installer.go | 49 +++--------------------------------------- 2 files changed, 4 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77f9fa1..c919557 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ * Simplifed the logic powering server updates to only pull information from the Panel rather than trying to accept updated values. All parts of Wings needing the most up-to-date server details should call `Server#Sync()` to fetch the latest stored build information. ### Removed +* Removes complicated (and unused) logic during the server installation process that was a hold-over from legacy Wings architectures. * Removes the `PATCH /api/servers/:server` endpoint — if you were previously using this API call it should be replaced with `POST /api/servers/:server/sync`. ## v1.4.7 diff --git a/installer/installer.go b/installer/installer.go index a4e1e7d..c34bdef 100644 --- a/installer/installer.go +++ b/installer/installer.go @@ -2,13 +2,11 @@ package installer import ( "context" - "encoding/json" "emperror.dev/errors" "github.com/asaskevich/govalidator" "github.com/buger/jsonparser" - "github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/remote" "github.com/pterodactyl/wings/server" ) @@ -21,53 +19,12 @@ type Installer struct { // have been passed along in the request. This should be manually run before // calling Execute(). func New(ctx context.Context, manager *server.Manager, data []byte) (*Installer, error) { - if !govalidator.IsUUIDv4(getString(data, "uuid")) { + uuid := getString(data, "uuid") + if !govalidator.IsUUIDv4(uuid) { return nil, NewValidationError("uuid provided was not in a valid format") } - cfg := &server.Configuration{ - Uuid: getString(data, "uuid"), - Suspended: false, - Invocation: getString(data, "invocation"), - SkipEggScripts: getBoolean(data, "skip_egg_scripts"), - StartOnCompletion: getBoolean(data, "start_on_completion"), - Build: environment.Limits{ - MemoryLimit: getInt(data, "build", "memory"), - Swap: getInt(data, "build", "swap"), - IoWeight: uint16(getInt(data, "build", "io")), - CpuLimit: getInt(data, "build", "cpu"), - DiskSpace: getInt(data, "build", "disk"), - Threads: getString(data, "build", "threads"), - }, - CrashDetectionEnabled: true, - } - - cfg.Allocations.DefaultMapping.Ip = getString(data, "allocations", "default", "ip") - cfg.Allocations.DefaultMapping.Port = int(getInt(data, "allocations", "default", "port")) - - // Unmarshal the environment variables from the request into the server struct. - if b, _, _, err := jsonparser.Get(data, "environment"); err != nil { - return nil, errors.WithStackIf(err) - } else { - cfg.EnvVars = make(environment.Variables) - if err := json.Unmarshal(b, &cfg.EnvVars); err != nil { - return nil, errors.WrapIf(err, "installer: could not unmarshal environment variables for server") - } - } - - // Unmarshal the allocation mappings from the request into the server struct. - if b, _, _, err := jsonparser.Get(data, "allocations", "mappings"); err != nil { - return nil, errors.WithStackIf(err) - } else { - cfg.Allocations.Mappings = make(map[string][]int) - if err := json.Unmarshal(b, &cfg.Allocations.Mappings); err != nil { - return nil, errors.Wrap(err, "installer: could not unmarshal allocation mappings") - } - } - - cfg.Container.Image = getString(data, "container", "image") - - c, err := manager.Client().GetServerConfiguration(ctx, cfg.Uuid) + c, err := manager.Client().GetServerConfiguration(ctx, uuid) if err != nil { if !remote.IsRequestError(err) { return nil, errors.WithStackIf(err) From 3b5e042ccc267422e18d914edd52ec49100b3137 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 29 Aug 2021 14:08:01 -0700 Subject: [PATCH 16/17] Simplify logic when creating a new installer; no longer requires an entire server object be passed. --- CHANGELOG.md | 1 + installer/installer.go | 45 +++++++++++---------------------------- router/router_system.go | 23 ++++++++++---------- router/router_transfer.go | 9 ++++---- server/configuration.go | 2 -- 5 files changed, 28 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c919557..676c71d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ ### Changed * Releases are now built using `Go 1.17` — the minimum version required to build Wings remains `Go 1.16`. * Simplifed the logic powering server updates to only pull information from the Panel rather than trying to accept updated values. All parts of Wings needing the most up-to-date server details should call `Server#Sync()` to fetch the latest stored build information. +* `Installer#New()` no longer requires passing all of the server data as a byte slice, rather a new `Installer#ServerDetails` struct is exposed which can be passed and accepts a UUID and if the server should be started after the installer finishes. ### Removed * Removes complicated (and unused) logic during the server installation process that was a hold-over from legacy Wings architectures. diff --git a/installer/installer.go b/installer/installer.go index c34bdef..e4616e0 100644 --- a/installer/installer.go +++ b/installer/installer.go @@ -5,26 +5,29 @@ import ( "emperror.dev/errors" "github.com/asaskevich/govalidator" - "github.com/buger/jsonparser" - "github.com/pterodactyl/wings/remote" "github.com/pterodactyl/wings/server" ) type Installer struct { - server *server.Server + server *server.Server + StartOnCompletion bool +} + +type ServerDetails struct { + UUID string `json:"uuid"` + StartOnCompletion bool `json:"start_on_completion"` } // New validates the received data to ensure that all the required fields // have been passed along in the request. This should be manually run before // calling Execute(). -func New(ctx context.Context, manager *server.Manager, data []byte) (*Installer, error) { - uuid := getString(data, "uuid") - if !govalidator.IsUUIDv4(uuid) { +func New(ctx context.Context, manager *server.Manager, details ServerDetails) (*Installer, error) { + if !govalidator.IsUUIDv4(details.UUID) { return nil, NewValidationError("uuid provided was not in a valid format") } - c, err := manager.Client().GetServerConfiguration(ctx, uuid) + c, err := manager.Client().GetServerConfiguration(ctx, details.UUID) if err != nil { if !remote.IsRequestError(err) { return nil, errors.WithStackIf(err) @@ -38,35 +41,11 @@ func New(ctx context.Context, manager *server.Manager, data []byte) (*Installer, if err != nil { return nil, errors.WrapIf(err, "installer: could not init server instance") } - return &Installer{server: s}, nil -} - -// Uuid returns the UUID associated with this installer instance. -func (i *Installer) Uuid() string { - return i.server.ID() + i := Installer{server: s, StartOnCompletion: details.StartOnCompletion} + return &i, nil } // Server returns the server instance. func (i *Installer) Server() *server.Server { return i.server } - -// Returns a string value from the JSON data provided. -func getString(data []byte, key ...string) string { - value, _ := jsonparser.GetString(data, key...) - - return value -} - -// Returns an int value from the JSON data provided. -func getInt(data []byte, key ...string) int64 { - value, _ := jsonparser.GetInt(data, key...) - - return value -} - -func getBoolean(data []byte, key ...string) bool { - value, _ := jsonparser.GetBoolean(data, key...) - - return value -} diff --git a/router/router_system.go b/router/router_system.go index 95fdb6c..2f384af 100644 --- a/router/router_system.go +++ b/router/router_system.go @@ -1,7 +1,6 @@ package router import ( - "bytes" "context" "errors" "net/http" @@ -44,10 +43,13 @@ func getAllServers(c *gin.Context) { // for it. func postCreateServer(c *gin.Context) { manager := middleware.ExtractManager(c) - buf := bytes.Buffer{} - buf.ReadFrom(c.Request.Body) - install, err := installer.New(c.Request.Context(), manager, buf.Bytes()) + details := installer.ServerDetails{} + if err := c.BindJSON(&details); err != nil { + return + } + + install, err := installer.New(c.Request.Context(), manager, details) if err != nil { if installer.IsValidationError(err) { c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{ @@ -74,24 +76,21 @@ func postCreateServer(c *gin.Context) { } if err := i.Server().Install(false); err != nil { - log.WithFields(log.Fields{"server": i.Uuid(), "error": err}).Error("failed to run install process for server") + log.WithFields(log.Fields{"server": i.Server().ID(), "error": err}).Error("failed to run install process for server") return } - if i.Server().Config().StartOnCompletion { + if i.StartOnCompletion { log.WithField("server_id", i.Server().ID()).Debug("starting server after successful installation") if err := i.Server().HandlePowerAction(server.PowerActionStart, 30); err != nil { if errors.Is(err, context.DeadlineExceeded) { - log.WithFields(log.Fields{"server_id": i.Server().ID(), "action": "start"}). - Warn("could not acquire a lock while attempting to perform a power action") + log.WithFields(log.Fields{"server_id": i.Server().ID(), "action": "start"}).Warn("could not acquire a lock while attempting to perform a power action") } else { - log.WithFields(log.Fields{"server_id": i.Server().ID(), "action": "start", "error": err}). - Error("encountered error processing a server power action in the background") + log.WithFields(log.Fields{"server_id": i.Server().ID(), "action": "start", "error": err}).Error("encountered error processing a server power action in the background") } } } else { - log.WithField("server_id", i.Server().ID()). - Debug("skipping automatic start after successful server installation") + log.WithField("server_id", i.Server().ID()).Debug("skipping automatic start after successful server installation") } }(install) diff --git a/router/router_transfer.go b/router/router_transfer.go index 8c16cd8..794981e 100644 --- a/router/router_transfer.go +++ b/router/router_transfer.go @@ -5,7 +5,6 @@ import ( "context" "crypto/sha256" "encoding/hex" - "encoding/json" "fmt" "io" "net/http" @@ -47,10 +46,10 @@ type downloadProgress struct { // Data passed over to initiate a server transfer. type serverTransferRequest struct { - ServerID string `binding:"required" json:"server_id"` - URL string `binding:"required" json:"url"` - Token string `binding:"required" json:"token"` - Server json.RawMessage `json:"server"` + ServerID string `binding:"required" json:"server_id"` + URL string `binding:"required" json:"url"` + Token string `binding:"required" json:"token"` + Server installer.ServerDetails `json:"server"` } func getArchivePath(sID string) string { diff --git a/server/configuration.go b/server/configuration.go index dfc9ac5..64bab10 100644 --- a/server/configuration.go +++ b/server/configuration.go @@ -35,8 +35,6 @@ type Configuration struct { // server, specific installation scripts will be skipped for the server process. SkipEggScripts bool `json:"skip_egg_scripts"` - StartOnCompletion bool `json:"start_on_completion"` - // An array of environment variables that should be passed along to the running // server process. EnvVars environment.Variables `json:"environment"` From 5cd43dd4c9dd110aeb6d7cc71067927a0d56506b Mon Sep 17 00:00:00 2001 From: Matthew Penner Date: Wed, 1 Sep 2021 09:54:41 -0600 Subject: [PATCH 17/17] archive: keep timestamps when extracting --- server/backup.go | 8 ++++++-- server/backup/backup.go | 3 ++- server/backup/backup_local.go | 2 +- server/backup/backup_s3.go | 2 +- server/filesystem/compress.go | 4 ++++ server/filesystem/filesystem.go | 17 +++++++++++++++++ 6 files changed, 31 insertions(+), 5 deletions(-) diff --git a/server/backup.go b/server/backup.go index acc8797..f511634 100644 --- a/server/backup.go +++ b/server/backup.go @@ -5,6 +5,7 @@ import ( "io/fs" "io/ioutil" "os" + "time" "emperror.dev/errors" "github.com/apex/log" @@ -152,12 +153,15 @@ func (s *Server) RestoreBackup(b backup.BackupInterface, reader io.ReadCloser) ( // Attempt to restore the backup to the server by running through each entry // in the file one at a time and writing them to the disk. s.Log().Debug("starting file writing process for backup restoration") - err = b.Restore(s.Context(), reader, func(file string, r io.Reader, mode fs.FileMode) error { + err = b.Restore(s.Context(), reader, func(file string, r io.Reader, mode fs.FileMode, atime, mtime time.Time) error { s.Events().Publish(DaemonMessageEvent, "(restoring): "+file) if err := s.Filesystem().Writefile(file, r); err != nil { return err } - return s.Filesystem().Chmod(file, mode) + if err := s.Filesystem().Chmod(file, mode); err != nil { + return err + } + return s.Filesystem().Chtimes(file, atime, mtime) }) return errors.WithStackIf(err) diff --git a/server/backup/backup.go b/server/backup/backup.go index e344ae2..496720e 100644 --- a/server/backup/backup.go +++ b/server/backup/backup.go @@ -8,6 +8,7 @@ import ( "io/fs" "os" "path" + "time" "emperror.dev/errors" "github.com/apex/log" @@ -26,7 +27,7 @@ const ( // RestoreCallback is a generic restoration callback that exists for both local // and remote backups allowing the files to be restored. -type RestoreCallback func(file string, r io.Reader, mode fs.FileMode) error +type RestoreCallback func(file string, r io.Reader, mode fs.FileMode, atime, mtime time.Time) error // noinspection GoNameStartsWithPackageName type BackupInterface interface { diff --git a/server/backup/backup_local.go b/server/backup/backup_local.go index 61dc164..c652b13 100644 --- a/server/backup/backup_local.go +++ b/server/backup/backup_local.go @@ -88,7 +88,7 @@ func (b *LocalBackup) Restore(ctx context.Context, _ io.Reader, callback Restore if f.IsDir() { return nil } - return callback(filesystem.ExtractNameFromArchive(f), f, f.Mode()) + return callback(filesystem.ExtractNameFromArchive(f), f, f.Mode(), f.ModTime(), f.ModTime()) } }) } diff --git a/server/backup/backup_s3.go b/server/backup/backup_s3.go index e240b5e..85faa59 100644 --- a/server/backup/backup_s3.go +++ b/server/backup/backup_s3.go @@ -116,7 +116,7 @@ func (s *S3Backup) Restore(ctx context.Context, r io.Reader, callback RestoreCal return err } if header.Typeflag == tar.TypeReg { - if err := callback(header.Name, tr, header.FileInfo().Mode()); err != nil { + if err := callback(header.Name, tr, header.FileInfo().Mode(), header.AccessTime, header.ModTime); err != nil { return err } } diff --git a/server/filesystem/compress.go b/server/filesystem/compress.go index 5227512..cabbfdd 100644 --- a/server/filesystem/compress.go +++ b/server/filesystem/compress.go @@ -136,6 +136,10 @@ func (fs *Filesystem) DecompressFile(dir string, file string) error { if err := fs.Chmod(p, f.Mode()); err != nil { return wrapError(err, source) } + // Update the file modification time to the one set in the archive. + if err := fs.Chtimes(p, f.ModTime(), f.ModTime()); err != nil { + return wrapError(err, source) + } return nil }) if err != nil { diff --git a/server/filesystem/filesystem.go b/server/filesystem/filesystem.go index 4f04d9a..b507709 100644 --- a/server/filesystem/filesystem.go +++ b/server/filesystem/filesystem.go @@ -528,3 +528,20 @@ func (fs *Filesystem) ListDirectory(p string) ([]Stat, error) { return out, nil } + +func (fs *Filesystem) Chtimes(path string, atime, mtime time.Time) error { + cleaned, err := fs.SafePath(path) + if err != nil { + return err + } + + if fs.isTest { + return nil + } + + if err := os.Chtimes(cleaned, atime, mtime); err != nil { + return err + } + + return nil +}