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

Add query params #73

Merged
merged 13 commits into from
Dec 9, 2020
18 changes: 11 additions & 7 deletions config/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package config
import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/zclconf/go-cty/cty"
)

var _ Inline = &Backend{}
Expand All @@ -27,13 +28,16 @@ func (b Backend) Schema(inline bool) *hcl.BodySchema {
}

type Inline struct {
Origin string `hcl:"origin,optional"`
Hostname string `hcl:"hostname,optional"`
Path string `hcl:"path,optional"`
RequestHeaders map[string]string `hcl:"request_headers,optional"`
ResponseHeaders map[string]string `hcl:"response_headers,optional"`
SetRequestHeaders map[string]string `hcl:"set_request_headers,optional"`
SetResponseHeaders map[string]string `hcl:"set_response_headers,optional"`
Origin string `hcl:"origin,optional"`
Hostname string `hcl:"hostname,optional"`
Path string `hcl:"path,optional"`
RequestHeaders map[string]string `hcl:"request_headers,optional"`
ResponseHeaders map[string]string `hcl:"response_headers,optional"`
SetRequestHeaders map[string]string `hcl:"set_request_headers,optional"`
SetResponseHeaders map[string]string `hcl:"set_response_headers,optional"`
AddQueryParams map[string]cty.Value `hcl:"add_query_params,optional"`
DelQueryParams []string `hcl:"remove_query_params,optional"`
SetQueryParams map[string]cty.Value `hcl:"set_query_params,optional"`
}

schema, _ = gohcl.ImpliedBodySchema(&Inline{})
Expand Down
8 changes: 6 additions & 2 deletions config/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package config
import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/zclconf/go-cty/cty"
)

var _ Inline = &Endpoint{}
Expand All @@ -26,8 +27,11 @@ func (e Endpoint) Schema(inline bool) *hcl.BodySchema {
}

type Inline struct {
Backend *Backend `hcl:"backend,block"`
Path string `hcl:"path,optional"`
Backend *Backend `hcl:"backend,block"`
Path string `hcl:"path,optional"`
AddQueryParams map[string]cty.Value `hcl:"add_query_params,optional"`
DelQueryParams []string `hcl:"remove_query_params,optional"`
SetQueryParams map[string]cty.Value `hcl:"set_query_params,optional"`
}
schema, _ := gohcl.ImpliedBodySchema(&Inline{})

