Skip to content

Commit

Permalink
admin: expect quoted ETags (#4879)
Browse files Browse the repository at this point in the history
* expect quoted etags

* admin: Minor refactor of etag facilities

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
  • Loading branch information
jhwz and mholt committed Jul 12, 2022
1 parent 53c4d78 commit ad3a83f
Show file tree
Hide file tree
Showing 3 changed files with 20 additions and 7 deletions.
9 changes: 7 additions & 2 deletions admin.go
Expand Up @@ -21,7 +21,6 @@ import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"expvar"
Expand Down Expand Up @@ -901,6 +900,12 @@ func (h adminHandler) originAllowed(origin *url.URL) bool {
// produce and verify ETags.
func etagHasher() hash.Hash32 { return fnv.New32a() }

// makeEtag returns an Etag header value (including quotes) for
// the given config path and hash of contents at that path.
func makeEtag(path string, hash hash.Hash) string {
return fmt.Sprintf(`"%s %x"`, path, hash.Sum(nil))
}

func handleConfig(w http.ResponseWriter, r *http.Request) error {
switch r.Method {
case http.MethodGet:
Expand All @@ -919,7 +924,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {

// we could consider setting up a sync.Pool for the summed
// hashes to reduce GC pressure.
w.Header().Set("ETag", r.URL.Path+" "+hex.EncodeToString(hash.Sum(nil)))
w.Header().Set("Etag", makeEtag(r.URL.Path, hash))

return nil

Expand Down
8 changes: 4 additions & 4 deletions admin_test.go
Expand Up @@ -15,8 +15,8 @@
package caddy

import (
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"reflect"
"sync"
Expand Down Expand Up @@ -168,7 +168,7 @@ func TestETags(t *testing.T) {
const key = "/" + rawConfigKey + "/apps/foo"

// try update the config with the wrong etag
err := changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}}`), "/"+rawConfigKey+" not_an_etag", false)
err := changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}}`), fmt.Sprintf(`"/%s not_an_etag"`, rawConfigKey), false)
if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed {
t.Fatalf("expected precondition failed; got %v", err)
}
Expand All @@ -180,13 +180,13 @@ func TestETags(t *testing.T) {
}

// do the same update with the correct key
err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}`), key+" "+hex.EncodeToString(hash.Sum(nil)), false)
err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}`), makeEtag(key, hash), false)
if err != nil {
t.Fatalf("expected update to work; got %v", err)
}

// now try another update. The hash should no longer match and we should get precondition failed
err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 2}`), key+" "+hex.EncodeToString(hash.Sum(nil)), false)
err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 2}`), makeEtag(key, hash), false)
if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed {
t.Fatalf("expected precondition failed; got %v", err)
}
Expand Down
10 changes: 9 additions & 1 deletion caddy.go
Expand Up @@ -145,8 +145,16 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force
defer currentCfgMu.Unlock()

if ifMatchHeader != "" {
// expect the first and last character to be quotes
if len(ifMatchHeader) < 2 || ifMatchHeader[0] != '"' || ifMatchHeader[len(ifMatchHeader)-1] != '"' {
return APIError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("malformed If-Match header; expect quoted string"),
}
}

// read out the parts
parts := strings.Fields(ifMatchHeader)
parts := strings.Fields(ifMatchHeader[1 : len(ifMatchHeader)-1])
if len(parts) != 2 {
return APIError{
HTTPStatus: http.StatusBadRequest,
Expand Down

0 comments on commit ad3a83f

Please sign in to comment.