internal handlers: support for response modifiers

Co-authored-by: Julien Salleyron <julien@containo.us>
Co-authored-by: Romain <rtribotte@users.noreply.github.com>
Co-authored-by: Jean-Baptiste Doumenjou <jb.doumenjou@gmail.com>
This commit is contained in:
mpl 2020-06-15 12:20:05 +02:00 committed by GitHub
parent b0f7b71453
commit 9f32292473
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 109 additions and 13 deletions

View file

@ -2,6 +2,9 @@
checkNewVersion = false checkNewVersion = false
sendAnonymousUsage = false sendAnonymousUsage = false
[api]
insecure = true
[log] [log]
level = "DEBUG" level = "DEBUG"
@ -24,6 +27,11 @@
rule = "Host(`test2.localhost`)" rule = "Host(`test2.localhost`)"
service = "service1" service = "service1"
[http.routers.router3]
rule = "Host(`internal.localhost`)"
middlewares = ["secure"]
service = "api@internal"
[http.middlewares] [http.middlewares]
[http.middlewares.secure.headers] [http.middlewares.secure.headers]
featurePolicy = "vibrate 'none';" featurePolicy = "vibrate 'none';"

View file

@ -134,6 +134,7 @@ func (s *HeadersSuite) TestSecureHeadersResponses(c *check.C) {
desc string desc string
expected http.Header expected http.Header
reqHost string reqHost string
internalReqHost string
}{ }{
{ {
desc: "Feature-Policy Set", desc: "Feature-Policy Set",
@ -141,6 +142,7 @@ func (s *HeadersSuite) TestSecureHeadersResponses(c *check.C) {
"Feature-Policy": {"vibrate 'none';"}, "Feature-Policy": {"vibrate 'none';"},
}, },
reqHost: "test.localhost", reqHost: "test.localhost",
internalReqHost: "internal.localhost",
}, },
} }
@ -149,7 +151,14 @@ func (s *HeadersSuite) TestSecureHeadersResponses(c *check.C) {
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
req.Host = test.reqHost req.Host = test.reqHost
err = try.Request(req, 500*time.Millisecond, try.HasHeaderStruct(test.expected)) err = try.Request(req, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK), try.HasHeaderStruct(test.expected))
c.Assert(err, checker.IsNil)
req, err = http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/api/rawdata", nil)
c.Assert(err, checker.IsNil)
req.Host = test.internalReqHost
err = try.Request(req, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK), try.HasHeaderStruct(test.expected))
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
} }
} }

View file

@ -54,13 +54,13 @@ func New(ctx context.Context, next http.Handler, cfg dynamic.Headers, name strin
nextHandler := next nextHandler := next
if hasSecureHeaders { if hasSecureHeaders {
logger.Debug("Setting up secureHeaders from %v", cfg) logger.Debugf("Setting up secureHeaders from %v", cfg)
handler = newSecure(next, cfg) handler = newSecure(next, cfg)
nextHandler = handler nextHandler = handler
} }
if hasCustomHeaders || hasCorsHeaders { if hasCustomHeaders || hasCorsHeaders {
logger.Debug("Setting up customHeaders/Cors from %v", cfg) logger.Debugf("Setting up customHeaders/Cors from %v", cfg)
handler = NewHeader(nextHandler, cfg) handler = NewHeader(nextHandler, cfg)
} }

View file

@ -19,6 +19,7 @@ type Builder struct {
} }
// Build Builds the response modifier. // Build Builds the response modifier.
// It returns nil if there is no modifier to apply.
func (f *Builder) Build(ctx context.Context, names []string) func(*http.Response) error { func (f *Builder) Build(ctx context.Context, names []string) func(*http.Response) error {
var modifiers []func(*http.Response) error var modifiers []func(*http.Response) error
@ -60,5 +61,5 @@ func (f *Builder) Build(ctx context.Context, names []string) func(*http.Response
} }
} }
return func(response *http.Response) error { return nil } return nil
} }

View file

@ -184,6 +184,9 @@ func TestBuilderBuild(t *testing.T) {
builder := NewBuilder(rtConf.Middlewares) builder := NewBuilder(rtConf.Middlewares)
rm := builder.Build(context.Background(), test.middlewares) rm := builder.Build(context.Background(), test.middlewares)
if rm == nil {
return
}
resp := test.buildResponse(test.conf) resp := test.buildResponse(test.conf)

View file

@ -8,6 +8,7 @@ import (
"strings" "strings"
"github.com/containous/traefik/v2/pkg/config/runtime" "github.com/containous/traefik/v2/pkg/config/runtime"
"github.com/containous/traefik/v2/pkg/log"
) )
type serviceManager interface { type serviceManager interface {
@ -42,13 +43,87 @@ func NewInternalHandlers(api func(configuration *runtime.Configuration) http.Han
} }
} }
// BuildHTTP builds an HTTP handler. type responseModifier struct {
func (m *InternalHandlers) BuildHTTP(rootCtx context.Context, serviceName string, responseModifier func(*http.Response) error) (http.Handler, error) { r *http.Request
if strings.HasSuffix(serviceName, "@internal") { w http.ResponseWriter
return m.get(serviceName)
headersSent bool // whether headers have already been sent
code int // status code, must default to 200
modifier func(*http.Response) error // can be nil
modified bool // whether modifier has already been called for the current request
modifierErr error // returned by modifier call
} }
return m.serviceManager.BuildHTTP(rootCtx, serviceName, responseModifier) // modifier can be nil.
func newResponseModifier(w http.ResponseWriter, r *http.Request, modifier func(*http.Response) error) *responseModifier {
return &responseModifier{
r: r,
w: w,
modifier: modifier,
code: http.StatusOK,
}
}
func (w *responseModifier) WriteHeader(code int) {
if w.headersSent {
return
}
defer func() {
w.code = code
w.headersSent = true
}()
if w.modifier == nil || w.modified {
w.w.WriteHeader(code)
return
}
resp := http.Response{
Header: w.w.Header(),
Request: w.r,
}
if err := w.modifier(&resp); err != nil {
w.modifierErr = err
// we are propagating when we are called in Write, but we're logging anyway,
// because we could be called from another place which does not take care of
// checking w.modifierErr.
log.Errorf("Error when applying response modifier: %v", err)
w.w.WriteHeader(http.StatusInternalServerError)
return
}
w.modified = true
w.w.WriteHeader(code)
}
func (w *responseModifier) Header() http.Header {
return w.w.Header()
}
func (w *responseModifier) Write(b []byte) (int, error) {
w.WriteHeader(w.code)
if w.modifierErr != nil {
return 0, w.modifierErr
}
return w.w.Write(b)
}
// BuildHTTP builds an HTTP handler.
func (m *InternalHandlers) BuildHTTP(rootCtx context.Context, serviceName string, respModifier func(*http.Response) error) (http.Handler, error) {
if !strings.HasSuffix(serviceName, "@internal") {
return m.serviceManager.BuildHTTP(rootCtx, serviceName, respModifier)
}
internalHandler, err := m.get(serviceName)
if err != nil {
return nil, err
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
internalHandler.ServeHTTP(newResponseModifier(w, r, respModifier), r)
}), nil
} }
func (m *InternalHandlers) get(serviceName string) (http.Handler, error) { func (m *InternalHandlers) get(serviceName string) (http.Handler, error) {