pax_global_header00006660000000000000000000000064137300716310014513gustar00rootroot0000000000000052 comment=59b9f0b2d2b30130019b42b49ee3108b1f9f4493 simples3-0.6.1/000077500000000000000000000000001373007163100132565ustar00rootroot00000000000000simples3-0.6.1/.gitignore000066400000000000000000000005511373007163100152470ustar00rootroot00000000000000### Go ### # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out ### Go Patch ### /vendor/ /Godeps/ ### Code ### # Visual Studio Code - https://code.visualstudio.com/ .settings/ .vscode/ tsconfig.json jsconfig.json .env simples3-0.6.1/LICENSE000066400000000000000000000027451373007163100142730ustar00rootroot00000000000000Copyright (c) 2018, Rohan Verma All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The views and conclusions contained in the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied.simples3-0.6.1/README.md000066400000000000000000000050251373007163100145370ustar00rootroot00000000000000# simples3 : Simple no frills AWS S3 Library using REST with V4 Signing ## Overview [![GoDoc](https://godoc.org/github.com/rhnvrm/simples3?status.svg)](https://godoc.org/github.com/rhnvrm/simples3) [![Go Report Card](https://goreportcard.com/badge/github.com/rhnvrm/simples3)](https://goreportcard.com/report/github.com/rhnvrm/simples3) [![GoCover](https://gocover.io/_badge/github.com/rhnvrm/simples3)](https://gocover.io/_badge/github.com/rhnvrm/simples3) SimpleS3 is a golang library for uploading and deleting objects on S3 buckets using REST API calls or Presigned URLs signed using AWS Signature Version 4. ## Install ```sh go get github.com/rhnvrm/simples3 ``` ## Example ```go testTxt, _ := os.Open("testdata/test.txt") defer testTxt.Close() // Create an instance of the package // You can either create by manually supplying credentials // (preferably using Environment vars) s3 := simples3.New(Region, AWSAccessKey, AWSSecretKey) // or you can use this on an EC2 instance to // obtain credentials from IAM attached to the instance. s3 := simples3.NewUsingIAM(Region) // You can also set a custom endpoint to a compatible s3 instance. s3.SetEndpoint(CustomEndpoint) // Note: Consider adding a testTxt.Seek(0, 0) // in case you have read // the body, as the pointer is shared by the library. // File Upload is as simple as providing the following // details. resp, err := s3.FileUpload(simples3.UploadInput{ Bucket: AWSBucket, ObjectKey: "test.txt", ContentType: "text/plain", FileName: "test.txt", Body: testTxt, }) // Similarly, Files can be deleted. err := s3.FileDelete(simples3.DeleteInput{ Bucket: os.Getenv("AWS_S3_BUCKET"), ObjectKey: "test.txt", }) // You can also download the file. file, _ := s3.FileDownload(simples3.DownloadInput{ Bucket: os.Getenv("AWS_S3_BUCKET"), ObjectKey: "test.txt", }) data, _ := ioutil.ReadAll(file) file.Close() // You can also use this library to generate // Presigned URLs that can for eg. be used to // GET/PUT files on S3 through the browser. var time, _ = time.Parse(time.RFC1123, "Fri, 24 May 2013 00:00:00 GMT") url := s.GeneratePresignedURL(PresignedInput{ Bucket: "examplebucket", ObjectKey: "test.txt", Method: "GET", Timestamp: time, ExpirySeconds: 86400, }) ``` ## Contributing You are more than welcome to contribute to this project. Fork and make a Pull Request, or create an Issue if you see any problem or want to propose a feature. ## Author Rohan Verma ## License MIT. simples3-0.6.1/go.mod000066400000000000000000000000531373007163100143620ustar00rootroot00000000000000module github.com/rhnvrm/simples3 go 1.13 simples3-0.6.1/go.sum000066400000000000000000000000001373007163100143770ustar00rootroot00000000000000simples3-0.6.1/policy.go000066400000000000000000000113721373007163100151100ustar00rootroot00000000000000// LICENSE MIT // Copyright (c) 2018, Rohan Verma // Copyright (c) 2017, L Campbell // forked from: https://github.com/lye/s3/ // For previous license information visit // https://github.com/lye/s3/blob/master/LICENSE package simples3 import ( "bytes" "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "time" ) // UploadConfig generate policies from config // for POST requests to S3 using Signing V4. type UploadConfig struct { // Required BucketName string ObjectKey string ContentType string FileSize int64 // Optional UploadURL string Expiration time.Duration MetaData map[string]string } // UploadPolicies Amazon s3 upload policies type UploadPolicies struct { URL string Form map[string]string } // PolicyJSON is policy rule type PolicyJSON struct { Expiration string `json:"expiration"` Conditions []interface{} `json:"conditions"` } const ( expirationTimeFormat = "2006-01-02T15:04:05Z07:00" amzDateISO8601TimeFormat = "20060102T150405Z" shortTimeFormat = "20060102" algorithm = "AWS4-HMAC-SHA256" serviceName = "s3" defaultUploadURLFormat = "http://%s.s3.amazonaws.com/" // defaultExpirationHour = 1 * time.Hour ) // nowTime mockable time.Now() var nowTime = func() time.Time { return time.Now().UTC() } var newLine = []byte{'\n'} // CreateUploadPolicies creates amazon s3 sigv4 compatible // policy and signing keys with the signature returns the upload policy. // https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/API/sigv4-authentication-HTTPPOST.html func (s3 *S3) CreateUploadPolicies(uploadConfig UploadConfig) (UploadPolicies, error) { nowTime := nowTime() credential := string(s3.buildCredential(nowTime)) data, err := buildUploadSign(nowTime, credential, uploadConfig) if err != nil { return UploadPolicies{}, err } // 1. StringToSign policy := base64.StdEncoding.EncodeToString(data) // 2. Signing Key hash := hmac.New(sha256.New, buildSignature(nowTime, s3.SecretKey, s3.Region, serviceName)) hash.Write([]byte(policy)) // 3. Signature signature := hex.EncodeToString(hash.Sum(nil)) uploadURL := uploadConfig.UploadURL if uploadURL == "" { uploadURL = fmt.Sprintf(defaultUploadURLFormat, uploadConfig.BucketName) } form := map[string]string{ "key": uploadConfig.ObjectKey, "Content-Type": uploadConfig.ContentType, "X-Amz-Credential": credential, "X-Amz-Algorithm": algorithm, "X-Amz-Date": nowTime.Format(amzDateISO8601TimeFormat), "Policy": policy, "X-Amz-Signature": signature, } for k, v := range uploadConfig.MetaData { form[k] = v } return UploadPolicies{ URL: uploadURL, Form: form, }, nil } func buildUploadSign(nowTime time.Time, credential string, uploadConfig UploadConfig) ([]byte, error) { conditions := []interface{}{ map[string]string{"bucket": uploadConfig.BucketName}, map[string]string{"key": uploadConfig.ObjectKey}, map[string]string{"Content-Type": uploadConfig.ContentType}, []interface{}{"content-length-range", uploadConfig.FileSize, uploadConfig.FileSize}, map[string]string{"x-amz-credential": credential}, map[string]string{"x-amz-algorithm": algorithm}, map[string]string{"x-amz-date": nowTime.Format(amzDateISO8601TimeFormat)}, } for k, v := range uploadConfig.MetaData { conditions = append(conditions, map[string]string{k: v}) } expiration := defaultExpirationHour if uploadConfig.Expiration > 0 { expiration = uploadConfig.Expiration } return json.Marshal(&PolicyJSON{ Expiration: nowTime.Add(expiration).Format(expirationTimeFormat), Conditions: conditions, }) } func (s3 S3) buildCredential(nowTime time.Time) []byte { var b bytes.Buffer b.WriteString(s3.AccessKey) b.WriteRune('/') b.WriteString(nowTime.Format(shortTimeFormat)) b.WriteRune('/') b.WriteString(s3.Region) b.WriteRune('/') b.WriteString(serviceName) b.WriteRune('/') b.WriteString("aws4_request") return b.Bytes() } func (s3 S3) buildCredentialWithoutKey(nowTime time.Time) []byte { var b bytes.Buffer b.WriteString(nowTime.Format(shortTimeFormat)) b.WriteRune('/') b.WriteString(s3.Region) b.WriteRune('/') b.WriteString(serviceName) b.WriteRune('/') b.WriteString("aws4_request") return b.Bytes() } func buildSignature(nowTime time.Time, secretAccessKey string, regionName string, serviceName string) []byte { shortTime := nowTime.Format(shortTimeFormat) date := makeHMac([]byte("AWS4"+secretAccessKey), []byte(shortTime)) region := makeHMac(date, []byte(regionName)) service := makeHMac(region, []byte(serviceName)) credentials := makeHMac(service, []byte("aws4_request")) return credentials } func makeHMac(key []byte, data []byte) []byte { hash := hmac.New(sha256.New, key) hash.Write(data) return hash.Sum(nil) } simples3-0.6.1/presigned.go000066400000000000000000000127571373007163100156010ustar00rootroot00000000000000package simples3 import ( "bytes" "crypto/sha256" "encoding/hex" "net/url" "sort" "strconv" "strings" "time" ) const ( defaultPresignedHost = "s3.amazonaws.com" // defaultProtocol = "https://" // ) // PresignedInput is passed to GeneratePresignedURL as a parameter. type PresignedInput struct { Bucket string ObjectKey string Method string Timestamp time.Time ExtraHeaders map[string]string ExpirySeconds int Protocol string Endpoint string } // GeneratePresignedURL creates a Presigned URL that can be used // for Authentication using Query Parameters. // (https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html) func (s3 *S3) GeneratePresignedURL(in PresignedInput) string { var ( nowTime = nowTime() protocol = defaultProtocol endpoint = defaultPresignedHost ) if !in.Timestamp.IsZero() { nowTime = in.Timestamp.UTC() } amzdate := nowTime.Format(amzDateISO8601TimeFormat) // Create cred b := bytes.Buffer{} b.WriteString(s3.AccessKey) b.WriteRune('/') b.Write(s3.buildCredentialWithoutKey(nowTime)) cred := b.Bytes() b.Reset() // Set the protocol as default if not provided. if in.Protocol != "" { protocol = in.Protocol } if in.Endpoint != "" { endpoint = in.Endpoint } // Add host to Headers signedHeaders := map[string][]byte{} for k, v := range in.ExtraHeaders { signedHeaders[k] = []byte(v) } host := bytes.Buffer{} host.WriteString(in.Bucket) host.WriteRune('.') host.WriteString(endpoint) signedHeaders["host"] = host.Bytes() // Start Canonical Request Formation h := sha256.New() // We write the canonical request directly to the SHA256 hash. h.Write([]byte(in.Method)) // HTTP Verb h.Write(newLine) h.Write([]byte{'/'}) h.Write([]byte(in.ObjectKey)) // CanonicalURL h.Write(newLine) // Start QueryString Params (before SignedHeaders) queryString := map[string]string{ "X-Amz-Algorithm": algorithm, "X-Amz-Credential": string(cred), "X-Amz-Date": amzdate, "X-Amz-Expires": strconv.Itoa(in.ExpirySeconds), } // include the x-amz-security-token incase we are using IAM role or AWS STS if s3.Token != "" { queryString["X-Amz-Security-Token"] = s3.Token } // We need to have a sorted order, // for QueryStrings and SignedHeaders sortedQS := make([]string, 0, len(queryString)) for name := range queryString { sortedQS = append(sortedQS, name) } sort.Strings(sortedQS) //sort by key sortedSH := make([]string, 0, len(signedHeaders)) for name := range signedHeaders { sortedSH = append(sortedSH, name) } sort.Strings(sortedSH) //sort by key // Proceed to write canonical query params for _, k := range sortedQS { // HTTP Verb h.Write([]byte(url.QueryEscape(k))) h.Write([]byte{'='}) h.Write([]byte(url.QueryEscape(string(queryString[k])))) h.Write([]byte{'&'}) } h.Write([]byte("X-Amz-SignedHeaders=")) // Add Signed Headers to Query String first := true for i := 0; i < len(sortedSH); i++ { if first { h.Write([]byte(url.QueryEscape(sortedSH[i]))) first = false } else { h.Write([]byte{';'}) h.Write([]byte(url.QueryEscape(sortedSH[i]))) } } h.Write(newLine) // End QueryString Params // Start Canonical Headers for i := 0; i < len(sortedSH); i++ { h.Write([]byte(strings.ToLower(sortedSH[i]))) h.Write([]byte{':'}) h.Write([]byte(strings.TrimSpace(string(signedHeaders[sortedSH[i]])))) h.Write(newLine) } h.Write(newLine) // End Canonical Headers // Start Signed Headers first = true for i := 0; i < len(sortedSH); i++ { if first { h.Write([]byte(url.QueryEscape(sortedSH[i]))) first = false } else { h.Write([]byte{';'}) h.Write([]byte(url.QueryEscape(sortedSH[i]))) } } h.Write(newLine) // End Canonical Headers // Mention Unsigned Payload h.Write([]byte("UNSIGNED-PAYLOAD")) // canonicalReq := h.Bytes() // Start StringToSign b.WriteString(algorithm) b.WriteRune('\n') b.WriteString(amzdate) b.WriteRune('\n') b.Write(s3.buildCredentialWithoutKey(nowTime)) b.WriteRune('\n') hashed := hex.EncodeToString(h.Sum(nil)) b.WriteString(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 b.WriteString(protocol) b.WriteString(in.Bucket) b.WriteRune('.') b.WriteString(endpoint) b.WriteRune('/') b.WriteString(in.ObjectKey) b.WriteRune('?') // We don't need to have a sorted order here, // but just to preserve tests. for i := 0; i < len(sortedQS); i++ { b.WriteString(url.QueryEscape(sortedQS[i])) b.WriteRune('=') b.WriteString(url.QueryEscape(string(queryString[sortedQS[i]]))) b.WriteRune('&') } b.WriteString("X-Amz-SignedHeaders") b.WriteRune('=') first = true for i := 0; i < len(sortedSH); i++ { if first { b.WriteString(url.QueryEscape(sortedSH[i])) first = false } else { b.WriteRune(';') b.WriteString(url.QueryEscape(sortedSH[i])) } } b.WriteString("&X-Amz-Signature=") b.WriteString(signature) return b.String() } simples3-0.6.1/presigned_test.go000066400000000000000000000152701373007163100166310ustar00rootroot00000000000000package 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 := "https://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_Token(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", ) s.SetToken("IQoJb3JpT2luX2VjEPP%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCmFwLXNvdXRoLTEiRzBFAiABaeeW0LZZaqVyQVx8EHfCY9KTLsR0hnw1nDae%2F%2BVDbwIhAKrGP4RYkoPv8x0qFScsp%2FQZZXAYWbspMOMpVEBa1%2FQ3Kr8DCPz%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQARoMOTMyNjk0MjUxNzI3IgxHyURIpz%2FBVH7V0ikqkwMTy9uf3umf7OWghmeDE8fpS7KxXYlTCQdVyC6tHcTQZdZ13qziy0ZgImvJEUz4lFNCszdQWR2jaDjgNGvWEUJ1ODAir7F1gTb%2BSx0PpH8o18yrrTJYCwZe7ZKtViCN2yDKHAk8DN9Ke77fYEl2W%2FLWV3VH9oqwEwUzCh4f6JrluiLW6HaxHcDqu7K6Qk8bhgTVlW5eHBzlyRJtrlmy232auL1m8XAoR01sjnpoCwE0ra1L3QuK7XmC9BIR5bRwMdZFcL0Ai0vzCyX9kd15hhDBRgzKrTNSrBFDaRJ9N%2FV3bZ61RAd%2FkwfQEDBiwUcTdm%2BVDLvxIUfVNmtQj628ZCWi%2BztUAe8Yz8IKpY50nEXr%2BHHX4wtVF2MZQPSOr%2B%2FON3OJYCl6TwVTGWoVGapn9y%2Bj9JOcdnnDuFUJMoJERRWnMNPCadZT68%2B3t30IgmXU4hcSX51olExLeGMSMtfK6LC7YCvMlGG8YxIJAeW5qznc2d9u%2BX7nXjqhvPCyc9hXMv4hXS4rowWnR6gaz6xZuY9fb8TMIK4v%2FQFOusBpv3m9H7b45zUr3o6xYh28GyB5%2F9zW%2FPkfm%2FpysDbwfz3r3G0WLchyE0t4%2BH8YZibj0KwY8rJyAV26u2DzIlp0bmJ%2F7Aaq4wUo%2BgUbhz7NMFUpWuR2ywszf28pdgsRQ4SHAlVQ4rOhx5XGqMREzjFPJo7jRW6uMCSJ8LvrQU38VTpZyrm7yQDCBK2lHwU00O8xTWSDhFXmrqFrCL9P76ZYXh2dCCJm6gPiSU3eGyqGBKDBWFt20lRHLWCyXwiyhGRULg3WLoLDVsjJDRO8xZta8nVxALUZLcteEv%2BE1QGCxVSg1W1WSAGLz8FQ%3D%3D") want := "https://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-Security-Token=IQoJb3JpT2luX2VjEPP%252F%252F%252F%252F%252F%252F%252F%252F%252F%252FwEaCmFwLXNvdXRoLTEiRzBFAiABaeeW0LZZaqVyQVx8EHfCY9KTLsR0hnw1nDae%252F%252BVDbwIhAKrGP4RYkoPv8x0qFScsp%252FQZZXAYWbspMOMpVEBa1%252FQ3Kr8DCPz%252F%252F%252F%252F%252F%252F%252F%252F%252F%252FwEQARoMOTMyNjk0MjUxNzI3IgxHyURIpz%252FBVH7V0ikqkwMTy9uf3umf7OWghmeDE8fpS7KxXYlTCQdVyC6tHcTQZdZ13qziy0ZgImvJEUz4lFNCszdQWR2jaDjgNGvWEUJ1ODAir7F1gTb%252BSx0PpH8o18yrrTJYCwZe7ZKtViCN2yDKHAk8DN9Ke77fYEl2W%252FLWV3VH9oqwEwUzCh4f6JrluiLW6HaxHcDqu7K6Qk8bhgTVlW5eHBzlyRJtrlmy232auL1m8XAoR01sjnpoCwE0ra1L3QuK7XmC9BIR5bRwMdZFcL0Ai0vzCyX9kd15hhDBRgzKrTNSrBFDaRJ9N%252FV3bZ61RAd%252FkwfQEDBiwUcTdm%252BVDLvxIUfVNmtQj628ZCWi%252BztUAe8Yz8IKpY50nEXr%252BHHX4wtVF2MZQPSOr%252B%252FON3OJYCl6TwVTGWoVGapn9y%252Bj9JOcdnnDuFUJMoJERRWnMNPCadZT68%252B3t30IgmXU4hcSX51olExLeGMSMtfK6LC7YCvMlGG8YxIJAeW5qznc2d9u%252BX7nXjqhvPCyc9hXMv4hXS4rowWnR6gaz6xZuY9fb8TMIK4v%252FQFOusBpv3m9H7b45zUr3o6xYh28GyB5%252F9zW%252FPkfm%252FpysDbwfz3r3G0WLchyE0t4%252BH8YZibj0KwY8rJyAV26u2DzIlp0bmJ%252F7Aaq4wUo%252BgUbhz7NMFUpWuR2ywszf28pdgsRQ4SHAlVQ4rOhx5XGqMREzjFPJo7jRW6uMCSJ8LvrQU38VTpZyrm7yQDCBK2lHwU00O8xTWSDhFXmrqFrCL9P76ZYXh2dCCJm6gPiSU3eGyqGBKDBWFt20lRHLWCyXwiyhGRULg3WLoLDVsjJDRO8xZta8nVxALUZLcteEv%252BE1QGCxVSg1W1WSAGLz8FQ%253D%253D&X-Amz-SignedHeaders=host&X-Amz-Signature=29d003f449ae4106d1c4cabaeebf84fc47960ee127e98f1b9132261852250cb4" 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"), Endpoint: os.Getenv("AWS_S3_ENDPOINT"), ObjectKey: "test1.txt", Method: "GET", Timestamp: nowTime(), ExpirySeconds: 3600, }); got == dontwant { t.Errorf("S3.GeneratePresignedURL() = %v, dontwant %v", got, dontwant) } }) } func TestS3_GeneratePresignedURL_ExtraHeader(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"), Endpoint: os.Getenv("AWS_S3_ENDPOINT"), ObjectKey: "test2.txt", Method: "GET", Timestamp: nowTime(), ExpirySeconds: 3600, ExtraHeaders: map[string]string{ "x-amz-meta-test": "test", }, }); got == dontwant { t.Errorf("S3.GeneratePresignedURL() = %v, dontwant %v", got, dontwant) } }) } func TestS3_GeneratePresignedURL_PUT(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"), Endpoint: os.Getenv("AWS_S3_ENDPOINT"), ObjectKey: "test2.txt", Method: "PUT", Timestamp: nowTime(), ExpirySeconds: 3600, }); got == dontwant { t.Errorf("S3.GeneratePresignedURL() = %v, dontwant %v", got, dontwant) } }) } func BenchmarkS3_GeneratePresigned(b *testing.B) { // run the Fib function b.N times s := New( os.Getenv("AWS_S3_REGION"), os.Getenv("AWS_S3_ACCESS_KEY"), os.Getenv("AWS_S3_SECRET_KEY"), ) b.ReportAllocs() b.ResetTimer() 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(), ExpirySeconds: 3600, }) } } simples3-0.6.1/sign.go000066400000000000000000000060051373007163100145460ustar00rootroot00000000000000// LICENSE MIT // Copyright (c) 2018, Rohan Verma // Copyright (C) 2012 Blake Mizerany // contains code from: github.com/bmizerany/aws4 package simples3 import ( "bytes" "crypto/sha256" "fmt" "io" "io/ioutil" "net/http" "net/url" "path/filepath" "sort" "strings" "time" ) func (s3 *S3) signKeys(t time.Time) []byte { h := makeHMac([]byte("AWS4"+s3.SecretKey), []byte(t.Format(shortTimeFormat))) h = makeHMac(h, []byte(s3.Region)) h = makeHMac(h, []byte(serviceName)) h = makeHMac(h, []byte("aws4_request")) return h } func (s3 *S3) writeRequest(w io.Writer, r *http.Request) { r.Header.Set("host", r.Host) w.Write([]byte(r.Method)) w.Write(newLine) writeURI(w, r) w.Write(newLine) writeQuery(w, r) w.Write(newLine) writeHeader(w, r) w.Write(newLine) w.Write(newLine) writeHeaderList(w, r) w.Write(newLine) writeBody(w, r) } func (s3 *S3) writeStringToSign(w io.Writer, t time.Time, r *http.Request) { w.Write([]byte(algorithm)) w.Write(newLine) w.Write([]byte(t.Format(amzDateISO8601TimeFormat))) w.Write(newLine) w.Write([]byte(s3.creds(t))) w.Write(newLine) h := sha256.New() s3.writeRequest(h, r) fmt.Fprintf(w, "%x", h.Sum(nil)) } func (s3 *S3) creds(t time.Time) string { return t.Format(shortTimeFormat) + "/" + s3.Region + "/" + serviceName + "/aws4_request" } func writeURI(w io.Writer, r *http.Request) { path := r.URL.RequestURI() if r.URL.RawQuery != "" { path = path[:len(path)-len(r.URL.RawQuery)-1] } slash := strings.HasSuffix(path, "/") path = filepath.Clean(path) if path != "/" && slash { path += "/" } w.Write([]byte(path)) } func writeQuery(w io.Writer, r *http.Request) { var a []string for k, vs := range r.URL.Query() { k = url.QueryEscape(k) for _, v := range vs { if v == "" { a = append(a, k) } else { v = url.QueryEscape(v) a = append(a, k+"="+v) } } } sort.Strings(a) for i, s := range a { if i > 0 { w.Write([]byte{'&'}) } w.Write([]byte(s)) } } func writeHeader(w io.Writer, r *http.Request) { i, a := 0, make([]string, len(r.Header)) for k, v := range r.Header { sort.Strings(v) a[i] = strings.ToLower(k) + ":" + strings.Join(v, ",") i++ } sort.Strings(a) for i, s := range a { if i > 0 { w.Write(newLine) } io.WriteString(w, s) } } func writeHeaderList(w io.Writer, r *http.Request) { i, a := 0, make([]string, len(r.Header)) for k := range r.Header { a[i] = strings.ToLower(k) i++ } sort.Strings(a) for i, s := range a { if i > 0 { w.Write([]byte{';'}) } w.Write([]byte(s)) } } func writeBody(w io.Writer, r *http.Request) { var ( b []byte err error ) // If the payload is empty, use the empty string as the input to the SHA256 function // http://docs.amazonwebservices.com/general/latest/gr/sigv4-create-canonical-request.html if r.Body == nil { b = []byte("") } else { b, err = ioutil.ReadAll(r.Body) if err != nil { panic(err) } r.Body = ioutil.NopCloser(bytes.NewBuffer(b)) } h := sha256.New() h.Write(b) fmt.Fprintf(w, "%x", h.Sum(nil)) } simples3-0.6.1/simples3.go000066400000000000000000000214151373007163100153470ustar00rootroot00000000000000// LICENSE MIT // Copyright (c) 2018, Rohan Verma package simples3 import ( "bytes" "crypto/hmac" "crypto/sha256" "encoding/json" "encoding/xml" "errors" "fmt" "io" "io/ioutil" "mime/multipart" "net/http" "strings" "time" ) const ( securityCredentialsURL = "http://169.254.169.254/latest/meta-data/iam/security-credentials/" ) // S3 provides a wrapper around your S3 credentials. type S3 struct { AccessKey string SecretKey string Region string Client *http.Client Token string Endpoint string URIFormat string } // DownloadInput is passed to FileUpload as a parameter. type DownloadInput struct { Bucket string ObjectKey string } // UploadInput is passed to FileUpload as a parameter. type UploadInput struct { Bucket string ObjectKey string FileName string ContentType string Body io.ReadSeeker } // UploadResponse receives the following XML // in case of success, since we set a 201 response from S3. // Sample response: // // https://s3.amazonaws.com/link-to-the-file // s3-bucket // development/8614bd40-691b-4668-9241-3b342c6cf429/image.jpg // "32-bit-tag" // type UploadResponse struct { Location string `xml:"Location"` Bucket string `xml:"Bucket"` Key string `xml:"Key"` ETag string `xml:"ETag"` } // DeleteInput is passed to FileDelete as a parameter. type DeleteInput struct { Bucket string ObjectKey string } // IAMResponse is used by NewUsingIAM to auto // detect the credentials type IAMResponse struct { Code string `json:"Code"` LastUpdated string `json:"LastUpdated"` Type string `json:"Type"` AccessKeyID string `json:"AccessKeyId"` SecretAccessKey string `json:"SecretAccessKey"` Token string `json:"Token"` Expiration string `json:"Expiration"` } // New returns an instance of S3. func New(region, accessKey, secretKey string) *S3 { return &S3{ Region: region, AccessKey: accessKey, SecretKey: secretKey, URIFormat: "https://s3.%s.amazonaws.com/%s", } } // NewUsingIAM automatically generates an Instance of S3 // using instance metatdata. func NewUsingIAM(region string) (*S3, error) { return newUsingIAMImpl(securityCredentialsURL, region) } func newUsingIAMImpl(baseURL, region string) (*S3, error) { // Get the IAM role resp, err := http.Get(baseURL) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != 200 { return nil, errors.New(http.StatusText(resp.StatusCode)) } role, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } resp, err = http.Get(baseURL + "/" + string(role)) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != 200 { return nil, errors.New(http.StatusText(resp.StatusCode)) } var jsonResp IAMResponse jsonString, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } if err := json.Unmarshal(jsonString, &jsonResp); err != nil { return nil, err } return &S3{ Region: region, AccessKey: jsonResp.AccessKeyID, SecretKey: jsonResp.SecretAccessKey, Token: jsonResp.Token, URIFormat: "https://s3.%s.amazonaws.com/%s", }, nil } func (s3 *S3) getClient() *http.Client { if s3.Client == nil { return http.DefaultClient } return s3.Client } func (s3 *S3) getURL(bucket string, args ...string) (uri string) { if len(s3.Endpoint) > 0 { uri = s3.Endpoint + "/" + bucket } else { uri = fmt.Sprintf(s3.URIFormat, s3.Region, bucket) } if len(args) > 0 { uri = uri + "/" + strings.Join(args, "/") } return } // SetEndpoint can be used to the set a custom endpoint for // using an alternate instance compatible with the s3 API. // If no protocol is included in the URI, defaults to HTTPS. func (s3 *S3) SetEndpoint(uri string) *S3 { if len(uri) > 0 { if !strings.HasPrefix(uri, "http") { uri = "https://" + uri } s3.Endpoint = uri } return s3 } // SetToken can be used to set a Temporary Security Credential token obtained from // using an IAM role or AWS STS. func (s3 *S3) SetToken(token string) *S3 { if token != "" { s3.Token = token } return s3 } func detectFileSize(body io.Seeker) (int64, error) { pos, err := body.Seek(0, 1) if err != nil { return -1, err } defer body.Seek(pos, 0) n, err := body.Seek(0, 2) if err != nil { return -1, err } return n, nil } // SetClient can be used to set the http client to be // used by the package. If client passed is nil, // http.DefaultClient is used. func (s3 *S3) SetClient(client *http.Client) *S3 { if client != nil { s3.Client = client } else { s3.Client = http.DefaultClient } return s3 } func (s3 *S3) signRequest(req *http.Request) error { var ( err error date = req.Header.Get("Date") t = time.Now().UTC() ) if date != "" { t, err = time.Parse(http.TimeFormat, date) if err != nil { return err } } req.Header.Set("Date", t.Format(amzDateISO8601TimeFormat)) // The x-amz-content-sha256 header is required for all AWS // Signature Version 4 requests. It provides a hash of the // request payload. If there is no payload, you must provide // the hash of an empty string. emptyhash := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" req.Header.Set("x-amz-content-sha256", emptyhash) k := s3.signKeys(t) h := hmac.New(sha256.New, k) s3.writeStringToSign(h, t, req) auth := bytes.NewBufferString(algorithm) auth.Write([]byte(" Credential=" + s3.AccessKey + "/" + s3.creds(t))) auth.Write([]byte{',', ' '}) auth.Write([]byte("SignedHeaders=")) writeHeaderList(auth, req) auth.Write([]byte{',', ' '}) auth.Write([]byte("Signature=" + fmt.Sprintf("%x", h.Sum(nil)))) req.Header.Set("Authorization", auth.String()) return nil } // FileDownload makes a GET call and returns a io.ReadCloser. // After reading the response body, ensure closing the response. func (s3 *S3) FileDownload(u DownloadInput) (io.ReadCloser, error) { req, err := http.NewRequest( http.MethodGet, s3.getURL(u.Bucket, u.ObjectKey), nil, ) if err != nil { return nil, err } if err := s3.signRequest(req); err != nil { return nil, err } res, err := s3.getClient().Do(req) if err != nil { return nil, err } if res.StatusCode != 200 { return nil, fmt.Errorf("status code: %s", res.Status) } return res.Body, nil } // FileUpload makes a POST call with the file written as multipart // and on successful upload, checks for 200 OK. func (s3 *S3) FileUpload(u UploadInput) (UploadResponse, error) { fSize, err := detectFileSize(u.Body) if err != nil { return UploadResponse{}, err } policies, err := s3.CreateUploadPolicies(UploadConfig{ UploadURL: s3.getURL(u.Bucket), BucketName: u.Bucket, ObjectKey: u.ObjectKey, ContentType: u.ContentType, FileSize: fSize, MetaData: map[string]string{ "success_action_status": "201", // returns XML doc on success }, }) if err != nil { return UploadResponse{}, err } var b bytes.Buffer w := multipart.NewWriter(&b) for k, v := range policies.Form { if err = w.WriteField(k, v); err != nil { return UploadResponse{}, err } } fw, err := w.CreateFormFile("file", u.FileName) if err != nil { return UploadResponse{}, err } if _, err = io.Copy(fw, u.Body); err != nil { return UploadResponse{}, err } // Don't forget to close the multipart writer. // If you don't close it, your request will be missing the terminating boundary. if err := w.Close(); err != nil { return UploadResponse{}, err } // Now that you have a form, you can submit it to your handler. req, err := http.NewRequest(http.MethodPost, policies.URL, &b) if err != nil { return UploadResponse{}, err } // Don't forget to set the content type, this will contain the boundary. req.Header.Set("Content-Type", w.FormDataContentType()) // Submit the request client := s3.getClient() res, err := client.Do(req) if err != nil { return UploadResponse{}, err } defer res.Body.Close() 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 } // 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 { req, err := http.NewRequest( http.MethodDelete, s3.getURL(u.Bucket, u.ObjectKey), nil, ) if err != nil { return err } if err := s3.signRequest(req); err != nil { return err } // Submit the request client := s3.getClient() res, err := client.Do(req) if err != nil { return err } // Check the response if res.StatusCode != 204 { return fmt.Errorf("status code: %s", res.Status) } return nil } simples3-0.6.1/simples3_test.go000066400000000000000000000155041373007163100164100ustar00rootroot00000000000000package simples3 import ( "bytes" "io" "io/ioutil" "net/http" "net/http/httptest" "os" "testing" ) type tConfig struct { AccessKey string SecretKey string Endpoint string Region string } func TestS3_FileUpload(t *testing.T) { testTxt, err := os.Open("testdata/test.txt") if err != nil { return } defer testTxt.Close() testPng, err := os.Open("testdata/avatar.png") if err != nil { return } defer testPng.Close() type args struct { u UploadInput } tests := []struct { name string fields tConfig args args wantErr bool }{ { name: "Upload test.txt", 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{ UploadInput{ Bucket: os.Getenv("AWS_S3_BUCKET"), ObjectKey: "test.txt", ContentType: "text/plain", FileName: "test.txt", Body: testTxt, }, }, wantErr: false, }, { name: "Upload avatar.png", 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{ UploadInput{ Bucket: os.Getenv("AWS_S3_BUCKET"), ObjectKey: "xyz/image.png", ContentType: "image/png", FileName: "avatar.png", Body: testPng, }, }, wantErr: false, }, } for _, testcase := range tests { tt := testcase 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) } // check for empty response if (resp == UploadResponse{}) { t.Errorf("S3.FileUpload() returned empty response, %v", resp) } }) } } func TestS3_FileDownload(t *testing.T) { testTxt, err := os.Open("testdata/test.txt") if err != nil { t.Fatal(err) } defer testTxt.Close() testTxtData, err := ioutil.ReadAll(testTxt) if err != nil { t.Fatal(err) } testPng, err := os.Open("testdata/avatar.png") if err != nil { t.Fatal(err) } defer testPng.Close() testPngData, err := ioutil.ReadAll(testPng) if err != nil { t.Fatal(err) } type args struct { u DownloadInput } tests := []struct { name string fields tConfig args args wantErr bool wantResponse []byte }{ { name: "txt", 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{ u: DownloadInput{ Bucket: os.Getenv("AWS_S3_BUCKET"), ObjectKey: "test.txt", }, }, wantErr: false, wantResponse: testTxtData, }, { name: "png", 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{ u: DownloadInput{ Bucket: os.Getenv("AWS_S3_BUCKET"), ObjectKey: "xyz/image.png", }, }, wantErr: false, wantResponse: testPngData, }, } for _, testcase := range tests { tt := testcase 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.FileDownload(tt.args.u) if (err != nil) != tt.wantErr { t.Fatalf("S3.FileDownload() error = %v, wantErr %v", err, tt.wantErr) } got, err := ioutil.ReadAll(resp) if err != nil { t.Fatalf("error = %v", err) } resp.Close() if !bytes.Equal(got, tt.wantResponse) { t.Fatalf("S3.FileDownload() = %v, want %v", got, tt.wantResponse) } }) } } func TestS3_FileDelete(t *testing.T) { type args struct { u DeleteInput } tests := []struct { name string fields tConfig args args wantErr bool }{ { name: "Delete test.txt", 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{ DeleteInput{ Bucket: os.Getenv("AWS_S3_BUCKET"), ObjectKey: "test.txt", }, }, wantErr: false, }, { name: "Delete avatar.png", 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{ DeleteInput{ Bucket: os.Getenv("AWS_S3_BUCKET"), ObjectKey: "xyz/image.png", }, }, wantErr: false, }, } for _, testcase := range tests { tt := testcase 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) } }) } } func TestS3_NewUsingIAM(t *testing.T) { var ( iam = `test-new-s3-using-iam` resp = `{"Code" : "Success","LastUpdated" : "2018-12-24T10:18:01Z", "Type" : "AWS-HMAC","AccessKeyId" : "abc", "SecretAccessKey" : "abc","Token" : "abc", "Expiration" : "2018-12-24T16:24:59Z"}` ) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { t.Errorf("Expected 'GET' request, got '%s'", r.Method) } if r.URL.EscapedPath() == "/" { w.WriteHeader(http.StatusOK) io.WriteString(w, iam) } if r.URL.EscapedPath() == "/"+iam { w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") io.WriteString(w, resp) } })) defer ts.Close() s3, err := newUsingIAMImpl(ts.URL, "abc") if err != nil { t.Errorf("S3.FileDelete() error = %v", err) } if s3.AccessKey != "abc" && s3.SecretKey != "abc" && s3.Region != "abc" { t.Errorf("S3.FileDelete() got = %v", s3) } } func TestCustomEndpoint(t *testing.T) { s3 := New("us-east-1", "AccessKey", "SuperSecretKey") // no protocol specified, should default to https s3.SetEndpoint("example.com") if s3.getURL("bucket1") != "https://example.com/bucket1" { t.Errorf("S3.SetEndpoint() got = %v", s3.Endpoint) } // explicit http protocol s3.SetEndpoint("http://localhost:9000") if s3.getURL("bucket2") != "http://localhost:9000/bucket2" { t.Errorf("S3.SetEndpoint() got = %v", s3.Endpoint) } // explicit http protocol s3.SetEndpoint("https://example.com") if s3.getURL("bucket3") != "https://example.com/bucket3" { t.Errorf("S3.SetEndpoint() got = %v", s3.Endpoint) } } simples3-0.6.1/testdata/000077500000000000000000000000001373007163100150675ustar00rootroot00000000000000simples3-0.6.1/testdata/avatar.png000066400000000000000000000142261373007163100170600ustar00rootroot00000000000000JFIFC       C xx }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ? 1_X sE1 *}(~XQTE%9-Je9(]I i ,qPG\20v>UK_5X[j$n&P8~iV*df|-eԽ=ֿyO[sHd=B rWKTDM~?ŘH7 Za%˸1L8nҁ A"=829@@ 4r܁v>56XwAsTIP%f:o_'ʰ1}=__RWI.#&I b8^d]֋S类YQDeuy$|צ?zj&S*<7&e{&5ivtzꥍNJWÅ2uٟW|"|GHQjvw26c91G|[R|Uz+?=o@7p=;P!q .1hy='@eFx_|-nl$6?>9y٭wC ]f8֊ m|'ks(Xpdz#Pm̾/n!-l?j$sQg}J7_f Ai #=G7md7x_ I՝Sn\ TUc?3ϰ+1kaڌcUFT:yhoo΁\v=h&PM jp"?r HecY@S˃~1׼Yt־O '9 t_,EQ$hV^j2Q{~EE:JFUI=U+Usk[4h2w0HG9?)\t4rFrt_''Эu5 B1nI#oX_Gc, y+$i hGm8 ($]@a@9&vLwM P t.'t}O$BymT,sU0S·m~~.X~"+)FIVG^ƫf2ºݭ>MխёsӊEћOtLb' (𹣮H|I\YqؽCEESܗa{?*b9f\`8*5=^v>o k+_M;P`qk+*0$x~e/7 /{]-Upzl|N9>w(^[Z0)xI6KV ȽkIHCL?w u8PK\73|fa(Sj|M4{= HaqEp ;aqxhbWu4 C@;\gz (>}lK\_FI!tqO' +߿שq4Id^w}UyC419*xT);v^FO8Z\P!@c@!/`@`4 GjrnZ}մ)j7 _|_éX1Prs>XVNgbjۃvV冈 srkdV}?X:M;zWa𱠯ol4$0pEpH2!@׌X)N6g_aГr]WG 7w-om*OO5'x=ixZ;UK-ץ;yOk׈$:-^ 4>#8ڙR~n_ZW4}@Ps3z0lfXc4 gg!RH߂8:cS{1{]r?<,%t=7bTy t0:e83F::ZBTlo)]kԸ.;mUDҹPM&TSQV-| j7NF<}SN>$^A߅;7խ4u|}On9EוeM=Wy.*iJ7Mߗ>C+H)Cǥ6"YfVQ.ꡬT|ĶR(u&w>րﰛ붹1r7c+:UKG#IqjWg5,_i~bIxy,FUy]5cQvF1նp~-F I SO?Zuj59,L=9m}?xb W> O/3KᏌV0.!;*Ȏǘx?.^sscw6?S\ˇ_Jc5Q8;=nXZ6wZ[ZN߿MZ`ho BsAUԔ~ups~ʥ]N*Wk]ۻnMo>./!Ƙ#s"ĊΧ.#ۻ)ԍHʤqXL.Pp读ޑ85? ʨH[$`\d WQvԞOC*pR"Z~ tAGͰ=zb{*FIU3F;t ٔ BI5Zۿ1x&G?Ju# 7B )#^Y}sLc% ]tEttqz49+6y9\}MrT-QǙz?&mνtD2y; :Ú=ISJǯOiKlw&dwR\G8Wq Q/k8o8HbE,o/5 Øǚ1Zxiׅ'i2VO9Y5Kk/hHfż(و@şCxU*OSj2GՋJ3BqI[XtTݭ{p?kwiidI^aq$j U 7%UZ.ĪyeU(nSm{.Vw{ZIu76xm"][Lkr"2Y fXFMoCFvѾly9<%|$q;IܤJN%dM\ƫQkqr&Xjj<>湗gY%|2HSҶnL]jGH!QG(>5G=< T&!,&Y"eU 51zZZ{7Ӌ g8I'Vgc(tKL\%&UUFt^@0n|?pHꢖ)[<=i{ؖ[2v,0zϒ~*u}dqZj {nJ0q$3k\֡-Ozt/ r?ֽ*3yLʥ8/ڔwtX ,[I)%ϒy; THIi[kgZxuyT2Q;ݴ5Wtq> <9-krB&n|@_/n'cqNǖr_Uc1ri>TTbyo*PxwSI(n%ʍ?όa1zUY5)ƜUjJ6K7v Dl0ʭ ;.Tvm$z+kmYٓJ5Pܟ*:;'hV_VgӮclu %L*+ێRG上R^_q*ʶ-G֫m Ksgcwg4$[\@BFO\EXŨ?Ϡɲ긊seʥK^+b cObx1u(*S]ofg:Կ|D6_(nm|TRA_a/?simples3-0.6.1/testdata/test.txt000066400000000000000000000000131373007163100166010ustar00rootroot00000000000000hello world