Compare commits

...

7 Commits

17 changed files with 463 additions and 14 deletions

View File

@ -5,6 +5,7 @@ import (
"crypto/tls"
"errors"
"fmt"
"github.com/pterodactyl/wings/internal/cron"
log2 "log"
"net/http"
_ "net/http/pprof"
@ -259,6 +260,13 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
}
}()
if s, err := cron.Scheduler(manager); err != nil {
log.WithField("error", err).Fatal("failed to initialize cron system")
} else {
log.WithField("subsystem", "cron").Info("starting cron processes")
s.StartAsync()
}
go func() {
// Run the SFTP server.
if err := sftp.New(manager).Run(); err != nil {

View File

@ -163,6 +163,15 @@ type SystemConfiguration struct {
// disk usage is not a concern.
DiskCheckInterval int64 `default:"150" yaml:"disk_check_interval"`
// ActivitySendInterval is the amount of time that should ellapse between aggregated server activity
// 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"`
// ActivitySendCount is the number of activity events to send per batch.
ActivitySendCount int64 `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
// cases disabling this should not have any major impact unless external processes are

12
go.mod
View File

@ -37,7 +37,7 @@ require (
github.com/pkg/sftp v1.13.4
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
github.com/spf13/cobra v1.4.0
github.com/stretchr/testify v1.7.0
github.com/stretchr/testify v1.7.5
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
gopkg.in/ini.v1 v1.66.4
@ -46,7 +46,7 @@ require (
require github.com/goccy/go-json v0.9.6
require golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect
require golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e // indirect
require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
@ -54,6 +54,7 @@ require (
github.com/Microsoft/hcsshim v0.9.2 // indirect
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bwmarrin/snowflake v0.3.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/containerd/containerd v1.6.2 // indirect
github.com/containerd/fifo v1.0.0 // indirect
@ -65,6 +66,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/go-co-op/gocron v1.15.0 // 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
@ -96,11 +98,15 @@ require (
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
github.com/ulikunitz/xz v0.5.10 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/xujiajun/mmap-go v1.0.1 // indirect
github.com/xujiajun/nutsdb v0.9.0 // indirect
github.com/xujiajun/utils v0.0.0-20190123093513-8bf096c4f53b // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
@ -111,5 +117,5 @@ require (
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.0-20210107192922-496545a6307b // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

24
go.sum
View File

@ -151,6 +151,8 @@ github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx2
github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo=
github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
@ -400,6 +402,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/go-co-op/gocron v1.15.0 h1:XmiPazahD9aq0/QdK5toCVHfgTXfrZ/s83RpAgzr6SM=
github.com/go-co-op/gocron v1.15.0/go.mod h1:On9zUZTv7FBeuj9D/cdYyAWcPUiLqqAx7nsPHd0EmKM=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@ -868,6 +872,8 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
@ -932,6 +938,7 @@ github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@ -940,6 +947,10 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q=
github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
@ -988,6 +999,13 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofm
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/xujiajun/gorouter v1.2.0/go.mod h1:yJrIta+bTNpBM/2UT8hLOaEAFckO+m/qmR3luMIQygM=
github.com/xujiajun/mmap-go v1.0.1 h1:7Se7ss1fLPPRW+ePgqGpCkfGIZzJV6JPq9Wq9iv/WHc=
github.com/xujiajun/mmap-go v1.0.1/go.mod h1:CNN6Sw4SL69Sui00p0zEzcZKbt+5HtEnYUsc6BKKRMg=
github.com/xujiajun/nutsdb v0.9.0 h1:vy8rjDp0Sk/SnTAqg61i+G4NIN/3tBKSdZ6rIyKYVIo=
github.com/xujiajun/nutsdb v0.9.0/go.mod h1:8ZdTTF0cEQO+wN940htfHYKswFql2iB6Osckx+GmOoU=
github.com/xujiajun/utils v0.0.0-20190123093513-8bf096c4f53b h1:jKG9OiL4T4xQN3IUrhUpc1tG+HfDXppkgVcrAiiaI/0=
github.com/xujiajun/utils v0.0.0-20190123093513-8bf096c4f53b/go.mod h1:AZd87GYJlUzl82Yab2kTjx1EyXSQCAfZDhpTo1SQC4k=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -1199,6 +1217,7 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -1296,6 +1315,9 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220405210540-1e041c57c461/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e h1:CsOuNlbOuf0mzxJIefr6Q4uAUetRUwZE4qt7VfzP+xo=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@ -1557,6 +1579,8 @@ gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=

View File

@ -0,0 +1,98 @@
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"
"github.com/xujiajun/nutsdb"
)
var key = []byte("events")
var processing system.AtomicBool
func processActivityLogs(m *server.Manager, c int64) error {
// Don't execute this cron if there is currently one running. Once this task is completed
// go ahead and mark it as no longer running.
if !processing.SwapIf(true) {
return nil
}
defer processing.Store(false)
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.
end := int(c)
if s, err := tx.LSize(database.ServerActivityBucket, key); err != nil {
return errors.WithStackIf(err)
} else if s < end || s == 0 {
if s == 0 {
return nil
}
end = s
}
l, err := tx.LRange(database.ServerActivityBucket, key, 0, end)
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
// that needs to be done if this error occurs.
if errors.Is(err, nutsdb.ErrBucket) {
return nil
}
return errors.WithStackIf(err)
}
list = l
return nil
})
if err != nil || len(list) == 0 {
return errors.WithStackIf(err)
}
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")
}
return database.DB().Update(func(tx *nutsdb.Tx) error {
if m, err := tx.LSize(database.ServerActivityBucket, key); err != nil {
return errors.WithStack(err)
} else if m > len(list) {
// As long as there are more elements than we have in the length of our list
// we can just use the existing `LTrim` functionality of nutsdb. This will remove
// all of the values we've already pulled and sent to the API.
return errors.WithStack(tx.LTrim(database.ServerActivityBucket, key, len(list), -1))
} else {
i := 0
// This is the only way I can figure out to actually empty the items out of the list
// because you cannot use `LTrim` (or I cannot for the life of me figure out how) to
// trim the slice down to 0 items without it triggering an internal logic error. Perhaps
// in a future release they'll have a function to do this (based on my skimming of issues
// on GitHub that I cannot read due to translation barriers).
for {
if i >= m {
break
}
if _, err := tx.LPop(database.ServerActivityBucket, key); err != nil {
return errors.WithStack(err)
}
i++
}
}
return nil
})
}

36
internal/cron/cron.go Normal file
View File

@ -0,0 +1,36 @@
package cron
import (
"emperror.dev/errors"
"github.com/apex/log"
"github.com/go-co-op/gocron"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/system"
"time"
)
var o system.AtomicBool
// Scheduler configures the internal cronjob system for Wings and returns the scheduler
// instance to the caller. This should only be called once per application lifecycle, additional
// calls will result in an error being returned.
func Scheduler(m *server.Manager) (*gocron.Scheduler, error) {
if o.Load() {
return nil, errors.New("cron: cannot call scheduler more than once in application lifecycle")
}
o.Store(true)
l, err := time.LoadLocation(config.Get().System.Timezone)
if err != nil {
return nil, errors.Wrap(err, "cron: failed to parse configured system timezone")
}
s := gocron.NewScheduler(l)
_, _ = s.Tag("activity").Every(config.Get().System.ActivitySendInterval).Seconds().Do(func() {
if err := processActivityLogs(m, config.Get().System.ActivitySendCount); err != nil {
log.WithField("error", err).Error("cron: failed to process activity events")
}
})
return s, nil
}

View File

@ -0,0 +1,38 @@
package database
import (
"emperror.dev/errors"
"github.com/apex/log"
"github.com/pterodactyl/wings/config"
"github.com/xujiajun/nutsdb"
"path/filepath"
"sync"
)
var db *nutsdb.DB
var syncer sync.Once
const (
ServerActivityBucket = "server_activity"
)
func initialize() error {
opt := nutsdb.DefaultOptions
opt.Dir = filepath.Join(config.Get().System.RootDirectory, "db")
instance, err := nutsdb.Open(opt)
if err != nil {
return errors.WithStack(err)
}
db = instance
return nil
}
func DB() *nutsdb.DB {
syncer.Do(func() {
if err := initialize(); err != nil {
log.WithField("error", err).Fatal("database: failed to initialize instance, this is an unrecoverable error")
}
})
return db
}

View File

@ -30,6 +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 []json.RawMessage) error
}
type client struct {
@ -128,10 +129,19 @@ func (c *client) requestOnce(ctx context.Context, method, path string, body io.R
// and adds the required authentication headers to the request that is being
// created. Errors returned will be of the RequestError type if there was some
// type of response from the API that can be parsed.
func (c *client) request(ctx context.Context, method, path string, body io.Reader, opts ...func(r *http.Request)) (*Response, error) {
func (c *client) request(ctx context.Context, method, path string, body *bytes.Buffer, opts ...func(r *http.Request)) (*Response, error) {
var res *Response
err := backoff.Retry(func() error {
r, err := c.requestOnce(ctx, method, path, body, opts...)
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"))
}
}
r, err := c.requestOnce(ctx, method, path, &b, opts...)
if err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return backoff.Permanent(err)

View File

@ -3,6 +3,7 @@ package remote
import (
"context"
"fmt"
"github.com/goccy/go-json"
"strconv"
"sync"
@ -178,6 +179,16 @@ func (c *client) SendRestorationStatus(ctx context.Context, backup string, succe
return nil
}
// SendActivityLogs sends activity logs back to the Panel for processing.
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)
}
_ = resp.Body.Close()
return nil
}
// getServersPaged returns a subset of servers from the Panel API using the
// pagination query parameters.
func (c *client) getServersPaged(ctx context.Context, page, limit int) ([]RawServerData, Pagination, error) {

View File

@ -2,11 +2,10 @@ package remote
import (
"bytes"
"regexp"
"strings"
"github.com/apex/log"
"github.com/goccy/go-json"
"regexp"
"strings"
"github.com/pterodactyl/wings/parser"
)

View File

@ -66,6 +66,7 @@ func Configure(m *wserver.Manager, client remote.Client) *gin.Engine {
server.DELETE("", deleteServer)
server.GET("/logs", getServerLogs)
server.GET("/activity", getServerActivityLogs)
server.POST("/power", postServerPower)
server.POST("/commands", postServerCommands)
server.POST("/install", postServerInstall)

View File

@ -2,6 +2,9 @@ package router
import (
"context"
"github.com/goccy/go-json"
"github.com/pterodactyl/wings/internal/database"
"github.com/xujiajun/nutsdb"
"net/http"
"os"
"strconv"
@ -40,6 +43,44 @@ func getServerLogs(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"data": out})
}
// Returns the activity logs tracked internally for the server instance. Note that these
// logs are routinely cleared out as Wings communicates directly with the Panel to pass
// along all of the logs for servers it monitors. As activities are passed to the panel
// they are deleted from Wings.
//
// As a result, this endpoint may or may not return data, and the data returned can change
// between requests.
func getServerActivityLogs(c *gin.Context) {
s := ExtractServer(c)
var out [][]byte
err := database.DB().View(func(tx *nutsdb.Tx) error {
items, err := tx.LRange(database.ServerActivityBucket, []byte(s.ID()), 0, 10)
if err != nil {
return err
}
out = items
return nil
})
if err != nil {
middleware.CaptureAndAbort(c, err)
return
}
var activity []*server.Activity
for _, b := range out {
var a server.Activity
if err := json.Unmarshal(b, &a); err != nil {
middleware.CaptureAndAbort(c, err)
return
}
activity = append(activity, &a)
}
c.JSON(http.StatusOK, gin.H{"data": activity})
}
// Handles a request to control the power state of a server. If the action being passed
// through is invalid a 404 is returned. Otherwise, a HTTP/202 Accepted response is returned
// and the actual power action is run asynchronously so that we don't have to block the

View File

@ -7,7 +7,6 @@ import (
"github.com/apex/log"
"github.com/gbrlsnchs/jwt/v3"
"github.com/goccy/go-json"
)
// The time at which Wings was booted. No JWT's created before this time are allowed to
@ -35,13 +34,13 @@ func DenyJTI(jti string) {
denylist.Store(jti, time.Now())
}
// A JWT payload for Websocket connections. This JWT is passed along to the Websocket after
// it has been connected to by sending an "auth" event.
// WebsocketPayload defines the JWT payload for a websocket connection. This JWT is passed along to
// the websocket after it has been connected to by sending an "auth" event.
type WebsocketPayload struct {
jwt.Payload
sync.RWMutex
UserID json.Number `json:"user_id"`
UserUUID string `json:"user_uuid"`
ServerUUID string `json:"server_uuid"`
Permissions []string `json:"permissions"`
}

View File

@ -40,6 +40,7 @@ type Handler struct {
Connection *websocket.Conn `json:"-"`
jwt *tokens.WebsocketPayload
server *server.Server
ra server.RequestActivity
uuid uuid.UUID
}
@ -109,6 +110,7 @@ func GetHandler(s *server.Server, w http.ResponseWriter, r *http.Request) (*Hand
Connection: conn,
jwt: nil,
server: s,
ra: s.NewRequestActivity("", r.RemoteAddr),
uuid: u,
}, nil
}
@ -264,6 +266,7 @@ func (h *Handler) GetJwt() *tokens.WebsocketPayload {
// setJwt sets the JWT for the websocket in a race-safe manner.
func (h *Handler) setJwt(token *tokens.WebsocketPayload) {
h.Lock()
h.ra = h.ra.SetUser(token.UserUUID)
h.jwt = token
h.Unlock()
}
@ -365,6 +368,10 @@ func (h *Handler) HandleInbound(ctx context.Context, m Message) error {
return nil
}
if err == nil {
_ = h.ra.Save(h.server, server.Event(server.ActivityPowerPrefix+action), nil)
}
return err
}
case SendServerLogsEvent:
@ -421,6 +428,10 @@ func (h *Handler) HandleInbound(ctx context.Context, m Message) error {
}
}
_ = h.ra.Save(h.server, server.ActivityConsoleCommand, server.ActivityMeta{
"command": strings.Join(m.Args, ""),
})
return h.server.Environment.SendCommand(strings.Join(m.Args, ""))
}
}

130
server/activity.go Normal file
View File

@ -0,0 +1,130 @@
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 = "power_"
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
// 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,
}
}

20
system/strings.go Normal file
View File

@ -0,0 +1,20 @@
package system
import (
"math/rand"
"strings"
)
const characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
// RandomString generates a random string of alpha-numeric characters using a
// pseudo-random number generator. The output of this function IS NOT cryptographically
// secure, it is used solely for generating random strings outside a security context.
func RandomString(n int) string {
var b strings.Builder
b.Grow(n)
for i := 0; i < n; i++ {
b.WriteByte(characters[rand.Intn(len(characters))])
}
return b.String()
}

View File

@ -2,8 +2,16 @@ package main
import (
"github.com/pterodactyl/wings/cmd"
"math/rand"
"time"
)
func main() {
// 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.
rand.Seed(time.Now().UnixNano())
// Execute the main binary code.
cmd.Execute()
}