|
- package writefreely
-
- import (
- "fmt"
- "html/template"
- "net/http"
- "net/url"
- "runtime/debug"
- "strconv"
- "strings"
- "time"
-
- "github.com/gorilla/sessions"
- "github.com/writeas/impart"
- "github.com/writeas/web-core/log"
- "github.com/writeas/writefreely/page"
- )
-
- type UserLevel int
-
- const (
- UserLevelNone UserLevel = iota // user or not -- ignored
- UserLevelOptional // user or not -- object fetched if user
- UserLevelNoneRequired // non-user (required)
- UserLevelUser // user (required)
- )
-
- type (
- handlerFunc func(app *app, w http.ResponseWriter, r *http.Request) error
- userHandlerFunc func(app *app, u *User, w http.ResponseWriter, r *http.Request) error
- dataHandlerFunc func(app *app, w http.ResponseWriter, r *http.Request) ([]byte, string, error)
- authFunc func(app *app, r *http.Request) (*User, error)
- )
-
- type Handler struct {
- errors *ErrorPages
- sessionStore *sessions.CookieStore
- app *app
- }
-
- // ErrorPages hold template HTML error pages for displaying errors to the user.
- // In each, there should be a defined template named "base".
- type ErrorPages struct {
- NotFound *template.Template
- Gone *template.Template
- InternalServerError *template.Template
- Blank *template.Template
- }
-
- // NewHandler returns a new Handler instance, using the given StaticPage data,
- // and saving alias to the application's CookieStore.
- func NewHandler(app *app) *Handler {
- h := &Handler{
- errors: &ErrorPages{
- NotFound: template.Must(template.New("").Parse("{{define \"base\"}}<html><head><title>404</title></head><body><p>Not found.</p></body></html>{{end}}")),
- Gone: template.Must(template.New("").Parse("{{define \"base\"}}<html><head><title>410</title></head><body><p>Gone.</p></body></html>{{end}}")),
- InternalServerError: template.Must(template.New("").Parse("{{define \"base\"}}<html><head><title>500</title></head><body><p>Internal server error.</p></body></html>{{end}}")),
- Blank: template.Must(template.New("").Parse("{{define \"base\"}}<html><head><title>{{.Title}}</title></head><body><p>{{.Content}}</p></body></html>{{end}}")),
- },
- sessionStore: app.sessionStore,
- app: app,
- }
-
- return h
- }
-
- // SetErrorPages sets the given set of ErrorPages as templates for any errors
- // that come up.
- func (h *Handler) SetErrorPages(e *ErrorPages) {
- h.errors = e
- }
-
- // User handles requests made in the web application by the authenticated user.
- // This provides user-friendly HTML pages and actions that work in the browser.
- func (h *Handler) User(f userHandlerFunc) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- h.handleHTTPError(w, r, func() error {
- var status int
- start := time.Now()
-
- defer func() {
- if e := recover(); e != nil {
- log.Error("%s: %s", e, debug.Stack())
- h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
- status = http.StatusInternalServerError
- }
-
- log.Info("\"%s %s\" %d %s \"%s\" \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent(), r.Host)
- }()
-
- u := getUserSession(h.app, r)
- if u == nil {
- err := ErrNotLoggedIn
- status = err.Status
- return err
- }
-
- err := f(h.app, u, w, r)
- if err == nil {
- status = http.StatusOK
- } else if err, ok := err.(impart.HTTPError); ok {
- status = err.Status
- } else {
- status = http.StatusInternalServerError
- }
-
- return err
- }())
- }
- }
-
- // UserAPI handles requests made in the API by the authenticated user.
- // This provides user-friendly HTML pages and actions that work in the browser.
- func (h *Handler) UserAPI(f userHandlerFunc) http.HandlerFunc {
- return h.UserAll(false, f, func(app *app, r *http.Request) (*User, error) {
- // Authorize user from Authorization header
- t := r.Header.Get("Authorization")
- if t == "" {
- return nil, ErrNoAccessToken
- }
- u := &User{ID: app.db.GetUserID(t)}
- if u.ID == -1 {
- return nil, ErrBadAccessToken
- }
-
- return u, nil
- })
- }
-
- func (h *Handler) UserAll(web bool, f userHandlerFunc, a authFunc) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- handleFunc := func() error {
- var status int
- start := time.Now()
-
- defer func() {
- if e := recover(); e != nil {
- log.Error("%s: %s", e, debug.Stack())
- impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "Something didn't work quite right."})
- status = 500
- }
-
- log.Info("\"%s %s\" %d %s \"%s\" \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent(), r.Host)
- }()
-
- u, err := a(h.app, r)
- if err != nil {
- if err, ok := err.(impart.HTTPError); ok {
- status = err.Status
- } else {
- status = 500
- }
- return err
- }
-
- err = f(h.app, u, w, r)
- if err == nil {
- status = 200
- } else if err, ok := err.(impart.HTTPError); ok {
- status = err.Status
- } else {
- status = 500
- }
-
- return err
- }
-
- if web {
- h.handleHTTPError(w, r, handleFunc())
- } else {
- h.handleError(w, r, handleFunc())
- }
- }
- }
-
- func (h *Handler) RedirectOnErr(f handlerFunc, loc string) handlerFunc {
- return func(app *app, w http.ResponseWriter, r *http.Request) error {
- err := f(app, w, r)
- if err != nil {
- if ie, ok := err.(impart.HTTPError); ok {
- // Override default redirect with returned error's, if it's a
- // redirect error.
- if ie.Status == http.StatusFound {
- return ie
- }
- }
- return impart.HTTPError{http.StatusFound, loc}
- }
- return nil
- }
- }
-
- func (h *Handler) Page(n string) http.HandlerFunc {
- return h.Web(func(app *app, w http.ResponseWriter, r *http.Request) error {
- t, ok := pages[n]
- if !ok {
- return impart.HTTPError{http.StatusNotFound, "Page not found."}
- }
-
- sp := pageForReq(app, r)
-
- err := t.ExecuteTemplate(w, "base", sp)
- if err != nil {
- log.Error("Unable to render page: %v", err)
- }
- return err
- }, UserLevelOptional)
- }
-
- func (h *Handler) WebErrors(f handlerFunc, ul UserLevel) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- // TODO: factor out this logic shared with Web()
- h.handleHTTPError(w, r, func() error {
- var status int
- start := time.Now()
-
- defer func() {
- if e := recover(); e != nil {
- u := getUserSession(h.app, r)
- username := "None"
- if u != nil {
- username = u.Username
- }
- log.Error("User: %s\n\n%s: %s", username, e, debug.Stack())
- h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
- status = 500
- }
-
- log.Info("\"%s %s\" %d %s \"%s\" \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent(), r.Host)
- }()
-
- var session *sessions.Session
- var err error
- if ul != UserLevelNone {
- session, err = h.sessionStore.Get(r, cookieName)
- if err != nil && (ul == UserLevelNoneRequired || ul == UserLevelUser) {
- // Cookie is required, but we can ignore this error
- log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul, err)
- }
-
- _, gotUser := session.Values[cookieUserVal].(*User)
- if ul == UserLevelNoneRequired && gotUser {
- to := correctPageFromLoginAttempt(r)
- log.Info("Handler: Required NO user, but got one. Redirecting to %s", to)
- err := impart.HTTPError{http.StatusFound, to}
- status = err.Status
- return err
- } else if ul == UserLevelUser && !gotUser {
- log.Info("Handler: Required a user, but DIDN'T get one. Sending not logged in.")
- err := ErrNotLoggedIn
- status = err.Status
- return err
- }
- }
-
- // TODO: pass User object to function
- err = f(h.app, w, r)
- if err == nil {
- status = 200
- } else if httpErr, ok := err.(impart.HTTPError); ok {
- status = httpErr.Status
- if status < 300 || status > 399 {
- addSessionFlash(h.app, w, r, httpErr.Message, session)
- return impart.HTTPError{http.StatusFound, r.Referer()}
- }
- } else {
- e := fmt.Sprintf("[Web handler] 500: %v", err)
- if !strings.HasSuffix(e, "write: broken pipe") {
- log.Error(e)
- } else {
- log.Error(e)
- }
- log.Info("Web handler internal error render")
- h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
- status = 500
- }
-
- return err
- }())
- }
- }
-
- // Web handles requests made in the web application. This provides user-
- // friendly HTML pages and actions that work in the browser.
- func (h *Handler) Web(f handlerFunc, ul UserLevel) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- h.handleHTTPError(w, r, func() error {
- var status int
- start := time.Now()
-
- defer func() {
- if e := recover(); e != nil {
- u := getUserSession(h.app, r)
- username := "None"
- if u != nil {
- username = u.Username
- }
- log.Error("User: %s\n\n%s: %s", username, e, debug.Stack())
- log.Info("Web deferred internal error render")
- h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
- status = 500
- }
-
- log.Info("\"%s %s\" %d %s \"%s\" \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent(), r.Host)
- }()
-
- if ul != UserLevelNone {
- session, err := h.sessionStore.Get(r, cookieName)
- if err != nil && (ul == UserLevelNoneRequired || ul == UserLevelUser) {
- // Cookie is required, but we can ignore this error
- log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul, err)
- }
-
- _, gotUser := session.Values[cookieUserVal].(*User)
- if ul == UserLevelNoneRequired && gotUser {
- to := correctPageFromLoginAttempt(r)
- log.Info("Handler: Required NO user, but got one. Redirecting to %s", to)
- err := impart.HTTPError{http.StatusFound, to}
- status = err.Status
- return err
- } else if ul == UserLevelUser && !gotUser {
- log.Info("Handler: Required a user, but DIDN'T get one. Sending not logged in.")
- err := ErrNotLoggedIn
- status = err.Status
- return err
- }
- }
-
- // TODO: pass User object to function
- err := f(h.app, w, r)
- if err == nil {
- status = 200
- } else if httpErr, ok := err.(impart.HTTPError); ok {
- status = httpErr.Status
- } else {
- e := fmt.Sprintf("[Web handler] 500: %v", err)
- log.Error(e)
- log.Info("Web internal error render")
- h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
- status = 500
- }
-
- return err
- }())
- }
- }
-
- func (h *Handler) All(f handlerFunc) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- h.handleError(w, r, func() error {
- // TODO: return correct "success" status
- status := 200
- start := time.Now()
-
- defer func() {
- if e := recover(); e != nil {
- log.Error("%s:\n%s", e, debug.Stack())
- impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "Something didn't work quite right."})
- status = 500
- }
-
- log.Info("\"%s %s\" %d %s \"%s\" \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent(), r.Host)
- }()
-
- // TODO: do any needed authentication
-
- err := f(h.app, w, r)
- if err != nil {
- if err, ok := err.(impart.HTTPError); ok {
- status = err.Status
- } else {
- status = 500
- }
- }
-
- return err
- }())
- }
- }
-
- func (h *Handler) Download(f dataHandlerFunc, ul UserLevel) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- h.handleHTTPError(w, r, func() error {
- var status int
- start := time.Now()
- defer func() {
- if e := recover(); e != nil {
- log.Error("%s: %s", e, debug.Stack())
- h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
- status = 500
- }
-
- log.Info("\"%s %s\" %d %s \"%s\" \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent(), r.Host)
- }()
-
- data, filename, err := f(h.app, w, r)
- if err != nil {
- if err, ok := err.(impart.HTTPError); ok {
- status = err.Status
- } else {
- status = 500
- }
- return err
- }
-
- ext := ".json"
- ct := "application/json"
- if strings.HasSuffix(r.URL.Path, ".csv") {
- ext = ".csv"
- ct = "text/csv"
- } else if strings.HasSuffix(r.URL.Path, ".zip") {
- ext = ".zip"
- ct = "application/zip"
- }
- w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s%s", filename, ext))
- w.Header().Set("Content-Type", ct)
- w.Header().Set("Content-Length", strconv.Itoa(len(data)))
- fmt.Fprint(w, string(data))
-
- status = 200
- return nil
- }())
- }
- }
-
- func (h *Handler) Redirect(url string, ul UserLevel) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- h.handleHTTPError(w, r, func() error {
- start := time.Now()
-
- var status int
- if ul != UserLevelNone {
- session, err := h.sessionStore.Get(r, cookieName)
- if err != nil && (ul == UserLevelNoneRequired || ul == UserLevelUser) {
- // Cookie is required, but we can ignore this error
- log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul, err)
- }
-
- _, gotUser := session.Values[cookieUserVal].(*User)
- if ul == UserLevelNoneRequired && gotUser {
- to := correctPageFromLoginAttempt(r)
- log.Info("Handler: Required NO user, but got one. Redirecting to %s", to)
- err := impart.HTTPError{http.StatusFound, to}
- status = err.Status
- return err
- } else if ul == UserLevelUser && !gotUser {
- log.Info("Handler: Required a user, but DIDN'T get one. Sending not logged in.")
- err := ErrNotLoggedIn
- status = err.Status
- return err
- }
- }
-
- status = sendRedirect(w, http.StatusFound, url)
-
- log.Info("\"%s %s\" %d %s \"%s\" \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent(), r.Host)
-
- return nil
- }())
- }
- }
-
- func (h *Handler) handleHTTPError(w http.ResponseWriter, r *http.Request, err error) {
- if err == nil {
- return
- }
-
- if err, ok := err.(impart.HTTPError); ok {
- if err.Status >= 300 && err.Status < 400 {
- sendRedirect(w, err.Status, err.Message)
- return
- } else if err.Status == http.StatusUnauthorized {
- q := ""
- if r.URL.RawQuery != "" {
- q = url.QueryEscape("?" + r.URL.RawQuery)
- }
- sendRedirect(w, http.StatusFound, "/login?to="+r.URL.Path+q)
- return
- } else if err.Status == http.StatusGone {
- p := &struct {
- page.StaticPage
- Content *template.HTML
- }{
- StaticPage: pageForReq(h.app, r),
- }
- if err.Message != "" {
- co := template.HTML(err.Message)
- p.Content = &co
- }
- h.errors.Gone.ExecuteTemplate(w, "base", p)
- return
- } else if err.Status == http.StatusNotFound {
- h.errors.NotFound.ExecuteTemplate(w, "base", pageForReq(h.app, r))
- return
- } else if err.Status == http.StatusInternalServerError {
- log.Info("handleHTTPErorr internal error render")
- h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
- return
- } else if err.Status == http.StatusAccepted {
- impart.WriteSuccess(w, "", err.Status)
- return
- } else {
- p := &struct {
- page.StaticPage
- Title string
- Content template.HTML
- }{
- pageForReq(h.app, r),
- fmt.Sprintf("Uh oh (%d)", err.Status),
- template.HTML(fmt.Sprintf("<p style=\"text-align: center\" class=\"introduction\">%s</p>", err.Message)),
- }
- h.errors.Blank.ExecuteTemplate(w, "base", p)
- return
- }
- impart.WriteError(w, err)
- return
- }
-
- impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "This is an unhelpful error message for a miscellaneous internal error."})
- }
-
- func (h *Handler) handleError(w http.ResponseWriter, r *http.Request, err error) {
- if err == nil {
- return
- }
-
- if err, ok := err.(impart.HTTPError); ok {
- if err.Status >= 300 && err.Status < 400 {
- sendRedirect(w, err.Status, err.Message)
- return
- }
-
- // if strings.Contains(r.Header.Get("Accept"), "text/html") {
- impart.WriteError(w, err)
- // }
- return
- }
-
- if IsJSON(r.Header.Get("Content-Type")) {
- impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "This is an unhelpful error message for a miscellaneous internal error."})
- return
- }
- h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
- }
-
- func correctPageFromLoginAttempt(r *http.Request) string {
- to := r.FormValue("to")
- if to == "" {
- to = "/"
- } else if !strings.HasPrefix(to, "/") {
- to = "/" + to
- }
- return to
- }
-
- func (h *Handler) LogHandlerFunc(f http.HandlerFunc) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- h.handleHTTPError(w, r, func() error {
- status := 200
- start := time.Now()
-
- defer func() {
- if e := recover(); e != nil {
- log.Error("Handler.LogHandlerFunc\n\n%s: %s", e, debug.Stack())
- h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r))
- status = 500
- }
-
- // TODO: log actual status code returned
- log.Info("\"%s %s\" %d %s \"%s\" \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent(), r.Host)
- }()
-
- f(w, r)
-
- return nil
- }())
- }
- }
-
- func sendRedirect(w http.ResponseWriter, code int, location string) int {
- w.Header().Set("Location", location)
- w.WriteHeader(code)
- return code
- }
|