yoink viper back out of code, simplify some config logic

This commit is contained in:
Dane Everitt 2021-01-14 20:11:01 -08:00
parent 9480ccdbba
commit 80faea3286
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
11 changed files with 527 additions and 602 deletions

View File

@ -147,7 +147,7 @@ func configureCmdRun(cmd *cobra.Command, args []string) {
b, err := ioutil.ReadAll(res.Body) b, err := ioutil.ReadAll(res.Body)
cfg, err := config.NewFromPath(configPath) cfg, err := config.NewAtPath(configPath)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -156,7 +156,7 @@ func configureCmdRun(cmd *cobra.Command, args []string) {
panic(err) panic(err)
} }
if err = cfg.WriteToDisk(); err != nil { if err = config.WriteToDisk(cfg); err != nil {
panic(err) panic(err)
} }

View File

@ -102,7 +102,7 @@ func diagnosticsCmdRun(cmd *cobra.Command, args []string) {
} }
printHeader(output, "Wings Configuration") printHeader(output, "Wings Configuration")
cfg, err := config.ReadConfiguration(config.DefaultLocation) cfg, err := config.FromFile(config.DefaultLocation)
if cfg != nil { if cfg != nil {
fmt.Fprintln(output, " Panel Location:", redact(cfg.PanelLocation)) fmt.Fprintln(output, " Panel Location:", redact(cfg.PanelLocation))
fmt.Fprintln(output, "") fmt.Fprintln(output, "")

View File

@ -8,6 +8,7 @@ import (
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"emperror.dev/errors" "emperror.dev/errors"
@ -26,13 +27,12 @@ import (
"github.com/pterodactyl/wings/sftp" "github.com/pterodactyl/wings/sftp"
"github.com/pterodactyl/wings/system" "github.com/pterodactyl/wings/system"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/crypto/acme" "golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert" "golang.org/x/crypto/acme/autocert"
) )
var ( var (
configPath = "" configPath = config.DefaultLocation
debug = false debug = false
) )
@ -40,6 +40,8 @@ var rootCommand = &cobra.Command{
Use: "wings", Use: "wings",
Short: "Runs the API server allowing programatic control of game servers for Pterodactyl Panel.", Short: "Runs the API server allowing programatic control of game servers for Pterodactyl Panel.",
PreRun: func(cmd *cobra.Command, args []string) { PreRun: func(cmd *cobra.Command, args []string) {
initConfig()
initLogging()
if tls, _ := cmd.Flags().GetBool("auto-tls"); tls { if tls, _ := cmd.Flags().GetBool("auto-tls"); tls {
if host, _ := cmd.Flags().GetString("tls-hostname"); host == "" { if host, _ := cmd.Flags().GetString("tls-hostname"); host == "" {
fmt.Println("A TLS hostname must be provided when running wings with automatic TLS, e.g.:\n\n ./wings --auto-tls --tls-hostname my.example.com") fmt.Println("A TLS hostname must be provided when running wings with automatic TLS, e.g.:\n\n ./wings --auto-tls --tls-hostname my.example.com")
@ -65,9 +67,7 @@ func Execute() {
} }
func init() { func init() {
cobra.OnInitialize(initConfig, initLogging) rootCommand.PersistentFlags().StringVar(&configPath, "config", config.DefaultLocation, "set the location for the configuration file")
rootCommand.PersistentFlags().StringVar(&configPath, "config", "", "set the location for the configuration file")
rootCommand.PersistentFlags().BoolVar(&debug, "debug", false, "pass in order to run wings in debug mode") rootCommand.PersistentFlags().BoolVar(&debug, "debug", false, "pass in order to run wings in debug mode")
// Flags specifically used when running the API. // Flags specifically used when running the API.
@ -81,27 +81,6 @@ func init() {
rootCommand.AddCommand(diagnosticsCmd) rootCommand.AddCommand(diagnosticsCmd)
} }
// Get the configuration path based on the arguments provided.
func readConfiguration() (*config.Configuration, error) {
p := configPath
if !strings.HasPrefix(p, "/") {
d, err := os.Getwd()
if err != nil {
return nil, err
}
p = path.Clean(path.Join(d, configPath))
}
if s, err := os.Stat(p); err != nil {
return nil, err
} else if s.IsDir() {
return nil, errors.New("cannot use directory as configuration file path")
}
return config.ReadConfiguration(p)
}
func rootCmdRun(cmd *cobra.Command, _ []string) { func rootCmdRun(cmd *cobra.Command, _ []string) {
switch cmd.Flag("profiler").Value.String() { switch cmd.Flag("profiler").Value.String() {
case "cpu": case "cpu":
@ -122,18 +101,9 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
defer profile.Start(profile.BlockProfile).Stop() defer profile.Start(profile.BlockProfile).Stop()
} }
c, err := readConfiguration()
if err != nil {
panic(err)
}
if debug {
c.Debug = true
}
printLogo() printLogo()
log.WithField("path", viper.ConfigFileUsed()).Info("loading configuration from file")
log.Debug("running in debug mode") log.Debug("running in debug mode")
log.WithField("config_file", configPath).Info("loading configuration from file")
if ok, _ := cmd.Flags().GetBool("ignore-certificate-errors"); ok { if ok, _ := cmd.Flags().GetBool("ignore-certificate-errors"); ok {
log.Warn("running with --ignore-certificate-errors: TLS certificate host chains and name will not be verified") log.Warn("running with --ignore-certificate-errors: TLS certificate host chains and name will not be verified")
@ -142,45 +112,39 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
} }
} }
config.Set(c)
config.SetDebugViaFlag(debug)
if err := config.ConfigureTimezone(); err != nil { if err := config.ConfigureTimezone(); err != nil {
log.WithField("error", err).Fatal("failed to detect system timezone or use supplied configuration value") log.WithField("error", err).Fatal("failed to detect system timezone or use supplied configuration value")
} }
log.WithField("timezone", c.System.Timezone).Info("configured wings with system timezone") log.WithField("timezone", config.Get().System.Timezone).Info("configured wings with system timezone")
if err := config.ConfigureDirectories(); err != nil { if err := config.ConfigureDirectories(); err != nil {
log.WithField("error", err).Fatal("failed to configure system directories for pterodactyl") log.WithField("error", err).Fatal("failed to configure system directories for pterodactyl")
return return
} }
if err := config.EnableLogRotation(); err != nil { if err := config.EnableLogRotation(); err != nil {
log.WithField("error", err).Fatal("failed to configure log rotation on the system") log.WithField("error", err).Fatal("failed to configure log rotation on the system")
return return
} }
log.WithField("username", c.System.Username).Info("checking for pterodactyl system user") log.WithField("username", config.Get().System.User).Info("checking for pterodactyl system user")
if err := config.EnsurePterodactylUser(); err != nil { if err := config.EnsurePterodactylUser(); err != nil {
log.WithField("error", err).Fatal("failed to create pterodactyl system user") log.WithField("error", err).Fatal("failed to create pterodactyl system user")
} }
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"username": viper.GetString("system.username"), "username": config.Get().System.Username,
"uid": viper.GetInt("system.user.uid"), "uid": config.Get().System.User.Uid,
"gid": viper.GetInt("system.user.gid"), "gid": config.Get().System.User.Gid,
}).Info("configured system user successfully") }).Info("configured system user successfully")
if err := server.LoadDirectory(); err != nil { if err := server.LoadDirectory(); err != nil {
log.WithField("error", err).Fatal("failed to load server configurations") log.WithField("error", err).Fatal("failed to load server configurations")
return
} }
if err := environment.ConfigureDocker(cmd.Context()); err != nil { if err := environment.ConfigureDocker(cmd.Context()); err != nil {
log.WithField("error", err).Fatal("failed to configure docker environment") log.WithField("error", err).Fatal("failed to configure docker environment")
return
} }
if err := viper.WriteConfig(); err != nil { if err := config.WriteToDisk(config.Get()); err != nil {
log.WithField("error", err).Error("failed to save configuration to disk") log.WithField("error", err).Fatal("failed to write configuration to disk")
} }
// Just for some nice log output. // Just for some nice log output.
@ -197,7 +161,6 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
// on Wings. This allows us to ensure the environment exists, write configurations, // on Wings. This allows us to ensure the environment exists, write configurations,
// and reboot processes without causing a slow-down due to sequential booting. // and reboot processes without causing a slow-down due to sequential booting.
pool := workerpool.New(4) pool := workerpool.New(4)
for _, serv := range server.GetServers().All() { for _, serv := range server.GetServers().All() {
s := serv s := serv
@ -252,6 +215,13 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
// Wait until all of the servers are ready to go before we fire up the SFTP and HTTP servers. // Wait until all of the servers are ready to go before we fire up the SFTP and HTTP servers.
pool.StopWait() pool.StopWait()
defer func() {
// Cancel the context on all of the running servers at this point, even though the
// program is just shutting down.
for _, s := range server.GetServers().All() {
s.CtxCancel()
}
}()
go func() { go func() {
// Run the SFTP server. // Run the SFTP server.
@ -261,13 +231,14 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
} }
}() }()
sys := config.Get().System
// Ensure the archive directory exists. // Ensure the archive directory exists.
if err := os.MkdirAll(c.System.ArchiveDirectory, 0755); err != nil { if err := os.MkdirAll(sys.ArchiveDirectory, 0755); err != nil {
log.WithField("error", err).Error("failed to create archive directory") log.WithField("error", err).Error("failed to create archive directory")
} }
// Ensure the backup directory exists. // Ensure the backup directory exists.
if err := os.MkdirAll(c.System.BackupDirectory, 0755); err != nil { if err := os.MkdirAll(sys.BackupDirectory, 0755); err != nil {
log.WithField("error", err).Error("failed to create backup directory") log.WithField("error", err).Error("failed to create backup directory")
} }
@ -277,47 +248,31 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
autotls = false autotls = false
} }
api := config.Get().Api
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"use_ssl": c.Api.Ssl.Enabled, "use_ssl": api.Ssl.Enabled,
"use_auto_tls": autotls, "use_auto_tls": autotls,
"host_address": c.Api.Host, "host_address": api.Host,
"host_port": c.Api.Port, "host_port": api.Port,
}).Info("configuring internal webserver") }).Info("configuring internal webserver")
// Configure the router. // Create a new HTTP server instance to handle inbound requests from the Panel
r := router.Configure() // and external clients.
s := &http.Server{ s := &http.Server{
Addr: fmt.Sprintf("%s:%d", c.Api.Host, c.Api.Port), Addr: api.Host + ":" + strconv.Itoa(api.Port),
Handler: r, Handler: router.Configure(),
TLSConfig: &tls.Config{ TLSConfig: config.DefaultTLSConfig,
NextProtos: []string{"h2", "http/1.1"},
// @see https://blog.cloudflare.com/exposing-go-on-the-internet
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
},
PreferServerCipherSuites: true,
MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS13,
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
},
} }
// Check if the server should run with TLS but using autocert. // Check if the server should run with TLS but using autocert.
if autotls { if autotls {
m := autocert.Manager{ m := autocert.Manager{
Prompt: autocert.AcceptTOS, Prompt: autocert.AcceptTOS,
Cache: autocert.DirCache(path.Join(c.System.RootDirectory, "/.tls-cache")), Cache: autocert.DirCache(path.Join(sys.RootDirectory, "/.tls-cache")),
HostPolicy: autocert.HostWhitelist(tlshostname), HostPolicy: autocert.HostWhitelist(tlshostname),
} }
log.WithField("hostname", tlshostname). log.WithField("hostname", tlshostname).Info("webserver is now listening with auto-TLS enabled; certificates will be automatically generated by Let's Encrypt")
Info("webserver is now listening with auto-TLS enabled; certificates will be automatically generated by Let's Encrypt")
// Hook autocert into the main http server. // Hook autocert into the main http server.
s.TLSConfig.GetCertificate = m.GetCertificate s.TLSConfig.GetCertificate = m.GetCertificate
@ -329,59 +284,53 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
log.WithError(err).Error("failed to serve autocert http server") log.WithError(err).Error("failed to serve autocert http server")
} }
}() }()
// Start the main http server with TLS using autocert. // Start the main http server with TLS using autocert.
if err := s.ListenAndServeTLS("", ""); err != nil { if err := s.ListenAndServeTLS("", ""); err != nil {
log.WithFields(log.Fields{"auto_tls": true, "tls_hostname": tlshostname, "error": err}). log.WithFields(log.Fields{"auto_tls": true, "tls_hostname": tlshostname, "error": err}).Fatal("failed to configure HTTP server using auto-tls")
Fatal("failed to configure HTTP server using auto-tls")
} }
return return
} }
// Check if main http server should run with TLS. // Check if main http server should run with TLS. Otherwise reset the TLS
if c.Api.Ssl.Enabled { // config on the server and then serve it over normal HTTP.
if err := s.ListenAndServeTLS(strings.ToLower(c.Api.Ssl.CertificateFile), strings.ToLower(c.Api.Ssl.KeyFile)); err != nil { if api.Ssl.Enabled {
if err := s.ListenAndServeTLS(strings.ToLower(api.Ssl.CertificateFile), strings.ToLower(api.Ssl.KeyFile)); err != nil {
log.WithFields(log.Fields{"auto_tls": false, "error": err}).Fatal("failed to configure HTTPS server") log.WithFields(log.Fields{"auto_tls": false, "error": err}).Fatal("failed to configure HTTPS server")
} }
return return
} }
// Run the main http server without TLS.
s.TLSConfig = nil s.TLSConfig = nil
if err := s.ListenAndServe(); err != nil { if err := s.ListenAndServe(); err != nil {
log.WithField("error", err).Fatal("failed to configure HTTP server") log.WithField("error", err).Fatal("failed to configure HTTP server")
} }
// Cancel the context on all of the running servers at this point, even though the
// program is just shutting down.
for _, s := range server.GetServers().All() {
s.CtxCancel()
}
} }
// Reads the configuration from the disk and then sets up the global singleton
// with all of the configuration values.
func initConfig() { func initConfig() {
if configPath != "" { if !strings.HasPrefix(configPath, "/") {
viper.SetConfigName("config") d, err := os.Getwd()
viper.SetConfigType("yaml") if err != nil {
viper.AddConfigPath("/etc/pterodactyl") log2.Fatalf("cmd/root: could not determine directory: %s", err)
viper.AddConfigPath("$HOME/.pterodactyl")
viper.AddConfigPath(".")
} else {
viper.SetConfigFile(configPath)
} }
if err := viper.ReadInConfig(); err != nil { configPath = path.Clean(path.Join(d, configPath))
if _, ok := err.(*viper.ConfigFileNotFoundError); ok { }
err := config.FromFile(configPath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
exitWithConfigurationNotice() exitWithConfigurationNotice()
} }
log2.Fatalf("cmd/root: failed to read configuration: %s", err) log2.Fatalf("cmd/root: error while reading configuration file: %s", err)
}
if debug && !config.Get().Debug {
config.SetDebugViaFlag(debug)
} }
} }
// Configures the global logger for Zap so that we can call it from any location // Configures the global logger for Zap so that we can call it from any location
// in the code without having to pass around a logger instance. // in the code without having to pass around a logger instance.
func initLogging() { func initLogging() {
dir := viper.GetString("system.log_directory") dir := config.Get().System.LogDirectory
if err := os.MkdirAll(path.Join(dir, "/install"), 0700); err != nil { if err := os.MkdirAll(path.Join(dir, "/install"), 0700); err != nil {
log2.Fatalf("cmd/root: failed to create install directory path: %s", err) log2.Fatalf("cmd/root: failed to create install directory path: %s", err)
} }
@ -390,12 +339,10 @@ func initLogging() {
if err != nil { if err != nil {
log2.Fatalf("cmd/root: failed to create wings log: %s", err) log2.Fatalf("cmd/root: failed to create wings log: %s", err)
} }
log.SetLevel(log.InfoLevel) log.SetLevel(log.InfoLevel)
if viper.GetBool("debug") { if config.Get().Debug {
log.SetLevel(log.DebugLevel) log.SetLevel(log.DebugLevel)
} }
log.SetHandler(multi.New(cli.Default, cli.New(w.File, false))) log.SetHandler(multi.New(cli.Default, cli.New(w.File, false)))
log.WithField("path", p).Info("writing log files to disk") log.WithField("path", p).Info("writing log files to disk")
} }

View File

@ -1,35 +1,248 @@
package config package config
import ( import (
"context"
"crypto/tls"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec" "os/exec"
"os/user" "os/user"
"path"
"path/filepath"
"regexp"
"strings" "strings"
"sync" "sync"
"text/template"
"time"
"emperror.dev/errors" "emperror.dev/errors"
"github.com/apex/log"
"github.com/cobaugh/osrelease" "github.com/cobaugh/osrelease"
"github.com/creasty/defaults" "github.com/creasty/defaults"
"github.com/gbrlsnchs/jwt/v3" "github.com/gbrlsnchs/jwt/v3"
"github.com/pterodactyl/wings/system" "github.com/pterodactyl/wings/system"
"github.com/spf13/viper"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
const DefaultLocation = "/etc/pterodactyl/config.yml" const DefaultLocation = "/etc/pterodactyl/config.yml"
type Configuration struct { // DefaultTLSConfig sets sane defaults to use when configuring the internal
sync.RWMutex `json:"-" yaml:"-"` // webserver to listen for public connections.
//
// @see https://blog.cloudflare.com/exposing-go-on-the-internet
var DefaultTLSConfig = &tls.Config{
NextProtos: []string{"h2", "http/1.1"},
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
},
PreferServerCipherSuites: true,
MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS13,
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
}
var mu sync.RWMutex
var _config *Configuration
var _jwtAlgo *jwt.HMACSHA
var _debugViaFlag bool
// Locker specific to writing the configuration to the disk, this happens
// in areas that might already be locked so we don't want to crash the process.
var _writeLock sync.Mutex
// SftpConfiguration defines the configuration of the internal SFTP server.
type SftpConfiguration struct {
// The bind address of the SFTP server.
Address string `default:"0.0.0.0" json:"bind_address" yaml:"bind_address"`
// The bind port of the SFTP server.
Port int `default:"2022" json:"bind_port" yaml:"bind_port"`
// If set to true, no write actions will be allowed on the SFTP server.
ReadOnly bool `default:"false" yaml:"read_only"`
}
// ApiConfiguration defines the configuration for the internal API that is
// exposed by the Wings webserver.
type ApiConfiguration struct {
// The interface that the internal webserver should bind to.
Host string `default:"0.0.0.0" yaml:"host"`
// The port that the internal webserver should bind to.
Port int `default:"8080" yaml:"port"`
// SSL configuration for the daemon.
Ssl struct {
Enabled bool `json:"enabled" yaml:"enabled"`
CertificateFile string `json:"cert" yaml:"cert"`
KeyFile string `json:"key" yaml:"key"`
}
// Determines if functionality for allowing remote download of files into server directories
// is enabled on this instance. If set to "true" remote downloads will not be possible for
// servers.
DisableRemoteDownload bool `json:"disable_remote_download" yaml:"disable_remote_download"`
// The maximum size for files uploaded through the Panel in bytes.
UploadLimit int `default:"100" json:"upload_limit" yaml:"upload_limit"`
}
// RemoteQueryConfiguration defines the configuration settings for remote requests
// from Wings to the Panel.
type RemoteQueryConfiguration struct {
// The amount of time in seconds that Wings should allow for a request to the Panel API
// to complete. If this time passes the request will be marked as failed. If your requests
// are taking longer than 30 seconds to complete it is likely a performance issue that
// should be resolved on the Panel, and not something that should be resolved by upping this
// number.
Timeout uint `default:"30" yaml:"timeout"`
// The number of servers to load in a single request to the Panel API when booting the
// Wings instance. A single request is initially made to the Panel to get this number
// of servers, and then the pagination status is checked and additional requests are
// fired off in parallel to request the remaining pages.
//
// It is not recommended to change this from the default as you will likely encounter
// memory limits on your Panel instance. In the grand scheme of things 4 requests for
// 50 servers is likely just as quick as two for 100 or one for 400, and will certainly
// be less likely to cause performance issues on the Panel.
BootServersPerPage uint `default:"50" yaml:"boot_servers_per_page"`
}
// SystemConfiguration defines basic system configuration settings.
type SystemConfiguration struct {
// The root directory where all of the pterodactyl data is stored at.
RootDirectory string `default:"/var/lib/pterodactyl" yaml:"root_directory"`
// Directory where logs for server installations and other wings events are logged.
LogDirectory string `default:"/var/log/pterodactyl" yaml:"log_directory"`
// Directory where the server data is stored at.
Data string `default:"/var/lib/pterodactyl/volumes" yaml:"data"`
// Directory where server archives for transferring will be stored.
ArchiveDirectory string `default:"/var/lib/pterodactyl/archives" yaml:"archive_directory"`
// Directory where local backups will be stored on the machine.
BackupDirectory string `default:"/var/lib/pterodactyl/backups" yaml:"backup_directory"`
// The user that should own all of the server files, and be used for containers.
Username string `default:"pterodactyl" yaml:"username"`
// The timezone for this Wings instance. This is detected by Wings automatically if possible,
// and falls back to UTC if not able to be detected. If you need to set this manually, that
// can also be done.
//
// This timezone value is passed into all containers created by Wings.
Timezone string `yaml:"timezone"`
// Definitions for the user that gets created to ensure that we can quickly access
// this information without constantly having to do a system lookup.
User struct {
Uid int
Gid int
}
// The amount of time in seconds that can elapse before a server's disk space calculation is
// considered stale and a re-check should occur. DANGER: setting this value too low can seriously
// impact system performance and cause massive I/O bottlenecks and high CPU usage for the Wings
// process.
//
// Set to 0 to disable disk checking entirely. This will always return 0 for the disk space used
// by a server and should only be set in extreme scenarios where performance is critical and
// disk usage is not a concern.
DiskCheckInterval int64 `default:"150" yaml:"disk_check_interval"`
// 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
// frequently modifying a servers' files.
CheckPermissionsOnBoot bool `default:"true" yaml:"check_permissions_on_boot"`
// If set to false Wings will not attempt to write a log rotate configuration to the disk
// when it boots and one is not detected.
EnableLogRotate bool `default:"true" yaml:"enable_log_rotate"`
// The number of lines to send when a server connects to the websocket.
WebsocketLogCount int `default:"150" yaml:"websocket_log_count"`
Sftp SftpConfiguration `yaml:"sftp"`
CrashDetection CrashDetection `yaml:"crash_detection"`
Backups Backups `yaml:"backups"`
Transfers Transfers `yaml:"transfers"`
}
type CrashDetection struct {
// Determines if Wings should detect a server that stops with a normal exit code of
// "0" as being crashed if the process stopped without any Wings interaction. E.g.
// the user did not press the stop button, but the process stopped cleanly.
DetectCleanExitAsCrash bool `default:"true" yaml:"detect_clean_exit_as_crash"`
// Timeout specifies the timeout between crashes that will not cause the server
// to be automatically restarted, this value is used to prevent servers from
// becoming stuck in a boot-loop after multiple consecutive crashes.
Timeout int `default:"60" json:"timeout"`
}
type Backups struct {
// WriteLimit imposes a Disk I/O write limit on backups to the disk, this affects all
// backup drivers as the archiver must first write the file to the disk in order to
// upload it to any external storage provider.
//
// If the value is less than 1, the write speed is unlimited,
// if the value is greater than 0, the write speed is the value in MiB/s.
//
// Defaults to 0 (unlimited)
WriteLimit int `default:"0" yaml:"write_limit"`
}
type Transfers struct {
// DownloadLimit imposes a Network I/O read limit when downloading a transfer archive.
//
// If the value is less than 1, the write speed is unlimited,
// if the value is greater than 0, the write speed is the value in MiB/s.
//
// Defaults to 0 (unlimited)
DownloadLimit int `default:"0" yaml:"download_limit"`
}
type ConsoleThrottles struct {
// Whether or not the throttler is enabled for this instance.
Enabled bool `json:"enabled" yaml:"enabled" default:"true"`
// The total number of lines that can be output in a given LineResetInterval period before
// a warning is triggered and counted against the server.
Lines uint64 `json:"lines" yaml:"lines" default:"2000"`
// The total number of throttle activations that can accumulate before a server is considered
// to be breaching and will be stopped. This value is decremented by one every DecayInterval.
MaximumTriggerCount uint64 `json:"maximum_trigger_count" yaml:"maximum_trigger_count" default:"5"`
// The amount of time after which the number of lines processed is reset to 0. This runs in
// a constant loop and is not affected by the current console output volumes. By default, this
// will reset the processed line count back to 0 every 100ms.
LineResetInterval uint64 `json:"line_reset_interval" yaml:"line_reset_interval" default:"100"`
// The amount of time in milliseconds that must pass without an output warning being triggered
// before a throttle activation is decremented.
DecayInterval uint64 `json:"decay_interval" yaml:"decay_interval" default:"10000"`
// The amount of time that a server is allowed to be stopping for before it is terminated
// forcefully if it triggers output throttles.
StopGracePeriod uint `json:"stop_grace_period" yaml:"stop_grace_period" default:"15"`
}
type Configuration struct {
// The location from which this configuration instance was instantiated. // The location from which this configuration instance was instantiated.
path string path string
// Locker specific to writing the configuration to the disk, this happens
// in areas that might already be locked so we don't want to crash the process.
writeLock sync.Mutex
// Determines if wings should be running in debug mode. This value is ignored // Determines if wings should be running in debug mode. This value is ignored
// if the debug flag is passed through the command line arguments. // if the debug flag is passed through the command line arguments.
Debug bool Debug bool
@ -68,168 +281,110 @@ type Configuration struct {
AllowedOrigins []string `json:"allowed_origins" yaml:"allowed_origins"` AllowedOrigins []string `json:"allowed_origins" yaml:"allowed_origins"`
} }
// Defines the configuration of the internal SFTP server. // NewAtPath creates a new struct and set the path where it should be stored.
type SftpConfiguration struct { // This function does not modify the currently stored global configuration.
// The bind address of the SFTP server. func NewAtPath(path string) (*Configuration, error) {
Address string `default:"0.0.0.0" json:"bind_address" yaml:"bind_address"` var c Configuration
// The bind port of the SFTP server.
Port int `default:"2022" json:"bind_port" yaml:"bind_port"`
// If set to true, no write actions will be allowed on the SFTP server.
ReadOnly bool `default:"false" yaml:"read_only"`
}
// Defines the configuration for the internal API that is exposed by the
// daemon webserver.
type ApiConfiguration struct {
// The interface that the internal webserver should bind to.
Host string `default:"0.0.0.0" yaml:"host"`
// The port that the internal webserver should bind to.
Port int `default:"8080" yaml:"port"`
// SSL configuration for the daemon.
Ssl struct {
Enabled bool `json:"enabled" yaml:"enabled"`
CertificateFile string `json:"cert" yaml:"cert"`
KeyFile string `json:"key" yaml:"key"`
}
// Determines if functionality for allowing remote download of files into server directories
// is enabled on this instance. If set to "true" remote downloads will not be possible for
// servers.
DisableRemoteDownload bool `json:"disable_remote_download" yaml:"disable_remote_download"`
// The maximum size for files uploaded through the Panel in bytes.
UploadLimit int `default:"100" json:"upload_limit" yaml:"upload_limit"`
}
// Defines the configuration settings for remote requests from Wings to the Panel.
type RemoteQueryConfiguration struct {
// The amount of time in seconds that Wings should allow for a request to the Panel API
// to complete. If this time passes the request will be marked as failed. If your requests
// are taking longer than 30 seconds to complete it is likely a performance issue that
// should be resolved on the Panel, and not something that should be resolved by upping this
// number.
Timeout uint `default:"30" yaml:"timeout"`
// The number of servers to load in a single request to the Panel API when booting the
// Wings instance. A single request is initially made to the Panel to get this number
// of servers, and then the pagination status is checked and additional requests are
// fired off in parallel to request the remaining pages.
//
// It is not recommended to change this from the default as you will likely encounter
// memory limits on your Panel instance. In the grand scheme of things 4 requests for
// 50 servers is likely just as quick as two for 100 or one for 400, and will certainly
// be less likely to cause performance issues on the Panel.
BootServersPerPage uint `default:"50" yaml:"boot_servers_per_page"`
}
// Reads the configuration from the provided file and returns the configuration
// object that can then be used.
func ReadConfiguration(path string) (*Configuration, error) {
b, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
c := new(Configuration)
// Configures the default values for many of the configuration options present // Configures the default values for many of the configuration options present
// in the structs. Values set in the configuration file take priority over the // in the structs. Values set in the configuration file take priority over the
// default values. // default values.
if err := defaults.Set(c); err != nil { if err := defaults.Set(&c); err != nil {
return nil, err return nil, err
} }
// Track the location where we created this configuration. // Track the location where we created this configuration.
c.unsafeSetPath(path) c.path = path
return &c, nil
// Replace environment variables within the configuration file with their
// values from the host system.
b = []byte(os.ExpandEnv(string(b)))
if err := yaml.Unmarshal(b, c); err != nil {
return nil, err
}
return c, nil
} }
var mu sync.RWMutex
var _config *Configuration
var _jwtAlgo *jwt.HMACSHA
var _debugViaFlag bool
// Set the global configuration instance. This is a blocking operation such that // Set the global configuration instance. This is a blocking operation such that
// anything trying to set a different configuration value, or read the configuration // anything trying to set a different configuration value, or read the configuration
// will be paused until it is complete. // will be paused until it is complete.
func Set(c *Configuration) { func Set(c *Configuration) {
mu.Lock() mu.Lock()
if _config == nil || _config.AuthenticationToken != c.AuthenticationToken { if _config == nil || _config.AuthenticationToken != c.AuthenticationToken {
_jwtAlgo = jwt.NewHS256([]byte(c.AuthenticationToken)) _jwtAlgo = jwt.NewHS256([]byte(c.AuthenticationToken))
} }
_config = c _config = c
mu.Unlock() mu.Unlock()
} }
// SetDebugViaFlag tracks if the application is running in debug mode because of
// a command line flag argument. If so we do not want to store that configuration
// change to the disk.
func SetDebugViaFlag(d bool) { func SetDebugViaFlag(d bool) {
mu.Lock()
_config.Debug = d
_debugViaFlag = d _debugViaFlag = d
mu.Unlock()
} }
// Get the global configuration instance. This is a read-safe operation that will block // Get returns the global configuration instance. This is a thread-safe operation
// if the configuration is presently being modified. // that will block if the configuration is presently being modified.
//
// Be aware that you CANNOT make modifications to the currently stored configuration
// by modifying the struct returned by this function. The only way to make
// modifications is by using the Update() function and passing data through in
// the callback.
func Get() *Configuration { func Get() *Configuration {
mu.RLock() mu.RLock()
defer mu.RUnlock() // Create a copy of the struct so that all modifications made beyond this
// point are immutable.
return _config //goland:noinspection GoVetCopyLock
c := *_config
mu.RUnlock()
return &c
} }
// Returns the in-memory JWT algorithm. // Update performs an in-situ update of the global configuration object using
// a thread-safe mutex lock. This is the correct way to make modifications to
// the global configuration.
func Update(callback func(c *Configuration)) {
mu.Lock()
defer mu.Unlock()
callback(_config)
}
// GetJwtAlgorithm returns the in-memory JWT algorithm.
func GetJwtAlgorithm() *jwt.HMACSHA { func GetJwtAlgorithm() *jwt.HMACSHA {
mu.RLock() mu.RLock()
defer mu.RUnlock() defer mu.RUnlock()
return _jwtAlgo return _jwtAlgo
} }
// Create a new struct and set the path where it should be stored. // WriteToDisk writes the configuration to the disk. This is a thread safe operation
func NewFromPath(path string) (*Configuration, error) { // and will only allow one write at a time. Additional calls while writing are
c := new(Configuration) // queued up.
if err := defaults.Set(c); err != nil { func WriteToDisk(c *Configuration) error {
return c, err _writeLock.Lock()
defer _writeLock.Unlock()
//goland:noinspection GoVetCopyLock
ccopy := *c
// If debugging is set with the flag, don't save that to the configuration file,
// otherwise you'll always end up in debug mode.
if _debugViaFlag {
ccopy.Debug = false
} }
if c.path == "" {
c.unsafeSetPath(path) return errors.New("cannot write configuration, no path defined in struct")
}
return c, nil b, err := yaml.Marshal(&ccopy)
} if err != nil {
return err
// Sets the path where the configuration file is located on the server. This function should }
// not be called except by processes that are generating the configuration such as the configuration if err := ioutil.WriteFile(c.path, b, 0600); err != nil {
// command shipped with this software. return err
func (c *Configuration) unsafeSetPath(path string) { }
c.Lock() return nil
c.path = path
c.Unlock()
}
// Returns the path for this configuration file.
func (c *Configuration) GetPath() string {
c.RLock()
defer c.RUnlock()
return c.path
} }
// EnsurePterodactylUser ensures that the Pterodactyl core user exists on the // EnsurePterodactylUser ensures that the Pterodactyl core user exists on the
// system. This user will be the owner of all data in the root data directory // system. This user will be the owner of all data in the root data directory
// and is used as the user within containers. // and is used as the user within containers. If files are not owned by this
// user there will be issues with permissions on Docker mount points.
// //
// If files are not owned by this user there will be issues with permissions on // This function IS NOT thread safe and should only be called in the main thread
// Docker mount points. // when the application is booting.
func EnsurePterodactylUser() error { func EnsurePterodactylUser() error {
sysName, err := getSystemName() sysName, err := getSystemName()
if err != nil { if err != nil {
@ -238,14 +393,13 @@ func EnsurePterodactylUser() error {
// Our way of detecting if wings is running inside of Docker. // Our way of detecting if wings is running inside of Docker.
if sysName == "busybox" { if sysName == "busybox" {
viper.Set("system.username", system.FirstNotEmpty(os.Getenv("WINGS_USERNAME"), "pterodactyl")) _config.System.Username = system.FirstNotEmpty(os.Getenv("WINGS_USERNAME"), "pterodactyl")
viper.Set("system.user.uid", system.MustInt(system.FirstNotEmpty(os.Getenv("WINGS_UID"), "988"))) _config.System.User.Uid = system.MustInt(system.FirstNotEmpty(os.Getenv("WINGS_UID"), "988"))
viper.Set("system.user.gid", system.MustInt(system.FirstNotEmpty(os.Getenv("WINGS_GID"), "988"))) _config.System.User.Gid = system.MustInt(system.FirstNotEmpty(os.Getenv("WINGS_UID"), "988"))
return nil return nil
} }
username := viper.GetString("system.username") u, err := user.Lookup(_config.System.Username)
u, err := user.Lookup(username)
// If an error is returned but it isn't the unknown user error just abort // If an error is returned but it isn't the unknown user error just abort
// the process entirely. If we did find a user, return it immediately. // the process entirely. If we did find a user, return it immediately.
if err != nil { if err != nil {
@ -253,19 +407,19 @@ func EnsurePterodactylUser() error {
return err return err
} }
} else { } else {
viper.Set("system.user.uid", system.MustInt(u.Uid)) _config.System.User.Uid = system.MustInt(u.Uid)
viper.Set("system.user.gid", system.MustInt(u.Gid)) _config.System.User.Gid = system.MustInt(u.Gid)
return nil return nil
} }
command := fmt.Sprintf("useradd --system --no-create-home --shell /usr/sbin/nologin %s", username) command := fmt.Sprintf("useradd --system --no-create-home --shell /usr/sbin/nologin %s", _config.System.Username)
// Alpine Linux is the only OS we currently support that doesn't work with the useradd // Alpine Linux is the only OS we currently support that doesn't work with the useradd
// command, so in those cases we just modify the command a bit to work as expected. // command, so in those cases we just modify the command a bit to work as expected.
if strings.HasPrefix(sysName, "alpine") { if strings.HasPrefix(sysName, "alpine") {
command = fmt.Sprintf("adduser -S -D -H -G %[1]s -s /sbin/nologin %[1]s", username) command = fmt.Sprintf("adduser -S -D -H -G %[1]s -s /sbin/nologin %[1]s", _config.System.Username)
// We have to create the group first on Alpine, so do that here before continuing on // We have to create the group first on Alpine, so do that here before continuing on
// to the user creation process. // to the user creation process.
if _, err := exec.Command("addgroup", "-S", username).Output(); err != nil { if _, err := exec.Command("addgroup", "-S", _config.System.Username).Output(); err != nil {
return err return err
} }
} }
@ -274,53 +428,189 @@ func EnsurePterodactylUser() error {
if _, err := exec.Command(split[0], split[1:]...).Output(); err != nil { if _, err := exec.Command(split[0], split[1:]...).Output(); err != nil {
return err return err
} }
u, err = user.Lookup(_config.System.Username)
u, err = user.Lookup(username)
if err != nil { if err != nil {
return err return err
} }
viper.Set("system.user.uid", system.MustInt(u.Uid)) _config.System.User.Uid = system.MustInt(u.Uid)
viper.Set("system.user.gid", system.MustInt(u.Gid)) _config.System.User.Gid = system.MustInt(u.Gid)
return nil return nil
} }
// Writes the configuration to the disk as a blocking operation by obtaining an exclusive // FromFile reads the configuration from the provided file and stores it in the
// lock on the file. This prevents something else from writing at the exact same time and // global singleton for this instance.
// leading to bad data conditions. func FromFile(path string) error {
func (c *Configuration) WriteToDisk() error { b, err := ioutil.ReadFile(path)
// Obtain an exclusive write against the configuration file.
c.writeLock.Lock()
defer c.writeLock.Unlock()
ccopy := *c
// If debugging is set with the flag, don't save that to the configuration file, otherwise
// you'll always end up in debug mode.
if _debugViaFlag {
ccopy.Debug = false
}
if c.path == "" {
return errors.New("cannot write configuration, no path defined in struct")
}
b, err := yaml.Marshal(&ccopy)
if err != nil { if err != nil {
return err return err
} }
c, err := NewAtPath(path)
if err != nil {
return err
}
// Replace environment variables within the configuration file with their
// values from the host system.
b = []byte(os.ExpandEnv(string(b)))
if err := yaml.Unmarshal(b, c); err != nil {
return err
}
// Store this configuration in the global state.
Set(c)
return nil
}
if err := ioutil.WriteFile(c.GetPath(), b, 0644); err != nil { // ConfigureDirectories ensures that all of the system directories exist on the
// system. These directories are created so that only the owner can read the data,
// and no other users.
//
// This function IS NOT thread-safe.
func ConfigureDirectories() error {
root := _config.System.RootDirectory
log.WithField("path", root).Debug("ensuring root data directory exists")
if err := os.MkdirAll(root, 0700); err != nil {
return err
}
// There are a non-trivial number of users out there whose data directories are actually a
// symlink to another location on the disk. If we do not resolve that final destination at this
// point things will appear to work, but endless errors will be encountered when we try to
// verify accessed paths since they will all end up resolving outside the expected data directory.
//
// For the sake of automating away as much of this as possible, see if the data directory is a
// symlink, and if so resolve to its final real path, and then update the configuration to use
// that.
if d, err := filepath.EvalSymlinks(_config.System.Data); err != nil {
if !os.IsNotExist(err) {
return err
}
} else if d != _config.System.Data {
_config.System.Data = d
}
log.WithField("path", _config.System.Data).Debug("ensuring server data directory exists")
if err := os.MkdirAll(_config.System.Data, 0700); err != nil {
return err
}
log.WithField("path", _config.System.ArchiveDirectory).Debug("ensuring archive data directory exists")
if err := os.MkdirAll(_config.System.ArchiveDirectory, 0700); err != nil {
return err
}
log.WithField("path", _config.System.BackupDirectory).Debug("ensuring backup data directory exists")
if err := os.MkdirAll(_config.System.BackupDirectory, 0700); err != nil {
return err return err
} }
return nil return nil
} }
// EnableLogRotation writes a logrotate file for wings to the system logrotate
// configuration directory if one exists and a logrotate file is not found. This
// allows us to basically automate away the log rotation for most installs, but
// also enable users to make modifications on their own.
//
// This function IS NOT thread-safe.
func EnableLogRotation() error {
if !_config.System.EnableLogRotate {
log.Info("skipping log rotate configuration, disabled in wings config file")
return nil
}
if st, err := os.Stat("/etc/logrotate.d"); err != nil && !os.IsNotExist(err) {
return err
} else if (err != nil && os.IsNotExist(err)) || !st.IsDir() {
return nil
}
if _, err := os.Stat("/etc/logrotate.d/wings"); err == nil || !os.IsNotExist(err) {
return err
}
log.Info("no log rotation configuration found: adding file now")
// If we've gotten to this point it means the logrotate directory exists on the system
// but there is not a file for wings already. In that case, let us write a new file to
// it so files can be rotated easily.
f, err := os.Create("/etc/logrotate.d/wings")
if err != nil {
return err
}
defer f.Close()
t, err := template.New("logrotate").Parse(`
{{.LogDirectory}}/wings.log {
size 10M
compress
delaycompress
dateext
maxage 7
missingok
notifempty
create 0640 {{.User.Uid}} {{.User.Gid}}
postrotate
killall -SIGHUP wings
endscript
}`)
if err != nil {
return err
}
return errors.Wrap(t.Execute(f, _config.System), "config: failed to write logrotate to disk")
}
// GetStatesPath returns the location of the JSON file that tracks server states.
func (sc *SystemConfiguration) GetStatesPath() string {
return path.Join(sc.RootDirectory, "/states.json")
}
// ConfigureTimezone sets the timezone data for the configuration if it is
// currently missing. If a value has been set, this functionality will only run
// to validate that the timezone being used is valid.
//
// This function IS NOT thread-safe.
func ConfigureTimezone() error {
if _config.System.Timezone == "" {
b, err := ioutil.ReadFile("/etc/timezone")
if err != nil {
if !os.IsNotExist(err) {
return errors.WithMessage(err, "config: failed to open timezone file")
}
_config.System.Timezone = "UTC"
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
// Okay, file isn't found on this OS, we will try using timedatectl to handle this. If this
// command fails, exit, but if it returns a value use that. If no value is returned we will
// fall through to UTC to get Wings booted at least.
out, err := exec.CommandContext(ctx, "timedatectl").Output()
if err != nil {
log.WithField("error", err).Warn("failed to execute \"timedatectl\" to determine system timezone, falling back to UTC")
return nil
}
r := regexp.MustCompile(`Time zone: ([\w/]+)`)
matches := r.FindSubmatch(out)
if len(matches) != 2 || string(matches[1]) == "" {
log.Warn("failed to parse timezone from \"timedatectl\" output, falling back to UTC")
return nil
}
_config.System.Timezone = string(matches[1])
} else {
_config.System.Timezone = string(b)
}
}
_config.System.Timezone = regexp.MustCompile(`(?i)[^a-z_/]+`).ReplaceAllString(_config.System.Timezone, "")
_, err := time.LoadLocation(_config.System.Timezone)
return errors.WithMessage(err, fmt.Sprintf("the supplied timezone %s is invalid", _config.System.Timezone))
}
// Gets the system release name. // Gets the system release name.
func getSystemName() (string, error) { func getSystemName() (string, error) {
// use osrelease to get release version and ID // use osrelease to get release version and ID
if release, err := osrelease.Read(); err != nil { release, err := osrelease.Read()
if err != nil {
return "", err return "", err
} else {
return release["ID"], nil
} }
return release["ID"], nil
} }

View File

@ -12,7 +12,6 @@ type dockerNetworkInterfaces struct {
Subnet string `default:"172.18.0.0/16"` Subnet string `default:"172.18.0.0/16"`
Gateway string `default:"172.18.0.1"` Gateway string `default:"172.18.0.1"`
} }
V6 struct { V6 struct {
Subnet string `default:"fdba:17c8:6c94::/64"` Subnet string `default:"fdba:17c8:6c94::/64"`
Gateway string `default:"fdba:17c8:6c94::1011"` Gateway string `default:"fdba:17c8:6c94::1011"`
@ -39,8 +38,8 @@ type DockerNetworkConfiguration struct {
Interfaces dockerNetworkInterfaces `yaml:"interfaces"` Interfaces dockerNetworkInterfaces `yaml:"interfaces"`
} }
// Defines the docker configuration used by the daemon when interacting with // DockerConfiguration defines the docker configuration used by the daemon when
// containers and networks on the system. // interacting with containers and networks on the system.
type DockerConfiguration struct { type DockerConfiguration struct {
// Network configuration that should be used when creating a new network // Network configuration that should be used when creating a new network
// for containers run through the daemon. // for containers run through the daemon.
@ -58,23 +57,22 @@ type DockerConfiguration struct {
TmpfsSize uint `default:"100" json:"tmpfs_size" yaml:"tmpfs_size"` TmpfsSize uint `default:"100" json:"tmpfs_size" yaml:"tmpfs_size"`
} }
// RegistryConfiguration . // RegistryConfiguration defines the authentication credentials for a given
// Docker registry.
type RegistryConfiguration struct { type RegistryConfiguration struct {
Username string `yaml:"username"` Username string `yaml:"username"`
Password string `yaml:"password"` Password string `yaml:"password"`
} }
// Base64 . // Base64 returns the authentication for a given registry as a base64 encoded
// string value.
func (c RegistryConfiguration) Base64() (string, error) { func (c RegistryConfiguration) Base64() (string, error) {
authConfig := types.AuthConfig{ b, err := json.Marshal(types.AuthConfig{
Username: c.Username, Username: c.Username,
Password: c.Password, Password: c.Password,
} })
b, err := json.Marshal(authConfig)
if err != nil { if err != nil {
return "", err return "", err
} }
return base64.URLEncoding.EncodeToString(b), nil return base64.URLEncoding.EncodeToString(b), nil
} }

View File

@ -1,279 +0,0 @@
package config
import (
"context"
"fmt"
"html/template"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"time"
"emperror.dev/errors"
"github.com/apex/log"
"github.com/spf13/viper"
)
// Defines basic system configuration settings.
type SystemConfiguration struct {
// The root directory where all of the pterodactyl data is stored at.
RootDirectory string `default:"/var/lib/pterodactyl" yaml:"root_directory"`
// Directory where logs for server installations and other wings events are logged.
LogDirectory string `default:"/var/log/pterodactyl" yaml:"log_directory"`
// Directory where the server data is stored at.
Data string `default:"/var/lib/pterodactyl/volumes" yaml:"data"`
// Directory where server archives for transferring will be stored.
ArchiveDirectory string `default:"/var/lib/pterodactyl/archives" yaml:"archive_directory"`
// Directory where local backups will be stored on the machine.
BackupDirectory string `default:"/var/lib/pterodactyl/backups" yaml:"backup_directory"`
// The user that should own all of the server files, and be used for containers.
Username string `default:"pterodactyl" yaml:"username"`
// The timezone for this Wings instance. This is detected by Wings automatically if possible,
// and falls back to UTC if not able to be detected. If you need to set this manually, that
// can also be done.
//
// This timezone value is passed into all containers created by Wings.
Timezone string `yaml:"timezone"`
// Definitions for the user that gets created to ensure that we can quickly access
// this information without constantly having to do a system lookup.
User struct {
Uid int
Gid int
}
// The amount of time in seconds that can elapse before a server's disk space calculation is
// considered stale and a re-check should occur. DANGER: setting this value too low can seriously
// impact system performance and cause massive I/O bottlenecks and high CPU usage for the Wings
// process.
//
// Set to 0 to disable disk checking entirely. This will always return 0 for the disk space used
// by a server and should only be set in extreme scenarios where performance is critical and
// disk usage is not a concern.
DiskCheckInterval int64 `default:"150" yaml:"disk_check_interval"`
// 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
// frequently modifying a servers' files.
CheckPermissionsOnBoot bool `default:"true" yaml:"check_permissions_on_boot"`
// If set to false Wings will not attempt to write a log rotate configuration to the disk
// when it boots and one is not detected.
EnableLogRotate bool `default:"true" yaml:"enable_log_rotate"`
// The number of lines to send when a server connects to the websocket.
WebsocketLogCount int `default:"150" yaml:"websocket_log_count"`
Sftp SftpConfiguration `yaml:"sftp"`
CrashDetection CrashDetection `yaml:"crash_detection"`
Backups Backups `yaml:"backups"`
Transfers Transfers `yaml:"transfers"`
}
type CrashDetection struct {
// Determines if Wings should detect a server that stops with a normal exit code of
// "0" as being crashed if the process stopped without any Wings interaction. E.g.
// the user did not press the stop button, but the process stopped cleanly.
DetectCleanExitAsCrash bool `default:"true" yaml:"detect_clean_exit_as_crash"`
// Timeout specifies the timeout between crashes that will not cause the server
// to be automatically restarted, this value is used to prevent servers from
// becoming stuck in a boot-loop after multiple consecutive crashes.
Timeout int `default:"60" json:"timeout"`
}
type Backups struct {
// WriteLimit imposes a Disk I/O write limit on backups to the disk, this affects all
// backup drivers as the archiver must first write the file to the disk in order to
// upload it to any external storage provider.
//
// If the value is less than 1, the write speed is unlimited,
// if the value is greater than 0, the write speed is the value in MiB/s.
//
// Defaults to 0 (unlimited)
WriteLimit int `default:"0" yaml:"write_limit"`
}
type Transfers struct {
// DownloadLimit imposes a Network I/O read limit when downloading a transfer archive.
//
// If the value is less than 1, the write speed is unlimited,
// if the value is greater than 0, the write speed is the value in MiB/s.
//
// Defaults to 0 (unlimited)
DownloadLimit int `default:"0" yaml:"download_limit"`
}
// ConfigureDirectories ensures that all of the system directories exist on the
// system. These directories are created so that only the owner can read the data,
// and no other users.
func ConfigureDirectories() error {
root := viper.GetString("system.root_directory")
log.WithField("path", root).Debug("ensuring root data directory exists")
if err := os.MkdirAll(root, 0700); err != nil {
return err
}
// There are a non-trivial number of users out there whose data directories are actually a
// symlink to another location on the disk. If we do not resolve that final destination at this
// point things will appear to work, but endless errors will be encountered when we try to
// verify accessed paths since they will all end up resolving outside the expected data directory.
//
// For the sake of automating away as much of this as possible, see if the data directory is a
// symlink, and if so resolve to its final real path, and then update the configuration to use
// that.
data := viper.GetString("system.data")
if d, err := filepath.EvalSymlinks(data); err != nil {
if !os.IsNotExist(err) {
return err
}
} else if d != data {
data = d
viper.Set("system.data", d)
}
log.WithField("path", data).Debug("ensuring server data directory exists")
if err := os.MkdirAll(data, 0700); err != nil {
return err
}
log.WithField("path", viper.GetString("system.archive_directory")).Debug("ensuring archive data directory exists")
if err := os.MkdirAll(viper.GetString("system.archive_directory"), 0700); err != nil {
return err
}
log.WithField("path", viper.GetString("system.backup_directory")).Debug("ensuring backup data directory exists")
if err := os.MkdirAll(viper.GetString("system.backup_directory"), 0700); err != nil {
return err
}
return nil
}
// EnableLogRotation writes a logrotate file for wings to the system logrotate
// configuration directory if one exists and a logrotate file is not found. This
// allows us to basically automate away the log rotation for most installs, but
// also enable users to make modifications on their own.
func EnableLogRotation() error {
// Do nothing if not enabled.
if !viper.GetBool("system.enable_log_rotate") {
log.Info("skipping log rotate configuration, disabled in wings config file")
return nil
}
if st, err := os.Stat("/etc/logrotate.d"); err != nil && !os.IsNotExist(err) {
return err
} else if (err != nil && os.IsNotExist(err)) || !st.IsDir() {
return nil
}
if _, err := os.Stat("/etc/logrotate.d/wings"); err == nil || !os.IsNotExist(err) {
return err
}
log.Info("no log rotation configuration found: adding file now")
// If we've gotten to this point it means the logrotate directory exists on the system
// but there is not a file for wings already. In that case, let us write a new file to
// it so files can be rotated easily.
f, err := os.Create("/etc/logrotate.d/wings")
if err != nil {
return err
}
defer f.Close()
type logrotateConfig struct {
Directory string
UserID int
GroupID int
}
t, err := template.New("logrotate").Parse(`
{{.Directory}}/wings.log {
size 10M
compress
delaycompress
dateext
maxage 7
missingok
notifempty
create 0640 {{.UserID}} {{.GroupID}}
postrotate
killall -SIGHUP wings
endscript
}`)
if err != nil {
return err
}
err = t.Execute(f, logrotateConfig{
Directory: viper.GetString("system.log_directory"),
UserID: viper.GetInt("system.user.uid"),
GroupID: viper.GetInt("system.user.gid"),
})
return errors.Wrap(err, "config: failed to write logrotate to disk")
}
// Returns the location of the JSON file that tracks server states.
func (sc *SystemConfiguration) GetStatesPath() string {
return path.Join(sc.RootDirectory, "states.json")
}
// Returns the location of the JSON file that tracks server states.
func (sc *SystemConfiguration) GetInstallLogPath() string {
return path.Join(sc.LogDirectory, "install/")
}
// ConfigureTimezone sets the timezone data for the configuration if it is
// currently missing. If a value has been set, this functionality will only run
// to validate that the timezone being used is valid.
func ConfigureTimezone() error {
tz := viper.GetString("system.timezone")
defer viper.Set("system.timezone", tz)
if tz == "" {
b, err := ioutil.ReadFile("/etc/timezone")
if err != nil {
if !os.IsNotExist(err) {
return errors.WithMessage(err, "config: failed to open timezone file")
}
tz = "UTC"
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
// Okay, file isn't found on this OS, we will try using timedatectl to handle this. If this
// command fails, exit, but if it returns a value use that. If no value is returned we will
// fall through to UTC to get Wings booted at least.
out, err := exec.CommandContext(ctx, "timedatectl").Output()
if err != nil {
log.WithField("error", err).Warn("failed to execute \"timedatectl\" to determine system timezone, falling back to UTC")
return nil
}
r := regexp.MustCompile(`Time zone: ([\w/]+)`)
matches := r.FindSubmatch(out)
if len(matches) != 2 || string(matches[1]) == "" {
log.Warn("failed to parse timezone from \"timedatectl\" output, falling back to UTC")
return nil
}
tz = string(matches[1])
} else {
tz = string(b)
}
}
tz = regexp.MustCompile(`(?i)[^a-z_/]+`).ReplaceAllString(tz, "")
_, err := time.LoadLocation(tz)
return errors.WithMessage(err, fmt.Sprintf("the supplied timezone %s is invalid", tz))
}

View File

@ -1,27 +0,0 @@
package config
type ConsoleThrottles struct {
// Whether or not the throttler is enabled for this instance.
Enabled bool `json:"enabled" yaml:"enabled" default:"true"`
// The total number of lines that can be output in a given LineResetInterval period before
// a warning is triggered and counted against the server.
Lines uint64 `json:"lines" yaml:"lines" default:"2000"`
// The total number of throttle activations that can accumulate before a server is considered
// to be breaching and will be stopped. This value is decremented by one every DecayInterval.
MaximumTriggerCount uint64 `json:"maximum_trigger_count" yaml:"maximum_trigger_count" default:"5"`
// The amount of time after which the number of lines processed is reset to 0. This runs in
// a constant loop and is not affected by the current console output volumes. By default, this
// will reset the processed line count back to 0 every 100ms.
LineResetInterval uint64 `json:"line_reset_interval" yaml:"line_reset_interval" default:"100"`
// The amount of time in milliseconds that must pass without an output warning being triggered
// before a throttle activation is decremented.
DecayInterval uint64 `json:"decay_interval" yaml:"decay_interval" default:"10000"`
// The amount of time that a server is allowed to be stopping for before it is terminated
// forcefully if it triggers output throttles.
StopGracePeriod uint `json:"stop_grace_period" yaml:"stop_grace_period" default:"15"`
}

View File

@ -9,7 +9,6 @@ import (
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/spf13/viper"
) )
var _conce sync.Once var _conce sync.Once

2
go.mod
View File

@ -63,8 +63,6 @@ require (
github.com/sabhiram/go-gitignore v0.0.0-20201211210132-54b8a0bf510f github.com/sabhiram/go-gitignore v0.0.0-20201211210132-54b8a0bf510f
github.com/sirupsen/logrus v1.7.0 // indirect github.com/sirupsen/logrus v1.7.0 // indirect
github.com/spf13/cobra v1.1.1 github.com/spf13/cobra v1.1.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.7.1
github.com/ugorji/go v1.2.2 // indirect github.com/ugorji/go v1.2.2 // indirect
github.com/ulikunitz/xz v0.5.9 // indirect github.com/ulikunitz/xz v0.5.9 // indirect
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad

3
go.sum
View File

@ -567,9 +567,8 @@ github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=

View File

@ -326,7 +326,7 @@ func (ip *InstallationProcess) BeforeExecute() error {
// Returns the log path for the installation process. // Returns the log path for the installation process.
func (ip *InstallationProcess) GetLogPath() string { func (ip *InstallationProcess) GetLogPath() string {
return filepath.Join(config.Get().System.GetInstallLogPath(), ip.Server.Id()+".log") return filepath.Join(config.Get().System.LogDirectory, "/install", ip.Server.Id()+".log")
} }
// Cleans up after the execution of the installation process. This grabs the logs from the // Cleans up after the execution of the installation process. This grabs the logs from the