From 89b521223afbb9e9d9676b9e5686264af6d68ca5 Mon Sep 17 00:00:00 2001 From: gOOvER Date: Tue, 19 May 2026 17:39:10 +0200 Subject: [PATCH 1/2] fix: parser correctness fixes (integer precision, nil safety, multi-variable lookup, type coercion, IfValue, XML duplicates) (#179) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: preserve integer precision in YAML parser (UseNumber + normalizeYamlTypes) * fix: multiple parser bugs - UnmarshalJSON: guard against nil pointer panic on missing 'file'/'parser' keys - LookupConfigurationValue: resolve each {{ config.x }} placeholder independently so composite values like '{{ config.a }}:{{ config.b }}' expand correctly - setValueWithSjson: drop implicit string→int coercion in default case to preserve the original value type - parseTextFile: honour if_value condition (was silently ignored before) - parseXmlFile: remove existing attribute before CreateAttr to prevent duplicates on repeated parses * fix: address Copilot review comments --- parser/helpers.go | 51 ++++++++++++++++----------------- parser/parser.go | 72 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 89 insertions(+), 34 deletions(-) diff --git a/parser/helpers.go b/parser/helpers.go index e34013b1..7f2f539b 100644 --- a/parser/helpers.go +++ b/parser/helpers.go @@ -185,11 +185,7 @@ func (cfr *ConfigurationFileReplacement) setValueWithSjson(jsonStr string, path } setValue = value default: - if v, err := strconv.Atoi(value); err == nil { - setValue = v - } else { - setValue = value - } + setValue = value } } @@ -208,29 +204,30 @@ func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplac // 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. - huntPath := configMatchRegex.ReplaceAllString( - configMatchRegex.FindString(cfr.ReplaceWith.String()), "$1", - ) - - var path []string - for _, value := range strings.Split(huntPath, ".") { - path = append(path, strcase.ToSnake(value)) - } - - // Look for the key in the configuration file, and if found return that value to the - // calling function. - match, _, _, err := jsonparser.Get(f.configuration, path...) - if err != nil { - if err != jsonparser.KeyPathNotFoundError { - return string(match), err + var lookupErr error + result := configMatchRegex.ReplaceAllStringFunc(cfr.ReplaceWith.String(), func(placeholder string) string { + if lookupErr != nil { + return placeholder } + keyPath := configMatchRegex.ReplaceAllString(placeholder, "$1") - log.WithFields(log.Fields{"path": path, "filename": f.FileName}).Debug("attempted to load a configuration value that does not exist") + var path []string + for _, part := range strings.Split(keyPath, ".") { + path = append(path, strcase.ToSnake(part)) + } - // If there is no key, keep the original value intact, that way it is obvious there - // is a replace issue at play. - return string(match), nil - } else { - return configMatchRegex.ReplaceAllString(cfr.ReplaceWith.String(), string(match)), nil - } + // Look for the key in the Wings configuration and substitute the placeholder. + match, _, _, err := jsonparser.Get(f.configuration, path...) + if err != nil { + if err != jsonparser.KeyPathNotFoundError { + lookupErr = err + return placeholder + } + log.WithFields(log.Fields{"path": path, "filename": f.FileName}).Debug("attempted to load a configuration value that does not exist") + // Leave placeholder intact so the misconfiguration is visible. + return placeholder + } + return string(match) + }) + return result, lookupErr } diff --git a/parser/parser.go b/parser/parser.go index a0482d86..cd394c67 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -128,18 +128,27 @@ func (f *ConfigurationFile) UnmarshalJSON(data []byte) error { return err } - if err := json.Unmarshal(*m["file"], &f.FileName); err != nil { + fileRaw, ok := m["file"] + if !ok || fileRaw == nil { + return errors.New("parser: configuration file missing required 'file' key") + } + if err := json.Unmarshal(*fileRaw, &f.FileName); err != nil { return err } - if err := json.Unmarshal(*m["parser"], &f.Parser); err != nil { + parserRaw, ok := m["parser"] + if !ok || parserRaw == nil { + return errors.New("parser: configuration file missing required 'parser' key") + } + if err := json.Unmarshal(*parserRaw, &f.Parser); err != nil { return err } - if err := json.Unmarshal(*m["replace"], &f.Replace); err != nil { - log.WithField("file", f.FileName).WithField("error", err).Warn("failed to unmarshal configuration file replacement") - - f.Replace = []ConfigurationFileReplacement{} + f.Replace = []ConfigurationFileReplacement{} + if replaceRaw, ok := m["replace"]; ok && replaceRaw != nil { + if err := json.Unmarshal(*replaceRaw, &f.Replace); err != nil { + log.WithField("file", f.FileName).WithField("error", err).Warn("failed to unmarshal configuration file replacement") + } } // test if "create_file" exists, if not just assume true @@ -292,6 +301,7 @@ func (f *ConfigurationFile) parseXmlFile(file ufs.File) error { k := xmlValueMatchRegex.ReplaceAllString(value, "$1") v := xmlValueMatchRegex.ReplaceAllString(value, "$2") + element.RemoveAttr(k) element.CreateAttr(k, v) } else { element.SetText(value) @@ -462,9 +472,12 @@ func (f *ConfigurationFile) parseYamlFile(file ufs.File) error { } var jsonData interface{} - if err := json.Unmarshal(data, &jsonData); err != nil { + yamlDecoder := json.NewDecoder(bytes.NewReader(data)) + yamlDecoder.UseNumber() + if err := yamlDecoder.Decode(&jsonData); err != nil { return err } + jsonData = normalizeYamlTypes(jsonData) marshaled, err := yaml.Marshal(jsonData) if err != nil { @@ -536,6 +549,43 @@ func (f *ConfigurationFile) parseTomlFile(file ufs.File) error { return nil } +// normalizeYamlTypes converts json.Number values (produced by UseNumber()) into +// proper Go numeric types so that yaml.Marshal writes integers as plain integers +// instead of float64 scientific notation (e.g. 1.5e+18 for large Snowflake IDs). +func normalizeYamlTypes(value interface{}) interface{} { + switch typed := value.(type) { + case map[string]interface{}: + for key, item := range typed { + typed[key] = normalizeYamlTypes(item) + } + return typed + case []interface{}: + for i := range typed { + typed[i] = normalizeYamlTypes(typed[i]) + } + return typed + case json.Number: + s := typed.String() + // Preserve float representation: if the number contains '.', 'e' or 'E' + // it was originally a float and must not be coerced to int64. + if strings.ContainsAny(s, ".eE") { + if floatVal, err := typed.Float64(); err == nil { + return floatVal + } + return s + } + if intVal, err := typed.Int64(); err == nil { + return intVal + } + if floatVal, err := typed.Float64(); err == nil { + return floatVal + } + return s + default: + return value + } +} + func normalizeTomlTypes(value interface{}) interface{} { switch typed := value.(type) { case map[string]interface{}: @@ -585,6 +635,14 @@ func (f *ConfigurationFile) parseTextFile(file ufs.File) error { if !bytes.HasPrefix(line, []byte(replace.Match)) { continue } + // If an if_value is set, only replace when the remainder of the line matches. + // Trim trailing \r\n so Windows line endings do not break the comparison. + if replace.IfValue != "" { + remainder := bytes.TrimRight(bytes.TrimPrefix(line, []byte(replace.Match)), "\r\n") + if string(remainder) != replace.IfValue { + continue + } + } b.Write(replace.ReplaceWith.Bytes()) replaced = true } From a0306ebeccf392b0edd6ba8a351cfc6306b6c965 Mon Sep 17 00:00:00 2001 From: "Michael (Parker) Parker" Date: Wed, 3 Jun 2026 20:49:30 -0500 Subject: [PATCH 2/2] implement ptero 1.12.3 security fix (#180) implement security change from ptero 1.12.3 code cleanup --- parser/helpers.go | 19 ++++++++++++------- parser/parser.go | 18 +++++++++++++++++- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/parser/helpers.go b/parser/helpers.go index 7f2f539b..b536ea68 100644 --- a/parser/helpers.go +++ b/parser/helpers.go @@ -193,7 +193,7 @@ func (cfr *ConfigurationFileReplacement) setValueWithSjson(jsonStr string, path } // Looks up a configuration value on the Daemon given a dot-notated syntax. -func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplacement) (string, error) { +func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplacement) (result string, err error) { // 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. @@ -204,9 +204,8 @@ func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplac // 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. - var lookupErr error - result := configMatchRegex.ReplaceAllStringFunc(cfr.ReplaceWith.String(), func(placeholder string) string { - if lookupErr != nil { + result = configMatchRegex.ReplaceAllStringFunc(cfr.ReplaceWith.String(), func(placeholder string) string { + if err != nil { return placeholder } keyPath := configMatchRegex.ReplaceAllString(placeholder, "$1") @@ -217,17 +216,23 @@ func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplac } // Look for the key in the Wings configuration and substitute the placeholder. - match, _, _, err := jsonparser.Get(f.configuration, path...) + match, dataType, _, err := jsonparser.Get(f.configuration, path...) if err != nil { if err != jsonparser.KeyPathNotFoundError { - lookupErr = err return placeholder } log.WithFields(log.Fields{"path": path, "filename": f.FileName}).Debug("attempted to load a configuration value that does not exist") // Leave placeholder intact so the misconfiguration is visible. return placeholder } + + // Only substitute scalar values, not whole objects or arrays. + if dataType == jsonparser.Object || dataType == jsonparser.Array { + return placeholder + } + return string(match) }) - return result, lookupErr + + return result, err } diff --git a/parser/parser.go b/parser/parser.go index cd394c67..62a6cd53 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -213,13 +213,29 @@ func (cfr *ConfigurationFileReplacement) UnmarshalJSON(data []byte) error { return nil } +type templatableConfig struct { + Docker struct { + Interface string `json:"interface"` + Network struct { + Interface string `json:"interface"` + } `json:"network"` + } `json:"docker"` +} + +func newTemplatableConfig(c *config.Configuration) templatableConfig { + var t templatableConfig + t.Docker.Interface = c.Docker.Network.Interface + t.Docker.Network.Interface = c.Docker.Network.Interface + return t +} + // 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 { + if mb, err := json.Marshal(newTemplatableConfig(config.Get())); err != nil { return err } else { f.configuration = mb