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  }