2019-12-01 20:25:16 +00:00
|
|
|
package parser
|
|
|
|
|
|
|
|
import (
|
2020-04-12 19:22:37 +00:00
|
|
|
"bytes"
|
2019-12-01 23:27:53 +00:00
|
|
|
"regexp"
|
2019-12-01 20:25:16 +00:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
2021-01-10 01:22:39 +00:00
|
|
|
|
|
|
|
"emperror.dev/errors"
|
|
|
|
"github.com/Jeffail/gabs/v2"
|
|
|
|
"github.com/apex/log"
|
|
|
|
"github.com/buger/jsonparser"
|
|
|
|
"github.com/iancoleman/strcase"
|
2019-12-01 20:25:16 +00:00
|
|
|
)
|
|
|
|
|
2019-12-01 23:27:53 +00:00
|
|
|
// 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?}}`)
|
|
|
|
|
2019-12-01 23:40:08 +00:00
|
|
|
// Regex to support modifying XML inline variable data using the config tools. This means
|
|
|
|
// you can pass a replacement of Root.Property='[value="testing"]' to get an XML node
|
|
|
|
// matching:
|
|
|
|
//
|
|
|
|
// <Root>
|
2024-03-13 03:44:55 +00:00
|
|
|
//
|
|
|
|
// <Property value="testing"/>
|
|
|
|
//
|
2019-12-01 23:40:08 +00:00
|
|
|
// </Root>
|
|
|
|
//
|
|
|
|
// noinspection RegExpRedundantEscape
|
|
|
|
var xmlValueMatchRegex = regexp.MustCompile(`^\[([\w]+)='(.*)'\]$`)
|
|
|
|
|
2019-12-01 20:53:47 +00:00
|
|
|
// Gets the value of a key based on the value type defined.
|
2021-08-24 23:05:02 +00:00
|
|
|
func (cfr *ConfigurationFileReplacement) getKeyValue(value string) interface{} {
|
2020-05-18 01:22:06 +00:00
|
|
|
if cfr.ReplaceWith.Type() == jsonparser.Boolean {
|
2021-08-24 23:05:02 +00:00
|
|
|
v, _ := strconv.ParseBool(value)
|
2020-04-12 19:22:37 +00:00
|
|
|
return v
|
|
|
|
}
|
|
|
|
|
2020-05-18 01:22:06 +00:00
|
|
|
// Try to parse into an int, if this fails just ignore the error and continue
|
|
|
|
// through, returning the string.
|
2021-08-24 23:05:02 +00:00
|
|
|
if v, err := strconv.Atoi(value); err == nil {
|
2020-04-12 19:22:37 +00:00
|
|
|
return v
|
2019-12-01 20:25:16 +00:00
|
|
|
}
|
2020-04-12 19:22:37 +00:00
|
|
|
|
2021-08-24 23:05:02 +00:00
|
|
|
return value
|
2019-12-01 20:25:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Iterate over an unstructured JSON/YAML/etc. interface and set all of the required
|
|
|
|
// key/value pairs for the configuration file.
|
|
|
|
//
|
|
|
|
// We need to support wildcard characters in key searches, this allows you to make
|
|
|
|
// modifications to multiple keys at once, especially useful for games with multiple
|
|
|
|
// configurations per-world (such as Spigot and Bungeecord) where we'll need to make
|
|
|
|
// adjustments to the bind address for the user.
|
|
|
|
//
|
2020-05-18 01:22:06 +00:00
|
|
|
// This does not currently support nested wildcard matches. For example, foo.*.bar
|
|
|
|
// will work, however foo.*.bar.*.baz will not, since we'll only be splitting at the
|
|
|
|
// first wildcard, and not subsequent ones.
|
2019-12-01 20:25:16 +00:00
|
|
|
func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error) {
|
|
|
|
parsed, err := gabs.ParseJSON(data)
|
|
|
|
if err != nil {
|
2020-11-28 23:57:10 +00:00
|
|
|
return nil, err
|
2019-12-01 20:25:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, v := range f.Replace {
|
2020-04-12 19:22:37 +00:00
|
|
|
value, err := f.LookupConfigurationValue(v)
|
2019-12-01 20:25:16 +00:00
|
|
|
if err != nil {
|
2020-11-28 23:57:10 +00:00
|
|
|
return nil, err
|
2019-12-01 20:25:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Check for a wildcard character, and if found split the key on that value to
|
|
|
|
// begin doing a search and replace in the data.
|
|
|
|
if strings.Contains(v.Match, ".*") {
|
|
|
|
parts := strings.SplitN(v.Match, ".*", 2)
|
|
|
|
|
|
|
|
// Iterate over each matched child and set the remaining path to the value
|
|
|
|
// that is passed through in the loop.
|
|
|
|
//
|
|
|
|
// If the child is a null value, nothing will happen. Seems reasonable as of the
|
|
|
|
// time this code is being written.
|
|
|
|
for _, child := range parsed.Path(strings.Trim(parts[0], ".")).Children() {
|
2021-08-24 23:05:02 +00:00
|
|
|
if err := v.SetAtPathway(child, strings.Trim(parts[1], "."), value); err != nil {
|
2020-09-08 00:27:23 +00:00
|
|
|
if errors.Is(err, gabs.ErrNotFound) {
|
|
|
|
continue
|
|
|
|
}
|
2020-11-28 23:57:10 +00:00
|
|
|
return nil, errors.WithMessage(err, "failed to set config value of array child")
|
2019-12-01 20:25:16 +00:00
|
|
|
}
|
|
|
|
}
|
2021-08-24 23:05:02 +00:00
|
|
|
continue
|
|
|
|
}
|
2020-09-08 00:27:23 +00:00
|
|
|
|
2021-08-24 23:05:02 +00:00
|
|
|
if err := v.SetAtPathway(parsed, v.Match, value); err != nil {
|
|
|
|
if errors.Is(err, gabs.ErrNotFound) {
|
|
|
|
continue
|
2019-12-01 20:25:16 +00:00
|
|
|
}
|
2021-08-24 23:05:02 +00:00
|
|
|
return nil, errors.WithMessage(err, "unable to set config value at pathway: "+v.Match)
|
2019-12-01 20:25:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return parsed, nil
|
|
|
|
}
|
|
|
|
|
2020-09-08 00:27:23 +00:00
|
|
|
// Regex used to check if there is an array element present in the given pathway by looking for something
|
|
|
|
// along the lines of "something[1]" or "something[1].nestedvalue" as the path.
|
|
|
|
var checkForArrayElement = regexp.MustCompile(`^([^\[\]]+)\[([\d]+)](\..+)?$`)
|
|
|
|
|
|
|
|
// Attempt to set the value of the path depending on if it is an array or not. Gabs cannot handle array
|
|
|
|
// values as "something[1]" but can parse them just fine. This is basically just overly complex code
|
|
|
|
// to handle that edge case and ensure the value gets set correctly.
|
2020-09-10 03:27:41 +00:00
|
|
|
//
|
|
|
|
// Bless thee who has to touch these most unholy waters.
|
2020-09-08 00:27:23 +00:00
|
|
|
func setValueAtPath(c *gabs.Container, path string, value interface{}) error {
|
|
|
|
var err error
|
|
|
|
|
|
|
|
matches := checkForArrayElement.FindStringSubmatch(path)
|
|
|
|
|
2021-08-24 23:05:02 +00:00
|
|
|
// Check if we are **NOT** updating an array element.
|
|
|
|
if len(matches) < 3 {
|
|
|
|
_, err = c.SetP(value, path)
|
2020-11-28 23:57:10 +00:00
|
|
|
return err
|
2020-09-08 00:27:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
i, _ := strconv.Atoi(matches[2])
|
2020-09-10 03:27:41 +00:00
|
|
|
// Find the array element "i" or try to create it if "i" is equal to 0 and is not found
|
|
|
|
// at the given path.
|
2020-09-08 00:27:23 +00:00
|
|
|
ct, err := c.ArrayElementP(i, matches[1])
|
|
|
|
if err != nil {
|
2020-09-10 03:27:41 +00:00
|
|
|
if i != 0 || (!errors.Is(err, gabs.ErrNotArray) && !errors.Is(err, gabs.ErrNotFound)) {
|
2020-11-28 23:57:10 +00:00
|
|
|
return errors.WithMessage(err, "error while parsing array element at path")
|
2020-09-10 03:27:41 +00:00
|
|
|
}
|
2020-09-08 00:27:23 +00:00
|
|
|
|
2020-12-27 18:49:08 +00:00
|
|
|
t := make([]interface{}, 1)
|
2020-09-10 03:27:41 +00:00
|
|
|
// If the length of matches is 4 it means we're trying to access an object down in this array
|
|
|
|
// key, so make sure we generate the array as an array of objects, and not just a generic nil
|
|
|
|
// array.
|
|
|
|
if len(matches) == 4 {
|
|
|
|
t = []interface{}{map[string]interface{}{}}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the error is because this isn't an array or isn't found go ahead and create the array with
|
|
|
|
// an empty object if we have additional things to set on the array, or just an empty array type
|
|
|
|
// if there is not an object structure detected (no matches[3] available).
|
|
|
|
if _, err = c.SetP(t, matches[1]); err != nil {
|
2020-11-28 23:57:10 +00:00
|
|
|
return errors.WithMessage(err, "failed to create empty array for missing element")
|
2020-09-10 03:27:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Set our cursor to be the array element we expect, which in this case is just the first element
|
|
|
|
// since we won't run this code unless the array element is 0. There is too much complexity in trying
|
|
|
|
// to match additional elements. In those cases the server will just have to be rebooted or something.
|
|
|
|
ct, err = c.ArrayElementP(0, matches[1])
|
2020-09-08 00:27:23 +00:00
|
|
|
if err != nil {
|
2020-11-28 23:57:10 +00:00
|
|
|
return errors.WithMessage(err, "failed to find array element at path")
|
2020-09-08 00:27:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Try to set the value. If the path does not exist an error will be raised to the caller which will
|
|
|
|
// then check if the error is because the path is missing. In those cases we just ignore the error since
|
|
|
|
// we don't want to do anything specifically when that happens.
|
2020-09-10 03:27:41 +00:00
|
|
|
//
|
|
|
|
// If there are four matches in the regex it means that we managed to also match a trailing pathway
|
|
|
|
// for the key, which should be found in the given array key item and modified further.
|
|
|
|
if len(matches) == 4 {
|
|
|
|
_, err = ct.SetP(value, strings.TrimPrefix(matches[3], "."))
|
|
|
|
} else {
|
|
|
|
_, err = ct.Set(value)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err != nil {
|
2020-11-28 23:57:10 +00:00
|
|
|
return errors.WithMessage(err, "failed to set value at config path: "+path)
|
2020-09-08 00:27:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-04-12 19:22:37 +00:00
|
|
|
// Sets the value at a specific pathway, but checks if we were looking for a specific
|
|
|
|
// value or not before doing it.
|
2021-08-24 23:05:02 +00:00
|
|
|
func (cfr *ConfigurationFileReplacement) SetAtPathway(c *gabs.Container, path string, value string) error {
|
2020-09-08 00:27:23 +00:00
|
|
|
if cfr.IfValue == "" {
|
|
|
|
return setValueAtPath(c, path, cfr.getKeyValue(value))
|
|
|
|
}
|
2020-04-12 22:57:07 +00:00
|
|
|
|
2021-08-24 23:05:02 +00:00
|
|
|
// Check if we are replacing instead of overwriting.
|
|
|
|
if strings.HasPrefix(cfr.IfValue, "regex:") {
|
|
|
|
// Doing a regex replacement requires an existing value.
|
|
|
|
// TODO: Do we try passing an empty string to the regex?
|
|
|
|
if c.ExistsP(path) {
|
|
|
|
return gabs.ErrNotFound
|
|
|
|
}
|
|
|
|
|
2020-09-08 00:27:23 +00:00
|
|
|
r, err := regexp.Compile(strings.TrimPrefix(cfr.IfValue, "regex:"))
|
|
|
|
if err != nil {
|
|
|
|
log.WithFields(log.Fields{"if_value": strings.TrimPrefix(cfr.IfValue, "regex:"), "error": err}).
|
|
|
|
Warn("configuration if_value using invalid regexp, cannot perform replacement")
|
2020-04-12 19:22:37 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-08-24 23:05:02 +00:00
|
|
|
v := strings.Trim(c.Path(path).String(), "\"")
|
2020-09-08 00:27:23 +00:00
|
|
|
if r.Match([]byte(v)) {
|
2021-08-24 23:05:02 +00:00
|
|
|
return setValueAtPath(c, path, r.ReplaceAllString(v, value))
|
2020-09-08 00:27:23 +00:00
|
|
|
}
|
|
|
|
return nil
|
2021-08-24 23:05:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if c.ExistsP(path) && !bytes.Equal(c.Bytes(), []byte(cfr.IfValue)) {
|
2020-09-08 00:27:23 +00:00
|
|
|
return nil
|
|
|
|
}
|
2020-04-12 19:22:37 +00:00
|
|
|
|
2020-09-08 00:27:23 +00:00
|
|
|
return setValueAtPath(c, path, cfr.getKeyValue(value))
|
2020-04-12 19:22:37 +00:00
|
|
|
}
|
|
|
|
|
2019-12-01 20:25:16 +00:00
|
|
|
// Looks up a configuration value on the Daemon given a dot-notated syntax.
|
2020-06-30 03:08:36 +00:00
|
|
|
func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplacement) (string, error) {
|
2020-04-12 19:22:37 +00:00
|
|
|
// If this is not something that we can do a regex lookup on then just continue
|
|
|
|
// on our merry way. If the value isn't a string, we're not going to be doing anything
|
|
|
|
// with it anyways.
|
|
|
|
if cfr.ReplaceWith.Type() != jsonparser.String || !configMatchRegex.Match(cfr.ReplaceWith.Value()) {
|
2020-06-30 03:08:36 +00:00
|
|
|
return cfr.ReplaceWith.String(), nil
|
2019-12-01 20:25:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
2019-12-01 23:27:53 +00:00
|
|
|
huntPath := configMatchRegex.ReplaceAllString(
|
2020-04-12 19:22:37 +00:00
|
|
|
configMatchRegex.FindString(cfr.ReplaceWith.String()), "$1",
|
2019-12-01 23:27:53 +00:00
|
|
|
)
|
2019-12-01 20:25:16 +00:00
|
|
|
|
|
|
|
var path []string
|
|
|
|
for _, value := range strings.Split(huntPath, ".") {
|
2020-05-18 00:25:53 +00:00
|
|
|
path = append(path, strcase.ToSnake(value))
|
2019-12-01 20:25:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Look for the key in the configuration file, and if found return that value to the
|
|
|
|
// calling function.
|
2020-04-12 19:22:37 +00:00
|
|
|
match, _, _, err := jsonparser.Get(f.configuration, path...)
|
2019-12-01 20:25:16 +00:00
|
|
|
if err != nil {
|
|
|
|
if err != jsonparser.KeyPathNotFoundError {
|
2020-11-28 23:57:10 +00:00
|
|
|
return string(match), err
|
2019-12-01 20:25:16 +00:00
|
|
|
}
|
|
|
|
|
2020-06-13 17:26:35 +00:00
|
|
|
log.WithFields(log.Fields{"path": path, "filename": f.FileName}).Debug("attempted to load a configuration value that does not exist")
|
2020-04-12 19:22:37 +00:00
|
|
|
|
2019-12-01 20:25:16 +00:00
|
|
|
// If there is no key, keep the original value intact, that way it is obvious there
|
|
|
|
// is a replace issue at play.
|
2020-06-30 03:08:36 +00:00
|
|
|
return string(match), nil
|
2019-12-01 20:25:16 +00:00
|
|
|
} else {
|
2020-06-30 03:08:36 +00:00
|
|
|
return configMatchRegex.ReplaceAllString(cfr.ReplaceWith.String(), string(match)), nil
|
2019-12-01 20:25:16 +00:00
|
|
|
}
|
|
|
|
}
|