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  }