65102966a1
Instead of making users create the data folder create it for them on startup if it doesn't exist.
446 lines
14 KiB
Go
446 lines
14 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/cobaugh/osrelease"
|
|
"github.com/creasty/defaults"
|
|
"github.com/gbrlsnchs/jwt/v3"
|
|
"go.uber.org/zap"
|
|
"gopkg.in/yaml.v2"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"path"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
type Configuration struct {
|
|
sync.RWMutex `json:"-" yaml:"-"`
|
|
|
|
// 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
|
|
// if the debug flag is passed through the command line arguments.
|
|
Debug bool
|
|
|
|
// A unique identifier for this node in the Panel.
|
|
Uuid string
|
|
|
|
// An identifier for the token which must be included in any requests to the panel
|
|
// so that the token can be looked up correctly.
|
|
AuthenticationTokenId string `json:"token_id" yaml:"token_id"`
|
|
|
|
// The token used when performing operations. Requests to this instance must
|
|
// validate against it.
|
|
AuthenticationToken string `json:"token" yaml:"token"`
|
|
|
|
Api ApiConfiguration
|
|
System SystemConfiguration
|
|
Docker DockerConfiguration
|
|
|
|
// 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 `default:"5" 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 `default:"10" json:"decay" yaml:"decay"`
|
|
|
|
// The total number of bytes allowed to be output by a server process
|
|
// per interval.
|
|
BytesPerInterval int `default:"4096" json:"bytes" yaml:"bytes"`
|
|
|
|
// The amount of time that should lapse between data output throttle
|
|
// checks. This should be defined in milliseconds.
|
|
CheckInterval int `default:"100" yaml:"check_interval"`
|
|
}
|
|
|
|
// The location where the panel is running that this daemon should connect to
|
|
// to collect data and send events.
|
|
PanelLocation string `json:"remote" yaml:"remote"`
|
|
}
|
|
|
|
// Defines basic system configuration settings.
|
|
type SystemConfiguration struct {
|
|
// Directory where the server data is stored at.
|
|
Data string `default:"/srv/daemon-data" yaml:"data"`
|
|
|
|
// Directory where server archives for transferring will be stored.
|
|
ArchiveDirectory string `default:"/srv/daemon-data/.archives" yaml:"archive_directory"`
|
|
|
|
// Directory where local backups will be stored on the machine.
|
|
BackupDirectory string `default:"/srv/daemon-data/.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"`
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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.
|
|
//
|
|
// 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.
|
|
SetPermissionsOnBoot bool `default:"true" yaml:"set_permissions_on_boot"`
|
|
|
|
// 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"`
|
|
|
|
Sftp *SftpConfiguration `yaml:"sftp"`
|
|
}
|
|
|
|
// Defines the configuration of the internal SFTP server.
|
|
type SftpConfiguration struct {
|
|
// If set to false, the internal SFTP server will not be booted and you will need
|
|
// to run the SFTP server independent of this program.
|
|
UseInternalSystem bool `default:"true" json:"use_internal" yaml:"use_internal"`
|
|
// If set to true disk checking will not be performed. This will prevent the SFTP
|
|
// server from checking the total size of a directory when uploading files.
|
|
DisableDiskChecking bool `default:"false" yaml:"disable_disk_checking"`
|
|
// 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"`
|
|
}
|
|
|
|
type dockerNetworkInterfaces struct {
|
|
V4 struct {
|
|
Subnet string `default:"172.18.0.0/16"`
|
|
Gateway string `default:"172.18.0.1"`
|
|
}
|
|
|
|
V6 struct {
|
|
Subnet string `default:"fdba:17c8:6c94::/64"`
|
|
Gateway string `default:"fdba:17c8:6c94::1011"`
|
|
}
|
|
}
|
|
|
|
type DockerNetworkConfiguration 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 `default:"172.18.0.1"`
|
|
|
|
// 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 `default:"pterodactyl_nw"`
|
|
ISPN bool `default:"false" yaml:"ispn"`
|
|
Driver string `default:"bridge"`
|
|
IsInternal bool `default:"false" yaml:"is_internal"`
|
|
EnableICC bool `default:"true" yaml:"enable_icc"`
|
|
Interfaces dockerNetworkInterfaces `yaml:"interfaces"`
|
|
}
|
|
|
|
// Defines the docker configuration used by the daemon when interacting with
|
|
// containers and networks on the system.
|
|
type DockerConfiguration struct {
|
|
// Network configuration that should be used when creating a new network
|
|
// for containers run through the daemon.
|
|
Network DockerNetworkConfiguration `json:"network" yaml:"network"`
|
|
|
|
// 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 `default:"true" json:"update_images" yaml:"update_images"`
|
|
|
|
// The location of the Docker socket.
|
|
Socket string `default:"/var/run/docker.sock"`
|
|
|
|
// 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 `default:"/etc/timezone" json:"timezone_path" yaml:"timezone_path"`
|
|
}
|
|
|
|
// 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 `default:"false"`
|
|
CertificateFile string `json:"cert" yaml:"cert"`
|
|
KeyFile string `json:"key" yaml:"key"`
|
|
}
|
|
|
|
// The maximum size for files uploaded through the Panel in bytes.
|
|
UploadLimit int `default:"100" json:"upload_limit" yaml:"upload_limit"`
|
|
}
|
|
|
|
// 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
|
|
// in the structs. Values set in the configuration file take priority over the
|
|
// default values.
|
|
if err := defaults.Set(c); err != nil {
|
|
return nil, 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 nil, err
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
var Mutex sync.RWMutex
|
|
|
|
var _config *Configuration
|
|
var _jwtAlgo *jwt.HMACSHA
|
|
var _debugViaFlag bool
|
|
|
|
// Set the global configuration instance. This is a blocking operation such that
|
|
// anything trying to set a different configuration value, or read the configuration
|
|
// will be paused until it is complete.
|
|
func Set(c *Configuration) {
|
|
Mutex.Lock()
|
|
|
|
if _config == nil || _config.AuthenticationToken != c.AuthenticationToken {
|
|
_jwtAlgo = jwt.NewHS256([]byte(c.AuthenticationToken))
|
|
}
|
|
|
|
_config = c
|
|
Mutex.Unlock()
|
|
}
|
|
|
|
func SetDebugViaFlag(d bool) {
|
|
_debugViaFlag = d
|
|
}
|
|
|
|
// Get the global configuration instance. This is a read-safe operation that will block
|
|
// if the configuration is presently being modified.
|
|
func Get() *Configuration {
|
|
Mutex.RLock()
|
|
defer Mutex.RUnlock()
|
|
|
|
return _config
|
|
}
|
|
|
|
// Returns the in-memory JWT algorithm.
|
|
func GetJwtAlgorithm() *jwt.HMACSHA {
|
|
Mutex.RLock()
|
|
defer Mutex.RUnlock()
|
|
|
|
return _jwtAlgo
|
|
}
|
|
|
|
// Ensures that the Pterodactyl core user exists on the system. This user will be the
|
|
// owner of all data in the root data directory 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.
|
|
func (c *Configuration) EnsurePterodactylUser() (*user.User, error) {
|
|
u, err := user.Lookup(c.System.Username)
|
|
|
|
// 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.
|
|
if err == nil {
|
|
return u, c.setSystemUser(u)
|
|
} else if _, ok := err.(user.UnknownUserError); !ok {
|
|
return nil, err
|
|
}
|
|
|
|
sysName, err := getSystemName()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var command = fmt.Sprintf("useradd --system --no-create-home --shell /bin/false %s", c.System.Username)
|
|
|
|
// 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.
|
|
if strings.HasPrefix(sysName, "alpine") {
|
|
command = fmt.Sprintf("adduser -S -D -H -G %[1]s -s /bin/false %[1]s", c.System.Username)
|
|
|
|
// We have to create the group first on Alpine, so do that here before continuing on
|
|
// to the user creation process.
|
|
if _, err := exec.Command("addgroup", "-S", c.System.Username).Output(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
split := strings.Split(command, " ")
|
|
if _, err := exec.Command(split[0], split[1:]...).Output(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if u, err := user.Lookup(c.System.Username); err != nil {
|
|
return nil, err
|
|
} else {
|
|
return u, c.setSystemUser(u)
|
|
}
|
|
}
|
|
|
|
// Set the system user into the configuration and then write it to the disk so that
|
|
// it is persisted on boot.
|
|
func (c *Configuration) setSystemUser(u *user.User) error {
|
|
uid, _ := strconv.Atoi(u.Uid)
|
|
gid, _ := strconv.Atoi(u.Gid)
|
|
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
|
|
c.System.Username = u.Username
|
|
c.System.User.Uid = uid
|
|
c.System.User.Gid = gid
|
|
|
|
return c.WriteToDisk()
|
|
}
|
|
|
|
// Ensures that the configured data directory has the correct permissions assigned to
|
|
// all of the files and folders within.
|
|
func (c *Configuration) EnsureFilePermissions() error {
|
|
// Don't run this unless it is configured to be run. On large system this can often slow
|
|
// things down dramatically during the boot process.
|
|
if !c.System.SetPermissionsOnBoot {
|
|
return nil
|
|
}
|
|
|
|
r := regexp.MustCompile("^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$")
|
|
|
|
// add trailing slash on data directory is no trailing slash exists
|
|
if ! strings.HasSuffix(c.System.Data, "/") {
|
|
c.System.Data = c.System.Data + "/"
|
|
}
|
|
|
|
// create the daemon-data dir if it doesn't exist
|
|
p, file := path.Split(c.System.Data)
|
|
|
|
if _, err := os.Stat(c.System.Data); err != nil {
|
|
// if file doesn't exist
|
|
if os.IsNotExist(err) {
|
|
//
|
|
if _, err = os.Stat(c.System.Data); err != nil {
|
|
if file == "" {
|
|
if err = os.Mkdir(p, 0755); err != nil {
|
|
}
|
|
zap.S().Debugf("created %s folder", c.System.Data)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
files, err := ioutil.ReadDir(c.System.Data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
su, err := user.Lookup(c.System.Username)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
wg := new(sync.WaitGroup)
|
|
|
|
for _, file := range files {
|
|
wg.Add(1)
|
|
|
|
// Asynchronously run through the list of files and folders in the data directory. If
|
|
// the item is not a folder, or is not a folder that matches the expected UUIDv4 format
|
|
// skip over it.
|
|
//
|
|
// If we do have a positive match, run a chown against the directory.
|
|
go func(f os.FileInfo) {
|
|
defer wg.Done()
|
|
|
|
if !f.IsDir() || !r.MatchString(f.Name()) {
|
|
return
|
|
}
|
|
|
|
uid, _ := strconv.Atoi(su.Uid)
|
|
gid, _ := strconv.Atoi(su.Gid)
|
|
|
|
if err := os.Chown(path.Join(c.System.Data, f.Name()), uid, gid); err != nil {
|
|
zap.S().Warnw("failed to chown server directory", zap.String("directory", f.Name()), zap.Error(err))
|
|
}
|
|
}(file)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Writes the configuration to the disk as a blocking operation by obtaining an exclusive
|
|
// lock on the file. This prevents something else from writing at the exact same time and
|
|
// leading to bad data conditions.
|
|
func (c *Configuration) WriteToDisk() error {
|
|
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
|
|
}
|
|
|
|
b, err := yaml.Marshal(&ccopy)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Obtain an exclusive write against the configuration file.
|
|
c.writeLock.Lock()
|
|
defer c.writeLock.Unlock()
|
|
|
|
if err := ioutil.WriteFile("config.yml", b, 0644); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Gets the system release name.
|
|
func getSystemName() (string, error) {
|
|
// use osrelease to get release version and ID
|
|
if release, err := osrelease.Read(); err != nil {
|
|
return "", err
|
|
} else {
|
|
return release["ID"], nil
|
|
}
|
|
}
|