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

Global response headers #248

Merged
merged 12 commits into from
Jun 2, 2021
Merged
Show file tree
Hide file tree
Changes from 4 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
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