github.com/demonoid81/moby@v0.0.0-20200517203328-62dd8e17c460/builder/remotecontext/remote.go (about)

     1  package remotecontext // import "github.com/demonoid81/moby/builder/remotecontext"
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"net"
     9  	"net/http"
    10  	"net/url"
    11  	"regexp"
    12  
    13  	"github.com/demonoid81/moby/errdefs"
    14  	"github.com/demonoid81/moby/pkg/ioutils"
    15  	"github.com/pkg/errors"
    16  )
    17  
    18  // When downloading remote contexts, limit the amount (in bytes)
    19  // to be read from the response body in order to detect its Content-Type
    20  const maxPreambleLength = 100
    21  
    22  const acceptableRemoteMIME = `(?:application/(?:(?:x\-)?tar|octet\-stream|((?:x\-)?(?:gzip|bzip2?|xz)))|(?:text/plain))`
    23  
    24  var mimeRe = regexp.MustCompile(acceptableRemoteMIME)
    25  
    26  // downloadRemote context from a url and returns it, along with the parsed content type
    27  func downloadRemote(remoteURL string) (string, io.ReadCloser, error) {
    28  	response, err := GetWithStatusError(remoteURL)
    29  	if err != nil {
    30  		return "", nil, errors.Wrapf(err, "error downloading remote context %s", remoteURL)
    31  	}
    32  
    33  	contentType, contextReader, err := inspectResponse(
    34  		response.Header.Get("Content-Type"),
    35  		response.Body,
    36  		response.ContentLength)
    37  	if err != nil {
    38  		response.Body.Close()
    39  		return "", nil, errors.Wrapf(err, "error detecting content type for remote %s", remoteURL)
    40  	}
    41  
    42  	return contentType, ioutils.NewReadCloserWrapper(contextReader, response.Body.Close), nil
    43  }
    44  
    45  // GetWithStatusError does an http.Get() and returns an error if the
    46  // status code is 4xx or 5xx.
    47  func GetWithStatusError(address string) (resp *http.Response, err error) {
    48  	// #nosec G107
    49  	if resp, err = http.Get(address); err != nil {
    50  		if uerr, ok := err.(*url.Error); ok {
    51  			if derr, ok := uerr.Err.(*net.DNSError); ok && !derr.IsTimeout {
    52  				return nil, errdefs.NotFound(err)
    53  			}
    54  		}
    55  		return nil, errdefs.System(err)
    56  	}
    57  	if resp.StatusCode < 400 {
    58  		return resp, nil
    59  	}
    60  	msg := fmt.Sprintf("failed to GET %s with status %s", address, resp.Status)
    61  	body, err := ioutil.ReadAll(resp.Body)
    62  	resp.Body.Close()
    63  	if err != nil {
    64  		return nil, errdefs.System(errors.New(msg + ": error reading body"))
    65  	}
    66  
    67  	msg += ": " + string(bytes.TrimSpace(body))
    68  	switch resp.StatusCode {
    69  	case http.StatusNotFound:
    70  		return nil, errdefs.NotFound(errors.New(msg))
    71  	case http.StatusBadRequest:
    72  		return nil, errdefs.InvalidParameter(errors.New(msg))
    73  	case http.StatusUnauthorized:
    74  		return nil, errdefs.Unauthorized(errors.New(msg))
    75  	case http.StatusForbidden:
    76  		return nil, errdefs.Forbidden(errors.New(msg))
    77  	}
    78  	return nil, errdefs.Unknown(errors.New(msg))
    79  }
    80  
    81  // inspectResponse looks into the http response data at r to determine whether its
    82  // content-type is on the list of acceptable content types for remote build contexts.
    83  // This function returns:
    84  //    - a string representation of the detected content-type
    85  //    - an io.Reader for the response body
    86  //    - an error value which will be non-nil either when something goes wrong while
    87  //      reading bytes from r or when the detected content-type is not acceptable.
    88  func inspectResponse(ct string, r io.Reader, clen int64) (string, io.Reader, error) {
    89  	plen := clen
    90  	if plen <= 0 || plen > maxPreambleLength {
    91  		plen = maxPreambleLength
    92  	}
    93  
    94  	preamble := make([]byte, plen)
    95  	rlen, err := r.Read(preamble)
    96  	if rlen == 0 {
    97  		return ct, r, errors.New("empty response")
    98  	}
    99  	if err != nil && err != io.EOF {
   100  		return ct, r, err
   101  	}
   102  
   103  	preambleR := bytes.NewReader(preamble[:rlen])
   104  	bodyReader := io.MultiReader(preambleR, r)
   105  	// Some web servers will use application/octet-stream as the default
   106  	// content type for files without an extension (e.g. 'Dockerfile')
   107  	// so if we receive this value we better check for text content
   108  	contentType := ct
   109  	if len(ct) == 0 || ct == mimeTypes.OctetStream {
   110  		contentType, _, err = detectContentType(preamble)
   111  		if err != nil {
   112  			return contentType, bodyReader, err
   113  		}
   114  	}
   115  
   116  	contentType = selectAcceptableMIME(contentType)
   117  	var cterr error
   118  	if len(contentType) == 0 {
   119  		cterr = fmt.Errorf("unsupported Content-Type %q", ct)
   120  		contentType = ct
   121  	}
   122  
   123  	return contentType, bodyReader, cterr
   124  }
   125  
   126  func selectAcceptableMIME(ct string) string {
   127  	return mimeRe.FindString(ct)
   128  }