remote.go 5.1 KB

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