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:
Matthew Penner
2024-03-12 21:44:55 -06:00
parent 27f3e76c77
commit d1c0ca5260
51 changed files with 3694 additions and 1225 deletions

View File

@@ -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 {

View File

@@ -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
}