/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 common import ( "encoding/base64" "fmt" "math" "net" "net/url" "strconv" "strings" ) import ( gxset "github.com/dubbogo/gost/container/set" "github.com/jinzhu/copier" perrors "github.com/pkg/errors" "github.com/satori/go.uuid" ) import ( "github.com/apache/dubbo-go/common/constant" "github.com/apache/dubbo-go/common/logger" ) // /////////////////////////////// // dubbo role type // /////////////////////////////// // role constant const ( // CONSUMER is consumer role CONSUMER = iota // CONFIGURATOR is configurator role CONFIGURATOR // ROUTER is router role ROUTER // PROVIDER is provider role PROVIDER PROTOCOL = "protocol" ) var ( // DubboNodes Dubbo service node DubboNodes = [...]string{"consumers", "configurators", "routers", "providers"} // DubboRole Dubbo service role DubboRole = [...]string{"consumer", "", "routers", "provider"} ) // nolint type RoleType int func (t RoleType) String() string { return DubboNodes[t] } // Role returns role by @RoleType func (t RoleType) Role() string { return DubboRole[t] } type baseUrl struct { Protocol string Location string // ip+port Ip string Port string params url.Values PrimitiveURL string } // URL is not thread-safe. // we fail to define this struct to be immutable object. // but, those method which will update the URL, including SetParam, SetParams // are only allowed to be invoked in creating URL instance // Please keep in mind that this struct is immutable after it has been created and initialized. type URL struct { baseUrl Path string // like /com.ikurento.dubbo.UserProvider3 Username string Password string Methods []string // special for registry SubURL *URL } // Option accepts url // Option will define a function of handling URL type option func(*URL) // WithUsername sets username for url func WithUsername(username string) option { return func(url *URL) { url.Username = username } } // WithPassword sets password for url func WithPassword(pwd string) option { return func(url *URL) { url.Password = pwd } } // WithMethods sets methods for url func WithMethods(methods []string) option { return func(url *URL) { url.Methods = methods } } // WithParams sets params for url func WithParams(params url.Values) option { return func(url *URL) { url.params = params } } // WithParamsValue sets params field for url func WithParamsValue(key, val string) option { return func(url *URL) { url.SetParam(key, val) } } // WithProtocol sets protocol for url func WithProtocol(proto string) option { return func(url *URL) { url.Protocol = proto } } // WithIp sets ip for url func WithIp(ip string) option { return func(url *URL) { url.Ip = ip } } // WithPort sets port for url func WithPort(port string) option { return func(url *URL) { url.Port = port } } // WithPath sets path for url func WithPath(path string) option { return func(url *URL) { url.Path = "/" + strings.TrimPrefix(path, "/") } } // WithLocation sets location for url func WithLocation(location string) option { return func(url *URL) { url.Location = location } } // WithToken sets token for url func WithToken(token string) option { return func(url *URL) { if len(token) > 0 { value := token if strings.ToLower(token) == "true" || strings.ToLower(token) == "default" { u, err := uuid.NewV4() if err != nil { logger.Errorf("could not generator UUID: %v", err) return } value = u.String() } url.SetParam(constant.TOKEN_KEY, value) } } } // NewURLWithOptions will create a new url with options func NewURLWithOptions(opts ...option) *URL { url := &URL{} for _, opt := range opts { opt(url) } url.Location = url.Ip + ":" + url.Port return url } // NewURL will create a new url // the urlString should not be empty func NewURL(urlString string, opts ...option) (URL, error) { var ( err error rawUrlString string serviceUrl *url.URL s = URL{baseUrl: baseUrl{}} ) // new a null instance if urlString == "" { return s, nil } rawUrlString, err = url.QueryUnescape(urlString) if err != nil { return s, perrors.Errorf("url.QueryUnescape(%s), error{%v}", urlString, err) } // rawUrlString = "//" + rawUrlString if strings.Index(rawUrlString, "//") < 0 { t := URL{baseUrl: baseUrl{}} for _, opt := range opts { opt(&t) } rawUrlString = t.Protocol + "://" + rawUrlString } serviceUrl, err = url.Parse(rawUrlString) if err != nil { return s, perrors.Errorf("url.Parse(url string{%s}), error{%v}", rawUrlString, err) } s.params, err = url.ParseQuery(serviceUrl.RawQuery) if err != nil { return s, perrors.Errorf("url.ParseQuery(raw url string{%s}), error{%v}", serviceUrl.RawQuery, err) } s.PrimitiveURL = urlString s.Protocol = serviceUrl.Scheme s.Username = serviceUrl.User.Username() s.Password, _ = serviceUrl.User.Password() s.Location = serviceUrl.Host s.Path = serviceUrl.Path if strings.Contains(s.Location, ":") { s.Ip, s.Port, err = net.SplitHostPort(s.Location) if err != nil { return s, perrors.Errorf("net.SplitHostPort(url.Host{%s}), error{%v}", s.Location, err) } } for _, opt := range opts { opt(&s) } return s, nil } // URLEqual judge @url and @c is equal or not. func (c URL) URLEqual(url URL) bool { c.Ip = "" c.Port = "" url.Ip = "" url.Port = "" cGroup := c.GetParam(constant.GROUP_KEY, "") urlGroup := url.GetParam(constant.GROUP_KEY, "") cKey := c.Key() urlKey := url.Key() if cGroup == constant.ANY_VALUE { cKey = strings.Replace(cKey, "group=*", "group="+urlGroup, 1) } else if urlGroup == constant.ANY_VALUE { urlKey = strings.Replace(urlKey, "group=*", "group="+cGroup, 1) } // 1. protocol, username, password, ip, port, service name, group, version should be equal if cKey != urlKey { return false } // 2. if url contains enabled key, should be true, or * if url.GetParam(constant.ENABLED_KEY, "true") != "true" && url.GetParam(constant.ENABLED_KEY, "") != constant.ANY_VALUE { return false } // TODO :may need add interface key any value condition return isMatchCategory(url.GetParam(constant.CATEGORY_KEY, constant.DEFAULT_CATEGORY), c.GetParam(constant.CATEGORY_KEY, constant.DEFAULT_CATEGORY)) } func isMatchCategory(category1 string, category2 string) bool { if len(category2) == 0 { return category1 == constant.DEFAULT_CATEGORY } else if strings.Contains(category2, constant.ANY_VALUE) { return true } else if strings.Contains(category2, constant.REMOVE_VALUE_PREFIX) { return !strings.Contains(category2, constant.REMOVE_VALUE_PREFIX+category1) } else { return strings.Contains(category2, category1) } } func (c URL) String() string { var buf strings.Builder if len(c.Username) == 0 && len(c.Password) == 0 { buf.WriteString(fmt.Sprintf( "%s://%s:%s%s?", c.Protocol, c.Ip, c.Port, c.Path)) } else { buf.WriteString(fmt.Sprintf( "%s://%s:%s@%s:%s%s?", c.Protocol, c.Username, c.Password, c.Ip, c.Port, c.Path)) } buf.WriteString(c.params.Encode()) return buf.String() } // Key gets key func (c URL) Key() string { buildString := fmt.Sprintf( "%s://%s:%s@%s:%s/?interface=%s&group=%s&version=%s", c.Protocol, c.Username, c.Password, c.Ip, c.Port, c.Service(), c.GetParam(constant.GROUP_KEY, ""), c.GetParam(constant.VERSION_KEY, "")) return buildString } // ServiceKey gets a unique key of a service. func (c URL) ServiceKey() string { intf := c.GetParam(constant.INTERFACE_KEY, strings.TrimPrefix(c.Path, "/")) if intf == "" { return "" } var buf strings.Builder group := c.GetParam(constant.GROUP_KEY, "") if group != "" { buf.WriteString(group) buf.WriteString("/") } buf.WriteString(intf) version := c.GetParam(constant.VERSION_KEY, "") if version != "" && version != "0.0.0" { buf.WriteString(":") buf.WriteString(version) } return buf.String() } // ColonSeparatedKey // The format is "{interface}:[version]:[group]" func (c *URL) ColonSeparatedKey() string { intf := c.GetParam(constant.INTERFACE_KEY, strings.TrimPrefix(c.Path, "/")) if intf == "" { return "" } var buf strings.Builder buf.WriteString(intf) buf.WriteString(":") version := c.GetParam(constant.VERSION_KEY, "") if version != "" && version != "0.0.0" { buf.WriteString(version) } group := c.GetParam(constant.GROUP_KEY, "") buf.WriteString(":") if group != "" { buf.WriteString(group) } return buf.String() } // EncodedServiceKey encode the service key func (c *URL) EncodedServiceKey() string { serviceKey := c.ServiceKey() return strings.Replace(serviceKey, "/", "*", 1) } // Service gets service func (c URL) Service() string { service := c.GetParam(constant.INTERFACE_KEY, strings.TrimPrefix(c.Path, "/")) if service != "" { return service } else if c.SubURL != nil { service = c.SubURL.GetParam(constant.INTERFACE_KEY, strings.TrimPrefix(c.Path, "/")) if service != "" { // if url.path is "" then return suburl's path, special for registry url return service } } return "" } // AddParam will add the key-value pair // Not thread-safe // think twice before using it. func (c *URL) AddParam(key string, value string) { c.params.Add(key, value) } // SetParam will put the key-value pair into url // it's not thread safe. // think twice before you want to use this method // usually it should only be invoked when you want to initialized an url func (c *URL) SetParam(key string, value string) { c.params.Set(key, value) } // RangeParams will iterate the params // it's not thread-safe func (c *URL) RangeParams(f func(key, value string) bool) { for k, v := range c.params { if !f(k, v[0]) { break } } } // GetParam gets value by key func (c URL) GetParam(s string, d string) string { r := c.params.Get(s) if len(r) == 0 { r = d } return r } // GetParams gets values func (c URL) GetParams() url.Values { return c.params } // GetParamAndDecoded gets values and decode func (c URL) GetParamAndDecoded(key string) (string, error) { ruleDec, err := base64.URLEncoding.DecodeString(c.GetParam(key, "")) value := string(ruleDec) return value, err } // GetRawParam gets raw param func (c URL) GetRawParam(key string) string { switch key { case PROTOCOL: return c.Protocol case "username": return c.Username case "host": return strings.Split(c.Location, ":")[0] case "password": return c.Password case "port": return c.Port case "path": return c.Path default: return c.GetParam(key, "") } } // GetParamBool judge whether @key exists or not func (c URL) GetParamBool(key string, d bool) bool { r, err := strconv.ParseBool(c.GetParam(key, "")) if err != nil { return d } return r } // GetParamInt gets int value by @key func (c URL) GetParamInt(key string, d int64) int64 { r, err := strconv.Atoi(c.GetParam(key, "")) if r == 0 || err != nil { return d } return int64(r) } // GetMethodParamInt gets int method param func (c URL) GetMethodParamInt(method string, key string, d int64) int64 { r, err := strconv.Atoi(c.GetParam("methods."+method+"."+key, "")) if r == 0 || err != nil { return d } return int64(r) } // GetMethodParamInt64 gets int64 method param func (c URL) GetMethodParamInt64(method string, key string, d int64) int64 { r := c.GetMethodParamInt(method, key, math.MinInt64) if r == math.MinInt64 { return c.GetParamInt(key, d) } return r } // GetMethodParam gets method param func (c URL) GetMethodParam(method string, key string, d string) string { r := c.GetParam("methods."+method+"."+key, "") if r == "" { r = d } return r } // GetMethodParamBool judge whether @method param exists or not func (c URL) GetMethodParamBool(method string, key string, d bool) bool { r := c.GetParamBool("methods."+method+"."+key, d) return r } // SetParams will put all key-value pair into url. // 1. if there already has same key, the value will be override // 2. it's not thread safe // 3. think twice when you want to invoke this method func (c *URL) SetParams(m url.Values) { for k := range m { c.SetParam(k, m.Get(k)) } } // ToMap transfer URL to Map func (c URL) ToMap() map[string]string { paramsMap := make(map[string]string) c.RangeParams(func(key, value string) bool { paramsMap[key] = value return true }) if c.Protocol != "" { paramsMap[PROTOCOL] = c.Protocol } if c.Username != "" { paramsMap["username"] = c.Username } if c.Password != "" { paramsMap["password"] = c.Password } if c.Location != "" { paramsMap["host"] = strings.Split(c.Location, ":")[0] var port string if strings.Contains(c.Location, ":") { port = strings.Split(c.Location, ":")[1] } else { port = "0" } paramsMap["port"] = port } if c.Protocol != "" { paramsMap[PROTOCOL] = c.Protocol } if c.Path != "" { paramsMap["path"] = c.Path } if len(paramsMap) == 0 { return nil } return paramsMap } // configuration > reference config >service config // in this function we should merge the reference local url config into the service url from registry. // TODO configuration merge, in the future , the configuration center's config should merge too. // MergeUrl will merge those two url // the result is based on serviceUrl, and the key which si only contained in referenceUrl // will be added into result. // for example, if serviceUrl contains params (a1->v1, b1->v2) and referenceUrl contains params(a2->v3, b1 -> v4) // the params of result will be (a1->v1, b1->v2, a2->v3). // You should notice that the value of b1 is v2, not v4. // due to URL is not thread-safe, so this method is not thread-safe func MergeUrl(serviceUrl *URL, referenceUrl *URL) *URL { mergedUrl := serviceUrl.Clone() // iterator the referenceUrl if serviceUrl not have the key ,merge in referenceUrl.RangeParams(func(key, value string) bool { if v := mergedUrl.GetParam(key, ""); len(v) == 0 { mergedUrl.SetParam(key, value) } return true }) // loadBalance,cluster,retries strategy config methodConfigMergeFcn := mergeNormalParam(mergedUrl, referenceUrl, []string{constant.LOADBALANCE_KEY, constant.CLUSTER_KEY, constant.RETRIES_KEY, constant.TIMEOUT_KEY}) // remote timestamp if v := serviceUrl.GetParam(constant.TIMESTAMP_KEY, ""); len(v) > 0 { mergedUrl.SetParam(constant.REMOTE_TIMESTAMP_KEY, v) mergedUrl.SetParam(constant.TIMESTAMP_KEY, referenceUrl.GetParam(constant.TIMESTAMP_KEY, "")) } // finally execute methodConfigMergeFcn for _, method := range referenceUrl.Methods { for _, fcn := range methodConfigMergeFcn { fcn("methods." + method) } } return mergedUrl } // Clone will copy the url func (c *URL) Clone() *URL { newUrl := &URL{} copier.Copy(newUrl, c) newUrl.params = url.Values{} c.RangeParams(func(key, value string) bool { newUrl.SetParam(key, value) return true }) return newUrl } func (c *URL) CloneExceptParams(excludeParams *gxset.HashSet) *URL { newUrl := &URL{} copier.Copy(newUrl, c) newUrl.params = url.Values{} c.RangeParams(func(key, value string) bool { if !excludeParams.Contains(key) { newUrl.SetParam(key, value) } return true }) return newUrl } // Copy url based on the reserved parameter's keys. func (c *URL) CloneWithParams(reserveParams []string) *URL { params := url.Values{} for _, reserveParam := range reserveParams { v := c.GetParam(reserveParam, "") if len(v) != 0 { params.Set(reserveParam, v) } } return NewURLWithOptions( WithProtocol(c.Protocol), WithUsername(c.Username), WithPassword(c.Password), WithIp(c.Ip), WithPort(c.Port), WithPath(c.Path), WithMethods(c.Methods), WithParams(params), ) } func mergeNormalParam(mergedUrl *URL, referenceUrl *URL, paramKeys []string) []func(method string) { methodConfigMergeFcn := make([]func(method string), 0, len(paramKeys)) for _, paramKey := range paramKeys { if v := referenceUrl.GetParam(paramKey, ""); len(v) > 0 { mergedUrl.SetParam(paramKey, v) } methodConfigMergeFcn = append(methodConfigMergeFcn, func(method string) { if v := referenceUrl.GetParam(method+"."+paramKey, ""); len(v) > 0 { mergedUrl.SetParam(method+"."+paramKey, v) } }) } return methodConfigMergeFcn } // URLSlice will be used to sort URL instance // Instances will be order by URL.String() type URLSlice []URL // nolint func (s URLSlice) Len() int { return len(s) } // nolint func (s URLSlice) Less(i, j int) bool { return s[i].String() < s[j].String() } // nolint func (s URLSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }