Simplify HTTP routing

This commit is contained in:
Jan-Lukas Else 2021-07-17 09:33:44 +02:00
parent a933f47c7a
commit 86825ea601
10 changed files with 274 additions and 370 deletions

22
app.go
View File

@ -9,7 +9,6 @@ import (
shutdowner "git.jlel.se/jlelse/go-shutdowner"
ts "git.jlel.se/jlelse/template-strings"
ct "github.com/elnormous/contenttype"
"github.com/go-chi/chi/v5"
"github.com/go-fed/httpsig"
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
"github.com/yuin/goldmark"
@ -51,26 +50,7 @@ type goBlog struct {
// HTTP Client
httpClient httpClient
// HTTP Routers
d *dynamicHandler
privateMode bool
privateModeHandler []func(http.Handler) http.Handler
captchaHandler http.Handler
micropubRouter *chi.Mux
indieAuthRouter *chi.Mux
webmentionsRouter *chi.Mux
notificationsRouter *chi.Mux
activitypubRouter *chi.Mux
editorRouter *chi.Mux
commentsRouter *chi.Mux
searchRouter *chi.Mux
setBlogMiddlewares map[string]func(http.Handler) http.Handler
sectionMiddlewares map[string]func(http.Handler) http.Handler
taxonomyMiddlewares map[string]func(http.Handler) http.Handler
taxValueMiddlewares map[string]func(http.Handler) http.Handler
photosMiddlewares map[string]func(http.Handler) http.Handler
searchMiddlewares map[string]func(http.Handler) http.Handler
customPagesMiddlewares map[string]func(http.Handler) http.Handler
commentsMiddlewares map[string]func(http.Handler) http.Handler
d http.Handler
// Logs
logf *rotatelogs.RotateLogs
// Markdown

View File

@ -320,6 +320,7 @@ func (a *goBlog) initConfig() error {
}
// Check config for each blog
for _, blog := range a.cfg.Blogs {
// Blogroll
if br := blog.Blogroll; br != nil && br.Enabled && br.Opml == "" {
br.Enabled = false
}

View File

@ -62,6 +62,7 @@ func (a *goBlog) openDatabase(file string, logging bool) (*database, error) {
dbDriverName := generateRandomString(15)
var dr driver.Driver = &sqlite.SQLiteDriver{
ConnectHook: func(c *sqlite.SQLiteConn) error {
// Register functions
// Depends on app
if err := c.RegisterFunc("mdtext", a.renderText, true); err != nil {
return err
@ -79,6 +80,9 @@ func (a *goBlog) openDatabase(file string, logging bool) (*database, error) {
if err := c.RegisterFunc("charcount", charCount, true); err != nil {
return err
}
if err := c.RegisterFunc("urlize", urlize, true); err != nil {
return err
}
return nil
},
}

View File

@ -31,7 +31,7 @@ Requirements:
- Linux
- git
- go >= 1.16
- libsqlite3 >= 3.31 (the newer the better)
- libsqlite3 with FTS5 enabled >= 3.31 (the newer the better)
Build command:
@ -39,4 +39,12 @@ Build command:
git clone https://git.jlel.se/jlelse/GoBlog.git
cd GoBlog
go build -tags=linux,libsqlite3,sqlite_fts5 -o GoBlog
```
Alternatively you can also compile sqlite3 directly into GoBlog. This doesn't require libsqlite3, but takes more time.
```bash
git clone https://git.jlel.se/jlelse/GoBlog.git
cd GoBlog
go build -tags=linux,sqlite_fts5 -o GoBlog
```

5
go.mod
View File

@ -30,12 +30,13 @@ require (
// master
github.com/jlelse/feeds v1.2.1-0.20210704161900-189f94254ad4
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/justinas/alice v1.2.0
github.com/kaorimatz/go-opml v0.0.0-20210201121027-bc8e2852d7f9
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
github.com/lestrrat-go/strftime v1.0.5 // indirect
github.com/lib/pq v1.9.0 // indirect
github.com/lopezator/migrator v0.3.0
github.com/mattn/go-sqlite3 v1.14.7
github.com/mattn/go-sqlite3 v1.14.8
github.com/microcosm-cc/bluemonday v1.0.15
github.com/mitchellh/go-server-timing v1.0.1
github.com/paulmach/go.geojson v1.4.0
@ -54,7 +55,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-20210614182718-04defd469f4e
golang.org/x/net v0.0.0-20210716203947-853a461950ff
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

8
go.sum
View File

@ -254,6 +254,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo=
github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA=
github.com/kaorimatz/go-opml v0.0.0-20210201121027-bc8e2852d7f9 h1:+9REu9CK9D1AQ6C/PXXwGRcoKdT04cuHR5JgGD4DKqc=
github.com/kaorimatz/go-opml v0.0.0-20210201121027-bc8e2852d7f9/go.mod h1:OvY5ZBrAC9kOvM2PZs9Lw0BH+5K7tjrT6T7SFhn27OA=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@ -292,8 +294,9 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA=
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU=
github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/microcosm-cc/bluemonday v1.0.15 h1:J4uN+qPng9rvkBZBoBb8YGR+ijuklIMpSOZZLjYpbeY=
github.com/microcosm-cc/bluemonday v1.0.15/go.mod h1:ZLvAzeakRwrGnzQEvstVzVt3ZpqOF2+sdFr0Om+ce30=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
@ -490,8 +493,9 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
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 h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
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/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
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=

498
http.go
View File

@ -2,6 +2,8 @@ package main
import (
"compress/flate"
"database/sql"
"errors"
"fmt"
"log"
"net"
@ -9,12 +11,12 @@ import (
"net/url"
"strconv"
"strings"
"sync"
"time"
"github.com/dchest/captcha"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/justinas/alice"
servertiming "github.com/mitchellh/go-server-timing"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
@ -29,8 +31,12 @@ const (
func (a *goBlog) startServer() (err error) {
log.Println("Start server(s)...")
// Start
a.d = &dynamicHandler{}
// Load router
router, err := a.buildRouter()
if err != nil {
return err
}
a.d = fixHTTPHandler(router)
// Set basic middlewares
var finalHandler http.Handler = a.d
if a.cfg.Server.PublicHTTPS || a.cfg.Server.SecurityHeaders {
@ -43,14 +49,6 @@ func (a *goBlog) startServer() (err error) {
if a.cfg.Server.Logging {
finalHandler = a.logMiddleware(finalHandler)
}
// Create routers that don't change
if err = a.buildStaticHandlersRouters(); err != nil {
return err
}
// Load router
if err = a.reloadRouter(); err != nil {
return err
}
// Start Onion service
if a.cfg.Server.Tor {
go func() {
@ -126,93 +124,45 @@ func redirectToHttps(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, fmt.Sprintf("https://%s%s", requestHost, r.URL.RequestURI()), http.StatusMovedPermanently)
}
func (a *goBlog) reloadRouter() error {
h, err := a.buildDynamicRouter()
if err != nil {
return err
}
a.d.swapHandler(h)
a.cache.purge()
return nil
}
const (
paginationPath = "/page/{page:[0-9-]+}"
feedPath = ".{feed:rss|json|atom}"
)
func (a *goBlog) buildStaticHandlersRouters() error {
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 {
a.privateMode = true
a.privateModeHandler = append(a.privateModeHandler, a.authMiddleware)
} else {
a.privateMode = false
a.privateModeHandler = []func(http.Handler) http.Handler{}
privateMode = true
privateModeHandler = append(privateModeHandler, a.authMiddleware)
}
a.captchaHandler = captcha.Server(500, 250)
// Routers
editorRouter := chi.NewRouter()
editorRouter.Use(a.authMiddleware)
editorRouter.Get("/", a.serveEditor)
editorRouter.Post("/", a.serveEditorPost)
editorRouter.Get("/files", a.serveEditorFiles)
editorRouter.Post("/files/view", a.serveEditorFilesView)
editorRouter.Post("/files/delete", a.serveEditorFilesDelete)
editorRouter.Get("/drafts", a.serveDrafts)
editorRouter.Get("/drafts"+feedPath, a.serveDrafts)
editorRouter.Get("/drafts"+paginationPath, a.serveDrafts)
editorRouter.Get("/private", a.servePrivate)
editorRouter.Get("/private"+feedPath, a.servePrivate)
editorRouter.Get("/private"+paginationPath, a.servePrivate)
editorRouter.Get("/unlisted", a.serveUnlisted)
editorRouter.Get("/unlisted"+feedPath, a.serveUnlisted)
editorRouter.Get("/unlisted"+paginationPath, a.serveUnlisted)
a.micropubRouter = chi.NewRouter()
a.micropubRouter.Use(a.checkIndieAuth)
a.micropubRouter.Get("/", a.serveMicropubQuery)
a.micropubRouter.Post("/", a.serveMicropubPost)
a.micropubRouter.Post(micropubMediaSubPath, a.serveMicropubMedia)
a.indieAuthRouter = chi.NewRouter()
a.indieAuthRouter.Get("/", a.indieAuthRequest)
a.indieAuthRouter.With(a.authMiddleware).Post("/accept", a.indieAuthAccept)
a.indieAuthRouter.Post("/", a.indieAuthVerification)
a.indieAuthRouter.Get("/token", a.indieAuthToken)
a.indieAuthRouter.Post("/token", a.indieAuthToken)
a.webmentionsRouter = chi.NewRouter()
if wm := a.cfg.Webmention; wm != nil && !wm.DisableReceiving {
a.webmentionsRouter.Post("/", a.handleWebmention)
a.webmentionsRouter.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)
})
}
a.notificationsRouter = chi.NewRouter()
a.notificationsRouter.Use(a.authMiddleware)
a.notificationsRouter.Get("/", a.notificationsAdmin)
a.notificationsRouter.Get(paginationPath, a.notificationsAdmin)
a.notificationsRouter.Post("/delete", a.notificationsAdminDelete)
if ap := a.cfg.ActivityPub; ap != nil && ap.Enabled {
a.activitypubRouter = chi.NewRouter()
a.activitypubRouter.Post("/inbox/{blog}", a.apHandleInbox)
a.activitypubRouter.Post("/{blog}/inbox", a.apHandleInbox)
}
a.editorRouter = chi.NewRouter()
a.editorRouter.Use(a.authMiddleware)
a.editorRouter.Get("/", a.serveEditor)
a.editorRouter.Post("/", a.serveEditorPost)
a.editorRouter.Get("/files", a.serveEditorFiles)
a.editorRouter.Post("/files/view", a.serveEditorFilesView)
a.editorRouter.Post("/files/delete", a.serveEditorFilesDelete)
a.editorRouter.Get("/drafts", a.serveDrafts)
a.editorRouter.Get("/drafts"+feedPath, a.serveDrafts)
a.editorRouter.Get("/drafts"+paginationPath, a.serveDrafts)
a.editorRouter.Get("/private", a.servePrivate)
a.editorRouter.Get("/private"+feedPath, a.servePrivate)
a.editorRouter.Get("/private"+paginationPath, a.servePrivate)
a.editorRouter.Get("/unlisted", a.serveUnlisted)
a.editorRouter.Get("/unlisted"+feedPath, a.serveUnlisted)
a.editorRouter.Get("/unlisted"+paginationPath, a.serveUnlisted)
a.commentsRouter = chi.NewRouter()
a.commentsRouter.Use(a.privateModeHandler...)
a.commentsRouter.With(a.cache.cacheMiddleware, noIndexHeader).Get("/{id:[0-9]+}", a.serveComment)
a.commentsRouter.With(a.captchaMiddleware).Post("/", a.createComment)
a.commentsRouter.Group(func(r chi.Router) {
commentsRouter := chi.NewRouter()
commentsRouter.Use(privateModeHandler...)
commentsRouter.With(a.cache.cacheMiddleware, noIndexHeader).Get("/{id:[0-9]+}", a.serveComment)
commentsRouter.With(a.captchaMiddleware).Post("/", a.createComment)
commentsRouter.Group(func(r chi.Router) {
// Admin
r.Use(a.authMiddleware)
r.Get("/", a.commentsAdmin)
@ -220,9 +170,9 @@ func (a *goBlog) buildStaticHandlersRouters() error {
r.Post("/delete", a.commentsAdminDelete)
})
a.searchRouter = chi.NewRouter()
a.searchRouter.Group(func(r chi.Router) {
r.Use(a.privateModeHandler...)
searchRouter := chi.NewRouter()
searchRouter.Group(func(r chi.Router) {
r.Use(privateModeHandler...)
r.Use(a.cache.cacheMiddleware)
r.Get("/", a.serveSearch)
r.Post("/", a.serveSearch)
@ -231,69 +181,7 @@ func (a *goBlog) buildStaticHandlersRouters() error {
r.Get(searchResultPath+feedPath, a.serveSearchResult)
r.Get(searchResultPath+paginationPath, a.serveSearchResult)
})
a.searchRouter.With(a.cache.cacheMiddleware).Get("/opensearch.xml", a.serveOpenSearch)
a.setBlogMiddlewares = map[string]func(http.Handler) http.Handler{}
a.sectionMiddlewares = map[string]func(http.Handler) http.Handler{}
a.taxonomyMiddlewares = map[string]func(http.Handler) http.Handler{}
a.taxValueMiddlewares = map[string]func(http.Handler) http.Handler{}
a.photosMiddlewares = map[string]func(http.Handler) http.Handler{}
a.searchMiddlewares = map[string]func(http.Handler) http.Handler{}
a.customPagesMiddlewares = map[string]func(http.Handler) http.Handler{}
a.commentsMiddlewares = map[string]func(http.Handler) http.Handler{}
for blog, blogConfig := range a.cfg.Blogs {
sbm := middleware.WithValue(blogContextKey, blog)
a.setBlogMiddlewares[blog] = sbm
for _, section := range blogConfig.Sections {
if section.Name != "" {
secPath := blogConfig.getRelativePath(section.Name)
a.sectionMiddlewares[secPath] = middleware.WithValue(indexConfigKey, &indexConfig{
path: secPath,
section: section,
})
}
}
for _, taxonomy := range blogConfig.Taxonomies {
if taxonomy.Name != "" {
taxPath := blogConfig.getRelativePath(taxonomy.Name)
a.taxonomyMiddlewares[taxPath] = middleware.WithValue(taxonomyContextKey, taxonomy)
}
}
if pc := blogConfig.Photos; pc != nil && pc.Enabled {
a.photosMiddlewares[blog] = middleware.WithValue(indexConfigKey, &indexConfig{
path: blogConfig.getRelativePath(defaultIfEmpty(pc.Path, defaultPhotosPath)),
parameter: pc.Parameter,
title: pc.Title,
description: pc.Description,
summaryTemplate: templatePhotosSummary,
})
}
if bsc := blogConfig.Search; bsc != nil && bsc.Enabled {
a.searchMiddlewares[blog] = middleware.WithValue(
pathContextKey,
blogConfig.getRelativePath(defaultIfEmpty(bsc.Path, defaultSearchPath)),
)
}
for _, cp := range blogConfig.CustomPages {
a.customPagesMiddlewares[cp.Path] = middleware.WithValue(customPageContextKey, cp)
}
if commentsConfig := blogConfig.Comments; commentsConfig != nil && commentsConfig.Enabled {
a.commentsMiddlewares[blog] = middleware.WithValue(pathContextKey, blogConfig.getRelativePath("/comment"))
}
}
return nil
}
func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
r := chi.NewRouter()
searchRouter.With(a.cache.cacheMiddleware).Get("/opensearch.xml", a.serveOpenSearch)
// Basic middleware
r.Use(a.redirectShortDomain)
@ -305,7 +193,7 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
}
// No Index Header
if a.privateMode {
if privateMode {
r.Use(noIndexHeader)
}
@ -319,14 +207,28 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
r.With(a.authMiddleware).Get("/logout", a.serveLogout)
// Micropub
r.Mount(micropubPath, a.micropubRouter)
r.Route(micropubPath, func(r chi.Router) {
r.Use(a.checkIndieAuth)
r.Get("/", a.serveMicropubQuery)
r.Post("/", a.serveMicropubPost)
r.Post(micropubMediaSubPath, a.serveMicropubMedia)
})
// IndieAuth
r.Mount("/indieauth", a.indieAuthRouter)
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)
})
// ActivityPub and stuff
if ap := a.cfg.ActivityPub; ap != nil && ap.Enabled {
r.Mount("/activitypub", a.activitypubRouter)
r.Route("/activitypub", func(r chi.Router) {
r.Post("/inbox/{blog}", a.apHandleInbox)
r.Post("/{blog}/inbox", a.apHandleInbox)
})
r.With(a.cache.cacheMiddleware).Get("/.well-known/webfinger", a.apHandleWebfinger)
r.With(a.cache.cacheMiddleware).Get("/.well-known/host-meta", handleWellKnownHostMeta)
r.With(a.cache.cacheMiddleware).Get("/.well-known/nodeinfo", a.serveNodeInfoDiscover)
@ -334,72 +236,27 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
}
// Webmentions
r.Mount(webmentionPath, a.webmentionsRouter)
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)
})
})
}
// Notifications
r.Mount(notificationsPath, a.notificationsRouter)
// Posts
pp, err := a.db.getPostPaths(statusPublished)
if err != nil {
return nil, err
}
r.Group(func(r chi.Router) {
r.Use(a.privateModeHandler...)
r.Use(a.checkActivityStreamsRequest, a.cache.cacheMiddleware)
for _, path := range pp {
r.Get(path, a.servePost)
}
})
// Unlisted posts
up, err := a.db.getPostPaths(statusUnlisted)
if err != nil {
return nil, err
}
r.Group(func(r chi.Router) {
r.Use(a.privateModeHandler...)
r.Use(a.checkActivityStreamsRequest, a.cache.cacheMiddleware)
for _, path := range up {
r.Get(path, a.servePost)
}
})
// Private posts
priv, err := a.db.getPostPaths(statusPrivate)
if err != nil {
return nil, err
}
r.Group(func(r chi.Router) {
r.Route(notificationsPath, func(r chi.Router) {
r.Use(a.authMiddleware)
for _, path := range priv {
r.Get(path, a.servePost)
}
})
// Draft posts
dp, err := a.db.getPostPaths(statusDraft)
if err != nil {
return nil, err
}
r.Group(func(r chi.Router) {
r.Use(a.authMiddleware)
for _, path := range dp {
r.Get(path, a.servePost)
}
})
// Post aliases
allPostAliases, err := a.db.allPostAliases()
if err != nil {
return nil, err
}
r.Group(func(r chi.Router) {
r.Use(a.privateModeHandler...)
r.Use(a.cache.cacheMiddleware)
for _, path := range allPostAliases {
r.Get(path, a.servePostAlias)
}
r.Get("/", a.notificationsAdmin)
r.Get(paginationPath, a.notificationsAdmin)
r.Post("/delete", a.notificationsAdminDelete)
})
// Assets
@ -409,30 +266,33 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
// Static files
for _, path := range allStaticPaths() {
r.Get(path, a.serveStaticFile)
r.With(privateModeHandler...).Get(path, a.serveStaticFile)
}
// Media files
r.With(a.privateModeHandler...).Get(`/m/{file:[0-9a-fA-F]+(\.[0-9a-zA-Z]+)?}`, a.serveMediaFile)
r.With(privateModeHandler...).Get(`/m/{file:[0-9a-fA-F]+(\.[0-9a-zA-Z]+)?}`, a.serveMediaFile)
// Captcha
r.Handle("/captcha/*", a.captchaHandler)
r.Handle("/captcha/*", captcha.Server(500, 250))
// Short paths
r.With(a.privateModeHandler...).With(a.cache.cacheMiddleware).Get("/s/{id:[0-9a-fA-F]+}", a.redirectToLongPath)
r.With(privateModeHandler...).With(a.cache.cacheMiddleware).Get("/s/{id:[0-9a-fA-F]+}", a.redirectToLongPath)
for blog, blogConfig := range a.cfg.Blogs {
sbm := a.setBlogMiddlewares[blog]
sbm := middleware.WithValue(blogContextKey, blog)
// Sections
r.Group(func(r chi.Router) {
r.Use(a.privateModeHandler...)
r.Use(privateModeHandler...)
r.Use(a.cache.cacheMiddleware, sbm)
for _, section := range blogConfig.Sections {
if section.Name != "" {
secPath := blogConfig.getRelativePath(section.Name)
r.Group(func(r chi.Router) {
r.Use(a.sectionMiddlewares[secPath])
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)
@ -442,43 +302,36 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
})
// Taxonomies
for _, taxonomy := range blogConfig.Taxonomies {
if taxonomy.Name != "" {
taxPath := blogConfig.getRelativePath(taxonomy.Name)
taxValues, err := a.db.allTaxonomyValues(blog, taxonomy.Name)
if err != nil {
return nil, err
r.Group(func(r chi.Router) {
r.Use(privateModeHandler...)
r.Use(a.cache.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)
})
}
r.Group(func(r chi.Router) {
r.Use(a.privateModeHandler...)
r.Use(a.cache.cacheMiddleware, sbm)
r.With(a.taxonomyMiddlewares[taxPath]).Get(taxPath, a.serveTaxonomy)
for _, tv := range taxValues {
r.Group(func(r chi.Router) {
vPath := taxPath + "/" + urlize(tv)
if _, ok := a.taxValueMiddlewares[vPath]; !ok {
a.taxValueMiddlewares[vPath] = middleware.WithValue(indexConfigKey, &indexConfig{
path: vPath,
tax: taxonomy,
taxValue: tv,
})
}
r.Use(a.taxValueMiddlewares[vPath])
r.Get(vPath, a.serveIndex)
r.Get(vPath+feedPath, a.serveIndex)
r.Get(vPath+paginationPath, a.serveIndex)
})
}
})
}
}
})
// Photos
if pc := blogConfig.Photos; pc != nil && pc.Enabled {
r.Group(func(r chi.Router) {
r.Use(a.privateModeHandler...)
r.Use(a.cache.cacheMiddleware, sbm, a.photosMiddlewares[blog])
photoPath := blogConfig.getRelativePath(defaultIfEmpty(pc.Path, defaultPhotosPath))
r.Use(privateModeHandler...)
r.Use(a.cache.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)
@ -487,14 +340,18 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
// Search
if bsc := blogConfig.Search; bsc != nil && bsc.Enabled {
r.With(sbm, a.searchMiddlewares[blog]).Mount(blogConfig.getRelativePath(defaultIfEmpty(bsc.Path, defaultSearchPath)), a.searchRouter)
searchPath := blogConfig.getRelativePath(defaultIfEmpty(bsc.Path, defaultSearchPath))
r.With(sbm, middleware.WithValue(
pathContextKey,
searchPath,
)).Mount(searchPath, searchRouter)
}
// Stats
if bsc := blogConfig.BlogStats; bsc != nil && bsc.Enabled {
statsPath := blogConfig.getRelativePath(defaultIfEmpty(bsc.Path, defaultBlogStatsPath))
r.Group(func(r chi.Router) {
r.Use(a.privateModeHandler...)
r.Use(privateModeHandler...)
r.Use(a.cache.cacheMiddleware, sbm)
r.Get(statsPath, a.serveBlogStats)
r.Get(statsPath+".table.html", a.serveBlogStatsTable)
@ -503,7 +360,7 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
// Date archives
r.Group(func(r chi.Router) {
r.Use(a.privateModeHandler...)
r.Use(privateModeHandler...)
r.Use(a.cache.cacheMiddleware, sbm)
yearRegex := `/{year:x|\d\d\d\d}`
@ -529,7 +386,7 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
// Blog
if !blogConfig.PostAsHome {
r.Group(func(r chi.Router) {
r.Use(a.privateModeHandler...)
r.Use(privateModeHandler...)
r.Use(sbm)
r.With(a.checkActivityStreamsRequest, a.cache.cacheMiddleware).Get(blogConfig.getRelativePath(""), a.serveHome)
r.With(a.cache.cacheMiddleware).Get(blogConfig.getRelativePath("")+feedPath, a.serveHome)
@ -539,32 +396,33 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
// Custom pages
for _, cp := range blogConfig.CustomPages {
scp := a.customPagesMiddlewares[cp.Path]
scp := middleware.WithValue(customPageContextKey, cp)
if cp.Cache {
r.With(a.privateModeHandler...).With(a.cache.cacheMiddleware, sbm, scp).Get(cp.Path, a.serveCustomPage)
r.With(privateModeHandler...).With(a.cache.cacheMiddleware, sbm, scp).Get(cp.Path, a.serveCustomPage)
} else {
r.With(a.privateModeHandler...).With(sbm, scp).Get(cp.Path, a.serveCustomPage)
r.With(privateModeHandler...).With(sbm, scp).Get(cp.Path, a.serveCustomPage)
}
}
// Random post
if rp := blogConfig.RandomPost; rp != nil && rp.Enabled {
r.With(a.privateModeHandler...).With(sbm).Get(blogConfig.getRelativePath(defaultIfEmpty(rp.Path, "/random")), a.redirectToRandomPost)
r.With(privateModeHandler...).With(sbm).Get(blogConfig.getRelativePath(defaultIfEmpty(rp.Path, "/random")), a.redirectToRandomPost)
}
// Editor
r.With(sbm).Mount(blogConfig.getRelativePath("/editor"), a.editorRouter)
r.With(sbm).Mount(blogConfig.getRelativePath("/editor"), editorRouter)
// Comments
if commentsConfig := blogConfig.Comments; commentsConfig != nil && commentsConfig.Enabled {
r.With(sbm, a.commentsMiddlewares[blog]).Mount(blogConfig.getRelativePath("/comment"), a.commentsRouter)
commentsPath := blogConfig.getRelativePath("/comment")
r.With(sbm, middleware.WithValue(pathContextKey, commentsPath)).Mount(commentsPath, commentsRouter)
}
// Blogroll
if brConfig := blogConfig.Blogroll; brConfig != nil && brConfig.Enabled {
brPath := blogConfig.getRelativePath(defaultIfEmpty(brConfig.Path, defaultBlogrollPath))
r.Group(func(r chi.Router) {
r.Use(a.privateModeHandler...)
r.Use(privateModeHandler...)
r.Use(a.cache.cacheMiddleware, sbm)
r.Get(brPath, a.serveBlogroll)
r.Get(brPath+".opml", a.serveBlogrollExport)
@ -573,29 +431,88 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
// Geo map
if mc := blogConfig.Map; mc != nil && mc.Enabled {
r.With(a.privateModeHandler...).With(a.cache.cacheMiddleware, sbm).Get(blogConfig.getRelativePath(defaultIfEmpty(mc.Path, defaultGeoMapPath)), a.serveGeoMap)
r.With(privateModeHandler...).With(a.cache.cacheMiddleware, sbm).Get(blogConfig.getRelativePath(defaultIfEmpty(mc.Path, defaultGeoMapPath)), a.serveGeoMap)
}
}
// Sitemap
r.With(a.privateModeHandler...).With(a.cache.cacheMiddleware).Get(sitemapPath, a.serveSitemap)
r.With(privateModeHandler...).With(a.cache.cacheMiddleware).Get(sitemapPath, a.serveSitemap)
// Robots.txt - doesn't need cache, because it's too simple
if !a.privateMode {
if !privateMode {
r.Get("/robots.txt", a.serveRobotsTXT)
} else {
r.Get("/robots.txt", servePrivateRobotsTXT)
}
// Check redirects, then serve 404
r.With(a.cache.cacheMiddleware, a.checkRegexRedirects).NotFound(a.serve404)
r.NotFound(a.servePostsAliasesRedirects(privateModeHandler...))
r.MethodNotAllowed(a.serveNotAllowed)
return r, nil
}
func (a *goBlog) servePostsAliasesRedirects(pmh ...func(http.Handler) http.Handler) http.HandlerFunc {
// Private mode
alicePrivate := alice.New()
for _, h := range pmh {
alicePrivate = alicePrivate.Append(h)
}
// Return handler func
return func(w http.ResponseWriter, r *http.Request) {
// Only allow GET requests
if r.Method != http.MethodGet {
a.serveNotAllowed(w, r)
return
}
// Check if post or alias
path := r.URL.Path
row, err := a.db.queryRow(`
select 'post', status from posts where path = @path
union all
select 'alias', path from post_parameters where parameter = 'aliases' and value = @path
limit 1
`, sql.Named("path", path))
if err != nil {
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return
}
var postAliasType, value string
err = row.Scan(&postAliasType, &value)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
// Error
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return
}
// No result, continue...
} else {
// Found post or alias
switch postAliasType {
case "post":
// Is post, check status
switch postStatus(value) {
case statusPublished, statusUnlisted:
alicePrivate.Append(a.checkActivityStreamsRequest, a.cache.cacheMiddleware).ThenFunc(a.servePost).ServeHTTP(w, r)
return
case statusDraft, statusPrivate:
alice.New(a.authMiddleware).ThenFunc(a.servePost).ServeHTTP(w, r)
return
}
case "alias":
// Is alias, redirect
alicePrivate.Append(a.cache.cacheMiddleware).ThenFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, value, http.StatusFound)
}).ServeHTTP(w, r)
return
}
}
// No post, check regex redirects or serve 404 error
alice.New(a.cache.cacheMiddleware, a.checkRegexRedirects).ThenFunc(a.serve404).ServeHTTP(w, r)
}
}
const blogContextKey contextKey = "blog"
const pathContextKey contextKey = "httpPath"
@ -636,28 +553,9 @@ func noIndexHeader(next http.Handler) http.Handler {
})
}
type dynamicHandler struct {
router *chi.Mux
mutex sync.RWMutex
initialized bool
}
func (d *dynamicHandler) swapHandler(h *chi.Mux) {
d.mutex.Lock()
d.router = h
d.initialized = true
d.mutex.Unlock()
}
func (d *dynamicHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Fix to use Path routing instead of RawPath routing in Chi
r.URL.RawPath = ""
// Serve request
d.mutex.RLock()
for !d.initialized {
time.Sleep(10 * time.Millisecond)
}
router := d.router
d.mutex.RUnlock()
router.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)
})
}

View File

@ -1,39 +0,0 @@
package main
import (
"database/sql"
"net/http"
)
func (db *database) allPostAliases() ([]string, error) {
var aliases []string
rows, err := db.query("select distinct value from post_parameters where parameter = 'aliases' and value != path")
if err != nil {
return nil, err
}
for rows.Next() {
var path string
_ = rows.Scan(&path)
if path != "" {
aliases = append(aliases, path)
}
}
return aliases, nil
}
func (a *goBlog) servePostAlias(w http.ResponseWriter, r *http.Request) {
row, err := a.db.queryRow("select path from post_parameters where parameter = 'aliases' and value = @alias", sql.Named("alias", r.URL.Path))
if err != nil {
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return
}
var path string
if err := row.Scan(&path); err == sql.ErrNoRows {
a.serve404(w, r)
return
} else if err != nil {
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, path, http.StatusFound)
}

View File

@ -138,8 +138,9 @@ func (a *goBlog) createOrReplacePost(p *post, o *postCreationOptions) error {
defer a.postUpdateHooks(p)
}
}
// Reload router
return a.reloadRouter()
// Purge cache
a.cache.purge()
return nil
}
// Save check post to database
@ -192,8 +193,11 @@ func (a *goBlog) deletePost(path string) error {
if err != nil || p == nil {
return err
}
defer a.postDeleteHooks(p)
return a.reloadRouter()
// Purge cache
a.cache.purge()
// Trigger hooks
a.postDeleteHooks(p)
return nil
}
func (db *database) deletePost(path string) (*post, error) {

View File

@ -1,6 +1,14 @@
package main
import "net/http"
import (
"context"
"database/sql"
"errors"
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
)
const taxonomyContextKey = "taxonomy"
@ -21,3 +29,38 @@ 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)
tax := r.Context().Value(taxonomyContextKey).(*configTaxonomy)
taxValueParam := chi.URLParam(r, "taxValue")
if taxValueParam == "" {
a.serve404(w, r)
return
}
// Get value from DB
row, err := a.db.queryRow(
"select value from post_parameters where parameter = @tax and urlize(value) = @taxValue limit 1",
sql.Named("tax", tax.Name), sql.Named("taxValue", taxValueParam),
)
if err != nil {
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return
}
var taxValue string
err = row.Scan(&taxValue)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
a.serve404(w, r)
return
}
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return
}
// Serve index
a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
path: a.getRelativePath(blog, fmt.Sprintf("/%s/%s", tax.Name, taxValueParam)),
tax: tax,
taxValue: taxValue,
})))
}