aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md6
-rw-r--r--account.go12
-rw-r--r--capabilities.go1
-rw-r--r--object.go30
-rw-r--r--object_test.go59
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:
diff --git a/account.go b/account.go
index e29561c..21cedc0 100644
--- a/account.go
+++ b/account.go
@@ -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"`
diff --git a/object.go b/object.go
index d9ecb8d..623818a 100644
--- a/object.go
+++ b/object.go
@@ -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)
}