admin: Preserve "@id" fields through partial changes (fixes #2902)

This commit is contained in:
Matthew Holt 2019-11-27 11:49:49 -07:00
parent 8de1a76227
commit 6e10586303
No known key found for this signature in database
GPG Key ID: 2A349DD577D586A5
2 changed files with 33 additions and 11 deletions

View File

@ -27,6 +27,7 @@ import (
"net/url" "net/url"
"os" "os"
"path" "path"
"regexp"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -764,6 +765,12 @@ var (
} }
) )
// idRegexp is used to match ID fields and their associated values
// in the config. It also matches adjacent commas so that syntax
// can be preserved no matter where in the object the field appears.
// It supports string and most numeric values.
var idRegexp = regexp.MustCompile(`(?m),?\s*"` + idKey + `":\s?(-?[0-9]+(\.[0-9]+)?|(?U)".*")\s*,?`)
const ( const (
rawConfigKey = "config" rawConfigKey = "config"
idKey = "@id" idKey = "@id"

View File

@ -102,16 +102,6 @@ func changeConfig(method, path string, input []byte, forceReload bool) error {
return err return err
} }
// find any IDs in this config and index them
idx := make(map[string]string)
err = indexConfigObjects(rawCfg[rawConfigKey], "/"+rawConfigKey, idx)
if err != nil {
return APIError{
Code: http.StatusInternalServerError,
Err: fmt.Errorf("indexing config: %v", err),
}
}
// the mutation is complete, so encode the entire config as JSON // the mutation is complete, so encode the entire config as JSON
newCfg, err := json.Marshal(rawCfg[rawConfigKey]) newCfg, err := json.Marshal(rawCfg[rawConfigKey])
if err != nil { if err != nil {
@ -127,6 +117,32 @@ func changeConfig(method, path string, input []byte, forceReload bool) error {
return nil return nil
} }
// find any IDs in this config and index them
idx := make(map[string]string)
err = indexConfigObjects(rawCfg[rawConfigKey], "/"+rawConfigKey, idx)
if err != nil {
return APIError{
Code: http.StatusInternalServerError,
Err: fmt.Errorf("indexing config: %v", err),
}
}
// remove any @id fields from the JSON, which would cause
// loading to break since the field wouldn't be recognized
// (an alternate way to do this would be to delete them from
// rawCfg as they are indexed, then iterate the index we made
// and add them back after encoding as JSON)
newCfg = idRegexp.ReplaceAllFunc(newCfg, func(in []byte) []byte {
// matches with a comma on both sides (when "@id" property is
// not the first or last in the object) need to keep exactly
// one comma for correct JSON syntax
comma := []byte{','}
if bytes.HasPrefix(in, comma) && bytes.HasSuffix(in, comma) {
return comma
}
return []byte{}
})
// load this new config; if it fails, we need to revert to // load this new config; if it fails, we need to revert to
// our old representation of caddy's actual config // our old representation of caddy's actual config
err = unsyncedDecodeAndRun(newCfg) err = unsyncedDecodeAndRun(newCfg)
@ -183,7 +199,6 @@ func indexConfigObjects(ptr interface{}, configPath string, index map[string]str
default: default:
return fmt.Errorf("%s: %s field must be a string or number", configPath, idKey) return fmt.Errorf("%s: %s field must be a string or number", configPath, idKey)
} }
delete(val, idKey) // field is no longer needed, and will break config if not removed
continue continue
} }
// traverse this object property recursively // traverse this object property recursively