diff --git a/go.mod b/go.mod index 564d203..7ec8181 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/docker/go-units v0.3.3 // indirect github.com/gabriel-vasile/mimetype v0.1.4 github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.0 + github.com/ghodss/yaml v1.0.0 github.com/gogo/protobuf v1.2.1 // indirect github.com/google/uuid v1.1.1 github.com/gorilla/websocket v1.4.0 diff --git a/go.sum b/go.sum index 7c4fbc4..1a6cd91 100644 --- a/go.sum +++ b/go.sum @@ -26,6 +26,8 @@ github.com/gabriel-vasile/mimetype v0.1.4 h1:5mcsq3+DXypREUkW+1juhjeKmE/XnWgs+pa github.com/gabriel-vasile/mimetype v0.1.4/go.mod h1:kMJbg3SlWZCsj4R73F1WDzbT9AyGCOVmUtIxxwO5pmI= github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.0 h1:7KeiSrO5puFH1+vdAdbpiie2TrNnkvFc/eOQzT60Z2k= github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.0/go.mod h1:D1+3UtCYAJ1os1PI+zhTVEj6Tb+IHJvXjXKz83OstmM= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= diff --git a/parser/parser.go b/parser/parser.go index 5e8eb35..4d4ee9f 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -1,16 +1,21 @@ package parser import ( + "bufio" + "encoding/json" "fmt" "github.com/buger/jsonparser" "github.com/magiconair/properties" "github.com/pkg/errors" + "github.com/pterodactyl/wings/config" "go.uber.org/zap" + "gopkg.in/yaml.v2" + "io/ioutil" "os" + "regexp" + "strings" ) -type ConfigurationParser string - // The file parsing options that are available for a server configuration file. const ( File = "file" @@ -21,6 +26,17 @@ const ( Xml = "xml" ) +// Regex to match anything that has a value matching the format of {{ config.$1 }} which +// will cause the program to lookup that configuration value from itself and set that +// value to the configuration one. +// +// This allows configurations to reference values that are node dependent, such as the +// internal IP address used by the daemon, useful in Bungeecord setups for example, where +// it is common to see variables such as "{{config.docker.interface}}" +var configMatchRegex = regexp.MustCompile(`^{{\s?config\.([\w.-]+)\s?}}$`) + +type ConfigurationParser string + // Defines a configuration file for the server startup. These will be looped over // and modified before the server finishes booting. type ConfigurationFile struct { @@ -64,10 +80,122 @@ func (cfr *ConfigurationFileReplacement) UnmarshalJSON(data []byte) error { func (f *ConfigurationFile) Parse(path string) error { zap.S().Debugw("parsing configuration file", zap.String("path", path), zap.String("parser", string(f.Parser))) + var err error + switch f.Parser { case Properties: - f.parsePropertiesFile(path) + err = f.parsePropertiesFile(path) break + case File: + err = f.parseTextFile(path) + break + case Yaml, "yml": + err = f.parseYamlFile(path) + break + } + + return err +} + +// Parses a yaml file and updates any matching key/value pairs before persisting +// it back to the disk. +func (f *ConfigurationFile) parseYamlFile(path string) error { + file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0644) + if err != nil { + return errors.WithStack(err) + } + defer file.Close() + + b, err := ioutil.ReadAll(file) + if err != nil { + return errors.WithStack(err) + } + + var raw interface{} + // Unmarshall the yaml data into a raw interface such that we can work with any arbitrary + // data structure. + if err := yaml.Unmarshal(b, &raw); err != nil { + return errors.WithStack(err) + } + + // Create an indexable map that we can use while looping through elements. + m := raw.(map[interface{}]interface{}) + + for _, v := range f.Replace { + value, err := lookupConfigurationValue(v.Value) + if err != nil { + return errors.WithStack(err) + } + + layer := m + nest := strings.Split(v.Match, ".") + + // Split the key name on any periods, as we do this, initalize the struct for the yaml + // data at that key and then reset the later to point to that newly created layer. If + // we have reached the last split item, set the value of the key to the value defined + // in the replacement data. + for i, key := range nest { + if i == (len(nest) - 1) { + layer[key] = value + } else { + // Don't overwrite the key if it exists in the data already. But, if it is missing, + // go ahead and create the key otherwise we'll hit a panic when trying to access an + // index that does not exist. + if m[key] == nil { + layer[key] = make(map[interface{}]interface{}) + } + + layer = m[key].(map[interface{}]interface{}) + } + } + } + + file.Close() + + if o, err := yaml.Marshal(m); err != nil { + return errors.WithStack(err) + } else { + return ioutil.WriteFile(path, o, 0644) + } +} + +// Parses a text file using basic find and replace. This is a highly inefficient method of +// scanning a file and performing a replacement. You should attempt to use anything other +// than this function where possible. +func (f *ConfigurationFile) parseTextFile(path string) error { + file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0644) + if err != nil { + return errors.WithStack(err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + hasReplaced := false + t := scanner.Text() + + // Iterate over the potential replacements for the line and check if there are + // any matches. + for _, replace := range f.Replace { + if !strings.HasPrefix(t, replace.Match) { + continue + } + + hasReplaced = true + t = strings.Replace(t, replace.Match, replace.Value, 1) + } + + // If there was a replacement that occurred on this specific line, do a write to the file + // immediately to write that modified content to the disk. + if hasReplaced { + if _, err := file.WriteAt([]byte(t), int64(len(scanner.Bytes()))); err != nil { + return errors.WithStack(err) + } + } + } + + if err := scanner.Err(); err != nil { + return errors.WithStack(err) } return nil @@ -82,12 +210,17 @@ func (f *ConfigurationFile) parsePropertiesFile(path string) error { } for _, replace := range f.Replace { - if _, _, err := p.Set(replace.Match, replace.Value); err != nil { + v, err := lookupConfigurationValue(replace.Value) + if err != nil { + return errors.WithStack(err) + } + + if _, _, err := p.Set(replace.Match, v); err != nil { return errors.WithStack(err) } } - w, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644); + w, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return errors.WithStack(err) } @@ -96,3 +229,33 @@ func (f *ConfigurationFile) parsePropertiesFile(path string) error { return err } + +// Looks up a configuration value on the Daemon given a dot-notated syntax. +func lookupConfigurationValue(value string) (string, error) { + // @todo there is probably a much better way to handle this + mb, _ := json.Marshal(config.Get()) + + if !configMatchRegex.Match([]byte(value)) { + return value, nil + } + + // If there is a match, lookup the value in the configuration for the Daemon. If no key + // is found, just return the string representation, otherwise use the value from the + // daemon configuration here. + v := configMatchRegex.ReplaceAllString(value, "$1") + + match, err := jsonparser.GetString(mb, strings.Split(v, ".")...) + if err != nil { + if err != jsonparser.KeyPathNotFoundError { + return "", errors.WithStack(err) + } + + // If there is no key, keep the original value intact, that way it is obvious there + // is a replace issue at play. + v = value + } else { + v = match + } + + return v, nil +}