From 1ba44a832f6aae811dfc6235287dd5b99e8aa94c Mon Sep 17 00:00:00 2001 From: David Calavera Date: Wed, 24 Feb 2016 13:17:43 -0500 Subject: [PATCH] Make server middleware standalone functions. Removing direct dependencies from the server configuration. Signed-off-by: David Calavera --- api/server/middleware.go | 186 ++---------------- api/server/middleware/authorization.go | 42 ++++ api/server/middleware/cors.go | 33 ++++ api/server/middleware/debug.go | 56 ++++++ api/server/middleware/middleware.go | 7 + api/server/middleware/user_agent.go | 35 ++++ api/server/middleware/version.go | 38 ++++ .../version_test.go} | 21 +- api/server/server.go | 7 - 9 files changed, 241 insertions(+), 184 deletions(-) create mode 100644 api/server/middleware/authorization.go create mode 100644 api/server/middleware/cors.go create mode 100644 api/server/middleware/debug.go create mode 100644 api/server/middleware/middleware.go create mode 100644 api/server/middleware/user_agent.go create mode 100644 api/server/middleware/version.go rename api/server/{middleware_test.go => middleware/version_test.go} (67%) diff --git a/api/server/middleware.go b/api/server/middleware.go index 5326904de4..2622bf1bbe 100644 --- a/api/server/middleware.go +++ b/api/server/middleware.go @@ -1,195 +1,41 @@ package server import ( - "bufio" - "encoding/json" - "io" - "net/http" - "runtime" - "strings" - "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/dockerversion" - "github.com/docker/docker/errors" "github.com/docker/docker/pkg/authorization" - "github.com/docker/docker/pkg/ioutils" - "github.com/docker/docker/pkg/version" - "golang.org/x/net/context" ) -// middleware is an adapter to allow the use of ordinary functions as Docker API filters. -// Any function that has the appropriate signature can be register as a middleware. -type middleware func(handler httputils.APIFunc) httputils.APIFunc - -// debugRequestMiddleware dumps the request to logger -func debugRequestMiddleware(handler httputils.APIFunc) httputils.APIFunc { - return func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { - logrus.Debugf("%s %s", r.Method, r.RequestURI) - - if r.Method != "POST" { - return handler(ctx, w, r, vars) - } - if err := httputils.CheckForJSON(r); err != nil { - return handler(ctx, w, r, vars) - } - maxBodySize := 4096 // 4KB - if r.ContentLength > int64(maxBodySize) { - return handler(ctx, w, r, vars) - } - - body := r.Body - bufReader := bufio.NewReaderSize(body, maxBodySize) - r.Body = ioutils.NewReadCloserWrapper(bufReader, func() error { return body.Close() }) - - b, err := bufReader.Peek(maxBodySize) - if err != io.EOF { - // either there was an error reading, or the buffer is full (in which case the request is too large) - return handler(ctx, w, r, vars) - } - - var postForm map[string]interface{} - if err := json.Unmarshal(b, &postForm); err == nil { - if _, exists := postForm["password"]; exists { - postForm["password"] = "*****" - } - formStr, errMarshal := json.Marshal(postForm) - if errMarshal == nil { - logrus.Debugf("form data: %s", string(formStr)) - } else { - logrus.Debugf("form data: %q", postForm) - } - } - - return handler(ctx, w, r, vars) - } -} - -// authorizationMiddleware perform authorization on the request. -func (s *Server) authorizationMiddleware(handler httputils.APIFunc) httputils.APIFunc { - return func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { - // FIXME: fill when authN gets in - // User and UserAuthNMethod are taken from AuthN plugins - // Currently tracked in https://github.com/docker/docker/pull/13994 - user := "" - userAuthNMethod := "" - authCtx := authorization.NewCtx(s.authZPlugins, user, userAuthNMethod, r.Method, r.RequestURI) - - if err := authCtx.AuthZRequest(w, r); err != nil { - logrus.Errorf("AuthZRequest for %s %s returned error: %s", r.Method, r.RequestURI, err) - return err - } - - rw := authorization.NewResponseModifier(w) - - if err := handler(ctx, rw, r, vars); err != nil { - logrus.Errorf("Handler for %s %s returned error: %s", r.Method, r.RequestURI, err) - return err - } - - if err := authCtx.AuthZResponse(rw, r); err != nil { - logrus.Errorf("AuthZResponse for %s %s returned error: %s", r.Method, r.RequestURI, err) - return err - } - return nil - } -} - -// userAgentMiddleware checks the User-Agent header looking for a valid docker client spec. -func (s *Server) userAgentMiddleware(handler httputils.APIFunc) httputils.APIFunc { - return func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { - if strings.Contains(r.Header.Get("User-Agent"), "Docker-Client/") { - dockerVersion := version.Version(s.cfg.Version) - - 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 && !dockerVersion.Equal(version.Version(userAgent[1])) { - logrus.Debugf("Client and server don't have the same version (client: %s, server: %s)", userAgent[1], dockerVersion) - } - } - return handler(ctx, w, r, vars) - } -} - -// corsMiddleware sets the CORS header expectations in the server. -func (s *Server) corsMiddleware(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 := s.cfg.CorsHeaders - if corsHeaders == "" && s.cfg.EnableCors { - corsHeaders = "*" - } - - if corsHeaders != "" { - writeCorsHeaders(w, r, corsHeaders) - } - return handler(ctx, w, r, vars) - } -} - -// versionMiddleware checks the api version requirements before passing the request to the server handler. -func versionMiddleware(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 = api.DefaultVersion - } - - if apiVersion.GreaterThan(api.DefaultVersion) { - return errors.ErrorCodeNewerClientVersion.WithArgs(apiVersion, api.DefaultVersion) - } - if apiVersion.LessThan(api.MinVersion) { - return errors.ErrorCodeOldClientVersion.WithArgs(apiVersion, api.MinVersion) - } - - w.Header().Set("Server", "Docker/"+dockerversion.Version+" ("+runtime.GOOS+")") - ctx = context.WithValue(ctx, httputils.APIVersionKey, apiVersion) - return handler(ctx, w, r, vars) - } -} - // handleWithGlobalMiddlwares wraps the handler function for a request with // the server's global middlewares. The order of the middlewares is backwards, // meaning that the first in the list will be evaluated last. -// -// Example: handleWithGlobalMiddlewares(s.getContainersName) -// -// s.loggingMiddleware( -// s.userAgentMiddleware( -// s.corsMiddleware( -// versionMiddleware(s.getContainersName) -// ) -// ) -// ) -// ) func (s *Server) handleWithGlobalMiddlewares(handler httputils.APIFunc) httputils.APIFunc { - middlewares := []middleware{ - versionMiddleware, - s.corsMiddleware, - s.userAgentMiddleware, + next := handler + + handleVersion := middleware.NewVersionMiddleware(dockerversion.Version, api.DefaultVersion, api.MinVersion) + next = handleVersion(next) + + if s.cfg.EnableCors { + handleCORS := middleware.NewCORSMiddleware(s.cfg.CorsHeaders) + next = handleCORS(next) } + handleUserAgent := middleware.NewUserAgentMiddleware(s.cfg.Version) + next = handleUserAgent(next) + // Only want this on debug level if s.cfg.Logging && logrus.GetLevel() == logrus.DebugLevel { - middlewares = append(middlewares, debugRequestMiddleware) + next = middleware.DebugRequestMiddleware(next) } if len(s.cfg.AuthorizationPluginNames) > 0 { s.authZPlugins = authorization.NewPlugins(s.cfg.AuthorizationPluginNames) - middlewares = append(middlewares, s.authorizationMiddleware) + handleAuthorization := middleware.NewAuthorizationMiddleware(s.authZPlugins) + next = handleAuthorization(next) } - h := handler - for _, m := range middlewares { - h = m(h) - } - return h + return next } diff --git a/api/server/middleware/authorization.go b/api/server/middleware/authorization.go new file mode 100644 index 0000000000..cbfa99e7b3 --- /dev/null +++ b/api/server/middleware/authorization.go @@ -0,0 +1,42 @@ +package middleware + +import ( + "net/http" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/server/httputils" + "github.com/docker/docker/pkg/authorization" + "golang.org/x/net/context" +) + +// NewAuthorizationMiddleware creates a new Authorization middleware. +func NewAuthorizationMiddleware(plugins []authorization.Plugin) Middleware { + return func(handler httputils.APIFunc) httputils.APIFunc { + return func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + // FIXME: fill when authN gets in + // User and UserAuthNMethod are taken from AuthN plugins + // Currently tracked in https://github.com/docker/docker/pull/13994 + user := "" + userAuthNMethod := "" + authCtx := authorization.NewCtx(plugins, user, userAuthNMethod, r.Method, r.RequestURI) + + if err := authCtx.AuthZRequest(w, r); err != nil { + logrus.Errorf("AuthZRequest for %s %s returned error: %s", r.Method, r.RequestURI, err) + return err + } + + rw := authorization.NewResponseModifier(w) + + if err := handler(ctx, rw, r, vars); err != nil { + logrus.Errorf("Handler for %s %s returned error: %s", r.Method, r.RequestURI, err) + return err + } + + if err := authCtx.AuthZResponse(rw, r); err != nil { + logrus.Errorf("AuthZResponse for %s %s returned error: %s", r.Method, r.RequestURI, err) + return err + } + return nil + } + } +} diff --git a/api/server/middleware/cors.go b/api/server/middleware/cors.go new file mode 100644 index 0000000000..de21897d2c --- /dev/null +++ b/api/server/middleware/cors.go @@ -0,0 +1,33 @@ +package middleware + +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 = "*" + } + + writeCorsHeaders(w, r, corsHeaders) + return handler(ctx, w, r, vars) + } + } +} + +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") +} diff --git a/api/server/middleware/debug.go b/api/server/middleware/debug.go new file mode 100644 index 0000000000..967fe7000f --- /dev/null +++ b/api/server/middleware/debug.go @@ -0,0 +1,56 @@ +package middleware + +import ( + "bufio" + "encoding/json" + "io" + "net/http" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/server/httputils" + "github.com/docker/docker/pkg/ioutils" + "golang.org/x/net/context" +) + +// DebugRequestMiddleware dumps the request to logger +func DebugRequestMiddleware(handler httputils.APIFunc) httputils.APIFunc { + return func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + logrus.Debugf("%s %s", r.Method, r.RequestURI) + + if r.Method != "POST" { + return handler(ctx, w, r, vars) + } + if err := httputils.CheckForJSON(r); err != nil { + return handler(ctx, w, r, vars) + } + maxBodySize := 4096 // 4KB + if r.ContentLength > int64(maxBodySize) { + return handler(ctx, w, r, vars) + } + + body := r.Body + bufReader := bufio.NewReaderSize(body, maxBodySize) + r.Body = ioutils.NewReadCloserWrapper(bufReader, func() error { return body.Close() }) + + b, err := bufReader.Peek(maxBodySize) + if err != io.EOF { + // either there was an error reading, or the buffer is full (in which case the request is too large) + return handler(ctx, w, r, vars) + } + + var postForm map[string]interface{} + if err := json.Unmarshal(b, &postForm); err == nil { + if _, exists := postForm["password"]; exists { + postForm["password"] = "*****" + } + formStr, errMarshal := json.Marshal(postForm) + if errMarshal == nil { + logrus.Debugf("form data: %s", string(formStr)) + } else { + logrus.Debugf("form data: %q", postForm) + } + } + + return handler(ctx, w, r, vars) + } +} diff --git a/api/server/middleware/middleware.go b/api/server/middleware/middleware.go new file mode 100644 index 0000000000..b4b28ec52c --- /dev/null +++ b/api/server/middleware/middleware.go @@ -0,0 +1,7 @@ +package middleware + +import "github.com/docker/docker/api/server/httputils" + +// Middleware is an adapter to allow the use of ordinary functions as Docker API filters. +// Any function that has the appropriate signature can be register as a middleware. +type Middleware func(handler httputils.APIFunc) httputils.APIFunc diff --git a/api/server/middleware/user_agent.go b/api/server/middleware/user_agent.go new file mode 100644 index 0000000000..be4e171cd2 --- /dev/null +++ b/api/server/middleware/user_agent.go @@ -0,0 +1,35 @@ +package middleware + +import ( + "net/http" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/server/httputils" + "github.com/docker/docker/pkg/version" + "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 { + 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) + } + } + return handler(ctx, w, r, vars) + } + } +} diff --git a/api/server/middleware/version.go b/api/server/middleware/version.go new file mode 100644 index 0000000000..72784a5677 --- /dev/null +++ b/api/server/middleware/version.go @@ -0,0 +1,38 @@ +package middleware + +import ( + "fmt" + "net/http" + "runtime" + + "github.com/docker/docker/api/server/httputils" + "github.com/docker/docker/errors" + "github.com/docker/docker/pkg/version" + "golang.org/x/net/context" +) + +// 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 errors.ErrorCodeNewerClientVersion.WithArgs(apiVersion, defaultVersion) + } + if apiVersion.LessThan(minVersion) { + return errors.ErrorCodeOldClientVersion.WithArgs(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) + } + } +} diff --git a/api/server/middleware_test.go b/api/server/middleware/version_test.go similarity index 67% rename from api/server/middleware_test.go rename to api/server/middleware/version_test.go index 4f48b20990..4e3d92141b 100644 --- a/api/server/middleware_test.go +++ b/api/server/middleware/version_test.go @@ -1,13 +1,13 @@ -package server +package middleware import ( "net/http" "net/http/httptest" + "strings" "testing" - "github.com/docker/distribution/registry/api/errcode" "github.com/docker/docker/api/server/httputils" - "github.com/docker/docker/errors" + "github.com/docker/docker/pkg/version" "golang.org/x/net/context" ) @@ -19,7 +19,10 @@ func TestVersionMiddleware(t *testing.T) { return nil } - h := versionMiddleware(handler) + defaultVersion := version.Version("1.10.0") + minVersion := version.Version("1.2.0") + m := NewVersionMiddleware(defaultVersion.String(), defaultVersion, minVersion) + h := m(handler) req, _ := http.NewRequest("GET", "/containers/json", nil) resp := httptest.NewRecorder() @@ -37,7 +40,10 @@ func TestVersionMiddlewareWithErrors(t *testing.T) { return nil } - h := versionMiddleware(handler) + defaultVersion := version.Version("1.10.0") + minVersion := version.Version("1.2.0") + m := NewVersionMiddleware(defaultVersion.String(), defaultVersion, minVersion) + h := m(handler) req, _ := http.NewRequest("GET", "/containers/json", nil) resp := httptest.NewRecorder() @@ -45,13 +51,14 @@ func TestVersionMiddlewareWithErrors(t *testing.T) { vars := map[string]string{"version": "0.1"} err := h(ctx, resp, req, vars) - if derr, ok := err.(errcode.Error); !ok || derr.ErrorCode() != errors.ErrorCodeOldClientVersion { + + if !strings.Contains(err.Error(), "client version 0.1 is too old. Minimum supported API version is 1.2.0") { t.Fatalf("Expected ErrorCodeOldClientVersion, got %v", err) } vars["version"] = "100000" err = h(ctx, resp, req, vars) - if derr, ok := err.(errcode.Error); !ok || derr.ErrorCode() != errors.ErrorCodeNewerClientVersion { + if !strings.Contains(err.Error(), "client is newer than server") { t.Fatalf("Expected ErrorCodeNewerClientVersion, got %v", err) } } diff --git a/api/server/server.go b/api/server/server.go index 3ded607f6f..f65dfccfaa 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -113,13 +113,6 @@ func (s *HTTPServer) Close() error { return s.l.Close() } -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") -} - func (s *Server) makeHTTPHandler(handler httputils.APIFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // log the handler call