Merge branch 'develop' into v2

This commit is contained in:
Matthew Penner 2021-10-28 23:58:12 -06:00
commit 10a2ffc0a7
No known key found for this signature in database
GPG Key ID: BAB67850901908A8
6 changed files with 123 additions and 41 deletions

View File

@ -1,5 +1,23 @@
# Changelog # Changelog
## v1.5.3
### Fixed
* Fixes improper event registration and error handling during socket authentication that would cause the incorrect error message to be returned to the client, or no error in some scenarios. Event registration is now delayed until the socket is fully authenticated to ensure needless listeners are not registed.
* Fixes dollar signs always being evaluated as environment variables with no way to escape them. They can now be escaped as `$$` which will transform into a single dollar sign.
### Changed
* A websocket connection to a server will be closed by Wings if there is a send error encountered and the client will be left to handle reconnections, rather than simply logging the error and continuing to listen for new events.
## v1.5.2
### Fixed
* Fixes servers not properly re-syncing with the Panel if they are already running causing them to be hard-stopped when terminated, rather than stopped with the configured action.
### Changed
* Changes SFTP server implementation to use ED25519 server keys rather than deprecated SHA1 RSA keys.
### Added
* Adds server uptime output in the stats event emitted to the websocket.
## v1.5.1 ## v1.5.1
### Added ### Added
* Global configuration option for toggling server crash detection (`system.crash_detection.enabled`) * Global configuration option for toggling server crash detection (`system.crash_detection.enabled`)

View File

@ -457,9 +457,22 @@ func FromFile(path string) error {
return err return err
} }
// Replace environment variables within the configuration file with their // Replace environment variables within the configuration file with their
// values from the host system. // values from the host system. This function works almost identically to
b = []byte(os.ExpandEnv(string(b))) // the default os.ExpandEnv function, except it supports escaping dollar
if err := yaml.Unmarshal(b, c); err != nil { // signs in the text if you pass "$$" through.
//
// "some$$foo" -> "some$foo"
// "some$foo" -> "some" (or "someVALUE_OF_FOO" if FOO is defined in env)
//
// @see https://github.com/pterodactyl/panel/issues/3692
exp := os.Expand(string(b), func(s string) string {
if s == "$" {
return s
}
return os.Getenv(s)
})
if err := yaml.Unmarshal([]byte(exp), c); err != nil {
return err return err
} }
// Store this configuration in the global state. // Store this configuration in the global state.

View File

