feat: Presigned URLs
This commit is contained in:
@@ -18,7 +18,7 @@ defer testTxt.Close()
|
||||
|
||||
s3 := simples3.New(Region, AWSAccessKey, AWSSecretKey)
|
||||
err := s3.FileUpload(simples3.UploadInput{
|
||||
Bucket: "zerodha-testbucket",
|
||||
Bucket: AWSBucket,
|
||||
ObjectKey: "test.txt",
|
||||
ContentType: "text/plain",
|
||||
FileName: "test.txt",
|
||||
|
||||
@@ -51,6 +51,7 @@ const (
|
||||
serviceName = "s3"
|
||||
|
||||
defaultUploadURLFormat = "http://%s.s3.amazonaws.com/" // <bucketName>
|
||||
defaultBucketURLFormat = "%s.s3.amazonaws.com/" // <bucketName>
|
||||
defaultExpirationHour = 1 * time.Hour
|
||||
)
|
||||
|
||||
@@ -130,6 +131,10 @@ func (s3 S3) buildCredential(nowTime time.Time) string {
|
||||
return fmt.Sprintf("%s/%s/%s/%s/%s", s3.AccessKey, nowTime.UTC().Format(shortTimeFormat), s3.Region, serviceName, "aws4_request")
|
||||
}
|
||||
|
||||
func (s3 S3) buildCredentialWithoutKey(nowTime time.Time) string {
|
||||
return fmt.Sprintf("%s/%s/%s/%s", nowTime.UTC().Format(shortTimeFormat), s3.Region, serviceName, "aws4_request")
|
||||
}
|
||||
|
||||
func buildSignature(nowTime time.Time, secretAccessKey string, regionName string, serviceName string) []byte {
|
||||
shortTime := nowTime.UTC().Format(shortTimeFormat)
|
||||
|
||||
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
package simples3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPresignedURLFormat = "%s.s3.amazonaws.com" // <bucket>
|
||||
)
|
||||
|
||||
type PresignedInput struct {
|
||||
Bucket string
|
||||
ObjectKey string
|
||||
Method string
|
||||
Timestamp time.Time
|
||||
ExtraHeaders map[string]string
|
||||
ExpirySeconds int
|
||||
}
|
||||
|
||||
func (s3 *S3) GeneratePresignedURL(in PresignedInput) string {
|
||||
var (
|
||||
nowTime = in.Timestamp.UTC()
|
||||
cred = s3.buildCredential(nowTime)
|
||||
amzdate = nowTime.Format(amzDateISO8601TimeFormat)
|
||||
expiry = strconv.Itoa(in.ExpirySeconds)
|
||||
)
|
||||
|
||||
// Add host to Headers
|
||||
signedHeaders := in.ExtraHeaders
|
||||
if signedHeaders == nil {
|
||||
signedHeaders = map[string]string{}
|
||||
}
|
||||
signedHeaders["host"] = fmt.Sprintf(defaultPresignedURLFormat, in.Bucket)
|
||||
|
||||
// Start Canonical Request Formation
|
||||
h := sha256.New()
|
||||
fmt.Fprintf(h, "%s\n", in.Method) // HTTP Verb
|
||||
fmt.Fprintf(h, "/%s\n", in.ObjectKey) // CanonicalURL
|
||||
|
||||
// Start QueryString Params (before SignedHeaders)
|
||||
queryString := map[string]string{
|
||||
"X-Amz-Algorithm": algorithm,
|
||||
"X-Amz-Credential": cred,
|
||||
"X-Amz-Date": amzdate,
|
||||
"X-Amz-Expires": expiry,
|
||||
}
|
||||
|
||||
// We need to have a sorted order,
|
||||
keysQS := make([]string, 0, len(queryString))
|
||||
for name := range queryString {
|
||||
keysQS = append(keysQS, name)
|
||||
}
|
||||
sort.Strings(keysQS) //sort by key
|
||||
for _, k := range keysQS {
|
||||
fmt.Fprintf(h, "%s=%s&", url.QueryEscape(k), url.QueryEscape(queryString[k])) // HTTP Verb
|
||||
}
|
||||
|
||||
fmt.Fprintf(h, "X-Amz-SignedHeaders=")
|
||||
// Add Signed Headers to Query String
|
||||
first := true
|
||||
for k := range signedHeaders {
|
||||
if first {
|
||||
fmt.Fprintf(h, "%s", url.QueryEscape(k))
|
||||
first = false
|
||||
} else {
|
||||
fmt.Fprintf(h, "%s", url.QueryEscape(";"+k))
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(h, "\n")
|
||||
// End QueryString Params
|
||||
|
||||
// Start Canonical Headers
|
||||
for k, v := range signedHeaders {
|
||||
fmt.Fprintf(h, "%s:%s\n", strings.ToLower(k), strings.TrimSpace(v))
|
||||
}
|
||||
fmt.Fprintf(h, "\n")
|
||||
// End Canonical Headers
|
||||
|
||||
// Start Signed Headers
|
||||
first = true
|
||||
for k := range signedHeaders {
|
||||
if first {
|
||||
fmt.Fprintf(h, "%s", url.QueryEscape(k))
|
||||
first = false
|
||||
} else {
|
||||
fmt.Fprintf(h, "%s", url.QueryEscape(";"+k))
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(h, "\n")
|
||||
// End Canonical Headers
|
||||
|
||||
// Mention Unsigned Payload
|
||||
fmt.Fprintf(h, "UNSIGNED-PAYLOAD")
|
||||
|
||||
// canonicalReq := h.Bytes()
|
||||
// Reset Buffer to create StringToSign
|
||||
var b bytes.Buffer
|
||||
|
||||
// Start StringToSign
|
||||
fmt.Fprintf(&b, "%s\n", algorithm) // Algo
|
||||
fmt.Fprintf(&b, "%s\n", amzdate) // Date
|
||||
fmt.Fprintf(&b, "%s\n", s3.buildCredentialWithoutKey(nowTime)) //Cred
|
||||
|
||||
hashed := hex.EncodeToString(h.Sum(nil))
|
||||
fmt.Fprintf(&b, "%s", hashed)
|
||||
|
||||
stringToSign := b.Bytes()
|
||||
|
||||
// End StringToSign
|
||||
|
||||
// Start Signature Key
|
||||
sigKey := makeHMac(makeHMac(
|
||||
makeHMac(
|
||||
makeHMac(
|
||||
[]byte("AWS4"+s3.SecretKey),
|
||||
[]byte(nowTime.UTC().Format(shortTimeFormat))),
|
||||
[]byte(s3.Region)),
|
||||
[]byte("s3")),
|
||||
[]byte("aws4_request"),
|
||||
)
|
||||
// sigKey gen verified using https://docs.aws.amazon.com/general/latest/gr/signature-v4-examples.html#signature-v4-examples-other
|
||||
// (TODO: add a test using the same, consolidate with signKeys())
|
||||
|
||||
signedStrToSign := makeHMac(sigKey, stringToSign)
|
||||
signature := hex.EncodeToString(signedStrToSign)
|
||||
// End Signature
|
||||
|
||||
// Reset Buffer to create URl
|
||||
b.Reset()
|
||||
|
||||
// Start Generating URL
|
||||
fmt.Fprintf(&b, "http://"+defaultPresignedURLFormat+"/", in.Bucket)
|
||||
fmt.Fprintf(&b, "%s?", in.ObjectKey)
|
||||
|
||||
// We don't need to have a sorted order here,
|
||||
// but just to preserve tests.
|
||||
for _, k := range keysQS {
|
||||
fmt.Fprintf(&b, "%s=%s&", url.QueryEscape(k), url.QueryEscape(queryString[k])) // HTTP Verb
|
||||
}
|
||||
fmt.Fprintf(&b, "%s=", "X-Amz-SignedHeaders")
|
||||
first = true
|
||||
for k := range signedHeaders {
|
||||
if first {
|
||||
fmt.Fprintf(&b, "%s", url.QueryEscape(k))
|
||||
first = false
|
||||
} else {
|
||||
fmt.Fprintf(&b, "%s", url.QueryEscape(";"+k))
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(&b, "&X-Amz-Signature=%s", signature)
|
||||
resultURL := b.String()
|
||||
|
||||
return resultURL
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package simples3
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestS3_GeneratePresignedURL(t *testing.T) {
|
||||
// Params based on
|
||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
|
||||
var time, _ = time.Parse(time.RFC1123, "Fri, 24 May 2013 00:00:00 GMT")
|
||||
t.Run("Test", func(t *testing.T) {
|
||||
s := New(
|
||||
"us-east-1",
|
||||
"AKIAIOSFODNN7EXAMPLE",
|
||||
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||
)
|
||||
want := "http://examplebucket.s3.amazonaws.com/test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130524T000000Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=aeeed9bbccd4d02ee5c0109b86d86835f995330da4c265957d157751f604d404"
|
||||
if got := s.GeneratePresignedURL(PresignedInput{
|
||||
Bucket: "examplebucket",
|
||||
ObjectKey: "test.txt",
|
||||
Method: "GET",
|
||||
Timestamp: time,
|
||||
ExpirySeconds: 86400,
|
||||
}); got != want {
|
||||
t.Errorf("S3.GeneratePresignedURL() = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestS3_GeneratePresignedURL_Personal(t *testing.T) {
|
||||
t.Run("Test", func(t *testing.T) {
|
||||
s := New(
|
||||
os.Getenv("AWS_S3_REGION"),
|
||||
os.Getenv("AWS_S3_ACCESS_KEY"),
|
||||
os.Getenv("AWS_S3_SECRET_KEY"),
|
||||
)
|
||||
dontwant := ""
|
||||
if got := s.GeneratePresignedURL(PresignedInput{
|
||||
Bucket: os.Getenv("AWS_S3_BUCKET"),
|
||||
ObjectKey: "test1.txt",
|
||||
Method: "GET",
|
||||
Timestamp: NowTime(),
|
||||
ExpirySeconds: 3600,
|
||||
}); got == dontwant {
|
||||
t.Errorf("S3.GeneratePresignedURL() = %v, dontwant %v", got, dontwant)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestS3_GeneratePresignedURL_POST(t *testing.T) {
|
||||
t.Run("Test", func(t *testing.T) {
|
||||
s := New(
|
||||
os.Getenv("AWS_S3_REGION"),
|
||||
os.Getenv("AWS_S3_ACCESS_KEY"),
|
||||
os.Getenv("AWS_S3_SECRET_KEY"),
|
||||
)
|
||||
dontwant := ""
|
||||
if got := s.GeneratePresignedURL(PresignedInput{
|
||||
Bucket: os.Getenv("AWS_S3_BUCKET"),
|
||||
ObjectKey: "test2.txt",
|
||||
Method: "PUT",
|
||||
Timestamp: NowTime(),
|
||||
ExpirySeconds: 3600,
|
||||
}); got == dontwant {
|
||||
t.Errorf("S3.GeneratePresignedURL() = %v, dontwant %v", got, dontwant)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -45,7 +45,7 @@ func (s3 *S3) writeRequest(w io.Writer, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s3 *S3) writeStringToSign(w io.Writer, t time.Time, r *http.Request) {
|
||||
w.Write([]byte("AWS4-HMAC-SHA256"))
|
||||
w.Write([]byte(algorithm))
|
||||
w.Write(newLine)
|
||||
w.Write([]byte(t.Format(amzDateISO8601TimeFormat)))
|
||||
w.Write(newLine)
|
||||
|
||||
+1
-1
@@ -170,7 +170,7 @@ func (s3 *S3) FileDelete(u DeleteInput) error {
|
||||
|
||||
s3.writeStringToSign(h, t, req)
|
||||
|
||||
auth := bytes.NewBufferString("AWS4-HMAC-SHA256 ")
|
||||
auth := bytes.NewBufferString(algorithm)
|
||||
auth.Write([]byte("Credential=" + s3.AccessKey + "/" + s3.creds(t)))
|
||||
auth.Write([]byte{',', ' '})
|
||||
auth.Write([]byte("SignedHeaders="))
|
||||
|
||||
+2
-2
@@ -35,7 +35,7 @@ func TestS3_FileUpload(t *testing.T) {
|
||||
},
|
||||
args: args{
|
||||
UploadInput{
|
||||
Bucket: "zerodha-testbucket",
|
||||
Bucket: os.Getenv("AWS_S3_BUCKET"),
|
||||
ObjectKey: "test.txt",
|
||||
ContentType: "text/plain",
|
||||
FileName: "test.txt",
|
||||
@@ -79,7 +79,7 @@ func TestS3_FileDelete(t *testing.T) {
|
||||
},
|
||||
args: args{
|
||||
DeleteInput{
|
||||
Bucket: "zerodha-testbucket",
|
||||
Bucket: os.Getenv("AWS_S3_BUCKET"),
|
||||
ObjectKey: "test.txt",
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user