From 88f26ef3c67b9a7d8435208ec6ecffd56961c289 Mon Sep 17 00:00:00 2001 From: Jan-Lukas Else Date: Tue, 27 Jul 2021 12:51:08 +0200 Subject: [PATCH] Reduce complexity of router build method --- activityPub.go | 7 +- activityStreams.go | 2 +- app.go | 2 - blogroll.go | 4 +- blogstats.go | 11 +- comments.go | 4 +- commentsAdmin.go | 4 +- config.go | 5 - contact.go | 4 +- customPages.go | 2 +- editor.go | 4 +- editorFiles.go | 4 +- feeds.go | 6 +- geoMap.go | 2 +- go.mod | 2 +- go.sum | 4 +- http.go | 393 +++---------------------------------- httpMiddlewares.go | 51 +++++ httpRouters.go | 414 +++++++++++++++++++++++++++++++++++++++ micropubMedia.go | 2 +- opensearch.go | 2 +- posts.go | 14 +- postsDb.go | 2 +- privateMode.go | 24 +++ robotstxt.go | 10 +- robotstxt_test.go | 16 +- search.go | 6 +- sessions.go | 9 +- sitemap.go | 6 +- taxonomies.go | 4 +- templates/posttax.gohtml | 2 +- utils.go | 4 + webmentionSending.go | 2 +- 33 files changed, 593 insertions(+), 435 deletions(-) create mode 100644 httpMiddlewares.go create mode 100644 httpRouters.go create mode 100644 privateMode.go diff --git a/activityPub.go b/activityPub.go index f3ec005..d50ab37 100644 --- a/activityPub.go +++ b/activityPub.go @@ -22,7 +22,12 @@ import ( ) func (a *goBlog) initActivityPub() error { - if !a.cfg.ActivityPub.Enabled { + if a.isPrivate() { + // Private mode, no AP + return nil + } + if apc := a.cfg.ActivityPub; apc == nil || !apc.Enabled { + // Disabled return nil } // Add hooks diff --git a/activityStreams.go b/activityStreams.go index e84a0e3..7df4fa4 100644 --- a/activityStreams.go +++ b/activityStreams.go @@ -26,7 +26,7 @@ func (a *goBlog) checkActivityStreamsRequest(next http.Handler) http.Handler { } } return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - if ap := a.cfg.ActivityPub; ap != nil && ap.Enabled { + if ap := a.cfg.ActivityPub; ap != nil && ap.Enabled && !a.isPrivate() { // Check if accepted media type is not HTML if mt, _, err := ct.GetAcceptableMediaType(r, a.asCheckMediaTypes); err == nil && mt.String() != a.asCheckMediaTypes[0].String() { next.ServeHTTP(rw, r.WithContext(context.WithValue(r.Context(), asRequestKey, true))) diff --git a/app.go b/app.go index da10e55..6e8ae25 100644 --- a/app.go +++ b/app.go @@ -45,8 +45,6 @@ type goBlog struct { pUpdateHooks []postHookFunc pDeleteHooks []postHookFunc hourlyHooks []hourlyHookFunc - // HTTP - cspDomains string // HTTP Client httpClient httpClient // HTTP Routers diff --git a/blogroll.go b/blogroll.go index 14eb408..dfe931f 100644 --- a/blogroll.go +++ b/blogroll.go @@ -18,7 +18,7 @@ import ( const defaultBlogrollPath = "/blogroll" func (a *goBlog) serveBlogroll(w http.ResponseWriter, r *http.Request) { - blog := r.Context().Value(blogContextKey).(string) + blog := r.Context().Value(blogKey).(string) outlines, err, _ := a.blogrollCacheGroup.Do(blog, func() (interface{}, error) { return a.getBlogrollOutlines(blog) }) @@ -42,7 +42,7 @@ func (a *goBlog) serveBlogroll(w http.ResponseWriter, r *http.Request) { } func (a *goBlog) serveBlogrollExport(w http.ResponseWriter, r *http.Request) { - blog := r.Context().Value(blogContextKey).(string) + blog := r.Context().Value(blogKey).(string) outlines, err, _ := a.blogrollCacheGroup.Do(blog, func() (interface{}, error) { return a.getBlogrollOutlines(blog) }) diff --git a/blogstats.go b/blogstats.go index 1f8dbfa..4db9517 100644 --- a/blogstats.go +++ b/blogstats.go @@ -7,7 +7,10 @@ import ( "net/http" ) -const defaultBlogStatsPath = "/statistics" +const ( + defaultBlogStatsPath = "/statistics" + blogStatsTablePath = ".table.html" +) func (a *goBlog) initBlogStats() { f := func(p *post) { @@ -19,20 +22,20 @@ func (a *goBlog) initBlogStats() { } func (a *goBlog) serveBlogStats(w http.ResponseWriter, r *http.Request) { - blog := r.Context().Value(blogContextKey).(string) + blog := r.Context().Value(blogKey).(string) bc := a.cfg.Blogs[blog] canonical := bc.getRelativePath(defaultIfEmpty(bc.BlogStats.Path, defaultBlogStatsPath)) a.render(w, r, templateBlogStats, &renderData{ BlogString: blog, Canonical: a.getFullAddress(canonical), Data: map[string]interface{}{ - "TableUrl": canonical + ".table.html", + "TableUrl": canonical + blogStatsTablePath, }, }) } func (a *goBlog) serveBlogStatsTable(w http.ResponseWriter, r *http.Request) { - blog := r.Context().Value(blogContextKey).(string) + blog := r.Context().Value(blogKey).(string) data, err, _ := a.blogStatsCacheGroup.Do(blog, func() (interface{}, error) { return a.db.getBlogStats(blog) }) diff --git a/comments.go b/comments.go index 4a227ad..dad5f2a 100644 --- a/comments.go +++ b/comments.go @@ -39,7 +39,7 @@ func (a *goBlog) serveComment(w http.ResponseWriter, r *http.Request) { a.serveError(w, r, err.Error(), http.StatusInternalServerError) return } - blog := r.Context().Value(blogContextKey).(string) + blog := r.Context().Value(blogKey).(string) a.render(w, r, templateComment, &renderData{ BlogString: blog, Canonical: a.getFullAddress(a.cfg.Blogs[blog].getRelativePath(fmt.Sprintf("/comment/%d", id))), @@ -75,7 +75,7 @@ func (a *goBlog) createComment(w http.ResponseWriter, r *http.Request) { // Serve error a.serveError(w, r, err.Error(), http.StatusInternalServerError) } else { - commentAddress := fmt.Sprintf("%s/%d", a.getRelativePath(r.Context().Value(blogContextKey).(string), "/comment"), commentID) + commentAddress := fmt.Sprintf("%s/%d", a.getRelativePath(r.Context().Value(blogKey).(string), "/comment"), commentID) // Send webmention _ = a.createWebmention(a.getFullAddress(commentAddress), a.getFullAddress(target)) // Redirect to comment diff --git a/commentsAdmin.go b/commentsAdmin.go index 147f8c1..1a06027 100644 --- a/commentsAdmin.go +++ b/commentsAdmin.go @@ -35,8 +35,8 @@ func (p *commentsPaginationAdapter) Slice(offset, length int, data interface{}) } func (a *goBlog) commentsAdmin(w http.ResponseWriter, r *http.Request) { - blog := r.Context().Value(blogContextKey).(string) - commentsPath := r.Context().Value(pathContextKey).(string) + blog := r.Context().Value(blogKey).(string) + commentsPath := r.Context().Value(pathKey).(string) // Adapter pageNoString := chi.URLParam(r, "page") pageNo, _ := strconv.Atoi(pageNoString) diff --git a/config.go b/config.go index e26a8e1..4b1994d 100644 --- a/config.go +++ b/config.go @@ -290,8 +290,6 @@ func (a *goBlog) initConfig() error { viper.SetDefault("micropub.locationParam", "location") viper.SetDefault("activityPub.keyPath", "data/private.pem") viper.SetDefault("activityPub.tagsTaxonomies", []string{"tags"}) - viper.SetDefault("webmention.disableSending", false) - viper.SetDefault("webmention.disableReceiving", false) // Unmarshal config a.cfg = &config{} err = viper.Unmarshal(a.cfg) @@ -329,9 +327,6 @@ func (a *goBlog) initConfig() error { } a.cfg.Micropub.MediaStorage.MediaURL = strings.TrimSuffix(a.cfg.Micropub.MediaStorage.MediaURL, "/") } - if pm := a.cfg.PrivateMode; pm != nil && pm.Enabled { - a.cfg.ActivityPub = &configActivityPub{Enabled: false} - } if wm := a.cfg.Webmention; wm != nil && wm.DisableReceiving { // Disable comments for all blogs for _, b := range a.cfg.Blogs { diff --git a/contact.go b/contact.go index 7eb4b8b..8a5ce17 100644 --- a/contact.go +++ b/contact.go @@ -16,7 +16,7 @@ import ( const defaultContactPath = "/contact" func (a *goBlog) serveContactForm(w http.ResponseWriter, r *http.Request) { - blog := r.Context().Value(blogContextKey).(string) + blog := r.Context().Value(blogKey).(string) cc := a.cfg.Blogs[blog].Contact a.render(w, r, templateContact, &renderData{ BlogString: blog, @@ -62,7 +62,7 @@ func (a *goBlog) sendContactSubmission(w http.ResponseWriter, r *http.Request) { } _, _ = message.WriteString(formMessage) // Send submission - blog := r.Context().Value(blogContextKey).(string) + blog := r.Context().Value(blogKey).(string) if cc := a.cfg.Blogs[blog].Contact; cc != nil && cc.SMTPHost != "" && cc.EmailFrom != "" && cc.EmailTo != "" { // Build email var email bytes.Buffer diff --git a/customPages.go b/customPages.go index 0565a6d..0209157 100644 --- a/customPages.go +++ b/customPages.go @@ -7,7 +7,7 @@ const customPageContextKey = "custompage" func (a *goBlog) serveCustomPage(w http.ResponseWriter, r *http.Request) { page := r.Context().Value(customPageContextKey).(*configCustomPage) a.render(w, r, page.Template, &renderData{ - BlogString: r.Context().Value(blogContextKey).(string), + BlogString: r.Context().Value(blogKey).(string), Canonical: a.getFullAddress(page.Path), Data: page.Data, }) diff --git a/editor.go b/editor.go index 99d12e3..880aaf3 100644 --- a/editor.go +++ b/editor.go @@ -14,7 +14,7 @@ import ( const editorPath = "/editor" func (a *goBlog) serveEditor(w http.ResponseWriter, r *http.Request) { - blog := r.Context().Value(blogContextKey).(string) + blog := r.Context().Value(blogKey).(string) a.render(w, r, templateEditor, &renderData{ BlogString: blog, Data: map[string]interface{}{}, @@ -22,7 +22,7 @@ func (a *goBlog) serveEditor(w http.ResponseWriter, r *http.Request) { } func (a *goBlog) serveEditorPost(w http.ResponseWriter, r *http.Request) { - blog := r.Context().Value(blogContextKey).(string) + blog := r.Context().Value(blogKey).(string) if action := r.FormValue("editoraction"); action != "" { switch action { case "loaddelete": diff --git a/editorFiles.go b/editorFiles.go index b30cae0..53c6922 100644 --- a/editorFiles.go +++ b/editorFiles.go @@ -8,7 +8,7 @@ import ( ) func (a *goBlog) serveEditorFiles(w http.ResponseWriter, r *http.Request) { - blog := r.Context().Value(blogContextKey).(string) + blog := r.Context().Value(blogKey).(string) // Get files files, err := a.mediaFiles() if err != nil { @@ -69,5 +69,5 @@ func (a *goBlog) serveEditorFilesDelete(w http.ResponseWriter, r *http.Request) a.serveError(w, r, err.Error(), http.StatusInternalServerError) return } - http.Redirect(w, r, a.getRelativePath(r.Context().Value(blogContextKey).(string), "/editor/files"), http.StatusFound) + http.Redirect(w, r, a.getRelativePath(r.Context().Value(blogKey).(string), "/editor/files"), http.StatusFound) } diff --git a/feeds.go b/feeds.go index 9087d54..825b44a 100644 --- a/feeds.go +++ b/feeds.go @@ -41,16 +41,14 @@ func (a *goBlog) generateFeed(blog string, f feedType, w http.ResponseWriter, r }, } for _, p := range posts { - created, _ := dateparse.ParseLocal(p.Published) - updated, _ := dateparse.ParseLocal(p.Updated) feed.Add(&feeds.Item{ Title: p.Title(), Link: &feeds.Link{Href: a.fullPostURL(p)}, Description: a.postSummary(p), Id: p.Path, Content: string(a.postHtml(p, true)), - Created: created, - Updated: updated, + Created: timeNoErr(dateparse.ParseLocal(p.Published)), + Updated: timeNoErr(dateparse.ParseLocal(p.Updated)), }) } var err error diff --git a/geoMap.go b/geoMap.go index 7c944ea..c52e0d0 100644 --- a/geoMap.go +++ b/geoMap.go @@ -17,7 +17,7 @@ import ( const defaultGeoMapPath = "/map" func (a *goBlog) serveGeoMap(w http.ResponseWriter, r *http.Request) { - blog := r.Context().Value(blogContextKey).(string) + blog := r.Context().Value(blogKey).(string) bc := a.cfg.Blogs[blog] allPostsWithLocation, err := a.db.getPosts(&postsRequestConfig{ diff --git a/go.mod b/go.mod index efa2d8a..e6fa40c 100644 --- a/go.mod +++ b/go.mod @@ -53,7 +53,7 @@ require ( // master github.com/yuin/goldmark-emoji v1.0.2-0.20210607094911-0487583eca38 golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 - golang.org/x/net v0.0.0-20210716203947-853a461950ff + golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect diff --git a/go.sum b/go.sum index 4ea8b9d..72a3e54 100644 --- a/go.sum +++ b/go.sum @@ -465,8 +465,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210716203947-853a461950ff h1:j2EK/QoxYNBsXI4R7fQkkRUk8y6wnOBI+6hgPdP/6Ds= -golang.org/x/net v0.0.0-20210716203947-853a461950ff/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 h1:4CSI6oo7cOjJKajidEljs9h+uP0rRZBPPPhcCbj5mw8= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/http.go b/http.go index 87afb33..ba29260 100644 --- a/http.go +++ b/http.go @@ -8,9 +8,7 @@ import ( "log" "net" "net/http" - "net/url" "strconv" - "strings" "time" "github.com/dchest/captcha" @@ -26,16 +24,18 @@ const ( contentType = "Content-Type" userAgent = "User-Agent" appUserAgent = "GoBlog" + + blogKey contextKey = "blog" + pathKey contextKey = "httpPath" ) func (a *goBlog) startServer() (err error) { log.Println("Start server(s)...") // Load router - router, err := a.buildRouter() + a.d, err = a.buildRouter() if err != nil { return err } - a.d = fixHTTPHandler(router) // Set basic middlewares h := alice.New() if a.cfg.Server.Logging { @@ -126,28 +126,21 @@ const ( feedPath = ".{feed:rss|json|atom}" ) -func (a *goBlog) buildRouter() (*chi.Mux, error) { - r := chi.NewRouter() - - // Private mode - privateMode := false - var privateModeHandler []func(http.Handler) http.Handler - if pm := a.cfg.PrivateMode; pm != nil && pm.Enabled { - privateMode = true - privateModeHandler = append(privateModeHandler, a.authMiddleware) - } +func (a *goBlog) buildRouter() (http.Handler, error) { + r := chi.NewMux() // Basic middleware + r.Use(fixHTTPHandler) r.Use(a.redirectShortDomain) r.Use(middleware.RedirectSlashes) r.Use(middleware.CleanPath) r.Use(middleware.GetHead) - if !a.cfg.Cache.Enable { + if cache := a.cfg.Cache; cache != nil && !cache.Enable { r.Use(middleware.NoCache) } // No Index Header - if privateMode { + if a.isPrivate() { r.Use(noIndexHeader) } @@ -155,340 +148,60 @@ func (a *goBlog) buildRouter() (*chi.Mux, error) { r.Use(a.checkIsLogin) r.Use(a.checkIsCaptcha) - // Logout - r.With(a.authMiddleware).Get("/login", serveLogin) - r.With(a.authMiddleware).Get("/logout", a.serveLogout) + // Login + r.Group(a.loginRouter) // Micropub - r.Route(micropubPath, func(r chi.Router) { - r.Use(a.checkIndieAuth) - r.Get("/", a.serveMicropubQuery) - r.Post("/", a.serveMicropubPost) - r.Post(micropubMediaSubPath, a.serveMicropubMedia) - }) + r.Route(micropubPath, a.micropubRouter) // IndieAuth - r.Route("/indieauth", func(r chi.Router) { - r.Get("/", a.indieAuthRequest) - r.With(a.authMiddleware).Post("/accept", a.indieAuthAccept) - r.Post("/", a.indieAuthVerification) - r.Get("/token", a.indieAuthToken) - r.Post("/token", a.indieAuthToken) - }) + r.Route("/indieauth", a.indieAuthRouter) // ActivityPub and stuff - if ap := a.cfg.ActivityPub; ap != nil && ap.Enabled { - r.Route("/activitypub", func(r chi.Router) { - r.Post("/inbox/{blog}", a.apHandleInbox) - r.Post("/{blog}/inbox", a.apHandleInbox) - }) - r.Group(func(r chi.Router) { - r.Use(cacheLoggedIn, a.cacheMiddleware) - r.Get("/.well-known/webfinger", a.apHandleWebfinger) - r.Get("/.well-known/host-meta", handleWellKnownHostMeta) - r.Get("/.well-known/nodeinfo", a.serveNodeInfoDiscover) - r.Get("/nodeinfo", a.serveNodeInfo) - }) - } + r.Group(a.activityPubRouter) // Webmentions - if wm := a.cfg.Webmention; wm != nil && !wm.DisableReceiving { - r.Route(webmentionPath, func(r chi.Router) { - r.Post("/", a.handleWebmention) - r.Group(func(r chi.Router) { - // Authenticated routes - r.Use(a.authMiddleware) - r.Get("/", a.webmentionAdmin) - r.Get(paginationPath, a.webmentionAdmin) - r.Post("/delete", a.webmentionAdminDelete) - r.Post("/approve", a.webmentionAdminApprove) - r.Post("/reverify", a.webmentionAdminReverify) - }) - }) - } + r.Route(webmentionPath, a.webmentionsRouter) // Notifications - r.Route(notificationsPath, func(r chi.Router) { - r.Use(a.authMiddleware) - r.Get("/", a.notificationsAdmin) - r.Get(paginationPath, a.notificationsAdmin) - r.Post("/delete", a.notificationsAdminDelete) - }) + r.Route(notificationsPath, a.notificationsRouter) // Assets - for _, path := range a.allAssetPaths() { - r.Get(path, a.serveAsset) - } + r.Group(a.assetsRouter) // Static files - for _, path := range allStaticPaths() { - r.With(privateModeHandler...).Get(path, a.serveStaticFile) - } + r.Group(a.staticFilesRouter) // Media files - r.With(privateModeHandler...).Get(`/m/{file:[0-9a-fA-F]+(\.[0-9a-zA-Z]+)?}`, a.serveMediaFile) + r.With(a.privateModeHandler).Get(`/m/{file:[0-9a-fA-F]+(\.[0-9a-zA-Z]+)?}`, a.serveMediaFile) // Captcha r.Handle("/captcha/*", captcha.Server(500, 250)) // Short paths - r.With(privateModeHandler...).With(cacheLoggedIn, a.cacheMiddleware).Get("/s/{id:[0-9a-fA-F]+}", a.redirectToLongPath) + r.With(a.privateModeHandler, cacheLoggedIn, a.cacheMiddleware).Get("/s/{id:[0-9a-fA-F]+}", a.redirectToLongPath) + // Blogs for blog, blogConfig := range a.cfg.Blogs { - sbm := middleware.WithValue(blogContextKey, blog) - - // Sections - r.Group(func(r chi.Router) { - r.Use(privateModeHandler...) - r.Use(a.cacheMiddleware, sbm) - for _, section := range blogConfig.Sections { - if section.Name != "" { - r.Group(func(r chi.Router) { - secPath := blogConfig.getRelativePath(section.Name) - r.Use(middleware.WithValue(indexConfigKey, &indexConfig{ - path: secPath, - section: section, - })) - r.Get(secPath, a.serveIndex) - r.Get(secPath+feedPath, a.serveIndex) - r.Get(secPath+paginationPath, a.serveIndex) - }) - } - } - }) - - // Taxonomies - r.Group(func(r chi.Router) { - r.Use(privateModeHandler...) - r.Use(a.cacheMiddleware, sbm) - for _, taxonomy := range blogConfig.Taxonomies { - if taxonomy.Name != "" { - r.Group(func(r chi.Router) { - r.Use(middleware.WithValue(taxonomyContextKey, taxonomy)) - taxBasePath := blogConfig.getRelativePath(taxonomy.Name) - r.Get(taxBasePath, a.serveTaxonomy) - taxValPath := taxBasePath + "/{taxValue}" - r.Get(taxValPath, a.serveTaxonomyValue) - r.Get(taxValPath+feedPath, a.serveTaxonomyValue) - r.Get(taxValPath+paginationPath, a.serveTaxonomyValue) - }) - } - } - }) - - // Photos - if pc := blogConfig.Photos; pc != nil && pc.Enabled { - r.Group(func(r chi.Router) { - photoPath := blogConfig.getRelativePath(defaultIfEmpty(pc.Path, defaultPhotosPath)) - r.Use(privateModeHandler...) - r.Use(a.cacheMiddleware, sbm, middleware.WithValue(indexConfigKey, &indexConfig{ - path: photoPath, - parameter: pc.Parameter, - title: pc.Title, - description: pc.Description, - summaryTemplate: templatePhotosSummary, - })) - r.Get(photoPath, a.serveIndex) - r.Get(photoPath+feedPath, a.serveIndex) - r.Get(photoPath+paginationPath, a.serveIndex) - }) - } - - // Search - if bsc := blogConfig.Search; bsc != nil && bsc.Enabled { - searchPath := blogConfig.getRelativePath(defaultIfEmpty(bsc.Path, defaultSearchPath)) - r.Route(searchPath, func(r chi.Router) { - r.Use(sbm, middleware.WithValue( - pathContextKey, - searchPath, - )) - r.Group(func(r chi.Router) { - r.Use(privateModeHandler...) - r.Use(a.cacheMiddleware) - r.Get("/", a.serveSearch) - r.Post("/", a.serveSearch) - searchResultPath := "/" + searchPlaceholder - r.Get(searchResultPath, a.serveSearchResult) - r.Get(searchResultPath+feedPath, a.serveSearchResult) - r.Get(searchResultPath+paginationPath, a.serveSearchResult) - }) - r.With(a.cacheMiddleware).Get("/opensearch.xml", a.serveOpenSearch) - }) - } - - // Stats - if bsc := blogConfig.BlogStats; bsc != nil && bsc.Enabled { - statsPath := blogConfig.getRelativePath(defaultIfEmpty(bsc.Path, defaultBlogStatsPath)) - r.Group(func(r chi.Router) { - r.Use(privateModeHandler...) - r.With(a.cacheMiddleware, sbm).Get(statsPath, a.serveBlogStats) - r.With(cacheLoggedIn, a.cacheMiddleware, sbm).Get(statsPath+".table.html", a.serveBlogStatsTable) - }) - } - - // Date archives - r.Group(func(r chi.Router) { - r.Use(privateModeHandler...) - r.Use(a.cacheMiddleware, sbm) - - yearRegex := `/{year:x|\d\d\d\d}` - monthRegex := `/{month:x|\d\d}` - dayRegex := `/{day:\d\d}` - - yearPath := blogConfig.getRelativePath(yearRegex) - r.Get(yearPath, a.serveDate) - r.Get(yearPath+feedPath, a.serveDate) - r.Get(yearPath+paginationPath, a.serveDate) - - monthPath := yearPath + monthRegex - r.Get(monthPath, a.serveDate) - r.Get(monthPath+feedPath, a.serveDate) - r.Get(monthPath+paginationPath, a.serveDate) - - dayPath := monthPath + dayRegex - r.Get(dayPath, a.serveDate) - r.Get(dayPath+feedPath, a.serveDate) - r.Get(dayPath+paginationPath, a.serveDate) - }) - - // Blog - if !blogConfig.PostAsHome { - r.Group(func(r chi.Router) { - r.Use(privateModeHandler...) - r.Use(sbm) - r.With(a.checkActivityStreamsRequest, a.cacheMiddleware).Get(blogConfig.getRelativePath(""), a.serveHome) - r.With(a.cacheMiddleware).Get(blogConfig.getRelativePath("")+feedPath, a.serveHome) - r.With(a.cacheMiddleware).Get(blogConfig.getRelativePath(paginationPath), a.serveHome) - }) - } - - // Custom pages - r.Group(func(r chi.Router) { - r.Use(privateModeHandler...) - r.Use(sbm) - for _, cp := range blogConfig.CustomPages { - r.Group(func(r chi.Router) { - scp := middleware.WithValue(customPageContextKey, cp) - if cp.Cache { - ce := cp.CacheExpiration - if ce == 0 { - ce = a.defaultCacheExpiration() - } - r.With( - a.cacheMiddleware, - middleware.WithValue(cacheExpirationKey, ce), - scp, - ).Get(cp.Path, a.serveCustomPage) - } else { - r.With(scp).Get(cp.Path, a.serveCustomPage) - } - }) - } - }) - - // Random post - if rp := blogConfig.RandomPost; rp != nil && rp.Enabled { - r.With(privateModeHandler...).With(sbm).Get(blogConfig.getRelativePath(defaultIfEmpty(rp.Path, "/random")), a.redirectToRandomPost) - } - - // Editor - r.Route(blogConfig.getRelativePath("/editor"), func(r chi.Router) { - r.Use(sbm, a.authMiddleware) - r.Get("/", a.serveEditor) - r.Post("/", a.serveEditorPost) - r.Get("/files", a.serveEditorFiles) - r.Post("/files/view", a.serveEditorFilesView) - r.Post("/files/delete", a.serveEditorFilesDelete) - r.Get("/drafts", a.serveDrafts) - r.Get("/drafts"+feedPath, a.serveDrafts) - r.Get("/drafts"+paginationPath, a.serveDrafts) - r.Get("/private", a.servePrivate) - r.Get("/private"+feedPath, a.servePrivate) - r.Get("/private"+paginationPath, a.servePrivate) - r.Get("/unlisted", a.serveUnlisted) - r.Get("/unlisted"+feedPath, a.serveUnlisted) - r.Get("/unlisted"+paginationPath, a.serveUnlisted) - }) - - // Comments - if commentsConfig := blogConfig.Comments; commentsConfig != nil && commentsConfig.Enabled { - commentsPath := blogConfig.getRelativePath("/comment") - r.Route(commentsPath, func(r chi.Router) { - r.Use(sbm, middleware.WithValue(pathContextKey, commentsPath)) - r.Use(privateModeHandler...) - r.With(a.cacheMiddleware, noIndexHeader).Get("/{id:[0-9]+}", a.serveComment) - r.With(a.captchaMiddleware).Post("/", a.createComment) - r.Group(func(r chi.Router) { - // Admin - r.Use(a.authMiddleware) - r.Get("/", a.commentsAdmin) - r.Get(paginationPath, a.commentsAdmin) - r.Post("/delete", a.commentsAdminDelete) - }) - }) - } - - // Blogroll - if brConfig := blogConfig.Blogroll; brConfig != nil && brConfig.Enabled { - brPath := blogConfig.getRelativePath(defaultIfEmpty(brConfig.Path, defaultBlogrollPath)) - r.Group(func(r chi.Router) { - r.Use(privateModeHandler...) - r.Use(middleware.WithValue(cacheExpirationKey, a.defaultCacheExpiration())) - r.Use(a.cacheMiddleware, sbm) - r.Get(brPath, a.serveBlogroll) - r.Get(brPath+".opml", a.serveBlogrollExport) - }) - } - - // Geo map - if mc := blogConfig.Map; mc != nil && mc.Enabled { - mapPath := blogConfig.getRelativePath(defaultIfEmpty(mc.Path, defaultGeoMapPath)) - r.Route(mapPath, func(r chi.Router) { - r.Use(privateModeHandler...) - r.Group(func(r chi.Router) { - r.With(a.cacheMiddleware, sbm).Get("/", a.serveGeoMap) - r.With(cacheLoggedIn, a.cacheMiddleware).HandleFunc("/leaflet/*", a.serveLeaflet(mapPath+"/")) - }) - r.Get("/tiles/{z}/{x}/{y}.png", a.proxyTiles(mapPath+"/tiles")) - }) - } - - // Contact - if cc := blogConfig.Contact; cc != nil && cc.Enabled { - contactPath := blogConfig.getRelativePath(defaultIfEmpty(cc.Path, defaultContactPath)) - r.Route(contactPath, func(r chi.Router) { - r.Use(privateModeHandler...) - r.Use(a.cacheMiddleware, sbm) - r.Get("/", a.serveContactForm) - r.With(a.captchaMiddleware).Post("/", a.sendContactSubmission) - }) - } - + r.Group(a.blogRouter(blog, blogConfig)) } // Sitemap - r.With(privateModeHandler...).With(cacheLoggedIn, a.cacheMiddleware).Get(sitemapPath, a.serveSitemap) + r.With(a.privateModeHandler, cacheLoggedIn, a.cacheMiddleware).Get(sitemapPath, a.serveSitemap) - // Robots.txt - doesn't need cache, because it's too simple - if !privateMode { - r.Get("/robots.txt", a.serveRobotsTXT) - } else { - r.Get("/robots.txt", servePrivateRobotsTXT) - } + // Robots.txt + r.With(cacheLoggedIn, a.cacheMiddleware).Get(robotsTXTPath, a.serveRobotsTXT) - r.NotFound(a.servePostsAliasesRedirects(privateModeHandler...)) + r.NotFound(a.servePostsAliasesRedirects()) r.MethodNotAllowed(a.serveNotAllowed) return r, nil } -func (a *goBlog) servePostsAliasesRedirects(pmh ...func(http.Handler) http.Handler) http.HandlerFunc { +func (a *goBlog) servePostsAliasesRedirects() http.HandlerFunc { // Private mode - alicePrivate := alice.New() - for _, h := range pmh { - alicePrivate = alicePrivate.Append(h) - } + alicePrivate := alice.New(a.privateModeHandler) // Return handler func return func(w http.ResponseWriter, r *http.Request) { // Only allow GET requests @@ -550,53 +263,3 @@ func (a *goBlog) servePostsAliasesRedirects(pmh ...func(http.Handler) http.Handl alice.New(a.cacheMiddleware, a.checkRegexRedirects).ThenFunc(a.serve404).ServeHTTP(w, r) } } - -const blogContextKey contextKey = "blog" -const pathContextKey contextKey = "httpPath" - -func (a *goBlog) refreshCSPDomains() { - var cspBuilder strings.Builder - if mp := a.cfg.Micropub.MediaStorage; mp != nil && mp.MediaURL != "" { - if u, err := url.Parse(mp.MediaURL); err == nil { - cspBuilder.WriteByte(' ') - cspBuilder.WriteString(u.Hostname()) - } - } - if len(a.cfg.Server.CSPDomains) > 0 { - cspBuilder.WriteByte(' ') - cspBuilder.WriteString(strings.Join(a.cfg.Server.CSPDomains, " ")) - } - a.cspDomains = cspBuilder.String() -} - -const cspHeader = "Content-Security-Policy" - -func (a *goBlog) securityHeaders(next http.Handler) http.Handler { - a.refreshCSPDomains() - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Strict-Transport-Security", "max-age=31536000;") - w.Header().Set("Referrer-Policy", "no-referrer") - w.Header().Set("X-Content-Type-Options", "nosniff") - w.Header().Set("X-Frame-Options", "SAMEORIGIN") - w.Header().Set("X-Xss-Protection", "1; mode=block") - w.Header().Set(cspHeader, "default-src 'self'"+a.cspDomains) - if a.cfg.Server.Tor && a.torAddress != "" { - w.Header().Set("Onion-Location", fmt.Sprintf("http://%v%v", a.torAddress, r.RequestURI)) - } - next.ServeHTTP(w, r) - }) -} - -func noIndexHeader(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("X-Robots-Tag", "noindex") - next.ServeHTTP(w, r) - }) -} - -func fixHTTPHandler(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - r.URL.RawPath = "" - next.ServeHTTP(w, r) - }) -} diff --git a/httpMiddlewares.go b/httpMiddlewares.go new file mode 100644 index 0000000..40689f8 --- /dev/null +++ b/httpMiddlewares.go @@ -0,0 +1,51 @@ +package main + +import ( + "fmt" + "net/http" + "net/url" + "strings" +) + +func noIndexHeader(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Robots-Tag", "noindex") + next.ServeHTTP(w, r) + }) +} + +func fixHTTPHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r.URL.RawPath = "" + next.ServeHTTP(w, r) + }) +} + +func (a *goBlog) securityHeaders(next http.Handler) http.Handler { + // Build CSP domains list + var cspBuilder strings.Builder + if mp := a.cfg.Micropub.MediaStorage; mp != nil && mp.MediaURL != "" { + if u, err := url.Parse(mp.MediaURL); err == nil { + cspBuilder.WriteByte(' ') + cspBuilder.WriteString(u.Hostname()) + } + } + if len(a.cfg.Server.CSPDomains) > 0 { + cspBuilder.WriteByte(' ') + cspBuilder.WriteString(strings.Join(a.cfg.Server.CSPDomains, " ")) + } + cspDomains := cspBuilder.String() + // Return handler + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Strict-Transport-Security", "max-age=31536000;") + w.Header().Set("Referrer-Policy", "no-referrer") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-Frame-Options", "SAMEORIGIN") + w.Header().Set("X-Xss-Protection", "1; mode=block") + w.Header().Set("Content-Security-Policy", "default-src 'self'"+cspDomains) + if a.cfg.Server.Tor && a.torAddress != "" { + w.Header().Set("Onion-Location", fmt.Sprintf("http://%v%v", a.torAddress, r.RequestURI)) + } + next.ServeHTTP(w, r) + }) +} diff --git a/httpRouters.go b/httpRouters.go new file mode 100644 index 0000000..299b130 --- /dev/null +++ b/httpRouters.go @@ -0,0 +1,414 @@ +package main + +import ( + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" +) + +// Login +func (a *goBlog) loginRouter(r chi.Router) { + r.Use(a.authMiddleware) + r.Get("/login", serveLogin) + r.Get("/logout", a.serveLogout) +} + +// Micropub +func (a *goBlog) micropubRouter(r chi.Router) { + r.Use(a.checkIndieAuth) + r.Get("/", a.serveMicropubQuery) + r.Post("/", a.serveMicropubPost) + r.Post(micropubMediaSubPath, a.serveMicropubMedia) +} + +// IndieAuth +func (a *goBlog) indieAuthRouter(r chi.Router) { + r.Get("/", a.indieAuthRequest) + r.With(a.authMiddleware).Post("/accept", a.indieAuthAccept) + r.Post("/", a.indieAuthVerification) + r.Get("/token", a.indieAuthToken) + r.Post("/token", a.indieAuthToken) +} + +// ActivityPub +func (a *goBlog) activityPubRouter(r chi.Router) { + if a.isPrivate() { + // Private mode, no ActivityPub + return + } + if ap := a.cfg.ActivityPub; ap != nil && ap.Enabled { + r.Route("/activitypub", func(r chi.Router) { + r.Post("/inbox/{blog}", a.apHandleInbox) + r.Post("/{blog}/inbox", a.apHandleInbox) + }) + r.Group(func(r chi.Router) { + r.Use(cacheLoggedIn, a.cacheMiddleware) + r.Get("/.well-known/webfinger", a.apHandleWebfinger) + r.Get("/.well-known/host-meta", handleWellKnownHostMeta) + r.Get("/.well-known/nodeinfo", a.serveNodeInfoDiscover) + r.Get("/nodeinfo", a.serveNodeInfo) + }) + } +} + +// Webmentions +func (a *goBlog) webmentionsRouter(r chi.Router) { + if wm := a.cfg.Webmention; wm != nil && !wm.DisableReceiving { + // Endpoint + r.Post("/", a.handleWebmention) + // Authenticated routes + r.Group(func(r chi.Router) { + r.Use(a.authMiddleware) + r.Get("/", a.webmentionAdmin) + r.Get(paginationPath, a.webmentionAdmin) + r.Post("/delete", a.webmentionAdminDelete) + r.Post("/approve", a.webmentionAdminApprove) + r.Post("/reverify", a.webmentionAdminReverify) + }) + } +} + +// Notifications +func (a *goBlog) notificationsRouter(r chi.Router) { + r.Use(a.authMiddleware) + r.Get("/", a.notificationsAdmin) + r.Get(paginationPath, a.notificationsAdmin) + r.Post("/delete", a.notificationsAdminDelete) +} + +// Assets +func (a *goBlog) assetsRouter(r chi.Router) { + for _, path := range a.allAssetPaths() { + r.Get(path, a.serveAsset) + } +} + +// Static files +func (a *goBlog) staticFilesRouter(r chi.Router) { + r.Use(a.privateModeHandler) + for _, path := range allStaticPaths() { + r.Get(path, a.serveStaticFile) + } +} + +// Blog +func (a *goBlog) blogRouter(blog string, conf *configBlog) func(r chi.Router) { + return func(r chi.Router) { + + // Set blog + r.Use(middleware.WithValue(blogKey, blog)) + + // Home + r.Group(a.blogHomeRouter(conf)) + + // Sections + r.Group(a.blogSectionsRouter(conf)) + + // Taxonomies + r.Group(a.blogTaxonomiesRouter(conf)) + + // Dates + r.Group(a.blogDatesRouter(conf)) + + // Photos + r.Group(a.blogPhotosRouter(conf)) + + // Search + r.Group(a.blogSearchRouter(conf)) + + // Custom pages + r.Group(a.blogCustomPagesRouter(conf)) + + // Random post + r.Group(a.blogRandomRouter(conf)) + + // Editor + r.Route(conf.getRelativePath(editorPath), a.blogEditorRouter(conf)) + + // Comments + r.Group(a.blogCommentsRouter(conf)) + + // Stats + r.Group(a.blogStatsRouter(conf)) + + // Blogroll + r.Group(a.blogBlogrollRouter(conf)) + + // Geo map + r.Group(a.blogGeoMapRouter(conf)) + + // Contact + r.Group(a.blogContactRouter(conf)) + } +} + +// Blog - Home +func (a *goBlog) blogHomeRouter(conf *configBlog) func(r chi.Router) { + return func(r chi.Router) { + if !conf.PostAsHome { + r.Use(a.privateModeHandler) + r.With(a.checkActivityStreamsRequest, a.cacheMiddleware).Get(conf.getRelativePath(""), a.serveHome) + r.With(a.cacheMiddleware).Get(conf.getRelativePath("")+feedPath, a.serveHome) + r.With(a.cacheMiddleware).Get(conf.getRelativePath(paginationPath), a.serveHome) + } + } +} + +// Blog - Sections +func (a *goBlog) blogSectionsRouter(conf *configBlog) func(r chi.Router) { + return func(r chi.Router) { + r.Use( + a.privateModeHandler, + a.cacheMiddleware, + ) + for _, section := range conf.Sections { + if section.Name != "" { + r.Group(func(r chi.Router) { + secPath := conf.getRelativePath(section.Name) + r.Use(middleware.WithValue(indexConfigKey, &indexConfig{ + path: secPath, + section: section, + })) + r.Get(secPath, a.serveIndex) + r.Get(secPath+feedPath, a.serveIndex) + r.Get(secPath+paginationPath, a.serveIndex) + }) + } + } + } +} + +// Blog - Taxonomies +func (a *goBlog) blogTaxonomiesRouter(conf *configBlog) func(r chi.Router) { + return func(r chi.Router) { + r.Use( + a.privateModeHandler, + a.cacheMiddleware, + ) + for _, taxonomy := range conf.Taxonomies { + if taxonomy.Name != "" { + r.Group(func(r chi.Router) { + r.Use(middleware.WithValue(taxonomyContextKey, taxonomy)) + taxBasePath := conf.getRelativePath(taxonomy.Name) + r.Get(taxBasePath, a.serveTaxonomy) + taxValPath := taxBasePath + "/{taxValue}" + r.Get(taxValPath, a.serveTaxonomyValue) + r.Get(taxValPath+feedPath, a.serveTaxonomyValue) + r.Get(taxValPath+paginationPath, a.serveTaxonomyValue) + }) + } + } + } +} + +// Blog - Dates +func (a *goBlog) blogDatesRouter(conf *configBlog) func(r chi.Router) { + return func(r chi.Router) { + r.Use( + a.privateModeHandler, + a.cacheMiddleware, + ) + + yearPath := conf.getRelativePath(`/{year:x|\d\d\d\d}`) + r.Get(yearPath, a.serveDate) + r.Get(yearPath+feedPath, a.serveDate) + r.Get(yearPath+paginationPath, a.serveDate) + + monthPath := yearPath + `/{month:x|\d\d}` + r.Get(monthPath, a.serveDate) + r.Get(monthPath+feedPath, a.serveDate) + r.Get(monthPath+paginationPath, a.serveDate) + + dayPath := monthPath + `/{day:\d\d}` + r.Get(dayPath, a.serveDate) + r.Get(dayPath+feedPath, a.serveDate) + r.Get(dayPath+paginationPath, a.serveDate) + } +} + +// Blog - Photos +func (a *goBlog) blogPhotosRouter(conf *configBlog) func(r chi.Router) { + return func(r chi.Router) { + if pc := conf.Photos; pc != nil && pc.Enabled { + photoPath := conf.getRelativePath(defaultIfEmpty(pc.Path, defaultPhotosPath)) + r.Use( + a.privateModeHandler, + a.cacheMiddleware, + middleware.WithValue(indexConfigKey, &indexConfig{ + path: photoPath, + parameter: pc.Parameter, + title: pc.Title, + description: pc.Description, + summaryTemplate: templatePhotosSummary, + }), + ) + r.Get(photoPath, a.serveIndex) + r.Get(photoPath+feedPath, a.serveIndex) + r.Get(photoPath+paginationPath, a.serveIndex) + } + } +} + +// Blog - Search +func (a *goBlog) blogSearchRouter(conf *configBlog) func(r chi.Router) { + return func(r chi.Router) { + if bsc := conf.Search; bsc != nil && bsc.Enabled { + searchPath := conf.getRelativePath(defaultIfEmpty(bsc.Path, defaultSearchPath)) + r.Route(searchPath, func(r chi.Router) { + r.Group(func(r chi.Router) { + r.Use( + a.privateModeHandler, + a.cacheMiddleware, + middleware.WithValue(pathKey, searchPath), + ) + r.Get("/", a.serveSearch) + r.Post("/", a.serveSearch) + searchResultPath := "/" + searchPlaceholder + r.Get(searchResultPath, a.serveSearchResult) + r.Get(searchResultPath+feedPath, a.serveSearchResult) + r.Get(searchResultPath+paginationPath, a.serveSearchResult) + }) + r.With( + // No private mode, to allow using OpenSearch in browser + a.cacheMiddleware, + middleware.WithValue(pathKey, searchPath), + ).Get("/opensearch.xml", a.serveOpenSearch) + }) + } + } +} + +// Blog - Custom pages +func (a *goBlog) blogCustomPagesRouter(conf *configBlog) func(r chi.Router) { + return func(r chi.Router) { + r.Use(a.privateModeHandler) + for _, cp := range conf.CustomPages { + r.Group(func(r chi.Router) { + r.Use(middleware.WithValue(customPageContextKey, cp)) + if cp.Cache { + ce := cp.CacheExpiration + if ce == 0 { + ce = a.defaultCacheExpiration() + } + r.Use( + a.cacheMiddleware, + middleware.WithValue(cacheExpirationKey, ce), + ) + } + r.Get(cp.Path, a.serveCustomPage) + }) + } + } +} + +// Blog - Random +func (a *goBlog) blogRandomRouter(conf *configBlog) func(r chi.Router) { + return func(r chi.Router) { + if rp := conf.RandomPost; rp != nil && rp.Enabled { + r.With(a.privateModeHandler).Get(conf.getRelativePath(defaultIfEmpty(rp.Path, "/random")), a.redirectToRandomPost) + } + } +} + +// Blog - Editor +func (a *goBlog) blogEditorRouter(conf *configBlog) func(r chi.Router) { + return func(r chi.Router) { + r.Use(a.authMiddleware) + r.Get("/", a.serveEditor) + r.Post("/", a.serveEditorPost) + r.Get("/files", a.serveEditorFiles) + r.Post("/files/view", a.serveEditorFilesView) + r.Post("/files/delete", a.serveEditorFilesDelete) + r.Get("/drafts", a.serveDrafts) + r.Get("/drafts"+feedPath, a.serveDrafts) + r.Get("/drafts"+paginationPath, a.serveDrafts) + r.Get("/private", a.servePrivate) + r.Get("/private"+feedPath, a.servePrivate) + r.Get("/private"+paginationPath, a.servePrivate) + r.Get("/unlisted", a.serveUnlisted) + r.Get("/unlisted"+feedPath, a.serveUnlisted) + r.Get("/unlisted"+paginationPath, a.serveUnlisted) + } +} + +// Blog - Comments +func (a *goBlog) blogCommentsRouter(conf *configBlog) func(r chi.Router) { + return func(r chi.Router) { + if commentsConfig := conf.Comments; commentsConfig != nil && commentsConfig.Enabled { + commentsPath := conf.getRelativePath("/comment") + r.Route(commentsPath, func(r chi.Router) { + r.Use( + a.privateModeHandler, + middleware.WithValue(pathKey, commentsPath), + ) + r.With(a.cacheMiddleware, noIndexHeader).Get("/{id:[0-9]+}", a.serveComment) + r.With(a.captchaMiddleware).Post("/", a.createComment) + r.Group(func(r chi.Router) { + // Admin + r.Use(a.authMiddleware) + r.Get("/", a.commentsAdmin) + r.Get(paginationPath, a.commentsAdmin) + r.Post("/delete", a.commentsAdminDelete) + }) + }) + } + } +} + +// Blog - Stats +func (a *goBlog) blogStatsRouter(conf *configBlog) func(r chi.Router) { + return func(r chi.Router) { + if bsc := conf.BlogStats; bsc != nil && bsc.Enabled { + statsPath := conf.getRelativePath(defaultIfEmpty(bsc.Path, defaultBlogStatsPath)) + r.Use(a.privateModeHandler) + r.With(a.cacheMiddleware).Get(statsPath, a.serveBlogStats) + r.With(cacheLoggedIn, a.cacheMiddleware).Get(statsPath+blogStatsTablePath, a.serveBlogStatsTable) + } + } +} + +// Blog - Blogroll +func (a *goBlog) blogBlogrollRouter(conf *configBlog) func(r chi.Router) { + return func(r chi.Router) { + if brConfig := conf.Blogroll; brConfig != nil && brConfig.Enabled { + brPath := conf.getRelativePath(defaultIfEmpty(brConfig.Path, defaultBlogrollPath)) + r.Use( + a.privateModeHandler, + middleware.WithValue(cacheExpirationKey, a.defaultCacheExpiration()), + a.cacheMiddleware, + ) + r.Get(brPath, a.serveBlogroll) + r.Get(brPath+".opml", a.serveBlogrollExport) + } + } +} + +// Blog - Geo Map +func (a *goBlog) blogGeoMapRouter(conf *configBlog) func(r chi.Router) { + return func(r chi.Router) { + if mc := conf.Map; mc != nil && mc.Enabled { + mapPath := conf.getRelativePath(defaultIfEmpty(mc.Path, defaultGeoMapPath)) + r.Route(mapPath, func(r chi.Router) { + r.Use(a.privateModeHandler) + r.Group(func(r chi.Router) { + r.With(a.cacheMiddleware).Get("/", a.serveGeoMap) + r.With(cacheLoggedIn, a.cacheMiddleware).HandleFunc("/leaflet/*", a.serveLeaflet(mapPath+"/")) + }) + r.Get("/tiles/{z}/{x}/{y}.png", a.proxyTiles(mapPath+"/tiles")) + }) + } + } +} + +// Blog - Contact +func (a *goBlog) blogContactRouter(conf *configBlog) func(r chi.Router) { + return func(r chi.Router) { + if cc := conf.Contact; cc != nil && cc.Enabled { + contactPath := conf.getRelativePath(defaultIfEmpty(cc.Path, defaultContactPath)) + r.Route(contactPath, func(r chi.Router) { + r.Use(a.privateModeHandler, a.cacheMiddleware) + r.Get("/", a.serveContactForm) + r.With(a.captchaMiddleware).Post("/", a.sendContactSubmission) + }) + } + } +} diff --git a/micropubMedia.go b/micropubMedia.go index c04a845..05b542a 100644 --- a/micropubMedia.go +++ b/micropubMedia.go @@ -57,7 +57,7 @@ func (a *goBlog) serveMicropubMedia(w http.ResponseWriter, r *http.Request) { return } // Try to compress file (only when not in private mode) - if pm := a.cfg.PrivateMode; pm == nil || !pm.Enabled { + if !a.isPrivate() { compressedLocation, compressionErr := a.compressMediaFile(location) if compressionErr != nil { a.serveError(w, r, "failed to compress file: "+compressionErr.Error(), http.StatusInternalServerError) diff --git a/opensearch.go b/opensearch.go index 720ea24..dfa2ff2 100644 --- a/opensearch.go +++ b/opensearch.go @@ -8,7 +8,7 @@ import ( ) func (a *goBlog) serveOpenSearch(w http.ResponseWriter, r *http.Request) { - blog := r.Context().Value(blogContextKey).(string) + blog := r.Context().Value(blogKey).(string) b := a.cfg.Blogs[blog] title := b.Title sURL := a.getFullAddress(b.getRelativePath(defaultIfEmpty(b.Search.Path, defaultSearchPath))) diff --git a/posts.go b/posts.go index 1cf7da6..7a0736f 100644 --- a/posts.go +++ b/posts.go @@ -78,7 +78,7 @@ func (a *goBlog) servePost(w http.ResponseWriter, r *http.Request) { } func (a *goBlog) redirectToRandomPost(rw http.ResponseWriter, r *http.Request) { - randomPath, err := a.getRandomPostPath(r.Context().Value(blogContextKey).(string)) + randomPath, err := a.getRandomPostPath(r.Context().Value(blogKey).(string)) if err != nil { a.serveError(rw, r, err.Error(), http.StatusInternalServerError) return @@ -111,7 +111,7 @@ func (p *postPaginationAdapter) Slice(offset, length int, data interface{}) erro } func (a *goBlog) serveHome(w http.ResponseWriter, r *http.Request) { - blog := r.Context().Value(blogContextKey).(string) + blog := r.Context().Value(blogKey).(string) if asRequest, ok := r.Context().Value(asRequestKey).(bool); ok && asRequest { a.serveActivityStreams(blog, w, r) return @@ -122,7 +122,7 @@ func (a *goBlog) serveHome(w http.ResponseWriter, r *http.Request) { } func (a *goBlog) serveDrafts(w http.ResponseWriter, r *http.Request) { - blog := r.Context().Value(blogContextKey).(string) + blog := r.Context().Value(blogKey).(string) a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{ path: a.getRelativePath(blog, "/editor/drafts"), title: a.ts.GetTemplateStringVariant(a.cfg.Blogs[blog].Lang, "drafts"), @@ -131,7 +131,7 @@ func (a *goBlog) serveDrafts(w http.ResponseWriter, r *http.Request) { } func (a *goBlog) servePrivate(w http.ResponseWriter, r *http.Request) { - blog := r.Context().Value(blogContextKey).(string) + blog := r.Context().Value(blogKey).(string) a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{ path: a.getRelativePath(blog, "/editor/private"), title: a.ts.GetTemplateStringVariant(a.cfg.Blogs[blog].Lang, "privateposts"), @@ -140,7 +140,7 @@ func (a *goBlog) servePrivate(w http.ResponseWriter, r *http.Request) { } func (a *goBlog) serveUnlisted(w http.ResponseWriter, r *http.Request) { - blog := r.Context().Value(blogContextKey).(string) + blog := r.Context().Value(blogKey).(string) a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{ path: a.getRelativePath(blog, "/editor/unlisted"), title: a.ts.GetTemplateStringVariant(a.cfg.Blogs[blog].Lang, "unlistedposts"), @@ -184,7 +184,7 @@ func (a *goBlog) serveDate(w http.ResponseWriter, r *http.Request) { dPath.WriteString(fmt.Sprintf("/%02d", day)) } a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{ - path: a.getRelativePath(r.Context().Value(blogContextKey).(string), dPath.String()), + path: a.getRelativePath(r.Context().Value(blogKey).(string), dPath.String()), year: year, month: month, day: day, @@ -214,7 +214,7 @@ func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) { ic := r.Context().Value(indexConfigKey).(*indexConfig) blog := ic.blog if blog == "" { - blog, _ = r.Context().Value(blogContextKey).(string) + blog, _ = r.Context().Value(blogKey).(string) } search := chi.URLParam(r, "search") if search != "" { diff --git a/postsDb.go b/postsDb.go index 8d465dd..3fbca98 100644 --- a/postsDb.go +++ b/postsDb.go @@ -79,7 +79,7 @@ func (a *goBlog) checkPost(p *post) (err error) { random := generateRandomString(5) p.Slug = fmt.Sprintf("%v-%02d-%02d-%v", now.Year(), int(now.Month()), now.Day(), random) } - published, _ := dateparse.ParseLocal(p.Published) + published := timeNoErr(dateparse.ParseLocal(p.Published)) pathTmplString := a.cfg.Blogs[p.Blog].Sections[p.Section].PathTemplate if pathTmplString == "" { return errors.New("path template empty") diff --git a/privateMode.go b/privateMode.go new file mode 100644 index 0000000..aeaa9cb --- /dev/null +++ b/privateMode.go @@ -0,0 +1,24 @@ +package main + +import ( + "net/http" + + "github.com/justinas/alice" +) + +func (a *goBlog) isPrivate() bool { + if pm := a.cfg.PrivateMode; pm != nil && pm.Enabled { + return true + } + return false +} + +func (a *goBlog) privateModeHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if a.isPrivate() { + alice.New(a.authMiddleware).Then(next).ServeHTTP(w, r) + } else { + next.ServeHTTP(w, r) + } + }) +} diff --git a/robotstxt.go b/robotstxt.go index 1ade68b..2a654fd 100644 --- a/robotstxt.go +++ b/robotstxt.go @@ -5,10 +5,12 @@ import ( "net/http" ) +const robotsTXTPath = "/robots.txt" + func (a *goBlog) serveRobotsTXT(w http.ResponseWriter, r *http.Request) { + if a.isPrivate() { + _, _ = w.Write([]byte("User-agent: *\nDisallow: /")) + return + } _, _ = w.Write([]byte(fmt.Sprintf("User-agent: *\nSitemap: %v", a.getFullAddress(sitemapPath)))) } - -func servePrivateRobotsTXT(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte("User-agent: *\nDisallow: /")) -} diff --git a/robotstxt_test.go b/robotstxt_test.go index 6f49caa..f79deb8 100644 --- a/robotstxt_test.go +++ b/robotstxt_test.go @@ -9,11 +9,6 @@ import ( func Test_robotsTXT(t *testing.T) { - h := http.HandlerFunc(servePrivateRobotsTXT) - assert.HTTPStatusCode(t, h, http.MethodGet, "", nil, 200) - txt := assert.HTTPBody(h, http.MethodGet, "", nil) - assert.Equal(t, "User-agent: *\nDisallow: /", txt) - app := &goBlog{ cfg: &config{ Server: &configServer{ @@ -22,9 +17,18 @@ func Test_robotsTXT(t *testing.T) { }, } + h := http.HandlerFunc(app.serveRobotsTXT) + assert.HTTPStatusCode(t, h, http.MethodGet, "", nil, 200) + txt := assert.HTTPBody(h, http.MethodGet, "", nil) + assert.Equal(t, "User-agent: *\nSitemap: https://example.com/sitemap.xml", txt) + + app.cfg.PrivateMode = &configPrivateMode{ + Enabled: true, + } + h = http.HandlerFunc(app.serveRobotsTXT) assert.HTTPStatusCode(t, h, http.MethodGet, "", nil, 200) txt = assert.HTTPBody(h, http.MethodGet, "", nil) - assert.Equal(t, "User-agent: *\nSitemap: https://example.com/sitemap.xml", txt) + assert.Equal(t, "User-agent: *\nDisallow: /", txt) } diff --git a/search.go b/search.go index 4fb146d..bb761df 100644 --- a/search.go +++ b/search.go @@ -15,8 +15,8 @@ const defaultSearchPath = "/search" const searchPlaceholder = "{search}" func (a *goBlog) serveSearch(w http.ResponseWriter, r *http.Request) { - blog := r.Context().Value(blogContextKey).(string) - servePath := r.Context().Value(pathContextKey).(string) + blog := r.Context().Value(blogKey).(string) + servePath := r.Context().Value(pathKey).(string) err := r.ParseForm() if err != nil { a.serveError(w, r, err.Error(), http.StatusBadRequest) @@ -37,7 +37,7 @@ func (a *goBlog) serveSearch(w http.ResponseWriter, r *http.Request) { func (a *goBlog) serveSearchResult(w http.ResponseWriter, r *http.Request) { a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{ - path: r.Context().Value(pathContextKey).(string) + "/" + searchPlaceholder, + path: r.Context().Value(pathKey).(string) + "/" + searchPlaceholder, }))) } diff --git a/sessions.go b/sessions.go index bf8e523..9ad67ad 100644 --- a/sessions.go +++ b/sessions.go @@ -114,15 +114,12 @@ func (s *dbSessionStore) load(session *sessions.Session) (err error) { if err = row.Scan(&data, &createdStr, &modifiedStr, &expiresStr); err != nil { return err } - created, _ := dateparse.ParseLocal(createdStr) - modified, _ := dateparse.ParseLocal(modifiedStr) - expires, _ := dateparse.ParseLocal(expiresStr) if err = securecookie.DecodeMulti(session.Name(), data, &session.Values, s.codecs...); err != nil { return err } - session.Values[sessionCreatedOn] = created - session.Values[sessionModifiedOn] = modified - session.Values[sessionExpiresOn] = expires + session.Values[sessionCreatedOn] = timeNoErr(dateparse.ParseLocal(createdStr)) + session.Values[sessionModifiedOn] = timeNoErr(dateparse.ParseLocal(modifiedStr)) + session.Values[sessionExpiresOn] = timeNoErr(dateparse.ParseLocal(expiresStr)) return nil } diff --git a/sitemap.go b/sitemap.go index 4d6ecf9..eb04e23 100644 --- a/sitemap.go +++ b/sitemap.go @@ -143,16 +143,16 @@ func (a *goBlog) serveSitemap(w http.ResponseWriter, r *http.Request) { }) } } - // Posts + // Published posts if posts, err := a.db.getPosts(&postsRequestConfig{status: statusPublished, withoutParameters: true}); err == nil { for _, p := range posts { item := &sitemap.URL{Loc: a.fullPostURL(p)} var lastMod time.Time if p.Updated != "" { - lastMod, _ = dateparse.ParseLocal(p.Updated) + lastMod = timeNoErr(dateparse.ParseLocal(p.Updated)) } if p.Published != "" && lastMod.IsZero() { - lastMod, _ = dateparse.ParseLocal(p.Published) + lastMod = timeNoErr(dateparse.ParseLocal(p.Published)) } if !lastMod.IsZero() { item.LastMod = &lastMod diff --git a/taxonomies.go b/taxonomies.go index 1868f51..1e8f570 100644 --- a/taxonomies.go +++ b/taxonomies.go @@ -13,7 +13,7 @@ import ( const taxonomyContextKey = "taxonomy" func (a *goBlog) serveTaxonomy(w http.ResponseWriter, r *http.Request) { - blog := r.Context().Value(blogContextKey).(string) + blog := r.Context().Value(blogKey).(string) tax := r.Context().Value(taxonomyContextKey).(*configTaxonomy) allValues, err := a.db.allTaxonomyValues(blog, tax.Name) if err != nil { @@ -31,7 +31,7 @@ func (a *goBlog) serveTaxonomy(w http.ResponseWriter, r *http.Request) { } func (a *goBlog) serveTaxonomyValue(w http.ResponseWriter, r *http.Request) { - blog := r.Context().Value(blogContextKey).(string) + blog := r.Context().Value(blogKey).(string) tax := r.Context().Value(taxonomyContextKey).(*configTaxonomy) taxValueParam := chi.URLParam(r, "taxValue") if taxValueParam == "" { diff --git a/templates/posttax.gohtml b/templates/posttax.gohtml index 0fe4dd8..53e6bb1 100644 --- a/templates/posttax.gohtml +++ b/templates/posttax.gohtml @@ -3,7 +3,7 @@ {{ $blog := .Blog }} {{ range $i, $tax := $blog.Taxonomies }} {{ $tvs := sort (ps $post $tax.Name) }} - {{ if gt (len $tvs) 0 }} + {{ if $tvs }}

{{ $tax.Title }}: {{ range $j, $tv := $tvs }} diff --git a/utils.go b/utils.go index acc4a3f..9351cf4 100644 --- a/utils.go +++ b/utils.go @@ -256,3 +256,7 @@ func containsStrings(s string, subStrings ...string) bool { } return false } + +func timeNoErr(t time.Time, _ error) time.Time { + return t +} diff --git a/webmentionSending.go b/webmentionSending.go index 14fea6e..5060c3b 100644 --- a/webmentionSending.go +++ b/webmentionSending.go @@ -49,7 +49,7 @@ func (a *goBlog) sendWebmentions(p *post) error { continue } // External mention - if pm := a.cfg.PrivateMode; pm != nil && pm.Enabled { + if a.isPrivate() { // Private mode, don't send external mentions continue }