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, &copyObjectResult)
   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, &copyObjectError)
   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, &copyObjectResult)
   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, &copyObjectError)
   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  }