/* * * Mint, (C) 2017 Minio, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package main import ( "bytes" "encoding/json" "encoding/xml" "errors" "fmt" "math/rand" "net/http" "os" "strings" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3" log "github.com/sirupsen/logrus" ) const letterBytes = "abcdefghijklmnopqrstuvwxyz01234569" const ( letterIdxBits = 6 // 6 bits to represent a letter index letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits ) const ( PASS = "PASS" // Indicate that a test passed FAIL = "FAIL" // Indicate that a test failed NA = "NA" // Indicate that a test is not applicable ) type ErrorResponse struct { XMLName xml.Name `xml:"Error" json:"-"` Code string Message string BucketName string Key string RequestID string `xml:"RequestId"` HostID string `xml:"HostId"` // Region where the bucket is located. This header is returned // only in HEAD bucket and ListObjects response. Region string // Headers of the returned S3 XML error Headers http.Header `xml:"-" json:"-"` } type mintJSONFormatter struct { } func (f *mintJSONFormatter) Format(entry *log.Entry) ([]byte, error) { data := make(log.Fields, len(entry.Data)) for k, v := range entry.Data { switch v := v.(type) { case error: // Otherwise errors are ignored by `encoding/json` // https://github.com/sirupsen/logrus/issues/137 data[k] = v.Error() default: data[k] = v } } serialized, err := json.Marshal(data) if err != nil { return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err) } return append(serialized, '\n'), nil } // log successful test runs func successLogger(function string, args map[string]interface{}, startTime time.Time) *log.Entry { // calculate the test case duration duration := time.Since(startTime) // log with the fields as per mint fields := log.Fields{"name": "aws-sdk-go", "function": function, "args": args, "duration": duration.Nanoseconds() / 1000000, "status": PASS} return log.WithFields(fields) } // log failed test runs func failureLog(function string, args map[string]interface{}, startTime time.Time, alert string, message string, err error) *log.Entry { // calculate the test case duration duration := time.Since(startTime) var fields log.Fields // log with the fields as per mint if err != nil { fields = log.Fields{"name": "aws-sdk-go", "function": function, "args": args, "duration": duration.Nanoseconds() / 1000000, "status": FAIL, "alert": alert, "message": message, "error": err} } else { fields = log.Fields{"name": "aws-sdk-go", "function": function, "args": args, "duration": duration.Nanoseconds() / 1000000, "status": FAIL, "alert": alert, "message": message} } return log.WithFields(fields) } func randString(n int, src rand.Source, prefix string) string { b := make([]byte, n) // A rand.Int63() generates 63 random bits, enough for letterIdxMax letters! for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; { if remain == 0 { cache, remain = src.Int63(), letterIdxMax } if idx := int(cache & letterIdxMask); idx < len(letterBytes) { b[i] = letterBytes[idx] i-- } cache >>= letterIdxBits remain-- } return prefix + string(b[0:30-len(prefix)]) } func cleanup(s3Client *s3.S3, bucket string, object string, function string, args map[string]interface{}, startTime time.Time, deleteBucket bool) { // Deleting the object, just in case it was created. Will not check for errors. s3Client.DeleteObject(&s3.DeleteObjectInput{ Bucket: aws.String(bucket), Key: aws.String(object), }) if deleteBucket { _, err := s3Client.DeleteBucket(&s3.DeleteBucketInput{ Bucket: aws.String(bucket), }) if err != nil { failureLog(function, args, startTime, "", "AWS SDK Go DeleteBucket Failed", err).Fatal() return } } } func testPresignedPutInvalidHash(s3Client *s3.S3) { startTime := time.Now() function := "PresignedPut" bucket := randString(60, rand.NewSource(time.Now().UnixNano()), "aws-sdk-go-test-") object := "presignedTest" expiry := 1 * time.Minute args := map[string]interface{}{ "bucketName": bucket, "objectName": object, "expiry": expiry, } _, err := s3Client.CreateBucket(&s3.CreateBucketInput{ Bucket: aws.String(bucket), }) if err != nil { failureLog(function, args, startTime, "", "AWS SDK Go CreateBucket Failed", err).Fatal() return } defer cleanup(s3Client, bucket, object, function, args, startTime, true) req, _ := s3Client.PutObjectRequest(&s3.PutObjectInput{ Bucket: aws.String(bucket), Key: aws.String(object), ContentType: aws.String("application/octet-stream"), }) req.HTTPRequest.Header.Set("X-Amz-Content-Sha256", "invalid-sha256") url, err := req.Presign(expiry) if err != nil { failureLog(function, args, startTime, "", "AWS SDK Go presigned Put request creation failed", err).Fatal() return } rreq, err := http.NewRequest("PUT", url, bytes.NewReader([]byte(""))) rreq.Header.Add("X-Amz-Content-Sha256", "invalid-sha256") rreq.Header.Add("Content-Type", "application/octet-stream") resp, err := http.DefaultClient.Do(rreq) if err != nil { failureLog(function, args, startTime, "", "AWS SDK Go presigned put request failed", err).Fatal() return } defer resp.Body.Close() dec := xml.NewDecoder(resp.Body) errResp := ErrorResponse{} err = dec.Decode(&errResp) if err != nil { failureLog(function, args, startTime, "", "AWS SDK Go unmarshalling xml failed", err).Fatal() return } if errResp.Code != "XAmzContentSHA256Mismatch" { failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go presigned PUT expected to fail with XAmzContentSHA256Mismatch but got %v", errResp.Code), errors.New("AWS S3 error code mismatch")).Fatal() return } successLogger(function, args, startTime).Info() } func testListObjects(s3Client *s3.S3) { startTime := time.Now() function := "testListObjects" bucket := randString(60, rand.NewSource(time.Now().UnixNano()), "aws-sdk-go-test-") object1 := "testObject1" object2 := "testObject2" expiry := 1 * time.Minute args := map[string]interface{}{ "bucketName": bucket, "objectName1": object1, "objectName2": object2, "expiry": expiry, } getKeys := func(objects []*s3.Object) []string { var rv []string for _, obj := range objects { rv = append(rv, *obj.Key) } return rv } _, err := s3Client.CreateBucket(&s3.CreateBucketInput{ Bucket: aws.String(bucket), }) if err != nil { failureLog(function, args, startTime, "", "AWS SDK Go CreateBucket Failed", err).Fatal() return } defer cleanup(s3Client, bucket, object1, function, args, startTime, true) defer cleanup(s3Client, bucket, object2, function, args, startTime, false) listInput := &s3.ListObjectsV2Input{ Bucket: aws.String(bucket), MaxKeys: aws.Int64(1000), Prefix: aws.String(""), } result, err := s3Client.ListObjectsV2(listInput) if err != nil { failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go listobjects expected to success but got %v", err), err).Fatal() return } if *result.KeyCount != 0 { failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go listobjects with prefix '' expected 0 key but got %v, %v", result.KeyCount, getKeys(result.Contents)), errors.New("AWS S3 key count mismatch")).Fatal() return } putInput1 := &s3.PutObjectInput{ Body: aws.ReadSeekCloser(strings.NewReader("filetoupload")), Bucket: aws.String(bucket), Key: aws.String(object1), } _, err = s3Client.PutObject(putInput1) if err != nil { failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUT expected to success but got %v", err), err).Fatal() return } putInput2 := &s3.PutObjectInput{ Body: aws.ReadSeekCloser(strings.NewReader("filetoupload")), Bucket: aws.String(bucket), Key: aws.String(object2), } _, err = s3Client.PutObject(putInput2) if err != nil { failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUT expected to success but got %v", err), err).Fatal() return } result, err = s3Client.ListObjectsV2(listInput) if err != nil { failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go listobjects expected to success but got %v", err), err).Fatal() return } if *result.KeyCount != 2 { failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go listobjects with prefix '' expected 2 key but got %v, %v", *result.KeyCount, getKeys(result.Contents)), errors.New("AWS S3 key count mismatch")).Fatal() return } successLogger(function, args, startTime).Info() } func testSelectObject(s3Client *s3.S3) { startTime := time.Now() function := "testSelectObject" bucket := randString(60, rand.NewSource(time.Now().UnixNano()), "aws-sdk-go-test-") object1 := "object1.csv" object2 := "object2.csv" args := map[string]interface{}{ "bucketName": bucket, "objectName1": object1, "objectName2": object2, } _, err := s3Client.CreateBucket(&s3.CreateBucketInput{ Bucket: aws.String(bucket), }) if err != nil { failureLog(function, args, startTime, "", "AWS SDK Go CreateBucket Failed", err).Fatal() return } // Test comma field separator inputCsv1 := `year,gender,ethnicity,firstname,count,rank 2011,FEMALE,ASIAN AND PACIFIC ISLANDER,SOPHIA,119,1 2011,FEMALE,ASIAN AND PACIFIC ISLANDER,CHLOE,106,2 2011,FEMALE,ASIAN AND PACIFIC ISLANDER,EMILY,93,3 2011,FEMALE,ASIAN AND PACIFIC ISLANDER,OLIVIA,89,4 2011,FEMALE,ASIAN AND PACIFIC ISLANDER,EMMA,75,5 2011,FEMALE,ASIAN AND PACIFIC ISLANDER,ISABELLA,67,6 2011,FEMALE,ASIAN AND PACIFIC ISLANDER,TIFFANY,54,7 2011,FEMALE,ASIAN AND PACIFIC ISLANDER,ASHLEY,52,8 2011,FEMALE,ASIAN AND PACIFIC ISLANDER,FIONA,48,9 2011,FEMALE,ASIAN AND PACIFIC ISLANDER,ANGELA,47,10 ` outputCSV1 := `2011 2011 2011 2011 2011 2011 2011 2011 2011 2011 ` putInput1 := &s3.PutObjectInput{ Body: aws.ReadSeekCloser(strings.NewReader(inputCsv1)), Bucket: aws.String(bucket), Key: aws.String(object1), } _, err = s3Client.PutObject(putInput1) defer cleanup(s3Client, bucket, object1, function, args, startTime, true) params := &s3.SelectObjectContentInput{ Bucket: &bucket, Key: &object1, ExpressionType: aws.String(s3.ExpressionTypeSql), Expression: aws.String("SELECT s._1 FROM S3Object s"), RequestProgress: &s3.RequestProgress{}, InputSerialization: &s3.InputSerialization{ CompressionType: aws.String("NONE"), CSV: &s3.CSVInput{ FileHeaderInfo: aws.String(s3.FileHeaderInfoIgnore), FieldDelimiter: aws.String(","), RecordDelimiter: aws.String("\n"), }, }, OutputSerialization: &s3.OutputSerialization{ CSV: &s3.CSVOutput{}, }, } resp, err := s3Client.SelectObjectContent(params) if err != nil { failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go Select object failed %v", err), err).Fatal() return } defer resp.EventStream.Close() payload := "" for event := range resp.EventStream.Events() { switch v := event.(type) { case *s3.RecordsEvent: // s3.RecordsEvent.Records is a byte slice of select records payload = string(v.Payload) } } if err := resp.EventStream.Err(); err != nil { failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go Select object failed %v", err), err).Fatal() return } if payload != outputCSV1 { failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go Select object output mismatch %v", payload), errors.New("AWS S3 select object mismatch")).Fatal() return } // Test unicode field separator inputCsv2 := `"year"╦"gender"╦"ethnicity"╦"firstname"╦"count"╦"rank" "2011"╦"FEMALE"╦"ASIAN AND PACIFIC ISLANDER"╦"SOPHIA"╦"119"╦"1" "2011"╦"FEMALE"╦"ASIAN AND PACIFIC ISLANDER"╦"CHLOE"╦"106"╦"2" "2011"╦"FEMALE"╦"ASIAN AND PACIFIC ISLANDER"╦"EMILY"╦"93"╦"3" "2011"╦"FEMALE"╦"ASIAN AND PACIFIC ISLANDER"╦"OLIVIA"╦"89"╦"4" "2011"╦"FEMALE"╦"ASIAN AND PACIFIC ISLANDER"╦"EMMA"╦"75"╦"5" "2011"╦"FEMALE"╦"ASIAN AND PACIFIC ISLANDER"╦"ISABELLA"╦"67"╦"6" "2011"╦"FEMALE"╦"ASIAN AND PACIFIC ISLANDER"╦"TIFFANY"╦"54"╦"7" "2011"╦"FEMALE"╦"ASIAN AND PACIFIC ISLANDER"╦"ASHLEY"╦"52"╦"8" "2011"╦"FEMALE"╦"ASIAN AND PACIFIC ISLANDER"╦"FIONA"╦"48"╦"9" "2011"╦"FEMALE"╦"ASIAN AND PACIFIC ISLANDER"╦"ANGELA"╦"47"╦"10" ` outputCSV2 := `2011 2011 2011 2011 2011 2011 2011 2011 2011 2011 ` putInput2 := &s3.PutObjectInput{ Body: aws.ReadSeekCloser(strings.NewReader(inputCsv2)), Bucket: aws.String(bucket), Key: aws.String(object2), } _, err = s3Client.PutObject(putInput2) defer cleanup(s3Client, bucket, object2, function, args, startTime, false) params2 := &s3.SelectObjectContentInput{ Bucket: &bucket, Key: &object2, ExpressionType: aws.String(s3.ExpressionTypeSql), Expression: aws.String("SELECT s._1 FROM S3Object s"), RequestProgress: &s3.RequestProgress{}, InputSerialization: &s3.InputSerialization{ CompressionType: aws.String("NONE"), CSV: &s3.CSVInput{ FileHeaderInfo: aws.String(s3.FileHeaderInfoIgnore), FieldDelimiter: aws.String("╦"), RecordDelimiter: aws.String("\n"), }, }, OutputSerialization: &s3.OutputSerialization{ CSV: &s3.CSVOutput{}, }, } resp, err = s3Client.SelectObjectContent(params2) if err != nil { failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go Select object failed for unicode separator %v", err), err).Fatal() return } defer resp.EventStream.Close() for event := range resp.EventStream.Events() { switch v := event.(type) { case *s3.RecordsEvent: // s3.RecordsEvent.Records is a byte slice of select records payload = string(v.Payload) } } if err := resp.EventStream.Err(); err != nil { failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go Select object failed for unicode separator %v", err), err).Fatal() return } if payload != outputCSV2 { failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go Select object output mismatch %v", payload), errors.New("AWS S3 select object mismatch")).Fatal() return } successLogger(function, args, startTime).Info() } func main() { endpoint := os.Getenv("SERVER_ENDPOINT") accessKey := os.Getenv("ACCESS_KEY") secretKey := os.Getenv("SECRET_KEY") secure := os.Getenv("ENABLE_HTTPS") sdkEndpoint := "http://" + endpoint if secure == "1" { sdkEndpoint = "https://" + endpoint } creds := credentials.NewStaticCredentials(accessKey, secretKey, "") newSession := session.New() s3Config := &aws.Config{ Credentials: creds, Endpoint: aws.String(sdkEndpoint), Region: aws.String("us-east-1"), S3ForcePathStyle: aws.Bool(true), } // Create an S3 service object in the default region. s3Client := s3.New(newSession, s3Config) // Output to stdout instead of the default stderr log.SetOutput(os.Stdout) // create custom formatter mintFormatter := mintJSONFormatter{} // set custom formatter log.SetFormatter(&mintFormatter) // log Info or above -- success cases are Info level, failures are Fatal level log.SetLevel(log.InfoLevel) // execute tests testPresignedPutInvalidHash(s3Client) testListObjects(s3Client) testSelectObject(s3Client) }