diff options
Diffstat (limited to 'http/middleware')
-rw-r--r-- | http/middleware/basic_auth.go | 72 | ||||
-rw-r--r-- | http/middleware/context_keys.go | 46 | ||||
-rw-r--r-- | http/middleware/fever.go | 57 | ||||
-rw-r--r-- | http/middleware/middleware.go | 36 | ||||
-rw-r--r-- | http/middleware/session.go | 84 | ||||
-rw-r--r-- | http/middleware/user_session.go | 78 |
6 files changed, 373 insertions, 0 deletions
diff --git a/http/middleware/basic_auth.go b/http/middleware/basic_auth.go new file mode 100644 index 0000000..35a9f81 --- /dev/null +++ b/http/middleware/basic_auth.go @@ -0,0 +1,72 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package middleware + +import ( + "context" + "net/http" + + "github.com/miniflux/miniflux/logger" + "github.com/miniflux/miniflux/storage" +) + +// BasicAuthMiddleware is the middleware for HTTP Basic authentication. +type BasicAuthMiddleware struct { + store *storage.Storage +} + +// Handler executes the middleware. +func (b *BasicAuthMiddleware) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) + errorResponse := `{"error_message": "Not Authorized"}` + + username, password, authOK := r.BasicAuth() + if !authOK { + logger.Debug("[Middleware:BasicAuth] No authentication headers sent") + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(errorResponse)) + return + } + + if err := b.store.CheckPassword(username, password); err != nil { + logger.Info("[Middleware:BasicAuth] Invalid username or password: %s", username) + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(errorResponse)) + return + } + + user, err := b.store.UserByUsername(username) + if err != nil { + logger.Error("[Middleware:BasicAuth] %v", err) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(errorResponse)) + return + } + + if user == nil { + logger.Info("[Middleware:BasicAuth] User not found: %s", username) + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(errorResponse)) + return + } + + logger.Info("[Middleware:BasicAuth] User authenticated: %s", username) + b.store.SetLastLogin(user.ID) + + ctx := r.Context() + ctx = context.WithValue(ctx, UserIDContextKey, user.ID) + ctx = context.WithValue(ctx, UserTimezoneContextKey, user.Timezone) + ctx = context.WithValue(ctx, IsAdminUserContextKey, user.IsAdmin) + ctx = context.WithValue(ctx, IsAuthenticatedContextKey, true) + + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +// NewBasicAuthMiddleware returns a new BasicAuthMiddleware. +func NewBasicAuthMiddleware(s *storage.Storage) *BasicAuthMiddleware { + return &BasicAuthMiddleware{store: s} +} diff --git a/http/middleware/context_keys.go b/http/middleware/context_keys.go new file mode 100644 index 0000000..31ad286 --- /dev/null +++ b/http/middleware/context_keys.go @@ -0,0 +1,46 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package middleware + +// ContextKey represents a context key. +type ContextKey struct { + name string +} + +func (c ContextKey) String() string { + return c.name +} + +var ( + // UserIDContextKey is the context key used to store the user ID. + UserIDContextKey = &ContextKey{"UserID"} + + // UserTimezoneContextKey is the context key used to store the user timezone. + UserTimezoneContextKey = &ContextKey{"UserTimezone"} + + // IsAdminUserContextKey is the context key used to store the user role. + IsAdminUserContextKey = &ContextKey{"IsAdminUser"} + + // IsAuthenticatedContextKey is the context key used to store the authentication flag. + IsAuthenticatedContextKey = &ContextKey{"IsAuthenticated"} + + // UserSessionTokenContextKey is the context key used to store the user session ID. + UserSessionTokenContextKey = &ContextKey{"UserSessionToken"} + + // SessionIDContextKey is the context key used to store the session ID. + SessionIDContextKey = &ContextKey{"SessionID"} + + // CSRFContextKey is the context key used to store CSRF token. + CSRFContextKey = &ContextKey{"CSRF"} + + // OAuth2StateContextKey is the context key used to store OAuth2 state. + OAuth2StateContextKey = &ContextKey{"OAuth2State"} + + // FlashMessageContextKey is the context key used to store a flash message. + FlashMessageContextKey = &ContextKey{"FlashMessage"} + + // FlashErrorMessageContextKey is the context key used to store a flash error message. + FlashErrorMessageContextKey = &ContextKey{"FlashErrorMessage"} +) diff --git a/http/middleware/fever.go b/http/middleware/fever.go new file mode 100644 index 0000000..54eb0ca --- /dev/null +++ b/http/middleware/fever.go @@ -0,0 +1,57 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package middleware + +import ( + "context" + "net/http" + + "github.com/miniflux/miniflux/logger" + "github.com/miniflux/miniflux/storage" +) + +// FeverMiddleware is the middleware that handles Fever API. +type FeverMiddleware struct { + store *storage.Storage +} + +// Handler executes the middleware. +func (f *FeverMiddleware) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + logger.Debug("[Middleware:Fever]") + + apiKey := r.FormValue("api_key") + user, err := f.store.UserByFeverToken(apiKey) + if err != nil { + logger.Error("[Fever] %v", err) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"api_version": 3, "auth": 0}`)) + return + } + + if user == nil { + logger.Info("[Middleware:Fever] Fever authentication failure") + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"api_version": 3, "auth": 0}`)) + return + } + + logger.Info("[Middleware:Fever] User #%d is authenticated", user.ID) + f.store.SetLastLogin(user.ID) + + ctx := r.Context() + ctx = context.WithValue(ctx, UserIDContextKey, user.ID) + ctx = context.WithValue(ctx, UserTimezoneContextKey, user.Timezone) + ctx = context.WithValue(ctx, IsAdminUserContextKey, user.IsAdmin) + ctx = context.WithValue(ctx, IsAuthenticatedContextKey, true) + + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +// NewFeverMiddleware returns a new FeverMiddleware. +func NewFeverMiddleware(s *storage.Storage) *FeverMiddleware { + return &FeverMiddleware{store: s} +} diff --git a/http/middleware/middleware.go b/http/middleware/middleware.go new file mode 100644 index 0000000..9853bc3 --- /dev/null +++ b/http/middleware/middleware.go @@ -0,0 +1,36 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package middleware + +import ( + "net/http" +) + +// Middleware represents a HTTP middleware. +type Middleware func(http.Handler) http.Handler + +// Chain handles a list of middlewares. +type Chain struct { + middlewares []Middleware +} + +// Wrap adds a HTTP handler into the chain. +func (m *Chain) Wrap(h http.Handler) http.Handler { + for i := range m.middlewares { + h = m.middlewares[len(m.middlewares)-1-i](h) + } + + return h +} + +// WrapFunc adds a HTTP handler function into the chain. +func (m *Chain) WrapFunc(fn http.HandlerFunc) http.Handler { + return m.Wrap(fn) +} + +// NewChain returns a new Chain. +func NewChain(middlewares ...Middleware) *Chain { + return &Chain{append(([]Middleware)(nil), middlewares...)} +} diff --git a/http/middleware/session.go b/http/middleware/session.go new file mode 100644 index 0000000..a0e9fbd --- /dev/null +++ b/http/middleware/session.go @@ -0,0 +1,84 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package middleware + +import ( + "context" + "net/http" + + "github.com/miniflux/miniflux/config" + "github.com/miniflux/miniflux/http/cookie" + "github.com/miniflux/miniflux/logger" + "github.com/miniflux/miniflux/model" + "github.com/miniflux/miniflux/storage" +) + +// SessionMiddleware represents a session middleware. +type SessionMiddleware struct { + cfg *config.Config + store *storage.Storage +} + +// Handler execute the middleware. +func (s *SessionMiddleware) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var err error + session := s.getSessionValueFromCookie(r) + + if session == nil { + logger.Debug("[Middleware:Session] Session not found") + session, err = s.store.CreateSession() + if err != nil { + logger.Error("[Middleware:Session] %v", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + http.SetCookie(w, cookie.New(cookie.CookieSessionID, session.ID, s.cfg.IsHTTPS)) + } else { + logger.Debug("[Middleware:Session] %s", session) + } + + if r.Method == "POST" { + formValue := r.FormValue("csrf") + headerValue := r.Header.Get("X-Csrf-Token") + + if session.Data.CSRF != formValue && session.Data.CSRF != headerValue { + logger.Error(`[Middleware:Session] Invalid or missing CSRF token: Form="%s", Header="%s"`, formValue, headerValue) + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("Invalid or missing CSRF session!")) + return + } + } + + ctx := r.Context() + ctx = context.WithValue(ctx, SessionIDContextKey, session.ID) + ctx = context.WithValue(ctx, CSRFContextKey, session.Data.CSRF) + ctx = context.WithValue(ctx, OAuth2StateContextKey, session.Data.OAuth2State) + ctx = context.WithValue(ctx, FlashMessageContextKey, session.Data.FlashMessage) + ctx = context.WithValue(ctx, FlashErrorMessageContextKey, session.Data.FlashErrorMessage) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func (s *SessionMiddleware) getSessionValueFromCookie(r *http.Request) *model.Session { + sessionCookie, err := r.Cookie(cookie.CookieSessionID) + if err == http.ErrNoCookie { + return nil + } + + session, err := s.store.Session(sessionCookie.Value) + if err != nil { + logger.Error("[Middleware:Session] %v", err) + return nil + } + + return session +} + +// NewSessionMiddleware returns a new SessionMiddleware. +func NewSessionMiddleware(cfg *config.Config, store *storage.Storage) *SessionMiddleware { + return &SessionMiddleware{cfg, store} +} diff --git a/http/middleware/user_session.go b/http/middleware/user_session.go new file mode 100644 index 0000000..d67445f --- /dev/null +++ b/http/middleware/user_session.go @@ -0,0 +1,78 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package middleware + +import ( + "context" + "net/http" + + "github.com/miniflux/miniflux/http/cookie" + "github.com/miniflux/miniflux/http/route" + "github.com/miniflux/miniflux/logger" + "github.com/miniflux/miniflux/model" + "github.com/miniflux/miniflux/storage" + + "github.com/gorilla/mux" +) + +// UserSessionMiddleware represents a user session middleware. +type UserSessionMiddleware struct { + store *storage.Storage + router *mux.Router +} + +// Handler execute the middleware. +func (s *UserSessionMiddleware) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + session := s.getSessionFromCookie(r) + + if session == nil { + logger.Debug("[Middleware:UserSession] Session not found") + if s.isPublicRoute(r) { + next.ServeHTTP(w, r) + } else { + http.Redirect(w, r, route.Path(s.router, "login"), http.StatusFound) + } + } else { + logger.Debug("[Middleware:UserSession] %s", session) + ctx := r.Context() + ctx = context.WithValue(ctx, UserIDContextKey, session.UserID) + ctx = context.WithValue(ctx, IsAuthenticatedContextKey, true) + ctx = context.WithValue(ctx, UserSessionTokenContextKey, session.Token) + + next.ServeHTTP(w, r.WithContext(ctx)) + } + }) +} + +func (s *UserSessionMiddleware) isPublicRoute(r *http.Request) bool { + route := mux.CurrentRoute(r) + switch route.GetName() { + case "login", "checkLogin", "stylesheet", "javascript", "oauth2Redirect", "oauth2Callback", "appIcon", "favicon", "webManifest": + return true + default: + return false + } +} + +func (s *UserSessionMiddleware) getSessionFromCookie(r *http.Request) *model.UserSession { + sessionCookie, err := r.Cookie(cookie.CookieUserSessionID) + if err == http.ErrNoCookie { + return nil + } + + session, err := s.store.UserSessionByToken(sessionCookie.Value) + if err != nil { + logger.Error("[Middleware:UserSession] %v", err) + return nil + } + + return session +} + +// NewUserSessionMiddleware returns a new UserSessionMiddleware. +func NewUserSessionMiddleware(s *storage.Storage, r *mux.Router) *UserSessionMiddleware { + return &UserSessionMiddleware{store: s, router: r} +} |