github.com/jgadling/terraform@v0.3.8-0.20150227214559-abd68c2c87bc/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  }