Switch to gorm for activity logging

This commit is contained in:
DaneEveritt 2022-07-24 11:43:48 -04:00
parent 61baccb1a3
commit 8a867ccc44
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
17 changed files with 514 additions and 421 deletions

View File

@ -6,7 +6,7 @@ import (
"errors"
"fmt"
"github.com/pterodactyl/wings/internal/cron"
"github.com/pterodactyl/wings/internal/sqlite"
"github.com/pterodactyl/wings/internal/database"
log2 "log"
"net/http"
_ "net/http/pprof"
@ -132,7 +132,7 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
}),
)
if err := sqlite.Initialize(cmd.Context()); err != nil {
if err := database.Initialize(); err != nil {
log.WithField("error", err).Fatal("failed to initialize database")
}

19
go.mod
View File

@ -45,13 +45,14 @@ require (
)
require (
github.com/glebarez/sqlite v1.4.6
github.com/go-co-op/gocron v1.15.0
github.com/goccy/go-json v0.9.6
github.com/klauspost/compress v1.15.1
modernc.org/sqlite v1.17.3
gorm.io/gorm v1.23.8
)
require golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e // indirect
require golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
@ -70,6 +71,7 @@ require (
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/gammazero/deque v0.1.1 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.17.3 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.10.1 // indirect
@ -78,6 +80,8 @@ require (
github.com/golang/snappy v0.0.4 // indirect
github.com/gorilla/mux v1.7.4 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kr/fs v0.1.0 // indirect
@ -109,24 +113,17 @@ require (
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect
golang.org/x/tools v0.1.1 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb // indirect
google.golang.org/grpc v1.45.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/uint128 v1.1.1 // indirect
modernc.org/cc/v3 v3.36.0 // indirect
modernc.org/ccgo/v3 v3.16.6 // indirect
modernc.org/libc v1.16.7 // indirect
modernc.org/libc v1.16.17 // indirect
modernc.org/mathutil v1.4.1 // indirect
modernc.org/memory v1.1.1 // indirect
modernc.org/opt v0.1.1 // indirect
modernc.org/strutil v1.1.1 // indirect
modernc.org/token v1.0.0 // indirect
modernc.org/sqlite v1.17.3 // indirect
)

316
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,12 @@
package cron
import (
"bytes"
"context"
"database/sql"
"emperror.dev/errors"
"encoding/gob"
"github.com/pterodactyl/wings/internal/sqlite"
"github.com/pterodactyl/wings/internal/database"
"github.com/pterodactyl/wings/internal/models"
"github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/system"
"strings"
)
type activityCron struct {
@ -18,32 +15,6 @@ type activityCron struct {
max int64
}
const queryRegularActivity = `
SELECT id, event, user_uuid, server_uuid, metadata, ip, timestamp FROM activity_logs
WHERE event NOT LIKE 'server:sftp.%'
ORDER BY timestamp
LIMIT ?
`
type QueriedActivity struct {
id int
b []byte
server.Activity
}
// Parse parses the internal query results into the QueriedActivity type and then properly
// sets the Metadata onto it. This also sets the ID that was returned to ensure we're able
// to then delete all of the matching rows in the database after we're done.
func (qa *QueriedActivity) Parse(r *sql.Rows) error {
if err := r.Scan(&qa.id, &qa.Event, &qa.User, &qa.Server, &qa.b, &qa.IP, &qa.Timestamp); err != nil {
return errors.Wrap(err, "cron: failed to parse activity log")
}
if err := gob.NewDecoder(bytes.NewBuffer(qa.b)).Decode(&qa.Metadata); err != nil {
return errors.WithStack(err)
}
return nil
}
// Run executes the cronjob and ensures we fetch and send all of the stored activity to the
// Panel instance. Once activity is sent it is deleted from the local database instance. Any
// SFTP specific events are not handled in this cron, they're handled seperately to account
@ -56,47 +27,31 @@ func (ac *activityCron) Run(ctx context.Context) error {
}
defer ac.mu.Store(false)
rows, err := sqlite.Instance().QueryContext(ctx, queryRegularActivity, ac.max)
if err != nil {
return errors.Wrap(err, "cron: failed to query activity logs")
}
defer rows.Close()
var activity []models.Activity
tx := database.Instance().WithContext(ctx).
Where("event NOT LIKE ?", "server:sftp.%").
Limit(int(ac.max)).
Find(&activity)
var logs []server.Activity
var ids []int
for rows.Next() {
var qa QueriedActivity
if err := qa.Parse(rows); err != nil {
return err
}
ids = append(ids, qa.id)
logs = append(logs, qa.Activity)
if tx.Error != nil {
return errors.WithStack(tx.Error)
}
if err := rows.Err(); err != nil {
return errors.WithStack(err)
}
if len(logs) == 0 {
if len(activity) == 0 {
return nil
}
if err := ac.manager.Client().SendActivityLogs(ctx, logs); err != nil {
if err := ac.manager.Client().SendActivityLogs(ctx, activity); err != nil {
return errors.WrapIf(err, "cron: failed to send activity events to Panel")
}
if tx, err := sqlite.Instance().Begin(); err != nil {
return err
} else {
t := make([]string, len(ids))
params := make([]interface{}, len(ids))
for i := 0; i < len(ids); i++ {
t[i] = "?"
params[i] = ids[i]
}
q := strings.Join(t, ",")
_, err := tx.Exec(`DELETE FROM activity_logs WHERE id IN(`+q+`)`, params...)
if err != nil {
return errors.Combine(errors.WithStack(err), tx.Rollback())
}
return errors.WithStack(tx.Commit())
var ids []int
for _, v := range activity {
ids = append(ids, v.ID)
}
tx = database.Instance().WithContext(ctx).Where("id IN ?", ids).Delete(&models.Activity{})
if tx.Error != nil {
return errors.WithStack(tx.Error)
}
return nil
}

View File

@ -33,14 +33,8 @@ func Scheduler(ctx context.Context, m *server.Manager) (*gocron.Scheduler, error
max: config.Get().System.ActivitySendCount,
}
sftp := sftpActivityCron{
mu: system.NewAtomicBool(false),
manager: m,
max: config.Get().System.ActivitySendCount,
}
s := gocron.NewScheduler(l)
_, _ = s.Tag("activity").Every(config.Get().System.ActivitySendInterval).Seconds().Do(func() {
_, _ = s.Tag("activity").Every(5).Seconds().Do(func() {
if err := activity.Run(ctx); err != nil {
if errors.Is(err, ErrCronRunning) {
log.WithField("cron", "activity").Warn("cron: process is already running, skipping...")
@ -50,15 +44,5 @@ func Scheduler(ctx context.Context, m *server.Manager) (*gocron.Scheduler, error
}
})
_, _ = s.Tag("sftp_activity").Every(5).Seconds().Do(func() {
if err := sftp.Run(ctx); err != nil {
if errors.Is(err, ErrCronRunning) {
log.WithField("cron", "sftp").Warn("cron: process is already running, skipping...")
} else {
log.WithField("error", err).Error("cron: failed to process sftp events")
}
}
})
return s, nil
}

View File

@ -1,145 +0,0 @@
package cron
import (
"bytes"
"context"
"emperror.dev/errors"
"encoding/gob"
"github.com/pterodactyl/wings/internal/sqlite"
"github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/system"
"time"
)
const querySftpActivity = `
SELECT
event,
user_uuid,
server_uuid,
ip,
GROUP_CONCAT(metadata, '::') AS metadata,
MIN(timestamp) AS first_timestamp
FROM activity_logs
WHERE event LIKE 'server:sftp.%'
GROUP BY event, STRFTIME('%Y-%m-%d %H:%M:00', DATETIME(timestamp, 'unixepoch', 'utc')), user_uuid, server_uuid, ip
LIMIT ?
`
type sftpActivityGroup struct {
Event server.Event
User string
Server string
IP string
Metadata []byte
Timestamp int64
}
// Activity takes the struct and converts it into a single activity entity to
// process and send back over to the Panel.
func (g *sftpActivityGroup) Activity() (server.Activity, error) {
m, err := g.processMetadata()
if err != nil {
return server.Activity{}, err
}
t := time.Unix(g.Timestamp, 0)
a := server.Activity{
User: g.User,
Server: g.Server,
Event: g.Event,
Metadata: m,
IP: g.IP,
Timestamp: t.UTC(),
}
return a, nil
}
// processMetadata takes all of the concatenated metadata returned by the SQL
// query and then processes it all into individual entity records before then
// merging them into a final, single metadata, object.
func (g *sftpActivityGroup) processMetadata() (server.ActivityMeta, error) {
b := bytes.Split(g.Metadata, []byte("::"))
if len(b) == 0 {
return server.ActivityMeta{}, nil
}
entities := make([]server.ActivityMeta, len(b))
for i, v := range b {
if len(v) == 0 {
continue
}
if err := gob.NewDecoder(bytes.NewBuffer(v)).Decode(&entities[i]); err != nil {
return nil, errors.Wrap(err, "could not decode metadata bytes")
}
}
var files []interface{}
// Iterate over every entity that we've gotten back from the database's metadata fields
// and merge them all into a single entity by checking what the data type returned is and
// going from there.
//
// We only store a slice of strings, or a string/string map value in the database for SFTP
// actions, hence the case statement.
for _, e := range entities {
if e == nil {
continue
}
if f, ok := e["files"]; ok {
var a []interface{}
switch f.(type) {
case []string:
for _, v := range f.([]string) {
a = append(a, v)
}
case map[string]string:
a = append(a, f)
}
files = append(files, a)
}
}
return server.ActivityMeta{"files": files}, nil
}
type sftpActivityCron struct {
mu *system.AtomicBool
manager *server.Manager
max int64
}
// Run executes the cronjob and finds all associated SFTP events, bundles them up so
// that multiple events in the same timespan are recorded as a single event, and then
// cleans up the database.
func (sac *sftpActivityCron) Run(ctx context.Context) error {
if !sac.mu.SwapIf(true) {
return errors.WithStack(ErrCronRunning)
}
defer sac.mu.Store(false)
rows, err := sqlite.Instance().QueryContext(ctx, querySftpActivity, sac.max)
if err != nil {
return errors.Wrap(err, "cron: failed to query sftp activity")
}
defer rows.Close()
if err := rows.Err(); err != nil {
return errors.WithStack(err)
}
var out []server.Activity
for rows.Next() {
v := sftpActivityGroup{}
if err := rows.Scan(&v.Event, &v.User, &v.Server, &v.IP, &v.Metadata, &v.Timestamp); err != nil {
return errors.Wrap(err, "failed to scan row")
}
if a, err := v.Activity(); err != nil {
return errors.Wrap(err, "could not parse data into activity type")
} else {
out = append(out, a)
}
}
if len(out) == 0 {
return nil
}
if err := sac.manager.Client().SendActivityLogs(ctx, out); err != nil {
return errors.Wrap(err, "could not send activity logs to Panel")
}
return nil
}

View File

@ -0,0 +1,41 @@
package database
import (
"emperror.dev/errors"
"github.com/glebarez/sqlite"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/internal/models"
"github.com/pterodactyl/wings/system"
"gorm.io/gorm"
"path/filepath"
)
var o system.AtomicBool
var db *gorm.DB
// Initialize configures the local SQLite database for Wings and ensures that the models have
// been fully migrated.
func Initialize() error {
if !o.SwapIf(true) {
panic("database: attempt to initialize more than once during application lifecycle")
}
p := filepath.Join(config.Get().System.RootDirectory, "wings.db")
instance, err := gorm.Open(sqlite.Open(p), &gorm.Config{})
if err != nil {
return errors.Wrap(err, "database: could not open database file")
}
db = instance
if err := db.AutoMigrate(&models.Activity{}); err != nil {
return errors.WithStack(err)
}
return nil
}
// Instance returns the gorm database instance that was configured when the application was
// booted.
func Instance() *gorm.DB {
if db == nil {
panic("database: attempt to access instance before initialized")
}
return db
}

View File

@ -0,0 +1,64 @@
package models
import (
"github.com/pterodactyl/wings/system"
"gorm.io/gorm"
"time"
)
type Event string
type ActivityMeta map[string]interface{}
// Activity defines an activity log event for a server entity performed by a user. This is
// used for tracking commands, power actions, and SFTP events so that they can be reconciled
// and sent back to the Panel instance to be displayed to the user.
type Activity struct {
ID int `gorm:"primaryKey;not null" json:"-"`
// 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 JsonNullString `gorm:"type:uuid" json:"user"`
// Server is the UUID of the server this event is associated with.
Server string `gorm:"type:uuid;not null" 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 `gorm:"index;not null" json:"event"`
// Metadata is either a null value, string, or a JSON blob with additional event specific
// metadata that can be provided.
Metadata ActivityMeta `gorm:"serializer:json" json:"metadata"`
// IP is the IP address that triggered this event, or an empty string if it cannot be
// determined properly. This should be the connecting user's IP address, and not the
// internal system IP.
IP string `gorm:"not null" json:"ip"`
Timestamp time.Time `gorm:"not null" json:"timestamp"`
}
// SetUser sets the current user that performed the action. If an empty string is provided
// it is cast into a null value when stored.
func (a Activity) SetUser(u string) *Activity {
var ns JsonNullString
if u == "" {
if err := ns.Scan(nil); err != nil {
panic(err)
}
} else {
if err := ns.Scan(u); err != nil {
panic(err)
}
}
a.User = ns
return &a
}
// BeforeCreate executes before we create any activity entry to ensure the IP address
// is trimmed down to remove any extraneous data, and the timestamp is set to the current
// system time and then stored as UTC.
func (a *Activity) BeforeCreate(_ *gorm.DB) error {
a.IP = system.TrimIPSuffix(a.IP)
if a.Timestamp.IsZero() {
a.Timestamp = time.Now()
}
a.Timestamp = a.Timestamp.UTC()
return nil
}

31
internal/models/models.go Normal file
View File

@ -0,0 +1,31 @@
package models
import (
"database/sql"
"emperror.dev/errors"
"github.com/goccy/go-json"
)
type JsonNullString struct {
sql.NullString
}
func (v JsonNullString) MarshalJSON() ([]byte, error) {
if v.Valid {
return json.Marshal(v.String)
} else {
return json.Marshal(nil)
}
}
func (v *JsonNullString) UnmarshalJSON(data []byte) error {
var s *string
if err := json.Unmarshal(data, &s); err != nil {
return errors.WithStack(err)
}
if s != nil {
v.String = *s
}
v.Valid = s != nil
return nil
}

View File

@ -1,56 +0,0 @@
package sqlite
import (
"context"
"database/sql"
"emperror.dev/errors"
"github.com/apex/log"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/system"
_ "modernc.org/sqlite"
"path/filepath"
)
var o system.AtomicBool
var db *sql.DB
const schema = `
CREATE TABLE IF NOT EXISTS "activity_logs" (
"id" integer,
"event" varchar NOT NULL,
"user_uuid" varchar,
"server_uuid" varchar NOT NULL,
"metadata" blob,
"ip" varchar,
"timestamp" integer NOT NULL,
PRIMARY KEY (id)
);
-- Add an index otherwise we're gonna end up with performance issues over time especially
-- on huge Wings instances where we'll have a large number of activity logs to parse through.
CREATE INDEX IF NOT EXISTS idx_event ON activity_logs(event);
`
func Initialize(ctx context.Context) error {
if !o.SwapIf(true) {
panic("database: attempt to initialize more than once during application lifecycle")
}
p := filepath.Join(config.Get().System.RootDirectory, "wings.db")
log.WithField("subsystem", "sqlite").WithField("path", p).Info("initializing local database")
database, err := sql.Open("sqlite", p)
if err != nil {
return errors.Wrap(err, "database: could not open database file")
}
db = database
if _, err := db.ExecContext(ctx, schema); err != nil {
return errors.Wrap(err, "database: failed to initialize base schema")
}
return nil
}
func Instance() *sql.DB {
if db == nil {
panic("database: attempt to access instance before initialized")
}
return db
}

View File

@ -4,6 +4,7 @@ import (
"bytes"
"context"
"fmt"
"github.com/pterodactyl/wings/internal/models"
"io"
"net/http"
"strconv"
@ -30,7 +31,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 interface{}) error
SendActivityLogs(ctx context.Context, activity []models.Activity) error
}
type client struct {

View File

@ -3,6 +3,7 @@ package remote
import (
"context"
"fmt"
"github.com/pterodactyl/wings/internal/models"
"strconv"
"sync"
@ -179,7 +180,7 @@ 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 interface{}) error {
func (c *client) SendActivityLogs(ctx context.Context, activity []models.Activity) error {
resp, err := c.Post(ctx, "/activity", d{"data": activity})
if err != nil {
return errors.WithStackIf(err)

View File

@ -3,6 +3,7 @@ package websocket
import (
"context"
"fmt"
"github.com/pterodactyl/wings/internal/models"
"net/http"
"strings"
"sync"
@ -369,7 +370,7 @@ func (h *Handler) HandleInbound(ctx context.Context, m Message) error {
}
if err == nil {
_ = h.ra.Save(h.server, server.Event(server.ActivityPowerPrefix+action), nil)
_ = h.ra.Save(h.server, models.Event(server.ActivityPowerPrefix+action), nil)
}
return err
@ -428,7 +429,7 @@ func (h *Handler) HandleInbound(ctx context.Context, m Message) error {
}
}
_ = h.ra.Save(h.server, server.ActivityConsoleCommand, server.ActivityMeta{
_ = h.ra.Save(h.server, server.ActivityConsoleCommand, models.ActivityMeta{
"command": strings.Join(m.Args, ""),
})

View File

@ -1,50 +1,22 @@
package server
import (
"bytes"
"emperror.dev/errors"
"encoding/gob"
"github.com/apex/log"
"github.com/pterodactyl/wings/internal/sqlite"
"regexp"
"time"
"github.com/pterodactyl/wings/internal/database"
"github.com/pterodactyl/wings/internal/models"
)
type Event string
type ActivityMeta map[string]interface{}
const ActivityPowerPrefix = "server:power."
const (
ActivityConsoleCommand = Event("server:console.command")
ActivitySftpWrite = Event("server:sftp.write")
ActivitySftpCreate = Event("server:sftp.create")
ActivitySftpCreateDirectory = Event("server:sftp.create-directory")
ActivitySftpRename = Event("server:sftp.rename")
ActivitySftpDelete = Event("server:sftp.delete")
ActivityConsoleCommand = models.Event("server:console.command")
ActivitySftpWrite = models.Event("server:sftp.write")
ActivitySftpCreate = models.Event("server:sftp.create")
ActivitySftpCreateDirectory = models.Event("server:sftp.create-directory")
ActivitySftpRename = models.Event("server:sftp.rename")
ActivitySftpDelete = models.Event("server:sftp.delete")
)
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().
@ -56,23 +28,22 @@ type RequestActivity struct {
// 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,
}
func (ra RequestActivity) Event(event models.Event, metadata models.ActivityMeta) *models.Activity {
a := models.Activity{Server: ra.server, IP: ra.ip, Event: event, Metadata: metadata}
return a.SetUser(ra.user)
}
// 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 {
func (ra RequestActivity) Save(s *Server, event models.Event, metadata models.ActivityMeta) error {
if tx := database.Instance().Create(ra.Event(event, metadata)); tx.Error != nil {
err := errors.WithStackIf(tx.Error)
s.Log().WithField("error", err).WithField("event", event).Error("activity: failed to save event")
return errors.WithStack(err)
return err
}
return nil
}
@ -94,46 +65,6 @@ func (ra RequestActivity) SetUser(u string) RequestActivity {
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, "")
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
if err := enc.Encode(&a.Metadata); err != nil {
return errors.Wrap(err, "activity: error encoding metadata")
}
log.WithField("subsystem", "activity").
WithFields(log.Fields{"server": a.Server, "user": a.User, "event": a.Event, "ip": a.IP}).
Debug("saving activity to database")
stmt := `INSERT INTO activity_logs(event, user_uuid, server_uuid, metadata, ip, timestamp) VALUES(?, ?, ?, ?, ?, ?)`
if _, err := sqlite.Instance().Exec(stmt, a.Event, a.User, a.Server, buf.Bytes(), a.IP, a.Timestamp.UTC().Unix()); 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,
}
}

View File

@ -3,8 +3,8 @@ package sftp
import (
"emperror.dev/errors"
"github.com/apex/log"
"github.com/pterodactyl/wings/server"
"time"
"github.com/pterodactyl/wings/internal/database"
"github.com/pterodactyl/wings/internal/models"
)
type eventHandler struct {
@ -26,7 +26,7 @@ type FileAction struct {
// Log parses a SFTP specific file activity event and then passes it off to be stored
// in the normal activity database.
func (eh *eventHandler) Log(e server.Event, fa FileAction) error {
func (eh *eventHandler) Log(e models.Event, fa FileAction) error {
metadata := map[string]interface{}{
"files": []string{fa.Entity},
}
@ -36,21 +36,22 @@ func (eh *eventHandler) Log(e server.Event, fa FileAction) error {
}
}
r := server.Activity{
User: eh.user,
Server: eh.server,
Event: e,
Metadata: metadata,
IP: eh.ip,
Timestamp: time.Now().UTC(),
a := models.Activity{
Server: eh.server,
Event: e,
Metadata: metadata,
IP: eh.ip,
}
return errors.Wrap(r.Save(), "sftp: failed to store file event")
if tx := database.Instance().Create(a.SetUser(eh.user)); tx.Error != nil {
return errors.Wrap(tx.Error, "sftp: failed to save event to database")
}
return nil
}
// MustLog is a wrapper around log that will trigger a fatal error and exit the application
// if an error is encountered during the logging of the event.
func (eh *eventHandler) MustLog(e server.Event, fa FileAction) {
func (eh *eventHandler) MustLog(e models.Event, fa FileAction) {
if err := eh.Log(e, fa); err != nil {
log.WithField("error", err).Fatal("sftp: failed to log event")
}

View File

@ -2,9 +2,12 @@ package system
import (
"math/rand"
"regexp"
"strings"
)
var ipTrimRegex = regexp.MustCompile(`(:\d*)?$`)
const characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
// RandomString generates a random string of alpha-numeric characters using a
@ -18,3 +21,9 @@ func RandomString(n int) string {
}
return b.String()
}
// TrimIPSuffix removes the internal port value from an IP address to ensure we're only
// ever working directly with the IP address.
func TrimIPSuffix(s string) string {
return ipTrimRegex.ReplaceAllString(s, "")
}

View File

@ -1,16 +1,12 @@
package main
import (
"encoding/gob"
"github.com/pterodactyl/wings/cmd"
"github.com/pterodactyl/wings/server"
"math/rand"
"time"
)
func main() {
gob.Register(server.ActivityMeta{})
// Since we make use of the math/rand package in the code, especially for generating
// non-cryptographically secure random strings we need to seed the RNG. Just make use
// of the current time for this.