13 "github.com/hashicorp/go-safetemp"
16 // HttpGetter is a Getter implementation that will download from an HTTP
19 // For file downloads, HTTP is used directly.
21 // The protocol for downloading a directory from an HTTP endpoing is as follows:
23 // An HTTP GET request is made to the URL with the additional GET parameter
24 // "terraform-get=1". This lets you handle that scenario specially if you
25 // wish. The response must be a 2xx.
27 // First, a header is looked for "X-Terraform-Get" which should contain
28 // a source URL to download.
30 // If the header is not present, then a meta tag is searched for named
31 // "terraform-get" and the content should be a source URL.
33 // The source URL, whether from the header or meta tag, must be a fully
34 // formed URL. The shorthand syntax of "github.com/foo/bar" or relative
35 // paths are not allowed.
36 type HttpGetter struct {
37 // Netrc, if true, will lookup and use auth information found
38 // in the user's netrc file if available.
41 // Client is the http.Client to use for Get requests.
42 // This defaults to a cleanhttp.DefaultClient if left unset.
46 func (g *HttpGetter) ClientMode(u *url.URL) (ClientMode, error) {
47 if strings.HasSuffix(u.Path, "/") {
48 return ClientModeDir, nil
50 return ClientModeFile, nil
53 func (g *HttpGetter) Get(dst string, u *url.URL) error {
54 // Copy the URL so we can modify it
59 // Add auth from netrc if we can
60 if err := addAuthFromNetrc(u); err != nil {
69 // Add terraform-get to the parameter.
71 q.Add("terraform-get", "1")
72 u.RawQuery = q.Encode()
75 resp, err := g.Client.Get(u.String())
79 defer resp.Body.Close()
80 if resp.StatusCode < 200 || resp.StatusCode >= 300 {
81 return fmt.Errorf("bad response code: %d", resp.StatusCode)
84 // Extract the source URL
86 if v := resp.Header.Get("X-Terraform-Get"); v != "" {
89 source, err = g.parseMeta(resp.Body)
95 return fmt.Errorf("no source URL was returned")
98 // If there is a subdir component, then we download the root separately
99 // into a temporary directory, then copy over the proper subdir.
100 source, subDir := SourceDirSubdir(source)
102 return Get(dst, source)
105 // We have a subdir, time to jump some hoops
106 return g.getSubdir(dst, source, subDir)
109 func (g *HttpGetter) GetFile(dst string, u *url.URL) error {
111 // Add auth from netrc if we can
112 if err := addAuthFromNetrc(u); err != nil {
118 g.Client = httpClient
121 resp, err := g.Client.Get(u.String())
125 defer resp.Body.Close()
126 if resp.StatusCode != 200 {
127 return fmt.Errorf("bad response code: %d", resp.StatusCode)
130 // Create all the parent directories
131 if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
135 f, err := os.Create(dst)
140 n, err := io.Copy(f, resp.Body)
141 if err == nil && n < resp.ContentLength {
142 err = io.ErrShortWrite
144 if err1 := f.Close(); err == nil {
150 // getSubdir downloads the source into the destination, but with
151 // the proper subdir.
152 func (g *HttpGetter) getSubdir(dst, source, subDir string) error {
153 // Create a temporary directory to store the full source. This has to be
154 // a non-existent directory.
155 td, tdcloser, err := safetemp.Dir("", "getter")
159 defer tdcloser.Close()
161 // Download that into the given directory
162 if err := Get(td, source); err != nil {
166 // Process any globbing
167 sourcePath, err := SubdirGlob(td, subDir)
172 // Make sure the subdir path actually exists
173 if _, err := os.Stat(sourcePath); err != nil {
175 "Error downloading %s: %s", source, err)
178 // Copy the subdirectory into our actual destination.
179 if err := os.RemoveAll(dst); err != nil {
183 // Make the final destination
184 if err := os.MkdirAll(dst, 0755); err != nil {
188 return copyDir(dst, sourcePath, false)
191 // parseMeta looks for the first meta tag in the given reader that
192 // will give us the source URL.
193 func (g *HttpGetter) parseMeta(r io.Reader) (string, error) {
194 d := xml.NewDecoder(r)
195 d.CharsetReader = charsetReader
207 if e, ok := t.(xml.StartElement); ok && strings.EqualFold(e.Name.Local, "body") {
210 if e, ok := t.(xml.EndElement); ok && strings.EqualFold(e.Name.Local, "head") {
213 e, ok := t.(xml.StartElement)
214 if !ok || !strings.EqualFold(e.Name.Local, "meta") {
217 if attrValue(e.Attr, "name") != "terraform-get" {
220 if f := attrValue(e.Attr, "content"); f != "" {
226 // attrValue returns the attribute value for the case-insensitive key
227 // `name', or the empty string if nothing is found.
228 func attrValue(attrs []xml.Attr, name string) string {
229 for _, a := range attrs {
230 if strings.EqualFold(a.Name.Local, name) {
237 // charsetReader returns a reader for the given charset. Currently
238 // it only supports UTF-8 and ASCII. Otherwise, it returns a meaningful
239 // error which is printed by go get, so the user can find why the package
240 // wasn't downloaded if the encoding is not supported. Note that, in
241 // order to reduce potential errors, ASCII is treated as UTF-8 (i.e. characters
242 // greater than 0x7f are not rejected).
243 func charsetReader(charset string, input io.Reader) (io.Reader, error) {
244 switch strings.ToLower(charset) {
248 return nil, fmt.Errorf("can't decode XML document using charset %q", charset)