wings/server/activity.go
DaneEveritt 59fbd2bcea
Add initial niaeve implementation of SFTP logging
This will end up flooding the activity logs due to the way SFTP works, we'll need to have an intermediate step in Wings that batches events every 10 seconds or so and submits them as a single "event" for activity.
2022-07-09 19:37:42 -04:00

136 lines
4.5 KiB
Go

package server
import (
"emperror.dev/errors"
"github.com/apex/log"
"github.com/goccy/go-json"
"github.com/pterodactyl/wings/internal/database"
"github.com/xujiajun/nutsdb"
"regexp"
"time"
)
type Event string
type ActivityMeta map[string]interface{}
const ActivityPowerPrefix = "server:power."
const (
ActivityConsoleCommand = Event("server:console.command")
ActivityFileDeleted = Event("server:file.delete")
ActivityFileRename = Event("server:file.rename")
ActivityFileCreateDirectory = Event("server:file.create-directory")
ActivityFileWrite = Event("server:file.write")
ActivityFileRead = Event("server:file.read")
)
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
// user.
User string `json:"user"`
// Server is the UUID of the server this event is associated with.
Server string `json:"server"`
// Event is a string that describes what occurred, and is used by the Panel instance to
// properly associate this event in the activity logs.
Event Event `json:"event"`
// Metadata is either a null value, string, or a JSON blob with additional event specific
// metadata that can be provided.
Metadata ActivityMeta `json:"metadata"`
// IP is the IP address that triggered this event, or an empty string if it cannot be
// determined properly.
IP string `json:"ip"`
Timestamp time.Time `json:"timestamp"`
}
// RequestActivity is a wrapper around a LoggedEvent that is able to track additional request
// specific metadata including the specific user and IP address associated with all subsequent
// events. The internal logged event structure can be extracted by calling RequestEvent.Event().
type RequestActivity struct {
server string
user string
ip string
}
// Event returns the underlying logged event from the RequestEvent instance and sets the
// specific event and metadata on it.
func (ra RequestActivity) Event(event Event, metadata ActivityMeta) Activity {
return Activity{
User: ra.user,
Server: ra.server,
IP: ra.ip,
Event: event,
Metadata: metadata,
}
}
// Save creates a new event instance and saves it. If an error is encountered it is automatically
// logged to the provided server's error logging output. The error is also returned to the caller
// but can be ignored.
func (ra RequestActivity) Save(s *Server, event Event, metadata ActivityMeta) error {
if err := ra.Event(event, metadata).Save(); err != nil {
s.Log().WithField("error", err).WithField("event", event).Error("activity: failed to save event")
return errors.WithStack(err)
}
return nil
}
// IP returns the IP address associated with this entry.
func (ra RequestActivity) IP() string {
return ra.ip
}
// SetUser clones the RequestActivity struct and sets a new user value on the copy
// before returning it.
func (ra RequestActivity) SetUser(u string) RequestActivity {
c := ra
c.user = u
return c
}
// Save logs the provided event using Wings' internal K/V store so that we can then
// pass it along to the Panel at set intervals. In addition, this will ensure that the events
// are persisted to the disk, even between instance restarts.
func (a Activity) Save() error {
if a.Timestamp.IsZero() {
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")
}
return database.DB().Update(func(tx *nutsdb.Tx) error {
log.WithField("subsystem", "activity").
WithFields(log.Fields{"server": a.Server, "user": a.User, "event": a.Event, "ip": a.IP}).
Debug("saving activity to database")
if err := tx.RPush(database.ServerActivityBucket, []byte("events"), value); err != nil {
return errors.WithStack(err)
}
return nil
})
}
func (s *Server) NewRequestActivity(user string, ip string) RequestActivity {
return RequestActivity{server: s.ID(), user: user, ip: ip}
}
// NewActivity creates a new event instance for the server in question.
func (s *Server) NewActivity(user string, event Event, metadata ActivityMeta, ip string) Activity {
return Activity{
User: user,
Server: s.ID(),
Event: event,
Metadata: metadata,
IP: ip,
}
}