Skip to content

Commit

Permalink
Merge pull request #248 from avenga/global-response-headers
Browse files Browse the repository at this point in the history
Global response headers
  • Loading branch information
Marcel Ludwig authored Jun 2, 2021
2 parents 477c37f + 7c2035e commit b26ccb1
Show file tree
Hide file tree
Showing 19 changed files with 350 additions and 36 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Unreleased changes are available as `avenga/couper:edge` container.

* **Changed**
* Stronger configuration check for `path` and `path_prefix` attributes, possibly resulting in configuration errors ([#232](https://github.com/avenga/couper/pull/232))
* Modifier (`set/add/remove_response_headers`) is available for `api`, `files`, `server` and `spa` block too ([#248](https://github.com/avenga/couper/pull/248))

* **Fixed**
* The `path` field in the backend log ([#232](https://github.com/avenga/couper/pull/232))
Expand Down
31 changes: 31 additions & 0 deletions config/api.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
package config

import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
)

var _ Inline = &API{}

// API represents the <API> object.
type API struct {
AccessControl []string `hcl:"access_control,optional"`
Expand All @@ -8,9 +15,33 @@ type API struct {
DisableAccessControl []string `hcl:"disable_access_control,optional"`
Endpoints Endpoints `hcl:"endpoint,block"`
ErrorFile string `hcl:"error_file,optional"`
Remain hcl.Body `hcl:",remain"`
// internally used
CatchAllEndpoint *Endpoint
}

// APIs represents a list of <API> objects.
type APIs []*API

// HCLBody implements the <Inline> interface.
func (a API) HCLBody() hcl.Body {
return a.Remain
}

// Schema implements the <Inline> interface.
func (a API) Schema(inline bool) *hcl.BodySchema {
if !inline {
schema, _ := gohcl.ImpliedBodySchema(a)
return schema
}

type Inline struct {
AddResponseHeaders map[string]string `hcl:"add_response_headers,optional"`
DelResponseHeaders []string `hcl:"remove_response_headers,optional"`
SetResponseHeaders map[string]string `hcl:"set_response_headers,optional"`
}

schema, _ := gohcl.ImpliedBodySchema(&Inline{})

return schema
}
31 changes: 31 additions & 0 deletions config/files.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
package config

import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
)

var _ Inline = &Files{}

// Files represents the <Files> object.
type Files struct {
AccessControl []string `hcl:"access_control,optional"`
Expand All @@ -8,4 +15,28 @@ type Files struct {
DisableAccessControl []string `hcl:"disable_access_control,optional"`
DocumentRoot string `hcl:"document_root"`
ErrorFile string `hcl:"error_file,optional"`
Remain hcl.Body `hcl:",remain"`
}

// HCLBody implements the <Inline> interface.
func (f Files) HCLBody() hcl.Body {
return f.Remain
}

// Schema implements the <Inline> interface.
func (f Files) Schema(inline bool) *hcl.BodySchema {
if !inline {
schema, _ := gohcl.ImpliedBodySchema(f)
return schema
}

type Inline struct {
AddResponseHeaders map[string]string `hcl:"add_response_headers,optional"`
DelResponseHeaders []string `hcl:"remove_response_headers,optional"`
SetResponseHeaders map[string]string `hcl:"set_response_headers,optional"`
}

schema, _ := gohcl.ImpliedBodySchema(&Inline{})

return schema
}
12 changes: 8 additions & 4 deletions config/runtime/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca

var spaHandler http.Handler
if srvConf.Spa != nil {
spaHandler, err = handler.NewSpa(srvConf.Spa.BootstrapFile, serverOptions)
spaHandler, err = handler.NewSpa(srvConf.Spa.BootstrapFile, serverOptions, []hcl.Body{srvConf.Spa.Remain, srvConf.Remain})
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -173,7 +173,7 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca
}

if srvConf.Files != nil {
fileHandler, ferr := handler.NewFile(srvConf.Files.DocumentRoot, serverOptions)
fileHandler, ferr := handler.NewFile(srvConf.Files.DocumentRoot, serverOptions, []hcl.Body{srvConf.Files.Remain, srvConf.Remain})
if ferr != nil {
return nil, ferr
}
Expand Down Expand Up @@ -241,13 +241,17 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca
return nil, err
}

modifier := []hcl.Body{srvConf.Remain}

kind := endpoint
if parentAPI != nil {
kind = api

modifier = []hcl.Body{parentAPI.Remain, srvConf.Remain}
}
epOpts.LogHandlerKind = kind.String()

epHandler := handler.NewEndpoint(epOpts, log)
epHandler := handler.NewEndpoint(epOpts, log, modifier)
protectedHandler := middleware.NewCORSHandler(corsOptions, epHandler)

accessControl := newAC(srvConf, parentAPI)
Expand Down Expand Up @@ -517,7 +521,7 @@ func newErrorHandler(ctx *hcl.EvalContext, opts *protectedOptions, log *logrus.E
}

epOpts.LogHandlerKind = "error_" + k
kindsHandler[k] = handler.NewEndpoint(epOpts, log)
kindsHandler[k] = handler.NewEndpoint(epOpts, log, nil)
}
}
}
Expand Down
31 changes: 31 additions & 0 deletions config/server.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
package config

import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
)

var _ Inline = &Server{}

// Server represents the <Server> object.
type Server struct {
AccessControl []string `hcl:"access_control,optional"`
Expand All @@ -12,8 +19,32 @@ type Server struct {
Files *Files `hcl:"files,block"`
Hosts []string `hcl:"hosts,optional"`
Name string `hcl:"name,label"`
Remain hcl.Body `hcl:",remain"`
Spa *Spa `hcl:"spa,block"`
}

// Servers represents a list of <Server> objects.
type Servers []*Server

// HCLBody implements the <Inline> interface.
func (s Server) HCLBody() hcl.Body {
return s.Remain
}

// Schema implements the <Inline> interface.
func (s Server) Schema(inline bool) *hcl.BodySchema {
if !inline {
schema, _ := gohcl.ImpliedBodySchema(s)
return schema
}

type Inline struct {
AddResponseHeaders map[string]string `hcl:"add_response_headers,optional"`
DelResponseHeaders []string `hcl:"remove_response_headers,optional"`
SetResponseHeaders map[string]string `hcl:"set_response_headers,optional"`
}

schema, _ := gohcl.ImpliedBodySchema(&Inline{})

return schema
}
31 changes: 31 additions & 0 deletions config/spa.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
package config

import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
)

var _ Inline = &Spa{}

// Spa represents the <Spa> object.
type Spa struct {
AccessControl []string `hcl:"access_control,optional"`
Expand All @@ -8,4 +15,28 @@ type Spa struct {
CORS *CORS `hcl:"cors,block"`
DisableAccessControl []string `hcl:"disable_access_control,optional"`
Paths []string `hcl:"paths"`
Remain hcl.Body `hcl:",remain"`
}

// HCLBody implements the <Inline> interface.
func (s Spa) HCLBody() hcl.Body {
return s.Remain
}

// Schema implements the <Inline> interface.
func (s Spa) Schema(inline bool) *hcl.BodySchema {
if !inline {
schema, _ := gohcl.ImpliedBodySchema(s)
return schema
}

type Inline struct {
AddResponseHeaders map[string]string `hcl:"add_response_headers,optional"`
DelResponseHeaders []string `hcl:"remove_response_headers,optional"`
SetResponseHeaders map[string]string `hcl:"set_response_headers,optional"`
}

schema, _ := gohcl.ImpliedBodySchema(&Inline{})

return schema
}
8 changes: 4 additions & 4 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -700,11 +700,11 @@ executed ordered as follows:

| Modifier | Contexts | Description |
|:--------------------------|:------------------------------------------------------------------------------------------------|:------------|
| `remove_response_headers` | [Endpoint Block](#endpoint-block), [Proxy Block](#proxy-block), [Backend Block](#backend-block), [Error Handler](#error-handler) | List of response header to be removed from the client response. |
| `set_response_headers` | [Endpoint Block](#endpoint-block), [Proxy Block](#proxy-block), [Backend Block](#backend-block), [Error Handler](#error-handler) | Key/value(s) pairs to set response header in the client response. |
| `add_response_headers` | [Endpoint Block](#endpoint-block), [Proxy Block](#proxy-block), [Backend Block](#backend-block), [Error Handler](#error-handler) | Key/value(s) pairs to add response header to the client response. |
| `remove_response_headers` | [Server Block](#server-block), [Files Block](#files-block), [SPA Block](#spa-block), [API Block](#api-block), [Endpoint Block](#endpoint-block), [Proxy Block](#proxy-block), [Backend Block](#backend-block), [Error Handler](#error-handler) | List of response header to be removed from the client response. |
| `set_response_headers` | [Server Block](#server-block), [Files Block](#files-block), [SPA Block](#spa-block), [API Block](#api-block), [Endpoint Block](#endpoint-block), [Proxy Block](#proxy-block), [Backend Block](#backend-block), [Error Handler](#error-handler) | Key/value(s) pairs to set response header in the client response. |
| `add_response_headers` | [Server Block](#server-block), [Files Block](#files-block), [SPA Block](#spa-block), [API Block](#api-block), [Endpoint Block](#endpoint-block), [Proxy Block](#proxy-block), [Backend Block](#backend-block), [Error Handler](#error-handler) | Key/value(s) pairs to add response header to the client response. |

All `*_response_headers` are executed from: `endpoint`, `proxy`, `backend` and `error_handler`.
All `*_response_headers` are executed from: `server`, `files`, `spa`, `api`, `endpoint`, `proxy`, `backend` and `error_handler`.

#### Query Parameter

Expand Down
47 changes: 29 additions & 18 deletions eval/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,18 +108,11 @@ func ApplyRequestContext(ctx context.Context, body hcl.Body, req *http.Request)
httpCtx = c.HCLContext()
}

content, _, diags := body.PartialContent(meta.AttributesSchema)
if diags.HasErrors() {
return diags
}

headerCtx := req.Header

// map to name
// TODO: sorted data structure on load
attrs := make(map[string]*hcl.Attribute)
for _, attr := range content.Attributes {
attrs[attr.Name] = attr
attrs, err := getAllAttributes(body)
if err != nil {
return err
}

if err := evalURLPath(req, attrs, httpCtx); err != nil {
Expand Down Expand Up @@ -297,11 +290,35 @@ func ApplyResponseContext(ctx context.Context, body hcl.Body, beresp *http.Respo
return nil
}

return ApplyResponseHeaderOps(ctx, body, beresp.Header)
}

func ApplyResponseHeaderOps(ctx context.Context, body hcl.Body, headers ...http.Header) error {
var httpCtx *hcl.EvalContext
if c, ok := ctx.Value(ContextType).(*Context); ok {
httpCtx = c.eval
}
content, _, _ := body.PartialContent(meta.AttributesSchema)

attrs, err := getAllAttributes(body)
if err != nil {
return err
}

// sort and apply header values in hierarchical and logical order: delete, set, add
h := []string{attrDelResHeaders, attrSetResHeaders, attrAddResHeaders}
err = applyHeaderOps(attrs, h, httpCtx, headers...)
if err != nil {
return errors.Evaluation.With(err)
}

return nil
}

func getAllAttributes(body hcl.Body) (map[string]*hcl.Attribute, error) {
content, _, diags := body.PartialContent(meta.AttributesSchema)
if diags.HasErrors() {
return nil, diags
}

// map to name
// TODO: sorted data structure on load
Expand All @@ -311,13 +328,7 @@ func ApplyResponseContext(ctx context.Context, body hcl.Body, beresp *http.Respo
attrs[attr.Name] = attr
}

// sort and apply header values in hierarchical and logical order: delete, set, add
headers := []string{attrDelResHeaders, attrSetResHeaders, attrAddResHeaders}
err := applyHeaderOps(attrs, headers, httpCtx, beresp.Header)
if err != nil {
return errors.Evaluation.With(err)
}
return nil
return attrs, nil
}

func applyHeaderOps(attrs map[string]*hcl.Attribute, names []string, httpCtx *hcl.EvalContext, headers ...http.Header) error {
Expand Down
13 changes: 10 additions & 3 deletions handler/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/avenga/couper/errors"
"github.com/avenga/couper/eval"
"github.com/avenga/couper/handler/producer"
"github.com/avenga/couper/server/writer"
)

var _ http.Handler = &Endpoint{}
Expand All @@ -24,6 +25,7 @@ var _ EndpointLimit = &Endpoint{}
type Endpoint struct {
log *logrus.Entry
logHandlerKind string
modifier []hcl.Body
opts *EndpointOptions
}

Expand All @@ -46,11 +48,12 @@ type EndpointLimit interface {
RequestLimit() int64
}

func NewEndpoint(opts *EndpointOptions, log *logrus.Entry) *Endpoint {
func NewEndpoint(opts *EndpointOptions, log *logrus.Entry, modifier []hcl.Body) *Endpoint {
opts.ReqBufferOpts |= eval.MustBuffer(opts.Context) // TODO: proper configuration on all hcl levels
return &Endpoint{
log: log.WithField("handler", opts.LogHandlerKind),
opts: opts,
log: log.WithField("handler", opts.LogHandlerKind),
modifier: modifier,
opts: opts,
}
}

Expand Down Expand Up @@ -175,6 +178,10 @@ func (e *Endpoint) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
default:
}

if r, ok := rw.(*writer.Response); ok {
r.AddModifier(evalContext, e.modifier)
}

if err = clientres.Write(rw); err != nil {
log.Errorf("endpoint write: %v", err)
}
Expand Down
6 changes: 3 additions & 3 deletions handler/endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func TestEndpoint_RoundTrip_Eval(t *testing.T) {
&producer.Proxy{Name: "default", RoundTrip: backend},
},
Requests: make(producer.Requests, 0),
}, logger)
}, logger, nil)

req := httptest.NewRequest(tt.method, "http://couper.io", tt.body)
if tt.body != nil {
Expand Down Expand Up @@ -214,7 +214,7 @@ func TestEndpoint_RoundTripContext_Variables_json_body(t *testing.T) {
&producer.Proxy{Name: "default", RoundTrip: backend},
},
Requests: make(producer.Requests, 0),
}, logger)
}, logger, nil)

var body io.Reader
if tt.body != "" {
Expand Down Expand Up @@ -326,7 +326,7 @@ func TestEndpoint_RoundTripContext_Null_Eval(t *testing.T) {
&producer.Proxy{Name: "default", RoundTrip: backend},
},
Requests: make(producer.Requests, 0),
}, logger)
}, logger, nil)

req := httptest.NewRequest(http.MethodGet, "http://localhost/", bytes.NewReader(clientPayload))
if tc.ct != "" {
Expand Down
Loading

0 comments on commit b26ccb1

Please sign in to comment.