github.com/anuaimi/terraform@v0.6.4-0.20150904235404-2bf9aec61da8/config/module/get_http.go (about) 1 package module 2 3 import ( 4 "encoding/xml" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "net/http" 9 "net/url" 10 "os" 11 "path/filepath" 12 "strings" 13 ) 14 15 // HttpGetter is a Getter implementation that will download a module from 16 // an HTTP endpoint. The protocol for downloading a module from an HTTP 17 // endpoing is as follows: 18 // 19 // An HTTP GET request is made to the URL with the additional GET parameter 20 // "terraform-get=1". This lets you handle that scenario specially if you 21 // wish. The response must be a 2xx. 22 // 23 // First, a header is looked for "X-Terraform-Get" which should contain 24 // a source URL to download. 25 // 26 // If the header is not present, then a meta tag is searched for named 27 // "terraform-get" and the content should be a source URL. 28 // 29 // The source URL, whether from the header or meta tag, must be a fully 30 // formed URL. The shorthand syntax of "github.com/foo/bar" or relative 31 // paths are not allowed. 32 type HttpGetter struct{} 33 34 func (g *HttpGetter) Get(dst string, u *url.URL) error { 35 // Copy the URL so we can modify it 36 var newU url.URL = *u 37 u = &newU 38 39 // Add terraform-get to the parameter. 40 q := u.Query() 41 q.Add("terraform-get", "1") 42 u.RawQuery = q.Encode() 43 44 // Get the URL 45 resp, err := http.Get(u.String()) 46 if err != nil { 47 return err 48 } 49 defer resp.Body.Close() 50 if resp.StatusCode < 200 || resp.StatusCode >= 300 { 51 return fmt.Errorf("bad response code: %d", resp.StatusCode) 52 } 53 54 // Extract the source URL 55 var source string 56 if v := resp.Header.Get("X-Terraform-Get"); v != "" { 57 source = v 58 } else { 59 source, err = g.parseMeta(resp.Body) 60 if err != nil { 61 return err 62 } 63 } 64 if source == "" { 65 return fmt.Errorf("no source URL was returned") 66 } 67 68 // If there is a subdir component, then we download the root separately 69 // into a temporary directory, then copy over the proper subdir. 70 source, subDir := getDirSubdir(source) 71 if subDir == "" { 72 return Get(dst, source) 73 } 74 75 // We have a subdir, time to jump some hoops 76 return g.getSubdir(dst, source, subDir) 77 } 78 79 // getSubdir downloads the source into the destination, but with 80 // the proper subdir. 81 func (g *HttpGetter) getSubdir(dst, source, subDir string) error { 82 // Create a temporary directory to store the full source 83 td, err := ioutil.TempDir("", "tf") 84 if err != nil { 85 return err 86 } 87 defer os.RemoveAll(td) 88 89 // Download that into the given directory 90 if err := Get(td, source); err != nil { 91 return err 92 } 93 94 // Make sure the subdir path actually exists 95 sourcePath := filepath.Join(td, subDir) 96 if _, err := os.Stat(sourcePath); err != nil { 97 return fmt.Errorf( 98 "Error downloading %s: %s", source, err) 99 } 100 101 // Copy the subdirectory into our actual destination. 102 if err := os.RemoveAll(dst); err != nil { 103 return err 104 } 105 106 // Make the final destination 107 if err := os.MkdirAll(dst, 0755); err != nil { 108 return err 109 } 110 111 return copyDir(dst, sourcePath) 112 } 113 114 // parseMeta looks for the first meta tag in the given reader that 115 // will give us the source URL. 116 func (g *HttpGetter) parseMeta(r io.Reader) (string, error) { 117 d := xml.NewDecoder(r) 118 d.CharsetReader = charsetReader 119 d.Strict = false 120 var err error 121 var t xml.Token 122 for { 123 t, err = d.Token() 124 if err != nil { 125 if err == io.EOF { 126 err = nil 127 } 128 return "", err 129 } 130 if e, ok := t.(xml.StartElement); ok && strings.EqualFold(e.Name.Local, "body") { 131 return "", nil 132 } 133 if e, ok := t.(xml.EndElement); ok && strings.EqualFold(e.Name.Local, "head") { 134 return "", nil 135 } 136 e, ok := t.(xml.StartElement) 137 if !ok || !strings.EqualFold(e.Name.Local, "meta") { 138 continue 139 } 140 if attrValue(e.Attr, "name") != "terraform-get" { 141 continue 142 } 143 if f := attrValue(e.Attr, "content"); f != "" { 144 return f, nil 145 } 146 } 147 } 148 149 // attrValue returns the attribute value for the case-insensitive key 150 // `name', or the empty string if nothing is found. 151 func attrValue(attrs []xml.Attr, name string) string { 152 for _, a := range attrs { 153 if strings.EqualFold(a.Name.Local, name) { 154 return a.Value 155 } 156 } 157 return "" 158 } 159 160 // charsetReader returns a reader for the given charset. Currently 161 // it only supports UTF-8 and ASCII. Otherwise, it returns a meaningful 162 // error which is printed by go get, so the user can find why the package 163 // wasn't downloaded if the encoding is not supported. Note that, in 164 // order to reduce potential errors, ASCII is treated as UTF-8 (i.e. characters 165 // greater than 0x7f are not rejected). 166 func charsetReader(charset string, input io.Reader) (io.Reader, error) { 167 switch strings.ToLower(charset) { 168 case "ascii": 169 return input, nil 170 default: 171 return nil, fmt.Errorf("can't decode XML document using charset %q", charset) 172 } 173 }