// Copyright (c) 2022 Alibaba Group Holding Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package annotations import ( "net/url" "strings" "github.com/gogo/protobuf/types" networking "istio.io/api/networking/v1alpha3" ) const ( // annotation key enableCors = "enable-cors" allowOrigin = "cors-allow-origin" allowMethods = "cors-allow-methods" allowHeaders = "cors-allow-headers" exposeHeaders = "cors-expose-headers" allowCredentials = "cors-allow-credentials" maxAge = "cors-max-age" // default annotation value defaultAllowOrigin = "*" defaultAllowMethods = "GET, PUT, POST, DELETE, PATCH, OPTIONS" defaultAllowHeaders = "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With," + "If-Modified-Since,Cache-Control,Content-Type,Authorization" defaultAllowCredentials = true defaultMaxAge = 1728000 ) var ( _ Parser = &cors{} _ RouteHandler = &cors{} ) type CorsConfig struct { Enabled bool AllowOrigin []string AllowMethods []string AllowHeaders []string ExposeHeaders []string AllowCredentials bool MaxAge int } type cors struct{} func (c cors) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error { if !needCorsConfig(annotations) { return nil } // cors enable enable, _ := annotations.ParseBoolASAP(enableCors) if !enable { return nil } corsConfig := &CorsConfig{ Enabled: enable, AllowOrigin: []string{defaultAllowOrigin}, AllowMethods: splitStringWithSpaceTrim(defaultAllowMethods), AllowHeaders: splitStringWithSpaceTrim(defaultAllowHeaders), AllowCredentials: defaultAllowCredentials, MaxAge: defaultMaxAge, } defer func() { config.Cors = corsConfig }() // allow origin if origin, err := annotations.ParseStringASAP(allowOrigin); err == nil { corsConfig.AllowOrigin = splitStringWithSpaceTrim(origin) } // allow methods if methods, err := annotations.ParseStringASAP(allowMethods); err == nil { corsConfig.AllowMethods = splitStringWithSpaceTrim(methods) } // allow headers if headers, err := annotations.ParseStringASAP(allowHeaders); err == nil { corsConfig.AllowHeaders = splitStringWithSpaceTrim(headers) } // expose headers if exposeHeaders, err := annotations.ParseStringASAP(exposeHeaders); err == nil { corsConfig.ExposeHeaders = splitStringWithSpaceTrim(exposeHeaders) } // allow credentials if allowCredentials, err := annotations.ParseBoolASAP(allowCredentials); err == nil { corsConfig.AllowCredentials = allowCredentials } // max age if age, err := annotations.ParseIntASAP(maxAge); err == nil { corsConfig.MaxAge = age } return nil } func (c cors) ApplyRoute(route *networking.HTTPRoute, config *Ingress) { corsConfig := config.Cors if corsConfig == nil || !corsConfig.Enabled { return } corsPolicy := &networking.CorsPolicy{ AllowMethods: corsConfig.AllowMethods, AllowHeaders: corsConfig.AllowHeaders, ExposeHeaders: corsConfig.ExposeHeaders, AllowCredentials: &types.BoolValue{ Value: corsConfig.AllowCredentials, }, MaxAge: &types.Duration{ Seconds: int64(corsConfig.MaxAge), }, } var allowOrigins []*networking.StringMatch for _, origin := range corsConfig.AllowOrigin { if origin == "*" { allowOrigins = append(allowOrigins, &networking.StringMatch{ MatchType: &networking.StringMatch_Regex{ Regex: ".*", }, }) break } if strings.Contains(origin, "*") { parsedURL, err := url.Parse(origin) if err != nil { continue } if strings.HasPrefix(parsedURL.Host, "*") { var sb strings.Builder sb.WriteString(".*") for idx, char := range parsedURL.Host { if idx == 0 { continue } if char == '.' { sb.WriteString("\\") } sb.WriteString(string(char)) } allowOrigins = append(allowOrigins, &networking.StringMatch{ MatchType: &networking.StringMatch_Regex{ Regex: sb.String(), }, }) } continue } allowOrigins = append(allowOrigins, &networking.StringMatch{ MatchType: &networking.StringMatch_Exact{ Exact: origin, }, }) } corsPolicy.AllowOrigins = allowOrigins route.CorsPolicy = corsPolicy } func needCorsConfig(annotations Annotations) bool { return annotations.HasASAP(enableCors) } func splitStringWithSpaceTrim(input string) []string { out := strings.Split(input, ",") for i, item := range out { converted := strings.TrimSpace(item) if converted == "*" { return []string{"*"} } out[i] = converted } return out }