-
-
Notifications
You must be signed in to change notification settings - Fork 320
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
59fbd2b
commit f28e062
Showing
9 changed files
with
331 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
package cron | ||
|
||
import ( | ||
"bytes" | ||
"emperror.dev/errors" | ||
"encoding/gob" | ||
"github.com/pterodactyl/wings/internal/database" | ||
"github.com/pterodactyl/wings/server" | ||
"github.com/pterodactyl/wings/sftp" | ||
"github.com/pterodactyl/wings/system" | ||
"github.com/xujiajun/nutsdb" | ||
"path/filepath" | ||
) | ||
|
||
type UserDetail struct { | ||
UUID string | ||
IP string | ||
} | ||
|
||
type Users map[UserDetail][]sftp.EventRecord | ||
type Events map[sftp.Event]Users | ||
|
||
type sftpEventProcessor struct { | ||
mu *system.AtomicBool | ||
manager *server.Manager | ||
} | ||
|
||
// Run executes the cronjob and processes sftp activities into normal activity log entries | ||
// by merging together similar records. This helps to reduce the sheer amount of data that | ||
// gets passed back to the Panel and provides simpler activity logging. | ||
func (sep *sftpEventProcessor) Run() error { | ||
if !sep.mu.SwapIf(true) { | ||
return errors.WithStack(ErrCronRunning) | ||
} | ||
defer sep.mu.Store(false) | ||
|
||
set, err := sep.Events() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
for s, el := range set { | ||
events := make(Events) | ||
// Take all of the events that we've pulled out of the system for every server and then | ||
// parse them into a more usable format in order to create activity log entries for each | ||
// user, ip, and server instance. | ||
for _, e := range el { | ||
u := UserDetail{UUID: e.User, IP: e.IP} | ||
if _, ok := events[e.Event]; !ok { | ||
events[e.Event] = make(Users) | ||
} | ||
if _, ok := events[e.Event][u]; !ok { | ||
events[e.Event][u] = []sftp.EventRecord{} | ||
} | ||
events[e.Event][u] = append(events[e.Event][u], e) | ||
} | ||
|
||
// Now that we have all of the events, go ahead and create a normal activity log entry | ||
// for each instance grouped by user & IP for easier Panel reporting. | ||
for k, v := range events { | ||
for u, records := range v { | ||
files := make([]interface{}, len(records)) | ||
for i, r := range records { | ||
if r.Action.Target != "" { | ||
files[i] = map[string]string{ | ||
"from": filepath.Clean(r.Action.Entity), | ||
"to": filepath.Clean(r.Action.Target), | ||
} | ||
} else { | ||
files[i] = filepath.Clean(r.Action.Entity) | ||
} | ||
} | ||
|
||
entry := server.Activity{ | ||
Server: s, | ||
User: u.UUID, | ||
Event: server.Event("server:sftp." + string(k)), | ||
Metadata: server.ActivityMeta{"files": files}, | ||
IP: u.IP, | ||
// Just assume that the first record in the set is the oldest and the most relevant | ||
// of the timestamps to use. | ||
Timestamp: records[0].Timestamp, | ||
} | ||
|
||
if err := entry.Save(); err != nil { | ||
return errors.Wrap(err, "cron: failed to save new event for server") | ||
} | ||
|
||
if err := sep.Cleanup([]byte(s)); err != nil { | ||
return errors.Wrap(err, "cron: failed to cleanup events") | ||
} | ||
} | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Cleanup runs through all of the events we have currently tracked in the bucket and removes | ||
// them once we've managed to process them and created the associated server activity events. | ||
func (sep *sftpEventProcessor) Cleanup(key []byte) error { | ||
return database.DB().Update(func(tx *nutsdb.Tx) error { | ||
s, err := sep.sizeOf(tx, key) | ||
if err != nil { | ||
return err | ||
} | ||
if s == 0 { | ||
return nil | ||
} else if s < sep.limit() { | ||
for i := 0; i < s; i++ { | ||
if _, err := tx.LPop(database.SftpActivityBucket, key); err != nil { | ||
return errors.WithStack(err) | ||
} | ||
} | ||
} else { | ||
if err := tx.LTrim(database.ServerActivityBucket, key, sep.limit()-1, -1); err != nil { | ||
return errors.WithStack(err) | ||
} | ||
} | ||
return nil | ||
}) | ||
} | ||
|
||
// Events pulls all of the events in the SFTP event bucket and parses them into an iterable | ||
// set allowing Wings to process the events and send them back to the Panel instance. | ||
func (sep *sftpEventProcessor) Events() (map[string][]sftp.EventRecord, error) { | ||
set := make(map[string][]sftp.EventRecord, len(sep.manager.Keys())) | ||
err := database.DB().View(func(tx *nutsdb.Tx) error { | ||
for _, k := range sep.manager.Keys() { | ||
lim := sep.limit() | ||
if s, err := sep.sizeOf(tx, []byte(k)); err != nil { | ||
return err | ||
} else if s == 0 { | ||
continue | ||
} else if s < lim { | ||
lim = -1 | ||
} | ||
list, err := tx.LRange(database.SftpActivityBucket, []byte(k), 0, lim) | ||
if err != nil { | ||
return errors.WithStack(err) | ||
} | ||
set[k] = make([]sftp.EventRecord, len(list)) | ||
for i, l := range list { | ||
if err := gob.NewDecoder(bytes.NewBuffer(l)).Decode(&set[k][i]); err != nil { | ||
return errors.WithStack(err) | ||
} | ||
} | ||
} | ||
return nil | ||
}) | ||
|
||
return set, err | ||
} | ||
|
||
// sizeOf is a wrapper around a nutsdb transaction to get the size of a key in the | ||
// bucket while also accounting for some expected error conditions and handling those | ||
// automatically. | ||
func (sep *sftpEventProcessor) sizeOf(tx *nutsdb.Tx, key []byte) (int, error) { | ||
s, err := tx.LSize(database.SftpActivityBucket, key) | ||
if err != nil { | ||
if errors.Is(err, nutsdb.ErrBucket) { | ||
return 0, nil | ||
} | ||
return 0, errors.WithStack(err) | ||
} | ||
return s, nil | ||
} | ||
|
||
// limit returns the number of records that are processed for each server at | ||
// once. This will then be translated into a variable number of activity log | ||
// events, with the worst case being a single event with "n" associated files. | ||
func (sep *sftpEventProcessor) limit() int { | ||
return 500 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
package sftp | ||
|
||
import ( | ||
"bytes" | ||
"emperror.dev/errors" | ||
"encoding/gob" | ||
"github.com/apex/log" | ||
"github.com/pterodactyl/wings/internal/database" | ||
"github.com/xujiajun/nutsdb" | ||
"regexp" | ||
"time" | ||
) | ||
|
||
type eventHandler struct { | ||
ip string | ||
user string | ||
server string | ||
} | ||
|
||
type Event string | ||
type FileAction struct { | ||
// Entity is the targeted file or directory (depending on the event) that the action | ||
// is being performed _against_, such as "/foo/test.txt". This will always be the full | ||
// path to the element. | ||
Entity string | ||
// Target is an optional (often blank) field that only has a value in it when the event | ||
// is specifically modifying the entity, such as a rename or move event. In that case | ||
// the Target field will be the final value, such as "/bar/new.txt" | ||
Target string | ||
} | ||
|
||
type EventRecord struct { | ||
Event Event | ||
Action FileAction | ||
IP string | ||
User string | ||
Timestamp time.Time | ||
} | ||
|
||
const ( | ||
EventWrite = Event("write") | ||
EventCreate = Event("create") | ||
EventCreateDirectory = Event("create-directory") | ||
EventRename = Event("rename") | ||
EventDelete = Event("delete") | ||
) | ||
|
||
var ipTrimRegex = regexp.MustCompile(`(:\d*)?$`) | ||
|
||
// Log logs an event into the Wings bucket for SFTP activity which then allows a seperate | ||
// cron to run and parse the events into a more manageable stream of event data to send | ||
// back to the Panel instance. | ||
func (eh *eventHandler) Log(e Event, fa FileAction) error { | ||
r := EventRecord{ | ||
Event: e, | ||
Action: fa, | ||
IP: ipTrimRegex.ReplaceAllString(eh.ip, ""), | ||
User: eh.user, | ||
Timestamp: time.Now().UTC(), | ||
} | ||
|
||
var buf bytes.Buffer | ||
enc := gob.NewEncoder(&buf) | ||
if err := enc.Encode(r); err != nil { | ||
return errors.Wrap(err, "sftp: failed to encode event") | ||
} | ||
|
||
return database.DB().Update(func(tx *nutsdb.Tx) error { | ||
if err := tx.RPush(database.SftpActivityBucket, []byte(eh.server), buf.Bytes()); err != nil { | ||
return errors.Wrap(err, "sftp: failed to push event to stack") | ||
} | ||
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 Event, fa FileAction) { | ||
if err := eh.Log(e, fa); err != nil { | ||
log.WithField("error", err).Fatal("sftp: failed to log event") | ||
} | ||
} |
Oops, something went wrong.