github.com/jiasir/docker@v1.3.3-0.20170609024000-252e610103e7/builder/remotecontext/remote.go (about) 1 package remotecontext 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "net/http" 9 "regexp" 10 11 "github.com/docker/docker/builder" 12 "github.com/pkg/errors" 13 ) 14 15 // When downloading remote contexts, limit the amount (in bytes) 16 // to be read from the response body in order to detect its Content-Type 17 const maxPreambleLength = 100 18 19 const acceptableRemoteMIME = `(?:application/(?:(?:x\-)?tar|octet\-stream|((?:x\-)?(?:gzip|bzip2?|xz)))|(?:text/plain))` 20 21 var mimeRe = regexp.MustCompile(acceptableRemoteMIME) 22 23 // MakeRemoteContext downloads a context from remoteURL and returns it. 24 // 25 // If contentTypeHandlers is non-nil, then the Content-Type header is read along with a maximum of 26 // maxPreambleLength bytes from the body to help detecting the MIME type. 27 // Look at acceptableRemoteMIME for more details. 28 // 29 // If a match is found, then the body is sent to the contentType handler and a (potentially compressed) tar stream is expected 30 // to be returned. If no match is found, it is assumed the body is a tar stream (compressed or not). 31 // In either case, an (assumed) tar stream is passed to MakeTarSumContext whose result is returned. 32 func MakeRemoteContext(remoteURL string, contentTypeHandlers map[string]func(io.ReadCloser) (io.ReadCloser, error)) (builder.Source, error) { 33 f, err := GetWithStatusError(remoteURL) 34 if err != nil { 35 return nil, fmt.Errorf("error downloading remote context %s: %v", remoteURL, err) 36 } 37 defer f.Body.Close() 38 39 var contextReader io.ReadCloser 40 if contentTypeHandlers != nil { 41 contentType := f.Header.Get("Content-Type") 42 clen := f.ContentLength 43 44 contentType, contextReader, err = inspectResponse(contentType, f.Body, clen) 45 if err != nil { 46 return nil, fmt.Errorf("error detecting content type for remote %s: %v", remoteURL, err) 47 } 48 defer contextReader.Close() 49 50 // This loop tries to find a content-type handler for the detected content-type. 51 // If it could not find one from the caller-supplied map, it tries the empty content-type `""` 52 // which is interpreted as a fallback handler (usually used for raw tar contexts). 53 for _, ct := range []string{contentType, ""} { 54 if fn, ok := contentTypeHandlers[ct]; ok { 55 defer contextReader.Close() 56 if contextReader, err = fn(contextReader); err != nil { 57 return nil, err 58 } 59 break 60 } 61 } 62 } 63 64 // Pass through - this is a pre-packaged context, presumably 65 // with a Dockerfile with the right name inside it. 66 return MakeTarSumContext(contextReader) 67 } 68 69 // GetWithStatusError does an http.Get() and returns an error if the 70 // status code is 4xx or 5xx. 71 func GetWithStatusError(url string) (resp *http.Response, err error) { 72 if resp, err = http.Get(url); err != nil { 73 return nil, err 74 } 75 if resp.StatusCode < 400 { 76 return resp, nil 77 } 78 msg := fmt.Sprintf("failed to GET %s with status %s", url, resp.Status) 79 body, err := ioutil.ReadAll(resp.Body) 80 resp.Body.Close() 81 if err != nil { 82 return nil, errors.Wrapf(err, msg+": error reading body") 83 } 84 return nil, errors.Errorf(msg+": %s", bytes.TrimSpace(body)) 85 } 86 87 // inspectResponse looks into the http response data at r to determine whether its 88 // content-type is on the list of acceptable content types for remote build contexts. 89 // This function returns: 90 // - a string representation of the detected content-type 91 // - an io.Reader for the response body 92 // - an error value which will be non-nil either when something goes wrong while 93 // reading bytes from r or when the detected content-type is not acceptable. 94 func inspectResponse(ct string, r io.ReadCloser, clen int64) (string, io.ReadCloser, error) { 95 plen := clen 96 if plen <= 0 || plen > maxPreambleLength { 97 plen = maxPreambleLength 98 } 99 100 preamble := make([]byte, plen, plen) 101 rlen, err := r.Read(preamble) 102 if rlen == 0 { 103 return ct, r, errors.New("empty response") 104 } 105 if err != nil && err != io.EOF { 106 return ct, r, err 107 } 108 109 preambleR := bytes.NewReader(preamble[:rlen]) 110 bodyReader := ioutil.NopCloser(io.MultiReader(preambleR, r)) 111 // Some web servers will use application/octet-stream as the default 112 // content type for files without an extension (e.g. 'Dockerfile') 113 // so if we receive this value we better check for text content 114 contentType := ct 115 if len(ct) == 0 || ct == mimeTypes.OctetStream { 116 contentType, _, err = detectContentType(preamble) 117 if err != nil { 118 return contentType, bodyReader, err 119 } 120 } 121 122 contentType = selectAcceptableMIME(contentType) 123 var cterr error 124 if len(contentType) == 0 { 125 cterr = fmt.Errorf("unsupported Content-Type %q", ct) 126 contentType = ct 127 } 128 129 return contentType, bodyReader, cterr 130 } 131 132 func selectAcceptableMIME(ct string) string { 133 return mimeRe.FindString(ct) 134 }