diff --git a/internal/cron/activity_cron.go b/internal/cron/activity_cron.go index fd55eea..c47b811 100644 --- a/internal/cron/activity_cron.go +++ b/internal/cron/activity_cron.go @@ -3,6 +3,8 @@ package cron import ( "context" "emperror.dev/errors" + "github.com/apex/log" + "github.com/goccy/go-json" "github.com/pterodactyl/wings/internal/database" "github.com/pterodactyl/wings/server" "github.com/pterodactyl/wings/system" @@ -19,12 +21,12 @@ func processActivityLogs(m *server.Manager) error { } defer processing.Store(false) - var b [][]byte + var list [][]byte err := database.DB().View(func(tx *nutsdb.Tx) error { // Grab the oldest 100 activity events that have been logged and send them back to the // Panel for processing. Once completed, delete those events from the database and then // release the lock on this process. - list, err := tx.LRange(database.ServerActivityBucket, []byte("events"), 0, 1) + l, err := tx.LRange(database.ServerActivityBucket, []byte("events"), 0, 1) if err != nil { // This error is returned when the bucket doesn't exist, which is likely on the // first invocations of Wings since we haven't yet logged any data. There is nothing @@ -34,18 +36,25 @@ func processActivityLogs(m *server.Manager) error { } return errors.WithStackIf(err) } - b = list + list = l return nil }) - // If there is an error, return it. If there is no data to send to the Panel don't waste - // an API call, just return here. WithStackIf will return "nil" when the value provided to - // it is also nil. - if err != nil || len(b) == 0 { + if err != nil || len(list) == 0 { return errors.WithStackIf(err) } - if err := m.Client().SendActivityLogs(context.Background(), b); err != nil { + var processed []json.RawMessage + for _, l := range list { + var v json.RawMessage + if err := json.Unmarshal(l, &v); err != nil { + log.WithField("error", errors.WithStack(err)).Warn("failed to parse activity event json, skipping entry") + continue + } + processed = append(processed, v) + } + + if err := m.Client().SendActivityLogs(context.Background(), processed); err != nil { return errors.WrapIf(err, "cron: failed to send activity events to Panel") } diff --git a/remote/http.go b/remote/http.go index 8f44bd6..4b953ba 100644 --- a/remote/http.go +++ b/remote/http.go @@ -30,7 +30,7 @@ type Client interface { SetInstallationStatus(ctx context.Context, uuid string, successful bool) error SetTransferStatus(ctx context.Context, uuid string, successful bool) error ValidateSftpCredentials(ctx context.Context, request SftpAuthRequest) (SftpAuthResponse, error) - SendActivityLogs(ctx context.Context, activity [][]byte) error + SendActivityLogs(ctx context.Context, activity []json.RawMessage) error } type client struct { @@ -134,6 +134,9 @@ func (c *client) request(ctx context.Context, method, path string, body *bytes.B err := backoff.Retry(func() error { var b bytes.Buffer if body != nil { + // We have to create a copy of the body, otherwise attempting this request again will + // send no data if there was initially a body since the "requestOnce" method will read + // the whole buffer, thus leaving it empty at the end. if _, err := b.Write(body.Bytes()); err != nil { return backoff.Permanent(errors.Wrap(err, "http: failed to copy body buffer")) } diff --git a/remote/servers.go b/remote/servers.go index 44f0c4f..588bdd4 100644 --- a/remote/servers.go +++ b/remote/servers.go @@ -3,6 +3,7 @@ package remote import ( "context" "fmt" + "github.com/goccy/go-json" "strconv" "sync" @@ -179,8 +180,8 @@ func (c *client) SendRestorationStatus(ctx context.Context, backup string, succe } // SendActivityLogs sends activity logs back to the Panel for processing. -func (c *client) SendActivityLogs(ctx context.Context, activity [][]byte) error { - resp, err := c.Post(ctx, "/activty", d{"data": activity}) +func (c *client) SendActivityLogs(ctx context.Context, activity []json.RawMessage) error { + resp, err := c.Post(ctx, "/activity", d{"data": activity}) if err != nil { return errors.WithStackIf(err) } diff --git a/server/activity.go b/server/activity.go index 8356050..d15fc21 100644 --- a/server/activity.go +++ b/server/activity.go @@ -6,6 +6,7 @@ import ( "github.com/goccy/go-json" "github.com/pterodactyl/wings/internal/database" "github.com/xujiajun/nutsdb" + "regexp" "time" ) @@ -17,6 +18,8 @@ const ( ActivityConsoleCommand = Event("console_command") ) +var ipTrimRegex = regexp.MustCompile(`(:\d*)?$`) + type Activity struct { // User is UUID of the user that triggered this event, or an empty string if the event // cannot be tied to a specific user, in which case we will assume it was the system @@ -89,6 +92,10 @@ func (a Activity) Save() error { a.Timestamp = time.Now().UTC() } + // Since the "RemoteAddr" field can often include a port on the end we need to + // trim that off, otherwise it'll fail validation when sent to the Panel. + a.IP = ipTrimRegex.ReplaceAllString(a.IP, "") + value, err := json.Marshal(a) if err != nil { return errors.Wrap(err, "database: failed to marshal activity into json bytes")