| package remotecontext // import "github.com/docker/docker/builder/remotecontext" | |
| import ( | |
| "bytes" | |
| "fmt" | |
| "io" | |
| "net" | |
| "net/http" | |
| "net/url" | |
| "regexp" | |
| "github.com/docker/docker/errdefs" | |
| "github.com/docker/docker/pkg/ioutils" | |
| "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) | |
| // downloadRemote context from a url and returns it, along with the parsed content type | |
| func downloadRemote(remoteURL string) (string, io.ReadCloser, error) { | |
| response, err := GetWithStatusError(remoteURL) | |
| if err != nil { | |
| return "", nil, errors.Wrapf(err, "error downloading remote context %s", remoteURL) | |
| } | |
| contentType, contextReader, err := inspectResponse( | |
| response.Header.Get("Content-Type"), | |
| response.Body, | |
| response.ContentLength) | |
| if err != nil { | |
| response.Body.Close() | |
| return "", nil, errors.Wrapf(err, "error detecting content type for remote %s", remoteURL) | |
| } | |
| return contentType, ioutils.NewReadCloserWrapper(contextReader, response.Body.Close), nil | |
| } | |
| // 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) { | |
| resp, err = http.Get(address) // #nosec G107 -- ignore G107: Potential HTTP request made with variable url | |
| if err != nil { | |
| if uerr, ok := err.(*url.Error); ok { | |
| if derr, ok := uerr.Err.(*net.DNSError); ok && !derr.IsTimeout { | |
| return nil, errdefs.NotFound(err) | |
| } | |
| } | |
| return nil, errdefs.System(err) | |
| } | |
| if resp.StatusCode < 400 { | |
| return resp, nil | |
| } | |
| msg := fmt.Sprintf("failed to GET %s with status %s", address, resp.Status) | |
| body, err := io.ReadAll(resp.Body) | |
| resp.Body.Close() | |
| if err != nil { | |
| return nil, errdefs.System(errors.New(msg + ": error reading body")) | |
| } | |
| msg += ": " + string(bytes.TrimSpace(body)) | |
| switch resp.StatusCode { | |
| case http.StatusNotFound: | |
| return nil, errdefs.NotFound(errors.New(msg)) | |
| case http.StatusBadRequest: | |
| return nil, errdefs.InvalidParameter(errors.New(msg)) | |
| case http.StatusUnauthorized: | |
| return nil, errdefs.Unauthorized(errors.New(msg)) | |
| case http.StatusForbidden: | |
| return nil, errdefs.Forbidden(errors.New(msg)) | |
| } | |
| return nil, errdefs.Unknown(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.Reader, clen int64) (string, io.Reader, error) { | |
| plen := clen | |
| if plen <= 0 || plen > maxPreambleLength { | |
| plen = maxPreambleLength | |
| } | |
| preamble := make([]byte, 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 := 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 == mimeTypeOctetStream { | |
| 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) | |
| } |