2020-07-28 19:17:07 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2020-10-19 19:09:51 +00:00
|
|
|
"compress/flate"
|
2021-07-17 07:33:44 +00:00
|
|
|
"database/sql"
|
|
|
|
"errors"
|
2020-12-13 14:16:47 +00:00
|
|
|
"fmt"
|
2021-03-10 17:08:20 +00:00
|
|
|
"log"
|
2021-04-02 08:28:04 +00:00
|
|
|
"net"
|
2020-07-28 19:17:07 +00:00
|
|
|
"net/http"
|
2021-02-16 20:27:52 +00:00
|
|
|
"net/url"
|
2020-07-28 19:17:07 +00:00
|
|
|
"strconv"
|
2021-02-16 20:27:52 +00:00
|
|
|
"strings"
|
2021-03-31 07:29:52 +00:00
|
|
|
"time"
|
2020-09-25 17:23:01 +00:00
|
|
|
|
2021-01-23 16:24:47 +00:00
|
|
|
"github.com/dchest/captcha"
|
2021-03-03 17:19:55 +00:00
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"github.com/go-chi/chi/v5/middleware"
|
2021-07-17 07:33:44 +00:00
|
|
|
"github.com/justinas/alice"
|
2021-06-30 14:15:07 +00:00
|
|
|
"golang.org/x/crypto/acme"
|
|
|
|
"golang.org/x/crypto/acme/autocert"
|
2021-04-03 13:39:43 +00:00
|
|
|
"golang.org/x/net/context"
|
2020-07-28 19:17:07 +00:00
|
|
|
)
|
|
|
|
|
2020-10-26 16:37:31 +00:00
|
|
|
const (
|
2021-06-18 12:32:03 +00:00
|
|
|
contentType = "Content-Type"
|
2020-11-16 17:34:29 +00:00
|
|
|
userAgent = "User-Agent"
|
|
|
|
appUserAgent = "GoBlog"
|
2020-10-26 16:37:31 +00:00
|
|
|
)
|
|
|
|
|
2021-06-06 12:39:42 +00:00
|
|
|
func (a *goBlog) startServer() (err error) {
|
2021-06-17 14:34:57 +00:00
|
|
|
log.Println("Start server(s)...")
|
2021-07-17 07:33:44 +00:00
|
|
|
// Load router
|
|
|
|
router, err := a.buildRouter()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
a.d = fixHTTPHandler(router)
|
2021-03-10 17:47:56 +00:00
|
|
|
// Set basic middlewares
|
2021-07-24 11:35:26 +00:00
|
|
|
h := alice.New()
|
2021-06-06 12:39:42 +00:00
|
|
|
if a.cfg.Server.Logging {
|
2021-07-24 11:35:26 +00:00
|
|
|
h = h.Append(a.logMiddleware)
|
|
|
|
}
|
|
|
|
h = h.Append(middleware.Recoverer, middleware.Compress(flate.DefaultCompression), middleware.Heartbeat("/ping"))
|
|
|
|
if a.cfg.Server.PublicHTTPS || a.cfg.Server.SecurityHeaders {
|
|
|
|
h = h.Append(a.securityHeaders)
|
2021-03-10 17:47:56 +00:00
|
|
|
}
|
2021-07-24 11:35:26 +00:00
|
|
|
finalHandler := h.Then(a.d)
|
2021-03-19 09:10:47 +00:00
|
|
|
// Start Onion service
|
2021-06-06 12:39:42 +00:00
|
|
|
if a.cfg.Server.Tor {
|
2021-03-19 09:10:47 +00:00
|
|
|
go func() {
|
2021-06-06 12:39:42 +00:00
|
|
|
if err := a.startOnionService(finalHandler); err != nil {
|
2021-04-03 13:39:43 +00:00
|
|
|
log.Println("Tor failed:", err.Error())
|
|
|
|
}
|
2021-03-19 09:10:47 +00:00
|
|
|
}()
|
|
|
|
}
|
2021-04-02 08:28:04 +00:00
|
|
|
// Start server
|
|
|
|
s := &http.Server{
|
|
|
|
Handler: finalHandler,
|
|
|
|
ReadTimeout: 5 * time.Minute,
|
|
|
|
WriteTimeout: 5 * time.Minute,
|
|
|
|
}
|
2021-06-17 14:34:57 +00:00
|
|
|
a.shutdown.Add(shutdownServer(s, "main server"))
|
2021-06-06 12:39:42 +00:00
|
|
|
if a.cfg.Server.PublicHTTPS {
|
2021-04-03 13:39:43 +00:00
|
|
|
// Start HTTP server for redirects
|
2021-04-02 08:28:04 +00:00
|
|
|
httpServer := &http.Server{
|
|
|
|
Addr: ":http",
|
|
|
|
Handler: http.HandlerFunc(redirectToHttps),
|
|
|
|
ReadTimeout: 5 * time.Minute,
|
|
|
|
WriteTimeout: 5 * time.Minute,
|
|
|
|
}
|
2021-06-17 14:34:57 +00:00
|
|
|
a.shutdown.Add(shutdownServer(httpServer, "http server"))
|
2021-04-02 08:28:04 +00:00
|
|
|
go func() {
|
2021-04-03 13:39:43 +00:00
|
|
|
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
2021-04-02 08:28:04 +00:00
|
|
|
log.Println("Failed to start HTTP server:", err.Error())
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
// Start HTTPS
|
|
|
|
s.Addr = ":https"
|
2021-06-06 12:39:42 +00:00
|
|
|
hosts := []string{a.cfg.Server.publicHostname}
|
|
|
|
if a.cfg.Server.shortPublicHostname != "" {
|
|
|
|
hosts = append(hosts, a.cfg.Server.shortPublicHostname)
|
2020-12-24 10:00:16 +00:00
|
|
|
}
|
2021-06-30 14:15:07 +00:00
|
|
|
acmeDir := acme.LetsEncryptURL
|
|
|
|
// acmeDir := "https://acme-staging-v02.api.letsencrypt.org/directory"
|
|
|
|
m := &autocert.Manager{
|
|
|
|
Prompt: autocert.AcceptTOS,
|
|
|
|
HostPolicy: autocert.HostWhitelist(hosts...),
|
|
|
|
Cache: &httpsCache{db: a.db},
|
|
|
|
Client: &acme.Client{DirectoryURL: acmeDir},
|
2021-03-31 07:29:52 +00:00
|
|
|
}
|
2021-06-30 14:15:07 +00:00
|
|
|
if err = s.Serve(m.Listener()); err != nil && err != http.ErrServerClosed {
|
2021-04-03 13:39:43 +00:00
|
|
|
return err
|
|
|
|
}
|
2021-04-02 08:28:04 +00:00
|
|
|
} else {
|
2021-06-06 12:39:42 +00:00
|
|
|
s.Addr = ":" + strconv.Itoa(a.cfg.Server.Port)
|
2021-04-03 13:39:43 +00:00
|
|
|
if err = s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
|
|
return err
|
|
|
|
}
|
2020-07-29 14:41:36 +00:00
|
|
|
}
|
2021-04-03 13:39:43 +00:00
|
|
|
return nil
|
2020-08-01 15:49:46 +00:00
|
|
|
}
|
2020-07-29 14:41:36 +00:00
|
|
|
|
2021-05-07 14:14:15 +00:00
|
|
|
func shutdownServer(s *http.Server, name string) func() {
|
|
|
|
return func() {
|
|
|
|
toc, c := context.WithTimeout(context.Background(), 5*time.Second)
|
2021-06-17 14:34:57 +00:00
|
|
|
defer c()
|
|
|
|
if err := s.Shutdown(toc); err != nil {
|
|
|
|
log.Printf("Error on server shutdown (%v): %v", name, err)
|
|
|
|
}
|
2021-05-07 14:14:15 +00:00
|
|
|
log.Println("Stopped server:", name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-02 08:28:04 +00:00
|
|
|
func redirectToHttps(w http.ResponseWriter, r *http.Request) {
|
|
|
|
requestHost, _, err := net.SplitHostPort(r.Host)
|
|
|
|
if err != nil {
|
|
|
|
requestHost = r.Host
|
|
|
|
}
|
|
|
|
w.Header().Set("Connection", "close")
|
|
|
|
http.Redirect(w, r, fmt.Sprintf("https://%s%s", requestHost, r.URL.RequestURI()), http.StatusMovedPermanently)
|
|
|
|
}
|
|
|
|
|
2021-03-19 12:04:11 +00:00
|
|
|
const (
|
|
|
|
paginationPath = "/page/{page:[0-9-]+}"
|
|
|
|
feedPath = ".{feed:rss|json|atom}"
|
|
|
|
)
|
2021-01-30 18:37:26 +00:00
|
|
|
|
2021-07-17 07:33:44 +00:00
|
|
|
func (a *goBlog) buildRouter() (*chi.Mux, error) {
|
|
|
|
r := chi.NewRouter()
|
2021-03-19 12:04:11 +00:00
|
|
|
|
2021-07-17 07:33:44 +00:00
|
|
|
// 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)
|
2021-03-19 12:04:11 +00:00
|
|
|
}
|
|
|
|
|
2021-02-27 07:31:06 +00:00
|
|
|
// Basic middleware
|
2021-06-06 12:39:42 +00:00
|
|
|
r.Use(a.redirectShortDomain)
|
2020-11-05 16:06:42 +00:00
|
|
|
r.Use(middleware.RedirectSlashes)
|
2020-12-22 19:29:25 +00:00
|
|
|
r.Use(middleware.CleanPath)
|
2020-10-19 19:09:51 +00:00
|
|
|
r.Use(middleware.GetHead)
|
2021-06-06 12:39:42 +00:00
|
|
|
if !a.cfg.Cache.Enable {
|
2020-10-26 16:37:31 +00:00
|
|
|
r.Use(middleware.NoCache)
|
|
|
|
}
|
2021-02-27 07:31:06 +00:00
|
|
|
|
|
|
|
// No Index Header
|
2021-07-17 07:33:44 +00:00
|
|
|
if privateMode {
|
2021-02-27 07:31:06 +00:00
|
|
|
r.Use(noIndexHeader)
|
|
|
|
}
|
|
|
|
|
2021-07-24 11:35:26 +00:00
|
|
|
// Login and captcha middleware
|
2021-06-06 12:39:42 +00:00
|
|
|
r.Use(a.checkIsLogin)
|
|
|
|
r.Use(a.checkIsCaptcha)
|
2020-07-29 15:17:48 +00:00
|
|
|
|
2021-02-24 15:01:10 +00:00
|
|
|
// Logout
|
2021-06-06 12:39:42 +00:00
|
|
|
r.With(a.authMiddleware).Get("/login", serveLogin)
|
|
|
|
r.With(a.authMiddleware).Get("/logout", a.serveLogout)
|
2021-02-24 15:01:10 +00:00
|
|
|
|
2020-10-06 17:07:48 +00:00
|
|
|
// Micropub
|
2021-07-17 07:33:44 +00:00
|
|
|
r.Route(micropubPath, func(r chi.Router) {
|
|
|
|
r.Use(a.checkIndieAuth)
|
|
|
|
r.Get("/", a.serveMicropubQuery)
|
|
|
|
r.Post("/", a.serveMicropubPost)
|
|
|
|
r.Post(micropubMediaSubPath, a.serveMicropubMedia)
|
|
|
|
})
|
2020-10-06 17:07:48 +00:00
|
|
|
|
2020-10-13 19:35:39 +00:00
|
|
|
// IndieAuth
|
2021-07-17 07:33:44 +00:00
|
|
|
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)
|
|
|
|
})
|
2020-10-13 19:35:39 +00:00
|
|
|
|
2020-10-26 16:37:31 +00:00
|
|
|
// ActivityPub and stuff
|
2021-06-06 12:39:42 +00:00
|
|
|
if ap := a.cfg.ActivityPub; ap != nil && ap.Enabled {
|
2021-07-17 07:33:44 +00:00
|
|
|
r.Route("/activitypub", func(r chi.Router) {
|
|
|
|
r.Post("/inbox/{blog}", a.apHandleInbox)
|
|
|
|
r.Post("/{blog}/inbox", a.apHandleInbox)
|
|
|
|
})
|
2021-07-17 16:08:13 +00:00
|
|
|
r.Group(func(r chi.Router) {
|
2021-07-24 11:35:26 +00:00
|
|
|
r.Use(a.cacheMiddleware)
|
2021-07-17 16:08:13 +00:00
|
|
|
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)
|
|
|
|
})
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
|
|
|
|
2020-11-06 17:45:31 +00:00
|
|
|
// Webmentions
|
2021-07-17 07:33:44 +00:00
|
|
|
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)
|
|
|
|
})
|
|
|
|
})
|
2021-07-14 13:44:57 +00:00
|
|
|
}
|
|
|
|
|
2021-07-17 07:33:44 +00:00
|
|
|
// Notifications
|
|
|
|
r.Route(notificationsPath, func(r chi.Router) {
|
2021-06-06 12:39:42 +00:00
|
|
|
r.Use(a.authMiddleware)
|
2021-07-17 07:33:44 +00:00
|
|
|
r.Get("/", a.notificationsAdmin)
|
|
|
|
r.Get(paginationPath, a.notificationsAdmin)
|
|
|
|
r.Post("/delete", a.notificationsAdminDelete)
|
2021-02-16 15:26:21 +00:00
|
|
|
})
|
2020-07-29 15:17:48 +00:00
|
|
|
|
2020-09-19 10:27:07 +00:00
|
|
|
// Assets
|
2021-06-06 12:39:42 +00:00
|
|
|
for _, path := range a.allAssetPaths() {
|
|
|
|
r.Get(path, a.serveAsset)
|
2020-09-19 10:27:07 +00:00
|
|
|
}
|
|
|
|
|
2020-12-23 13:11:14 +00:00
|
|
|
// Static files
|
|
|
|
for _, path := range allStaticPaths() {
|
2021-07-17 07:33:44 +00:00
|
|
|
r.With(privateModeHandler...).Get(path, a.serveStaticFile)
|
2020-12-23 13:11:14 +00:00
|
|
|
}
|
|
|
|
|
2021-01-10 14:59:43 +00:00
|
|
|
// Media files
|
2021-07-17 07:33:44 +00:00
|
|
|
r.With(privateModeHandler...).Get(`/m/{file:[0-9a-fA-F]+(\.[0-9a-zA-Z]+)?}`, a.serveMediaFile)
|
2021-01-10 14:59:43 +00:00
|
|
|
|
2021-01-23 16:24:47 +00:00
|
|
|
// Captcha
|
2021-07-17 07:33:44 +00:00
|
|
|
r.Handle("/captcha/*", captcha.Server(500, 250))
|
2021-01-23 16:24:47 +00:00
|
|
|
|
2020-12-22 21:15:29 +00:00
|
|
|
// Short paths
|
2021-07-24 11:35:26 +00:00
|
|
|
r.With(privateModeHandler...).With(a.cacheMiddleware).Get("/s/{id:[0-9a-fA-F]+}", a.redirectToLongPath)
|
2020-12-22 21:15:29 +00:00
|
|
|
|
2021-06-06 12:39:42 +00:00
|
|
|
for blog, blogConfig := range a.cfg.Blogs {
|
2021-07-17 07:33:44 +00:00
|
|
|
sbm := middleware.WithValue(blogContextKey, blog)
|
2020-08-31 19:12:43 +00:00
|
|
|
|
2021-02-16 15:26:21 +00:00
|
|
|
// Sections
|
|
|
|
r.Group(func(r chi.Router) {
|
2021-07-17 07:33:44 +00:00
|
|
|
r.Use(privateModeHandler...)
|
2021-07-24 11:35:26 +00:00
|
|
|
r.Use(a.cacheMiddleware, sbm)
|
2021-02-16 15:26:21 +00:00
|
|
|
for _, section := range blogConfig.Sections {
|
|
|
|
if section.Name != "" {
|
2021-03-22 07:20:56 +00:00
|
|
|
r.Group(func(r chi.Router) {
|
2021-07-17 07:33:44 +00:00
|
|
|
secPath := blogConfig.getRelativePath(section.Name)
|
|
|
|
r.Use(middleware.WithValue(indexConfigKey, &indexConfig{
|
|
|
|
path: secPath,
|
|
|
|
section: section,
|
|
|
|
}))
|
2021-06-06 12:39:42 +00:00
|
|
|
r.Get(secPath, a.serveIndex)
|
|
|
|
r.Get(secPath+feedPath, a.serveIndex)
|
|
|
|
r.Get(secPath+paginationPath, a.serveIndex)
|
2021-03-22 07:20:56 +00:00
|
|
|
})
|
2021-02-16 15:26:21 +00:00
|
|
|
}
|
2020-08-31 19:12:43 +00:00
|
|
|
}
|
2021-02-16 15:26:21 +00:00
|
|
|
})
|
2020-10-06 17:07:48 +00:00
|
|
|
|
2021-02-16 15:26:21 +00:00
|
|
|
// Taxonomies
|
2021-07-17 07:33:44 +00:00
|
|
|
r.Group(func(r chi.Router) {
|
|
|
|
r.Use(privateModeHandler...)
|
2021-07-24 11:35:26 +00:00
|
|
|
r.Use(a.cacheMiddleware, sbm)
|
2021-07-17 07:33:44 +00:00
|
|
|
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)
|
|
|
|
})
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
2020-08-31 19:12:43 +00:00
|
|
|
}
|
2021-07-17 07:33:44 +00:00
|
|
|
})
|
2020-08-25 18:55:32 +00:00
|
|
|
|
2020-10-06 17:07:48 +00:00
|
|
|
// Photos
|
2021-07-12 14:19:28 +00:00
|
|
|
if pc := blogConfig.Photos; pc != nil && pc.Enabled {
|
2021-02-16 15:26:21 +00:00
|
|
|
r.Group(func(r chi.Router) {
|
2021-07-12 14:19:28 +00:00
|
|
|
photoPath := blogConfig.getRelativePath(defaultIfEmpty(pc.Path, defaultPhotosPath))
|
2021-07-17 07:33:44 +00:00
|
|
|
r.Use(privateModeHandler...)
|
2021-07-24 11:35:26 +00:00
|
|
|
r.Use(a.cacheMiddleware, sbm, middleware.WithValue(indexConfigKey, &indexConfig{
|
2021-07-17 07:33:44 +00:00
|
|
|
path: photoPath,
|
|
|
|
parameter: pc.Parameter,
|
|
|
|
title: pc.Title,
|
|
|
|
description: pc.Description,
|
|
|
|
summaryTemplate: templatePhotosSummary,
|
|
|
|
}))
|
2021-06-06 12:39:42 +00:00
|
|
|
r.Get(photoPath, a.serveIndex)
|
|
|
|
r.Get(photoPath+feedPath, a.serveIndex)
|
|
|
|
r.Get(photoPath+paginationPath, a.serveIndex)
|
2021-02-16 15:26:21 +00:00
|
|
|
})
|
2020-10-06 17:07:48 +00:00
|
|
|
}
|
2020-09-21 16:03:05 +00:00
|
|
|
|
2020-11-15 10:34:48 +00:00
|
|
|
// Search
|
2021-07-12 14:19:28 +00:00
|
|
|
if bsc := blogConfig.Search; bsc != nil && bsc.Enabled {
|
2021-07-17 07:33:44 +00:00
|
|
|
searchPath := blogConfig.getRelativePath(defaultIfEmpty(bsc.Path, defaultSearchPath))
|
2021-07-17 16:08:13 +00:00
|
|
|
r.Route(searchPath, func(r chi.Router) {
|
|
|
|
r.Use(sbm, middleware.WithValue(
|
|
|
|
pathContextKey,
|
|
|
|
searchPath,
|
|
|
|
))
|
|
|
|
r.Group(func(r chi.Router) {
|
|
|
|
r.Use(privateModeHandler...)
|
2021-07-24 11:35:26 +00:00
|
|
|
r.Use(a.cacheMiddleware)
|
2021-07-17 16:08:13 +00:00
|
|
|
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)
|
|
|
|
})
|
2021-07-24 11:35:26 +00:00
|
|
|
r.With(a.cacheMiddleware).Get("/opensearch.xml", a.serveOpenSearch)
|
2021-07-17 16:08:13 +00:00
|
|
|
})
|
2020-11-15 10:34:48 +00:00
|
|
|
}
|
|
|
|
|
2021-01-04 19:29:49 +00:00
|
|
|
// Stats
|
2021-07-12 14:19:28 +00:00
|
|
|
if bsc := blogConfig.BlogStats; bsc != nil && bsc.Enabled {
|
|
|
|
statsPath := blogConfig.getRelativePath(defaultIfEmpty(bsc.Path, defaultBlogStatsPath))
|
2021-05-09 07:08:31 +00:00
|
|
|
r.Group(func(r chi.Router) {
|
2021-07-17 07:33:44 +00:00
|
|
|
r.Use(privateModeHandler...)
|
2021-07-24 11:35:26 +00:00
|
|
|
r.Use(a.cacheMiddleware, sbm)
|
2021-06-06 12:39:42 +00:00
|
|
|
r.Get(statsPath, a.serveBlogStats)
|
|
|
|
r.Get(statsPath+".table.html", a.serveBlogStatsTable)
|
2021-05-09 07:08:31 +00:00
|
|
|
})
|
2021-01-04 19:29:49 +00:00
|
|
|
}
|
|
|
|
|
2021-03-22 07:20:56 +00:00
|
|
|
// Date archives
|
2021-02-16 15:26:21 +00:00
|
|
|
r.Group(func(r chi.Router) {
|
2021-07-17 07:33:44 +00:00
|
|
|
r.Use(privateModeHandler...)
|
2021-07-24 11:35:26 +00:00
|
|
|
r.Use(a.cacheMiddleware, sbm)
|
2021-03-22 07:20:56 +00:00
|
|
|
|
|
|
|
yearRegex := `/{year:x|\d\d\d\d}`
|
|
|
|
monthRegex := `/{month:x|\d\d}`
|
|
|
|
dayRegex := `/{day:\d\d}`
|
|
|
|
|
2021-06-10 20:09:50 +00:00
|
|
|
yearPath := blogConfig.getRelativePath(yearRegex)
|
2021-06-06 12:39:42 +00:00
|
|
|
r.Get(yearPath, a.serveDate)
|
|
|
|
r.Get(yearPath+feedPath, a.serveDate)
|
|
|
|
r.Get(yearPath+paginationPath, a.serveDate)
|
2021-03-22 07:20:56 +00:00
|
|
|
|
|
|
|
monthPath := yearPath + monthRegex
|
2021-06-06 12:39:42 +00:00
|
|
|
r.Get(monthPath, a.serveDate)
|
|
|
|
r.Get(monthPath+feedPath, a.serveDate)
|
|
|
|
r.Get(monthPath+paginationPath, a.serveDate)
|
2021-03-22 07:20:56 +00:00
|
|
|
|
|
|
|
dayPath := monthPath + dayRegex
|
2021-06-06 12:39:42 +00:00
|
|
|
r.Get(dayPath, a.serveDate)
|
|
|
|
r.Get(dayPath+feedPath, a.serveDate)
|
|
|
|
r.Get(dayPath+paginationPath, a.serveDate)
|
2021-02-16 15:26:21 +00:00
|
|
|
})
|
2020-12-13 14:16:47 +00:00
|
|
|
|
2020-10-06 17:07:48 +00:00
|
|
|
// Blog
|
2020-12-23 15:53:10 +00:00
|
|
|
if !blogConfig.PostAsHome {
|
2021-02-27 07:31:06 +00:00
|
|
|
r.Group(func(r chi.Router) {
|
2021-07-17 07:33:44 +00:00
|
|
|
r.Use(privateModeHandler...)
|
2021-03-23 06:27:12 +00:00
|
|
|
r.Use(sbm)
|
2021-07-24 11:35:26 +00:00
|
|
|
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)
|
2021-02-27 07:31:06 +00:00
|
|
|
})
|
2020-10-26 16:37:31 +00:00
|
|
|
}
|
2020-10-13 11:40:16 +00:00
|
|
|
|
|
|
|
// Custom pages
|
|
|
|
for _, cp := range blogConfig.CustomPages {
|
2021-07-17 07:33:44 +00:00
|
|
|
scp := middleware.WithValue(customPageContextKey, cp)
|
2020-10-13 11:40:16 +00:00
|
|
|
if cp.Cache {
|
2021-07-24 11:35:26 +00:00
|
|
|
r.With(privateModeHandler...).With(a.cacheMiddleware, sbm, scp).Get(cp.Path, a.serveCustomPage)
|
2020-10-13 11:40:16 +00:00
|
|
|
} else {
|
2021-07-17 07:33:44 +00:00
|
|
|
r.With(privateModeHandler...).With(sbm, scp).Get(cp.Path, a.serveCustomPage)
|
2020-10-13 11:40:16 +00:00
|
|
|
}
|
|
|
|
}
|
2021-01-17 11:53:07 +00:00
|
|
|
|
2021-01-20 12:38:24 +00:00
|
|
|
// Random post
|
|
|
|
if rp := blogConfig.RandomPost; rp != nil && rp.Enabled {
|
2021-07-17 07:33:44 +00:00
|
|
|
r.With(privateModeHandler...).With(sbm).Get(blogConfig.getRelativePath(defaultIfEmpty(rp.Path, "/random")), a.redirectToRandomPost)
|
2021-01-20 12:38:24 +00:00
|
|
|
}
|
|
|
|
|
2021-01-17 11:53:07 +00:00
|
|
|
// Editor
|
2021-07-17 16:08:13 +00:00
|
|
|
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)
|
|
|
|
})
|
2021-01-23 16:24:47 +00:00
|
|
|
|
|
|
|
// Comments
|
|
|
|
if commentsConfig := blogConfig.Comments; commentsConfig != nil && commentsConfig.Enabled {
|
2021-07-17 07:33:44 +00:00
|
|
|
commentsPath := blogConfig.getRelativePath("/comment")
|
2021-07-17 16:08:13 +00:00
|
|
|
r.Route(commentsPath, func(r chi.Router) {
|
|
|
|
r.Use(sbm, middleware.WithValue(pathContextKey, commentsPath))
|
|
|
|
r.Use(privateModeHandler...)
|
2021-07-24 11:35:26 +00:00
|
|
|
r.With(a.cacheMiddleware, noIndexHeader).Get("/{id:[0-9]+}", a.serveComment)
|
2021-07-17 16:08:13 +00:00
|
|
|
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)
|
|
|
|
})
|
|
|
|
})
|
2021-01-23 16:24:47 +00:00
|
|
|
}
|
2021-05-08 19:22:48 +00:00
|
|
|
|
|
|
|
// Blogroll
|
|
|
|
if brConfig := blogConfig.Blogroll; brConfig != nil && brConfig.Enabled {
|
2021-07-12 14:19:28 +00:00
|
|
|
brPath := blogConfig.getRelativePath(defaultIfEmpty(brConfig.Path, defaultBlogrollPath))
|
2021-05-08 19:22:48 +00:00
|
|
|
r.Group(func(r chi.Router) {
|
2021-07-17 07:33:44 +00:00
|
|
|
r.Use(privateModeHandler...)
|
2021-07-24 11:35:26 +00:00
|
|
|
r.Use(a.cacheMiddleware, sbm)
|
2021-06-06 12:39:42 +00:00
|
|
|
r.Get(brPath, a.serveBlogroll)
|
|
|
|
r.Get(brPath+".opml", a.serveBlogrollExport)
|
2021-05-08 19:22:48 +00:00
|
|
|
})
|
|
|
|
}
|
2021-07-06 19:06:39 +00:00
|
|
|
|
|
|
|
// Geo map
|
|
|
|
if mc := blogConfig.Map; mc != nil && mc.Enabled {
|
2021-07-19 16:41:38 +00:00
|
|
|
mapPath := blogConfig.getRelativePath(defaultIfEmpty(mc.Path, defaultGeoMapPath))
|
|
|
|
r.Route(mapPath, func(r chi.Router) {
|
|
|
|
r.Use(privateModeHandler...)
|
|
|
|
r.Group(func(r chi.Router) {
|
2021-07-24 11:35:26 +00:00
|
|
|
r.Use(a.cacheMiddleware, sbm)
|
2021-07-19 16:41:38 +00:00
|
|
|
r.Get("/", a.serveGeoMap)
|
|
|
|
r.HandleFunc("/leaflet/*", a.serveLeaflet(mapPath+"/"))
|
|
|
|
})
|
|
|
|
r.Get("/tiles/{z}/{x}/{y}.png", a.proxyTiles(mapPath+"/tiles"))
|
|
|
|
})
|
2021-07-06 19:06:39 +00:00
|
|
|
}
|
|
|
|
|
2021-07-22 11:41:52 +00:00
|
|
|
// 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...)
|
2021-07-24 11:35:26 +00:00
|
|
|
r.Use(a.cacheMiddleware, sbm)
|
2021-07-22 11:41:52 +00:00
|
|
|
r.Get("/", a.serveContactForm)
|
|
|
|
r.With(a.captchaMiddleware).Post("/", a.sendContactSubmission)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-08-05 17:14:10 +00:00
|
|
|
}
|
|
|
|
|
2020-09-22 15:08:34 +00:00
|
|
|
// Sitemap
|
2021-07-24 11:35:26 +00:00
|
|
|
r.With(privateModeHandler...).With(a.cacheMiddleware).Get(sitemapPath, a.serveSitemap)
|
2020-09-22 15:08:34 +00:00
|
|
|
|
2020-11-09 15:40:12 +00:00
|
|
|
// Robots.txt - doesn't need cache, because it's too simple
|
2021-07-17 07:33:44 +00:00
|
|
|
if !privateMode {
|
2021-06-06 12:39:42 +00:00
|
|
|
r.Get("/robots.txt", a.serveRobotsTXT)
|
2021-02-27 07:31:06 +00:00
|
|
|
} else {
|
|
|
|
r.Get("/robots.txt", servePrivateRobotsTXT)
|
|
|
|
}
|
2020-11-09 15:40:12 +00:00
|
|
|
|
2021-07-17 07:33:44 +00:00
|
|
|
r.NotFound(a.servePostsAliasesRedirects(privateModeHandler...))
|
2020-07-31 19:02:47 +00:00
|
|
|
|
2021-06-06 12:39:42 +00:00
|
|
|
r.MethodNotAllowed(a.serveNotAllowed)
|
2020-12-24 09:09:34 +00:00
|
|
|
|
2020-07-29 15:17:48 +00:00
|
|
|
return r, nil
|
2020-07-28 19:17:07 +00:00
|
|
|
}
|
|
|
|
|
2021-07-17 07:33:44 +00:00
|
|
|
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
|
2021-07-23 07:53:35 +00:00
|
|
|
union all
|
|
|
|
select 'deleted', '' from deleted where path = @path
|
2021-07-17 07:33:44 +00:00
|
|
|
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:
|
2021-07-24 11:35:26 +00:00
|
|
|
alicePrivate.Append(a.checkActivityStreamsRequest, a.cacheMiddleware).ThenFunc(a.servePost).ServeHTTP(w, r)
|
2021-07-17 07:33:44 +00:00
|
|
|
return
|
|
|
|
case statusDraft, statusPrivate:
|
|
|
|
alice.New(a.authMiddleware).ThenFunc(a.servePost).ServeHTTP(w, r)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
case "alias":
|
|
|
|
// Is alias, redirect
|
2021-07-24 11:35:26 +00:00
|
|
|
alicePrivate.Append(a.cacheMiddleware).ThenFunc(func(w http.ResponseWriter, r *http.Request) {
|
2021-07-17 07:33:44 +00:00
|
|
|
http.Redirect(w, r, value, http.StatusFound)
|
|
|
|
}).ServeHTTP(w, r)
|
|
|
|
return
|
2021-07-23 07:53:35 +00:00
|
|
|
case "deleted":
|
|
|
|
// Is deleted, serve 410
|
2021-07-24 11:35:26 +00:00
|
|
|
alicePrivate.Append(a.cacheMiddleware).ThenFunc(func(w http.ResponseWriter, r *http.Request) {
|
2021-07-23 07:53:35 +00:00
|
|
|
a.serve410(w, r)
|
|
|
|
}).ServeHTTP(w, r)
|
|
|
|
return
|
2021-07-17 07:33:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// No post, check regex redirects or serve 404 error
|
2021-07-24 11:35:26 +00:00
|
|
|
alice.New(a.cacheMiddleware, a.checkRegexRedirects).ThenFunc(a.serve404).ServeHTTP(w, r)
|
2021-07-17 07:33:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-29 15:07:08 +00:00
|
|
|
const blogContextKey contextKey = "blog"
|
|
|
|
const pathContextKey contextKey = "httpPath"
|
2021-03-22 07:20:56 +00:00
|
|
|
|
2021-06-06 12:39:42 +00:00
|
|
|
func (a *goBlog) refreshCSPDomains() {
|
2021-07-23 15:26:14 +00:00
|
|
|
var cspBuilder strings.Builder
|
2021-06-06 12:39:42 +00:00
|
|
|
if mp := a.cfg.Micropub.MediaStorage; mp != nil && mp.MediaURL != "" {
|
2021-02-16 20:27:52 +00:00
|
|
|
if u, err := url.Parse(mp.MediaURL); err == nil {
|
2021-07-23 15:26:14 +00:00
|
|
|
cspBuilder.WriteByte(' ')
|
|
|
|
cspBuilder.WriteString(u.Hostname())
|
2021-02-16 20:27:52 +00:00
|
|
|
}
|
|
|
|
}
|
2021-06-06 12:39:42 +00:00
|
|
|
if len(a.cfg.Server.CSPDomains) > 0 {
|
2021-07-23 15:26:14 +00:00
|
|
|
cspBuilder.WriteByte(' ')
|
|
|
|
cspBuilder.WriteString(strings.Join(a.cfg.Server.CSPDomains, " "))
|
2021-02-16 20:27:52 +00:00
|
|
|
}
|
2021-07-23 15:26:14 +00:00
|
|
|
a.cspDomains = cspBuilder.String()
|
2021-03-19 09:10:47 +00:00
|
|
|
}
|
|
|
|
|
2021-07-06 19:06:39 +00:00
|
|
|
const cspHeader = "Content-Security-Policy"
|
|
|
|
|
2021-06-06 12:39:42 +00:00
|
|
|
func (a *goBlog) securityHeaders(next http.Handler) http.Handler {
|
|
|
|
a.refreshCSPDomains()
|
2020-10-16 13:35:38 +00:00
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2021-02-27 07:31:06 +00:00
|
|
|
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")
|
2021-07-06 19:06:39 +00:00
|
|
|
w.Header().Set(cspHeader, "default-src 'self'"+a.cspDomains)
|
2021-06-06 12:39:42 +00:00
|
|
|
if a.cfg.Server.Tor && a.torAddress != "" {
|
|
|
|
w.Header().Set("Onion-Location", fmt.Sprintf("http://%v%v", a.torAddress, r.RequestURI))
|
2021-03-19 09:10:47 +00:00
|
|
|
}
|
2021-02-27 07:31:06 +00:00
|
|
|
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")
|
2020-10-16 13:35:38 +00:00
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-07-17 07:33:44 +00:00
|
|
|
func fixHTTPHandler(next http.Handler) http.Handler {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
r.URL.RawPath = ""
|
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
})
|
2020-07-29 15:17:48 +00:00
|
|
|
}
|