2019-04-20 23:20:08 +00:00
|
|
|
package server
|
|
|
|
|
2020-01-18 22:04:26 +00:00
|
|
|
import (
|
2020-09-18 03:13:04 +00:00
|
|
|
"context"
|
2020-01-18 22:04:26 +00:00
|
|
|
"fmt"
|
2020-08-11 04:38:42 +00:00
|
|
|
"sync"
|
|
|
|
"sync/atomic"
|
|
|
|
"time"
|
2021-01-10 01:22:39 +00:00
|
|
|
|
|
|
|
"emperror.dev/errors"
|
|
|
|
"github.com/mitchellh/colorstring"
|
2021-08-02 21:07:00 +00:00
|
|
|
|
2021-01-10 01:22:39 +00:00
|
|
|
"github.com/pterodactyl/wings/config"
|
|
|
|
"github.com/pterodactyl/wings/system"
|
2020-01-18 22:04:26 +00:00
|
|
|
)
|
2019-04-20 23:20:08 +00:00
|
|
|
|
2021-04-03 18:11:36 +00:00
|
|
|
// appName is a local cache variable to avoid having to make expensive copies of
|
|
|
|
// the configuration every time we need to send output along to the websocket for
|
|
|
|
// a server.
|
|
|
|
var appName string
|
|
|
|
|
2020-11-28 23:57:10 +00:00
|
|
|
var ErrTooMuchConsoleData = errors.New("console is outputting too much data")
|
2020-09-18 03:13:04 +00:00
|
|
|
|
2020-08-11 04:38:42 +00:00
|
|
|
type ConsoleThrottler struct {
|
2020-09-18 03:13:04 +00:00
|
|
|
mu sync.Mutex
|
2020-08-11 04:38:42 +00:00
|
|
|
config.ConsoleThrottles
|
|
|
|
|
|
|
|
// The total number of activations that have occurred thus far.
|
|
|
|
activations uint64
|
|
|
|
|
2020-09-18 03:13:04 +00:00
|
|
|
// The total number of lines that have been sent since the last reset timer period.
|
|
|
|
count uint64
|
2019-04-20 23:20:08 +00:00
|
|
|
|
2020-09-18 03:13:04 +00:00
|
|
|
// Wether or not the console output is being throttled. It is up to calling code to
|
|
|
|
// determine what to do if it is.
|
2020-12-26 01:04:18 +00:00
|
|
|
isThrottled *system.AtomicBool
|
2020-08-11 04:38:42 +00:00
|
|
|
|
2020-09-18 03:13:04 +00:00
|
|
|
// The total number of lines processed so far during the given time period.
|
|
|
|
timerCancel *context.CancelFunc
|
|
|
|
}
|
2020-08-11 04:38:42 +00:00
|
|
|
|
2020-09-18 03:13:04 +00:00
|
|
|
// Resets the state of the throttler.
|
|
|
|
func (ct *ConsoleThrottler) Reset() {
|
|
|
|
atomic.StoreUint64(&ct.count, 0)
|
|
|
|
atomic.StoreUint64(&ct.activations, 0)
|
2020-12-26 01:04:18 +00:00
|
|
|
ct.isThrottled.Store(false)
|
2020-08-11 04:38:42 +00:00
|
|
|
}
|
2019-04-20 23:20:08 +00:00
|
|
|
|
2020-09-18 03:13:04 +00:00
|
|
|
// Triggers an activation for a server. You can also decrement the number of activations
|
|
|
|
// by passing a negative number.
|
|
|
|
func (ct *ConsoleThrottler) markActivation(increment bool) uint64 {
|
|
|
|
if !increment {
|
|
|
|
if atomic.LoadUint64(&ct.activations) == 0 {
|
|
|
|
return 0
|
|
|
|
}
|
2019-04-20 23:20:08 +00:00
|
|
|
|
2020-09-18 03:13:04 +00:00
|
|
|
// This weird dohicky subtracts 1 from the activation count.
|
|
|
|
return atomic.AddUint64(&ct.activations, ^uint64(0))
|
2019-04-20 23:20:08 +00:00
|
|
|
}
|
|
|
|
|
2020-09-18 03:13:04 +00:00
|
|
|
return atomic.AddUint64(&ct.activations, 1)
|
|
|
|
}
|
2020-08-11 04:38:42 +00:00
|
|
|
|
2020-09-18 03:13:04 +00:00
|
|
|
// Determines if the console is currently being throttled. Calls to this function can be used to
|
|
|
|
// determine if output should be funneled along to the websocket processes.
|
|
|
|
func (ct *ConsoleThrottler) Throttled() bool {
|
2020-12-26 01:04:18 +00:00
|
|
|
return ct.isThrottled.Load()
|
2020-08-11 04:38:42 +00:00
|
|
|
}
|
|
|
|
|
2020-09-18 03:13:04 +00:00
|
|
|
// Starts a timer that runs in a seperate thread and will continually decrement the lines processed
|
2020-12-25 19:21:09 +00:00
|
|
|
// and number of activations, regardless of the current console message volume. All of the timers
|
|
|
|
// are canceled if the context passed through is canceled.
|
|
|
|
func (ct *ConsoleThrottler) StartTimer(ctx context.Context) {
|
2020-12-26 01:04:18 +00:00
|
|
|
system.Every(ctx, time.Duration(int64(ct.LineResetInterval))*time.Millisecond, func(_ time.Time) {
|
|
|
|
ct.isThrottled.Store(false)
|
2020-12-25 19:21:09 +00:00
|
|
|
atomic.StoreUint64(&ct.count, 0)
|
|
|
|
})
|
|
|
|
|
2020-12-26 01:04:18 +00:00
|
|
|
system.Every(ctx, time.Duration(int64(ct.DecayInterval))*time.Millisecond, func(_ time.Time) {
|
2020-12-25 19:21:09 +00:00
|
|
|
ct.markActivation(false)
|
|
|
|
})
|
2020-01-18 22:04:26 +00:00
|
|
|
}
|
|
|
|
|
2020-08-11 04:38:42 +00:00
|
|
|
// Handles output from a server's console. This code ensures that a server is not outputting
|
|
|
|
// an excessive amount of data to the console that could indicate a malicious or run-away process
|
|
|
|
// and lead to performance issues for other users.
|
|
|
|
//
|
|
|
|
// This was much more of a problem for the NodeJS version of the daemon which struggled to handle
|
|
|
|
// large volumes of output. However, this code is much more performant so I generally feel a lot
|
|
|
|
// better about it's abilities.
|
|
|
|
//
|
|
|
|
// However, extreme output is still somewhat of a DoS attack vector against this software since we
|
|
|
|
// are still logging it to the disk temporarily and will want to avoid dumping a huge amount of
|
|
|
|
// data all at once. These values are all configurable via the wings configuration file, however the
|
|
|
|
// defaults have been in the wild for almost two years at the time of this writing, so I feel quite
|
|
|
|
// confident in them.
|
2020-09-18 03:13:04 +00:00
|
|
|
//
|
|
|
|
// This function returns an error if the server should be stopped due to violating throttle constraints
|
|
|
|
// and a boolean value indicating if a throttle is being violated when it is checked.
|
|
|
|
func (ct *ConsoleThrottler) Increment(onTrigger func()) error {
|
|
|
|
if !ct.Enabled {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Increment the line count and if we have now output more lines than are allowed, trigger a throttle
|
|
|
|
// activation. Once the throttle is triggered and has passed the kill at value we will trigger a server
|
|
|
|
// stop automatically.
|
|
|
|
if atomic.AddUint64(&ct.count, 1) >= ct.Lines && !ct.Throttled() {
|
2020-12-26 01:04:18 +00:00
|
|
|
ct.isThrottled.Store(true)
|
2020-09-18 03:13:04 +00:00
|
|
|
if ct.markActivation(true) >= ct.MaximumTriggerCount {
|
|
|
|
return ErrTooMuchConsoleData
|
|
|
|
}
|
2020-08-11 04:38:42 +00:00
|
|
|
|
2020-09-18 03:13:04 +00:00
|
|
|
onTrigger()
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2020-08-11 04:38:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Returns the throttler instance for the server or creates a new one.
|
|
|
|
func (s *Server) Throttler() *ConsoleThrottler {
|
2020-12-26 01:04:18 +00:00
|
|
|
s.throttleOnce.Do(func() {
|
2020-08-11 04:38:42 +00:00
|
|
|
s.throttler = &ConsoleThrottler{
|
2021-01-10 01:22:39 +00:00
|
|
|
isThrottled: system.NewAtomicBool(false),
|
2020-08-11 04:38:42 +00:00
|
|
|
ConsoleThrottles: config.Get().Throttles,
|
|
|
|
}
|
2020-12-26 01:04:18 +00:00
|
|
|
})
|
2020-09-18 03:13:04 +00:00
|
|
|
return s.throttler
|
2020-08-11 04:38:42 +00:00
|
|
|
}
|
|
|
|
|
2021-04-03 18:11:36 +00:00
|
|
|
// PublishConsoleOutputFromDaemon sends output to the server console formatted
|
|
|
|
// to appear correctly as being sent from Wings.
|
2020-01-18 22:04:26 +00:00
|
|
|
func (s *Server) PublishConsoleOutputFromDaemon(data string) {
|
2021-04-03 18:11:36 +00:00
|
|
|
if appName == "" {
|
|
|
|
appName = config.Get().AppName
|
|
|
|
}
|
2020-01-18 22:04:26 +00:00
|
|
|
s.Events().Publish(
|
|
|
|
ConsoleOutputEvent,
|
2021-04-03 18:11:36 +00:00
|
|
|
colorstring.Color(fmt.Sprintf("[yellow][bold][%s Daemon]:[default] %s", appName, data)),
|
2020-01-18 22:04:26 +00:00
|
|
|
)
|
|
|
|
}
|