k8s.io/apiserver@v0.31.1/pkg/cel/library/urls.go (about) 1 /* 2 Copyright 2022 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package library 18 19 import ( 20 "net/url" 21 22 "github.com/google/cel-go/cel" 23 "github.com/google/cel-go/common/types" 24 "github.com/google/cel-go/common/types/ref" 25 26 apiservercel "k8s.io/apiserver/pkg/cel" 27 ) 28 29 // URLs provides a CEL function library extension of URL parsing functions. 30 // 31 // url 32 // 33 // Converts a string to a URL or results in an error if the string is not a valid URL. The URL must be an absolute URI 34 // or an absolute path. 35 // 36 // url(<string>) <URL> 37 // 38 // Examples: 39 // 40 // url('https://user:pass@example.com:80/path?query=val#fragment') // returns a URL 41 // url('/absolute-path') // returns a URL 42 // url('https://a:b:c/') // error 43 // url('../relative-path') // error 44 // 45 // isURL 46 // 47 // Returns true if a string is a valid URL. The URL must be an absolute URI or an absolute path. 48 // 49 // isURL( <string>) <bool> 50 // 51 // Examples: 52 // 53 // isURL('https://user:pass@example.com:80/path?query=val#fragment') // returns true 54 // isURL('/absolute-path') // returns true 55 // isURL('https://a:b:c/') // returns false 56 // isURL('../relative-path') // returns false 57 // 58 // getScheme / getHost / getHostname / getPort / getEscapedPath / getQuery 59 // 60 // Return the parsed components of a URL. 61 // 62 // - getScheme: If absent in the URL, returns an empty string. 63 // 64 // - getHostname: IPv6 addresses are returned without braces, e.g. "::1". If absent in the URL, returns an empty string. 65 // 66 // - getHost: IPv6 addresses are returned with braces, e.g. "[::1]". If absent in the URL, returns an empty string. 67 // 68 // - getEscapedPath: The string returned by getEscapedPath is URL escaped, e.g. "with space" becomes "with%20space". 69 // If absent in the URL, returns an empty string. 70 // 71 // - getPort: If absent in the URL, returns an empty string. 72 // 73 // - getQuery: Returns the query parameters in "matrix" form where a repeated query key is interpreted to 74 // mean that there are multiple values for that key. The keys and values are returned unescaped. 75 // If absent in the URL, returns an empty map. 76 // 77 // <URL>.getScheme() <string> 78 // <URL>.getHost() <string> 79 // <URL>.getHostname() <string> 80 // <URL>.getPort() <string> 81 // <URL>.getEscapedPath() <string> 82 // <URL>.getQuery() <map <string>, <list <string>> 83 // 84 // Examples: 85 // 86 // url('/path').getScheme() // returns '' 87 // url('https://example.com/').getScheme() // returns 'https' 88 // url('https://example.com:80/').getHost() // returns 'example.com:80' 89 // url('https://example.com/').getHost() // returns 'example.com' 90 // url('https://[::1]:80/').getHost() // returns '[::1]:80' 91 // url('https://[::1]/').getHost() // returns '[::1]' 92 // url('/path').getHost() // returns '' 93 // url('https://example.com:80/').getHostname() // returns 'example.com' 94 // url('https://127.0.0.1:80/').getHostname() // returns '127.0.0.1' 95 // url('https://[::1]:80/').getHostname() // returns '::1' 96 // url('/path').getHostname() // returns '' 97 // url('https://example.com:80/').getPort() // returns '80' 98 // url('https://example.com/').getPort() // returns '' 99 // url('/path').getPort() // returns '' 100 // url('https://example.com/path').getEscapedPath() // returns '/path' 101 // url('https://example.com/path with spaces/').getEscapedPath() // returns '/path%20with%20spaces/' 102 // url('https://example.com').getEscapedPath() // returns '' 103 // url('https://example.com/path?k1=a&k2=b&k2=c').getQuery() // returns { 'k1': ['a'], 'k2': ['b', 'c']} 104 // url('https://example.com/path?key with spaces=value with spaces').getQuery() // returns { 'key with spaces': ['value with spaces']} 105 // url('https://example.com/path?').getQuery() // returns {} 106 // url('https://example.com/path').getQuery() // returns {} 107 func URLs() cel.EnvOption { 108 return cel.Lib(urlsLib) 109 } 110 111 var urlsLib = &urls{} 112 113 type urls struct{} 114 115 func (*urls) LibraryName() string { 116 return "k8s.urls" 117 } 118 119 var urlLibraryDecls = map[string][]cel.FunctionOpt{ 120 "url": { 121 cel.Overload("string_to_url", []*cel.Type{cel.StringType}, apiservercel.URLType, 122 cel.UnaryBinding(stringToUrl))}, 123 "getScheme": { 124 cel.MemberOverload("url_get_scheme", []*cel.Type{apiservercel.URLType}, cel.StringType, 125 cel.UnaryBinding(getScheme))}, 126 "getHost": { 127 cel.MemberOverload("url_get_host", []*cel.Type{apiservercel.URLType}, cel.StringType, 128 cel.UnaryBinding(getHost))}, 129 "getHostname": { 130 cel.MemberOverload("url_get_hostname", []*cel.Type{apiservercel.URLType}, cel.StringType, 131 cel.UnaryBinding(getHostname))}, 132 "getPort": { 133 cel.MemberOverload("url_get_port", []*cel.Type{apiservercel.URLType}, cel.StringType, 134 cel.UnaryBinding(getPort))}, 135 "getEscapedPath": { 136 cel.MemberOverload("url_get_escaped_path", []*cel.Type{apiservercel.URLType}, cel.StringType, 137 cel.UnaryBinding(getEscapedPath))}, 138 "getQuery": { 139 cel.MemberOverload("url_get_query", []*cel.Type{apiservercel.URLType}, 140 cel.MapType(cel.StringType, cel.ListType(cel.StringType)), 141 cel.UnaryBinding(getQuery))}, 142 "isURL": { 143 cel.Overload("is_url_string", []*cel.Type{cel.StringType}, cel.BoolType, 144 cel.UnaryBinding(isURL))}, 145 } 146 147 func (*urls) CompileOptions() []cel.EnvOption { 148 options := []cel.EnvOption{} 149 for name, overloads := range urlLibraryDecls { 150 options = append(options, cel.Function(name, overloads...)) 151 } 152 return options 153 } 154 155 func (*urls) ProgramOptions() []cel.ProgramOption { 156 return []cel.ProgramOption{} 157 } 158 159 func stringToUrl(arg ref.Val) ref.Val { 160 s, ok := arg.Value().(string) 161 if !ok { 162 return types.MaybeNoSuchOverloadErr(arg) 163 } 164 // Use ParseRequestURI to check the URL before conversion. 165 // ParseRequestURI requires absolute URLs and is used by the OpenAPIv3 'uri' format. 166 _, err := url.ParseRequestURI(s) 167 if err != nil { 168 return types.NewErr("URL parse error during conversion from string: %v", err) 169 } 170 // We must parse again with Parse since ParseRequestURI incorrectly parses URLs that contain a fragment 171 // part and will incorrectly append the fragment to either the path or the query, depending on which it was adjacent to. 172 u, err := url.Parse(s) 173 if err != nil { 174 // Errors are not expected here since Parse is a more lenient parser than ParseRequestURI. 175 return types.NewErr("URL parse error during conversion from string: %v", err) 176 } 177 return apiservercel.URL{URL: u} 178 } 179 180 func getScheme(arg ref.Val) ref.Val { 181 u, ok := arg.Value().(*url.URL) 182 if !ok { 183 return types.MaybeNoSuchOverloadErr(arg) 184 } 185 return types.String(u.Scheme) 186 } 187 188 func getHost(arg ref.Val) ref.Val { 189 u, ok := arg.Value().(*url.URL) 190 if !ok { 191 return types.MaybeNoSuchOverloadErr(arg) 192 } 193 return types.String(u.Host) 194 } 195 196 func getHostname(arg ref.Val) ref.Val { 197 u, ok := arg.Value().(*url.URL) 198 if !ok { 199 return types.MaybeNoSuchOverloadErr(arg) 200 } 201 return types.String(u.Hostname()) 202 } 203 204 func getPort(arg ref.Val) ref.Val { 205 u, ok := arg.Value().(*url.URL) 206 if !ok { 207 return types.MaybeNoSuchOverloadErr(arg) 208 } 209 return types.String(u.Port()) 210 } 211 212 func getEscapedPath(arg ref.Val) ref.Val { 213 u, ok := arg.Value().(*url.URL) 214 if !ok { 215 return types.MaybeNoSuchOverloadErr(arg) 216 } 217 return types.String(u.EscapedPath()) 218 } 219 220 func getQuery(arg ref.Val) ref.Val { 221 u, ok := arg.Value().(*url.URL) 222 if !ok { 223 return types.MaybeNoSuchOverloadErr(arg) 224 } 225 226 result := map[ref.Val]ref.Val{} 227 for k, v := range u.Query() { 228 result[types.String(k)] = types.NewStringList(types.DefaultTypeAdapter, v) 229 } 230 return types.NewRefValMap(types.DefaultTypeAdapter, result) 231 } 232 233 func isURL(arg ref.Val) ref.Val { 234 s, ok := arg.Value().(string) 235 if !ok { 236 return types.MaybeNoSuchOverloadErr(arg) 237 } 238 _, err := url.ParseRequestURI(s) 239 return types.Bool(err == nil) 240 }