mirror of
https://github.com/pterodactyl/wings.git
synced 2025-12-10 15:40:14 -06:00
This change fixes pterodactyl/panel#3921 by implementing logic to drop the oldest message in a channel and push the newest message onto the channel when the channel buffer is full. This is distinctly different than the previous implementation which just dropped the newest messages, leading to confusing behavior on the client side when a large amount of data was sent over the connection. Up to 10ms per channel is allowed for blocking before falling back to the drop logic.
164 lines
4.9 KiB
Go
164 lines
4.9 KiB
Go
package websocket
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"time"
|
|
|
|
"emperror.dev/errors"
|
|
"github.com/goccy/go-json"
|
|
|
|
"github.com/pterodactyl/wings/events"
|
|
"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
|
|
// 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 that notice as well.
|
|
func (h *Handler) listenForExpiration(ctx context.Context) {
|
|
// 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.
|
|
ticker := time.NewTicker(time.Second * 30)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
jwt := h.GetJwt()
|
|
if jwt != nil {
|
|
if jwt.ExpirationTime.Unix()-time.Now().Unix() <= 0 {
|
|
_ = h.SendJson(Message{Event: TokenExpiredEvent})
|
|
} else if jwt.ExpirationTime.Unix()-time.Now().Unix() <= 60 {
|
|
_ = h.SendJson(Message{Event: TokenExpiringEvent})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var e = []string{
|
|
server.StatsEvent,
|
|
server.StatusEvent,
|
|
server.ConsoleOutputEvent,
|
|
server.InstallOutputEvent,
|
|
server.InstallStartedEvent,
|
|
server.InstallCompletedEvent,
|
|
server.DaemonMessageEvent,
|
|
server.BackupCompletedEvent,
|
|
server.BackupRestoreCompletedEvent,
|
|
server.TransferLogsEvent,
|
|
server.TransferStatusEvent,
|
|
}
|
|
|
|
// ListenForServerEvents will listen for different events happening on a server
|
|
// and send them along to the connected websocket client. This function will
|
|
// block until the context provided to it is canceled.
|
|
func (h *Handler) listenForServerEvents(ctx context.Context) error {
|
|
var o sync.Once
|
|
var err error
|
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
eventChan := make(chan events.Event)
|
|
logOutput := make(chan []byte, 8)
|
|
installOutput := make(chan []byte, 4)
|
|
h.server.Events().On(eventChan, e...)
|
|
h.server.Sink(server.LogSink).On(logOutput)
|
|
h.server.Sink(server.InstallSink).On(installOutput)
|
|
|
|
onError := func(evt string, err2 error) {
|
|
h.Logger().WithField("event", evt).WithField("error", err2).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 = err2
|
|
})
|
|
cancel()
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
break
|
|
case e := <-logOutput:
|
|
sendErr := h.SendJson(Message{Event: server.ConsoleOutputEvent, Args: []string{string(e)}})
|
|
if sendErr == nil {
|
|
continue
|
|
}
|
|
onError(server.ConsoleOutputEvent, sendErr)
|
|
case e := <-installOutput:
|
|
sendErr := h.SendJson(Message{Event: server.InstallOutputEvent, Args: []string{string(e)}})
|
|
if sendErr == nil {
|
|
continue
|
|
}
|
|
onError(server.InstallOutputEvent, sendErr)
|
|
case e := <-eventChan:
|
|
var sendErr error
|
|
message := Message{Event: e.Topic}
|
|
if str, ok := e.Data.(string); ok {
|
|
message.Args = []string{str}
|
|
} else if b, ok := e.Data.([]byte); ok {
|
|
message.Args = []string{string(b)}
|
|
} else {
|
|
b, sendErr = json.Marshal(e.Data)
|
|
if sendErr == nil {
|
|
message.Args = []string{string(b)}
|
|
}
|
|
}
|
|
|
|
if sendErr == nil {
|
|
sendErr = h.SendJson(message)
|
|
if sendErr == nil {
|
|
continue
|
|
}
|
|
}
|
|
onError(message.Event, sendErr)
|
|
}
|
|
break
|
|
}
|
|
|
|
// These functions will automatically close the channel if it hasn't been already.
|
|
h.server.Events().Off(eventChan, e...)
|
|
h.server.Sink(server.LogSink).Off(logOutput)
|
|
h.server.Sink(server.InstallSink).Off(installOutput)
|
|
|
|
// 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
|
|
}
|