diff --git a/cmd/root.go b/cmd/root.go index 4ddf4f2..854fe7f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,6 +2,7 @@ package cmd import ( "crypto/tls" + "errors" "fmt" log2 "log" "net/http" @@ -9,8 +10,8 @@ import ( "path" "path/filepath" "strings" + "time" - "emperror.dev/errors" "github.com/NYTimes/logrotate" "github.com/apex/log" "github.com/apex/log/handlers/multi" @@ -21,6 +22,7 @@ import ( "github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/loggers/cli" + "github.com/pterodactyl/wings/panelapi" "github.com/pterodactyl/wings/router" "github.com/pterodactyl/wings/server" "github.com/pterodactyl/wings/sftp" @@ -188,7 +190,17 @@ func rootCmdRun(cmd *cobra.Command, _ []string) { }).Info("configured system user successfully") } - if err := server.LoadDirectory(); err != nil { + panelClient := panelapi.CreateClient( + config.Get().PanelLocation, + config.Get().AuthenticationTokenId, + config.Get().AuthenticationToken, + panelapi.WithTimeout(time.Second*time.Duration(config.Get().RemoteQuery.Timeout)), + ) + _ = panelClient + + serverManager := server.NewManager(panelClient) + + if err := serverManager.Initialize(int(c.RemoteQuery.BootServersPerPage)); err != nil { log.WithField("error", err).Fatal("failed to load server configurations") return } @@ -203,7 +215,7 @@ func rootCmdRun(cmd *cobra.Command, _ []string) { } // Just for some nice log output. - for _, s := range server.GetServers().All() { + for _, s := range serverManager.GetAll() { log.WithField("server", s.Id()).Info("loaded configuration for server") } @@ -217,7 +229,7 @@ func rootCmdRun(cmd *cobra.Command, _ []string) { // and reboot processes without causing a slow-down due to sequential booting. pool := workerpool.New(4) - for _, serv := range server.GetServers().All() { + for _, serv := range serverManager.GetAll() { s := serv pool.Submit(func() { @@ -302,7 +314,7 @@ func rootCmdRun(cmd *cobra.Command, _ []string) { }).Info("configuring internal webserver") // Configure the router. - r := router.Configure() + r := router.Configure(serverManager) s := &http.Server{ Addr: fmt.Sprintf("%s:%d", c.Api.Host, c.Api.Port), @@ -372,7 +384,7 @@ func rootCmdRun(cmd *cobra.Command, _ []string) { // Cancel the context on all of the running servers at this point, even though the // program is just shutting down. - for _, s := range server.GetServers().All() { + for _, s := range serverManager.GetAll() { s.CtxCancel() } } diff --git a/router/middleware.go b/router/middleware.go index c3f32fd..cb24e82 100644 --- a/router/middleware.go +++ b/router/middleware.go @@ -12,7 +12,9 @@ import ( "github.com/pterodactyl/wings/server" ) -type Middleware struct{} +type Middleware struct { + serverManager server.Manager +} // A custom handler function allowing for errors bubbled up by c.Error() to be returned in a // standardized format with tracking UUIDs on them for easier log searching. @@ -92,14 +94,19 @@ func (m *Middleware) RequireAuthorization() gin.HandlerFunc { } } -// Helper function to fetch a server out of the servers collection stored in memory. -// -// This function should not be used in new controllers, prefer ExtractServer where -// possible. -func GetServer(uuid string) *server.Server { - return server.GetServers().Find(func(s *server.Server) bool { - return uuid == s.Id() - }) +func (m *Middleware) WithServerManager() gin.HandlerFunc { + return func(c *gin.Context) { + c.Set("servermanager", m.serverManager) + } +} + +func ServerManagerFromContext(c *gin.Context) server.Manager { + if s, ok := c.Get("servermanager"); ok { + if srvs, ok := s.(server.Manager); ok { + return srvs + } + } + return nil } // Ensure that the requested server exists in this setup. Returns a 404 if we cannot @@ -108,7 +115,7 @@ func (m *Middleware) ServerExists() gin.HandlerFunc { return func(c *gin.Context) { u, err := uuid.Parse(c.Param("server")) if err == nil { - if s := GetServer(u.String()); s != nil { + if s := m.serverManager.Get(u.String()); s != nil { c.Set("server", s) c.Next() return diff --git a/router/router.go b/router/router.go index 85a97a7..9ca91bb 100644 --- a/router/router.go +++ b/router/router.go @@ -3,15 +3,18 @@ package router import ( "github.com/apex/log" "github.com/gin-gonic/gin" + "github.com/pterodactyl/wings/server" ) // Configures the routing infrastructure for this daemon instance. -func Configure() *gin.Engine { +func Configure(serverManager server.Manager) *gin.Engine { gin.SetMode("release") - m := Middleware{} + m := Middleware{ + serverManager, + } router := gin.New() - router.Use(gin.Recovery(), m.ErrorHandler(), m.SetAccessControlHeaders()) + router.Use(gin.Recovery(), m.ErrorHandler(), m.SetAccessControlHeaders(), m.WithServerManager()) // @todo log this into a different file so you can setup IP blocking for abusive requests and such. // This should still dump requests in debug mode since it does help with understanding the request // lifecycle and quickly seeing what was called leading to the logs. However, it isn't feasible to mix diff --git a/router/router_download.go b/router/router_download.go index abd8960..7ddb6f5 100644 --- a/router/router_download.go +++ b/router/router_download.go @@ -14,13 +14,15 @@ import ( // Handle a download request for a server backup. func getDownloadBackup(c *gin.Context) { + serverManager := ServerManagerFromContext(c) + token := tokens.BackupPayload{} if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil { NewTrackedError(err).Abort(c) return } - s := GetServer(token.ServerUuid) + s := serverManager.Get(token.ServerUuid) if s == nil || !token.IsUniqueRequest() { c.AbortWithStatusJSON(http.StatusNotFound, gin.H{ "error": "The requested resource was not found on this server.", @@ -57,13 +59,15 @@ func getDownloadBackup(c *gin.Context) { // Handles downloading a specific file for a server. func getDownloadFile(c *gin.Context) { + serverManager := ServerManagerFromContext(c) + token := tokens.FilePayload{} if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil { NewTrackedError(err).Abort(c) return } - s := GetServer(token.ServerUuid) + s := serverManager.Get(token.ServerUuid) if s == nil || !token.IsUniqueRequest() { c.AbortWithStatusJSON(http.StatusNotFound, gin.H{ "error": "The requested resource was not found on this server.", diff --git a/router/router_server.go b/router/router_server.go index dae87a7..cfeb3d0 100644 --- a/router/router_server.go +++ b/router/router_server.go @@ -22,7 +22,7 @@ type serverProcData struct { // Returns a single server from the collection of servers. func getServer(c *gin.Context) { - s := GetServer(c.Param("server")) + s := ExtractServer(c) c.JSON(http.StatusOK, serverProcData{ ResourceUsage: s.Proc(), @@ -32,7 +32,7 @@ func getServer(c *gin.Context) { // Returns the logs for a given server instance. func getServerLogs(c *gin.Context) { - s := GetServer(c.Param("server")) + s := ExtractServer(c) l, _ := strconv.Atoi(c.DefaultQuery("size", "100")) if l <= 0 { @@ -59,7 +59,7 @@ func getServerLogs(c *gin.Context) { // things are happening, so theres no reason to sit and wait for a request to finish. We'll // just see over the socket if something isn't working correctly. func postServerPower(c *gin.Context) { - s := GetServer(c.Param("server")) + s := ExtractServer(c) var data struct { Action server.PowerAction `json:"action"` @@ -109,7 +109,7 @@ func postServerPower(c *gin.Context) { // Sends an array of commands to a running server instance. func postServerCommands(c *gin.Context) { - s := GetServer(c.Param("server")) + s := ExtractServer(c) if running, err := s.Environment.IsRunning(); err != nil { NewServerError(err, s).Abort(c) @@ -140,7 +140,7 @@ func postServerCommands(c *gin.Context) { // Updates information about a server internally. func patchServer(c *gin.Context) { - s := GetServer(c.Param("server")) + s := ExtractServer(c) buf := bytes.Buffer{} buf.ReadFrom(c.Request.Body) @@ -157,7 +157,7 @@ func patchServer(c *gin.Context) { // Performs a server installation in a background thread. func postServerInstall(c *gin.Context) { - s := GetServer(c.Param("server")) + s := ExtractServer(c) go func(serv *server.Server) { if err := serv.Install(true); err != nil { @@ -170,7 +170,7 @@ func postServerInstall(c *gin.Context) { // Reinstalls a server. func postServerReinstall(c *gin.Context) { - s := GetServer(c.Param("server")) + s := ExtractServer(c) if s.ExecutingPowerAction() { c.AbortWithStatusJSON(http.StatusConflict, gin.H{ @@ -191,6 +191,7 @@ func postServerReinstall(c *gin.Context) { // Deletes a server from the wings daemon and dissociate it's objects. func deleteServer(c *gin.Context) { s := ExtractServer(c) + sm := ServerManagerFromContext(c) // Immediately suspend the server to prevent a user from attempting // to start it while this process is running. @@ -234,10 +235,7 @@ func deleteServer(c *gin.Context) { } }(s.Filesystem().Path()) - uuid := s.Id() - server.GetServers().Remove(func(s2 *server.Server) bool { - return s2.Id() == uuid - }) + sm.Remove(s) // Deallocate the reference to this server. s = nil diff --git a/router/router_server_backup.go b/router/router_server_backup.go index b0afd19..e741e89 100644 --- a/router/router_server_backup.go +++ b/router/router_server_backup.go @@ -13,7 +13,7 @@ import ( // Backs up a server. func postServerBackup(c *gin.Context) { - s := GetServer(c.Param("server")) + s := ExtractServer(c) data := &backup.Request{} // BindJSON sends 400 if the request fails, all we need to do is return @@ -57,7 +57,7 @@ func postServerBackup(c *gin.Context) { // a 404 error. The service calling this endpoint can make its own decisions as to how it wants // to handle that response. func deleteServerBackup(c *gin.Context) { - s := GetServer(c.Param("server")) + s := ExtractServer(c) b, _, err := backup.LocateLocal(c.Param("backup")) if err != nil { diff --git a/router/router_server_files.go b/router/router_server_files.go index d0e6da5..278bce9 100644 --- a/router/router_server_files.go +++ b/router/router_server_files.go @@ -76,7 +76,7 @@ type renameFile struct { // Renames (or moves) files for a server. func putServerRenameFiles(c *gin.Context) { - s := GetServer(c.Param("server")) + s := ExtractServer(c) var data struct { Root string `json:"root"` @@ -138,7 +138,7 @@ func putServerRenameFiles(c *gin.Context) { // Copies a server file. func postServerCopyFile(c *gin.Context) { - s := GetServer(c.Param("server")) + s := ExtractServer(c) var data struct { Location string `json:"location"` @@ -158,7 +158,7 @@ func postServerCopyFile(c *gin.Context) { // Deletes files from a server. func postServerDeleteFiles(c *gin.Context) { - s := GetServer(c.Param("server")) + s := ExtractServer(c) var data struct { Root string `json:"root"` @@ -203,7 +203,7 @@ func postServerDeleteFiles(c *gin.Context) { // Writes the contents of the request to a file on a server. func postServerWriteFile(c *gin.Context) { - s := GetServer(c.Param("server")) + s := ExtractServer(c) f := c.Query("file") f = "/" + strings.TrimLeft(f, "/") @@ -300,7 +300,7 @@ func deleteServerPullRemoteFile(c *gin.Context) { // Create a directory on a server. func postServerCreateDirectory(c *gin.Context) { - s := GetServer(c.Param("server")) + s := ExtractServer(c) var data struct { Name string `json:"name"` @@ -327,7 +327,7 @@ func postServerCreateDirectory(c *gin.Context) { } func postServerCompressFiles(c *gin.Context) { - s := GetServer(c.Param("server")) + s := ExtractServer(c) var data struct { RootPath string `json:"root"` @@ -365,7 +365,7 @@ func postServerCompressFiles(c *gin.Context) { } func postServerDecompressFiles(c *gin.Context) { - s := GetServer(c.Param("server")) + s := ExtractServer(c) var data struct { RootPath string `json:"root"` @@ -433,7 +433,7 @@ type chmodFile struct { var errInvalidFileMode = errors.New("invalid file mode") func postServerChmodFile(c *gin.Context) { - s := GetServer(c.Param("server")) + s := ExtractServer(c) var data struct { Root string `json:"root"` @@ -497,13 +497,15 @@ func postServerChmodFile(c *gin.Context) { } func postServerUploadFiles(c *gin.Context) { + serverManager := ServerManagerFromContext(c) + token := tokens.UploadPayload{} if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil { NewTrackedError(err).Abort(c) return } - s := GetServer(token.ServerUuid) + s := serverManager.Get(token.ServerUuid) if s == nil || !token.IsUniqueRequest() { c.AbortWithStatusJSON(http.StatusNotFound, gin.H{ "error": "The requested resource was not found on this server.", diff --git a/router/router_server_ws.go b/router/router_server_ws.go index 936e534..5597a3e 100644 --- a/router/router_server_ws.go +++ b/router/router_server_ws.go @@ -12,7 +12,8 @@ import ( // Upgrades a connection to a websocket and passes events along between. func getServerWebsocket(c *gin.Context) { - s := GetServer(c.Param("server")) + serverManager := ServerManagerFromContext(c) + s := serverManager.Get(c.Param("server")) handler, err := websocket.GetHandler(s, c.Writer, c.Request) if err != nil { NewServerError(err, s).Abort(c) diff --git a/router/router_system.go b/router/router_system.go index 140decc..ee46032 100644 --- a/router/router_system.go +++ b/router/router_system.go @@ -9,7 +9,6 @@ import ( "github.com/gin-gonic/gin" "github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/installer" - "github.com/pterodactyl/wings/server" "github.com/pterodactyl/wings/system" ) @@ -28,7 +27,8 @@ func getSystemInformation(c *gin.Context) { // Returns all of the servers that are registered and configured correctly on // this wings instance. func getAllServers(c *gin.Context) { - c.JSON(http.StatusOK, server.GetServers().All()) + serverManager := ServerManagerFromContext(c) + c.JSON(http.StatusOK, serverManager.GetAll()) } // Creates a new server on the wings daemon and begins the installation process @@ -52,7 +52,8 @@ func postCreateServer(c *gin.Context) { // Plop that server instance onto the request so that it can be referenced in // requests from here-on out. - server.GetServers().Add(install.Server()) + serverManager := ServerManagerFromContext(c) + serverManager.Add(install.Server()) // Begin the installation process in the background to not block the request // cycle. If there are any errors they will be logged and communicated back diff --git a/router/router_transfer.go b/router/router_transfer.go index ee13c97..e487c04 100644 --- a/router/router_transfer.go +++ b/router/router_transfer.go @@ -323,19 +323,19 @@ func postTransfer(c *gin.Context) { i.Server().Events().Publish(server.TransferLogsEvent, output) } + serverManager := ServerManagerFromContext(c) + // Mark the server as transferring to prevent problems later on during the process and // then push the server into the global server collection for this instance. i.Server().SetTransferring(true) - server.GetServers().Add(i.Server()) + serverManager.Add(i.Server()) defer func(s *server.Server) { // In the event that this transfer call fails, remove the server from the global // server tracking so that we don't have a dangling instance. if err := data.sendTransferStatus(!hasError); hasError || err != nil { sendTransferLog("Server transfer failed, check Wings logs for additional information.") s.Events().Publish(server.TransferStatusEvent, "failure") - server.GetServers().Remove(func(s2 *server.Server) bool { - return s.Id() == s2.Id() - }) + serverManager.Remove(s) // If the transfer status was successful but the request failed, act like the transfer failed. if !hasError && err != nil { diff --git a/server/loader.go b/server/loader.go index c2d401e..b22b93f 100644 --- a/server/loader.go +++ b/server/loader.go @@ -1,6 +1,7 @@ package server import ( + "context" "encoding/json" "fmt" "os" @@ -15,26 +16,21 @@ import ( "github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/environment/docker" + "github.com/pterodactyl/wings/panelapi" "github.com/pterodactyl/wings/server/filesystem" ) -var servers = NewCollection(nil) - -func GetServers() *Collection { - return servers -} - // Iterates over a given directory and loads all of the servers listed before returning // them to the calling function. -func LoadDirectory() error { - if len(servers.items) != 0 { +func (m *manager) Initialize(serversPerPage int) error { + if len(m.servers.items) != 0 { return errors.New("cannot call LoadDirectory with a non-nil collection") } log.Info("fetching list of servers from API") - configs, err := api.New().GetServers() + assignedServers, err := m.panelClient.GetServers(context.TODO(), serversPerPage) if err != nil { - if !api.IsRequestError(err) { + if !panelapi.IsRequestError(err) { return err } @@ -42,13 +38,11 @@ func LoadDirectory() error { } start := time.Now() - log.WithField("total_configs", len(configs)).Info("processing servers returned by the API") + log.WithField("total_configs", len(assignedServers)).Info("processing servers returned by the API") pool := workerpool.New(runtime.NumCPU()) log.Debugf("using %d workerpools to instantiate server instances", runtime.NumCPU()) - for _, data := range configs { - data := data - + for _, data := range assignedServers { pool.Submit(func() { // Parse the json.RawMessage into an expected struct value. We do this here so that a single broken // server does not cause the entire boot process to hang, and allows us to show more useful error @@ -69,7 +63,7 @@ func LoadDirectory() error { return } - servers.Add(s) + m.Add(s) }) } diff --git a/server/manager.go b/server/manager.go new file mode 100644 index 0000000..4d4fe60 --- /dev/null +++ b/server/manager.go @@ -0,0 +1,46 @@ +package server + +import ( + "github.com/pterodactyl/wings/panelapi" +) + +type Manager interface { + // Initialize fetches all servers assigned to this node from the API. + Initialize(serversPerPage int) error + GetAll() []*Server + Get(uuid string) *Server + Add(s *Server) + Remove(s *Server) +} + +type manager struct { + servers Collection + + panelClient panelapi.Client +} + +// NewManager creates a new server manager. +func NewManager(panelClient panelapi.Client) Manager { + return &manager{panelClient: panelClient} +} + +func (m *manager) GetAll() []*Server { + return m.servers.items +} + +func (m *manager) Get(uuid string) *Server { + return m.servers.Find(func(s *Server) bool { + return s.Id() == uuid + }) +} + +func (m *manager) Add(s *Server) { + s.manager = m + m.servers.Add(s) +} + +func (m *manager) Remove(s *Server) { + m.servers.Remove(func(sf *Server) bool { + return sf.Id() == s.Id() + }) +} diff --git a/server/server.go b/server/server.go index f7acc9a..5d824a5 100644 --- a/server/server.go +++ b/server/server.go @@ -27,6 +27,9 @@ type Server struct { ctx context.Context ctxCancel *context.CancelFunc + // manager holds a reference to the manager responsible for the server + manager *manager + emitterLock sync.Mutex powerLock *semaphore.Weighted throttleOnce sync.Once diff --git a/server/state.go b/server/state.go index 682b8b7..3e4378a 100644 --- a/server/state.go +++ b/server/state.go @@ -36,10 +36,10 @@ func CachedServerStates() (map[string]string, error) { } // saveServerStates . -func saveServerStates() error { +func (m *manager) saveServerStates() error { // Get the states of all servers on the daemon. states := map[string]string{} - for _, s := range GetServers().All() { + for _, s := range m.GetAll() { states[s.Id()] = s.Environment.State() } @@ -84,7 +84,7 @@ func (s *Server) OnStateChange() { // We also get the benefit of server status changes always propagating corrected configurations // to the disk should we forget to do it elsewhere. go func() { - if err := saveServerStates(); err != nil { + if err := s.manager.saveServerStates(); err != nil { s.Log().WithField("error", err).Warn("failed to write server states to disk") } }() diff --git a/sftp/server.go b/sftp/server.go index a0ae24b..38b8b4b 100644 --- a/sftp/server.go +++ b/sftp/server.go @@ -12,12 +12,12 @@ import ( "os" "path" "strings" - "time" "github.com/apex/log" "github.com/patrickmn/go-cache" "github.com/pkg/sftp" "github.com/pterodactyl/wings/api" + "github.com/pterodactyl/wings/server" "golang.org/x/crypto/ssh" ) @@ -39,6 +39,8 @@ type Server struct { Settings Settings User User + serverManager server.Manager + PathValidator func(fs FileSystem, p string) (string, error) DiskSpaceValidator func(fs FileSystem) bool @@ -48,13 +50,6 @@ type Server struct { CredentialValidator func(r api.SftpAuthRequest) (*api.SftpAuthResponse, error) } -// Create a new server configuration instance. -func New(c *Server) error { - c.cache = cache.New(5*time.Minute, 10*time.Minute) - - return nil -} - // Initialize the SFTP server and add a persistent listener to handle inbound SFTP connections. func (c *Server) Initialize() error { serverConfig := &ssh.ServerConfig{ diff --git a/sftp/sftp.go b/sftp/sftp.go index 995db58..d1e6849 100644 --- a/sftp/sftp.go +++ b/sftp/sftp.go @@ -1,11 +1,13 @@ package sftp import ( + "time" + "emperror.dev/errors" "github.com/apex/log" + "github.com/patrickmn/go-cache" "github.com/pterodactyl/wings/api" "github.com/pterodactyl/wings/config" - "github.com/pterodactyl/wings/server" ) var noMatchingServerError = errors.New("no matching server with that UUID was found") @@ -22,14 +24,11 @@ func Initialize(config config.SystemConfiguration) error { BindAddress: config.Sftp.Address, BindPort: config.Sftp.Port, }, - CredentialValidator: validateCredentials, - PathValidator: validatePath, - DiskSpaceValidator: validateDiskSpace, - } - - if err := New(s); err != nil { - return err + cache: cache.New(5*time.Minute, 10*time.Minute), } + s.CredentialValidator = s.validateCredentials + s.PathValidator = s.validatePath + s.DiskSpaceValidator = s.validateDiskSpace // Initialize the SFTP server in a background thread since this is // a long running operation. @@ -42,33 +41,25 @@ func Initialize(config config.SystemConfiguration) error { return nil } -func validatePath(fs FileSystem, p string) (string, error) { - s := server.GetServers().Find(func(server *server.Server) bool { - return server.Id() == fs.UUID - }) - - if s == nil { +func (s *Server) validatePath(fs FileSystem, p string) (string, error) { + srv := s.serverManager.Get(fs.UUID) + if srv == nil { return "", noMatchingServerError } - - return s.Filesystem().SafePath(p) + return srv.Filesystem().SafePath(p) } -func validateDiskSpace(fs FileSystem) bool { - s := server.GetServers().Find(func(server *server.Server) bool { - return server.Id() == fs.UUID - }) - - if s == nil { +func (s *Server) validateDiskSpace(fs FileSystem) bool { + srv := s.serverManager.Get(fs.UUID) + if srv == nil { return false } - - return s.Filesystem().HasSpaceAvailable(true) + return srv.Filesystem().HasSpaceAvailable(true) } // Validates a set of credentials for a SFTP login against Pterodactyl Panel and returns // the server's UUID if the credentials were valid. -func validateCredentials(c api.SftpAuthRequest) (*api.SftpAuthResponse, error) { +func (s *Server) validateCredentials(c api.SftpAuthRequest) (*api.SftpAuthResponse, error) { f := log.Fields{"subsystem": "sftp", "username": c.User, "ip": c.IP} log.WithFields(f).Debug("validating credentials for SFTP connection") @@ -83,15 +74,12 @@ func validateCredentials(c api.SftpAuthRequest) (*api.SftpAuthResponse, error) { return resp, err } - s := server.GetServers().Find(func(server *server.Server) bool { - return server.Id() == resp.Server - }) - - if s == nil { + srv := s.serverManager.Get(resp.Server) + if srv == nil { return resp, noMatchingServerError } - s.Log().WithFields(f).Debug("credentials successfully validated and matched user to server instance") + srv.Log().WithFields(f).Debug("credentials successfully validated and matched user to server instance") return resp, err }