terraform/config/module/get_http.go

129 lines
3.4 KiB
Go

package module
import (
"encoding/xml"
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
// HttpGetter is a Getter implementation that will download a module from
// an HTTP endpoint. The protocol for downloading a module from an HTTP
// endpoing is as follows:
//
// An HTTP GET request is made to the URL with the additional GET parameter
// "terraform-get=1". This lets you handle that scenario specially if you
// wish. The response must be a 2xx.
//
// First, a header is looked for "X-Terraform-Get" which should contain
// a source URL to download.
//
// If the header is not present, then a meta tag is searched for named
// "terraform-get" and the content should be a source URL.
//
// The source URL, whether from the header or meta tag, must be a fully
// formed URL. The shorthand syntax of "github.com/foo/bar" or relative
// paths are not allowed.
type HttpGetter struct{}
func (g *HttpGetter) Get(dst string, u *url.URL) error {
// Copy the URL so we can modify it
var newU url.URL = *u
u = &newU
// Add terraform-get to the parameter.
q := u.Query()
q.Add("terraform-get", "1")
u.RawQuery = q.Encode()
// Get the URL
resp, err := http.Get(u.String())
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("bad response code: %d", resp.StatusCode)
}
// Extract the source URL
var source string
if v := resp.Header.Get("X-Terraform-Get"); v != "" {
source = v
} else {
source, err = g.parseMeta(resp.Body)
if err != nil {
return err
}
}
if source == "" {
return fmt.Errorf("no source URL was returned")
}
// Get it!
return Get(dst, source)
}
// parseMeta looks for the first meta tag in the given reader that
// will give us the source URL.
func (g *HttpGetter) parseMeta(r io.Reader) (string, error) {
d := xml.NewDecoder(r)
d.CharsetReader = charsetReader
d.Strict = false
var err error
var t xml.Token
for {
t, err = d.Token()
if err != nil {
if err == io.EOF {
err = nil
}
return "", err
}
if e, ok := t.(xml.StartElement); ok && strings.EqualFold(e.Name.Local, "body") {
return "", nil
}
if e, ok := t.(xml.EndElement); ok && strings.EqualFold(e.Name.Local, "head") {
return "", nil
}
e, ok := t.(xml.StartElement)
if !ok || !strings.EqualFold(e.Name.Local, "meta") {
continue
}
if attrValue(e.Attr, "name") != "terraform-get" {
continue
}
if f := attrValue(e.Attr, "content"); f != "" {
return f, nil
}
}
}
// attrValue returns the attribute value for the case-insensitive key
// `name', or the empty string if nothing is found.
func attrValue(attrs []xml.Attr, name string) string {
for _, a := range attrs {
if strings.EqualFold(a.Name.Local, name) {
return a.Value
}
}
return ""
}
// charsetReader returns a reader for the given charset. Currently
// it only supports UTF-8 and ASCII. Otherwise, it returns a meaningful
// error which is printed by go get, so the user can find why the package
// wasn't downloaded if the encoding is not supported. Note that, in
// order to reduce potential errors, ASCII is treated as UTF-8 (i.e. characters
// greater than 0x7f are not rejected).
func charsetReader(charset string, input io.Reader) (io.Reader, error) {
switch strings.ToLower(charset) {
case "ascii":
return input, nil
default:
return nil, fmt.Errorf("can't decode XML document using charset %q", charset)
}
}