package handlers import ( "encoding/json" "fmt" "io/ioutil" "log" "net/http" "net/url" "path/filepath" "runtime/debug" "strconv" "strings" "git.andreafazzi.eu/andrea/oef/config" "git.andreafazzi.eu/andrea/oef/errors" "git.andreafazzi.eu/andrea/oef/orm" "git.andreafazzi.eu/andrea/oef/reflect" "git.andreafazzi.eu/andrea/oef/renderer" jwtmiddleware "github.com/auth0/go-jwt-middleware" jwt "github.com/dgrijalva/jwt-go" "github.com/gorilla/mux" "github.com/gorilla/sessions" "github.com/dchest/captcha" ) type handlerFuncWithError func(http.ResponseWriter, *http.Request) error type rootMiddleware struct { h *Handlers fn handlerFuncWithError } func newRootMiddleware(h *Handlers, fn func(http.ResponseWriter, *http.Request) error) *rootMiddleware { return &rootMiddleware{h, fn} } // type rootHandler func(http.ResponseWriter, *http.Request) error type Handlers struct { Config *config.ConfigT Database *orm.Database Models []interface{} Renderer map[string]renderer.Renderer Login func(db *orm.Database, store *sessions.CookieStore, signingKey []byte) handlerFuncWithError Logout func(store *sessions.CookieStore) http.Handler Home func() http.Handler GetToken func(db *orm.Database, signingKey []byte) handlerFuncWithError Static func() http.Handler Recover func(next http.Handler) http.Handler CookieStore *sessions.CookieStore JWTSigningKey []byte JWTCookieMiddleware *jwtmiddleware.JWTMiddleware JWTHeaderMiddleware *jwtmiddleware.JWTMiddleware Router *mux.Router permissions map[string]map[string]bool } // Generate CRUD handlers for models func (h *Handlers) generateModelHandlers(r *mux.Router, model interface{}) { // Install standard paths for _, pattern := range DefaultPathPatterns { r.Handle( pattern.Path( reflect.ModelNameLowerPlural(model), ), h.JWTCookieMiddleware.Handler( h.Recover( newRootMiddleware( h, h.modelHandler( reflect.ModelNameLowerPlural(model), pattern, ))))).Methods(pattern.Methods...) } // Install API paths for _, pattern := range h.Config.Handlers.APIPathPatterns { r.Handle(pattern.Path( reflect.ModelNameLowerPlural(model), ), h.JWTHeaderMiddleware.Handler( h.Recover( newRootMiddleware( h, h.modelHandler( reflect.ModelNameLowerPlural(model), pattern, ))))).Methods(pattern.Methods...) } // Set permissions for role, modelPermissions := range h.Config.Handlers.Permissions { for m, perm := range modelPermissions { if m == reflect.ModelName(model) { for _, p := range perm { for _, pattern := range h.Config.Handlers.PathPatterns { if pattern.Permission == p { if h.permissions[role] == nil { h.permissions[role] = make(map[string]bool) } h.permissions[role][pattern.Path(reflect.ModelNameLowerPlural(model))] = true } } for _, pattern := range DefaultAPIPathPatterns { if pattern.Permission == p { if h.permissions[role] == nil { h.permissions[role] = make(map[string]bool) } h.permissions[role][pattern.Path(reflect.ModelNameLowerPlural(model))] = true } } } } } } } func NewHandlers(config *config.ConfigT, renderer map[string]renderer.Renderer, db *orm.Database, models []interface{}) *Handlers { handlers := new(Handlers) if config.JWTExpireTime == 0 { config.JWTExpireTime = DefaultJWTExpireTime } handlers.Config = config handlers.Renderer = renderer handlers.Database = db handlers.CookieStore = sessions.NewCookieStore([]byte(config.Keys.CookieStoreKey)) handlers.Login = handlers.DefaultLoginHandler handlers.Logout = DefaultLogoutHandler handlers.Recover = DefaultRecoverHandler handlers.Home = DefaultHomeHandler handlers.GetToken = DefaultGetTokenHandler handlers.JWTCookieMiddleware = jwtmiddleware.New(jwtmiddleware.Options{ ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) { return []byte(config.Keys.JWTSigningKey), nil }, SigningMethod: jwt.SigningMethodHS256, Extractor: handlers.cookieExtractor, ErrorHandler: handlers.onError, }) handlers.JWTHeaderMiddleware = jwtmiddleware.New(jwtmiddleware.Options{ ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) { return []byte(config.Keys.JWTSigningKey), nil }, SigningMethod: jwt.SigningMethodHS256, }) handlers.permissions = make(map[string]map[string]bool) r := mux.NewRouter() // Authentication r.Handle("/login", newRootMiddleware(handlers, handlers.Login(handlers.Database, handlers.CookieStore, []byte(config.Keys.JWTSigningKey)))) r.Handle("/logout", handlers.Logout(handlers.CookieStore)) // School subscription r.Handle("/subscribe", newRootMiddleware(handlers, handlers.Login(handlers.Database, handlers.CookieStore, []byte(config.Keys.JWTSigningKey)))) // Captcha r.Handle("/captcha/{img}", handlers.JWTCookieMiddleware.Handler(handlers.Recover(captcha.Server(captcha.StdWidth, captcha.StdHeight)))) // Home r.Handle("/", handlers.JWTCookieMiddleware.Handler(handlers.Recover(handlers.Home()))) // Generate CRUD handlers for _, model := range models { handlers.generateModelHandlers(r, model) } // Token handling r.Handle("/get_token", newRootMiddleware(handlers, handlers.GetToken(handlers.Database, []byte(config.Keys.JWTSigningKey)))) // Static file server r.PathPrefix("/").Handler(http.FileServer(http.Dir("./dist/"))) handlers.Router = r return handlers } func (h *Handlers) NewReadAllRequest(model interface{}, path string, format string) (*http.Request, error) { return http.NewRequest("GET", h.Config.ReadAllPath(model, path, format), nil) } func (h *Handlers) NewReadRequest(model interface{}, path string, format string) (*http.Request, error) { return http.NewRequest("GET", h.Config.ReadPath(model, path, format), nil) } func (h *Handlers) NewUpdateRequest(model interface{}, path string, format string, method string, form url.Values) (*http.Request, error) { var ( request *http.Request err error ) switch method { case "GET": request, err = http.NewRequest("GET", h.Config.UpdatePath(model, path, format), nil) case "POST": request, err = http.NewRequest("POST", h.Config.UpdatePath(model, path, format), strings.NewReader(form.Encode())) } return request, err } func (h *Handlers) NewCreateRequest(model interface{}, path string, format string, method string, form url.Values) (*http.Request, error) { var ( request *http.Request err error ) switch method { case "GET": request, err = http.NewRequest("GET", h.Config.CreatePath(model, path, format), nil) case "POST": request, err = http.NewRequest("POST", h.Config.CreatePath(model, path, format), strings.NewReader(form.Encode())) } return request, err } func (h *Handlers) NewDeleteRequest(model interface{}, path string, format string) (*http.Request, error) { return http.NewRequest("DELETE", h.Config.DeletePath(model, path, format), nil) } func (h *Handlers) onError(w http.ResponseWriter, r *http.Request, err string) { log.Println("Authorization error:", err) http.Redirect(w, r, "/login?tpl_layout=login&tpl_content=login", http.StatusTemporaryRedirect) } func respondWithStaticFile(w http.ResponseWriter, filename string) error { f, err := ioutil.ReadFile(filepath.Join("public/html", filename)) if err != nil { return err } w.Write(f) return nil } func (h *Handlers) cookieExtractor(r *http.Request) (string, error) { if session := r.URL.Query().Get("login_session"); h.Config.Handlers.AllowSessionURLQuery && session != "" { return session, nil } session, err := h.CookieStore.Get(r, "login-session") if err != nil { return "", nil } if session.Values["token"] == nil { return "", nil } token := session.Values["token"].([]uint8) return string(token), nil } func getClaims(r *http.Request) jwt.MapClaims { return r.Context().Value("user").(*jwt.Token).Claims.(jwt.MapClaims) } func DefaultRecoverHandler(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { panicMsg := fmt.Sprintf("PANIC: %v\n\n== STACKTRACE ==\n%s", err, debug.Stack()) log.Print(panicMsg) http.Error(w, panicMsg, http.StatusInternalServerError) } }() next.ServeHTTP(w, r) } return http.HandlerFunc(fn) } func (h *Handlers) hasPermission(r *http.Request, path string) bool { claims := getClaims(r) role := claims["role"].(string) if h.permissions[role] == nil { return false } return h.permissions[role][path] } func (h *Handlers) callModelFunc(w http.ResponseWriter, r *http.Request, model string, pattern config.PathPattern) (interface{}, error) { fn, err := h.Database.GetFunc(pattern.Path(model)) if err != nil { return nil, err } if !h.hasPermission(r, pattern.Path(model)) { return nil, errors.NotAuthorized } data, err := fn(h.Database, mux.Vars(r), w, r) if err != nil { return nil, err } return data, nil } func (h *Handlers) get(w http.ResponseWriter, r *http.Request, model string, pattern config.PathPattern) error { data, err := h.callModelFunc(w, r, model, pattern) if err != nil { return err } format := r.URL.Query().Get("format") err = h.Renderer[format].Render(w, r, h.CookieStore, data, r.URL.Query()) if err != nil { return err } return nil } func (h *Handlers) post(w http.ResponseWriter, r *http.Request, model string, pattern config.PathPattern) error { data, err := h.callModelFunc(w, r, model, pattern) if err != nil { return err } if pattern.RedirectPattern != "" { if id := mux.Vars(r)["id"]; id != "" { modelId, _ := strconv.Atoi(id) http.Redirect(w, r, pattern.RedirectPath(model, uint(modelId)), http.StatusSeeOther) } else { http.Redirect(w, r, pattern.RedirectPath(model, data.(orm.IDer).GetID()), http.StatusSeeOther) } } else { format := renderer.GetContentFormat(r) err := h.Renderer[format].Render(w, r, h.CookieStore, data.(orm.IDer).GetID()) if err != nil { return err } } return nil } func (h *Handlers) delete(w http.ResponseWriter, r *http.Request, model string, pattern config.PathPattern) error { data, err := h.callModelFunc(w, r, model, pattern) if err != nil { return err } if pattern.RedirectPattern != "" { var data struct { RedirectUrl string `json:"redirect_url"` } data.RedirectUrl = pattern.RedirectPath(model) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(data) } else { format := renderer.GetContentFormat(r) err := h.Renderer[format].Render(w, r, h.CookieStore, data.(orm.IDer).GetID()) if err != nil { return err } } return nil } func respondWithError(h *Handlers, w http.ResponseWriter, r *http.Request, err error) { var format string format = r.URL.Query().Get("format") if format == "" { format = renderer.GetContentFormat(r) } panicMsg := fmt.Sprintf("PANIC: %v\n\n== STACKTRACE ==\n%s", err, debug.Stack()) if h.Config.LogLevel > config.LOG_LEVEL_OFF { log.Println("Error:", panicMsg) } // FIXME: this call could be superflous when an error occurs // in a template execution w.WriteHeader(http.StatusInternalServerError) h.Renderer[format].WriteError(w, r, err) } func (h *Handlers) Create(model interface{}) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) error { switch r.Method { case "GET": err := h.get(w, r, reflect.ModelNameLowerPlural(model), h.Config.CreatePattern()) if err != nil { return err } case "POST": err := h.post(w, r, reflect.ModelNameLowerPlural(model), h.Config.CreatePattern()) if err != nil { return err } } return nil } return newRootMiddleware(h, fn) } func (h *Handlers) Update(model interface{}) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) error { switch r.Method { case "GET": err := h.get(w, r, reflect.ModelNameLowerPlural(model), h.Config.UpdatePattern()) if err != nil { return err } case "POST": err := h.post(w, r, reflect.ModelNameLowerPlural(model), h.Config.UpdatePattern()) if err != nil { return err } } return nil } return newRootMiddleware(h, fn) } func (h *Handlers) ReadAll(model interface{}) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { h.get(w, r, reflect.ModelNameLowerPlural(model), h.Config.ReadAllPattern()) } return http.HandlerFunc(fn) } func (h *Handlers) Read(model interface{}) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { h.get(w, r, reflect.ModelNameLowerPlural(model), h.Config.ReadPattern()) } return http.HandlerFunc(fn) } func (h *Handlers) Delete(model interface{}) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { h.delete(w, r, reflect.ModelNameLowerPlural(model), h.Config.DeletePattern()) } return http.HandlerFunc(fn) } func (h *Handlers) modelHandler(model string, pattern config.PathPattern) handlerFuncWithError { fn := func(w http.ResponseWriter, r *http.Request) error { var err error // Replace the "api" prefix. pattern.PathPattern = strings.Replace(pattern.PathPattern, "/api", "", -1) switch r.Method { case "GET": err = h.get(w, r, model, pattern) case "POST": err = h.post(w, r, model, pattern) case "DELETE": err = h.delete(w, r, model, pattern) } return err } return handlerFuncWithError(fn) } func DefaultHomeHandler() http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { claims := getClaims(r) switch claims["role"] { case "subscriber": http.Redirect( w, r, fmt.Sprintf( "/schools/create/?format=html&tpl_layout=base&tpl_content=schools_add_update", ), http.StatusSeeOther, ) case "participant": http.Redirect( w, r, fmt.Sprintf( "/participants/%s?format=html&tpl_layout=base&tpl_content=participants_show", claims["model_id"].(string)), http.StatusSeeOther, ) case "school": http.Redirect( w, r, fmt.Sprintf( "/schools/%s?format=html&tpl_layout=base&tpl_content=schools_show", claims["model_id"].(string)), http.StatusSeeOther, ) default: http.Redirect( w, r, "/contests?format=html&tpl_layout=base&tpl_content=contests", http.StatusSeeOther, ) } } return http.HandlerFunc(fn) } func (m *rootMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { err := m.fn(w, r) // Call handler function if err == nil { return } // This is where our error handling logic starts. if err, ok := err.(*errors.Error); ok { err.Referer = r.Header.Get("Referer") } respondWithError(m.h, w, r, err) }