15 // HttpGetter is a Getter implementation that will download from an HTTP
18 // For file downloads, HTTP is used directly.
20 // The protocol for downloading a directory from an HTTP endpoing is as follows:
22 // An HTTP GET request is made to the URL with the additional GET parameter
23 // "terraform-get=1". This lets you handle that scenario specially if you
24 // wish. The response must be a 2xx.
26 // First, a header is looked for "X-Terraform-Get" which should contain
27 // a source URL to download.
29 // If the header is not present, then a meta tag is searched for named
30 // "terraform-get" and the content should be a source URL.
32 // The source URL, whether from the header or meta tag, must be a fully
33 // formed URL. The shorthand syntax of "github.com/foo/bar" or relative
34 // paths are not allowed.
35 type HttpGetter struct {
36 // Netrc, if true, will lookup and use auth information found
37 // in the user's netrc file if available.
41 func (g *HttpGetter) ClientMode(u *url.URL) (ClientMode, error) {
42 if strings.HasSuffix(u.Path, "/") {
43 return ClientModeDir, nil
45 return ClientModeFile, nil
48 func (g *HttpGetter) Get(dst string, u *url.URL) error {
49 // Copy the URL so we can modify it
54 // Add auth from netrc if we can
55 if err := addAuthFromNetrc(u); err != nil {
60 // Add terraform-get to the parameter.
62 q.Add("terraform-get", "1")
63 u.RawQuery = q.Encode()
66 resp, err := http.Get(u.String())
70 defer resp.Body.Close()
71 if resp.StatusCode < 200 || resp.StatusCode >= 300 {
72 return fmt.Errorf("bad response code: %d", resp.StatusCode)
75 // Extract the source URL
77 if v := resp.Header.Get("X-Terraform-Get"); v != "" {
80 source, err = g.parseMeta(resp.Body)
86 return fmt.Errorf("no source URL was returned")
89 // If there is a subdir component, then we download the root separately
90 // into a temporary directory, then copy over the proper subdir.
91 source, subDir := SourceDirSubdir(source)
93 return Get(dst, source)
96 // We have a subdir, time to jump some hoops
97 return g.getSubdir(dst, source, subDir)
100 func (g *HttpGetter) GetFile(dst string, u *url.URL) error {
101 resp, err := http.Get(u.String())
105 defer resp.Body.Close()
106 if resp.StatusCode != 200 {
107 return fmt.Errorf("bad response code: %d", resp.StatusCode)
110 // Create all the parent directories
111 if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
115 f, err := os.Create(dst)
121 _, err = io.Copy(f, resp.Body)
125 // getSubdir downloads the source into the destination, but with
126 // the proper subdir.
127 func (g *HttpGetter) getSubdir(dst, source, subDir string) error {
128 // Create a temporary directory to store the full source
129 td, err := ioutil.TempDir("", "tf")
133 defer os.RemoveAll(td)
135 // Download that into the given directory
136 if err := Get(td, source); err != nil {
140 // Make sure the subdir path actually exists
141 sourcePath := filepath.Join(td, subDir)
142 if _, err := os.Stat(sourcePath); err != nil {
144 "Error downloading %s: %s", source, err)
147 // Copy the subdirectory into our actual destination.
148 if err := os.RemoveAll(dst); err != nil {
152 // Make the final destination
153 if err := os.MkdirAll(dst, 0755); err != nil {
157 return copyDir(dst, sourcePath, false)
160 // parseMeta looks for the first meta tag in the given reader that
161 // will give us the source URL.
162 func (g *HttpGetter) parseMeta(r io.Reader) (string, error) {
163 d := xml.NewDecoder(r)
164 d.CharsetReader = charsetReader
176 if e, ok := t.(xml.StartElement); ok && strings.EqualFold(e.Name.Local, "body") {
179 if e, ok := t.(xml.EndElement); ok && strings.EqualFold(e.Name.Local, "head") {
182 e, ok := t.(xml.StartElement)
183 if !ok || !strings.EqualFold(e.Name.Local, "meta") {
186 if attrValue(e.Attr, "name") != "terraform-get" {
189 if f := attrValue(e.Attr, "content"); f != "" {
195 // attrValue returns the attribute value for the case-insensitive key
196 // `name', or the empty string if nothing is found.
197 func attrValue(attrs []xml.Attr, name string) string {
198 for _, a := range attrs {
199 if strings.EqualFold(a.Name.Local, name) {
206 // charsetReader returns a reader for the given charset. Currently
207 // it only supports UTF-8 and ASCII. Otherwise, it returns a meaningful
208 // error which is printed by go get, so the user can find why the package
209 // wasn't downloaded if the encoding is not supported. Note that, in
210 // order to reduce potential errors, ASCII is treated as UTF-8 (i.e. characters
211 // greater than 0x7f are not rejected).
212 func charsetReader(charset string, input io.Reader) (io.Reader, error) {
213 switch strings.ToLower(charset) {
217 return nil, fmt.Errorf("can't decode XML document using charset %q", charset)