Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HCL relative_url() function #361

Merged
merged 10 commits into from
Oct 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
}
})
}
}