2019-12-01 02:07:05 +00:00
|
|
|
package parser
|
|
|
|
|
|
|
|
import (
|
2019-12-01 06:24:44 +00:00
|
|
|
"bufio"
|
2024-03-13 03:44:55 +00:00
|
|
|
"bytes"
|
|
|
|
"io"
|
2021-01-10 01:22:39 +00:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"emperror.dev/errors"
|
2020-06-13 17:26:35 +00:00
|
|
|
"github.com/apex/log"
|
2019-12-01 23:04:03 +00:00
|
|
|
"github.com/beevik/etree"
|
2019-12-01 02:07:05 +00:00
|
|
|
"github.com/buger/jsonparser"
|
2022-02-01 00:30:07 +00:00
|
|
|
"github.com/goccy/go-json"
|
2020-05-18 01:22:06 +00:00
|
|
|
"github.com/icza/dyno"
|
2019-12-01 02:07:05 +00:00
|
|
|
"github.com/magiconair/properties"
|
2019-12-01 20:53:47 +00:00
|
|
|
"gopkg.in/ini.v1"
|
2022-09-21 17:50:55 +00:00
|
|
|
"gopkg.in/yaml.v3"
|
2021-08-02 21:07:00 +00:00
|
|
|
|
|
|
|
"github.com/pterodactyl/wings/config"
|
2024-03-13 03:44:55 +00:00
|
|
|
"github.com/pterodactyl/wings/internal/ufs"
|
2019-12-01 02:07:05 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// The file parsing options that are available for a server configuration file.
|
|
|
|
const (
|
|
|
|
File = "file"
|
|
|
|
Yaml = "yaml"
|
|
|
|
Properties = "properties"
|
|
|
|
Ini = "ini"
|
|
|
|
Json = "json"
|
|
|
|
Xml = "xml"
|
|
|
|
)
|
|
|
|
|
2021-04-03 20:20:07 +00:00
|
|
|
type ReplaceValue struct {
|
|
|
|
value []byte
|
|
|
|
valueType jsonparser.ValueType
|
|
|
|
}
|
|
|
|
|
2021-04-03 21:02:37 +00:00
|
|
|
// Value returns the underlying value of the replacement. Be aware that this
|
|
|
|
// can include escaped UTF-8 sequences that will need to be handled by the caller
|
|
|
|
// in order to avoid accidentally injecting invalid sequences into the running
|
|
|
|
// process.
|
|
|
|
//
|
|
|
|
// For example the expected value may be "§Foo" but you'll be working directly
|
|
|
|
// with "\u00a7FOo" for this value. This will cause user pain if not solved since
|
|
|
|
// that is clearly not the value they were expecting to be using.
|
2021-04-03 20:20:07 +00:00
|
|
|
func (cv *ReplaceValue) Value() []byte {
|
|
|
|
return cv.value
|
|
|
|
}
|
|
|
|
|
2021-04-03 21:02:37 +00:00
|
|
|
// Type returns the underlying data type for the Value field.
|
2021-04-03 20:20:07 +00:00
|
|
|
func (cv *ReplaceValue) Type() jsonparser.ValueType {
|
|
|
|
return cv.valueType
|
|
|
|
}
|
|
|
|
|
2021-04-03 21:02:37 +00:00
|
|
|
// String returns the value as a string representation. This will automatically
|
|
|
|
// handle casting the UTF-8 sequence into the expected value, switching something
|
|
|
|
// like "\u00a7Foo" into "§Foo".
|
2021-04-03 20:20:07 +00:00
|
|
|
func (cv *ReplaceValue) String() string {
|
2021-08-24 23:05:02 +00:00
|
|
|
switch cv.Type() {
|
|
|
|
case jsonparser.String:
|
|
|
|
str, err := jsonparser.ParseString(cv.value)
|
|
|
|
if err != nil {
|
|
|
|
panic(errors.Wrap(err, "parser: could not parse value"))
|
2021-04-03 21:02:37 +00:00
|
|
|
}
|
2021-08-24 23:05:02 +00:00
|
|
|
return str
|
|
|
|
case jsonparser.Null:
|
|
|
|
return "<nil>"
|
|
|
|
case jsonparser.Boolean:
|
|
|
|
return string(cv.value)
|
|
|
|
case jsonparser.Number:
|
|
|
|
return string(cv.value)
|
|
|
|
default:
|
2021-04-03 21:02:37 +00:00
|
|
|
return "<invalid>"
|
|
|
|
}
|
2021-04-03 20:20:07 +00:00
|
|
|
}
|
|
|
|
|
2024-03-13 03:44:55 +00:00
|
|
|
func (cv *ReplaceValue) Bytes() []byte {
|
|
|
|
switch cv.Type() {
|
|
|
|
case jsonparser.String:
|
|
|
|
var stackbuf [64]byte
|
|
|
|
bU, err := jsonparser.Unescape(cv.value, stackbuf[:])
|
|
|
|
if err != nil {
|
|
|
|
panic(errors.Wrap(err, "parser: could not parse value"))
|
|
|
|
}
|
|
|
|
return bU
|
|
|
|
case jsonparser.Null:
|
|
|
|
return []byte("<nil>")
|
|
|
|
case jsonparser.Boolean:
|
|
|
|
return cv.value
|
|
|
|
case jsonparser.Number:
|
|
|
|
return cv.value
|
|
|
|
default:
|
|
|
|
return []byte("<invalid>")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-01 06:24:44 +00:00
|
|
|
type ConfigurationParser string
|
|
|
|
|
2020-06-13 17:26:35 +00:00
|
|
|
func (cp ConfigurationParser) String() string {
|
|
|
|
return string(cp)
|
|
|
|
}
|
|
|
|
|
2022-01-23 20:17:40 +00:00
|
|
|
// ConfigurationFile defines a configuration file for the server startup. These
|
|
|
|
// will be looped over and modified before the server finishes booting.
|
2019-12-01 02:07:05 +00:00
|
|
|
type ConfigurationFile struct {
|
|
|
|
FileName string `json:"file"`
|
|
|
|
Parser ConfigurationParser `json:"parser"`
|
|
|
|
Replace []ConfigurationFileReplacement `json:"replace"`
|
2019-12-01 19:19:49 +00:00
|
|
|
|
|
|
|
// Tracks Wings' configuration so that we can quickly get values
|
|
|
|
// out of it when variables request it.
|
|
|
|
configuration []byte
|
2019-12-01 02:07:05 +00:00
|
|
|
}
|
|
|
|
|
2022-01-23 20:17:40 +00:00
|
|
|
// UnmarshalJSON is a custom unmarshaler for configuration files. If there is an
|
|
|
|
// error while parsing out the replacements, don't fail the entire operation,
|
|
|
|
// just log a global warning so someone can find the issue, and return an empty
|
|
|
|
// array of replacements.
|
2020-05-17 22:57:59 +00:00
|
|
|
func (f *ConfigurationFile) UnmarshalJSON(data []byte) error {
|
|
|
|
var m map[string]*json.RawMessage
|
|
|
|
if err := json.Unmarshal(data, &m); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := json.Unmarshal(*m["file"], &f.FileName); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := json.Unmarshal(*m["parser"], &f.Parser); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := json.Unmarshal(*m["replace"], &f.Replace); err != nil {
|
2020-06-13 17:26:35 +00:00
|
|
|
log.WithField("file", f.FileName).WithField("error", err).Warn("failed to unmarshal configuration file replacement")
|
2020-05-17 22:57:59 +00:00
|
|
|
|
|
|
|
f.Replace = []ConfigurationFileReplacement{}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-04-03 21:02:37 +00:00
|
|
|
// ConfigurationFileReplacement defines a single find/replace instance for a
|
|
|
|
// given server configuration file.
|
2019-12-01 02:07:05 +00:00
|
|
|
type ConfigurationFileReplacement struct {
|
2020-04-12 19:22:37 +00:00
|
|
|
Match string `json:"match"`
|
2020-04-12 22:57:07 +00:00
|
|
|
IfValue string `json:"if_value"`
|
2020-04-12 19:22:37 +00:00
|
|
|
ReplaceWith ReplaceValue `json:"replace_with"`
|
2019-12-01 02:07:05 +00:00
|
|
|
}
|
|
|
|
|
2021-04-03 21:02:37 +00:00
|
|
|
// UnmarshalJSON handles unmarshaling the JSON representation into a struct that
|
|
|
|
// provides more useful data to this functionality.
|
2019-12-01 02:07:05 +00:00
|
|
|
func (cfr *ConfigurationFileReplacement) UnmarshalJSON(data []byte) error {
|
2020-04-12 22:57:07 +00:00
|
|
|
m, err := jsonparser.GetString(data, "match")
|
|
|
|
if err != nil {
|
2020-05-17 22:57:59 +00:00
|
|
|
return err
|
2019-12-01 02:07:05 +00:00
|
|
|
}
|
2020-05-18 01:22:06 +00:00
|
|
|
|
2020-09-08 00:27:23 +00:00
|
|
|
cfr.Match = m
|
2019-12-01 02:07:05 +00:00
|
|
|
|
2020-04-12 22:57:07 +00:00
|
|
|
iv, err := jsonparser.GetString(data, "if_value")
|
|
|
|
// We only check keypath here since match & replace_with should be present on all of
|
|
|
|
// them, however if_value is optional.
|
|
|
|
if err != nil && err != jsonparser.KeyPathNotFoundError {
|
2020-05-17 22:57:59 +00:00
|
|
|
return err
|
2020-04-12 22:57:07 +00:00
|
|
|
}
|
|
|
|
cfr.IfValue = iv
|
|
|
|
|
|
|
|
rw, dt, _, err := jsonparser.Get(data, "replace_with")
|
|
|
|
if err != nil {
|
2020-05-17 22:57:59 +00:00
|
|
|
if err != jsonparser.KeyPathNotFoundError {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Okay, likely dealing with someone who forgot to upgrade their eggs, so in
|
|
|
|
// that case, fallback to using the old key which was "value".
|
|
|
|
rw, dt, _, err = jsonparser.Get(data, "value")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-04-12 22:57:07 +00:00
|
|
|
}
|
2020-05-17 22:57:59 +00:00
|
|
|
|
2020-04-12 22:57:07 +00:00
|
|
|
cfr.ReplaceWith = ReplaceValue{
|
|
|
|
value: rw,
|
|
|
|
valueType: dt,
|
2019-12-01 02:07:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-03-13 03:44:55 +00:00
|
|
|
// Parse parses a given configuration file and updates all the values within
|
|
|
|
// as defined in the API response from the Panel.
|
|
|
|
func (f *ConfigurationFile) Parse(file ufs.File) error {
|
|
|
|
//log.WithField("path", path).WithField("parser", f.Parser.String()).Debug("parsing server configuration file")
|
2019-12-01 02:07:05 +00:00
|
|
|
|
2024-03-13 03:44:55 +00:00
|
|
|
// What the fuck is going on here?
|
2020-05-18 00:25:53 +00:00
|
|
|
if mb, err := json.Marshal(config.Get()); err != nil {
|
|
|
|
return err
|
|
|
|
} else {
|
|
|
|
f.configuration = mb
|
|
|
|
}
|
|
|
|
|
2019-12-01 06:24:44 +00:00
|
|
|
var err error
|
|
|
|
|
2019-12-01 02:07:05 +00:00
|
|
|
switch f.Parser {
|
|
|
|
case Properties:
|
2024-03-13 03:44:55 +00:00
|
|
|
err = f.parsePropertiesFile(file)
|
2019-12-01 06:24:44 +00:00
|
|
|
case File:
|
2024-03-13 03:44:55 +00:00
|
|
|
err = f.parseTextFile(file)
|
2019-12-01 06:24:44 +00:00
|
|
|
case Yaml, "yml":
|
2024-03-13 03:44:55 +00:00
|
|
|
err = f.parseYamlFile(file)
|
2019-12-01 19:19:49 +00:00
|
|
|
case Json:
|
2024-03-13 03:44:55 +00:00
|
|
|
err = f.parseJsonFile(file)
|
2019-12-01 20:53:47 +00:00
|
|
|
case Ini:
|
2024-03-13 03:44:55 +00:00
|
|
|
err = f.parseIniFile(file)
|
2019-12-01 23:04:03 +00:00
|
|
|
case Xml:
|
2024-03-13 03:44:55 +00:00
|
|
|
err = f.parseXmlFile(file)
|
2019-12-01 06:24:44 +00:00
|
|
|
}
|
2020-11-28 23:57:10 +00:00
|
|
|
return err
|
2019-12-01 06:24:44 +00:00
|
|
|
}
|
|
|
|
|
2019-12-01 23:04:03 +00:00
|
|
|
// Parses an xml file.
|
2024-03-13 03:44:55 +00:00
|
|
|
func (f *ConfigurationFile) parseXmlFile(file ufs.File) error {
|
2019-12-01 23:04:03 +00:00
|
|
|
doc := etree.NewDocument()
|
|
|
|
if _, err := doc.ReadFrom(file); err != nil {
|
2019-12-08 00:48:26 +00:00
|
|
|
return err
|
2019-12-01 23:04:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// If there is no root we should create a basic start to the file. This isn't required though,
|
|
|
|
// and if it doesn't work correctly I'll just remove the code.
|
|
|
|
if doc.Root() == nil {
|
|
|
|
doc.CreateProcInst("xml", `version="1.0" encoding="utf-8"`)
|
|
|
|
}
|
|
|
|
|
|
|
|
for i, replacement := range f.Replace {
|
2020-04-12 19:22:37 +00:00
|
|
|
value, err := f.LookupConfigurationValue(replacement)
|
2019-12-01 23:04:03 +00:00
|
|
|
if err != nil {
|
2019-12-08 00:48:26 +00:00
|
|
|
return err
|
2019-12-01 23:04:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// If this is the first item and there is no root element, create that root now and apply
|
|
|
|
// it for future use.
|
|
|
|
if i == 0 && doc.Root() == nil {
|
|
|
|
parts := strings.SplitN(replacement.Match, ".", 2)
|
|
|
|
doc.SetRoot(doc.CreateElement(parts[0]))
|
|
|
|
}
|
|
|
|
|
|
|
|
path := "./" + strings.Replace(replacement.Match, ".", "/", -1)
|
|
|
|
|
|
|
|
// If we're not doing a wildcard replacement go ahead and create the
|
|
|
|
// missing element if we cannot find it yet.
|
|
|
|
if !strings.Contains(path, "*") {
|
|
|
|
parts := strings.Split(replacement.Match, ".")
|
|
|
|
|
|
|
|
// Set the initial element to be the root element, and then work from there.
|
2020-12-27 18:49:08 +00:00
|
|
|
element := doc.Root()
|
2019-12-01 23:04:03 +00:00
|
|
|
|
|
|
|
// Iterate over the path to create the required structure for the given element's path.
|
|
|
|
// This does not set a value, only ensures that the base structure exists. We start at index
|
|
|
|
// 1 because an XML document can only contain a single root element, and from there we'll
|
|
|
|
// work our way down the chain.
|
|
|
|
for _, tag := range parts[1:] {
|
|
|
|
if e := element.FindElement(tag); e == nil {
|
|
|
|
element = element.CreateElement(tag)
|
|
|
|
} else {
|
|
|
|
element = e
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Iterate over the elements we found and update their values.
|
|
|
|
for _, element := range doc.FindElements(path) {
|
2020-06-30 03:08:36 +00:00
|
|
|
if xmlValueMatchRegex.MatchString(value) {
|
|
|
|
k := xmlValueMatchRegex.ReplaceAllString(value, "$1")
|
|
|
|
v := xmlValueMatchRegex.ReplaceAllString(value, "$2")
|
2019-12-01 23:40:08 +00:00
|
|
|
|
|
|
|
element.CreateAttr(k, v)
|
|
|
|
} else {
|
2020-06-30 03:08:36 +00:00
|
|
|
element.SetText(value)
|
2019-12-01 23:40:08 +00:00
|
|
|
}
|
2019-12-01 23:04:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-13 03:44:55 +00:00
|
|
|
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-12-01 23:04:03 +00:00
|
|
|
if err := file.Truncate(0); err != nil {
|
2019-12-08 00:48:26 +00:00
|
|
|
return err
|
2019-12-01 23:04:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure the XML is indented properly.
|
|
|
|
doc.Indent(2)
|
|
|
|
|
2024-03-13 03:44:55 +00:00
|
|
|
// Write the XML to the file.
|
|
|
|
if _, err := doc.WriteTo(file); err != nil {
|
2020-04-25 20:12:17 +00:00
|
|
|
return err
|
|
|
|
}
|
2024-03-13 03:44:55 +00:00
|
|
|
return nil
|
2019-12-01 23:04:03 +00:00
|
|
|
}
|
|
|
|
|
2019-12-01 20:53:47 +00:00
|
|
|
// Parses an ini file.
|
2024-03-13 03:44:55 +00:00
|
|
|
func (f *ConfigurationFile) parseIniFile(file ufs.File) error {
|
|
|
|
// Wrap the file in a NopCloser so the ini package doesn't close the file.
|
|
|
|
cfg, err := ini.Load(io.NopCloser(file))
|
2019-12-01 20:53:47 +00:00
|
|
|
if err != nil {
|
2019-12-08 00:48:26 +00:00
|
|
|
return err
|
2019-12-01 20:53:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, replacement := range f.Replace {
|
2021-11-04 19:24:12 +00:00
|
|
|
var (
|
|
|
|
path []string
|
|
|
|
bracketDepth int
|
|
|
|
v []int32
|
|
|
|
)
|
|
|
|
for _, c := range replacement.Match {
|
|
|
|
switch c {
|
|
|
|
case '[':
|
|
|
|
bracketDepth++
|
|
|
|
case ']':
|
|
|
|
bracketDepth--
|
|
|
|
case '.':
|
|
|
|
if bracketDepth > 0 || len(path) == 1 {
|
|
|
|
v = append(v, c)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
path = append(path, string(v))
|
|
|
|
v = v[:0]
|
|
|
|
default:
|
|
|
|
v = append(v, c)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
path = append(path, string(v))
|
2019-12-01 20:53:47 +00:00
|
|
|
|
2020-04-12 19:22:37 +00:00
|
|
|
value, err := f.LookupConfigurationValue(replacement)
|
2019-12-01 20:53:47 +00:00
|
|
|
if err != nil {
|
2019-12-08 00:48:26 +00:00
|
|
|
return err
|
2019-12-01 20:53:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
k := path[0]
|
|
|
|
s := cfg.Section("")
|
|
|
|
// Passing a key of foo.bar will look for "bar" in the "[foo]" section of the file.
|
|
|
|
if len(path) == 2 {
|
|
|
|
k = path[1]
|
|
|
|
s = cfg.Section(path[0])
|
|
|
|
}
|
|
|
|
|
|
|
|
// If no section was found, create that new section now and then set the
|
|
|
|
// section value we're using to be the new one.
|
|
|
|
if s == nil {
|
|
|
|
s, err = cfg.NewSection(path[0])
|
|
|
|
if err != nil {
|
2019-12-08 00:48:26 +00:00
|
|
|
return err
|
2019-12-01 20:53:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the key exists in the file go ahead and set the value, otherwise try to
|
|
|
|
// create it in the section.
|
|
|
|
if s.HasKey(k) {
|
2020-06-30 03:08:36 +00:00
|
|
|
s.Key(k).SetValue(value)
|
2019-12-01 20:53:47 +00:00
|
|
|
} else {
|
2020-06-30 03:08:36 +00:00
|
|
|
if _, err := s.NewKey(k, value); err != nil {
|
2019-12-08 00:48:26 +00:00
|
|
|
return err
|
2019-12-01 20:53:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-13 03:44:55 +00:00
|
|
|
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := file.Truncate(0); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err := cfg.WriteTo(file); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
2019-12-01 20:53:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Parses a json file updating any matching key/value pairs. If a match is not found, the
|
2019-12-01 19:19:49 +00:00
|
|
|
// value is set regardless in the file. See the commentary in parseYamlFile for more details
|
|
|
|
// about what is happening during this process.
|
2024-03-13 03:44:55 +00:00
|
|
|
func (f *ConfigurationFile) parseJsonFile(file ufs.File) error {
|
|
|
|
b, err := io.ReadAll(file)
|
2019-12-01 06:24:44 +00:00
|
|
|
if err != nil {
|
2019-12-08 00:48:26 +00:00
|
|
|
return err
|
2019-12-01 06:24:44 +00:00
|
|
|
}
|
|
|
|
|
2019-12-01 19:19:49 +00:00
|
|
|
data, err := f.IterateOverJson(b)
|
|
|
|
if err != nil {
|
2019-12-08 00:48:26 +00:00
|
|
|
return err
|
2019-12-01 06:24:44 +00:00
|
|
|
}
|
|
|
|
|
2024-03-13 03:44:55 +00:00
|
|
|
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := file.Truncate(0); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Write the data to the file.
|
|
|
|
if _, err := io.Copy(file, bytes.NewReader(data.BytesIndent("", " "))); err != nil {
|
|
|
|
return errors.Wrap(err, "parser: failed to write properties file to disk")
|
|
|
|
}
|
|
|
|
return nil
|
2019-12-01 19:19:49 +00:00
|
|
|
}
|
2019-12-01 06:24:44 +00:00
|
|
|
|
2019-12-01 19:19:49 +00:00
|
|
|
// Parses a yaml file and updates any matching key/value pairs before persisting
|
|
|
|
// it back to the disk.
|
2024-03-13 03:44:55 +00:00
|
|
|
func (f *ConfigurationFile) parseYamlFile(file ufs.File) error {
|
|
|
|
b, err := io.ReadAll(file)
|
2019-12-01 19:19:49 +00:00
|
|
|
if err != nil {
|
2020-11-28 23:57:10 +00:00
|
|
|
return err
|
2019-12-01 19:19:49 +00:00
|
|
|
}
|
2019-12-01 06:24:44 +00:00
|
|
|
|
2020-05-18 01:22:06 +00:00
|
|
|
i := make(map[string]interface{})
|
|
|
|
if err := yaml.Unmarshal(b, &i); err != nil {
|
2020-11-28 23:57:10 +00:00
|
|
|
return err
|
2020-05-18 01:22:06 +00:00
|
|
|
}
|
|
|
|
|
2019-12-01 19:19:49 +00:00
|
|
|
// Unmarshal the yaml data into a JSON interface such that we can work with
|
|
|
|
// any arbitrary data structure. If we don't do this, I can't use gabs which
|
2020-09-05 19:08:40 +00:00
|
|
|
// makes working with unknown JSON significantly easier.
|
2020-05-18 01:22:06 +00:00
|
|
|
jsonBytes, err := json.Marshal(dyno.ConvertMapI2MapS(i))
|
2019-12-01 19:19:49 +00:00
|
|
|
if err != nil {
|
2020-11-28 23:57:10 +00:00
|
|
|
return err
|
2019-12-01 06:24:44 +00:00
|
|
|
}
|
|
|
|
|
2019-12-01 19:19:49 +00:00
|
|
|
// Now that the data is converted, treat it just like JSON and pass it to the
|
|
|
|
// iterator function to update values as necessary.
|
|
|
|
data, err := f.IterateOverJson(jsonBytes)
|
|
|
|
if err != nil {
|
2020-11-28 23:57:10 +00:00
|
|
|
return err
|
2019-12-01 19:19:49 +00:00
|
|
|
}
|
2019-12-01 06:24:44 +00:00
|
|
|
|
2019-12-01 19:19:49 +00:00
|
|
|
// Remarshal the JSON into YAML format before saving it back to the disk.
|
2020-05-18 01:22:06 +00:00
|
|
|
marshaled, err := yaml.Marshal(data.Data())
|
2019-12-01 19:19:49 +00:00
|
|
|
if err != nil {
|
2020-11-28 23:57:10 +00:00
|
|
|
return err
|
2019-12-01 06:24:44 +00:00
|
|
|
}
|
2019-12-01 19:19:49 +00:00
|
|
|
|
2024-03-13 03:44:55 +00:00
|
|
|
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := file.Truncate(0); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Write the data to the file.
|
|
|
|
if _, err := io.Copy(file, bytes.NewReader(marshaled)); err != nil {
|
|
|
|
return errors.Wrap(err, "parser: failed to write properties file to disk")
|
|
|
|
}
|
|
|
|
return nil
|
2019-12-01 06:24:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
2024-03-13 03:44:55 +00:00
|
|
|
func (f *ConfigurationFile) parseTextFile(file ufs.File) error {
|
|
|
|
b := bytes.NewBuffer(nil)
|
|
|
|
s := bufio.NewScanner(file)
|
|
|
|
var replaced bool
|
|
|
|
for s.Scan() {
|
|
|
|
line := s.Bytes()
|
|
|
|
replaced = false
|
2019-12-01 06:24:44 +00:00
|
|
|
for _, replace := range f.Replace {
|
2020-10-11 22:27:27 +00:00
|
|
|
// If this line doesn't match what we expect for the replacement, move on to the next
|
|
|
|
// line. Otherwise, update the line to have the replacement value.
|
2024-03-13 03:44:55 +00:00
|
|
|
if !bytes.HasPrefix(line, []byte(replace.Match)) {
|
2019-12-01 06:24:44 +00:00
|
|
|
continue
|
|
|
|
}
|
2024-03-13 03:44:55 +00:00
|
|
|
b.Write(replace.ReplaceWith.Bytes())
|
|
|
|
replaced = true
|
2019-12-01 06:24:44 +00:00
|
|
|
}
|
2024-03-13 03:44:55 +00:00
|
|
|
if !replaced {
|
|
|
|
b.Write(line)
|
|
|
|
}
|
|
|
|
b.WriteByte('\n')
|
2019-12-01 06:24:44 +00:00
|
|
|
}
|
|
|
|
|
2024-03-13 03:44:55 +00:00
|
|
|
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := file.Truncate(0); err != nil {
|
2020-11-28 23:57:10 +00:00
|
|
|
return err
|
2019-12-01 02:07:05 +00:00
|
|
|
}
|
|
|
|
|
2024-03-13 03:44:55 +00:00
|
|
|
// Write the data to the file.
|
|
|
|
if _, err := io.Copy(file, b); err != nil {
|
|
|
|
return errors.Wrap(err, "parser: failed to write properties file to disk")
|
|
|
|
}
|
2019-12-01 02:07:05 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-04-03 21:16:00 +00:00
|
|
|
// parsePropertiesFile parses a properties file and updates the values within it
|
|
|
|
// to match those that are passed. Once completed the new file is written to the
|
|
|
|
// disk. This will cause comments not present at the head of the file to be
|
|
|
|
// removed unfortunately.
|
|
|
|
//
|
|
|
|
// Any UTF-8 value will be written back to the disk as their escaped value rather
|
|
|
|
// than the raw value There is no winning with this logic. This fixes a bug where
|
|
|
|
// users with hand rolled UTF-8 escape sequences would have all sorts of pain in
|
|
|
|
// their configurations because we were writing the UTF-8 literal characters which
|
|
|
|
// their games could not actually handle.
|
|
|
|
//
|
|
|
|
// However, by adding this fix to only store the escaped UTF-8 sequence we
|
|
|
|
// unwittingly introduced a "regression" that causes _other_ games to have issues
|
|
|
|
// because they can only handle the unescaped representations. I cannot think of
|
|
|
|
// a simple approach to this problem that doesn't just lead to more complicated
|
|
|
|
// cases and problems.
|
|
|
|
//
|
|
|
|
// So, if your game cannot handle parsing UTF-8 sequences that are escaped into
|
|
|
|
// the string, well, sucks. There are fewer of those games than there are games
|
|
|
|
// that have issues parsing the raw UTF-8 sequence into a string? Also how does
|
|
|
|
// one really know what the user intended at this point? We'd need to know if
|
|
|
|
// the value was escaped or not to begin with before setting it, which I suppose
|
|
|
|
// can work but jesus that is going to be some annoyingly complicated logic?
|
|
|
|
//
|
|
|
|
// @see https://github.com/pterodactyl/panel/issues/2308 (original)
|
|
|
|
// @see https://github.com/pterodactyl/panel/issues/3009 ("bug" introduced as result)
|
2024-03-13 03:44:55 +00:00
|
|
|
func (f *ConfigurationFile) parsePropertiesFile(file ufs.File) error {
|
|
|
|
b, err := io.ReadAll(file)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
s := bytes.NewBuffer(nil)
|
|
|
|
scanner := bufio.NewScanner(bytes.NewReader(b))
|
|
|
|
// Scan until we hit a line that is not a comment that actually has content
|
|
|
|
// on it. Keep appending the comments until that time.
|
|
|
|
for scanner.Scan() {
|
|
|
|
text := scanner.Bytes()
|
|
|
|
if len(text) > 0 && text[0] != '#' {
|
|
|
|
break
|
2020-09-05 20:50:03 +00:00
|
|
|
}
|
2024-03-13 03:44:55 +00:00
|
|
|
s.Write(text)
|
|
|
|
s.WriteByte('\n')
|
|
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
|
|
return errors.WithStackIf(err)
|
2020-09-05 20:50:03 +00:00
|
|
|
}
|
|
|
|
|
2024-03-13 03:44:55 +00:00
|
|
|
p, err := properties.Load(b, properties.UTF8)
|
2019-12-01 02:07:05 +00:00
|
|
|
if err != nil {
|
2021-04-03 21:16:00 +00:00
|
|
|
return errors.Wrap(err, "parser: could not load properties file for configuration update")
|
2019-12-01 02:07:05 +00:00
|
|
|
}
|
|
|
|
|
2020-09-05 20:50:03 +00:00
|
|
|
// Replace any values that need to be replaced.
|
2019-12-01 02:07:05 +00:00
|
|
|
for _, replace := range f.Replace {
|
2020-04-12 19:22:37 +00:00
|
|
|
data, err := f.LookupConfigurationValue(replace)
|
2019-12-01 06:24:44 +00:00
|
|
|
if err != nil {
|
2021-04-03 21:16:00 +00:00
|
|
|
return errors.Wrap(err, "parser: failed to lookup configuration value")
|
2019-12-01 06:24:44 +00:00
|
|
|
}
|
|
|
|
|
2020-04-12 19:22:37 +00:00
|
|
|
v, ok := p.Get(replace.Match)
|
|
|
|
// Don't attempt to replace the value if we're looking for a specific value and
|
|
|
|
// it does not match. If there was no match at all in the file for this key but
|
|
|
|
// we're doing an IfValue match, do nothing.
|
2020-04-12 22:57:07 +00:00
|
|
|
if replace.IfValue != "" && (!ok || (ok && v != replace.IfValue)) {
|
2020-04-12 19:22:37 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2020-06-30 03:08:36 +00:00
|
|
|
if _, _, err := p.Set(replace.Match, data); err != nil {
|
2021-04-03 21:16:00 +00:00
|
|
|
return errors.Wrap(err, "parser: failed to set replacement value")
|
2019-12-01 02:07:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-05 20:50:03 +00:00
|
|
|
// Add the new file content to the string builder.
|
2020-08-07 04:07:56 +00:00
|
|
|
for _, key := range p.Keys() {
|
2020-09-05 20:50:03 +00:00
|
|
|
value, ok := p.Get(key)
|
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
2021-04-03 21:16:00 +00:00
|
|
|
// This escape is intentional!
|
2021-04-03 21:02:37 +00:00
|
|
|
//
|
2021-04-03 21:16:00 +00:00
|
|
|
// See the docblock for this function for more details, do not change this
|
|
|
|
// or you'll cause a flood of new issue reports no one wants to deal with.
|
|
|
|
s.WriteString(key + "=" + strings.Trim(strconv.QuoteToASCII(value), "\"") + "\n")
|
2020-09-05 20:50:03 +00:00
|
|
|
}
|
|
|
|
|
2024-03-13 03:44:55 +00:00
|
|
|
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := file.Truncate(0); err != nil {
|
2020-11-28 23:57:10 +00:00
|
|
|
return err
|
2020-08-07 04:07:56 +00:00
|
|
|
}
|
|
|
|
|
2020-09-05 20:50:03 +00:00
|
|
|
// Write the data to the file.
|
2024-03-13 03:44:55 +00:00
|
|
|
if _, err := io.Copy(file, s); err != nil {
|
2021-04-03 21:16:00 +00:00
|
|
|
return errors.Wrap(err, "parser: failed to write properties file to disk")
|
2020-08-07 04:07:56 +00:00
|
|
|
}
|
|
|
|
return nil
|
2019-12-01 02:07:05 +00:00
|
|
|
}
|