github.com/rawahars/moby@v24.0.4+incompatible/builder/remotecontext/remote.go (about)

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