diff --git a/doc/config.md b/doc/config.md index c880d4e..5e3336e 100644 --- a/doc/config.md +++ b/doc/config.md @@ -30,9 +30,7 @@ variables it supports: INBUCKET_WEB_ADDR 0.0.0.0:9000 Web server IP4 host:port INBUCKET_WEB_UIDIR ui User interface dir INBUCKET_WEB_GREETINGFILE ui/greeting.html Home page greeting HTML - INBUCKET_WEB_TEMPLATECACHE true Cache templates after first use? INBUCKET_WEB_MAILBOXPROMPT @inbucket Prompt next to mailbox input - INBUCKET_WEB_COOKIEAUTHKEY Session cipher key (text) INBUCKET_WEB_MONITORVISIBLE true Show monitor tab in UI? INBUCKET_WEB_MONITORHISTORY 30 Monitor remembered messages INBUCKET_WEB_PPROF false Expose profiling tools on /debug/pprof @@ -311,16 +309,6 @@ Inbucket installation, as well as link to REST documentation, etc. - Default: `ui/greeting.html` -### Template Caching - -`INBUCKET_WEB_TEMPLATECACHE` - -Tells Inbucket to cache parsed template files. This should be left as default -unless you are a developer working on the Inbucket web interface. - -- Default: `true` -- Values: `true` or `false` - ### Mailbox Prompt `INBUCKET_WEB_MAILBOXPROMPT` @@ -333,17 +321,6 @@ Set to an empty string to hide the prompt. - Default: `@inbucket` -### Cookie Authentication Key - -`INBUCKET_WEB_COOKIEAUTHKEY` - -Inbucket stores session information in an encrypted browser cookie. Unless -specified, Inbucket generates a random key at startup. The only notable data -stored in a user session is the list of recently accessed mailboxes. - -- Default: None -- Value: Text string, no particular format required - ### Monitor Visible `INBUCKET_WEB_MONITORVISIBLE` diff --git a/go.mod b/go.mod index fb2389c..27e5b9d 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,9 @@ module github.com/jhillyerd/inbucket require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/subcommands v0.0.0-20181012225330-46f0354f6315 + github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/css v1.0.0 github.com/gorilla/mux v1.6.2 - github.com/gorilla/securecookie v1.1.1 - github.com/gorilla/sessions v1.1.3 github.com/gorilla/websocket v1.4.0 github.com/jhillyerd/enmime v0.4.0 github.com/jhillyerd/goldiff v0.1.0 diff --git a/go.sum b/go.sum index f51db8a..4b89317 100644 --- a/go.sum +++ b/go.sum @@ -12,10 +12,6 @@ github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU= -github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 h1:xqgexXAGQgY3HAjNPSaCqn5Aahbo5TKsmhp8VRfr1iQ= diff --git a/pkg/config/config.go b/pkg/config/config.go index b1bf39a..754b5bc 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -95,9 +95,7 @@ type Web struct { Addr string `required:"true" default:"0.0.0.0:9000" desc:"Web server IP4 host:port"` UIDir string `required:"true" default:"ui" desc:"User interface dir"` GreetingFile string `required:"true" default:"ui/greeting.html" desc:"Home page greeting HTML"` - TemplateCache bool `required:"true" default:"true" desc:"Cache templates after first use?"` MailboxPrompt string `required:"true" default:"@inbucket" desc:"Prompt next to mailbox input"` - CookieAuthKey string `desc:"Session cipher key (text)"` MonitorVisible bool `required:"true" default:"true" desc:"Show monitor tab in UI?"` MonitorHistory int `required:"true" default:"30" desc:"Monitor remembered messages"` PProf bool `required:"true" default:"false" desc:"Expose profiling tools on /debug/pprof"` diff --git a/pkg/policy/address.go b/pkg/policy/address.go index 29de48d..dc2b855 100644 --- a/pkg/policy/address.go +++ b/pkg/policy/address.go @@ -159,16 +159,16 @@ func ValidateDomainPart(domain string) bool { // domain part is optional and not validated. func parseEmailAddress(address string) (local string, domain string, err error) { if address == "" { - return "", "", fmt.Errorf("Empty address") + return "", "", fmt.Errorf("empty address") } if len(address) > 320 { - return "", "", fmt.Errorf("Address exceeds 320 characters") + return "", "", fmt.Errorf("address exceeds 320 characters") } if address[0] == '@' { - return "", "", fmt.Errorf("Address cannot start with @ symbol") + return "", "", fmt.Errorf("address cannot start with @ symbol") } if address[0] == '.' { - return "", "", fmt.Errorf("Address cannot start with a period") + return "", "", fmt.Errorf("address cannot start with a period") } // Loop over address parsing out local part. buf := new(bytes.Buffer) diff --git a/pkg/server/web/context.go b/pkg/server/web/context.go index 65e6b3d..32c2c6f 100644 --- a/pkg/server/web/context.go +++ b/pkg/server/web/context.go @@ -5,7 +5,6 @@ import ( "strings" "github.com/gorilla/mux" - "github.com/gorilla/sessions" "github.com/jhillyerd/inbucket/pkg/config" "github.com/jhillyerd/inbucket/pkg/message" "github.com/jhillyerd/inbucket/pkg/msghub" @@ -15,7 +14,6 @@ import ( // TODO remove redundant web config type Context struct { Vars map[string]string - Session *sessions.Session MsgHub *msghub.Hub Manager message.Manager RootConfig *config.Root @@ -48,24 +46,13 @@ func headerMatch(req *http.Request, name string, value string) bool { // NewContext returns a Context for the given HTTP Request func NewContext(req *http.Request) (*Context, error) { vars := mux.Vars(req) - sess, err := sessionStore.Get(req, "inbucket") - if err != nil { - if sess == nil { - // No session, must fail - return nil, err - } - // The session cookie was probably signed by an old key, ignore it - // gorilla created an empty session for us - err = nil - } ctx := &Context{ Vars: vars, - Session: sess, MsgHub: msgHub, Manager: manager, RootConfig: rootConfig, WebConfig: rootConfig.Web, IsJSON: headerMatch(req, "Accept", "application/json"), } - return ctx, err + return ctx, nil } diff --git a/pkg/server/web/helpers.go b/pkg/server/web/helpers.go index 50dcc61..7ab2890 100644 --- a/pkg/server/web/helpers.go +++ b/pkg/server/web/helpers.go @@ -3,55 +3,13 @@ package web import ( "fmt" "html" - "html/template" "regexp" "strings" - "time" - - "github.com/jhillyerd/inbucket/pkg/stringutil" - "github.com/rs/zerolog/log" ) -// TemplateFuncs declares functions made available to all templates (including partials) -var TemplateFuncs = template.FuncMap{ - "address": stringutil.StringAddress, - "friendlyTime": FriendlyTime, - "reverse": Reverse, - "stringsJoin": strings.Join, - "textToHtml": TextToHTML, -} - // From http://daringfireball.net/2010/07/improved_regex_for_matching_urls var urlRE = regexp.MustCompile("(?i)\\b((?:[a-z][\\w-]+:(?:/{1,3}|[a-z0-9%])|www\\d{0,3}[.]|[a-z0-9.\\-]+[.][a-z]{2,4}/)(?:[^\\s()<>]+|\\(([^\\s()<>]+|(\\([^\\s()<>]+\\)))*\\))+(?:\\(([^\\s()<>]+|(\\([^\\s()<>]+\\)))*\\)|[^\\s`!()\\[\\]{};:'\".,<>?«»“”‘’]))") -// FriendlyTime renders a timestamp in a friendly fashion: 03:04:05 PM if same day, -// otherwise Mon Jan 2, 2006 -func FriendlyTime(t time.Time) template.HTML { - ty, tm, td := t.Date() - ny, nm, nd := time.Now().Date() - if (ty == ny) && (tm == nm) && (td == nd) { - return template.HTML(t.Format("03:04:05 PM")) - } - return template.HTML(t.Format("Mon Jan 2, 2006")) -} - -// Reverse routing function (shared with templates) -func Reverse(name string, things ...interface{}) string { - // Convert the things to strings - strs := make([]string, len(things)) - for i, th := range things { - strs[i] = fmt.Sprint(th) - } - // Grab the route - u, err := Router.Get(name).URL(strs...) - if err != nil { - log.Error().Str("module", "web").Str("name", name).Err(err). - Msg("Failed to reverse route") - return "/ROUTE-ERROR" - } - return u.Path -} - // TextToHTML takes plain text, escapes it and tries to pretty it up for // HTML display func TextToHTML(text string) string { diff --git a/pkg/server/web/server.go b/pkg/server/web/server.go index fa0c111..957b7f7 100644 --- a/pkg/server/web/server.go +++ b/pkg/server/web/server.go @@ -11,19 +11,12 @@ import ( "time" "github.com/gorilla/mux" - "github.com/gorilla/securecookie" - "github.com/gorilla/sessions" "github.com/jhillyerd/inbucket/pkg/config" "github.com/jhillyerd/inbucket/pkg/message" "github.com/jhillyerd/inbucket/pkg/msghub" "github.com/rs/zerolog/log" ) -const ( - staticDir = "static" - templateDir = "templates" -) - var ( // msgHub holds a reference to the message pub/sub system msgHub *msghub.Hub @@ -36,7 +29,6 @@ var ( rootConfig *config.Root server *http.Server listener net.Listener - sessionStore sessions.Store globalShutdown chan bool // ExpWebSocketConnectsCurrent tracks the number of open WebSockets @@ -94,17 +86,6 @@ func Initialize( http.StatusNotFound, "No route matches URI path") Router.MethodNotAllowedHandler = noMatchHandler( http.StatusMethodNotAllowed, "Method not allowed for URI path") - - // Session cookie setup. - if conf.Web.CookieAuthKey == "" { - log.Info().Str("module", "web").Str("phase", "startup"). - Msg("Generating random cookie.auth.key") - sessionStore = sessions.NewCookieStore(securecookie.GenerateRandomKey(64)) - } else { - log.Info().Str("module", "web").Str("phase", "startup"). - Msg("Using configured cookie.auth.key") - sessionStore = sessions.NewCookieStore([]byte(conf.Web.CookieAuthKey)) - } } // Start begins listening for HTTP requests diff --git a/pkg/server/web/template.go b/pkg/server/web/template.go deleted file mode 100644 index f9f8da2..0000000 --- a/pkg/server/web/template.go +++ /dev/null @@ -1,84 +0,0 @@ -package web - -import ( - "html/template" - "net/http" - "path" - "path/filepath" - "sync" - - "github.com/rs/zerolog/log" -) - -var cachedMutex sync.Mutex -var cachedTemplates = map[string]*template.Template{} -var cachedPartials = map[string]*template.Template{} - -// RenderTemplate fetches the named template and renders it to the provided -// ResponseWriter. -func RenderTemplate(name string, w http.ResponseWriter, data interface{}) error { - t, err := ParseTemplate(name, false) - if err != nil { - log.Error().Str("module", "web").Str("path", name).Err(err). - Msg("Error in template") - return err - } - w.Header().Set("Expires", "-1") - return t.Execute(w, data) -} - -// RenderPartial fetches the named template and renders it to the provided -// ResponseWriter. -func RenderPartial(name string, w http.ResponseWriter, data interface{}) error { - t, err := ParseTemplate(name, true) - if err != nil { - log.Error().Str("module", "web").Str("path", name).Err(err). - Msg("Error in template") - return err - } - w.Header().Set("Expires", "-1") - return t.Execute(w, data) -} - -// ParseTemplate loads the requested template along with _base.html, caching -// the result (if configured to do so) -func ParseTemplate(name string, partial bool) (*template.Template, error) { - cachedMutex.Lock() - defer cachedMutex.Unlock() - - if t, ok := cachedTemplates[name]; ok { - return t, nil - } - - tempFile := filepath.Join(rootConfig.Web.UIDir, templateDir, filepath.FromSlash(name)) - log.Debug().Str("module", "web").Str("path", name).Msg("Parsing template") - - var err error - var t *template.Template - if partial { - // Need to get basename of file to make it root template w/ funcs - base := path.Base(name) - t = template.New(base).Funcs(TemplateFuncs) - t, err = t.ParseFiles(tempFile) - } else { - t = template.New("_base.html").Funcs(TemplateFuncs) - t, err = t.ParseFiles( - filepath.Join(rootConfig.Web.UIDir, templateDir, "_base.html"), tempFile) - } - if err != nil { - return nil, err - } - - // Allows us to disable caching for theme development - if rootConfig.Web.TemplateCache { - if partial { - log.Debug().Str("module", "web").Str("path", name).Msg("Caching partial") - cachedTemplates[name] = t - } else { - log.Debug().Str("module", "web").Str("path", name).Msg("Caching template") - cachedTemplates[name] = t - } - } - - return t, nil -} diff --git a/pkg/webui/mailbox_controller.go b/pkg/webui/mailbox_controller.go index 4ceab38..c9423e1 100644 --- a/pkg/webui/mailbox_controller.go +++ b/pkg/webui/mailbox_controller.go @@ -1,8 +1,8 @@ package webui import ( + "errors" "fmt" - "html/template" "io" "net/http" "strconv" @@ -109,14 +109,10 @@ func MailboxHTML(w http.ResponseWriter, req *http.Request, ctx *web.Context) (er // This doesn't indicate empty, likely an IO error return fmt.Errorf("GetMessage(%q) failed: %v", id, err) } - // Render partial template + // Render HTML w.Header().Set("Content-Type", "text/html; charset=UTF-8") - return web.RenderPartial("mailbox/_html.html", w, map[string]interface{}{ - "ctx": ctx, - "name": name, - "message": msg, - "body": template.HTML(msg.HTML()), - }) + _, err = w.Write([]byte(msg.HTML())) + return err } // MailboxSource displays the raw source of a message, including headers. Renders text/plain @@ -147,19 +143,13 @@ func MailboxViewAttach(w http.ResponseWriter, req *http.Request, ctx *web.Contex // Don't have to validate these aren't empty, Gorilla returns 404 name, err := ctx.Manager.MailboxForAddress(ctx.Vars["name"]) if err != nil { - ctx.Session.AddFlash(err.Error(), "errors") - _ = ctx.Session.Save(req, w) - http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther) - return nil + return err } id := ctx.Vars["id"] numStr := ctx.Vars["num"] num, err := strconv.ParseUint(numStr, 10, 32) if err != nil { - ctx.Session.AddFlash("Attachment number must be unsigned numeric", "errors") - _ = ctx.Session.Save(req, w) - http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther) - return nil + return err } msg, err := ctx.Manager.GetMessage(name, id) if err == storage.ErrNotExist { @@ -171,10 +161,7 @@ func MailboxViewAttach(w http.ResponseWriter, req *http.Request, ctx *web.Contex return fmt.Errorf("GetMessage(%q) failed: %v", id, err) } if int(num) >= len(msg.Attachments()) { - ctx.Session.AddFlash("Attachment number too high", "errors") - _ = ctx.Session.Save(req, w) - http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther) - return nil + return errors.New("requested attachment number does not exist") } // Output attachment part := msg.Attachments()[num] diff --git a/pkg/webui/recent.go b/pkg/webui/recent.go deleted file mode 100644 index 97168d3..0000000 --- a/pkg/webui/recent.go +++ /dev/null @@ -1,35 +0,0 @@ -package webui - -import ( - "github.com/jhillyerd/inbucket/pkg/server/web" -) - -const ( - // maximum mailboxes to remember - maxRemembered = 8 - // session value key; referenced in templates, do not change - mailboxKey = "recentMailboxes" -) - -// RememberMailbox manages the list of recently accessed mailboxes stored in the session -func RememberMailbox(ctx *web.Context, mailbox string) { - recent := RecentMailboxes(ctx) - newRecent := make([]string, 1, maxRemembered) - newRecent[0] = mailbox - - for _, recBox := range recent { - // Insert until newRecent is full, but don't repeat the new mailbox - if len(newRecent) < maxRemembered && mailbox != recBox { - newRecent = append(newRecent, recBox) - } - } - - ctx.Session.Values[mailboxKey] = newRecent -} - -// RecentMailboxes returns a slice of the most recently accessed mailboxes -func RecentMailboxes(ctx *web.Context) []string { - val := ctx.Session.Values[mailboxKey] - recent, _ := val.([]string) - return recent -} diff --git a/pkg/webui/root_controller.go b/pkg/webui/root_controller.go index 4f2d672..f3b7eb3 100644 --- a/pkg/webui/root_controller.go +++ b/pkg/webui/root_controller.go @@ -21,55 +21,7 @@ func RootGreeting(w http.ResponseWriter, req *http.Request, ctx *web.Context) (e return err } -// RootMonitor serves the Inbucket monitor page -func RootMonitor(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) { - if !ctx.RootConfig.Web.MonitorVisible { - ctx.Session.AddFlash("Monitor is disabled in configuration", "errors") - _ = ctx.Session.Save(req, w) - http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther) - return nil - } - // Get flash messages, save session - errorFlash := ctx.Session.Flashes("errors") - if err = ctx.Session.Save(req, w); err != nil { - return err - } - // Render template - return web.RenderTemplate("root/monitor.html", w, map[string]interface{}{ - "ctx": ctx, - "errorFlash": errorFlash, - }) -} - -// RootMonitorMailbox serves the Inbucket monitor page for a particular mailbox -func RootMonitorMailbox(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) { - if !ctx.RootConfig.Web.MonitorVisible { - ctx.Session.AddFlash("Monitor is disabled in configuration", "errors") - _ = ctx.Session.Save(req, w) - http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther) - return nil - } - name, err := ctx.Manager.MailboxForAddress(ctx.Vars["name"]) - if err != nil { - ctx.Session.AddFlash(err.Error(), "errors") - _ = ctx.Session.Save(req, w) - http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther) - return nil - } - // Get flash messages, save session - errorFlash := ctx.Session.Flashes("errors") - if err = ctx.Session.Save(req, w); err != nil { - return err - } - // Render template - return web.RenderTemplate("root/monitor.html", w, map[string]interface{}{ - "ctx": ctx, - "errorFlash": errorFlash, - "name": name, - }) -} - -// RootStatus serves the Inbucket status page +// RootStatus renders portions of the server configuration as JSON. func RootStatus(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) { root := ctx.RootConfig retPeriod := "" diff --git a/pkg/webui/routes.go b/pkg/webui/routes.go index a480f7b..7673f9e 100644 --- a/pkg/webui/routes.go +++ b/pkg/webui/routes.go @@ -10,16 +10,12 @@ import ( func SetupRoutes(r *mux.Router) { r.Path("/greeting").Handler( web.Handler(RootGreeting)).Name("RootGreeting").Methods("GET") - r.Path("/monitor").Handler( - web.Handler(RootMonitor)).Name("RootMonitor").Methods("GET") - r.Path("/monitor/{name}").Handler( - web.Handler(RootMonitorMailbox)).Name("RootMonitorMailbox").Methods("GET") r.Path("/status").Handler( web.Handler(RootStatus)).Name("RootStatus").Methods("GET") r.Path("/m/{name}/{id}").Handler( web.Handler(MailboxMessage)).Name("MailboxMessage").Methods("GET") r.Path("/m/{name}/{id}/html").Handler( - web.Handler(MailboxHTML)).Name("MailboxHtml").Methods("GET") + web.Handler(MailboxHTML)).Name("MailboxHTML").Methods("GET") r.Path("/m/{name}/{id}/source").Handler( web.Handler(MailboxSource)).Name("MailboxSource").Methods("GET") r.Path("/m/attach/{name}/{id}/{num}/{file}").Handler(