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 }