GoBlog/http.go

304 lines
7.6 KiB
Go
Raw Normal View History

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-09-23 16:24:45 +00:00
"net"
2020-07-28 19:17:07 +00:00
"net/http"
"strconv"
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"
"go.goblog.app/app/pkgs/maprouter"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
"golang.org/x/net/context"
2020-07-28 19:17:07 +00:00
)
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)...")
2021-07-17 07:33:44 +00:00
// Load router
a.d, err = a.buildRouter()
2021-07-17 07:33:44 +00:00
if err != nil {
return err
}
2021-03-10 17:47:56 +00:00
// Set basic middlewares
h := alice.New()
if a.cfg.Server.Logging {
h = h.Append(a.logMiddleware)
}
h = h.Append(middleware.Recoverer, middleware.Compress(flate.DefaultCompression), middleware.Heartbeat("/ping"))
2021-09-23 06:42:00 +00:00
if a.httpsConfigured(false) {
h = h.Append(a.securityHeaders)
2021-03-10 17:47:56 +00:00
}
finalHandler := h.Then(a.d)
2021-03-19 09:10:47 +00:00
// Start Onion service
if a.cfg.Server.Tor {
2021-03-19 09:10:47 +00:00
go func() {
if err := a.startOnionService(finalHandler); err != nil {
log.Println("Tor failed:", err.Error())
}
2021-03-19 09:10:47 +00:00
}()
}
// Start server
s := &http.Server{
Handler: finalHandler,
ReadTimeout: 5 * time.Minute,
WriteTimeout: 5 * time.Minute,
}
a.shutdown.Add(shutdownServer(s, "main server"))
if a.cfg.Server.PublicHTTPS || a.cfg.Server.TailscaleHTTPS {
// Start HTTP server for redirects
httpServer := &http.Server{
Addr: ":http",
2021-09-23 06:53:08 +00:00
Handler: http.HandlerFunc(a.redirectToHttps),
ReadTimeout: 5 * time.Minute,
WriteTimeout: 5 * time.Minute,
}
a.shutdown.Add(shutdownServer(httpServer, "http server"))
go func() {
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Println("Failed to start HTTP server:", err.Error())
}
}()
// Start HTTPS
s.Addr = ":https"
if a.cfg.Server.TailscaleHTTPS {
// HTTPS via Tailscale
2021-09-23 06:42:00 +00:00
if err = a.startTailscaleHttps(s); err != nil {
return err
}
} else {
// Public HTTPS via Let's Encrypt
hosts := []string{a.cfg.Server.publicHostname}
if shn := a.cfg.Server.shortPublicHostname; shn != "" {
hosts = append(hosts, shn)
}
if mhn := a.cfg.Server.mediaHostname; mhn != "" {
hosts = append(hosts, mhn)
}
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},
}
if err = s.Serve(m.Listener()); err != nil && err != http.ErrServerClosed {
return err
}
}
} else {
s.Addr = ":" + strconv.Itoa(a.cfg.Server.Port)
if err = s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return err
}
2020-07-29 14:41:36 +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)
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-09-23 16:24:45 +00:00
func (*goBlog) redirectToHttps(w http.ResponseWriter, r *http.Request) {
requestHost, _, err := net.SplitHostPort(r.Host)
if err != nil {
requestHost = r.Host
}
w.Header().Set("Connection", "close")
2021-09-23 16:24:45 +00:00
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
func (a *goBlog) buildRouter() (http.Handler, error) {
mapRouter := &maprouter.MapRouter{
Handlers: map[string]http.Handler{},
}
if shn := a.cfg.Server.shortPublicHostname; shn != "" {
mapRouter.Handlers[shn] = http.HandlerFunc(a.redirectShortDomain)
}
if mhn := a.cfg.Server.mediaHostname; mhn != "" && !a.isPrivate() {
mr := chi.NewMux()
mr.Use(middleware.RedirectSlashes)
mr.Use(middleware.CleanPath)
mr.Use(middleware.GetHead)
mr.Group(a.mediaFilesRouter)
mapRouter.Handlers[mhn] = mr
}
// Default router
r := chi.NewMux()
2021-03-19 12:04:11 +00:00
2021-02-27 07:31:06 +00:00
// Basic middleware
r.Use(fixHTTPHandler)
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-08-02 19:10:38 +00:00
// Tor
if a.cfg.Server.Tor {
r.Use(a.addOnionLocation)
}
// Cache
if cache := a.cfg.Cache; cache != nil && !cache.Enable {
r.Use(middleware.NoCache)
}
2021-02-27 07:31:06 +00:00
// No Index Header
if a.isPrivate() {
2021-02-27 07:31:06 +00:00
r.Use(noIndexHeader)
}
// Login and captcha middleware
r.Use(a.checkIsLogin)
r.Use(a.checkIsCaptcha)
// Login
r.Group(a.loginRouter)
2020-10-06 17:07:48 +00:00
// Micropub
r.Route(micropubPath, a.micropubRouter)
2020-10-06 17:07:48 +00:00
2020-10-13 19:35:39 +00:00
// IndieAuth
r.Route("/indieauth", a.indieAuthRouter)
2020-10-13 19:35:39 +00:00
// ActivityPub and stuff
r.Group(a.activityPubRouter)
// Webmentions
r.Route(webmentionPath, a.webmentionsRouter)
2021-07-17 07:33:44 +00:00
// Notifications
r.Route(notificationsPath, a.notificationsRouter)
// Assets
r.Group(a.assetsRouter)
2020-12-23 13:11:14 +00:00
// Static files
r.Group(a.staticFilesRouter)
2020-12-23 13:11:14 +00:00
2021-01-10 14:59:43 +00:00
// Media files
r.Route("/m", a.mediaFilesRouter)
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
r.With(a.privateModeHandler, cacheLoggedIn, a.cacheMiddleware).Get("/s/{id:[0-9a-fA-F]+}", a.redirectToLongPath)
2020-12-22 21:15:29 +00:00
// Blogs
for blog, blogConfig := range a.cfg.Blogs {
r.Group(a.blogRouter(blog, blogConfig))
2020-08-05 17:14:10 +00:00
}
2020-09-22 15:08:34 +00:00
// Sitemap
r.With(a.privateModeHandler, cacheLoggedIn, a.cacheMiddleware).Get(sitemapPath, a.serveSitemap)
2020-09-22 15:08:34 +00:00
// Robots.txt
r.With(cacheLoggedIn, a.cacheMiddleware).Get(robotsTXTPath, a.serveRobotsTXT)
r.NotFound(a.servePostsAliasesRedirects())
r.MethodNotAllowed(a.serveNotAllowed)
2020-12-24 09:09:34 +00:00
mapRouter.DefaultHandler = r
return mapRouter, nil
2020-07-28 19:17:07 +00:00
}
func (a *goBlog) servePostsAliasesRedirects() http.HandlerFunc {
2021-07-17 07:33:44 +00:00
// Private mode
alicePrivate := alice.New(a.privateModeHandler)
2021-07-17 07:33:44 +00:00
// 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
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:
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-25 08:53:12 +00:00
alicePrivate.Append(cacheLoggedIn, 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
case "deleted":
// Is deleted, serve 410
alicePrivate.Append(a.cacheMiddleware).ThenFunc(func(w http.ResponseWriter, r *http.Request) {
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
alice.New(a.cacheMiddleware, a.checkRegexRedirects).ThenFunc(a.serve404).ServeHTTP(w, r)
2021-07-17 07:33:44 +00:00
}
}