From f259ed52bb3764ce4fd5d88f1712cb43247c2639 Mon Sep 17 00:00:00 2001 From: jhwz <52683873+jhwz@users.noreply.github.com> Date: Thu, 7 Jul 2022 07:50:07 +1200 Subject: admin: support ETag on config endpoints (#4579) * admin: support ETags * support etags Co-authored-by: Matt Holt --- caddy.go | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) (limited to 'caddy.go') diff --git a/caddy.go b/caddy.go index d155092..0c6dfcd 100644 --- a/caddy.go +++ b/caddy.go @@ -17,6 +17,7 @@ package caddy import ( "bytes" "context" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -111,7 +112,7 @@ func Load(cfgJSON []byte, forceReload bool) error { } }() - err := changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, forceReload) + err := changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, "", forceReload) if errors.Is(err, errSameConfig) { err = nil // not really an error } @@ -125,7 +126,12 @@ func Load(cfgJSON []byte, forceReload bool) error { // occur unless forceReload is true. If the config is unchanged and not // forcefully reloaded, then errConfigUnchanged This function is safe for // concurrent use. -func changeConfig(method, path string, input []byte, forceReload bool) error { +// The ifMatchHeader can optionally be given a string of the format: +// " " +// where is the absolute path in the config and is the expected hash of +// the config at that path. If the hash in the ifMatchHeader doesn't match +// the hash of the config, then an APIError with status 412 will be returned. +func changeConfig(method, path string, input []byte, ifMatchHeader string, forceReload bool) error { switch method { case http.MethodGet, http.MethodHead, @@ -138,6 +144,32 @@ func changeConfig(method, path string, input []byte, forceReload bool) error { currentCfgMu.Lock() defer currentCfgMu.Unlock() + if ifMatchHeader != "" { + // read out the parts + parts := strings.Fields(ifMatchHeader) + if len(parts) != 2 { + return APIError{ + HTTPStatus: http.StatusBadRequest, + Err: fmt.Errorf("malformed If-Match header; expect format \" \""), + } + } + + // get the current hash of the config + // at the given path + hash := etagHasher() + err := unsyncedConfigAccess(http.MethodGet, parts[0], nil, hash) + if err != nil { + return err + } + + if hex.EncodeToString(hash.Sum(nil)) != parts[1] { + return APIError{ + HTTPStatus: http.StatusPreconditionFailed, + Err: fmt.Errorf("If-Match header did not match current config hash"), + } + } + } + err := unsyncedConfigAccess(method, path, input, nil) if err != nil { return err @@ -500,7 +532,7 @@ func finishSettingUp(ctx Context, cfg *Config) error { runLoadedConfig := func(config []byte) error { logger.Info("applying dynamically-loaded config") - err := changeConfig(http.MethodPost, "/"+rawConfigKey, config, false) + err := changeConfig(http.MethodPost, "/"+rawConfigKey, config, "", false) if errors.Is(err, errSameConfig) { return err } -- cgit v1.2.3