github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/getproviders/http_mirror_source.go (about) 1 package getproviders 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "log" 9 "mime" 10 "net/http" 11 "net/url" 12 "path" 13 "strings" 14 15 "github.com/hashicorp/go-retryablehttp" 16 svchost "github.com/hashicorp/terraform-svchost" 17 svcauth "github.com/hashicorp/terraform-svchost/auth" 18 "golang.org/x/net/idna" 19 20 "github.com/hashicorp/terraform/internal/addrs" 21 "github.com/hashicorp/terraform/internal/httpclient" 22 "github.com/hashicorp/terraform/internal/logging" 23 "github.com/hashicorp/terraform/version" 24 ) 25 26 // HTTPMirrorSource is a source that reads provider metadata from a provider 27 // mirror that is accessible over the HTTP provider mirror protocol. 28 type HTTPMirrorSource struct { 29 baseURL *url.URL 30 creds svcauth.CredentialsSource 31 httpClient *retryablehttp.Client 32 } 33 34 var _ Source = (*HTTPMirrorSource)(nil) 35 36 // NewHTTPMirrorSource constructs and returns a new network mirror source with 37 // the given base URL. The relative URL offsets defined by the HTTP mirror 38 // protocol will be resolve relative to the given URL. 39 // 40 // The given URL must use the "https" scheme, or this function will panic. 41 // (When the URL comes from user input, such as in the CLI config, it's the 42 // UI/config layer's responsibility to validate this and return a suitable 43 // error message for the end-user audience.) 44 func NewHTTPMirrorSource(baseURL *url.URL, creds svcauth.CredentialsSource) *HTTPMirrorSource { 45 httpClient := httpclient.New() 46 httpClient.Timeout = requestTimeout 47 httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { 48 // If we get redirected more than five times we'll assume we're 49 // in a redirect loop and bail out, rather than hanging forever. 50 if len(via) > 5 { 51 return fmt.Errorf("too many redirects") 52 } 53 return nil 54 } 55 return newHTTPMirrorSourceWithHTTPClient(baseURL, creds, httpClient) 56 } 57 58 func newHTTPMirrorSourceWithHTTPClient(baseURL *url.URL, creds svcauth.CredentialsSource, httpClient *http.Client) *HTTPMirrorSource { 59 if baseURL.Scheme != "https" { 60 panic("non-https URL for HTTP mirror") 61 } 62 63 // We borrow the retry settings and behaviors from the registry client, 64 // because our needs here are very similar to those of the registry client. 65 retryableClient := retryablehttp.NewClient() 66 retryableClient.HTTPClient = httpClient 67 retryableClient.RetryMax = discoveryRetry 68 retryableClient.RequestLogHook = requestLogHook 69 retryableClient.ErrorHandler = maxRetryErrorHandler 70 71 retryableClient.Logger = log.New(logging.LogOutput(), "", log.Flags()) 72 73 return &HTTPMirrorSource{ 74 baseURL: baseURL, 75 creds: creds, 76 httpClient: retryableClient, 77 } 78 } 79 80 // AvailableVersions retrieves the available versions for the given provider 81 // from the object's underlying HTTP mirror service. 82 func (s *HTTPMirrorSource) AvailableVersions(ctx context.Context, provider addrs.Provider) (VersionList, Warnings, error) { 83 log.Printf("[DEBUG] Querying available versions of provider %s at network mirror %s", provider.String(), s.baseURL.String()) 84 85 endpointPath := path.Join( 86 provider.Hostname.String(), 87 provider.Namespace, 88 provider.Type, 89 "index.json", 90 ) 91 92 statusCode, body, finalURL, err := s.get(ctx, endpointPath) 93 defer func() { 94 if body != nil { 95 body.Close() 96 } 97 }() 98 if err != nil { 99 return nil, nil, s.errQueryFailed(provider, err) 100 } 101 102 switch statusCode { 103 case http.StatusOK: 104 // Great! 105 case http.StatusNotFound: 106 return nil, nil, ErrProviderNotFound{ 107 Provider: provider, 108 } 109 case http.StatusUnauthorized, http.StatusForbidden: 110 return nil, nil, s.errUnauthorized(finalURL) 111 default: 112 return nil, nil, s.errQueryFailed(provider, fmt.Errorf("server returned unsuccessful status %d", statusCode)) 113 } 114 115 // If we got here then the response had status OK and so our body 116 // will be non-nil and should contain some JSON for us to parse. 117 type ResponseBody struct { 118 Versions map[string]struct{} `json:"versions"` 119 } 120 var bodyContent ResponseBody 121 122 dec := json.NewDecoder(body) 123 if err := dec.Decode(&bodyContent); err != nil { 124 return nil, nil, s.errQueryFailed(provider, fmt.Errorf("invalid response content from mirror server: %s", err)) 125 } 126 127 if len(bodyContent.Versions) == 0 { 128 return nil, nil, nil 129 } 130 ret := make(VersionList, 0, len(bodyContent.Versions)) 131 for versionStr := range bodyContent.Versions { 132 version, err := ParseVersion(versionStr) 133 if err != nil { 134 log.Printf("[WARN] Ignoring invalid %s version string %q in provider mirror response", provider, versionStr) 135 continue 136 } 137 ret = append(ret, version) 138 } 139 140 ret.Sort() 141 return ret, nil, nil 142 } 143 144 // PackageMeta retrieves metadata for the requested provider package 145 // from the object's underlying HTTP mirror service. 146 func (s *HTTPMirrorSource) PackageMeta(ctx context.Context, provider addrs.Provider, version Version, target Platform) (PackageMeta, error) { 147 log.Printf("[DEBUG] Finding package URL for %s v%s on %s via network mirror %s", provider.String(), version.String(), target.String(), s.baseURL.String()) 148 149 endpointPath := path.Join( 150 provider.Hostname.String(), 151 provider.Namespace, 152 provider.Type, 153 version.String()+".json", 154 ) 155 156 statusCode, body, finalURL, err := s.get(ctx, endpointPath) 157 defer func() { 158 if body != nil { 159 body.Close() 160 } 161 }() 162 if err != nil { 163 return PackageMeta{}, s.errQueryFailed(provider, err) 164 } 165 166 switch statusCode { 167 case http.StatusOK: 168 // Great! 169 case http.StatusNotFound: 170 // A 404 Not Found for a version we previously saw in index.json is 171 // a protocol error, so we'll report this as "query failed. 172 return PackageMeta{}, s.errQueryFailed(provider, fmt.Errorf("provider mirror does not have archive index for previously-reported %s version %s", provider, version)) 173 case http.StatusUnauthorized, http.StatusForbidden: 174 return PackageMeta{}, s.errUnauthorized(finalURL) 175 default: 176 return PackageMeta{}, s.errQueryFailed(provider, fmt.Errorf("server returned unsuccessful status %d", statusCode)) 177 } 178 179 // If we got here then the response had status OK and so our body 180 // will be non-nil and should contain some JSON for us to parse. 181 type ResponseArchiveMeta struct { 182 RelativeURL string `json:"url"` 183 Hashes []string 184 } 185 type ResponseBody struct { 186 Archives map[string]*ResponseArchiveMeta `json:"archives"` 187 } 188 var bodyContent ResponseBody 189 190 dec := json.NewDecoder(body) 191 if err := dec.Decode(&bodyContent); err != nil { 192 return PackageMeta{}, s.errQueryFailed(provider, fmt.Errorf("invalid response content from mirror server: %s", err)) 193 } 194 195 archiveMeta, ok := bodyContent.Archives[target.String()] 196 if !ok { 197 return PackageMeta{}, ErrPlatformNotSupported{ 198 Provider: provider, 199 Version: version, 200 Platform: target, 201 MirrorURL: s.baseURL, 202 } 203 } 204 205 relURL, err := url.Parse(archiveMeta.RelativeURL) 206 if err != nil { 207 return PackageMeta{}, s.errQueryFailed( 208 provider, 209 fmt.Errorf("provider mirror returned invalid URL %q: %s", archiveMeta.RelativeURL, err), 210 ) 211 } 212 absURL := finalURL.ResolveReference(relURL) 213 214 ret := PackageMeta{ 215 Provider: provider, 216 Version: version, 217 TargetPlatform: target, 218 219 Location: PackageHTTPURL(absURL.String()), 220 Filename: path.Base(absURL.Path), 221 } 222 // A network mirror might not provide any hashes at all, in which case 223 // the package has no source-defined authentication whatsoever. 224 if len(archiveMeta.Hashes) > 0 { 225 hashes := make([]Hash, 0, len(archiveMeta.Hashes)) 226 for _, hashStr := range archiveMeta.Hashes { 227 hash, err := ParseHash(hashStr) 228 if err != nil { 229 return PackageMeta{}, s.errQueryFailed( 230 provider, 231 fmt.Errorf("provider mirror returned invalid provider hash %q: %s", hashStr, err), 232 ) 233 } 234 hashes = append(hashes, hash) 235 } 236 ret.Authentication = NewPackageHashAuthentication(target, hashes) 237 } 238 239 return ret, nil 240 } 241 242 // ForDisplay returns a string description of the source for user-facing output. 243 func (s *HTTPMirrorSource) ForDisplay(provider addrs.Provider) string { 244 return "provider mirror at " + s.baseURL.String() 245 } 246 247 // mirrorHost extracts the hostname portion of the configured base URL and 248 // returns it as a svchost.Hostname, normalized in the usual ways. 249 // 250 // If the returned error is non-nil then the given hostname doesn't comply 251 // with the IETF RFC 5891 section 5.3 and 5.4 validation rules, and thus cannot 252 // be interpreted as a valid Terraform service host. The IDNA validation errors 253 // are unfortunately usually not very user-friendly, but they are also 254 // relatively rare because the IDNA normalization rules are quite tolerant. 255 func (s *HTTPMirrorSource) mirrorHost() (svchost.Hostname, error) { 256 return svchostFromURL(s.baseURL) 257 } 258 259 // mirrorHostCredentials returns the HostCredentials, if any, for the hostname 260 // included in the mirror base URL. 261 // 262 // It might return an error if the mirror base URL is invalid, or if the 263 // credentials lookup itself fails. 264 func (s *HTTPMirrorSource) mirrorHostCredentials() (svcauth.HostCredentials, error) { 265 hostname, err := s.mirrorHost() 266 if err != nil { 267 return nil, fmt.Errorf("invalid provider mirror base URL %s: %s", s.baseURL.String(), err) 268 } 269 270 if s.creds == nil { 271 // No host-specific credentials, then. 272 return nil, nil 273 } 274 275 return s.creds.ForHost(hostname) 276 } 277 278 // get is the shared functionality for querying a JSON index from a mirror. 279 // 280 // It only handles the raw HTTP request. The "body" return value is the 281 // reader from the response if and only if the response status code is 200 OK 282 // and the Content-Type is application/json. In all other cases it's nil. 283 // If body is non-nil then the caller must close it after reading it. 284 // 285 // If the "finalURL" return value is not empty then it's the URL that actually 286 // produced the returned response, possibly after following some redirects. 287 func (s *HTTPMirrorSource) get(ctx context.Context, relativePath string) (statusCode int, body io.ReadCloser, finalURL *url.URL, error error) { 288 endpointPath, err := url.Parse(relativePath) 289 if err != nil { 290 // Should never happen because the caller should validate all of the 291 // components it's including in the path. 292 return 0, nil, nil, err 293 } 294 endpointURL := s.baseURL.ResolveReference(endpointPath) 295 296 req, err := retryablehttp.NewRequest("GET", endpointURL.String(), nil) 297 if err != nil { 298 return 0, nil, endpointURL, err 299 } 300 req = req.WithContext(ctx) 301 req.Request.Header.Set(terraformVersionHeader, version.String()) 302 creds, err := s.mirrorHostCredentials() 303 if err != nil { 304 return 0, nil, endpointURL, fmt.Errorf("failed to determine request credentials: %s", err) 305 } 306 if creds != nil { 307 // Note that if the initial requests gets redirected elsewhere 308 // then the credentials will still be included in the new request, 309 // even if they are on a different hostname. This is intentional 310 // and consistent with how we handle credentials for other 311 // Terraform-native services, because the user model is to configure 312 // credentials for the "friendly hostname" they configured, not for 313 // whatever hostname ends up ultimately serving the request as an 314 // implementation detail. 315 creds.PrepareRequest(req.Request) 316 } 317 318 resp, err := s.httpClient.Do(req) 319 if err != nil { 320 return 0, nil, endpointURL, err 321 } 322 defer func() { 323 // If we're not returning the body then we'll close it 324 // before we return. 325 if body == nil { 326 resp.Body.Close() 327 } 328 }() 329 // After this point, our final URL return value should always be the 330 // one from resp.Request, because that takes into account any redirects 331 // we followed along the way. 332 finalURL = resp.Request.URL 333 334 if resp.StatusCode == http.StatusOK { 335 // If and only if we get an OK response, we'll check that the response 336 // type is JSON and return the body reader. 337 ct := resp.Header.Get("Content-Type") 338 mt, params, err := mime.ParseMediaType(ct) 339 if err != nil { 340 return 0, nil, finalURL, fmt.Errorf("response has invalid Content-Type: %s", err) 341 } 342 if mt != "application/json" { 343 return 0, nil, finalURL, fmt.Errorf("response has invalid Content-Type: must be application/json") 344 } 345 for name := range params { 346 // The application/json content-type has no defined parameters, 347 // but some servers are configured to include a redundant "charset" 348 // parameter anyway, presumably out of a sense of completeness. 349 // We'll ignore them but warn that we're ignoring them in case the 350 // subsequent parsing fails due to the server trying to use an 351 // unsupported character encoding. (RFC 7159 defines its own 352 // JSON-specific character encoding rules.) 353 log.Printf("[WARN] Network mirror returned %q as part of its JSON content type, which is not defined. Ignoring.", name) 354 } 355 body = resp.Body 356 } 357 358 return resp.StatusCode, body, finalURL, nil 359 } 360 361 func (s *HTTPMirrorSource) errQueryFailed(provider addrs.Provider, err error) error { 362 if err == context.Canceled { 363 // This one has a special error type so that callers can 364 // handle it in a different way. 365 return ErrRequestCanceled{} 366 } 367 return ErrQueryFailed{ 368 Provider: provider, 369 Wrapped: err, 370 MirrorURL: s.baseURL, 371 } 372 } 373 374 func (s *HTTPMirrorSource) errUnauthorized(finalURL *url.URL) error { 375 hostname, err := svchostFromURL(finalURL) 376 if err != nil { 377 // Again, weird but we'll tolerate it. 378 return fmt.Errorf("invalid credentials for %s", finalURL) 379 } 380 381 return ErrUnauthorized{ 382 Hostname: hostname, 383 384 // We can't easily tell from here whether we had credentials or 385 // not, so for now we'll just assume we did because "host rejected 386 // the given credentials" is, hopefully, still understandable in 387 // the event that there were none. (If this ends up being confusing 388 // in practice then we'll need to do some refactoring of how 389 // we handle credentials in this source.) 390 HaveCredentials: true, 391 } 392 } 393 394 func svchostFromURL(u *url.URL) (svchost.Hostname, error) { 395 raw := u.Host 396 397 // When "friendly hostnames" appear in Terraform-specific identifiers we 398 // typically constrain their syntax more strictly than the 399 // Internationalized Domain Name specifications call for, such as 400 // forbidding direct use of punycode, but in this case we're just 401 // working with a standard http: or https: URL and so we'll first use the 402 // IDNA "lookup" rules directly, with no additional notational constraints, 403 // to effectively normalize away the differences that would normally 404 // produce an error. 405 var portPortion string 406 if colonPos := strings.Index(raw, ":"); colonPos != -1 { 407 raw, portPortion = raw[:colonPos], raw[colonPos:] 408 } 409 // HTTPMirrorSource requires all URLs to be https URLs, because running 410 // a network mirror over HTTP would potentially transmit any configured 411 // credentials in cleartext. Therefore we don't need to do any special 412 // handling of default ports here, because svchost.Hostname already 413 // considers the absense of a port to represent the standard HTTPS port 414 // 443, and will normalize away an explicit specification of port 443 415 // in svchost.ForComparison below. 416 417 normalized, err := idna.Display.ToUnicode(raw) 418 if err != nil { 419 return svchost.Hostname(""), err 420 } 421 422 // If ToUnicode succeeded above then "normalized" is now a hostname in the 423 // normalized IDNA form, with any direct punycode already interpreted and 424 // the case folding and other normalization rules applied. It should 425 // therefore now be accepted by svchost.ForComparison with no additional 426 // errors, but the port portion can still potentially be invalid. 427 return svchost.ForComparison(normalized + portPortion) 428 }