package httpproxy import ( "bytes" "crypto/md5" "crypto/rand" "encoding/base64" "encoding/hex" "fmt" "io" "io/ioutil" "net/http" "strings" "time" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/endpoints" v4 "github.com/aws/aws-sdk-go/aws/signer/v4" "github.com/rancher/norman/httperror" "github.com/rancher/rancher/pkg/catalog/utils" v1 "github.com/rancher/rancher/pkg/generated/norman/core/v1" ) const ( defaultAWSRegion = "us-east-1" defaultUSGovAWSRegion = "us-gov-west-1" cnNorth1AWSRegion = "cn-north-1" cnNorthwest1AWSRegion = "cn-northwest-1" ) // List of global services for AWS from: https://docs.aws.amazon.com/general/latest/gr/rande.html#global-endpoints var globalAWSServices = []string{"cloudfront", "globalaccelerator", "iam", "networkmanager", "organizations", "route53", "shield", "waf"} var requiredHeadersForAws = map[string]bool{"host": true, "x-amz-content-sha256": true, "x-amz-date": true, "x-amz-user-agent": true} type SecretGetter func(namespace, name string) (*v1.Secret, error) type Signer interface { sign(*http.Request, SecretGetter, string) error } func newSigner(auth string) Signer { splitAuth := strings.Split(auth, " ") switch strings.ToLower(splitAuth[0]) { case "awsv4": return awsv4{} case "bearer": return bearer{} case "basic": return basic{} case "digest": return digest{} case "arbitrary": return arbitrary{} } return nil } func (br bearer) sign(req *http.Request, secrets SecretGetter, auth string) error { data, secret, err := getAuthData(auth, secrets, []string{"passwordField", "credID"}) if err != nil { return err } req.Header.Set(AuthHeader, fmt.Sprintf("%s %s", "Bearer", secret[data["passwordField"]])) return nil } func (b basic) sign(req *http.Request, secrets SecretGetter, auth string) error { data, secret, err := getAuthData(auth, secrets, []string{"usernameField", "passwordField", "credID"}) if err != nil { return err } key := fmt.Sprintf("%s:%s", secret[data["usernameField"]], secret[data["passwordField"]]) encoded := base64.URLEncoding.EncodeToString([]byte(key)) req.Header.Set(AuthHeader, fmt.Sprintf("%s %s", "Basic", encoded)) return nil } func (a awsv4) sign(req *http.Request, secrets SecretGetter, auth string) error { _, secret, err := getAuthData(auth, secrets, []string{"credID"}) if err != nil { return err } service, region := a.getServiceAndRegion(req.URL.Host) creds := credentials.NewStaticCredentials(secret["accessKey"], secret["secretKey"], "") awsSigner := v4.NewSigner(creds) var body []byte if req.Body != nil { body, err = ioutil.ReadAll(req.Body) if err != nil { return fmt.Errorf("error reading request body %v", err) } } oldHeader, newHeader := http.Header{}, http.Header{} for header, value := range req.Header { if _, ok := requiredHeadersForAws[strings.ToLower(header)]; ok { newHeader[header] = value } else { oldHeader[header] = value } } req.Header = newHeader _, err = awsSigner.Sign(req, bytes.NewReader(body), service, region, time.Now()) if err != nil { return err } for key, val := range oldHeader { req.Header.Add(key, strings.Join(val, "")) } return nil } func (a awsv4) getServiceAndRegion(host string) (string, string) { service := "" region := "" for _, partition := range endpoints.DefaultPartitions() { service, region = partitionServiceAndRegion(partition, host) // Some services are global and don't have a region. If a partition returns a service // that is global then stop processing partitions. If we carry on processing partitions // for a global service then when new partitions are introduced the signing may break. if service != "" && region == "" { if utils.Contains(globalAWSServices, service) { break } } // empty region is valid, but if one is found it should be assumed correct if region != "" { return service, region } } if strings.EqualFold(service, "iam") { // This conditional is meant to cover a discrepancy in the IAM service for the China regions. // The following doc states that IAM uses a globally unique endpoint, and the default // region "us-east-1" should be used as part of the Credential authentication parameter // (Current backend behavior). However, using "us-east-1" with any of the China regions will throw // the error "SignatureDoesNotMatch: Credential should be scoped to a valid region, not 'us-east-1'.". // https://docs.aws.amazon.com/general/latest/gr/sigv4_elements.html // // This other doc states the region value for China services should be "cn-north-1" or "cn-northwest-1" // including IAM (See IAM endpoints in the tables). So they need to be set manually to prevent the error // caused by the "us-east-1" default. // https://docs.amazonaws.cn/en_us/aws/latest/userguide/endpoints-Beijing.html if strings.Contains(host, cnNorth1AWSRegion) { return service, cnNorth1AWSRegion } if strings.Contains(host, cnNorthwest1AWSRegion) { return service, cnNorthwest1AWSRegion } } // if no region is found, global endpoint is assumed. // https://docs.aws.amazon.com/general/latest/gr/sigv4_elements.html if strings.Contains(host, "us-gov") { return service, defaultUSGovAWSRegion } return service, defaultAWSRegion } func partitionServiceAndRegion(partition endpoints.Partition, host string) (string, string) { service := "" partitionServices := partition.Services() for _, part := range strings.Split(host, ".") { if id := partitionServices[part].ID(); id != "" { service = id break } } if service == "" { return "", "" } host = strings.Trim(host, service) serviceRegions := partitionServices[service].Regions() for _, part := range strings.Split(host, ".") { if id := serviceRegions[part].ID(); id != "" { return service, id } } return service, "" } func (d digest) sign(req *http.Request, secrets SecretGetter, auth string) error { data, secret, err := getAuthData(auth, secrets, []string{"usernameField", "passwordField", "credID"}) if err != nil { return err } resp, err := doNewRequest(req) // request to get challenge fields from server if err != nil { return err } challengeData, err := parseChallenge(resp.Header.Get("WWW-Authenticate")) if err != nil { return err } challengeData["username"] = secret[data["usernameField"]] challengeData["password"] = secret[data["passwordField"]] signature, err := buildSignature(challengeData, req) if err != nil { return err } req.Header.Set(AuthHeader, fmt.Sprintf("%s %s", "Digest", signature)) return nil } func doNewRequest(req *http.Request) (*http.Response, error) { newReq, err := http.NewRequest(req.Method, req.URL.String(), nil) if err != nil { return nil, err } newReq.Header.Set("Content-Type", "application/json") client := http.Client{} resp, err := client.Do(newReq) if err != nil { return nil, err } if resp.StatusCode != httperror.Unauthorized.Status { return nil, fmt.Errorf("expected 401 status code, got %v", resp.StatusCode) } resp.Body.Close() return resp, err } func parseChallenge(header string) (map[string]string, error) { if header == "" { return nil, fmt.Errorf("failed to get WWW-Authenticate header") } s := strings.Trim(header, " \n\r\t") if !strings.HasPrefix(s, "Digest ") { return nil, fmt.Errorf("bad challenge %s", header) } data := map[string]string{} s = strings.Trim(s[7:], " \n\r\t") terms := strings.Split(s, ", ") for _, term := range terms { splitTerm := strings.SplitN(term, "=", 2) data[splitTerm[0]] = strings.Trim(splitTerm[1], "\"") } return data, nil } func formResponse(qop string, data map[string]string, req *http.Request) (string, string) { hash1 := hash(fmt.Sprintf("%s:%s:%s", data["username"], data["realm"], data["password"])) hash2 := hash(fmt.Sprintf("%s:%s", req.Method, req.URL.Path)) if qop == "" { return hash(fmt.Sprintf("%s:%s:%s", hash1, data["nonce"], hash2)), "" } else if qop == "auth" { cnonce := data["cnonce"] if cnonce == "" { cnonce = getCnonce() } return hash(fmt.Sprintf("%s:%s:%08x:%s:%s:%s", hash1, data["nonce"], 00000001, cnonce, qop, hash2)), cnonce } return "", "" } func buildSignature(data map[string]string, req *http.Request) (string, error) { qop, ok := data["qop"] if ok && qop != "auth" && qop != "" { return "", fmt.Errorf("qop not implemented %s", data["qop"]) } response, cnonce := formResponse(qop, data, req) if response == "" { return "", fmt.Errorf("error forming response qop: %s", qop) } auth := []string{fmt.Sprintf(`username="%s"`, data["username"])} auth = append(auth, fmt.Sprintf(`realm="%s"`, data["realm"])) auth = append(auth, fmt.Sprintf(`nonce="%s"`, data["nonce"])) auth = append(auth, fmt.Sprintf(`uri="%s"`, req.URL.Path)) auth = append(auth, fmt.Sprintf(`response="%s"`, response)) if val, ok := data["opaque"]; ok && val != "" { auth = append(auth, fmt.Sprintf(`opaque="%s"`, data["opaque"])) } if qop != "" { auth = append(auth, fmt.Sprintf("qop=%s", qop)) auth = append(auth, fmt.Sprintf("nc=%08x", 00000001)) auth = append(auth, fmt.Sprintf("cnonce=%s", cnonce)) } return strings.Join(auth, ", "), nil } func hash(field string) string { f := md5.New() f.Write([]byte(field)) return hex.EncodeToString(f.Sum(nil)) } func getCnonce() string { b := make([]byte, 8) io.ReadFull(rand.Reader, b) return fmt.Sprintf("%x", b)[:16] } func (a arbitrary) sign(req *http.Request, secrets SecretGetter, auth string) error { data, _, err := getAuthData(auth, secrets, []string{}) if err != nil { return err } splitHeaders := strings.Split(data["headers"], ",") for _, header := range splitHeaders { val := strings.SplitN(header, "=", 2) req.Header.Set(val[0], val[1]) } return nil } type awsv4 struct{} type bearer struct{} type basic struct{} type digest struct{} type arbitrary struct{}