diff --git a/app.go b/app.go index bfed9f6..9e50d97 100644 --- a/app.go +++ b/app.go @@ -14,6 +14,7 @@ import ( "github.com/gorilla/sessions" "github.com/writeas/web-core/log" "github.com/writeas/writefreely/config" + "github.com/writeas/writefreely/page" ) const ( @@ -36,6 +37,35 @@ type app struct { sessionStore *sessions.CookieStore } +func pageForReq(app *app, r *http.Request) page.StaticPage { + p := page.StaticPage{ + AppCfg: app.cfg.App, + Path: r.URL.Path, + Version: "v" + softwareVer, + } + + // Add user information, if given + var u *User + accessToken := r.FormValue("t") + if accessToken != "" { + userID := app.db.GetUserID(accessToken) + if userID != -1 { + var err error + u, err = app.db.GetUserByID(userID) + if err == nil { + p.Username = u.Username + } + } + } else { + u = getUserSession(app, r) + if u != nil { + p.Username = u.Username + } + } + + return p +} + var shttp = http.NewServeMux() func Serve() { @@ -79,6 +109,8 @@ func Serve() { app.cfg.Server.Dev = *debugPtr + initTemplates() + // Load keys log.Info("Loading encryption keys...") err = initKeys(app) @@ -112,7 +144,13 @@ func Serve() { app.db.SetMaxOpenConns(50) r := mux.NewRouter() - handler := NewHandler(app.sessionStore) + handler := NewHandler(app) + handler.SetErrorPages(&ErrorPages{ + NotFound: pages["404-general.tmpl"], + Gone: pages["410.tmpl"], + InternalServerError: pages["500.tmpl"], + Blank: pages["blank.tmpl"], + }) // Handle app routes initRoutes(handler, r, app.cfg, app.db) diff --git a/handle.go b/handle.go new file mode 100644 index 0000000..cc74bd1 --- /dev/null +++ b/handle.go @@ -0,0 +1,584 @@ +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\"}}
Not found.
{{end}}")), + Gone: template.Must(template.New("").Parse("{{define \"base\"}}Gone.
{{end}}")), + InternalServerError: template.Must(template.New("").Parse("{{define \"base\"}}Internal server error.
{{end}}")), + Blank: template.Must(template.New("").Parse("{{define \"base\"}}{{.Content}}
{{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("%s
", 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 +} diff --git a/page/page.go b/page/page.go new file mode 100644 index 0000000..673424e --- /dev/null +++ b/page/page.go @@ -0,0 +1,29 @@ +// package page provides mechanisms and data for generating a WriteFreely page. +package page + +import ( + "github.com/writeas/writefreely/config" + "strings" +) + +type StaticPage struct { + // App configuration + config.AppCfg + Version string + HeaderNav bool + + // Request values + Path string + Username string + Values map[string]string + Flashes []string +} + +// SanitizeHost alters the StaticPage to contain a real hostname. This is +// especially important for the Tor hidden service, as it can be served over +// proxies, messing up the apparent hostname. +func (sp *StaticPage) SanitizeHost(cfg *config.Config) { + if cfg.Server.HiddenHost != "" && strings.HasPrefix(sp.Host, cfg.Server.HiddenHost) { + sp.Host = cfg.Server.HiddenHost + } +} diff --git a/pages/404-general.tmpl b/pages/404-general.tmpl new file mode 100644 index 0000000..dfc4653 --- /dev/null +++ b/pages/404-general.tmpl @@ -0,0 +1,7 @@ +{{define "head"}}This page is missing.
+Are you sure it was ever here?
+Post not found.
+ {{if and (not .SingleUser) .OpenRegistration}} +Why not share a thought of your own?
+Start a blog and spread your ideas on {{.SiteName}}, a simple{{if .Federation}}, federated{{end}} blogging community.
+ {{end}} +{{if .Content}}{{.Content}}{{else}}Post was unpublished by the author.{{end}}
+It might be back some day.
+Server error. 😲 😵
+The humans have been alerted and reminded of their many shortcomings.
+On behalf of them, we apologize.
+– The Write.as Bots
+