remote.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. package remotecontext // import "github.com/docker/docker/builder/remotecontext"
  2. import (
  3. "bytes"
  4. "fmt"
  5. "io"
  6. "net"
  7. "net/http"
  8. "net/url"
  9. "regexp"
  10. "github.com/docker/docker/errdefs"
  11. "github.com/docker/docker/pkg/ioutils"
  12. "github.com/pkg/errors"
  13. )
  14. // When downloading remote contexts, limit the amount (in bytes)
  15. // to be read from the response body in order to detect its Content-Type
  16. const maxPreambleLength = 100
  17. const acceptableRemoteMIME = `(?:application/(?:(?:x\-)?tar|octet\-stream|((?:x\-)?(?:gzip|bzip2?|xz)))|(?:text/plain))`
  18. var mimeRe = regexp.MustCompile(acceptableRemoteMIME)
  19. // downloadRemote context from a url and returns it, along with the parsed content type
  20. func downloadRemote(remoteURL string) (string, io.ReadCloser, error) {
  21. response, err := GetWithStatusError(remoteURL)
  22. if err != nil {
  23. return "", nil, errors.Wrapf(err, "error downloading remote context %s", remoteURL)
  24. }
  25. contentType, contextReader, err := inspectResponse(
  26. response.Header.Get("Content-Type"),
  27. response.Body,
  28. response.ContentLength)
  29. if err != nil {
  30. response.Body.Close()
  31. return "", nil, errors.Wrapf(err, "error detecting content type for remote %s", remoteURL)
  32. }
  33. return contentType, ioutils.NewReadCloserWrapper(contextReader, response.Body.Close), nil
  34. }
  35. // GetWithStatusError does an http.Get() and returns an error if the
  36. // status code is 4xx or 5xx.
  37. func GetWithStatusError(address string) (resp *http.Response, err error) {
  38. // #nosec G107
  39. if resp, err = http.Get(address); err != nil {
  40. if uerr, ok := err.(*url.Error); ok {
  41. if derr, ok := uerr.Err.(*net.DNSError); ok && !derr.IsTimeout {
  42. return nil, errdefs.NotFound(err)
  43. }
  44. }
  45. return nil, errdefs.System(err)
  46. }
  47. if resp.StatusCode < 400 {
  48. return resp, nil
  49. }
  50. msg := fmt.Sprintf("failed to GET %s with status %s", address, resp.Status)
  51. body, err := io.ReadAll(resp.Body)
  52. resp.Body.Close()
  53. if err != nil {
  54. return nil, errdefs.System(errors.New(msg + ": error reading body"))
  55. }
  56. msg += ": " + string(bytes.TrimSpace(body))
  57. switch resp.StatusCode {
  58. case http.StatusNotFound:
  59. return nil, errdefs.NotFound(errors.New(msg))
  60. case http.StatusBadRequest:
  61. return nil, errdefs.InvalidParameter(errors.New(msg))
  62. case http.StatusUnauthorized:
  63. return nil, errdefs.Unauthorized(errors.New(msg))
  64. case http.StatusForbidden:
  65. return nil, errdefs.Forbidden(errors.New(msg))
  66. }
  67. return nil, errdefs.Unknown(errors.New(msg))
  68. }
  69. // inspectResponse looks into the http response data at r to determine whether its
  70. // content-type is on the list of acceptable content types for remote build contexts.
  71. // This function returns:
  72. // - a string representation of the detected content-type
  73. // - an io.Reader for the response body
  74. // - an error value which will be non-nil either when something goes wrong while
  75. // reading bytes from r or when the detected content-type is not acceptable.
  76. func inspectResponse(ct string, r io.Reader, clen int64) (string, io.Reader, error) {
  77. plen := clen
  78. if plen <= 0 || plen > maxPreambleLength {
  79. plen = maxPreambleLength
  80. }
  81. preamble := make([]byte, plen)
  82. rlen, err := r.Read(preamble)
  83. if rlen == 0 {
  84. return ct, r, errors.New("empty response")
  85. }
  86. if err != nil && err != io.EOF {
  87. return ct, r, err
  88. }
  89. preambleR := bytes.NewReader(preamble[:rlen])
  90. bodyReader := io.MultiReader(preambleR, r)
  91. // Some web servers will use application/octet-stream as the default
  92. // content type for files without an extension (e.g. 'Dockerfile')
  93. // so if we receive this value we better check for text content
  94. contentType := ct
  95. if len(ct) == 0 || ct == mimeTypeOctetStream {
  96. contentType, err = detectContentType(preamble)
  97. if err != nil {
  98. return contentType, bodyReader, err
  99. }
  100. }
  101. contentType = selectAcceptableMIME(contentType)
  102. var cterr error
  103. if len(contentType) == 0 {
  104. cterr = fmt.Errorf("unsupported Content-Type %q", ct)
  105. contentType = ct
  106. }
  107. return contentType, bodyReader, cterr
  108. }
  109. func selectAcceptableMIME(ct string) string {
  110. return mimeRe.FindString(ct)
  111. }