diff --git a/config/config.go b/config/config.go index b1331236f..cbd913243 100644 --- a/config/config.go +++ b/config/config.go @@ -411,6 +411,34 @@ func Set(c *Configuration) { _config = c } +// ResolveToken populates the derived Token field, preferring values pinned +// through the environment over those in the configuration itself. +// +// Set remote when the values came from the Panel. Local values may use +// "file://" or "$VAR" indirection; expanding one sent over the network would +// leak files and environment variables back out through the token we attach to +// every request. +func (c *Configuration) ResolveToken(remote bool) error { + resolve := func(env, local string) (string, error) { + if env != "" { + return Expand(env) + } + if remote { + return local, nil + } + return Expand(local) + } + + var err error + if c.Token.ID, err = resolve(os.Getenv("WINGS_TOKEN_ID"), c.AuthenticationTokenId); err != nil { + return err + } + if c.Token.Token, err = resolve(os.Getenv("WINGS_TOKEN"), c.AuthenticationToken); err != nil { + return err + } + return nil +} + // SetDebugViaFlag tracks if the application is running in debug mode because of // a command line flag argument. If so we do not want to store that configuration // change to the disk. @@ -600,23 +628,7 @@ func FromFile(path string) error { return err } - c.Token = Token{ - ID: os.Getenv("WINGS_TOKEN_ID"), - Token: os.Getenv("WINGS_TOKEN"), - } - if c.Token.ID == "" { - c.Token.ID = c.AuthenticationTokenId - } - if c.Token.Token == "" { - c.Token.Token = c.AuthenticationToken - } - - c.Token.ID, err = Expand(c.Token.ID) - if err != nil { - return err - } - c.Token.Token, err = Expand(c.Token.Token) - if err != nil { + if err := c.ResolveToken(false); err != nil { return err } @@ -860,7 +872,7 @@ func Expand(v string) (string, error) { b, err := os.ReadFile(p) if err != nil { - return "", nil + return "", err } v = string(bytes.TrimRight(bytes.TrimRight(b, "\r"), "\n")) } diff --git a/remote/http.go b/remote/http.go index da3a413af..852db52fd 100644 --- a/remote/http.go +++ b/remote/http.go @@ -9,6 +9,7 @@ import ( "net/http" "strconv" "strings" + "sync" "time" "github.com/pterodactyl/wings/internal/models" @@ -33,11 +34,13 @@ type Client interface { SetTransferStatus(ctx context.Context, uuid string, successful bool) error ValidateSftpCredentials(ctx context.Context, request SftpAuthRequest) (SftpAuthResponse, error) SendActivityLogs(ctx context.Context, activity []models.Activity) error + SetCredentials(id, token string) } type client struct { httpClient *http.Client baseUrl string + mu sync.RWMutex tokenId string token string maxAttempts int @@ -68,6 +71,22 @@ func WithCredentials(id, token string) ClientOption { } } +// SetCredentials replaces the credentials used when making requests to the +// remote API endpoint. +func (c *client) SetCredentials(id, token string) { + c.mu.Lock() + defer c.mu.Unlock() + c.tokenId = id + c.token = token +} + +// credentials returns the credentials currently in use by this client. +func (c *client) credentials() (string, string) { + c.mu.RLock() + defer c.mu.RUnlock() + return c.tokenId, c.token +} + // WithHttpClient sets the underlying HTTP client instance to use when making // requests to the Panel API. func WithHttpClient(httpClient *http.Client) ClientOption { @@ -105,10 +124,11 @@ func (c *client) requestOnce(ctx context.Context, method, path string, body io.R return nil, err } - req.Header.Set("User-Agent", fmt.Sprintf("Pterodactyl Wings/v%s (id:%s)", system.Version, c.tokenId)) + tokenId, token := c.credentials() + req.Header.Set("User-Agent", fmt.Sprintf("Pterodactyl Wings/v%s (id:%s)", system.Version, tokenId)) req.Header.Set("Accept", "application/vnd.pterodactyl.v1+json") req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s.%s", c.tokenId, c.token)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s.%s", tokenId, token)) // Call all opts functions to allow modifying the request for _, o := range opts { diff --git a/router/router_server_backup_test.go b/router/router_server_backup_test.go index 3a9f5978f..15951fc73 100644 --- a/router/router_server_backup_test.go +++ b/router/router_server_backup_test.go @@ -83,6 +83,8 @@ func (c backupTestRemoteClient) SendActivityLogs(context.Context, []models.Activ return nil } +func (c backupTestRemoteClient) SetCredentials(_, _ string) {} + type backupTestEnvironment struct{} func (backupTestEnvironment) Type() string { return "test" } diff --git a/router/router_system.go b/router/router_system.go index 75773c2f3..0358ca0db 100644 --- a/router/router_system.go +++ b/router/router_system.go @@ -143,6 +143,22 @@ func postUpdateConfiguration(c *gin.Context) { cfg.Api.Ssl.CertificateFile = config.Get().Api.Ssl.CertificateFile } + // The token that everything authenticates against is a derived value that is + // not part of the payload sent by the Panel, so it has to be re-resolved from + // the new token values. + if err := cfg.ResolveToken(true); err != nil { + middleware.CaptureAndAbort(c, err) + return + } + + // Refuse to go any further with a token we could never authenticate against. + if cfg.Token.ID == "" || cfg.Token.Token == "" { + middleware.CaptureAndAbort(c, errors.New("config: refusing to apply an update with an empty authentication token")) + return + } + + tokenId, token := cfg.Token.ID, cfg.Token.Token + // Try to write this new configuration to the disk before updating our global // state with it. if err := config.WriteToDisk(cfg); err != nil { @@ -152,6 +168,11 @@ func postUpdateConfiguration(c *gin.Context) { // Since we wrote it to the disk successfully now update the global configuration // state to use this new configuration struct. config.Set(cfg) + + // Requests we make back to the Panel use credentials that were captured when + // the client was created at boot, so they have to be rotated explicitly. + middleware.ExtractManager(c).Client().SetCredentials(tokenId, token) + c.JSON(http.StatusOK, postUpdateConfigurationResponse{ Applied: true, })