github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/thirdparty/s3.go (about) 1 package thirdparty 2 3 import ( 4 "crypto/hmac" 5 "crypto/sha1" 6 "crypto/tls" 7 "crypto/x509" 8 "encoding/base64" 9 "encoding/xml" 10 "fmt" 11 "io" 12 "io/ioutil" 13 "net/http" 14 "net/url" 15 "os" 16 "runtime" 17 "sort" 18 "strings" 19 "time" 20 21 "github.com/goamz/goamz/aws" 22 "github.com/goamz/goamz/s3" 23 "github.com/mongodb/grip" 24 "github.com/pkg/errors" 25 ) 26 27 var s3ParamsToSign = map[string]bool{ 28 "acl": true, 29 "location": true, 30 "logging": true, 31 "notification": true, 32 "partNumber": true, 33 "policy": true, 34 "requestPayment": true, 35 "torrent": true, 36 "uploadId": true, 37 "uploads": true, 38 "versionId": true, 39 "versioning": true, 40 "versions": true, 41 "response-content-type": true, 42 "response-content-language": true, 43 "response-expires": true, 44 "response-cache-control": true, 45 "response-content-disposition": true, 46 "response-content-encoding": true, 47 } 48 49 const ( 50 S3ConnectTimeout = 2 * time.Minute 51 S3ReadTimeout = 10 * time.Minute 52 S3WriteTimeout = 10 * time.Minute 53 ) 54 55 // For our S3 copy operations, S3 either returns an CopyObjectResult or 56 // a CopyObjectError body. In order to determine what kind of response 57 // was returned we read the body returned from the API call 58 type CopyObjectResult struct { 59 XMLName xml.Name `xml:"CopyObjectResult"` 60 LastModified string `xml:"LastModified"` 61 ETag string `xml:"ETag"` 62 } 63 64 type CopyObjectError struct { 65 XMLName xml.Name `xml:"Error"` 66 Code string `xml:"Code"` 67 Message string `xml:"Message"` 68 Resource string `xml:"Resource"` 69 RequestId string `xml:"RequestId"` 70 ErrMsg string 71 } 72 73 func (e CopyObjectError) Error() string { 74 return fmt.Sprintf("Code: %v\nMessage: %v\nResource: %v"+ 75 "\nRequestId: %v\nErrMsg: %v\n", 76 e.Code, e.Message, e.Resource, e.RequestId, e.ErrMsg) 77 } 78 79 //This is used to get the bucket and filename, 80 //ignoring any username/password so that it can be 81 //securely printed in logs 82 //Returns: (bucket, filename, error) 83 func GetS3Location(s3URL string) (string, string, error) { 84 urlParsed, err := url.Parse(s3URL) 85 if err != nil { 86 return "", "", err 87 } 88 89 if urlParsed.Scheme != "s3" { 90 return "", "", errors.Errorf("Don't know how to use URL with scheme %v", urlParsed.Scheme) 91 } 92 93 return urlParsed.Host, urlParsed.Path, nil 94 } 95 96 func CopyS3File(awsAuth *aws.Auth, fromS3URL string, toS3URL string, permissionACL string) error { 97 fromParsed, err := url.Parse(fromS3URL) 98 if err != nil { 99 return errors.WithStack(err) 100 } 101 102 toParsed, err := url.Parse(toS3URL) 103 if err != nil { 104 return errors.WithStack(err) 105 } 106 107 client := &http.Client{} 108 destinationPath := fmt.Sprintf("http://%v.s3.amazonaws.com%v", toParsed.Host, toParsed.Path) 109 req, err := http.NewRequest("PUT", destinationPath, nil) 110 if err != nil { 111 return errors.Wrapf(err, "PUT request on %v failed", destinationPath) 112 } 113 req.Header.Add("x-amz-copy-source", fmt.Sprintf("/%v%v", fromParsed.Host, fromParsed.Path)) 114 req.Header.Add("x-amz-date", time.Now().Format(time.RFC850)) 115 if permissionACL != "" { 116 req.Header.Add("x-amz-acl", permissionACL) 117 } 118 SignAWSRequest(*awsAuth, "/"+toParsed.Host+toParsed.Path, req) 119 120 resp, err := client.Do(req) 121 if resp == nil { 122 return errors.Wrap(err, "Nil response received") 123 } 124 defer resp.Body.Close() 125 126 // attempt to read the response body to check for success/error message 127 respBody, respBodyErr := ioutil.ReadAll(resp.Body) 128 if respBodyErr != nil { 129 return errors.Wrap(respBodyErr, "Error reading s3 copy response body") 130 } 131 132 // Attempt to unmarshall the response body. If there's no errors, it means 133 // that the S3 copy was successful. If there's an error, or a non-200 134 // response code, it indicates a copy error 135 copyObjectResult := CopyObjectResult{} 136 xmlErr := xml.Unmarshal(respBody, ©ObjectResult) 137 if xmlErr != nil || resp.StatusCode != http.StatusOK { 138 var errMsg string 139 if xmlErr == nil { 140 errMsg = fmt.Sprintf("S3 returned status code: %d", resp.StatusCode) 141 } else { 142 errMsg = fmt.Sprintf("unmarshalling error: %v", xmlErr) 143 } 144 // an unmarshalling error or a non-200 status code indicates S3 returned 145 // an error so we'll now attempt to unmarshall that error response 146 copyObjectError := CopyObjectError{} 147 xmlErr = xml.Unmarshal(respBody, ©ObjectError) 148 if xmlErr != nil { 149 // *This should seldom happen since a non-200 status code or a 150 // copyObjectResult unmarshall error on a response from S3 should 151 // contain a CopyObjectError. An error here indicates possible 152 // backwards incompatible changes in the AWS API 153 return errors.Wrapf(xmlErr, "Unrecognized S3 response: %v", errMsg) 154 } 155 copyObjectError.ErrMsg = errMsg 156 // if we were able to parse out an error response, then we can reliably 157 // inform the user of the error 158 return copyObjectError 159 } 160 return errors.WithStack(err) 161 } 162 163 func S3CopyFile(awsAuth *aws.Auth, fromS3Bucket, fromS3Path, 164 toS3Bucket, toS3Path, permissionACL string) error { 165 client := &http.Client{} 166 destinationPath := fmt.Sprintf("http://%v.s3.amazonaws.com/%v", 167 toS3Bucket, toS3Path) 168 req, err := http.NewRequest("PUT", destinationPath, nil) 169 if err != nil { 170 return errors.Wrapf(err, "PUT request on %v failed", destinationPath) 171 } 172 req.Header.Add("x-amz-copy-source", fmt.Sprintf("/%v/%v", fromS3Bucket, 173 fromS3Path)) 174 req.Header.Add("x-amz-date", time.Now().Format(time.RFC850)) 175 if permissionACL != "" { 176 req.Header.Add("x-amz-acl", permissionACL) 177 } 178 signaturePath := fmt.Sprintf("/%v/%v", toS3Bucket, toS3Path) 179 SignAWSRequest(*awsAuth, signaturePath, req) 180 181 resp, err := client.Do(req) 182 if resp == nil { 183 return errors.Wrap(err, "Nil response received") 184 } 185 defer resp.Body.Close() 186 187 // attempt to read the response body to check for success/error message 188 respBody, respBodyErr := ioutil.ReadAll(resp.Body) 189 if respBodyErr != nil { 190 return errors.Errorf("Error reading s3 copy response body: %v", respBodyErr) 191 } 192 193 // Attempt to unmarshall the response body. If there's no errors, it means 194 // that the S3 copy was successful. If there's an error, or a non-200 195 // response code, it indicates a copy error 196 copyObjectResult := CopyObjectResult{} 197 xmlErr := xml.Unmarshal(respBody, ©ObjectResult) 198 if xmlErr != nil || resp.StatusCode != http.StatusOK { 199 var errMsg string 200 if xmlErr == nil { 201 errMsg = fmt.Sprintf("S3 returned status code: %d", resp.StatusCode) 202 } else { 203 errMsg = fmt.Sprintf("unmarshalling error: %v", xmlErr) 204 } 205 // an unmarshalling error or a non-200 status code indicates S3 returned 206 // an error so we'll now attempt to unmarshall that error response 207 copyObjectError := CopyObjectError{} 208 xmlErr = xml.Unmarshal(respBody, ©ObjectError) 209 if xmlErr != nil { 210 // *This should seldom happen since a non-200 status code or a 211 // copyObjectResult unmarshall error on a response from S3 should 212 // contain a CopyObjectError. An error here indicates possible 213 // backwards incompatible changes in the AWS API 214 return errors.Errorf("Unrecognized S3 response: %v: %v", errMsg, xmlErr) 215 } 216 copyObjectError.ErrMsg = errMsg 217 // if we were able to parse out an error response, then we can reliably 218 // inform the user of the error 219 return copyObjectError 220 } 221 return err 222 } 223 224 // PutS3File writes the specified file to an s3 bucket using the given permissions and content type. 225 // The details of where to put the file are included in the s3URL 226 func PutS3File(pushAuth *aws.Auth, localFilePath, s3URL, contentType, permissionACL string) error { 227 urlParsed, err := url.Parse(s3URL) 228 if err != nil { 229 return err 230 } 231 232 if urlParsed.Scheme != "s3" { 233 return errors.Errorf("Don't know how to use URL with scheme %v", urlParsed.Scheme) 234 } 235 236 localFileReader, err := os.Open(localFilePath) 237 if err != nil { 238 return err 239 } 240 241 fi, err := os.Stat(localFilePath) 242 if err != nil { 243 return err 244 } 245 246 session := NewS3Session(pushAuth, aws.USEast) 247 bucket := session.Bucket(urlParsed.Host) 248 // options for the header 249 options := s3.Options{} 250 return errors.Wrapf(bucket.PutReader(urlParsed.Path, localFileReader, fi.Size(), contentType, s3.ACL(permissionACL), options), 251 "problem putting %s to bucket", localFilePath) 252 } 253 254 func GetS3File(auth *aws.Auth, s3URL string) (io.ReadCloser, error) { 255 urlParsed, err := url.Parse(s3URL) 256 if err != nil { 257 return nil, err 258 } 259 session := NewS3Session(auth, aws.USEast) 260 261 bucket := session.Bucket(urlParsed.Host) 262 return bucket.GetReader(urlParsed.Path) 263 } 264 265 //Taken from https://github.com/mitchellh/goamz/blob/master/s3/sign.go 266 //Modified to access the headers/params on an HTTP req directly. 267 func SignAWSRequest(auth aws.Auth, canonicalPath string, req *http.Request) { 268 method := req.Method 269 headers := req.Header 270 params := req.URL.Query() 271 272 var md5, ctype, date, xamz string 273 var xamzDate bool 274 var sarray []string 275 var err error 276 for k, v := range headers { 277 k = strings.ToLower(k) 278 switch k { 279 case "content-md5": 280 md5 = v[0] 281 case "content-type": 282 ctype = v[0] 283 case "date": 284 if !xamzDate { 285 date = v[0] 286 } 287 default: 288 if strings.HasPrefix(k, "x-amz-") { 289 vall := strings.Join(v, ",") 290 sarray = append(sarray, k+":"+vall) 291 if k == "x-amz-date" { 292 xamzDate = true 293 date = "" 294 } 295 } 296 } 297 } 298 if len(sarray) > 0 { 299 sort.StringSlice(sarray).Sort() 300 xamz = strings.Join(sarray, "\n") + "\n" 301 } 302 303 expires := false 304 if v, ok := params["Expires"]; ok { 305 // Query string request authentication alternative. 306 expires = true 307 date = v[0] 308 params["AWSAccessKeyId"] = []string{auth.AccessKey} 309 } 310 311 sarray = sarray[0:0] 312 for k, v := range params { 313 if s3ParamsToSign[k] { 314 for _, vi := range v { 315 if vi == "" { 316 sarray = append(sarray, k) 317 } else { 318 // "When signing you do not encode these values." 319 sarray = append(sarray, k+"="+vi) 320 } 321 } 322 } 323 } 324 if len(sarray) > 0 { 325 sort.StringSlice(sarray).Sort() 326 canonicalPath = canonicalPath + "?" + strings.Join(sarray, "&") 327 } 328 329 payload := method + "\n" + md5 + "\n" + ctype + "\n" + date + "\n" + xamz + canonicalPath 330 hash := hmac.New(sha1.New, []byte(auth.SecretKey)) 331 _, err = hash.Write([]byte(payload)) 332 grip.Debug(err) 333 334 signature := make([]byte, base64.StdEncoding.EncodedLen(hash.Size())) 335 base64.StdEncoding.Encode(signature, hash.Sum(nil)) 336 337 if expires { 338 params["Signature"] = []string{string(signature)} 339 } else { 340 headers["Authorization"] = []string{"AWS " + auth.AccessKey + ":" + string(signature)} 341 } 342 } 343 344 // NewS3Session checks the OS of the agent if darwin, adds InsecureSkipVerify to the TLSConfig. 345 // This workaround is meant to fix 346 //"x509: failed to load system roots and no roots provided". This happens since cross-compiling 347 // disables cgo - however cgo is required to find system root 348 // certificates on darwin machines. Note that the client 349 // returned can only connect successfully to the 350 // supplied s3's region. 351 352 func NewS3Session(auth *aws.Auth, region aws.Region) *s3.S3 { 353 var s3Session *s3.S3 354 cert := x509.Certificate{} 355 // go's systemVerify panics with no verify options set 356 // TODO: EVG-483 357 if runtime.GOOS == "windows" { 358 s3Session = s3.New(*auth, region) 359 s3Session.ReadTimeout = S3ReadTimeout 360 s3Session.WriteTimeout = S3WriteTimeout 361 s3Session.ConnectTimeout = S3ConnectTimeout 362 return s3Session 363 } 364 // no verify options so system root ca will be used 365 _, err := cert.Verify(x509.VerifyOptions{}) 366 rootsError := x509.SystemRootsError{} 367 if err != nil && err.Error() == rootsError.Error() { 368 // create a Transport which includes our TLSConfig with InsecureSkipVerify 369 // and client timeouts. 370 tlsConfig := tls.Config{InsecureSkipVerify: true} 371 tr := http.Transport{ 372 TLSClientConfig: &tlsConfig} 373 // add the Transport to our http client 374 client := &http.Client{Transport: &tr} 375 s3Session = s3.New(*auth, region, client) 376 } else { 377 s3Session = s3.New(*auth, region) 378 } 379 s3Session.ReadTimeout = S3ReadTimeout 380 s3Session.WriteTimeout = S3WriteTimeout 381 s3Session.ConnectTimeout = S3ConnectTimeout 382 return s3Session 383 }