package remotecontext import ( "bytes" "fmt" "io" "io/ioutil" "net" "net/http" "net/url" "regexp" "github.com/docker/docker/builder" "github.com/pkg/errors" ) // When downloading remote contexts, limit the amount (in bytes) // to be read from the response body in order to detect its Content-Type const maxPreambleLength = 100 const acceptableRemoteMIME = `(?:application/(?:(?:x\-)?tar|octet\-stream|((?:x\-)?(?:gzip|bzip2?|xz)))|(?:text/plain))` var mimeRe = regexp.MustCompile(acceptableRemoteMIME) // MakeRemoteContext downloads a context from remoteURL and returns it. // // If contentTypeHandlers is non-nil, then the Content-Type header is read along with a maximum of // maxPreambleLength bytes from the body to help detecting the MIME type. // Look at acceptableRemoteMIME for more details. // // If a match is found, then the body is sent to the contentType handler and a (potentially compressed) tar stream is expected // to be returned. If no match is found, it is assumed the body is a tar stream (compressed or not). // In either case, an (assumed) tar stream is passed to FromArchive whose result is returned. func MakeRemoteContext(remoteURL string, contentTypeHandlers map[string]func(io.ReadCloser) (io.ReadCloser, error)) (builder.Source, error) { f, err := GetWithStatusError(remoteURL) if err != nil { return nil, fmt.Errorf("error downloading remote context %s: %v", remoteURL, err) } defer f.Body.Close() var contextReader io.ReadCloser if contentTypeHandlers != nil { contentType := f.Header.Get("Content-Type") clen := f.ContentLength contentType, contextReader, err = inspectResponse(contentType, f.Body, clen) if err != nil { return nil, fmt.Errorf("error detecting content type for remote %s: %v", remoteURL, err) } defer contextReader.Close() // This loop tries to find a content-type handler for the detected content-type. // If it could not find one from the caller-supplied map, it tries the empty content-type `""` // which is interpreted as a fallback handler (usually used for raw tar contexts). for _, ct := range []string{contentType, ""} { if fn, ok := contentTypeHandlers[ct]; ok { defer contextReader.Close() if contextReader, err = fn(contextReader); err != nil { return nil, err } break } } } // Pass through - this is a pre-packaged context, presumably // with a Dockerfile with the right name inside it. return FromArchive(contextReader) } // GetWithStatusError does an http.Get() and returns an error if the // status code is 4xx or 5xx. func GetWithStatusError(address string) (resp *http.Response, err error) { if resp, err = http.Get(address); err != nil { if uerr, ok := err.(*url.Error); ok { if derr, ok := uerr.Err.(*net.DNSError); ok && !derr.IsTimeout { return nil, dnsError{err} } } return nil, systemError{err} } if resp.StatusCode < 400 { return resp, nil } msg := fmt.Sprintf("failed to GET %s with status %s", address, resp.Status) body, err := ioutil.ReadAll(resp.Body) resp.Body.Close() if err != nil { return nil, errors.Wrap(systemError{err}, msg+": error reading body") } msg += ": " + string(bytes.TrimSpace(body)) switch resp.StatusCode { case http.StatusNotFound: return nil, notFoundError(msg) case http.StatusBadRequest: return nil, requestError(msg) case http.StatusUnauthorized: return nil, unauthorizedError(msg) case http.StatusForbidden: return nil, forbiddenError(msg) } return nil, unknownError{errors.New(msg)} } // inspectResponse looks into the http response data at r to determine whether its // content-type is on the list of acceptable content types for remote build contexts. // This function returns: // - a string representation of the detected content-type // - an io.Reader for the response body // - an error value which will be non-nil either when something goes wrong while // reading bytes from r or when the detected content-type is not acceptable. func inspectResponse(ct string, r io.ReadCloser, clen int64) (string, io.ReadCloser, error) { plen := clen if plen <= 0 || plen > maxPreambleLength { plen = maxPreambleLength } preamble := make([]byte, plen, plen) rlen, err := r.Read(preamble) if rlen == 0 { return ct, r, errors.New("empty response") } if err != nil && err != io.EOF { return ct, r, err } preambleR := bytes.NewReader(preamble[:rlen]) bodyReader := ioutil.NopCloser(io.MultiReader(preambleR, r)) // Some web servers will use application/octet-stream as the default // content type for files without an extension (e.g. 'Dockerfile') // so if we receive this value we better check for text content contentType := ct if len(ct) == 0 || ct == mimeTypes.OctetStream { contentType, _, err = detectContentType(preamble) if err != nil { return contentType, bodyReader, err } } contentType = selectAcceptableMIME(contentType) var cterr error if len(contentType) == 0 { cterr = fmt.Errorf("unsupported Content-Type %q", ct) contentType = ct } return contentType, bodyReader, cterr } func selectAcceptableMIME(ct string) string { return mimeRe.FindString(ct) }