Expand Down
29 changes: 16 additions & 13 deletions config/runtime/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,10 @@ func NewServerConfiguration(conf *config.Gateway, httpConf *HTTPConfig, log *log
// set server context for defined backends
be := backends[endpoint.Backend]
_, remain := be.conf.Merge(&config.Backend{Remain: endpoint.Remain})
backend = newProxy(confCtx, be.conf, srvConf.API.CORS, remain, log, serverOptions)
backend, err = newProxy(confCtx, be.conf, srvConf.API.CORS, remain, log, serverOptions)
if err != nil {
return nil, err
}
} else {
// otherwise try to parse an inline block and fallback for api reference or inline block
inlineBackend, err := newInlineBackend(confCtx, conf.Bytes, backends, srvConf.API, endpoint, log, serverOptions)
Expand All @@ -204,17 +207,17 @@ func NewServerConfiguration(conf *config.Gateway, httpConf *HTTPConfig, log *log
return serverConfiguration, nil
}

func newProxy(ctx *hcl.EvalContext, beConf *config.Backend, corsOpts *config.CORS, remainCtx []hcl.Body, log *logrus.Entry, srvOpts *server.Options) http.Handler {
func newProxy(ctx *hcl.EvalContext, beConf *config.Backend, corsOpts *config.CORS, remainCtx []hcl.Body, log *logrus.Entry, srvOpts *server.Options) (http.Handler, error) {
corsOptions, err := handler.NewCORSOptions(corsOpts)
if err != nil {
log.Fatal(err)
return nil, err
}

for _, name := range []string{"request_headers", "response_headers"} {
for _, body := range remainCtx {
attr, err := body.JustAttributes()
if err != nil {
log.Fatal(err)
return nil, err
}
if _, ok := attr[name]; ok {
log.Warningf("'%s' is deprecated, use 'set_%s' instead", name, name)
Expand All @@ -224,14 +227,10 @@ func newProxy(ctx *hcl.EvalContext, beConf *config.Backend, corsOpts *config.COR

proxyOptions, err := handler.NewProxyOptions(beConf, corsOptions, remainCtx)
if err != nil {
log.Fatal(err)
return nil, err
}

proxy, err := handler.NewProxy(proxyOptions, log, srvOpts, ctx)
if err != nil {
log.Fatal(err)
}
return proxy
return handler.NewProxy(proxyOptions, log, srvOpts, ctx)
}

func newBackendsFromDefinitions(conf *config.Gateway, confCtx *hcl.EvalContext, log *logrus.Entry) (map[string]backendDefinition, error) {
Expand All @@ -254,9 +253,13 @@ func newBackendsFromDefinitions(conf *config.Gateway, confCtx *hcl.EvalContext,
beConf, _ = defaultBackendConf.Merge(beConf)

srvOpts, _ := server.NewServerOptions(&config.Server{})
proxy, err := newProxy(confCtx, beConf, nil, []hcl.Body{beConf.Remain}, log, srvOpts)
if err != nil {
return nil, err
}
backends[beConf.Name] = backendDefinition{
conf: beConf,
handler: newProxy(confCtx, beConf, nil, []hcl.Body{beConf.Remain}, log, srvOpts),
handler: proxy,
}
}
return backends, nil
Expand Down Expand Up @@ -432,6 +435,7 @@ func newInlineBackend(
if diags.HasErrors() {
return nil, diags
}
bodies = append(bodies, inlineDef.Body())
malud marked this conversation as resolved.
Show resolved Hide resolved
bodies = append(bodies, backendConf.Body())
}

Expand Down Expand Up @@ -490,8 +494,7 @@ func newInlineBackend(
return nil, err
}

proxy := newProxy(evalCtx, backendConf, parentAPI.CORS, bodies, log, srvOpts)
return proxy, nil
return newProxy(evalCtx, backendConf, parentAPI.CORS, bodies, log, srvOpts)
}

func getBackendInlineBlock(inline config.Inline, evalCtx *hcl.EvalContext) (*hcl.Block, error) {
Expand Down
48 changes: 48 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,51 @@ Endpoints define the entry points of Couper. The mandatory *label* defines the p
| `path`|<ul><li>changeable part of upstream URL</li><li>changes the path suffix of the outgoing request</li></ul>|
|[**`access_control`**](#access_control_attribute)|sets predefined `access_control` for `endpoint`|
|[**`backend`**](#backend_block) block |configures connection to a local/remote backend service for `endpoint`|
|[**`remove_query_params`**](#query_params)|<ul><li>a list of query parameters to be removed from the upstream request URL</li></ul> |
|[**`set_query_params`**](#query_params)|<ul><li>key/value(s) pairs to set query parameters in the upstream request URL</li></ul> |
|[**`add_query_params`**](#query_params)|<ul><li>key/value(s) pairs to add query parameters to the upstream request URL</li></ul> |

#### Query parameter <a name="query_params"></a>

Couper offers three methods to manipulate the query parameter. The methods are
executed in the following order unrelated to the order within the configuration file:
malud marked this conversation as resolved.
Show resolved Hide resolved

* `remove_query_params`: a list of query parameters to be removed from the upstream request URL.
* `set_query_params`: key/value(s) pairs to set query parameters in the upstream request URL.
* `add_query_params`: key/value(s) pairs to add query parameters to the upstream request URL.

All `*_query_params` are collected and executed from: `definitions.backend`, `endpoint`,
`endpoint.backend` (if refined).

```hcl
server "my_project" {
api {
endpoint "/" {
backend "example"
}
}
}

definitions {
backend "example" {
origin = "http://example.com"

remove_query_params = ["a", "b"]

set_query_params = {
string = "string"
multi = ["foo", "bar"]
"${req.headers.example}" = "yes"
}

add_query_params = {
noop = req.headers.noop
null = null
empty = ""
}
}
}
```

#### Path parameter

Expand All @@ -300,6 +345,9 @@ A `backend` defines the connection to a local/remote backend service. Backends c
| `set_request_headers` | header map to define additional or override header for the `origin` request |
| `set_response_headers` | same as `set_request_headers` for the client response |
| `request_body_limit` | Limit to configure the maximum buffer size while accessing `req.post` or `req.json_body` content. Valid units are: `KiB, MiB, GiB`. Default: `64MiB`. |
|[**`remove_query_params`**](#query_params)|<ul><li>a list of query parameters to be removed from the upstream request URL</li></ul> |
|[**`set_query_params`**](#query_params)|<ul><li>key/value(s) pairs to set query parameters in the upstream request URL</li></ul> |
|[**`add_query_params`**](#query_params)|<ul><li>key/value(s) pairs to add query parameters to the upstream request URL</li></ul> |

### The `access_control` attribute <a name="access_control_attribute"></a>
The configuration of access control is twofold in Couper: You define the particular type (such as `jwt` or `basic_auth`) in `definitions`, each with a distinct label. Anywhere in the `server` block those labels can be used in the `access_control` list to protect that block.
Expand Down
67 changes: 65 additions & 2 deletions handler/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,28 @@ func (p *Proxy) Director(req *http.Request) error {
return nil
}

func modifyQuery(url *url.URL, del []string, set, add map[string][]string) {
query := url.Query()

for _, del := range del {
query.Del(del)
}
for k, values := range set {
query.Del(k)

for _, v := range values {
query.Add(k, v)
}
}
for k, values := range add {
for _, v := range values {
query.Add(k, v)
}
}

url.RawQuery = query.Encode()
}

func (p *Proxy) SetRoundtripContext(req *http.Request, beresp *http.Response) {
var (
attrCtx = []string{attrReqHeaders, attrSetReqHeaders}
Expand All @@ -376,6 +398,11 @@ func (p *Proxy) SetRoundtripContext(req *http.Request, beresp *http.Response) {
headerCtx = req.Header
}

var delQuery []string
addQuery := make(map[string][]string)
setQuery := make(map[string][]string)

schema := config.Backend{}.Schema(true)
evalCtx := eval.NewHTTPContext(p.evalContext, p.bufferOption, req, bereq, beresp)

// Remove blacklisted headers after evaluation to be accessible within our context configuration.
Expand All @@ -385,16 +412,35 @@ func (p *Proxy) SetRoundtripContext(req *http.Request, beresp *http.Response) {
}
}

for _, ctx := range attrCtx {
for _, ctxBody := range p.options.Context {
for _, ctxBody := range p.options.Context {
for _, ctx := range attrCtx {
// headers
options, err := NewCtxOptions(ctx, evalCtx, ctxBody)
if err != nil {
p.log.WithField("parse config", p.String()).Error(err)
}
setHeaderFields(headerCtx, options)
}

// query params
content, _, _ := ctxBody.PartialContent(schema)
if del, ok := content.Attributes["remove_query_params"]; ok {
originValue, diags := del.Expr.Value(evalCtx)
if diags != nil && diags.HasErrors() {
panic(diags.Error())
}
delQuery = append(delQuery, seetie.ValueToStringSlice(originValue)...)
}
if add, ok := content.Attributes["add_query_params"]; ok {
collectQueryParams(evalCtx, add, addQuery)
}
if set, ok := content.Attributes["set_query_params"]; ok {
collectQueryParams(evalCtx, set, setQuery)
}
}

modifyQuery(req.URL, delQuery, setQuery, addQuery)
malud marked this conversation as resolved.
Show resolved Hide resolved

if beresp != nil && isCorsRequest(req) {
p.setCorsRespHeaders(headerCtx, req)
}
Expand Down Expand Up @@ -613,6 +659,23 @@ func setHeaderFields(header http.Header, options OptionsMap) {
}
}

func collectQueryParams(ctx *hcl.EvalContext, attr *hcl.Attribute, dest map[string][]string) {
malud marked this conversation as resolved.
Show resolved Hide resolved
originValues, _ := seetie.ExpToMap(ctx, attr.Expr)

for k, v := range originValues {
values := dest[k]

switch v.(type) {
case []string:
values = append(values, v.([]string)...)
case string:
values = append(values, v.(string))
}

dest[k] = values
}
}

func getAttribute(ctx *hcl.EvalContext, name string, body *hcl.BodyContent) string {
attr := body.Attributes
if _, ok := attr[name]; !ok {
Expand Down
3 changes: 2 additions & 1 deletion internal/test/test_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func (b *Backend) Addr() string {
func createAnythingHandler(status int) func(rw http.ResponseWriter, req *http.Request) {
return func(rw http.ResponseWriter, req *http.Request) {
type anything struct {
Args url.Values
Args, Query url.Values
Headers http.Header
Host string
Path string
Expand All @@ -60,6 +60,7 @@ func createAnythingHandler(status int) func(rw http.ResponseWriter, req *http.Re
Method: req.Method,
Path: req.URL.Path,
RemoteAddr: req.RemoteAddr,
Query: req.URL.Query(),
Url: req.URL.String(),
UserAgent: req.UserAgent(),
ResponseStatus: status,
Expand Down
Loading