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 remove_, set_ and add_headers #98

Merged
merged 7 commits into from
Jan 19, 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
6 changes: 4 additions & 2 deletions config/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@ func (b Backend) Schema(inline bool) *hcl.BodySchema {
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"`
AddRequestHeaders map[string]string `hcl:"add_request_headers,optional"`
DelRequestHeaders []string `hcl:"remove_request_headers,optional"`
SetResponseHeaders map[string]string `hcl:"set_response_headers,optional"`
AddResponseHeaders map[string]string `hcl:"add_response_headers,optional"`
DelResponseHeaders []string `hcl:"remove_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"`
Expand Down
6 changes: 5 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,11 @@ A `backend` defines the connection to a local/remote backend service. Backends c
| `origin` | URL to connect to for backend requests </br> &#9888; must start with the scheme `http://...` ||
| `path` | changeable part of upstream URL ||
| `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`. | `64MiB` |
| `set_request_headers` | header map to define additional or override header for the `origin` request ||
| `add_request_headers` | header map to define additional header values for the `origin` request ||
| `add_response_headers` | same as `add_request_headers` for the client response ||
| `remove_request_headers` | header list to define header to be removed from the `origin` request ||
| `remove_response_headers` | same as `remove_request_headers` for the client response ||
| `set_request_headers` | header map to override header for the `origin` request ||
| `set_response_headers` | same as `set_request_headers` for the client response ||
| [`openapi`](#openapi_block) | Definition for validating outgoing requests to the `origin` and incoming responses from the `origin`. ||
| [`remove_query_params`](#query_params) | a list of query parameters to be removed from the upstream request URL ||
Expand Down
6 changes: 4 additions & 2 deletions handler/context_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import (
)

const (
attrReqHeaders = "request_headers"
attrResHeaders = "response_headers"
attrSetReqHeaders = "set_request_headers"
attrAddReqHeaders = "add_request_headers"
attrDelReqHeaders = "remove_request_headers"
attrSetResHeaders = "set_response_headers"
attrAddResHeaders = "add_response_headers"
attrDelResHeaders = "remove_response_headers"
attrAddQueryParams = "add_query_params"
attrDelQueryParams = "remove_query_params"
attrSetQueryParams = "set_query_params"
Expand Down
182 changes: 110 additions & 72 deletions handler/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ func (p *Proxy) getTransport(scheme, origin, hostname string) *http.Transport {
func (p *Proxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
startTime := time.Now()

if p.options.CORS != nil && isCorsPreflightRequest(req) {
if isCorsPreflightRequest(req) {
p.setCorsRespHeaders(rw.Header(), req)
rw.WriteHeader(http.StatusNoContent)
return
Expand Down Expand Up @@ -406,108 +406,146 @@ func (p *Proxy) Director(req *http.Request) error {

func (p *Proxy) SetRoundtripContext(req *http.Request, beresp *http.Response) {
var (
attrCtx = []string{attrReqHeaders, attrSetReqHeaders}
bereq *http.Request
headerCtx http.Header
attrCtxAdd = attrAddReqHeaders
attrCtxDel = attrDelReqHeaders
attrCtxSet = attrSetReqHeaders
bereq *http.Request
headerCtx http.Header
)

if beresp != nil {
attrCtx = []string{attrResHeaders, attrSetResHeaders}
attrCtxAdd = attrAddResHeaders
attrCtxDel = attrDelResHeaders
attrCtxSet = attrSetResHeaders
bereq = beresp.Request
headerCtx = beresp.Header

defer p.setCorsRespHeaders(headerCtx, req)
} else if req != nil {
headerCtx = req.Header
}

evalCtx := eval.NewHTTPContext(p.evalContext, p.bufferOption, req, bereq, beresp)

// Remove blacklisted headers after evaluation to be accessible within our context configuration.
if attrCtx[0] == attrReqHeaders {
for _, key := range headerBlacklist {
headerCtx.Del(key)
// Remove blacklisted headers after evaluation to
// be accessible within our context configuration.
if attrCtxSet == attrSetReqHeaders {
for _, key := range headerBlacklist {
headerCtx.Del(key)
}
}
}

allAttributes, attrOk := p.options.Context.(body.Attributes)
if !attrOk {
return
}

evalCtx := eval.NewHTTPContext(p.evalContext, p.bufferOption, req, bereq, beresp)

var modifyQuery bool

u := *req.URL
u.RawQuery = strings.ReplaceAll(u.RawQuery, "+", "%2B")
values := u.Query()

for _, attrs := range allAttributes.JustAllAttributes() {
// apply header values in hierarchical and logical order: delete, set, add
attr, ok := attrs[attrCtxDel]
if ok {
val, diags := attr.Expr.Value(evalCtx)
if seetie.SetSeverityLevel(diags).HasErrors() {
p.log.WithField("parse config", p.String()).Error(diags)
}

for _, key := range seetie.ValueToStringSlice(val) {
k := http.CanonicalHeaderKey(key)
if k == "User-Agent" {
headerCtx[k] = []string{}
continue
}

// apply header values
for _, ctxName := range attrCtx { // headers
if !attrOk {
break
headerCtx.Del(k)
}
}

for _, attrs := range allAttributes.JustAllAttributesWithName(ctxName) {
attr, ok := attrs[ctxName]
if !ok {
continue
attr, ok = attrs[attrCtxSet]
if ok {
options, diags := NewOptionsMap(evalCtx, attr)
if diags != nil {
p.log.WithField("parse config", p.String()).Error(diags)
}

for key, values := range options {
k := http.CanonicalHeaderKey(key)
headerCtx[k] = values
}
}

attr, ok = attrs[attrCtxAdd]
if ok {
options, diags := NewOptionsMap(evalCtx, attr)
if diags.HasErrors() {
if diags != nil {
p.log.WithField("parse config", p.String()).Error(diags)
continue
}
setHeaderFields(headerCtx, options)

for key, values := range options {
k := http.CanonicalHeaderKey(key)
headerCtx[k] = append(headerCtx[k], values...)
}
}
}

// apply query params in hierarchical and logical order: delete, set, add
if attrOk && req != nil && beresp == nil { // just one way -> origin
var modify bool
if req == nil || beresp != nil { // just one way -> origin
continue
}

u := *req.URL
u.RawQuery = strings.ReplaceAll(u.RawQuery, "+", "%2B")
values := u.Query()
// apply query params in hierarchical and logical order: delete, set, add
attr, ok = attrs[attrDelQueryParams]
if ok {
val, diags := attr.Expr.Value(evalCtx)
if seetie.SetSeverityLevel(diags).HasErrors() {
p.log.WithField("parse config", p.String()).Error(diags)
}

// not by name to ensure the order for all params
for _, attrs := range allAttributes.JustAllAttributes() {
attr, ok := attrs[attrDelQueryParams]
if ok {
val, diags := attr.Expr.Value(evalCtx)
if seetie.SetSeverityLevel(diags).HasErrors() {
p.log.WithField("parse config", p.String()).Error(diags)
}
for _, key := range seetie.ValueToStringSlice(val) {
values.Del(key)
}
modify = true
for _, key := range seetie.ValueToStringSlice(val) {
values.Del(key)
}

attr, ok = attrs[attrSetQueryParams]
if ok {
options, diags := NewOptionsMap(evalCtx, attr)
if diags != nil {
p.log.WithField("parse config", p.String()).Error(diags)
}
for k, v := range options {
values[k] = v
}
modify = true
modifyQuery = true
}

attr, ok = attrs[attrSetQueryParams]
if ok {
options, diags := NewOptionsMap(evalCtx, attr)
if diags != nil {
p.log.WithField("parse config", p.String()).Error(diags)
}

attr, ok = attrs[attrAddQueryParams]
if ok {
options, diags := NewOptionsMap(evalCtx, attr)
if diags != nil {
p.log.WithField("parse config", p.String()).Error(diags)
}
for k, v := range options {
if _, ok = values[k]; !ok {
values[k] = v
} else {
values[k] = append(values[k], v...)
}
}
modify = true
for k, v := range options {
values[k] = v
}

modifyQuery = true
}

if modify {
req.URL.RawQuery = strings.ReplaceAll(values.Encode(), "+", "%20")
attr, ok = attrs[attrAddQueryParams]
if ok {
options, diags := NewOptionsMap(evalCtx, attr)
if diags != nil {
p.log.WithField("parse config", p.String()).Error(diags)
}

for k, v := range options {
if _, ok = values[k]; !ok {
values[k] = v
} else {
values[k] = append(values[k], v...)
}
}

modifyQuery = true
}
}

if beresp != nil && isCorsRequest(req) {
p.setCorsRespHeaders(headerCtx, req)
if modifyQuery {
req.URL.RawQuery = strings.ReplaceAll(values.Encode(), "+", "%20")
}
}

Expand Down Expand Up @@ -549,15 +587,15 @@ func isCorsRequest(req *http.Request) bool {
}

func isCorsPreflightRequest(req *http.Request) bool {
return isCorsRequest(req) && req.Method == http.MethodOptions && (req.Header.Get("Access-Control-Request-Method") != "" || req.Header.Get("Access-Control-Request-Headers") != "")
return req.Method == http.MethodOptions && (req.Header.Get("Access-Control-Request-Method") != "" || req.Header.Get("Access-Control-Request-Headers") != "")
}

func IsCredentialed(headers http.Header) bool {
return headers.Get("Cookie") != "" || headers.Get("Authorization") != "" || headers.Get("Proxy-Authorization") != ""
}

func (p *Proxy) setCorsRespHeaders(headers http.Header, req *http.Request) {
if p.options.CORS == nil {
if p.options.CORS == nil || !isCorsRequest(req) {
return
}
requestOrigin := req.Header.Get("Origin")
Expand Down
12 changes: 0 additions & 12 deletions handler/proxy_cors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,18 +155,6 @@ func TestCORSOptions_isCorsPreflightRequest(t *testing.T) {
map[string]string{"Origin": "https://www.example.com"},
false,
},
{
"OPTIONS, without Origin, with ACRM",
http.MethodOptions,
map[string]string{"Access-Control-Request-Method": "POST"},
false,
},
{
"OPTIONS, without Origin, with ACRH",
http.MethodOptions,
map[string]string{"Access-Control-Request-Headers": "Content-Type"},
false,
},
{
"POST, with Origin, with ACRM",
http.MethodPost,
Expand Down
14 changes: 7 additions & 7 deletions handler/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -954,16 +954,16 @@ func TestProxy_SetRoundtripContext_Null_Eval(t *testing.T) {

for i, tc := range []testCase{
{"no eval", `path = "/"`, test.Header{}},
{"json_body client field", `response_headers = { "x-client" = "my-val-x-${req.json_body.client}" }`,
{"json_body client field", `set_response_headers = { "x-client" = "my-val-x-${req.json_body.client}" }`,
test.Header{
"x-client": "my-val-x-true",
}},
{"json_body non existing field", `response_headers = {
{"json_body non existing field", `set_response_headers = {
"${beresp.json_body.not-there}" = "my-val-0-${beresp.json_body.origin}"
"${req.json_body.client}-my-val-a" = "my-val-b-${beresp.json_body.client}"
}`,
test.Header{"true-my-val-a": ""}}, // since one reference is failing ('not-there') the whole block does
{"json_body null value", `response_headers = { "x-null" = "${beresp.json_body.nil}" }`, test.Header{"x-null": ""}},
{"json_body null value", `set_response_headers = { "x-null" = "${beresp.json_body.nil}" }`, test.Header{"x-null": ""}},
} {
t.Run(tc.name, func(st *testing.T) {
h := test.New(st)
Expand Down Expand Up @@ -1055,10 +1055,10 @@ func TestProxy_BufferingOptions(t *testing.T) {
{"beresp validation", newOptions(), `path = "/"`, eval.BufferResponse},
{"bereq validation", newOptions(), `path = "/"`, eval.BufferRequest},
{"no validation", newOptions(), `path = "/"`, eval.BufferNone},
{"req buffer json.body & beresp validation", newOptions(), `response_headers = { x-test = "${req.json_body.client}" }`, eval.BufferRequest | eval.BufferResponse},
{"beresp buffer json.body & bereq validation", newOptions(), `response_headers = { x-test = "${beresp.json_body.origin}" }`, eval.BufferRequest | eval.BufferResponse},
{"req buffer json.body & bereq validation", newOptions(), `response_headers = { x-test = "${req.json_body.client}" }`, eval.BufferRequest},
{"beresp buffer json.body & beresp validation", newOptions(), `response_headers = { x-test = "${beresp.json_body.origin}" }`, eval.BufferResponse},
{"req buffer json.body & beresp validation", newOptions(), `set_response_headers = { x-test = "${req.json_body.client}" }`, eval.BufferRequest | eval.BufferResponse},
{"beresp buffer json.body & bereq validation", newOptions(), `set_response_headers = { x-test = "${beresp.json_body.origin}" }`, eval.BufferRequest | eval.BufferResponse},
{"req buffer json.body & bereq validation", newOptions(), `set_response_headers = { x-test = "${req.json_body.client}" }`, eval.BufferRequest},
{"beresp buffer json.body & beresp validation", newOptions(), `set_response_headers = { x-test = "${beresp.json_body.origin}" }`, eval.BufferResponse},
} {
t.Run(tc.name, func(st *testing.T) {
h := test.New(st)
Expand Down
3 changes: 3 additions & 0 deletions internal/test/test_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ func createAnythingHandler(status int) func(rw http.ResponseWriter, req *http.Re
rw.Header().Set("Content-Length", strconv.Itoa(len(respContent)))
rw.Header().Set("Content-Type", "application/json")

rw.Header().Set("Remove-Me-1", "r1")
rw.Header().Set("Remove-Me-2", "r2")

rw.WriteHeader(status)
_, _ = rw.Write(respContent)
}
Expand Down
Loading