github.com/smugmug/godynamo@v0.0.0-20151122084750-7913028f6623/auth_v4/auth_v4.go (about)

     1  // Manages AWS Auth v4 requests to DynamoDB.
     2  // See http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
     3  // for more information on v4 signed requests. For examples, see any of
     4  // the package in the `endpoints` directory.
     5  package auth_v4
     6  
     7  import (
     8  	"crypto/sha256"
     9  	"encoding/hex"
    10  	"errors"
    11  	"fmt"
    12  	"github.com/smugmug/godynamo/auth_v4/tasks"
    13  	"github.com/smugmug/godynamo/aws_const"
    14  	"github.com/smugmug/godynamo/conf"
    15  	"hash"
    16  	"hash/crc32"
    17  	"io"
    18  	"io/ioutil"
    19  	"net/http"
    20  	"strconv"
    21  	"strings"
    22  	"time"
    23  )
    24  
    25  const (
    26  	IAM_WARN_MESSAGE = "check roles sources and make sure you have run one of the roles " +
    27  		"management functions in package conf_iam, such as GoIAM"
    28  )
    29  
    30  // Client for executing requests.
    31  var Client *http.Client
    32  
    33  // Initialize package-scoped client.
    34  func init() {
    35  	// The timeout seems too-long, but it accomodates the exponential decay retry loop.
    36  	// Programs using this can either change this directly or use goroutine timeouts
    37  	// to impose a local minimum.
    38  	tr := &http.Transport{MaxIdleConnsPerHost: 250,
    39  		ResponseHeaderTimeout: time.Duration(20) * time.Second}
    40  	Client = &http.Client{Transport: tr}
    41  }
    42  
    43  // GetRespReqID retrieves the unique identifier from the AWS Response
    44  func GetRespReqID(response http.Response) (string, error) {
    45  	if amz_reqid_list, reqid_ok := response.Header["X-Amzn-Requestid"]; reqid_ok {
    46  		if len(amz_reqid_list) == 1 {
    47  			return amz_reqid_list[0], nil
    48  		}
    49  	}
    50  	return "", errors.New("auth_v4.GetRespReqID: no X-Amzn-Requestid found")
    51  }
    52  
    53  // MatchCheckSum will perform a local crc32 on the response body and match it against the aws crc32
    54  // *** WARNING ***
    55  // There seems to be a mismatch between what Go calculates and what AWS (java?) calculates here,
    56  // I believe related to utf8 (go) vs utf16 (java), but I don't know enough about encodings to
    57  // solve it. So until that issue is solved, don't use this.
    58  func MatchCheckSum(response http.Response, respbody []byte) (bool, error) {
    59  	if amz_crc_list, crc_ok := response.Header["X-Amz-Crc32"]; crc_ok {
    60  		if len(amz_crc_list) == 1 {
    61  			amz_crc_int32, amz_crc32_err := strconv.Atoi(amz_crc_list[0])
    62  			if amz_crc32_err == nil {
    63  				client_crc_int32 := int(crc32.ChecksumIEEE(respbody))
    64  				if amz_crc_int32 != client_crc_int32 {
    65  					_ = fmt.Sprintf("auth_v4.MatchCheckSum: resp crc mismatch: amz %d client %d",
    66  						amz_crc_int32, client_crc_int32)
    67  					return false, nil
    68  				}
    69  			}
    70  		} else {
    71  			return false, errors.New("auth_v4.MatchCheckSum: X-Amz-Crc32 malformed")
    72  		}
    73  	} else {
    74  		return false, errors.New("auth_v4.MatchCheckSum: no X-Amz-Crc32 found")
    75  	}
    76  	return true, nil
    77  }
    78  
    79  // rawReqAll takes each parameter independently, forms and signs the request, and returns the
    80  // result (and error codes).
    81  func rawReqAll(reqJSON []byte, amzTarget string, useIAM bool, url, host, port, zone, IAMSecret, IAMAccessKey, IAMToken, authSecret, authAccessKey string) ([]byte, string, int, error) {
    82  
    83  	// initialize req with body reader
    84  	body := strings.NewReader(string(reqJSON))
    85  	request, req_err := http.NewRequest(aws_const.METHOD, url, body)
    86  	if req_err != nil {
    87  		e := fmt.Sprintf("auth_v4.rawReqAll:failed init conn %s", req_err.Error())
    88  		return nil, "", 0, errors.New(e)
    89  	}
    90  
    91  	// add headers
    92  	// content type
    93  	request.Header.Add(aws_const.CONTENT_TYPE_HDR, aws_const.CTYPE)
    94  	// amz target
    95  	request.Header.Add(aws_const.AMZ_TARGET_HDR, amzTarget)
    96  	// dates
    97  	now := time.Now()
    98  	request.Header.Add(aws_const.X_AMZ_DATE_HDR,
    99  		now.UTC().Format(aws_const.ISO8601FMT_CONDENSED))
   100  
   101  	// encode request json payload
   102  	var h256 hash.Hash = sha256.New()
   103  	h256.Write(reqJSON)
   104  	hexPayload := string(hex.EncodeToString([]byte(h256.Sum(nil))))
   105  
   106  	// create the various signed formats aws uses for v4 signed reqs
   107  	service := strings.ToLower(aws_const.DYNAMODB)
   108  	canonical_request := tasks.CanonicalRequest(
   109  		host,
   110  		port,
   111  		request.Header.Get(aws_const.X_AMZ_DATE_HDR),
   112  		request.Header.Get(aws_const.AMZ_TARGET_HDR),
   113  		hexPayload)
   114  	str2sign := tasks.String2Sign(now, canonical_request,
   115  		zone,
   116  		service)
   117  
   118  	// obtain the aws secret credential from the global Auth or from IAM
   119  	var secret string
   120  	if useIAM == true {
   121  		secret = IAMSecret
   122  	} else {
   123  		secret = authSecret
   124  	}
   125  	if secret == "" {
   126  		panic("auth_v4.rawReqAll: no Secret defined; " + IAM_WARN_MESSAGE)
   127  	}
   128  
   129  	signature := tasks.MakeSignature(str2sign, zone, service, secret)
   130  
   131  	// obtain the aws accessKey credential from the global Auth or from IAM
   132  	// if using IAM, read the token while we have the lock
   133  	var accessKey, token string
   134  	if useIAM == true {
   135  		accessKey = IAMAccessKey
   136  		token = IAMToken
   137  	} else {
   138  		accessKey = authAccessKey
   139  	}
   140  	if accessKey == "" {
   141  		panic("auth_v4.rawReqAll: no Access Key defined; " + IAM_WARN_MESSAGE)
   142  	}
   143  
   144  	v4auth := "AWS4-HMAC-SHA256 Credential=" + accessKey +
   145  		"/" + now.UTC().Format(aws_const.ISODATEFMT) + "/" +
   146  		zone + "/" + service + "/aws4_request," +
   147  		"SignedHeaders=content-type;host;x-amz-date;x-amz-target," +
   148  		"Signature=" + signature
   149  
   150  	request.Header.Add("Authorization", v4auth)
   151  	if useIAM == true {
   152  		if token == "" {
   153  			panic("auth_v4.rawReqAll: no Token defined;" + IAM_WARN_MESSAGE)
   154  		}
   155  		request.Header.Add(aws_const.X_AMZ_SECURITY_TOKEN_HDR, token)
   156  	}
   157  
   158  	// where we finally send req to aws
   159  	response, rsp_err := Client.Do(request)
   160  
   161  	if rsp_err != nil {
   162  		return nil, "", 0, rsp_err
   163  	}
   164  
   165  	respbody, read_err := ioutil.ReadAll(response.Body)
   166  	response.Body.Close()
   167  	if read_err != nil && read_err != io.EOF {
   168  		e := fmt.Sprintf("auth_v4.rawReqAll:err reading resp body: %s", read_err.Error())
   169  		return nil, "", 0, errors.New(e)
   170  	}
   171  
   172  	amz_requestid, amz_requestid_err := GetRespReqID(*response)
   173  	if amz_requestid_err != nil {
   174  		return nil, "", 0, amz_requestid_err
   175  	}
   176  
   177  	return respbody, amz_requestid, response.StatusCode, nil
   178  }
   179  
   180  // RawReqWithConf will sign and transmit the request to the AWS DynamoDB endpoint.
   181  // reqJSON is the json request
   182  // amzTarget is the dynamoDB endpoint
   183  // c is the configuration struct
   184  // returns []byte respBody, string aws reqID, int http code, error
   185  func RawReqWithConf(reqJSON []byte, amzTarget string, c *conf.AWS_Conf) ([]byte, string, int, error) {
   186  	if !conf.IsValid(c) {
   187  		return nil, "", 0, errors.New("auth_v4.RawReqWithConf: conf not valid")
   188  	}
   189  	// shadow conf vars in a read lock to minimize contention
   190  	var our_c conf.AWS_Conf
   191  	cp_err := our_c.Copy(c)
   192  	if cp_err != nil {
   193  		return nil, "", 0, cp_err
   194  	}
   195  	return rawReqAll(
   196  		reqJSON,
   197  		amzTarget,
   198  		our_c.UseIAM,
   199  		our_c.Network.DynamoDB.URL,
   200  		our_c.Network.DynamoDB.Host,
   201  		our_c.Network.DynamoDB.Port,
   202  		our_c.Network.DynamoDB.Zone,
   203  		our_c.IAM.Credentials.Secret,
   204  		our_c.IAM.Credentials.AccessKey,
   205  		our_c.IAM.Credentials.Token,
   206  		our_c.Auth.Secret,
   207  		our_c.Auth.AccessKey)
   208  }
   209  
   210  // RawReq will sign and transmit the request to the AWS DynamoDB endpoint.
   211  // This method uses the global conf.Vals to obtain credential and configuation information.
   212  func RawReq(reqJSON []byte, amzTarget string) ([]byte, string, int, error) {
   213  	return RawReqWithConf(reqJSON, amzTarget, &conf.Vals)
   214  }
   215  
   216  // Req  will sign and transmit the request to the AWS DynamoDB endpoint.
   217  // This method uses the global conf.Vals to obtain credential and configuation information.
   218  // At one point, RawReq and Req were different, now RawReq is just an alias.
   219  func Req(reqJSON []byte, amzTarget string) ([]byte, string, int, error) {
   220  	return RawReqWithConf(reqJSON, amzTarget, &conf.Vals)
   221  }
   222  
   223  // ReqConf is just a wrapper for RawReq if we need to massage data
   224  // before dispatch. Uses parameterized conf.
   225  func ReqWithConf(reqJSON []byte, amzTarget string, c *conf.AWS_Conf) ([]byte, string, int, error) {
   226  	return RawReqWithConf(reqJSON, amzTarget, c)
   227  }