forked from moby/moby
-
Notifications
You must be signed in to change notification settings - Fork 0
/
remote.go
153 lines (135 loc) · 5.06 KB
/
remote.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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.Reader, clen int64) (string, io.ReadCloser, error) {
plen := clen
if plen <= 0 || plen > maxPreambleLength {
plen = maxPreambleLength
}
preamble := make([]byte, plen)
rlen, err := r.Read(preamble)
if rlen == 0 {
return ct, ioutil.NopCloser(r), errors.New("empty response")
}
if err != nil && err != io.EOF {
return ct, ioutil.NopCloser(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)
}