diff options
| -rw-r--r-- | CHANGELOG.md | 6 | ||||
| -rw-r--r-- | account.go | 12 | ||||
| -rw-r--r-- | capabilities.go | 1 | ||||
| -rw-r--r-- | object.go | 30 | ||||
| -rw-r--r-- | object_test.go | 59 |
5 files changed, 88 insertions, 20 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index aa3a29a..4563931 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# v1.2.0 (TBD) + +Changes: + +- Digest signing now uses sha256 and sha512 (preference in that order) if enabled by swift. + # v1.1.0 (2022-02-07) Bugfixes: @@ -23,6 +23,7 @@ import ( "fmt" "net/http" "regexp" + "sync" ) // Account represents a Swift account. Instances are usually obtained by @@ -34,8 +35,9 @@ type Account struct { baseURL string name string //cache - headers *AccountHeaders - caps *Capabilities + headers *AccountHeaders + caps *Capabilities + capsMutex sync.Mutex } // IsEqualTo returns true if both Account instances refer to the same account. @@ -183,10 +185,10 @@ func (a *Account) Containers() *ContainerIterator { // Capabilities queries the GET /info endpoint of the Swift server providing // this account. Capabilities are cached, so the GET request will only be sent // once during the first call to this method. -// -// WARNING: This method is not thread-safe. Calling it concurrently on the same -// object results in undefined behavior. func (a *Account) Capabilities() (Capabilities, error) { + a.capsMutex.Lock() + defer a.capsMutex.Unlock() + if a.caps != nil { return *a.caps, nil } diff --git a/capabilities.go b/capabilities.go index 62290d9..26a3532 100644 --- a/capabilities.go +++ b/capabilities.go @@ -74,6 +74,7 @@ type Capabilities struct { AccountACLs bool `json:"account_acls"` } `json:"tempauth"` TempURL *struct { + AllowedDigests []string `json:"allowed_digests"` IncomingAllowHeaders []string `json:"incoming_allow_headers"` IncomingRemoveHeaders []string `json:"incoming_remove_headers"` Methods []string `json:"methods"` @@ -23,6 +23,8 @@ import ( "crypto/hmac" "crypto/md5" //nolint:gosec // Etag uses md5 "crypto/sha1" //nolint:gosec // Used by swift + "crypto/sha256" + "crypto/sha512" "encoding/hex" "fmt" "hash" @@ -609,6 +611,16 @@ func (o *Object) URL() (string, error) { }.URL(o.c.a.backend, nil) } +// Returns true if string is contained in slice +func contains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} + // TempURL is like Object.URL, but includes a token with a limited lifetime (as // specified by the `expires` argument) that permits anonymous access to this // object using the given HTTP method. This works only when the tempurl @@ -645,8 +657,24 @@ func (o *Object) TempURL(key, method string, expires time.Time) (string, error) return "", err } + capabilities, err := o.c.a.Capabilities() + if err != nil { + return "", err + } + allowedDigest := capabilities.TempURL.AllowedDigests + + var mac hash.Hash + if contains(allowedDigest, "sha256") { + mac = hmac.New(sha256.New, []byte(key)) + } else if contains(allowedDigest, "sha1") { + mac = hmac.New(sha1.New, []byte(key)) + } else if contains(allowedDigest, "sha512") { + mac = hmac.New(sha512.New, []byte(key)) + } else { + return "", fmt.Errorf("schwift only supports sha1, sha256 and sha512 digests but swift server only supports %s", strings.Join(allowedDigest, ", ")) + } + payload := fmt.Sprintf("%s\n%d\n%s", method, expires.Unix(), u.Path) - mac := hmac.New(sha1.New, []byte(key)) mac.Write([]byte(payload)) signature := hex.EncodeToString(mac.Sum(nil)) diff --git a/object_test.go b/object_test.go index 50bf622..a385434 100644 --- a/object_test.go +++ b/object_test.go @@ -19,12 +19,16 @@ package schwift import ( + "io" "net/http" + "strings" "testing" "time" ) -type tempurlBogusBackend struct{} +type tempurlBogusBackend struct { + mockInfoText string +} func (tempurlBogusBackend) EndpointURL() string { return "https://example.com/v1/AUTH_example/" @@ -32,27 +36,54 @@ func (tempurlBogusBackend) EndpointURL() string { func (tempurlBogusBackend) Clone(newEndpointURL string) Backend { panic("unimplemented") } -func (tempurlBogusBackend) Do(req *http.Request) (*http.Response, error) { +func (tBB tempurlBogusBackend) Do(req *http.Request) (*http.Response, error) { + if req.URL.Path == "/info" { + reader := strings.NewReader(tBB.mockInfoText) + return &http.Response{Body: io.NopCloser(reader)}, nil + } panic("unimplemented") } -func TestObjectTempURL(t *testing.T) { - //setup a bogus backend, account, container and object with exact names to - //reproducibly generate a temp URL - account, err := InitializeAccount(tempurlBogusBackend{}) - if err != nil { - t.Fatal(err.Error()) +func expectString(t *testing.T, expected, actual string) { + if actual != expected { + t.Error("temp URL generation failed") + t.Logf("expected: %s\n", expected) + t.Logf("actual: %s\n", actual) } +} - actualURL, err := account.Container("foo").Object("bar").TempURL("supersecretkey", "GET", time.Unix(1e9, 0)) +func must(t *testing.T, err error) { if err != nil { t.Fatal(err.Error()) } +} + +func TestObjectTempURLSha1Only(t *testing.T) { + //setup a bogus backend, account, container and object with exact names to + //reproducibly generate a temp URL + account, err := InitializeAccount(tempurlBogusBackend{ + mockInfoText: `{ "tempurl": { "allowed_digests": [ "sha1" ]}}`, + }) + must(t, err) + + actualURL, err := account.Container("foo").Object("bar").TempURL("supersecretkey", "GET", time.Unix(1e9, 0)) + must(t, err) expectedURL := "https://example.com/v1/AUTH_example/foo/bar?temp_url_sig=ed44d92005345aee463c884d76d4850ef6d2778d&temp_url_expires=1000000000" - if actualURL != expectedURL { - t.Error("temp URL generation failed") - t.Logf("expected: %s\n", expectedURL) - t.Logf("actual: %s\n", actualURL) - } + expectString(t, expectedURL, actualURL) +} + +func TestObjectTempURL(t *testing.T) { + //setup a bogus backend, account, container and object with exact names to + //reproducibly generate a temp URL + account, err := InitializeAccount(tempurlBogusBackend{ + mockInfoText: `{ "tempurl": { "allowed_digests": [ "sha1", "sha256", "sha512"]}}`, + }) + must(t, err) + + actualURL, err := account.Container("foo").Object("bar").TempURL("supersecretkey", "GET", time.Unix(1e9, 0)) + must(t, err) + + expectedURL := "https://example.com/v1/AUTH_example/foo/bar?temp_url_sig=5fc94a988b502d83e88863774812636ef0133b8aae04b20366fd906bff41189f&temp_url_expires=1000000000" + expectString(t, expectedURL, actualURL) } |
