feat: Presigned URLs

This commit is contained in:
Rohan Verma
2018-12-24 13:36:29 +05:30
parent 41b1c70183
commit d218b23f05
7 changed files with 242 additions and 5 deletions
+1 -1
View File
@@ -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",
+5
View File
@@ -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
View File
@@ -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
}
+70
View File
@@ -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)
}
})
}
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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",
},
},