diff --git a/.gitignore b/.gitignore index 93b09ee..a372b7c 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,6 @@ .settings/ .vscode/ tsconfig.json -jsconfig.json \ No newline at end of file +jsconfig.json + +.env diff --git a/policy.go b/policy.go index 90919f5..33d0357 100644 --- a/policy.go +++ b/policy.go @@ -45,7 +45,7 @@ type PolicyJSON struct { } const ( - expirationTimeFormat = "2006-01-02T15:04:05ZZ07:00" + expirationTimeFormat = "2006-01-02T15:04:05Z07:00" amzDateISO8601TimeFormat = "20060102T150405Z" shortTimeFormat = "20060102" algorithm = "AWS4-HMAC-SHA256" @@ -57,7 +57,7 @@ const ( // NowTime mockable time.Now() var NowTime = func() time.Time { - return time.Now() + return time.Now().UTC() } var newLine = []byte{'\n'} @@ -131,7 +131,7 @@ func (s3 S3) buildCredential(nowTime time.Time) []byte { var b bytes.Buffer b.WriteString(s3.AccessKey) b.WriteRune('/') - b.WriteString(nowTime.UTC().Format(shortTimeFormat)) + b.WriteString(nowTime.Format(shortTimeFormat)) b.WriteRune('/') b.WriteString(s3.Region) b.WriteRune('/') @@ -143,7 +143,7 @@ func (s3 S3) buildCredential(nowTime time.Time) []byte { func (s3 S3) buildCredentialWithoutKey(nowTime time.Time) []byte { var b bytes.Buffer - b.WriteString(nowTime.UTC().Format(shortTimeFormat)) + b.WriteString(nowTime.Format(shortTimeFormat)) b.WriteRune('/') b.WriteString(s3.Region) b.WriteRune('/') @@ -154,7 +154,7 @@ func (s3 S3) buildCredentialWithoutKey(nowTime time.Time) []byte { } func buildSignature(nowTime time.Time, secretAccessKey string, regionName string, serviceName string) []byte { - shortTime := nowTime.UTC().Format(shortTimeFormat) + shortTime := nowTime.Format(shortTimeFormat) date := makeHMac([]byte("AWS4"+secretAccessKey), []byte(shortTime)) region := makeHMac(date, []byte(regionName)) diff --git a/presigned.go b/presigned.go index 2ae960e..3ead23e 100644 --- a/presigned.go +++ b/presigned.go @@ -25,6 +25,7 @@ type PresignedInput struct { ExtraHeaders map[string]string ExpirySeconds int Protocol string + Endpoint string } // GeneratePresignedURL creates a Presigned URL that can be used @@ -32,11 +33,15 @@ type PresignedInput struct { // (https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html) func (s3 *S3) GeneratePresignedURL(in PresignedInput) string { var ( - nowTime = in.Timestamp.UTC() + nowTime = NowTime() - amzdate = nowTime.Format(amzDateISO8601TimeFormat) protocol = defaultProtocol + endpoint = defaultPresignedHost ) + if !in.Timestamp.IsZero() { + nowTime = in.Timestamp.UTC() + } + amzdate := nowTime.Format(amzDateISO8601TimeFormat) // Create cred b := bytes.Buffer{} @@ -50,6 +55,9 @@ func (s3 *S3) GeneratePresignedURL(in PresignedInput) string { if in.Protocol != "" { protocol = in.Protocol } + if in.Endpoint != "" { + endpoint = in.Endpoint + } // Add host to Headers signedHeaders := map[string][]byte{} @@ -59,7 +67,7 @@ func (s3 *S3) GeneratePresignedURL(in PresignedInput) string { host := bytes.Buffer{} host.WriteString(in.Bucket) host.WriteRune('.') - host.WriteString(defaultPresignedHost) + host.WriteString(endpoint) signedHeaders["host"] = host.Bytes() // Start Canonical Request Formation @@ -184,7 +192,7 @@ func (s3 *S3) GeneratePresignedURL(in PresignedInput) string { b.WriteString(protocol) b.WriteString(in.Bucket) b.WriteRune('.') - b.WriteString(defaultPresignedHost) + b.WriteString(endpoint) b.WriteRune('/') b.WriteString(in.ObjectKey) b.WriteRune('?') diff --git a/presigned_test.go b/presigned_test.go index 278e46e..f4607f1 100644 --- a/presigned_test.go +++ b/presigned_test.go @@ -39,6 +39,7 @@ func TestS3_GeneratePresignedURL_Personal(t *testing.T) { dontwant := "" if got := s.GeneratePresignedURL(PresignedInput{ Bucket: os.Getenv("AWS_S3_BUCKET"), + Endpoint: os.Getenv("AWS_S3_ENDPOINT"), ObjectKey: "test1.txt", Method: "GET", Timestamp: NowTime(), @@ -59,6 +60,7 @@ func TestS3_GeneratePresignedURL_ExtraHeader(t *testing.T) { dontwant := "" if got := s.GeneratePresignedURL(PresignedInput{ Bucket: os.Getenv("AWS_S3_BUCKET"), + Endpoint: os.Getenv("AWS_S3_ENDPOINT"), ObjectKey: "test2.txt", Method: "GET", Timestamp: NowTime(), @@ -82,6 +84,7 @@ func TestS3_GeneratePresignedURL_PUT(t *testing.T) { dontwant := "" if got := s.GeneratePresignedURL(PresignedInput{ Bucket: os.Getenv("AWS_S3_BUCKET"), + Endpoint: os.Getenv("AWS_S3_ENDPOINT"), ObjectKey: "test2.txt", Method: "PUT", Timestamp: NowTime(), @@ -105,6 +108,7 @@ func BenchmarkS3_GeneratePresigned(b *testing.B) { for n := 0; n < b.N; n++ { s.GeneratePresignedURL(PresignedInput{ Bucket: os.Getenv("AWS_S3_BUCKET"), + Endpoint: os.Getenv("AWS_S3_ENDPOINT"), ObjectKey: "test.txt", Method: "GET", Timestamp: NowTime(), diff --git a/simples3.go b/simples3.go index 14afca4..e30641f 100644 --- a/simples3.go +++ b/simples3.go @@ -15,6 +15,7 @@ import ( "io/ioutil" "mime/multipart" "net/http" + "strings" "time" ) @@ -29,6 +30,7 @@ type S3 struct { Region string Client *http.Client + Endpoint string URIFormat string } @@ -143,6 +145,27 @@ func (s3 *S3) getClient() *http.Client { return s3.Client } +func (s3 *S3) getURL(bucket string, args ...string) (uri string) { + if len(s3.Endpoint) > 0 { + uri = defaultProtocol + s3.Endpoint + "/" + bucket + } else { + uri = fmt.Sprintf(s3.URIFormat, s3.Region, bucket) + } + + if len(args) > 0 { + uri = uri + "/" + strings.Join(args, "/") + } + return +} + +// SetEndpoint ... +func (s3 *S3) SetEndpoint(uri string) *S3 { + if len(uri) > 0 { + s3.Endpoint = uri + } + return s3 +} + func detectFileSize(body io.Seeker) (int64, error) { pos, err := body.Seek(0, 1) if err != nil { @@ -177,7 +200,7 @@ func (s3 *S3) FileUpload(u UploadInput) (UploadResponse, error) { return UploadResponse{}, err } policies, err := s3.CreateUploadPolicies(UploadConfig{ - UploadURL: fmt.Sprintf(s3.URIFormat, s3.Region, u.Bucket), + UploadURL: s3.getURL(u.Bucket), BucketName: u.Bucket, ObjectKey: u.ObjectKey, ContentType: u.ContentType, @@ -230,15 +253,15 @@ func (s3 *S3) FileUpload(u UploadInput) (UploadResponse, error) { } defer res.Body.Close() - // Check the response - if res.Status != "201 Created" { - return UploadResponse{}, fmt.Errorf("status code: %s", res.Status) - } - data, err := ioutil.ReadAll(res.Body) if err != nil { return UploadResponse{}, err } + // Check the response + if res.StatusCode != 201 { + return UploadResponse{}, fmt.Errorf("status code: %s: %q", res.Status, data) + } + var ur UploadResponse xml.Unmarshal(data, &ur) return ur, nil @@ -247,12 +270,8 @@ func (s3 *S3) FileUpload(u UploadInput) (UploadResponse, error) { // FileDelete makes a DELETE call with the file written as multipart // and on successful upload, checks for 204 No Content. func (s3 *S3) FileDelete(u DeleteInput) error { - url := fmt.Sprintf( - s3.URIFormat+"/%s", - s3.Region, u.Bucket, u.ObjectKey, - ) req, err := http.NewRequest( - "DELETE", url, nil, + "DELETE", s3.getURL(u.Bucket, u.ObjectKey), nil, ) if err != nil { return err @@ -298,7 +317,7 @@ func (s3 *S3) FileDelete(u DeleteInput) error { } // Check the response - if res.Status != "204 No Content" { + if res.StatusCode != 204 { return fmt.Errorf("status code: %s", res.Status) } diff --git a/simples3_test.go b/simples3_test.go index d08e7d5..843eab8 100644 --- a/simples3_test.go +++ b/simples3_test.go @@ -8,6 +8,13 @@ import ( "testing" ) +type tConfig struct { + AccessKey string + SecretKey string + Endpoint string + Region string +} + func TestS3_FileUpload(t *testing.T) { testTxt, err := os.Open("test.txt") if err != nil { @@ -20,25 +27,21 @@ func TestS3_FileUpload(t *testing.T) { } defer testPng.Close() - type fields struct { - AccessKey string - SecretKey string - Region string - } type args struct { u UploadInput } tests := []struct { name string - fields fields + fields tConfig args args wantErr bool }{ { name: "Upload test.txt", - fields: fields{ + fields: tConfig{ AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"), SecretKey: os.Getenv("AWS_S3_SECRET_KEY"), + Endpoint: os.Getenv("AWS_S3_ENDPOINT"), Region: os.Getenv("AWS_S3_REGION"), }, args: args{ @@ -54,9 +57,10 @@ func TestS3_FileUpload(t *testing.T) { }, { name: "Upload avatar.png", - fields: fields{ + fields: tConfig{ AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"), SecretKey: os.Getenv("AWS_S3_SECRET_KEY"), + Endpoint: os.Getenv("AWS_S3_ENDPOINT"), Region: os.Getenv("AWS_S3_REGION"), }, args: args{ @@ -74,6 +78,7 @@ func TestS3_FileUpload(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s3 := New(tt.fields.Region, tt.fields.AccessKey, tt.fields.SecretKey) + s3.SetEndpoint(tt.fields.Endpoint) resp, err := s3.FileUpload(tt.args.u) if (err != nil) != tt.wantErr { t.Errorf("S3.FileUpload() error = %v, wantErr %v", err, tt.wantErr) @@ -87,25 +92,21 @@ func TestS3_FileUpload(t *testing.T) { } func TestS3_FileDelete(t *testing.T) { - type fields struct { - AccessKey string - SecretKey string - Region string - } type args struct { u DeleteInput } tests := []struct { name string - fields fields + fields tConfig args args wantErr bool }{ { name: "Delete test.txt", - fields: fields{ + fields: tConfig{ AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"), SecretKey: os.Getenv("AWS_S3_SECRET_KEY"), + Endpoint: os.Getenv("AWS_S3_ENDPOINT"), Region: os.Getenv("AWS_S3_REGION"), }, args: args{ @@ -120,6 +121,7 @@ func TestS3_FileDelete(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s3 := New(tt.fields.Region, tt.fields.AccessKey, tt.fields.SecretKey) + s3.SetEndpoint(tt.fields.Endpoint) if err := s3.FileDelete(tt.args.u); (err != nil) != tt.wantErr { t.Errorf("S3.FileDelete() error = %v, wantErr %v", err, tt.wantErr) }