github.com/pdecat/terraform@v0.11.9-beta1/svchost/disco/disco.go (about) 1 // Package disco handles Terraform's remote service discovery protocol. 2 // 3 // This protocol allows mapping from a service hostname, as produced by the 4 // svchost package, to a set of services supported by that host and the 5 // endpoint information for each supported service. 6 package disco 7 8 import ( 9 "encoding/json" 10 "errors" 11 "io" 12 "io/ioutil" 13 "log" 14 "mime" 15 "net/http" 16 "net/url" 17 "time" 18 19 cleanhttp "github.com/hashicorp/go-cleanhttp" 20 "github.com/hashicorp/terraform/svchost" 21 "github.com/hashicorp/terraform/svchost/auth" 22 ) 23 24 const ( 25 discoPath = "/.well-known/terraform.json" 26 maxRedirects = 3 // arbitrary-but-small number to prevent runaway redirect loops 27 discoTimeout = 11 * time.Second // arbitrary-but-small time limit to prevent UI "hangs" during discovery 28 maxDiscoDocBytes = 1 * 1024 * 1024 // 1MB - to prevent abusive services from using loads of our memory 29 ) 30 31 var httpTransport = cleanhttp.DefaultPooledTransport() // overridden during tests, to skip TLS verification 32 33 // Disco is the main type in this package, which allows discovery on given 34 // hostnames and caches the results by hostname to avoid repeated requests 35 // for the same information. 36 type Disco struct { 37 hostCache map[svchost.Hostname]Host 38 credsSrc auth.CredentialsSource 39 40 // Transport is a custom http.RoundTripper to use. 41 // A package default is used if this is nil. 42 Transport http.RoundTripper 43 } 44 45 // New returns a new initialized discovery object. 46 func New() *Disco { 47 return NewWithCredentialsSource(nil) 48 } 49 50 // NewWithCredentialsSource returns a new discovery object initialized with 51 // the given credentials source. 52 func NewWithCredentialsSource(credsSrc auth.CredentialsSource) *Disco { 53 return &Disco{credsSrc: credsSrc} 54 } 55 56 // SetCredentialsSource provides a credentials source that will be used to 57 // add credentials to outgoing discovery requests, where available. 58 // 59 // If this method is never called, no outgoing discovery requests will have 60 // credentials. 61 func (d *Disco) SetCredentialsSource(src auth.CredentialsSource) { 62 d.credsSrc = src 63 } 64 65 // CredentialsForHost returns a non-nil HostCredentials if the embedded source has 66 // credentials available for the host, and a nil HostCredentials if it does not. 67 func (d *Disco) CredentialsForHost(host svchost.Hostname) (auth.HostCredentials, error) { 68 if d.credsSrc == nil { 69 return nil, nil 70 } 71 return d.credsSrc.ForHost(host) 72 } 73 74 // ForceHostServices provides a pre-defined set of services for a given 75 // host, which prevents the receiver from attempting network-based discovery 76 // for the given host. Instead, the given services map will be returned 77 // verbatim. 78 // 79 // When providing "forced" services, any relative URLs are resolved against 80 // the initial discovery URL that would have been used for network-based 81 // discovery, yielding the same results as if the given map were published 82 // at the host's default discovery URL, though using absolute URLs is strongly 83 // recommended to make the configured behavior more explicit. 84 func (d *Disco) ForceHostServices(host svchost.Hostname, services map[string]interface{}) { 85 if d.hostCache == nil { 86 d.hostCache = map[svchost.Hostname]Host{} 87 } 88 if services == nil { 89 services = map[string]interface{}{} 90 } 91 d.hostCache[host] = Host{ 92 discoURL: &url.URL{ 93 Scheme: "https", 94 Host: string(host), 95 Path: discoPath, 96 }, 97 services: services, 98 } 99 } 100 101 // Discover runs the discovery protocol against the given hostname (which must 102 // already have been validated and prepared with svchost.ForComparison) and 103 // returns an object describing the services available at that host. 104 // 105 // If a given hostname supports no Terraform services at all, a non-nil but 106 // empty Host object is returned. When giving feedback to the end user about 107 // such situations, we say e.g. "the host <name> doesn't provide a module 108 // registry", regardless of whether that is due to that service specifically 109 // being absent or due to the host not providing Terraform services at all, 110 // since we don't wish to expose the detail of whole-host discovery to an 111 // end-user. 112 func (d *Disco) Discover(host svchost.Hostname) Host { 113 if d.hostCache == nil { 114 d.hostCache = map[svchost.Hostname]Host{} 115 } 116 if cache, cached := d.hostCache[host]; cached { 117 return cache 118 } 119 120 ret := d.discover(host) 121 d.hostCache[host] = ret 122 return ret 123 } 124 125 // DiscoverServiceURL is a convenience wrapper for discovery on a given 126 // hostname and then looking up a particular service in the result. 127 func (d *Disco) DiscoverServiceURL(host svchost.Hostname, serviceID string) *url.URL { 128 return d.Discover(host).ServiceURL(serviceID) 129 } 130 131 // discover implements the actual discovery process, with its result cached 132 // by the public-facing Discover method. 133 func (d *Disco) discover(host svchost.Hostname) Host { 134 discoURL := &url.URL{ 135 Scheme: "https", 136 Host: host.String(), 137 Path: discoPath, 138 } 139 140 t := d.Transport 141 if t == nil { 142 t = httpTransport 143 } 144 145 client := &http.Client{ 146 Transport: t, 147 Timeout: discoTimeout, 148 149 CheckRedirect: func(req *http.Request, via []*http.Request) error { 150 log.Printf("[DEBUG] Service discovery redirected to %s", req.URL) 151 if len(via) > maxRedirects { 152 return errors.New("too many redirects") // (this error message will never actually be seen) 153 } 154 return nil 155 }, 156 } 157 158 req := &http.Request{ 159 Method: "GET", 160 URL: discoURL, 161 } 162 163 if creds, err := d.CredentialsForHost(host); err != nil { 164 log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", host, err) 165 } else if creds != nil { 166 creds.PrepareRequest(req) // alters req to include credentials 167 } 168 169 log.Printf("[DEBUG] Service discovery for %s at %s", host, discoURL) 170 171 ret := Host{ 172 discoURL: discoURL, 173 } 174 175 resp, err := client.Do(req) 176 if err != nil { 177 log.Printf("[WARN] Failed to request discovery document: %s", err) 178 return ret // empty 179 } 180 defer resp.Body.Close() 181 182 if resp.StatusCode != 200 { 183 log.Printf("[WARN] Failed to request discovery document: %s", resp.Status) 184 return ret // empty 185 } 186 187 // If the client followed any redirects, we will have a new URL to use 188 // as our base for relative resolution. 189 ret.discoURL = resp.Request.URL 190 191 contentType := resp.Header.Get("Content-Type") 192 mediaType, _, err := mime.ParseMediaType(contentType) 193 if err != nil { 194 log.Printf("[WARN] Discovery URL has malformed Content-Type %q", contentType) 195 return ret // empty 196 } 197 if mediaType != "application/json" { 198 log.Printf("[DEBUG] Discovery URL returned Content-Type %q, rather than application/json", mediaType) 199 return ret // empty 200 } 201 202 // (this doesn't catch chunked encoding, because ContentLength is -1 in that case...) 203 if resp.ContentLength > maxDiscoDocBytes { 204 // Size limit here is not a contractual requirement and so we may 205 // adjust it over time if we find a different limit is warranted. 206 log.Printf("[WARN] Discovery doc response is too large (got %d bytes; limit %d)", resp.ContentLength, maxDiscoDocBytes) 207 return ret // empty 208 } 209 210 // If the response is using chunked encoding then we can't predict 211 // its size, but we'll at least prevent reading the entire thing into 212 // memory. 213 lr := io.LimitReader(resp.Body, maxDiscoDocBytes) 214 215 servicesBytes, err := ioutil.ReadAll(lr) 216 if err != nil { 217 log.Printf("[WARN] Error reading discovery document body: %s", err) 218 return ret // empty 219 } 220 221 var services map[string]interface{} 222 err = json.Unmarshal(servicesBytes, &services) 223 if err != nil { 224 log.Printf("[WARN] Failed to decode discovery document as a JSON object: %s", err) 225 return ret // empty 226 } 227 228 ret.services = services 229 return ret 230 } 231 232 // Forget invalidates any cached record of the given hostname. If the host 233 // has no cache entry then this is a no-op. 234 func (d *Disco) Forget(host svchost.Hostname) { 235 delete(d.hostCache, host) 236 } 237 238 // ForgetAll is like Forget, but for all of the hostnames that have cache entries. 239 func (d *Disco) ForgetAll() { 240 d.hostCache = nil 241 }