Skip to content

Commit

Permalink
HCL relative_url() function (#361)
Browse files Browse the repository at this point in the history
* Add HCL relative_url() function

* Tests

* Docu

* Changelog

* Typo

* Move code

* Docu

* Comment + error format
  • Loading branch information
Alex Schneider authored Oct 28, 2021
1 parent 22ff404 commit e977218
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 2 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ Unreleased changes are available as `avenga/couper:edge` container.

* **Added**
* Register `default` function as `coalesce` alias ([#356](https://github.com/avenga/couper/pull/356))

* New HCL function [`relative_url()`](./docs/REFERENCE.md#functions) ([#361](https://github.com/avenga/couper/pull/361))

* **Fixed**
* Handling of [`accept_forwarded_url`](./docs/REFERENCE.md#settings-block) "host" if `H-Forwarded-Host` request header field contains a port ([#360](https://github.com/avenga/couper/pull/360))
* Setting `Vary` response header fields for [CORS](./doc/REFERENCE.md#cors-block) ([#362](https://github.com/avenga/couper/pull/362))
Expand Down
1 change: 1 addition & 0 deletions docs/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,7 @@ To access the HTTP status code of the `default` response use `backend_responses.
| `merge` | object or tuple | Deep-merges two or more of either objects or tuples. `null` arguments are ignored. A `null` attribute value in an object removes the previous attribute value. An attribute value with a different type than the current value is set as the new value. `merge()` with no parameters returns `null`. | `arg...` (object or tuple) | `merge(request.headers, { x-additional = "myval" })` |
| `beta_oauth_authorization_url` | string | Creates an OAuth2 authorization URL from a referenced [OAuth2 AC Block](#oauth2-ac-block-beta) or [OIDC Block](#oidc-block-beta). | `label` (string) | `beta_oauth_authorization_url("myOAuth2")` |
| `beta_oauth_verifier` | string | Creates a cryptographically random key as specified in RFC 7636, applicable for all verifier methods; e.g. to be set as a cookie and read into `verifier_value`. Multiple calls of this function in the same client request context return the same value. | | `beta_oauth_verifier()` |
| `relative_url` | string | Returns a relative URL by retaining `path`, `query` and `fragment` components. The input URL `s` must begin with `/<path>`, `//<authority>`, `http://` or `https://`, otherwise an error is thrown. | s (string) | `relative_url("https://httpbin.org/anything?query#fragment") // returns "/anything?query#fragment"` |
| `saml_sso_url` | string | Creates a SAML SingleSignOn URL (including the `SAMLRequest` parameter) from a referenced [SAML Block](#saml-block). | `label` (string) | `saml_sso_url("mySAML")` |
| `to_lower` | string | Converts a given string to lowercase. | `s` (string) | `to_lower(request.cookies.name)` |
| `to_upper` | string | Converts a given string to uppercase. | `s` (string) | `to_upper("CamelCase")` |
Expand Down
1 change: 1 addition & 0 deletions eval/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,7 @@ func newFunctionsMap() map[string]function.Function {
"json_decode": stdlib.JSONDecodeFunc,
"json_encode": stdlib.JSONEncodeFunc,
"merge": lib.MergeFunc,
"relative_url": lib.RelativeUrlFunc,
"to_lower": stdlib.LowerFunc,
"to_upper": stdlib.UpperFunc,
"unixtime": lib.UnixtimeFunc,
Expand Down
39 changes: 38 additions & 1 deletion eval/lib/url.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package lib

import (
"fmt"
"net/url"
"regexp"
"strings"

"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)

var (
UrlEncodeFunc = newUrlEncodeFunction()
// https://datatracker.ietf.org/doc/html/rfc3986#page-50
regexParseURL = regexp.MustCompile(`^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?`)
UrlEncodeFunc = newUrlEncodeFunction()
RelativeUrlFunc = newRelativeUrlFunction()
)

func newUrlEncodeFunction() function.Function {
Expand Down Expand Up @@ -38,3 +43,35 @@ func AbsoluteURL(urlRef string, origin *url.URL) (string, error) {
}
return urlRef, nil
}

func newRelativeUrlFunction() function.Function {
return function.New(&function.Spec{
Params: []function.Parameter{{
Name: "s",
Type: cty.String,
}},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, _ cty.Type) (ret cty.Value, err error) {
absURL := strings.TrimSpace(args[0].AsString())

if !strings.HasPrefix(absURL, "/") && !strings.HasPrefix(absURL, "http://") && !strings.HasPrefix(absURL, "https://") {
return cty.StringVal(""), fmt.Errorf("invalid url given: %q", absURL)
}

// Do not use the result of url.Parse() to preserve the # character in an emtpy fragment.
if _, err := url.Parse(absURL); err != nil {
return cty.StringVal(""), err
}

// The regexParseURL garanties the len of 10 in the result.
urlParts := regexParseURL.FindStringSubmatch(absURL)

// The path must begin w/ a slash.
if !strings.HasPrefix(urlParts[5], "/") {
urlParts[5] = "/" + urlParts[5]
}

return cty.StringVal(urlParts[5] + urlParts[6] + urlParts[8]), nil
},
})
}
69 changes: 69 additions & 0 deletions eval/lib/url_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package lib_test

import (
"fmt"
"testing"

"github.com/zclconf/go-cty/cty"
Expand Down Expand Up @@ -33,3 +34,71 @@ func TestUrlEncode(t *testing.T) {
t.Errorf("Wrong return value; expected %s, got: %s", expected, encoded)
}
}

func TestRelativeUrl(t *testing.T) {
helper := test.New(t)

cf, err := configload.LoadBytes([]byte(`server "test" {}`), "couper.hcl")
helper.Must(err)

hclContext := cf.Context.Value(request.ContextType).(*eval.Context).HCLContext()
relativeUrlFunc := hclContext.Functions["relative_url"]

type testCase struct {
url string
expURL string
expErr string
}

for _, tc := range []testCase{
// Invalid
{"", "", `invalid url given: ""`},
{"rel", "", `invalid url given: "rel"`},
{"?q", "", `invalid url given: "?q"`},
{"?", "", `invalid url given: "?"`},
{"#f", "", `invalid url given: "#f"`},
{"#", "", `invalid url given: "#"`},
{"~", "", `invalid url given: "~"`},
{"abc@def.org", "", `invalid url given: "abc@def.org"`},
{"ftp://127.0.0.1", "", `invalid url given: "ftp://127.0.0.1"`},

// Valid
{"/abs", "/abs", ``},
{"//path", "/", ``},
{"///path", "/path", ``},
{"/abs:8080", "/abs:8080", ``},
{"https://abc.def:8443:9443", "/", ``},
{"http://", "/", ``},
{"http://abc", "/", ``},
{"http://abc.def", "/", ``},
{"http://abc.def?", "/?", ``},
{"http://abc.def#", "/#", ``},
{"http://abc.def/#", "/#", ``},
{"http://abc.def?#", "/?#", ``},
{"http://abc.def/?#", "/?#", ``},
{"https://abc.def/path", "/path", ``},
{"https://abc.def/path?a+b", "/path?a+b", ``},
{"https://abc.def/path?a%20b", "/path?a%20b", ``},
{"https://abc.def:8443/path?q", "/path?q", ``},
{"https://abc.def:8443/path?q#f", "/path?q#f", ``},
{"https://user:pass@abc.def:8443/path?q#f", "/path?q#f", ``},
{"//user:pass@abc.def:8443/path?q#f", "/path?q#f", ``},
{"//abc.def:8443/path?q#f", "/path?q#f", ``},
} {
t.Run(tc.url, func(subT *testing.T) {
got, err := relativeUrlFunc.Call([]cty.Value{cty.StringVal(tc.url)})

if tc.expURL != "" && got.AsString() != tc.expURL {
t.Errorf("'%#v': expected %q, got %q", tc.url, tc.expURL, got.AsString())
}
if got != cty.NilVal && tc.expURL == "" {
t.Errorf("'%#v': expected 'cty.NilVal', got %q", tc.url, got.AsString())
}
if tc.expErr != "" || err != nil {
if eerr := fmt.Sprintf("%s", err); tc.expErr != eerr {
t.Errorf("'%#v': expected %q, got %q", tc.url, tc.expErr, eerr)
}
}
})
}
}

0 comments on commit e977218

Please sign in to comment.