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  }