Compare commits
8 Commits
dane/sqlit
...
v1.7.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83861a6dec | ||
|
|
231e24aa33 | ||
|
|
e3ab241d7f | ||
|
|
c18e844689 | ||
|
|
8cee18a92b | ||
|
|
f952efd9c7 | ||
|
|
21cf66b2b4 | ||
|
|
251f91a08e |
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## v1.7.0
|
||||
### Fixed
|
||||
* Fixes multi-platform support for Wings' Docker image.
|
||||
|
||||
### Added
|
||||
* Adds support for tracking of SFTP actions, power actions, server commands, and file uploads by utilizing a local SQLite database and processing events before sending them to the Panel.
|
||||
* Adds support for configuring the MTU on the `pterodactyl0` network.
|
||||
|
||||
## v1.6.4
|
||||
### Fixed
|
||||
* Fixes a bug causing CPU limiting to not be properly applied to servers.
|
||||
|
||||
@@ -167,10 +167,10 @@ type SystemConfiguration struct {
|
||||
// being sent to the Panel. By default this will send activity collected over the last minute. Keep
|
||||
// in mind that only a fixed number of activity log entries, defined by ActivitySendCount, will be sent
|
||||
// in each run.
|
||||
ActivitySendInterval int64 `default:"60" yaml:"activity_send_interval"`
|
||||
ActivitySendInterval int `default:"60" yaml:"activity_send_interval"`
|
||||
|
||||
// ActivitySendCount is the number of activity events to send per batch.
|
||||
ActivitySendCount int64 `default:"100" yaml:"activity_send_count"`
|
||||
ActivitySendCount int `default:"100" yaml:"activity_send_count"`
|
||||
|
||||
// If set to true, file permissions for a server will be checked when the process is
|
||||
// booted. This can cause boot delays if the server has a large amount of files. In most
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
type activityCron struct {
|
||||
mu *system.AtomicBool
|
||||
manager *server.Manager
|
||||
max int64
|
||||
max int
|
||||
}
|
||||
|
||||
// Run executes the cronjob and ensures we fetch and send all of the stored activity to the
|
||||
@@ -30,7 +30,7 @@ func (ac *activityCron) Run(ctx context.Context) error {
|
||||
var activity []models.Activity
|
||||
tx := database.Instance().WithContext(ctx).
|
||||
Where("event NOT LIKE ?", "server:sftp.%").
|
||||
Limit(int(ac.max)).
|
||||
Limit(ac.max).
|
||||
Find(&activity)
|
||||
|
||||
if tx.Error != nil {
|
||||
|
||||
@@ -3,7 +3,7 @@ package cron
|
||||
import (
|
||||
"context"
|
||||
"emperror.dev/errors"
|
||||
"github.com/apex/log"
|
||||
log2 "github.com/apex/log"
|
||||
"github.com/go-co-op/gocron"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/server"
|
||||
@@ -40,7 +40,13 @@ func Scheduler(ctx context.Context, m *server.Manager) (*gocron.Scheduler, error
|
||||
}
|
||||
|
||||
s := gocron.NewScheduler(l)
|
||||
_, _ = s.Tag("activity").Every(config.Get().System.ActivitySendInterval).Seconds().Do(func() {
|
||||
log := log2.WithField("subsystem", "cron")
|
||||
|
||||
interval := time.Duration(config.Get().System.ActivitySendInterval) * time.Second
|
||||
log.WithField("interval", interval).Info("configuring system crons")
|
||||
|
||||
_, _ = s.Tag("activity").Every(interval).Do(func() {
|
||||
log.WithField("cron", "activity").Debug("sending internal activity events to Panel")
|
||||
if err := activity.Run(ctx); err != nil {
|
||||
if errors.Is(err, ErrCronRunning) {
|
||||
log.WithField("cron", "activity").Warn("activity process is already running, skipping...")
|
||||
@@ -50,7 +56,8 @@ func Scheduler(ctx context.Context, m *server.Manager) (*gocron.Scheduler, error
|
||||
}
|
||||
})
|
||||
|
||||
_, _ = s.Tag("sftp").Every(config.Get().System.ActivitySendInterval).Seconds().Do(func() {
|
||||
_, _ = s.Tag("sftp").Every(interval).Do(func() {
|
||||
log.WithField("cron", "sftp").Debug("sending sftp events to Panel")
|
||||
if err := sftp.Run(ctx); err != nil {
|
||||
if errors.Is(err, ErrCronRunning) {
|
||||
log.WithField("cron", "sftp").Warn("sftp events process already running, skipping...")
|
||||
|
||||
@@ -7,14 +7,13 @@ import (
|
||||
"github.com/pterodactyl/wings/internal/models"
|
||||
"github.com/pterodactyl/wings/server"
|
||||
"github.com/pterodactyl/wings/system"
|
||||
"gorm.io/gorm"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type sftpCron struct {
|
||||
mu *system.AtomicBool
|
||||
manager *server.Manager
|
||||
max int64
|
||||
max int
|
||||
}
|
||||
|
||||
type mapKey struct {
|
||||
@@ -52,7 +51,7 @@ func (sc *sftpCron) Run(ctx context.Context) error {
|
||||
events := &eventMap{
|
||||
m: map[mapKey]*models.Activity{},
|
||||
ids: []int{},
|
||||
max: int(sc.max),
|
||||
max: sc.max,
|
||||
}
|
||||
|
||||
for {
|
||||
@@ -79,17 +78,13 @@ func (sc *sftpCron) Run(ctx context.Context) error {
|
||||
if len(events.m) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = database.Instance().Transaction(func(tx *gorm.DB) error {
|
||||
tx.Where("id IN ?", events.ids).Delete(&models.Activity{})
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
if err := sc.manager.Client().SendActivityLogs(ctx, events.Elements()); err != nil {
|
||||
return errors.Wrap(err, "failed to send sftp activity logs to Panel")
|
||||
}
|
||||
|
||||
return sc.manager.Client().SendActivityLogs(ctx, events.Elements())
|
||||
})
|
||||
|
||||
return errors.WithStack(err)
|
||||
if tx := database.Instance().Where("id IN ?", events.ids).Delete(&models.Activity{}); tx.Error != nil {
|
||||
return errors.WithStack(tx.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchRecords returns a group of activity events starting at the given offset. This is used
|
||||
@@ -102,7 +97,7 @@ func (sc *sftpCron) fetchRecords(ctx context.Context, offset int) (activity []mo
|
||||
Where("event LIKE ?", "server:sftp.%").
|
||||
Order("event DESC").
|
||||
Offset(offset).
|
||||
Limit(int(sc.max)).
|
||||
Limit(sc.max).
|
||||
Find(&activity)
|
||||
if tx.Error != nil {
|
||||
err = errors.WithStack(tx.Error)
|
||||
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
"github.com/pterodactyl/wings/internal/models"
|
||||
"github.com/pterodactyl/wings/system"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
var o system.AtomicBool
|
||||
@@ -20,11 +22,25 @@ func Initialize() error {
|
||||
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{})
|
||||
instance, err := gorm.Open(sqlite.Open(p), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "database: could not open database file")
|
||||
}
|
||||
db = instance
|
||||
if sql, err := db.DB(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
} else {
|
||||
sql.SetMaxOpenConns(1)
|
||||
sql.SetConnMaxLifetime(time.Hour)
|
||||
}
|
||||
if tx := db.Exec("PRAGMA synchronous = OFF"); tx.Error != nil {
|
||||
return errors.WithStack(tx.Error)
|
||||
}
|
||||
if tx := db.Exec("PRAGMA journal_mode = MEMORY"); tx.Error != nil {
|
||||
return errors.WithStack(tx.Error)
|
||||
}
|
||||
if err := db.AutoMigrate(&models.Activity{}); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
@@ -60,5 +60,8 @@ func (a *Activity) BeforeCreate(_ *gorm.DB) error {
|
||||
a.Timestamp = time.Now()
|
||||
}
|
||||
a.Timestamp = a.Timestamp.UTC()
|
||||
if a.Metadata == nil {
|
||||
a.Metadata = ActivityMeta{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package router
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"github.com/pterodactyl/wings/internal/models"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
@@ -600,6 +601,11 @@ func postServerUploadFiles(c *gin.Context) {
|
||||
if err := handleFileUpload(p, s, header); err != nil {
|
||||
NewServerError(err, s).Abort(c)
|
||||
return
|
||||
} else {
|
||||
s.SaveActivity(s.NewRequestActivity(token.UserUuid, c.Request.RemoteAddr), server.ActivityFileUploaded, models.ActivityMeta{
|
||||
"file": header.Filename,
|
||||
"directory": filepath.Clean(directory),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -617,6 +623,5 @@ func handleFileUpload(p string, s *server.Server, header *multipart.FileHeader)
|
||||
if err := s.Filesystem().Writefile(p, file); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ type UploadPayload struct {
|
||||
jwt.Payload
|
||||
|
||||
ServerUuid string `json:"server_uuid"`
|
||||
UserUuid string `json:"user_uuid"`
|
||||
UniqueId string `json:"unique_id"`
|
||||
}
|
||||
|
||||
|
||||
@@ -370,7 +370,7 @@ func (h *Handler) HandleInbound(ctx context.Context, m Message) error {
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
_ = h.ra.Save(h.server, models.Event(server.ActivityPowerPrefix+action), nil)
|
||||
h.server.SaveActivity(h.ra, models.Event(server.ActivityPowerPrefix+action), nil)
|
||||
}
|
||||
|
||||
return err
|
||||
@@ -429,11 +429,13 @@ func (h *Handler) HandleInbound(ctx context.Context, m Message) error {
|
||||
}
|
||||
}
|
||||
|
||||
_ = h.ra.Save(h.server, server.ActivityConsoleCommand, models.ActivityMeta{
|
||||
if err := h.server.Environment.SendCommand(strings.Join(m.Args, "")); err != nil {
|
||||
return err
|
||||
}
|
||||
h.server.SaveActivity(h.ra, server.ActivityConsoleCommand, models.ActivityMeta{
|
||||
"command": strings.Join(m.Args, ""),
|
||||
})
|
||||
|
||||
return h.server.Environment.SendCommand(strings.Join(m.Args, ""))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"emperror.dev/errors"
|
||||
"github.com/pterodactyl/wings/internal/database"
|
||||
"github.com/pterodactyl/wings/internal/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
const ActivityPowerPrefix = "server:power."
|
||||
@@ -15,6 +17,7 @@ const (
|
||||
ActivitySftpCreateDirectory = models.Event("server:sftp.create-directory")
|
||||
ActivitySftpRename = models.Event("server:sftp.rename")
|
||||
ActivitySftpDelete = models.Event("server:sftp.delete")
|
||||
ActivityFileUploaded = models.Event("server:file.uploaded")
|
||||
)
|
||||
|
||||
// RequestActivity is a wrapper around a LoggedEvent that is able to track additional request
|
||||
@@ -34,29 +37,6 @@ func (ra RequestActivity) Event(event models.Event, metadata models.ActivityMeta
|
||||
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 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 err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IP returns the IP address associated with this entry.
|
||||
func (ra RequestActivity) IP() string {
|
||||
return ra.ip
|
||||
}
|
||||
|
||||
func (ra *RequestActivity) User() string {
|
||||
return ra.user
|
||||
}
|
||||
|
||||
// SetUser clones the RequestActivity struct and sets a new user value on the copy
|
||||
// before returning it.
|
||||
func (ra RequestActivity) SetUser(u string) RequestActivity {
|
||||
@@ -68,3 +48,17 @@ func (ra RequestActivity) SetUser(u string) RequestActivity {
|
||||
func (s *Server) NewRequestActivity(user string, ip string) RequestActivity {
|
||||
return RequestActivity{server: s.ID(), user: user, ip: ip}
|
||||
}
|
||||
|
||||
// SaveActivity saves an activity entry to the database in a background routine. If an error is
|
||||
// encountered it is logged but not returned to the caller.
|
||||
func (s *Server) SaveActivity(a RequestActivity, event models.Event, metadata models.ActivityMeta) {
|
||||
ctx, cancel := context.WithTimeout(s.Context(), time.Second*3)
|
||||
go func() {
|
||||
defer cancel()
|
||||
if tx := database.Instance().WithContext(ctx).Create(a.Event(event, metadata)); tx.Error != nil {
|
||||
s.Log().WithField("error", errors.WithStack(tx.Error)).
|
||||
WithField("event", event).
|
||||
Error("activity: failed to save event")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -16,6 +16,11 @@ type EggConfiguration struct {
|
||||
FileDenylist []string `json:"file_denylist"`
|
||||
}
|
||||
|
||||
type ConfigurationMeta struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type Configuration struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
@@ -24,6 +29,8 @@ type Configuration struct {
|
||||
// docker containers as well as in log output.
|
||||
Uuid string `json:"uuid"`
|
||||
|
||||
Meta ConfigurationMeta `json:"meta"`
|
||||
|
||||
// Whether or not the server is in a suspended state. Suspended servers cannot
|
||||
// be started or modified except in certain scenarios by an admin user.
|
||||
Suspended bool `json:"suspended"`
|
||||
|
||||
@@ -44,7 +44,7 @@ func (eh *eventHandler) Log(e models.Event, fa FileAction) error {
|
||||
}
|
||||
|
||||
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 errors.WithStack(tx.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -53,6 +53,6 @@ func (eh *eventHandler) Log(e models.Event, fa FileAction) error {
|
||||
// if an error is encountered during the logging of the event.
|
||||
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")
|
||||
log.WithField("error", errors.WithStack(err)).WithField("event", e).Error("sftp: failed to log event")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user