1package s3
2
3// Source: https://github.com/pivotal-golang/s3cli
4
5// Copyright (c) 2013 Damien Le Berrigaud and Nick Wade
6
7// Permission is hereby granted, free of charge, to any person obtaining a copy
8// of this software and associated documentation files (the "Software"), to deal
9// in the Software without restriction, including without limitation the rights
10// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11// copies of the Software, and to permit persons to whom the Software is
12// furnished to do so, subject to the following conditions:
13
14// The above copyright notice and this permission notice shall be included in
15// all copies or substantial portions of the Software.
16
17// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23// THE SOFTWARE.
24
25import (
26	"crypto/hmac"
27	"crypto/sha1"
28	"encoding/base64"
29	"net/http"
30	"net/url"
31	"sort"
32	"strings"
33	"time"
34
35	"github.com/aws/aws-sdk-go/aws/corehandlers"
36	"github.com/aws/aws-sdk-go/aws/credentials"
37	"github.com/aws/aws-sdk-go/aws/request"
38	"github.com/aws/aws-sdk-go/service/s3"
39	log "github.com/sirupsen/logrus"
40)
41
42const (
43	signatureVersion = "2"
44	signatureMethod  = "HmacSHA1"
45	timeFormat       = "2006-01-02T15:04:05Z"
46)
47
48type signer struct {
49	// Values that must be populated from the request
50	Request      *http.Request
51	Time         time.Time
52	Credentials  *credentials.Credentials
53	Query        url.Values
54	stringToSign string
55	signature    string
56}
57
58var s3ParamsToSign = map[string]bool{
59	"acl":                          true,
60	"location":                     true,
61	"logging":                      true,
62	"notification":                 true,
63	"partNumber":                   true,
64	"policy":                       true,
65	"requestPayment":               true,
66	"torrent":                      true,
67	"uploadId":                     true,
68	"uploads":                      true,
69	"versionId":                    true,
70	"versioning":                   true,
71	"versions":                     true,
72	"response-content-type":        true,
73	"response-content-language":    true,
74	"response-expires":             true,
75	"response-cache-control":       true,
76	"response-content-disposition": true,
77	"response-content-encoding":    true,
78	"website":                      true,
79	"delete":                       true,
80}
81
82// setv2Handlers will setup v2 signature signing on the S3 driver
83func setv2Handlers(svc *s3.S3) {
84	svc.Handlers.Build.PushBack(func(r *request.Request) {
85		parsedURL, err := url.Parse(r.HTTPRequest.URL.String())
86		if err != nil {
87			log.Fatalf("Failed to parse URL: %v", err)
88		}
89		r.HTTPRequest.URL.Opaque = parsedURL.Path
90	})
91
92	svc.Handlers.Sign.Clear()
93	svc.Handlers.Sign.PushBack(Sign)
94	svc.Handlers.Sign.PushBackNamed(corehandlers.BuildContentLengthHandler)
95}
96
97// Sign requests with signature version 2.
98//
99// Will sign the requests with the service config's Credentials object
100// Signing is skipped if the credentials is the credentials.AnonymousCredentials
101// object.
102func Sign(req *request.Request) {
103	// If the request does not need to be signed ignore the signing of the
104	// request if the AnonymousCredentials object is used.
105	if req.Config.Credentials == credentials.AnonymousCredentials {
106		return
107	}
108
109	v2 := signer{
110		Request:     req.HTTPRequest,
111		Time:        req.Time,
112		Credentials: req.Config.Credentials,
113	}
114	v2.Sign()
115}
116
117func (v2 *signer) Sign() error {
118	credValue, err := v2.Credentials.Get()
119	if err != nil {
120		return err
121	}
122	accessKey := credValue.AccessKeyID
123	var (
124		md5, ctype, date, xamz string
125		xamzDate               bool
126		sarray                 []string
127		smap                   map[string]string
128		sharray                []string
129	)
130
131	headers := v2.Request.Header
132	params := v2.Request.URL.Query()
133	parsedURL, err := url.Parse(v2.Request.URL.String())
134	if err != nil {
135		return err
136	}
137	host, canonicalPath := parsedURL.Host, parsedURL.Path
138	v2.Request.Header["Host"] = []string{host}
139	v2.Request.Header["date"] = []string{v2.Time.In(time.UTC).Format(time.RFC1123)}
140	if credValue.SessionToken != "" {
141		v2.Request.Header["x-amz-security-token"] = []string{credValue.SessionToken}
142	}
143
144	smap = make(map[string]string)
145	for k, v := range headers {
146		k = strings.ToLower(k)
147		switch k {
148		case "content-md5":
149			md5 = v[0]
150		case "content-type":
151			ctype = v[0]
152		case "date":
153			if !xamzDate {
154				date = v[0]
155			}
156		default:
157			if strings.HasPrefix(k, "x-amz-") {
158				vall := strings.Join(v, ",")
159				smap[k] = k + ":" + vall
160				if k == "x-amz-date" {
161					xamzDate = true
162					date = ""
163				}
164				sharray = append(sharray, k)
165			}
166		}
167	}
168	if len(sharray) > 0 {
169		sort.StringSlice(sharray).Sort()
170		for _, h := range sharray {
171			sarray = append(sarray, smap[h])
172		}
173		xamz = strings.Join(sarray, "\n") + "\n"
174	}
175
176	expires := false
177	if v, ok := params["Expires"]; ok {
178		expires = true
179		date = v[0]
180		params["AWSAccessKeyId"] = []string{accessKey}
181	}
182
183	sarray = sarray[0:0]
184	for k, v := range params {
185		if s3ParamsToSign[k] {
186			for _, vi := range v {
187				if vi == "" {
188					sarray = append(sarray, k)
189				} else {
190					sarray = append(sarray, k+"="+vi)
191				}
192			}
193		}
194	}
195	if len(sarray) > 0 {
196		sort.StringSlice(sarray).Sort()
197		canonicalPath = canonicalPath + "?" + strings.Join(sarray, "&")
198	}
199
200	v2.stringToSign = strings.Join([]string{
201		v2.Request.Method,
202		md5,
203		ctype,
204		date,
205		xamz + canonicalPath,
206	}, "\n")
207	hash := hmac.New(sha1.New, []byte(credValue.SecretAccessKey))
208	hash.Write([]byte(v2.stringToSign))
209	v2.signature = base64.StdEncoding.EncodeToString(hash.Sum(nil))
210
211	if expires {
212		params["Signature"] = []string{v2.signature}
213	} else {
214		headers["Authorization"] = []string{"AWS " + accessKey + ":" + v2.signature}
215	}
216
217	log.WithFields(log.Fields{
218		"string-to-sign": v2.stringToSign,
219		"signature":      v2.signature,
220	}).Debugln("request signature")
221	return nil
222}
223