github.com/slspeek/camlistore_namedsearch@v0.0.0-20140519202248-ed6f70f7721a/pkg/misc/amazon/s3/auth.go (about) 1 /* 2 Copyright 2011 Google Inc. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package s3 18 19 import ( 20 "bytes" 21 "crypto/hmac" 22 "crypto/sha1" 23 "encoding/base64" 24 "fmt" 25 "io" 26 "net/http" 27 "net/url" 28 "sort" 29 "strings" 30 "time" 31 ) 32 33 // See http://docs.amazonwebservices.com/AmazonS3/latest/dev/index.html?RESTAuthentication.html 34 35 type Auth struct { 36 AccessKey string 37 SecretAccessKey string 38 39 // Hostname is the S3 hostname to use. 40 // If empty, the standard US region of "s3.amazonaws.com" is 41 // used. 42 Hostname string 43 } 44 45 const standardUSRegionAWS = "s3.amazonaws.com" 46 47 func (a *Auth) hostname() string { 48 if a.Hostname != "" { 49 return a.Hostname 50 } 51 return standardUSRegionAWS 52 } 53 54 func (a *Auth) SignRequest(req *http.Request) { 55 if date := req.Header.Get("Date"); date == "" { 56 req.Header.Set("Date", time.Now().UTC().Format(http.TimeFormat)) 57 } 58 hm := hmac.New(sha1.New, []byte(a.SecretAccessKey)) 59 ss := a.stringToSign(req) 60 // log.Printf("String to sign: %q (%x)", ss, ss) 61 io.WriteString(hm, ss) 62 63 authHeader := new(bytes.Buffer) 64 fmt.Fprintf(authHeader, "AWS %s:", a.AccessKey) 65 encoder := base64.NewEncoder(base64.StdEncoding, authHeader) 66 encoder.Write(hm.Sum(nil)) 67 encoder.Close() 68 req.Header.Set("Authorization", authHeader.String()) 69 } 70 71 func firstNonEmptyString(strs ...string) string { 72 for _, s := range strs { 73 if s != "" { 74 return s 75 } 76 } 77 return "" 78 } 79 80 // From the Amazon docs: 81 // 82 // StringToSign = HTTP-Verb + "\n" + 83 // Content-MD5 + "\n" + 84 // Content-Type + "\n" + 85 // Date + "\n" + 86 // CanonicalizedAmzHeaders + 87 // CanonicalizedResource; 88 func (a *Auth) stringToSign(req *http.Request) string { 89 buf := new(bytes.Buffer) 90 buf.WriteString(req.Method) 91 buf.WriteByte('\n') 92 buf.WriteString(req.Header.Get("Content-MD5")) 93 buf.WriteByte('\n') 94 buf.WriteString(req.Header.Get("Content-Type")) 95 buf.WriteByte('\n') 96 if req.Header.Get("x-amz-date") == "" { 97 buf.WriteString(req.Header.Get("Date")) 98 } 99 buf.WriteByte('\n') 100 a.writeCanonicalizedAmzHeaders(buf, req) 101 a.writeCanonicalizedResource(buf, req) 102 return buf.String() 103 } 104 105 func hasPrefixCaseInsensitive(s, pfx string) bool { 106 if len(pfx) > len(s) { 107 return false 108 } 109 shead := s[:len(pfx)] 110 if shead == pfx { 111 return true 112 } 113 shead = strings.ToLower(shead) 114 return shead == pfx || shead == strings.ToLower(pfx) 115 } 116 117 func (a *Auth) writeCanonicalizedAmzHeaders(buf *bytes.Buffer, req *http.Request) { 118 amzHeaders := make([]string, 0) 119 vals := make(map[string][]string) 120 for k, vv := range req.Header { 121 if hasPrefixCaseInsensitive(k, "x-amz-") { 122 lk := strings.ToLower(k) 123 amzHeaders = append(amzHeaders, lk) 124 vals[lk] = vv 125 } 126 } 127 sort.Strings(amzHeaders) 128 for _, k := range amzHeaders { 129 buf.WriteString(k) 130 buf.WriteByte(':') 131 for idx, v := range vals[k] { 132 if idx > 0 { 133 buf.WriteByte(',') 134 } 135 if strings.Contains(v, "\n") { 136 // TODO: "Unfold" long headers that 137 // span multiple lines (as allowed by 138 // RFC 2616, section 4.2) by replacing 139 // the folding white-space (including 140 // new-line) by a single space. 141 buf.WriteString(v) 142 } else { 143 buf.WriteString(v) 144 } 145 } 146 buf.WriteByte('\n') 147 } 148 } 149 150 // Must be sorted: 151 var subResList = []string{"acl", "lifecycle", "location", "logging", "notification", "partNumber", "policy", "requestPayment", "torrent", "uploadId", "uploads", "versionId", "versioning", "versions", "website"} 152 153 // From the Amazon docs: 154 // 155 // CanonicalizedResource = [ "/" + Bucket ] + 156 // <HTTP-Request-URI, from the protocol name up to the query string> + 157 // [ sub-resource, if present. For example "?acl", "?location", "?logging", or "?torrent"]; 158 func (a *Auth) writeCanonicalizedResource(buf *bytes.Buffer, req *http.Request) { 159 bucket := a.bucketFromHostname(req) 160 if bucket != "" { 161 buf.WriteByte('/') 162 buf.WriteString(bucket) 163 } 164 buf.WriteString(req.URL.Path) 165 if req.URL.RawQuery != "" { 166 n := 0 167 vals, _ := url.ParseQuery(req.URL.RawQuery) 168 for _, subres := range subResList { 169 if vv, ok := vals[subres]; ok && len(vv) > 0 { 170 n++ 171 if n == 1 { 172 buf.WriteByte('?') 173 } else { 174 buf.WriteByte('&') 175 } 176 buf.WriteString(subres) 177 if len(vv[0]) > 0 { 178 buf.WriteByte('=') 179 buf.WriteString(url.QueryEscape(vv[0])) 180 } 181 } 182 } 183 } 184 } 185 186 // hasDotSuffix reports whether s ends with "." + suffix. 187 func hasDotSuffix(s string, suffix string) bool { 188 return len(s) >= len(suffix)+1 && strings.HasSuffix(s, suffix) && s[len(s)-len(suffix)-1] == '.' 189 } 190 191 func (a *Auth) bucketFromHostname(req *http.Request) string { 192 host := req.Host 193 if host == "" { 194 host = req.URL.Host 195 } 196 if host == a.hostname() { 197 return "" 198 } 199 if hostSuffix := a.hostname(); hasDotSuffix(host, hostSuffix) { 200 return host[:len(host)-len(hostSuffix)-1] 201 } 202 if lastColon := strings.LastIndex(host, ":"); lastColon != -1 { 203 return host[:lastColon] 204 } 205 return host 206 }