github.com/verrazzano/verrazzano@v1.7.1/tests/e2e/pkg/web.go (about) 1 // Copyright (c) 2020, 2023, Oracle and/or its affiliates. 2 // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. 3 4 package pkg 5 6 import ( 7 "context" 8 "crypto/x509" 9 "fmt" 10 "io" 11 "net/http" 12 "strings" 13 14 "github.com/hashicorp/go-retryablehttp" 15 "github.com/onsi/gomega" 16 "github.com/onsi/gomega/types" 17 "github.com/verrazzano/verrazzano/pkg/httputil" 18 "github.com/verrazzano/verrazzano/pkg/k8sutil" 19 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 20 ) 21 22 const ( 23 // defaultEnvName - default environment name 24 defaultEnvName = "default" 25 ) 26 27 // HTTPResponse represents an HTTP response including the read body 28 type HTTPResponse struct { 29 StatusCode int 30 Header http.Header 31 Body []byte 32 } 33 34 // GetWebPage makes an HTTP GET request using a retryable client configured with the Verrazzano cert bundle 35 func GetWebPage(url string, hostHeader string) (*HTTPResponse, error) { 36 kubeconfigPath, err := k8sutil.GetKubeConfigLocation() 37 if err != nil { 38 Log(Error, fmt.Sprintf("Error getting kubeconfig, error: %v", err)) 39 return nil, err 40 } 41 42 client, err := GetVerrazzanoHTTPClient(kubeconfigPath) 43 if err != nil { 44 return nil, err 45 } 46 return GetWebPageWithClient(client, url, hostHeader) 47 } 48 49 // GetWebPageInCluster makes an HTTP GET request using a retryable client configured with the Verrazzano cert bundle 50 func GetWebPageInCluster(url string, hostHeader string, kubeconfigPath string) (*HTTPResponse, error) { 51 client, err := GetVerrazzanoHTTPClient(kubeconfigPath) 52 if err != nil { 53 return nil, err 54 } 55 return GetWebPageWithClient(client, url, hostHeader) 56 } 57 58 // GetWebPageWithClient submits a GET request using the specified client. 59 func GetWebPageWithClient(httpClient *retryablehttp.Client, url string, hostHeader string) (*HTTPResponse, error) { 60 return doReq(url, "GET", "", hostHeader, "", "", nil, httpClient) 61 } 62 63 // GetWebPageWithBasicAuth gets a web page using basic auth, using a given kubeconfig 64 func GetWebPageWithBasicAuth(url string, hostHeader string, username string, password string, kubeconfigPath string) (*HTTPResponse, error) { 65 client, err := GetVerrazzanoHTTPClient(kubeconfigPath) 66 if err != nil { 67 return nil, err 68 } 69 return doReq(url, "GET", "", hostHeader, username, password, nil, client) 70 } 71 72 // GetCertificates will return the server SSL certificates for the given URL. 73 func GetCertificates(url string) ([]*x509.Certificate, error) { 74 kubeconfigPath, err := k8sutil.GetKubeConfigLocation() 75 if err != nil { 76 Log(Error, fmt.Sprintf("Error getting kubeconfig, error: %v", err)) 77 return nil, err 78 } 79 80 client, err := GetVerrazzanoHTTPClient(kubeconfigPath) 81 if err != nil { 82 return nil, err 83 } 84 resp, err := client.Get(url) 85 if err != nil { 86 return nil, err 87 } 88 defer resp.Body.Close() 89 return resp.TLS.PeerCertificates, nil 90 } 91 92 // PostWithHostHeader posts a request with a specified Host header 93 func PostWithHostHeader(url, contentType string, hostHeader string, body io.Reader) (*HTTPResponse, error) { 94 kubeconfigPath, err := k8sutil.GetKubeConfigLocation() 95 if err != nil { 96 Log(Error, fmt.Sprintf("Error getting kubeconfig, error: %v", err)) 97 return nil, err 98 } 99 100 client, err := GetVerrazzanoHTTPClient(kubeconfigPath) 101 if err != nil { 102 return nil, err 103 } 104 return doReq(url, "POST", contentType, hostHeader, "", "", body, client) 105 } 106 107 // Delete executes an HTTP DELETE 108 func Delete(url string, hostHeader string) (*HTTPResponse, error) { 109 kubeconfigPath, err := k8sutil.GetKubeConfigLocation() 110 if err != nil { 111 Log(Error, fmt.Sprintf("Error getting kubeconfig, error: %v", err)) 112 return nil, err 113 } 114 115 client, err := GetVerrazzanoHTTPClient(kubeconfigPath) 116 if err != nil { 117 return nil, err 118 } 119 return doReq(url, "DELETE", "", hostHeader, "", "", nil, client) 120 } 121 122 // GetVerrazzanoNoRetryHTTPClient returns an Http client configured with the Verrazzano CA cert 123 func GetVerrazzanoNoRetryHTTPClient(kubeconfigPath string) (*http.Client, error) { 124 caCert, err := getVerrazzanoCACert(kubeconfigPath) 125 if err != nil { 126 return nil, err 127 } 128 client, err := getHTTPClientWithCABundle(caCert, kubeconfigPath) 129 if err != nil { 130 return nil, err 131 } 132 return client, nil 133 } 134 135 // GetVerrazzanoHTTPClient returns a retryable Http client configured with the Verrazzano CA cert 136 func GetVerrazzanoHTTPClient(kubeconfigPath string) (*retryablehttp.Client, error) { 137 client, err := GetVerrazzanoNoRetryHTTPClient(kubeconfigPath) 138 if err != nil { 139 return nil, err 140 } 141 retryableClient := newRetryableHTTPClient(client) 142 return retryableClient, nil 143 } 144 145 // CheckNoServerHeader validates that the response does not include a Server header. 146 func CheckNoServerHeader(resp *HTTPResponse) bool { 147 // HTTP Server headers should never be returned. 148 for headerName, headerValues := range resp.Header { 149 if strings.EqualFold(headerName, "Server") { 150 Log(Error, fmt.Sprintf("Unexpected Server header %v", headerValues)) 151 return false 152 } 153 } 154 155 return true 156 } 157 158 // CheckStatusAndResponseHeaderAbsent checks that the given header name is not present in the http response, and that the 159 // response status code is as expected. If the statusCode is <= 0, the status code check is skipped. If 160 // the badRespHeader is "", the response headers are not checked. 161 func CheckStatusAndResponseHeaderAbsent(httpClient *retryablehttp.Client, req *retryablehttp.Request, badRespHeader string, statusCode int) error { 162 resp, err := httpClient.Do(req) 163 if err != nil { 164 return err 165 } 166 io.ReadAll(resp.Body) 167 resp.Body.Close() 168 if statusCode > 0 { 169 if resp.StatusCode != statusCode { 170 return fmt.Errorf("Expected status code %d but got %d", statusCode, resp.StatusCode) 171 } 172 } 173 if badRespHeader != "" { 174 // Check that the HTTP header we don't want is not present in the response. 175 badHeaderLower := strings.ToLower(badRespHeader) 176 for headerName, headerValues := range resp.Header { 177 if strings.ToLower(headerName) == badHeaderLower { 178 errMsg := fmt.Sprintf("Unexpected %s header %v", headerName, headerValues) 179 Log(Error, errMsg) 180 return fmt.Errorf(errMsg) 181 } 182 } 183 } 184 return nil 185 } 186 187 func EventuallyVerrazzanoRetryableHTTPClient() *retryablehttp.Client { 188 var client *retryablehttp.Client 189 gomega.Eventually(func() (*retryablehttp.Client, error) { 190 var err error 191 client, err = GetVerrazzanoRetryableHTTPClient() 192 return client, err 193 }, waitTimeout, pollingInterval).ShouldNot(gomega.BeNil(), "Unable to get Verrazzano HTTP client") 194 return client 195 } 196 197 // GetVerrazzanoRetryableHTTPClient returns a retryable HTTP client configured with the CA cert 198 func GetVerrazzanoRetryableHTTPClient() (*retryablehttp.Client, error) { 199 kubeconfigPath, err := k8sutil.GetKubeConfigLocation() 200 if err != nil { 201 Log(Error, fmt.Sprintf("Error getting kubeconfig, error: %v", err)) 202 return nil, err 203 } 204 205 caCert, err := getVerrazzanoCACert(kubeconfigPath) 206 if err != nil { 207 return nil, err 208 } 209 vmiRawClient, err := getHTTPClientWithCABundle(caCert, kubeconfigPath) 210 if err != nil { 211 return nil, err 212 } 213 return newRetryableHTTPClient(vmiRawClient), nil 214 } 215 216 func GetEnvName(kubeconfigPath string) (string, error) { 217 vz, err := GetVerrazzanoInstallResourceInClusterV1beta1(kubeconfigPath) 218 if err != nil { 219 return "", err 220 } 221 if len(vz.Spec.EnvironmentName) == 0 { 222 return defaultEnvName, nil 223 } 224 return vz.Spec.EnvironmentName, nil 225 } 226 227 func AssertOauthURLAccessibleAndUnauthorized(httpClient *retryablehttp.Client, url string) bool { 228 httpClient.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { 229 Log(Info, fmt.Sprintf("oidcUnauthorized req: %v \nvia: %v\n", req, via)) 230 return http.ErrUseLastResponse 231 } 232 resp, err := httpClient.Get(url) 233 if err != nil || resp == nil { 234 Log(Error, fmt.Sprintf("Failed making request: %v", err)) 235 return false 236 } 237 location, err := resp.Location() 238 if err != nil { 239 Log(Error, fmt.Sprintf("Error getting location from response: %v, error: %v", resp, err)) 240 return false 241 } 242 243 if location == nil { 244 Log(Error, fmt.Sprintf("Response location not found for %v", resp)) 245 return false 246 } 247 Log(Info, fmt.Sprintf("oidcUnauthorized %v StatusCode:%v host:%v", url, resp.StatusCode, location.Host)) 248 return resp.StatusCode == 302 && strings.Contains(location.Host, "keycloak") 249 } 250 251 func AssertBearerAuthorized(httpClient *retryablehttp.Client, url string) bool { 252 kubeconfigPath, err := k8sutil.GetKubeConfigLocation() 253 if err != nil { 254 Log(Error, fmt.Sprintf("Error getting kubeconfig location: %v", err)) 255 return false 256 } 257 258 token := EventuallyGetAccessToken(kubeconfigPath, false) 259 req, _ := retryablehttp.NewRequest("GET", url, nil) 260 if *token != "" { 261 bearer := fmt.Sprintf("Bearer %v", *token) 262 req.Header.Set("Authorization", bearer) 263 } 264 resp, err := httpClient.Do(req) 265 if err != nil { 266 Log(Error, fmt.Sprintf("Failed making request: %v", err)) 267 return false 268 } 269 resp.Body.Close() 270 Log(Info, fmt.Sprintf("assertBearerAuthorized %v Response:%v Error:%v", url, resp.StatusCode, err)) 271 return resp.StatusCode == http.StatusOK 272 } 273 274 // PutWithHostHeader PUTs a request with a specified Host header 275 func PutWithHostHeader(url, contentType string, hostHeader string, body io.Reader) (*HTTPResponse, error) { 276 kubeconfigPath, err := k8sutil.GetKubeConfigLocation() 277 if err != nil { 278 Log(Error, fmt.Sprintf("Error getting kubeconfig, error: %v", err)) 279 return nil, err 280 } 281 282 client, err := GetVerrazzanoHTTPClient(kubeconfigPath) 283 if err != nil { 284 return nil, err 285 } 286 return doReq(url, "PUT", contentType, hostHeader, "", "", body, client) 287 } 288 289 // PutWithHostHeaderInCluster PUTs a request with a specified Host header 290 func PutWithHostHeaderInCluster(url, contentType string, hostHeader string, body io.Reader, kubeconfigPath string) (*HTTPResponse, error) { 291 client, err := GetVerrazzanoHTTPClient(kubeconfigPath) 292 if err != nil { 293 return nil, err 294 } 295 return doReq(url, "PUT", contentType, hostHeader, "", "", body, client) 296 } 297 298 // doReq executes an HTTP request with the specified method (GET, POST, DELETE, etc) 299 func doReq(url, method string, contentType string, hostHeader string, username string, password string, 300 body io.Reader, httpClient *retryablehttp.Client, additionalHeaders ...string) (*HTTPResponse, error) { 301 req, err := retryablehttp.NewRequest(method, url, body) 302 if err != nil { 303 return nil, err 304 } 305 if contentType != "" { 306 req.Header.Set("Content-Type", contentType) 307 } 308 if hostHeader != "" { 309 req.Host = hostHeader 310 } 311 312 for _, header := range additionalHeaders { 313 splitArray := strings.Split(header, ":") 314 if len(splitArray) != 2 { 315 return nil, fmt.Errorf("Invalid additional header '%s'. Not in the format key:value", header) 316 } 317 req.Header.Set(splitArray[0], splitArray[1]) 318 } 319 320 if username != "" && password != "" { 321 req.SetBasicAuth(username, password) 322 } 323 resp, err := httpClient.Do(req) 324 if err != nil { 325 return nil, err 326 } 327 return ProcessHTTPResponse(resp) 328 } 329 330 // getHTTPClientWithCABundle returns an HTTP client configured with the provided CA cert 331 func getHTTPClientWithCABundle(caData []byte, kubeconfigPath string) (*http.Client, error) { 332 ca, err := rootCertPoolInCluster(caData, kubeconfigPath) 333 if err != nil { 334 return nil, err 335 } 336 return httputil.GetHTTPClientWithRootCA(ca), nil 337 } 338 339 // getVerrazzanoCACert returns the Verrazzano CA cert in the specified cluster 340 func getVerrazzanoCACert(kubeconfigPath string) ([]byte, error) { 341 cacert, err := GetCACertFromSecret("verrazzano-tls", "verrazzano-system", "ca.crt", kubeconfigPath) 342 if err != nil { 343 envName, err := GetEnvName(kubeconfigPath) 344 if err != nil { 345 return nil, err 346 } 347 return GetCACertFromSecret(envName+"-secret", "verrazzano-system", "ca.crt", kubeconfigPath) 348 } 349 return cacert, nil 350 } 351 352 // GetCACertFromSecret returns the CA cert from the specified kubernetes secret in the given cluster 353 func GetCACertFromSecret(secretName string, namespace string, caKey string, kubeconfigPath string) ([]byte, error) { 354 clientset, err := GetKubernetesClientsetForCluster(kubeconfigPath) 355 if err != nil { 356 return nil, err 357 } 358 certSecret, err := clientset.CoreV1().Secrets(namespace).Get(context.TODO(), secretName, metav1.GetOptions{}) 359 if err != nil { 360 return nil, err 361 } 362 return certSecret.Data[caKey], nil 363 } 364 365 // newRetryableHTTPClient returns a new instance of a retryable HTTP client 366 func newRetryableHTTPClient(client *http.Client) *retryablehttp.Client { 367 retryableClient := retryablehttp.NewClient() 368 retryableClient.RetryMax = NumRetries 369 retryableClient.RetryWaitMin = RetryWaitMinimum 370 retryableClient.RetryWaitMax = RetryWaitMaximum 371 retryableClient.HTTPClient = client 372 retryableClient.CheckRetry = GetRetryPolicy() 373 return retryableClient 374 } 375 376 // rootCertPoolInCluster returns the root cert pool 377 func rootCertPoolInCluster(caData []byte, kubeconfigPath string) (*x509.CertPool, error) { 378 var certPool *x509.CertPool 379 380 if len(caData) != 0 { 381 // if we have caData, use it 382 certPool = x509.NewCertPool() 383 certPool.AppendCertsFromPEM(caData) 384 } 385 386 env, err := GetACMEEnvironment(kubeconfigPath) 387 if err != nil { 388 return nil, err 389 } 390 if env == "staging" { 391 Log(Info, "Adding Let's Encrypt staging CAs to cert pool") 392 // Add the ACME staging CAs if necessary 393 if certPool == nil { 394 certPool = x509.NewCertPool() 395 } 396 for i, stagingCA := range getACMEStagingCAs() { 397 if len(stagingCA) > 0 { 398 Log(Info, fmt.Sprintf("Adding Let's Encrypt staging CA %v", i)) 399 certPool.AppendCertsFromPEM(stagingCA) 400 } 401 } 402 } 403 return certPool, nil 404 } 405 406 // HasStatus asserts that an HTTPResponse has a given status. 407 func HasStatus(expected int) types.GomegaMatcher { 408 return gomega.WithTransform(func(response *HTTPResponse) int { 409 if response == nil { 410 return 0 411 } 412 return response.StatusCode 413 }, gomega.Equal(expected)) 414 } 415 416 // BodyContains asserts that an HTTPResponse body contains a given substring. 417 func BodyContains(expected string) types.GomegaMatcher { 418 return gomega.WithTransform(func(response *HTTPResponse) string { 419 if response == nil { 420 return "" 421 } 422 return string(response.Body) 423 }, gomega.ContainSubstring(expected)) 424 } 425 426 // BodyDoesNotContain asserts that an HTTPResponse body does not contain a given substring. 427 func BodyDoesNotContain(unexpected string) types.GomegaMatcher { 428 return gomega.WithTransform(func(response *HTTPResponse) string { return string(response.Body) }, gomega.Not(gomega.ContainSubstring(unexpected))) 429 } 430 431 // BodyEquals asserts that an HTTPResponse body equals a given string. 432 func BodyEquals(expected string) types.GomegaMatcher { 433 return gomega.WithTransform(func(response *HTTPResponse) string { 434 if response == nil { 435 return "" 436 } 437 return string(response.Body) 438 }, gomega.Equal(expected)) 439 } 440 441 // BodyNotEmpty asserts that an HTTPResponse body is not empty. 442 func BodyNotEmpty() types.GomegaMatcher { 443 return gomega.WithTransform(func(response *HTTPResponse) []byte { 444 if response == nil { 445 return nil 446 } 447 return response.Body 448 }, gomega.Not(gomega.BeEmpty())) 449 }