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. //