2017-03-20 18:22:29 -04:00
|
|
|
package remotecontext
|
2015-09-06 13:26:40 -04:00
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
2017-07-19 10:20:13 -04:00
|
|
|
"net"
|
2017-06-01 17:05:44 -04:00
|
|
|
"net/http"
|
2017-07-19 10:20:13 -04:00
|
|
|
"net/url"
|
2015-09-06 13:26:40 -04:00
|
|
|
"regexp"
|
|
|
|
|
2017-11-08 13:06:57 -05:00
|
|
|
"github.com/docker/docker/pkg/ioutils"
|
2017-06-01 17:05:44 -04:00
|
|
|
"github.com/pkg/errors"
|
2015-09-06 13:26:40 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
2017-11-08 13:06:57 -05:00
|
|
|
// 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)
|
2015-09-06 13:26:40 -04:00
|
|
|
if err != nil {
|
2017-11-08 13:06:57 -05:00
|
|
|
return "", nil, fmt.Errorf("error downloading remote context %s: %v", remoteURL, err)
|
2015-09-06 13:26:40 -04:00
|
|
|
}
|
|
|
|
|
2017-11-08 13:06:57 -05:00
|
|
|
contentType, contextReader, err := inspectResponse(
|
|
|
|
response.Header.Get("Content-Type"),
|
|
|
|
response.Body,
|
|
|
|
response.ContentLength)
|
|
|
|
if err != nil {
|
|
|
|
response.Body.Close()
|
|
|
|
return "", nil, fmt.Errorf("error detecting content type for remote %s: %v", remoteURL, err)
|
2015-09-06 13:26:40 -04:00
|
|
|
}
|
|
|
|
|
2017-11-08 13:06:57 -05:00
|
|
|
return contentType, ioutils.NewReadCloserWrapper(contextReader, response.Body.Close), nil
|
2015-09-06 13:26:40 -04:00
|
|
|
}
|
|
|
|
|
2017-06-01 17:05:44 -04:00
|
|
|
// GetWithStatusError does an http.Get() and returns an error if the
|
|
|
|
// status code is 4xx or 5xx.
|
2017-07-19 10:20:13 -04:00
|
|
|
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}
|
2017-06-01 17:05:44 -04:00
|
|
|
}
|
|
|
|
if resp.StatusCode < 400 {
|
|
|
|
return resp, nil
|
|
|
|
}
|
2017-07-19 10:20:13 -04:00
|
|
|
msg := fmt.Sprintf("failed to GET %s with status %s", address, resp.Status)
|
2017-06-01 17:05:44 -04:00
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
|
|
resp.Body.Close()
|
|
|
|
if err != nil {
|
2017-07-19 10:20:13 -04:00
|
|
|
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)
|
2017-06-01 17:05:44 -04:00
|
|
|
}
|
2017-07-19 10:20:13 -04:00
|
|
|
return nil, unknownError{errors.New(msg)}
|
2017-06-01 17:05:44 -04:00
|
|
|
}
|
|
|
|
|
2015-09-06 13:26:40 -04:00
|
|
|
// 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.
|
2017-11-08 13:06:57 -05:00
|
|
|
func inspectResponse(ct string, r io.Reader, clen int64) (string, io.Reader, error) {
|
2015-09-06 13:26:40 -04:00
|
|
|
plen := clen
|
|
|
|
if plen <= 0 || plen > maxPreambleLength {
|
|
|
|
plen = maxPreambleLength
|
|
|
|
}
|
|
|
|
|
2017-09-11 14:55:05 -04:00
|
|
|
preamble := make([]byte, plen)
|
2015-09-06 13:26:40 -04:00
|
|
|
rlen, err := r.Read(preamble)
|
|
|
|
if rlen == 0 {
|
2017-11-08 13:06:57 -05:00
|
|
|
return ct, r, errors.New("empty response")
|
2015-09-06 13:26:40 -04:00
|
|
|
}
|
|
|
|
if err != nil && err != io.EOF {
|
2017-11-08 13:06:57 -05:00
|
|
|
return ct, r, err
|
2015-09-06 13:26:40 -04:00
|
|
|
}
|
|
|
|
|
2017-01-29 08:01:53 -05:00
|
|
|
preambleR := bytes.NewReader(preamble[:rlen])
|
2017-11-08 13:06:57 -05:00
|
|
|
bodyReader := io.MultiReader(preambleR, r)
|
2015-09-06 13:26:40 -04:00
|
|
|
// 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
|
2017-06-01 15:46:27 -04:00
|
|
|
if len(ct) == 0 || ct == mimeTypes.OctetStream {
|
|
|
|
contentType, _, err = detectContentType(preamble)
|
2015-09-06 13:26:40 -04:00
|
|
|
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)
|
|
|
|
}
|