@ -24,12 +24,6 @@ var expectedCloseCodes = []int{
func getServerWebsocket(c *gin.Context) { func getServerWebsocket(c *gin.Context) {
manager := middleware.ExtractManager(c) manager := middleware.ExtractManager(c)
s, _ := manager.Get(c.Param("server")) s, _ := manager.Get(c.Param("server"))
handler, err := websocket.GetHandler(s, c.Writer, c.Request)
if err != nil {
NewServerError(err, s).Abort(c)
return
}
defer handler.Connection.Close()
// Create a context that can be canceled when the user disconnects from this // Create a context that can be canceled when the user disconnects from this
// socket that will also cancel listeners running in separate threads. If the // socket that will also cancel listeners running in separate threads. If the
@ -38,10 +32,22 @@ func getServerWebsocket(c *gin.Context) {
ctx, cancel := context.WithCancel(c.Request.Context()) ctx, cancel := context.WithCancel(c.Request.Context())
defer cancel() defer cancel()
handler, err := websocket.GetHandler(s, c.Writer, c.Request)
if err != nil {
NewServerError(err, s).Abort(c)
return
}
defer handler.Connection.Close()
// Track this open connection on the server so that we can close them all programmatically // Track this open connection on the server so that we can close them all programmatically
// if the server is deleted. // if the server is deleted.
s.Websockets().Push(handler.Uuid(), &cancel) s.Websockets().Push(handler.Uuid(), &cancel)
defer s.Websockets().Remove(handler.Uuid()) handler.Logger().Debug("opening connection to server websocket")
defer func() {
s.Websockets().Remove(handler.Uuid())
handler.Logger().Debug("closing connection to server websocket")
}()
// If the server is deleted we need to send a close message to the connected client // If the server is deleted we need to send a close message to the connected client
// so that they disconnect since there will be no more events sent along. Listen for // so that they disconnect since there will be no more events sent along. Listen for
@ -57,16 +63,13 @@ func getServerWebsocket(c *gin.Context) {
} }
}() }()
go handler.ListenForServerEvents(ctx)
go handler.ListenForExpiration(ctx)
for { for {
j := websocket.Message{} j := websocket.Message{}
_, p, err := handler.Connection.ReadMessage() _, p, err := handler.Connection.ReadMessage()
if err != nil { if err != nil {
if ws.IsUnexpectedCloseError(err, expectedCloseCodes...) { if ws.IsUnexpectedCloseError(err, expectedCloseCodes...) {
s.Log().WithField("error", err).Warn("error handling websocket message for server") handler.Logger().WithField("error", err).Warn("error handling websocket message for server")
} }
break break
} }
@ -79,7 +82,7 @@ func getServerWebsocket(c *gin.Context) {
} }
go func(msg websocket.Message) { go func(msg websocket.Message) {
if err := handler.HandleInbound(msg); err != nil { if err := handler.HandleInbound(ctx, msg); err != nil {
handler.SendErrorJson(msg, err) handler.SendErrorJson(msg, err)
} }
}(j) }(j)

View File

@ -2,17 +2,45 @@ package websocket
import ( import (
"context" "context"
"sync"
"time" "time"
"emperror.dev/errors"
"github.com/pterodactyl/wings/events" "github.com/pterodactyl/wings/events"
"github.com/pterodactyl/wings/server" "github.com/pterodactyl/wings/server"
) )
// RegisterListenerEvents will setup the server event listeners and expiration
// timers. This is only triggered on first authentication with the websocket,
// reconnections will not call it.
//
// This needs to be called once the socket is properly authenticated otherwise
// you'll get a flood of error spam in the output that doesn't make sense because
// Docker events being output to the socket will fail when it hasn't been
// properly initialized yet.
//
// @see https://github.com/pterodactyl/panel/issues/3295
func (h *Handler) registerListenerEvents(ctx context.Context) {
h.Logger().Debug("registering event listeners for connection")
go func() {
if err := h.listenForServerEvents(ctx); err != nil {
h.Logger().Warn("error while processing server event; closing websocket connection")
if err := h.Connection.Close(); err != nil {
h.Logger().WithField("error", errors.WithStack(err)).Error("error closing websocket connection")
}
}
}()
go h.listenForExpiration(ctx)
}
// ListenForExpiration checks the time to expiration on the JWT every 30 seconds // ListenForExpiration checks the time to expiration on the JWT every 30 seconds
// until the token has expired. If we are within 3 minutes of the token expiring, // until the token has expired. If we are within 3 minutes of the token expiring,
// send a notice over the socket that it is expiring soon. If it has expired, // send a notice over the socket that it is expiring soon. If it has expired,
// send that notice as well. // send that notice as well.
func (h *Handler) ListenForExpiration(ctx context.Context) { func (h *Handler) listenForExpiration(ctx context.Context) {
// Make a ticker and completion channel that is used to continuously poll the // Make a ticker and completion channel that is used to continuously poll the
// JWT stored in the session to send events to the socket when it is expiring. // JWT stored in the session to send events to the socket when it is expiring.
ticker := time.NewTicker(time.Second * 30) ticker := time.NewTicker(time.Second * 30)
@ -52,24 +80,44 @@ var e = []string{
// ListenForServerEvents will listen for different events happening on a server // ListenForServerEvents will listen for different events happening on a server
// and send them along to the connected websocket client. This function will // and send them along to the connected websocket client. This function will
// block until the context provided to it is canceled. // block until the context provided to it is canceled.
func (h *Handler) ListenForServerEvents(ctx context.Context) { func (h *Handler) listenForServerEvents(pctx context.Context) error {
h.server.Log().Debug("listening for server events over websocket") var o sync.Once
var err error
ctx, cancel := context.WithCancel(pctx)
callback := func(e events.Event) { callback := func(e events.Event) {
if err := h.SendJson(&Message{Event: e.Topic, Args: []string{e.Data}}); err != nil { if sendErr := h.SendJson(&Message{Event: e.Topic, Args: []string{e.Data}}); sendErr != nil {
h.server.Log().WithField("error", err).Warn("error while sending server data over websocket") h.Logger().WithField("event", e.Topic).WithField("error", sendErr).Error("failed to send event over server websocket")
// Avoid race conditions by only setting the error once and then canceling
// the context. This way if additional processing errors come through due
// to a massive flood of things you still only report and stop at the first.
o.Do(func() {
err = sendErr
cancel()
})
} }
} }
// Subscribe to all of the events with the same callback that will push the data out over the // Subscribe to all of the events with the same callback that will push the
// websocket for the server. // data out over the websocket for the server.
for _, evt := range e { for _, evt := range e {
h.server.Events().On(evt, &callback) h.server.Events().On(evt, &callback)
} }
<-ctx.Done() // When this function returns de-register all of the event listeners.
// Block until the context is stopped and then de-register all of the event listeners defer func() {
// that we registered earlier.
for _, evt := range e { for _, evt := range e {
h.server.Events().Off(evt, &callback) h.server.Events().Off(evt, &callback)
} }
}()
<-ctx.Done()
// If the internal context is stopped it is either because the parent context
// got canceled or because we ran into an error. If the "err" variable is nil
// we can assume the parent was canceled and need not perform any actions.
if err != nil {
return errors.WithStack(err)
}
return nil
} }

View File

@ -75,7 +75,7 @@ func NewTokenPayload(token []byte) (*tokens.WebsocketPayload, error) {
return &payload, nil return &payload, nil
} }
// Returns a new websocket handler using the context provided. // GetHandler returns a new websocket handler using the context provided.
func GetHandler(s *server.Server, w http.ResponseWriter, r *http.Request) (*Handler, error) { func GetHandler(s *server.Server, w http.ResponseWriter, r *http.Request) (*Handler, error) {
upgrader := websocket.Upgrader{ upgrader := websocket.Upgrader{
// Ensure that the websocket request is originating from the Panel itself, // Ensure that the websocket request is originating from the Panel itself,
@ -116,6 +116,12 @@ func (h *Handler) Uuid() uuid.UUID {
return h.uuid return h.uuid
} }
func (h *Handler) Logger() *log.Entry {
return log.WithField("subsystem", "websocket").
WithField("connection", h.Uuid().String()).
WithField("server", h.server.ID())
}
func (h *Handler) SendJson(v *Message) error { func (h *Handler) SendJson(v *Message) error {
// Do not send JSON down the line if the JWT on the connection is not valid! // Do not send JSON down the line if the JWT on the connection is not valid!
if err := h.TokenValid(); err != nil { if err := h.TokenValid(); err != nil {
@ -263,7 +269,7 @@ func (h *Handler) setJwt(token *tokens.WebsocketPayload) {
} }
// HandleInbound handles an inbound socket request and route it to the proper action. // HandleInbound handles an inbound socket request and route it to the proper action.
func (h *Handler) HandleInbound(m Message) error { func (h *Handler) HandleInbound(ctx context.Context, m Message) error {
if m.Event != AuthenticationEvent { if m.Event != AuthenticationEvent {
if err := h.TokenValid(); err != nil { if err := h.TokenValid(); err != nil {
h.unsafeSendJson(Message{ h.unsafeSendJson(Message{
@ -279,13 +285,6 @@ func (h *Handler) HandleInbound(m Message) error {
{ {
token, err := NewTokenPayload([]byte(strings.Join(m.Args, ""))) token, err := NewTokenPayload([]byte(strings.Join(m.Args, "")))
if err != nil { if err != nil {
// If the error says the JWT expired, send a token expired
// event and hopefully the client renews the token.
if err == jwt.ErrExpValidation {
h.SendJson(&Message{Event: TokenExpiredEvent})
return nil
}
return err return err
} }
@ -298,10 +297,7 @@ func (h *Handler) HandleInbound(m Message) error {
h.setJwt(token) h.setJwt(token)
// Tell the client they authenticated successfully. // Tell the client they authenticated successfully.
h.unsafeSendJson(Message{ h.unsafeSendJson(Message{Event: AuthenticationSuccessEvent})
Event: AuthenticationSuccessEvent,
Args: []string{},
})
// Check if the client was refreshing their authentication token // Check if the client was refreshing their authentication token
// instead of authenticating for the first time. // instead of authenticating for the first time.
@ -311,6 +307,11 @@ func (h *Handler) HandleInbound(m Message) error {
return nil return nil
} }
// Now that we've authenticated with the token and confirmed that we're not
// reconnecting to the socket, register the event listeners for the server and
// the token expiration.
h.registerListenerEvents(ctx)
// On every authentication event, send the current server status back // On every authentication event, send the current server status back
// to the client. :) // to the client. :)
state := h.server.Environment.State() state := h.server.Environment.State()

View File

@ -52,8 +52,7 @@ func (w *WebsocketBag) CancelAll() {
if w.conns != nil { if w.conns != nil {
for _, cancel := range w.conns { for _, cancel := range w.conns {
c := *cancel (*cancel)()
c()
} }
} }