Skip to content

Commit

Permalink
Move middleware to interfaces.
Browse files Browse the repository at this point in the history
This makes separating middlewares from the core api easier.
As an example, the authorization middleware is moved to
it's own package.

Initialize all static middlewares when the server is created, reducing
allocations every time a route is wrapper with the middlewares.

Signed-off-by: David Calavera <david.calavera@gmail.com>
  • Loading branch information
calavera committed Apr 11, 2016
1 parent e626011 commit 8d34676
Show file tree
Hide file tree
Showing 12 changed files with 217 additions and 154 deletions.
20 changes: 2 additions & 18 deletions api/server/middleware.go
Expand Up @@ -2,10 +2,8 @@ package server

import (
"github.com/Sirupsen/logrus"
"github.com/docker/docker/api"
"github.com/docker/docker/api/server/httputils"
"github.com/docker/docker/api/server/middleware"
"github.com/docker/docker/pkg/authorization"
)

// handleWithGlobalMiddlwares wraps the handler function for a request with
Expand All @@ -14,27 +12,13 @@ import (
func (s *Server) handleWithGlobalMiddlewares(handler httputils.APIFunc) httputils.APIFunc {
next := handler

handleVersion := middleware.NewVersionMiddleware(s.cfg.Version, api.DefaultVersion, api.MinVersion)
next = handleVersion(next)

if s.cfg.EnableCors {
handleCORS := middleware.NewCORSMiddleware(s.cfg.CorsHeaders)
next = handleCORS(next)
for _, m := range s.middlewares {
next = m.WrapHandler(next)
}

handleUserAgent := middleware.NewUserAgentMiddleware(s.cfg.Version)
next = handleUserAgent(next)

// Only want this on debug level
if s.cfg.Logging && logrus.GetLevel() == logrus.DebugLevel {
next = middleware.DebugRequestMiddleware(next)
}

if len(s.cfg.AuthorizationPluginNames) > 0 {
s.authZPlugins = authorization.NewPlugins(s.cfg.AuthorizationPluginNames)
handleAuthorization := middleware.NewAuthorizationMiddleware(s.authZPlugins)
next = handleAuthorization(next)
}

return next
}
50 changes: 0 additions & 50 deletions api/server/middleware/authorization.go

This file was deleted.

44 changes: 24 additions & 20 deletions api/server/middleware/cors.go
Expand Up @@ -4,30 +4,34 @@ import (
"net/http"

"github.com/Sirupsen/logrus"
"github.com/docker/docker/api/server/httputils"
"golang.org/x/net/context"
)

// NewCORSMiddleware creates a new CORS middleware.
func NewCORSMiddleware(defaultHeaders string) Middleware {
return func(handler httputils.APIFunc) httputils.APIFunc {
return func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
// If "api-cors-header" is not given, but "api-enable-cors" is true, we set cors to "*"
// otherwise, all head values will be passed to HTTP handler
corsHeaders := defaultHeaders
if corsHeaders == "" {
corsHeaders = "*"
}
// CORSMiddleware injects CORS headers to each request
// when it's configured.
type CORSMiddleware struct {
defaultHeaders string
}

writeCorsHeaders(w, r, corsHeaders)
return handler(ctx, w, r, vars)
}
}
// NewCORSMiddleware creates a new CORSMiddleware with default headers.
func NewCORSMiddleware(d string) CORSMiddleware {
return CORSMiddleware{defaultHeaders: d}
}

func writeCorsHeaders(w http.ResponseWriter, r *http.Request, corsHeaders string) {
logrus.Debugf("CORS header is enabled and set to: %s", corsHeaders)
w.Header().Add("Access-Control-Allow-Origin", corsHeaders)
w.Header().Add("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, X-Registry-Auth")
w.Header().Add("Access-Control-Allow-Methods", "HEAD, GET, POST, DELETE, PUT, OPTIONS")
// WrapHandler returns a new handler function wrapping the previous one in the request chain.
func (c CORSMiddleware) WrapHandler(handler func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error) func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
return func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
// If "api-cors-header" is not given, but "api-enable-cors" is true, we set cors to "*"
// otherwise, all head values will be passed to HTTP handler
corsHeaders := c.defaultHeaders
if corsHeaders == "" {
corsHeaders = "*"
}

logrus.Debugf("CORS header is enabled and set to: %s", corsHeaders)
w.Header().Add("Access-Control-Allow-Origin", corsHeaders)
w.Header().Add("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, X-Registry-Auth")
w.Header().Add("Access-Control-Allow-Methods", "HEAD, GET, POST, DELETE, PUT, OPTIONS")
return handler(ctx, w, r, vars)
}
}
2 changes: 1 addition & 1 deletion api/server/middleware/debug.go
Expand Up @@ -13,7 +13,7 @@ import (
)

// DebugRequestMiddleware dumps the request to logger
func DebugRequestMiddleware(handler httputils.APIFunc) httputils.APIFunc {
func DebugRequestMiddleware(handler func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error) func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
return func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
logrus.Debugf("Calling %s %s", r.Method, r.RequestURI)

Expand Down
14 changes: 10 additions & 4 deletions api/server/middleware/middleware.go
@@ -1,7 +1,13 @@
package middleware

import "github.com/docker/docker/api/server/httputils"
import (
"net/http"

// Middleware is an adapter to allow the use of ordinary functions as Docker API filters.
// Any function that has the appropriate signature can be registered as a middleware.
type Middleware func(handler httputils.APIFunc) httputils.APIFunc
"golang.org/x/net/context"
)

// Middleware is an interface to allow the use of ordinary functions as Docker API filters.
// Any struct that has the appropriate signature can be registered as a middleware.
type Middleware interface {
WrapHandler(func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error) func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error
}
52 changes: 31 additions & 21 deletions api/server/middleware/user_agent.go
Expand Up @@ -10,28 +10,38 @@ import (
"golang.org/x/net/context"
)

// NewUserAgentMiddleware creates a new UserAgent middleware.
func NewUserAgentMiddleware(versionCheck string) Middleware {
serverVersion := version.Version(versionCheck)

return func(handler httputils.APIFunc) httputils.APIFunc {
return func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
ctx = context.WithValue(ctx, httputils.UAStringKey, r.Header.Get("User-Agent"))

if strings.Contains(r.Header.Get("User-Agent"), "Docker-Client/") {
userAgent := strings.Split(r.Header.Get("User-Agent"), "/")

// v1.20 onwards includes the GOOS of the client after the version
// such as Docker/1.7.0 (linux)
if len(userAgent) == 2 && strings.Contains(userAgent[1], " ") {
userAgent[1] = strings.Split(userAgent[1], " ")[0]
}

if len(userAgent) == 2 && !serverVersion.Equal(version.Version(userAgent[1])) {
logrus.Debugf("Client and server don't have the same version (client: %s, server: %s)", userAgent[1], serverVersion)
}
// UserAgentMiddleware is a middleware that
// validates the client user-agent.
type UserAgentMiddleware struct {
serverVersion version.Version
}

// NewUserAgentMiddleware creates a new UserAgentMiddleware
// with the server version.
func NewUserAgentMiddleware(s version.Version) UserAgentMiddleware {
return UserAgentMiddleware{
serverVersion: s,
}
}

// WrapHandler returns a new handler function wrapping the previous one in the request chain.
func (u UserAgentMiddleware) WrapHandler(handler func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error) func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
return func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
ctx = context.WithValue(ctx, httputils.UAStringKey, r.Header.Get("User-Agent"))

if strings.Contains(r.Header.Get("User-Agent"), "Docker-Client/") {
userAgent := strings.Split(r.Header.Get("User-Agent"), "/")

// v1.20 onwards includes the GOOS of the client after the version
// such as Docker/1.7.0 (linux)
if len(userAgent) == 2 && strings.Contains(userAgent[1], " ") {
userAgent[1] = strings.Split(userAgent[1], " ")[0]
}

if len(userAgent) == 2 && !u.serverVersion.Equal(version.Version(userAgent[1])) {
logrus.Debugf("Client and server don't have the same version (client: %s, server: %s)", userAgent[1], u.serverVersion)
}
return handler(ctx, w, r, vars)
}
return handler(ctx, w, r, vars)
}
}
60 changes: 37 additions & 23 deletions api/server/middleware/version.go
Expand Up @@ -5,7 +5,6 @@ import (
"net/http"
"runtime"

"github.com/docker/docker/api/server/httputils"
"github.com/docker/docker/pkg/version"
"golang.org/x/net/context"
)
Expand All @@ -18,28 +17,43 @@ func (badRequestError) HTTPErrorStatusCode() int {
return http.StatusBadRequest
}

// NewVersionMiddleware creates a new Version middleware.
func NewVersionMiddleware(versionCheck string, defaultVersion, minVersion version.Version) Middleware {
serverVersion := version.Version(versionCheck)

return func(handler httputils.APIFunc) httputils.APIFunc {
return func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
apiVersion := version.Version(vars["version"])
if apiVersion == "" {
apiVersion = defaultVersion
}

if apiVersion.GreaterThan(defaultVersion) {
return badRequestError{fmt.Errorf("client is newer than server (client API version: %s, server API version: %s)", apiVersion, defaultVersion)}
}
if apiVersion.LessThan(minVersion) {
return badRequestError{fmt.Errorf("client version %s is too old. Minimum supported API version is %s, please upgrade your client to a newer version", apiVersion, minVersion)}
}

header := fmt.Sprintf("Docker/%s (%s)", serverVersion, runtime.GOOS)
w.Header().Set("Server", header)
ctx = context.WithValue(ctx, httputils.APIVersionKey, apiVersion)
return handler(ctx, w, r, vars)
// VersionMiddleware is a middleware that
// validates the client and server versions.
type VersionMiddleware struct {
serverVersion version.Version
defaultVersion version.Version
minVersion version.Version
}

// NewVersionMiddleware creates a new VersionMiddleware
// with the default versions.
func NewVersionMiddleware(s, d, m version.Version) VersionMiddleware {
return VersionMiddleware{
serverVersion: s,
defaultVersion: d,
minVersion: m,
}
}

// WrapHandler returns a new handler function wrapping the previous one in the request chain.
func (v VersionMiddleware) WrapHandler(handler func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error) func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
return func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
apiVersion := version.Version(vars["version"])
if apiVersion == "" {
apiVersion = v.defaultVersion
}

if apiVersion.GreaterThan(v.defaultVersion) {
return badRequestError{fmt.Errorf("client is newer than server (client API version: %s, server API version: %s)", apiVersion, v.defaultVersion)}
}
if apiVersion.LessThan(v.minVersion) {
return badRequestError{fmt.Errorf("client version %s is too old. Minimum supported API version is %s, please upgrade your client to a newer version", apiVersion, v.minVersion)}
}

header := fmt.Sprintf("Docker/%s (%s)", v.serverVersion, runtime.GOOS)
w.Header().Set("Server", header)
ctx = context.WithValue(ctx, "api-version", apiVersion)
return handler(ctx, w, r, vars)
}

}
8 changes: 4 additions & 4 deletions api/server/middleware/version_test.go
Expand Up @@ -21,8 +21,8 @@ func TestVersionMiddleware(t *testing.T) {

defaultVersion := version.Version("1.10.0")
minVersion := version.Version("1.2.0")
m := NewVersionMiddleware(defaultVersion.String(), defaultVersion, minVersion)
h := m(handler)
m := NewVersionMiddleware(defaultVersion, defaultVersion, minVersion)
h := m.WrapHandler(handler)

req, _ := http.NewRequest("GET", "/containers/json", nil)
resp := httptest.NewRecorder()
Expand All @@ -42,8 +42,8 @@ func TestVersionMiddlewareWithErrors(t *testing.T) {

defaultVersion := version.Version("1.10.0")
minVersion := version.Version("1.2.0")
m := NewVersionMiddleware(defaultVersion.String(), defaultVersion, minVersion)
h := m(handler)
m := NewVersionMiddleware(defaultVersion, defaultVersion, minVersion)
h := m.WrapHandler(handler)

req, _ := http.NewRequest("GET", "/containers/json", nil)
resp := httptest.NewRecorder()
Expand Down
23 changes: 14 additions & 9 deletions api/server/server.go
Expand Up @@ -8,8 +8,8 @@ import (

"github.com/Sirupsen/logrus"
"github.com/docker/docker/api/server/httputils"
"github.com/docker/docker/api/server/middleware"
"github.com/docker/docker/api/server/router"
"github.com/docker/docker/pkg/authorization"
"github.com/gorilla/mux"
"golang.org/x/net/context"
)
Expand All @@ -20,22 +20,21 @@ const versionMatcher = "/v{version:[0-9.]+}"

// Config provides the configuration for the API server
type Config struct {
Logging bool
EnableCors bool
CorsHeaders string
AuthorizationPluginNames []string
Version string
SocketGroup string
TLSConfig *tls.Config
Logging bool
EnableCors bool
CorsHeaders string
Version string
SocketGroup string
TLSConfig *tls.Config
}

// Server contains instance details for the server
type Server struct {
cfg *Config
servers []*HTTPServer
routers []router.Router
authZPlugins []authorization.Plugin
routerSwapper *routerSwapper
middlewares []middleware.Middleware
}

// New returns a new instance of the server based on the specified configuration.
Expand All @@ -46,6 +45,12 @@ func New(cfg *Config) *Server {
}
}

// UseMiddleware appends a new middleware to the request chain.
// This needs to be called before the API routes are configured.
func (s *Server) UseMiddleware(m middleware.Middleware) {
s.middlewares = append(s.middlewares, m)
}

// Accept sets a listener the server accepts connections into.
func (s *Server) Accept(addr string, listeners ...net.Listener) {
for _, listener := range listeners {
Expand Down

0 comments on commit 8d34676

Please sign in to comment.