diff --git a/swift.go b/swift.go index 11de54407..9296db423 100644 --- a/swift.go +++ b/swift.go @@ -7,6 +7,9 @@ import ( "crypto/hmac" "crypto/md5" "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding/base64" "encoding/hex" "encoding/json" "fmt" @@ -1908,21 +1911,51 @@ func (c *Connection) ObjectDelete(ctx context.Context, container string, objectN return err } -// ObjectTempUrl returns a temporary URL for an object +// ObjectTempUrl returns a temporary URL for an object using the sha1 hashing function. +// +// The cluster must support the sha1 digest. func (c *Connection) ObjectTempUrl(container string, objectName string, secretKey string, method string, expires time.Time) string { + sig := c.objectTempUrlSignature(container, objectName, secretKey, method, expires, sha1.New) + s := hex.EncodeToString(sig) + + return fmt.Sprintf("%s/%s/%s?temp_url_sig=%s&temp_url_expires=%d", c.StorageUrl, container, objectName, s, expires.Unix()) +} + +// ObjectTempUrlSha256 returns a temporary URL for an object using the sha256 hashing function. +// +// The cluster must support the sha256 digest. +func (c *Connection) ObjectTempUrlSha256(container string, objectName string, secretKey string, method string, expires time.Time) string { + sig := c.objectTempUrlSignature(container, objectName, secretKey, method, expires, sha256.New) + s := hex.EncodeToString(sig) + + return fmt.Sprintf("%s/%s/%s?temp_url_sig=%s&temp_url_expires=%d", c.StorageUrl, container, objectName, s, expires.Unix()) +} + +// ObjectTempUrlSha512 returns a temporary URL for an object using the sha512 hashing function. +// +// The cluster must support the sha512 digest. +func (c *Connection) ObjectTempUrlSha512(container string, objectName string, secretKey string, method string, expires time.Time) string { + sig := c.objectTempUrlSignature(container, objectName, secretKey, method, expires, sha512.New) + // The sha512 digest must be encoded using base64. + s := base64.RawURLEncoding.EncodeToString(sig) + + return fmt.Sprintf("%s/%s/%s?temp_url_sig=sha512:%s&temp_url_expires=%d", c.StorageUrl, container, objectName, s, expires.Unix()) +} + +// objectTempUrlSignature returns a signature for an object using the provided hash function. +func (c *Connection) objectTempUrlSignature(container string, objectName string, secretKey string, method string, expires time.Time, hash func() hash.Hash) []byte { c.authLock.Lock() storageUrl := c.StorageUrl c.authLock.Unlock() if storageUrl == "" { - return "" // Cannot do better without changing the interface + return []byte{} // Cannot do better without changing the interface } - mac := hmac.New(sha1.New, []byte(secretKey)) + mac := hmac.New(hash, []byte(secretKey)) prefix, _ := url.Parse(storageUrl) body := fmt.Sprintf("%s\n%d\n%s/%s/%s", method, expires.Unix(), prefix.Path, container, objectName) mac.Write([]byte(body)) - sig := hex.EncodeToString(mac.Sum(nil)) - return fmt.Sprintf("%s/%s/%s?temp_url_sig=%s&temp_url_expires=%d", c.StorageUrl, container, objectName, sig, expires.Unix()) + return mac.Sum(nil) } // parseResponseStatus parses string like "200 OK" and returns Error. diff --git a/swift_test.go b/swift_test.go index 12f03dddb..7dc949799 100644 --- a/swift_test.go +++ b/swift_test.go @@ -2210,6 +2210,130 @@ func TestTempUrl(t *testing.T) { } } +func TestTempUrlSha256(t *testing.T) { + ctx := context.Background() + c, rollback := makeConnectionWithContainer(t) + defer rollback() + err := c.ObjectPutBytes(ctx, CONTAINER, OBJECT, []byte(CONTENTS), "") + if err != nil { + t.Fatal(err) + } + defer func() { + err = c.ObjectDelete(ctx, CONTAINER, OBJECT) + if err != nil { + t.Fatal(err) + } + }() + + m := swift.Metadata{} + m["temp-url-key"] = SECRET_KEY + err = c.AccountUpdate(ctx, m.AccountHeaders()) + if err != nil { + t.Fatal(err) + } + + expiresTime := time.Now().Add(20 * time.Minute) + tempUrl := c.ObjectTempUrlSha256(CONTAINER, OBJECT, SECRET_KEY, "GET", expiresTime) + + resp, err := http.Get(tempUrl) + if err != nil { + t.Fatal("Failed to retrieve file from temporary url") + } + defer func() { + err := resp.Body.Close() + if err != nil { + t.Error("Close failed", err) + } + }() + + if resp.StatusCode == 401 { + t.Log("Server doesn't support tempurl") + } else if resp.StatusCode != 200 { + t.Fatal("HTTP Error retrieving file from temporary url", resp.StatusCode) + } else { + var content []byte + if content, err = io.ReadAll(resp.Body); err != nil || string(content) != CONTENTS { + t.Error("Bad content", err) + } + + resp, err = http.Post(tempUrl, "image/jpeg", bytes.NewReader([]byte(CONTENTS))) + if err != nil { + t.Fatal("Failed to retrieve file from temporary url") + } + defer func() { + err := resp.Body.Close() + if err != nil { + t.Error("Close failed", err) + } + }() + if resp.StatusCode != 401 { + t.Fatal("Expecting server to forbid access to object") + } + } +} + +func TestTempUrlSha512(t *testing.T) { + ctx := context.Background() + c, rollback := makeConnectionWithContainer(t) + defer rollback() + err := c.ObjectPutBytes(ctx, CONTAINER, OBJECT, []byte(CONTENTS), "") + if err != nil { + t.Fatal(err) + } + defer func() { + err = c.ObjectDelete(ctx, CONTAINER, OBJECT) + if err != nil { + t.Fatal(err) + } + }() + + m := swift.Metadata{} + m["temp-url-key"] = SECRET_KEY + err = c.AccountUpdate(ctx, m.AccountHeaders()) + if err != nil { + t.Fatal(err) + } + + expiresTime := time.Now().Add(20 * time.Minute) + tempUrl := c.ObjectTempUrlSha256(CONTAINER, OBJECT, SECRET_KEY, "GET", expiresTime) + + resp, err := http.Get(tempUrl) + if err != nil { + t.Fatal("Failed to retrieve file from temporary url") + } + defer func() { + err := resp.Body.Close() + if err != nil { + t.Error("Close failed", err) + } + }() + + if resp.StatusCode == 401 { + t.Log("Server doesn't support tempurl") + } else if resp.StatusCode != 200 { + t.Fatal("HTTP Error retrieving file from temporary url", resp.StatusCode) + } else { + var content []byte + if content, err = io.ReadAll(resp.Body); err != nil || string(content) != CONTENTS { + t.Error("Bad content", err) + } + + resp, err = http.Post(tempUrl, "image/jpeg", bytes.NewReader([]byte(CONTENTS))) + if err != nil { + t.Fatal("Failed to retrieve file from temporary url") + } + defer func() { + err := resp.Body.Close() + if err != nil { + t.Error("Close failed", err) + } + }() + if resp.StatusCode != 401 { + t.Fatal("Expecting server to forbid access to object") + } + } +} + func TestQueryInfo(t *testing.T) { ctx := context.Background() c, rollback := makeConnectionAuth(t)