diff --git a/command/root.go b/command/root.go deleted file mode 100644 index 124c2f3..0000000 --- a/command/root.go +++ /dev/null @@ -1,68 +0,0 @@ -package command - -import ( - "path/filepath" - "strconv" - - "github.com/spf13/viper" - - "github.com/pterodactyl/wings/api" - "github.com/pterodactyl/wings/config" - "github.com/pterodactyl/wings/constants" - "github.com/pterodactyl/wings/control" - "github.com/pterodactyl/wings/utils" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" -) - -// RootCommand is the root command of wings -var RootCommand = &cobra.Command{ - Use: "wings", - Short: "Wings is the next generation server control daemon for Pterodactyl", - Long: "Wings is the next generation server control daemon for Pterodactyl", - Run: run, - Version: constants.Version, -} - -var configPath string - -func init() { - RootCommand.Flags().StringVarP(&configPath, "config", "c", "./config.yml", "Allows to set the path of the configuration file.") -} - -// Execute registers the RootCommand -func Execute() { - RootCommand.Execute() -} - -func run(cmd *cobra.Command, args []string) { - utils.InitLogging() - log.Info("Loading configuration...") - if err := config.LoadConfiguration(configPath); err != nil { - log.WithError(err).Fatal("Could not locate a suitable config.yml file. Aborting startup.") - log.Exit(1) - } - utils.ConfigureLogging() - - log.Info(` ____`) - log.Info(`__ Pterodactyl _____/___/_______ _______ ______`) - log.Info(`\_____\ \/\/ / / / __ / ___/`) - log.Info(` \___\ / / / / /_/ /___ /`) - log.Info(` \___/\___/___/___/___/___ /______/`) - log.Info(` /_______/ v` + constants.Version) - log.Info() - - log.Info("Configuration loaded successfully.") - - log.Info("Loading configured servers.") - if err := control.LoadServerConfigurations(filepath.Join(viper.GetString(config.DataPath), constants.ServersPath)); err != nil { - log.WithError(err).Error("Failed to load configured servers.") - } - if amount := len(control.GetServers()); amount > 0 { - log.Info("Loaded " + strconv.Itoa(amount) + " server(s).") - } - - log.Info("Starting API server.") - api := &api.InternalAPI{} - api.Listen() -} diff --git a/config.example.yml b/config.example.yml index 4eb8787..0fa508c 100644 --- a/config.example.yml +++ b/config.example.yml @@ -7,8 +7,7 @@ api: enabled: false cert: '' key: '' - uploads: - size_limit: 150000000 + upload_limit: 150000000 docker: container: user: '' @@ -18,24 +17,12 @@ docker: update_images: true socket: '/var/run/docker.sock' timezone_path: '/etc/timezone' -sftp: - host: '0.0.0.0' - port: 2022 - keypair: - bits: 2048 - e: 65537 -log: - path: './logs/' - level: 'info' - prune_days: 10 -internal: - temp_logs: '/tmp/pterodactyl' - disk_check_seconds: 30 - set_permissions_on_boot: true - throttle: - kill_at_count: 5 - decay: 10 - bytes: 4096 - check_interval_ms: 100 +set_permissions_on_boot: true +disk_check_timeout: 30 +throttles: + kill_at_count: 5 + decay: 10 + bytes: 4096 + check_interval: 100 remote: 'http://example.com' token: 'test123' diff --git a/config.go b/config.go new file mode 100644 index 0000000..e35a0f4 --- /dev/null +++ b/config.go @@ -0,0 +1,176 @@ +package main + +import ( + "gopkg.in/yaml.v2" + "io/ioutil" + "os" +) + +type Configuration struct { + // Determines if wings should be running in debug mode. This value is ignored + // if the debug flag is passed through the command line arguments. + Debug bool + + // Directory where the server data is stored at. + Data string + + Api *ApiConfiguration + Docker *DockerConfiguration + + // Determines if permissions for a server should be set automatically on + // daemon boot. This can take a long time on systems with many servers, or on + // systems with servers containing thousands of files. + SetPermissionsOnBoot bool `yaml:"set_permissions_on_boot"` + + // The amount of time in seconds that should elapse between disk usage checks + // run by the daemon. Setting a higher number can result in better IO performance + // at an increased risk of a malicious user creating a process that goes over + // the assigned disk limits. + DiskCheckTimeout int `yaml:"disk_check_timeout"` + + // Defines internal throttling configurations for server processes to prevent + // someone from running an endless loop that spams data to logs. + Throttles struct { + // The number of data overage warnings (inclusive) that can accumulate + // before a process is terminated. + KillAtCount int `yaml:"kill_at_count"` + + // The number of seconds that must elapse before the internal counter + // begins decrementing warnings assigned to a process that is outputting + // too much data. + DecaySeconds int `yaml:"decay"` + + // The total number of bytes allowed to be output by a server process + // per interval. + BytesPerInterval int `yaml:"bytes"` + + // The amount of time that should lapse between data output throttle + // checks. This should be defined in milliseconds. + CheckInterval int `yaml:"check_interval"` + } + + // The location where the panel is running that this daemon should connect to + // to collect data and send events. + PanelLocation string `yaml:"remote"` + + // The token used when performing operations. Requests to this instance must + // validate aganist it. + AuthenticationToken string `yaml:"token"` +} + +// 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 + + // The port that the internal webserver should bind to. + Port int + + // SSL configuration for the daemon. + Ssl struct { + Enabled bool + CertificateFile string `yaml:"cert"` + KeyFile string `yaml:"key"` + } + + // The maximum size for files uploaded through the Panel in bytes. + UploadLimit int `yaml:"upload_limit"` +} + +// Defines the docker configuration used by the daemon when interacting with +// containers and networks on the system. +type DockerConfiguration struct { + Container struct { + User string + } + + // Network configuration that should be used when creating a new network + // for containers run through the daemon. + Network struct { + // The interface that should be used to create the network. Must not conflict + // with any other interfaces in use by Docker or on the system. + Interface string + + // The name of the network to use. If this network already exists it will not + // be created. If it is not found, a new network will be created using the interface + // defined. + Name string + } + + // If true, container images will be updated when a server starts if there + // is an update available. If false the daemon will not attempt updates and will + // defer to the host system to manage image updates. + UpdateImages bool `yaml:"update_images"` + + // The location of the Docker socket. + Socket string + + // Defines the location of the timezone file on the host system that should + // be mounted into the created containers so that they all use the same time. + TimezonePath string `yaml:"timezone_path"` +} + +// Configures the default values for many of the configuration options present +// in the structs. If these values are set in the configuration file they will +// be overridden. However, if they don't exist and we don't set these here, all +// of the places in the code using them will need to be doing checks, which is +// a tedious thing to have to do. +func (c *Configuration) SetDefaults() { + // Set the default data directory. + c.Data = "/srv/daemon-data" + + // By default the internal webserver should bind to all interfaces and + // be served on port 8080. + c.Api = &ApiConfiguration{ + Host: "0.0.0.0", + Port: 8080, + } + + // Setting this to true by default helps us avoid a lot of support requests + // from people that keep trying to move files around as a root user leading + // to server permission issues. + // + // In production and heavy use environments where boot speed is essential, + // this should be set to false as servers will self-correct permissions on + // boot anyways. + c.SetPermissionsOnBoot = true + + // Configure the default throttler implementation. This should work fine + // for 99% of people running servers on the platform. The occasional host + // might need to tweak them to be less restrictive depending on their hardware + // and target audience. + c.Throttles.KillAtCount = 5 + c.Throttles.DecaySeconds = 10 + c.Throttles.BytesPerInterval = 4096 + c.Throttles.CheckInterval = 100 + + // Configure the defaults for Docker connection and networks. + c.Docker = &DockerConfiguration{} + c.Docker.UpdateImages = true + c.Docker.Socket = "/var/run/docker.sock" + c.Docker.Network.Name = "pterodactyl_nw" + c.Docker.Network.Interface = "172.18.0.1" +} + +// 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 := &Configuration{} + c.SetDefaults() + + // 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 +} diff --git a/config/config.go b/config/config.go deleted file mode 100644 index 7635697..0000000 --- a/config/config.go +++ /dev/null @@ -1,59 +0,0 @@ -package config - -import ( - "github.com/spf13/viper" -) - -// LoadConfiguration loads the configuration from a specified file -func LoadConfiguration(path string) error { - if path != "" { - viper.SetConfigFile(path) - } else { - viper.AddConfigPath("./") - viper.SetConfigType("yaml") - viper.SetConfigName("config") - } - - // Find and read the config file - if err := viper.ReadInConfig(); err != nil { - return err - } - - return nil -} - -// StoreConfiguration stores the configuration to a specified file -func StoreConfiguration(path string) error { - // TODO: Implement - - return nil -} - -func setDefaults() { - viper.SetDefault(Debug, false) - viper.SetDefault(DataPath, "./data") - viper.SetDefault(APIHost, "0.0.0.0") - viper.SetDefault(APIPort, 8080) - viper.SetDefault(SSLEnabled, false) - viper.SetDefault(SSLGenerateLetsencrypt, false) - viper.SetDefault(UploadsMaximumSize, 100000) - viper.SetDefault(DockerSocket, "/var/run/docker.sock") - viper.SetDefault(DockerAutoupdateImages, true) - viper.SetDefault(DockerNetworkInterface, "127.18.0.0") - viper.SetDefault(DockerTimezonePath, "/etc/timezone") - viper.SetDefault(SftpHost, "0.0.0.0") - viper.SetDefault(SftpPort, "2202") - viper.SetDefault(LogPath, "./logs") - viper.SetDefault(LogLevel, "info") - viper.SetDefault(LogDeleteAfterDays, "30") -} - -// ContainsAuthKey checks wether the config contains a specified authentication key -func ContainsAuthKey(key string) bool { - for _, k := range viper.GetStringSlice(AuthKey) { - if k == key { - return true - } - } - return false -} diff --git a/config/config_test.go b/config/config_test.go deleted file mode 100644 index 7450c44..0000000 --- a/config/config_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package config - -import ( - "testing" - - "github.com/spf13/viper" - - "github.com/stretchr/testify/assert" -) - -const configFile = "../config.example.yml" - -func TestLoadConfiguraiton(t *testing.T) { - err := LoadConfiguration(configFile) - assert.Nil(t, err) - assert.Equal(t, "0.0.0.0", viper.GetString(APIHost)) -} - -func TestContainsAuthKey(t *testing.T) { - t.Run("key exists", func(t *testing.T) { - LoadConfiguration(configFile) - assert.True(t, ContainsAuthKey("somekey")) - }) - - t.Run("key doesn't exist", func(t *testing.T) { - LoadConfiguration(configFile) - assert.False(t, ContainsAuthKey("someotherkey")) - }) -} diff --git a/config/keys.go b/config/keys.go deleted file mode 100644 index a7076da..0000000 --- a/config/keys.go +++ /dev/null @@ -1,66 +0,0 @@ -package config - -const ( - // Debug is a boolean value that enables debug mode - Debug = "debug" - - // DataPath is a string containing the path where data should - // be stored on the system - DataPath = "data" - - // APIHost is a string containing the interface ip address - // on what the api should listen on - APIHost = "api.host" - // APIPort is an integer containing the port the api should - // listen on - APIPort = "api.port" - // SSLEnabled is a boolean that states whether ssl should be enabled or not - SSLEnabled = "api.ssl.enabled" - // SSLGenerateLetsencrypt is a boolean that enables automatic SSL certificate - // creation with letsencrypt - SSLGenerateLetsencrypt = "api.ssl.letsencrypt" - // SSLCertificate is a string containing the location of - // a ssl certificate to use - SSLCertificate = "api.ssl.cert" - // SSLKey is a string containing the location of the key - // for the ssl certificate - SSLKey = "api.ssl.key" - // UploadsMaximumSize is an integer that sets the maximum size for - // file uploads through the api in Kilobytes - UploadsMaximumSize = "api.uploads.maximumSize" - - // DockerSocket is a string containing the path to the docker socket - DockerSocket = "docker.socket" - // DockerAutoupdateImages is a boolean that enables automatic - // docker image updates on every server install - DockerAutoupdateImages = "docker.autoupdateImages" - // DockerNetworkInterface is a string containing the network interface - // to use for the wings docker network - DockerNetworkInterface = "docker.networkInterface" - // DockerTimezonePath is a string containing the path to the timezone - // file that should be mounted into all containers - DockerTimezonePath = "docker.timezonePath" - - // SftpHost is a string containing the interface ip address on - // which the sftp server should be listening - SftpHost = "sftp.host" - // SftpPort is an integer containing the port the sftp servers hould - // be listening on - SftpPort = "sftp.port" - - // Remote is a string containing the url to the Pterodactyl panel - // wings should send updates to - Remote = "remote" - - // LogPath is a string containing the path where logfiles should be - // stored - LogPath = "log.path" - // LogLevel is a string containing the log level - LogLevel = "log.level" - // LogDeleteAfterDays is an integer containing the amounts of days - // logs should be stored. They will be deleted after. If set to 0 - // logs will be stored indefinitely. - LogDeleteAfterDays = "log.deleteAfterDays" - // AuthKey contains a key that will be replaced by something better - AuthKey = "authKey" -) diff --git a/go.mod b/go.mod index 402911b..db5e508 100644 --- a/go.mod +++ b/go.mod @@ -48,6 +48,7 @@ require ( golang.org/x/net v0.0.0-20180330215511-b68f30494add // indirect golang.org/x/sys v0.0.0-20180329131831-378d26f46672 // indirect golang.org/x/text v0.3.0 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/go-playground/validator.v8 v8.18.2 // indirect - gopkg.in/yaml.v2 v2.2.1 // indirect + gopkg.in/yaml.v2 v2.2.2 ) diff --git a/go.sum b/go.sum index 954203a..fd1eaff 100644 --- a/go.sum +++ b/go.sum @@ -89,7 +89,10 @@ golang.org/x/sys v0.0.0-20180329131831-378d26f46672/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/wings.go b/wings.go index 71dec73..8575a60 100644 --- a/wings.go +++ b/wings.go @@ -6,34 +6,41 @@ import ( "go.uber.org/zap" ) -var ( - configPath string - debug bool -) - // Entrypoint for the Wings application. Configures the logger and checks any // flags that were passed through in the boot arguments. func main() { - flag.StringVar(&configPath, "config", "config.yml", "set the location for the configuration file") - flag.BoolVar(&debug, "debug", false, "pass in order to run wings in debug mode") + var configPath = *flag.String("config", "config.yml", "set the location for the configuration file") + var debug = *flag.Bool("debug", false, "pass in order to run wings in debug mode") flag.Parse() - printLogo() - if err := configureLogging(); err != nil { + zap.S().Infof("using configuration file: %s", configPath) + + c, err := ReadConfiguration(configPath) + if err != nil { panic(err) + return } if debug { + c.Debug = true + } + + printLogo() + if err := configureLogging(c.Debug); err != nil { + panic(err) + } + + if c.Debug { zap.S().Debugw("running in debug mode") } - zap.S().Infof("using configuration file: %s", configPath) + zap.S().Infow("configuration initalized", zap.Any("config", c)) } // 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. -func configureLogging() error { +func configureLogging(debug bool) error { cfg := zap.NewProductionConfig() if debug { cfg = zap.NewDevelopmentConfig()