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 }