github.com/olivere/camlistore@v0.0.0-20140121221811-1b7ac2da0199/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 "sort" 28 "strings" 29 "time" 30 ) 31 32 // See http://docs.amazonwebservices.com/AmazonS3/latest/dev/index.html?RESTAuthentication.html 33 34 type Auth struct { 35 AccessKey string 36 SecretAccessKey string 37 38 // Hostname is the S3 hostname to use. 39 // If empty, the stanard US region of "s3.amazonaws.com" is 40 // used. 41 Hostname string 42 } 43 44 const standardUSRegionAWS = "s3.amazonaws.com" 45 46 func (a *Auth) hostname() string { 47 if a.Hostname != "" { 48 return a.Hostname 49 } 50 return standardUSRegionAWS 51 } 52 53 func (a *Auth) SignRequest(req *http.Request) { 54 if date := req.Header.Get("Date"); date == "" { 55 req.Header.Set("Date", time.Now().UTC().Format(http.TimeFormat)) 56 } 57 hm := hmac.New(sha1.New, []byte(a.SecretAccessKey)) 58 ss := a.stringToSign(req) 59 io.WriteString(hm, ss) 60 61 authHeader := new(bytes.Buffer) 62 fmt.Fprintf(authHeader, "AWS %s:", a.AccessKey) 63 encoder := base64.NewEncoder(base64.StdEncoding, authHeader) 64 encoder.Write(hm.Sum(nil)) 65 encoder.Close() 66 req.Header.Set("Authorization", authHeader.String()) 67 } 68 69 func firstNonEmptyString(strs ...string) string { 70 for _, s := range strs { 71 if s != "" { 72 return s 73 } 74 } 75 return "" 76 } 77 78 // From the Amazon docs: 79 // 80 // StringToSign = HTTP-Verb + "\n" + 81 // Content-MD5 + "\n" + 82 // Content-Type + "\n" + 83 // Date + "\n" + 84 // CanonicalizedAmzHeaders + 85 // CanonicalizedResource; 86 func (a *Auth) stringToSign(req *http.Request) string { 87 buf := new(bytes.Buffer) 88 buf.WriteString(req.Method) 89 buf.WriteByte('\n') 90 buf.WriteString(req.Header.Get("Content-MD5")) 91 buf.WriteByte('\n') 92 buf.WriteString(req.Header.Get("Content-Type")) 93 buf.WriteByte('\n') 94 if req.Header.Get("x-amz-date") == "" { 95 buf.WriteString(req.Header.Get("Date")) 96 } 97 buf.WriteByte('\n') 98 a.writeCanonicalizedAmzHeaders(buf, req) 99 a.writeCanonicalizedResource(buf, req) 100 return buf.String() 101 } 102 103 func hasPrefixCaseInsensitive(s, pfx string) bool { 104 if len(pfx) > len(s) { 105 return false 106 } 107 shead := s[:len(pfx)] 108 if shead == pfx { 109 return true 110 } 111 shead = strings.ToLower(shead) 112 return shead == pfx || shead == strings.ToLower(pfx) 113 } 114 115 func (a *Auth) writeCanonicalizedAmzHeaders(buf *bytes.Buffer, req *http.Request) { 116 amzHeaders := make([]string, 0) 117 vals := make(map[string][]string) 118 for k, vv := range req.Header { 119 if hasPrefixCaseInsensitive(k, "x-amz-") { 120 lk := strings.ToLower(k) 121 amzHeaders = append(amzHeaders, lk) 122 vals[lk] = vv 123 } 124 } 125 sort.Strings(amzHeaders) 126 for _, k := range amzHeaders { 127 buf.WriteString(k) 128 buf.WriteByte(':') 129 for idx, v := range vals[k] { 130 if idx > 0 { 131 buf.WriteByte(',') 132 } 133 if strings.Contains(v, "\n") { 134 // TODO: "Unfold" long headers that 135 // span multiple lines (as allowed by 136 // RFC 2616, section 4.2) by replacing 137 // the folding white-space (including 138 // new-line) by a single space. 139 buf.WriteString(v) 140 } else { 141 buf.WriteString(v) 142 } 143 } 144 buf.WriteByte('\n') 145 } 146 } 147 148 // From the Amazon docs: 149 // 150 // CanonicalizedResource = [ "/" + Bucket ] + 151 // <HTTP-Request-URI, from the protocol name up to the query string> + 152 // [ sub-resource, if present. For example "?acl", "?location", "?logging", or "?torrent"]; 153 func (a *Auth) writeCanonicalizedResource(buf *bytes.Buffer, req *http.Request) { 154 if bucket := a.bucketFromHostname(req); bucket != "" { 155 buf.WriteByte('/') 156 buf.WriteString(bucket) 157 } 158 buf.WriteString(req.URL.Path) 159 // TODO: subresource 160 } 161 162 // hasDotSuffix reports whether s ends with "." + suffix. 163 func hasDotSuffix(s string, suffix string) bool { 164 return len(s) >= len(suffix)+1 && strings.HasSuffix(s, suffix) && s[len(s)-len(suffix)-1] == '.' 165 } 166 167 func (a *Auth) bucketFromHostname(req *http.Request) string { 168 host := req.Host 169 if host == "" { 170 host = req.URL.Host 171 } 172 if host == a.hostname() { 173 return "" 174 } 175 if hostSuffix := a.hostname(); hasDotSuffix(host, hostSuffix) { 176 return host[:len(host)-len(hostSuffix)-1] 177 } 178 if lastColon := strings.LastIndex(host, ":"); lastColon != -1 { 179 return host[:lastColon] 180 } 181 return host 182 }