server(filesystem): rebuild everything imaginable
This wonderfully large commit replaces basically everything under the `server/filesystem` package, re-implementing essentially everything. This is related to https://github.com/pterodactyl/wings/security/advisories/GHSA-494h-9924-xww9 If any vulnerabilities related to symlinks persist after this commit, I will be very upset. Signed-off-by: Matthew Penner <me@matthewp.io>
This commit is contained in:
@@ -2,8 +2,6 @@ package parser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -29,24 +27,14 @@ var configMatchRegex = regexp.MustCompile(`{{\s?config\.([\w.-]+)\s?}}`)
|
||||
// matching:
|
||||
//
|
||||
// <Root>
|
||||
// <Property value="testing"/>
|
||||
//
|
||||
// <Property value="testing"/>
|
||||
//
|
||||
// </Root>
|
||||
//
|
||||
// noinspection RegExpRedundantEscape
|
||||
var xmlValueMatchRegex = regexp.MustCompile(`^\[([\w]+)='(.*)'\]$`)
|
||||
|
||||
// Gets the []byte representation of a configuration file to be passed through to other
|
||||
// handler functions. If the file does not currently exist, it will be created.
|
||||
func readFileBytes(path string) ([]byte, error) {
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
return io.ReadAll(file)
|
||||
}
|
||||
|
||||
// Gets the value of a key based on the value type defined.
|
||||
func (cfr *ConfigurationFileReplacement) getKeyValue(value string) interface{} {
|
||||
if cfr.ReplaceWith.Type() == jsonparser.Boolean {
|
||||
|
||||
252
parser/parser.go
252
parser/parser.go
@@ -2,8 +2,8 @@ package parser
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"bytes"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/internal/ufs"
|
||||
)
|
||||
|
||||
// The file parsing options that are available for a server configuration file.
|
||||
@@ -74,6 +75,26 @@ func (cv *ReplaceValue) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
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>")
|
||||
}
|
||||
}
|
||||
|
||||
type ConfigurationParser string
|
||||
|
||||
func (cp ConfigurationParser) String() string {
|
||||
@@ -167,11 +188,12 @@ func (cfr *ConfigurationFileReplacement) UnmarshalJSON(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parses a given configuration file and updates all of the values within as defined
|
||||
// in the API response from the Panel.
|
||||
func (f *ConfigurationFile) Parse(path string, internal bool) error {
|
||||
log.WithField("path", path).WithField("parser", f.Parser.String()).Debug("parsing server configuration file")
|
||||
// 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")
|
||||
|
||||
// What the fuck is going on here?
|
||||
if mb, err := json.Marshal(config.Get()); err != nil {
|
||||
return err
|
||||
} else {
|
||||
@@ -182,56 +204,24 @@ func (f *ConfigurationFile) Parse(path string, internal bool) error {
|
||||
|
||||
switch f.Parser {
|
||||
case Properties:
|
||||
err = f.parsePropertiesFile(path)
|
||||
break
|
||||
err = f.parsePropertiesFile(file)
|
||||
case File:
|
||||
err = f.parseTextFile(path)
|
||||
break
|
||||
err = f.parseTextFile(file)
|
||||
case Yaml, "yml":
|
||||
err = f.parseYamlFile(path)
|
||||
break
|
||||
err = f.parseYamlFile(file)
|
||||
case Json:
|
||||
err = f.parseJsonFile(path)
|
||||
break
|
||||
err = f.parseJsonFile(file)
|
||||
case Ini:
|
||||
err = f.parseIniFile(path)
|
||||
break
|
||||
err = f.parseIniFile(file)
|
||||
case Xml:
|
||||
err = f.parseXmlFile(path)
|
||||
break
|
||||
err = f.parseXmlFile(file)
|
||||
}
|
||||
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// File doesn't exist, we tried creating it, and same error is returned? Pretty
|
||||
// sure this pathway is impossible, but if not, abort here.
|
||||
if internal {
|
||||
return nil
|
||||
}
|
||||
|
||||
b := strings.TrimSuffix(path, filepath.Base(path))
|
||||
if err := os.MkdirAll(b, 0o755); err != nil {
|
||||
return errors.WithMessage(err, "failed to create base directory for missing configuration file")
|
||||
} else {
|
||||
if _, err := os.Create(path); err != nil {
|
||||
return errors.WithMessage(err, "failed to create missing configuration file")
|
||||
}
|
||||
}
|
||||
|
||||
return f.Parse(path, true)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Parses an xml file.
|
||||
func (f *ConfigurationFile) parseXmlFile(path string) error {
|
||||
func (f *ConfigurationFile) parseXmlFile(file ufs.File) error {
|
||||
doc := etree.NewDocument()
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if _, err := doc.ReadFrom(file); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -291,41 +281,27 @@ func (f *ConfigurationFile) parseXmlFile(path string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// If you don't truncate the file you'll end up duplicating the data in there (or just appending
|
||||
// to the end of the file. We don't want to do that.
|
||||
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.Truncate(0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Move the cursor to the start of the file to avoid weird spacing issues.
|
||||
file.Seek(0, 0)
|
||||
|
||||
// Ensure the XML is indented properly.
|
||||
doc.Indent(2)
|
||||
|
||||
// Truncate the file before attempting to write the changes.
|
||||
if err := os.Truncate(path, 0); err != nil {
|
||||
// Write the XML to the file.
|
||||
if _, err := doc.WriteTo(file); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write the XML to the file.
|
||||
_, err = doc.WriteTo(file)
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parses an ini file.
|
||||
func (f *ConfigurationFile) parseIniFile(path string) error {
|
||||
// Ini package can't handle a non-existent file, so handle that automatically here
|
||||
// by creating it if not exists. Then, immediately close the file since we will use
|
||||
// other methods to write the new contents.
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file.Close()
|
||||
|
||||
cfg, err := ini.Load(path)
|
||||
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))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -388,14 +364,24 @@ func (f *ConfigurationFile) parseIniFile(path string) error {
|
||||
}
|
||||
}
|
||||
|
||||
return cfg.SaveTo(path)
|
||||
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
|
||||
}
|
||||
|
||||
// Parses a json file updating any matching key/value pairs. If a match is not found, the
|
||||
// value is set regardless in the file. See the commentary in parseYamlFile for more details
|
||||
// about what is happening during this process.
|
||||
func (f *ConfigurationFile) parseJsonFile(path string) error {
|
||||
b, err := readFileBytes(path)
|
||||
func (f *ConfigurationFile) parseJsonFile(file ufs.File) error {
|
||||
b, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -405,14 +391,24 @@ func (f *ConfigurationFile) parseJsonFile(path string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
output := []byte(data.StringIndent("", " "))
|
||||
return os.WriteFile(path, output, 0o644)
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
b, err := readFileBytes(path)
|
||||
func (f *ConfigurationFile) parseYamlFile(file ufs.File) error {
|
||||
b, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -443,35 +439,56 @@ func (f *ConfigurationFile) parseYamlFile(path string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(path, marshaled, 0o644)
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
input, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(input), "\n")
|
||||
for i, line := range lines {
|
||||
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
|
||||
for _, replace := range f.Replace {
|
||||
// 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.
|
||||
if !strings.HasPrefix(line, replace.Match) {
|
||||
if !bytes.HasPrefix(line, []byte(replace.Match)) {
|
||||
continue
|
||||
}
|
||||
|
||||
lines[i] = replace.ReplaceWith.String()
|
||||
b.Write(replace.ReplaceWith.Bytes())
|
||||
replaced = true
|
||||
}
|
||||
if !replaced {
|
||||
b.Write(line)
|
||||
}
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, []byte(strings.Join(lines, "\n")), 0o644); err != nil {
|
||||
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, b); err != nil {
|
||||
return errors.Wrap(err, "parser: failed to write properties file to disk")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -501,31 +518,29 @@ func (f *ConfigurationFile) parseTextFile(path string) error {
|
||||
//
|
||||
// @see https://github.com/pterodactyl/panel/issues/2308 (original)
|
||||
// @see https://github.com/pterodactyl/panel/issues/3009 ("bug" introduced as result)
|
||||
func (f *ConfigurationFile) parsePropertiesFile(path string) error {
|
||||
var s strings.Builder
|
||||
// Open the file and attempt to load any comments that currenty exist at the start
|
||||
// of the file. This is kind of a hack, but should work for a majority of users for
|
||||
// the time being.
|
||||
if fd, err := os.Open(path); err != nil {
|
||||
return errors.Wrap(err, "parser: could not open file for reading")
|
||||
} else {
|
||||
scanner := bufio.NewScanner(fd)
|
||||
// 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.Text()
|
||||
if len(text) > 0 && text[0] != '#' {
|
||||
break
|
||||
}
|
||||
s.WriteString(text + "\n")
|
||||
}
|
||||
_ = fd.Close()
|
||||
if err := scanner.Err(); err != nil {
|
||||
return errors.WithStackIf(err)
|
||||
}
|
||||
func (f *ConfigurationFile) parsePropertiesFile(file ufs.File) error {
|
||||
b, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p, err := properties.LoadFile(path, properties.UTF8)
|
||||
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
|
||||
}
|
||||
s.Write(text)
|
||||
s.WriteByte('\n')
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return errors.WithStackIf(err)
|
||||
}
|
||||
|
||||
p, err := properties.Load(b, properties.UTF8)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "parser: could not load properties file for configuration update")
|
||||
}
|
||||
@@ -563,17 +578,16 @@ func (f *ConfigurationFile) parsePropertiesFile(path string) error {
|
||||
s.WriteString(key + "=" + strings.Trim(strconv.QuoteToASCII(value), "\"") + "\n")
|
||||
}
|
||||
|
||||
// Open the file for writing.
|
||||
w, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
|
||||
if err != nil {
|
||||
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
// Write the data to the file.
|
||||
if _, err := w.Write([]byte(s.String())); err != nil {
|
||||
return errors.Wrap(err, "parser: failed to write properties file to disk")
|
||||
if err := file.Truncate(0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write the data to the file.
|
||||
if _, err := io.Copy(file, s); err != nil {
|
||||
return errors.Wrap(err, "parser: failed to write properties file to disk")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user