From e25199b2a3683b9036adc6b9228229da00a30e39 Mon Sep 17 00:00:00 2001 From: Jan-Lukas Else Date: Sat, 27 Feb 2021 08:31:06 +0100 Subject: [PATCH] Private mode --- activityStreams.go | 2 +- config.go | 8 +++ go.mod | 5 +- go.sum | 10 +-- http.go | 128 ++++++++++++++++++++++++-------------- robotstxt.go | 4 ++ templates/login.gohtml | 1 + webmentionSending.go | 11 +++- webmentionVerification.go | 5 ++ 9 files changed, 118 insertions(+), 56 deletions(-) diff --git a/activityStreams.go b/activityStreams.go index d66b53c..a9b0f9e 100644 --- a/activityStreams.go +++ b/activityStreams.go @@ -15,7 +15,7 @@ var asContext = []string{"https://www.w3.org/ns/activitystreams"} const asRequestKey requestContextKey = "asRequest" -func manipulateAsPath(next http.Handler) http.Handler { +func checkActivityStreamsRequest(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { if ap := appConfig.ActivityPub; ap != nil && ap.Enabled { if lowerAccept := strings.ToLower(r.Header.Get("Accept")); (strings.Contains(lowerAccept, contentTypeAS) || strings.Contains(lowerAccept, "application/ld+json")) && !strings.Contains(lowerAccept, contentTypeHTML) { diff --git a/config.go b/config.go index 5c772e3..2b25da7 100644 --- a/config.go +++ b/config.go @@ -20,6 +20,7 @@ type config struct { PathRedirects []*configRegexRedirect `mapstructure:"pathRedirects"` ActivityPub *configActivityPub `mapstructure:"activityPub"` Notifications *configNotifications `mapstructure:"notifications"` + PrivateMode *configPrivateMode `mapstructure:"privateMode"` } type configServer struct { @@ -190,6 +191,10 @@ type configTelegram struct { InstantViewHash string `mapstructure:"instantViewHash"` } +type configPrivateMode struct { + Enabled bool `mapstructure:"enabled"` +} + var appConfig = &config{} func initConfig() error { @@ -255,6 +260,9 @@ func initConfig() error { } appConfig.Micropub.MediaStorage.MediaURL = strings.TrimSuffix(appConfig.Micropub.MediaStorage.MediaURL, "/") } + if pm := appConfig.PrivateMode; pm != nil && pm.Enabled { + appConfig.ActivityPub = &configActivityPub{Enabled: false} + } return nil } diff --git a/go.mod b/go.mod index d4d5306..76e2dac 100644 --- a/go.mod +++ b/go.mod @@ -44,6 +44,7 @@ require ( github.com/spf13/viper v1.7.1 github.com/tdewolff/minify/v2 v2.9.13 github.com/tdewolff/parse/v2 v2.5.11 // indirect + github.com/thoas/go-funk v0.7.1-0.20201128100912-5035611e402b github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 github.com/vcraescu/go-paginator v1.0.1-0.20201114172518-2cfc59fe05c2 github.com/yuin/goldmark v1.3.2 @@ -53,9 +54,9 @@ require ( golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 // indirect golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect golang.org/x/mod v0.4.1 // indirect - golang.org/x/net v0.0.0-20210224082022-3d97a244fca7 // indirect + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073 // indirect + golang.org/x/sys v0.0.0-20210227040730-b0d1d43c014d // indirect golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect golang.org/x/text v0.3.5 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/go.sum b/go.sum index 7d0cfce..c94b9f3 100644 --- a/go.sum +++ b/go.sum @@ -288,6 +288,8 @@ github.com/tdewolff/parse/v2 v2.5.11 h1:Wq0x026IKZh9GPUB5Fp+v5bki/SNmpIkdltcnm6H github.com/tdewolff/parse/v2 v2.5.11/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4= github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= +github.com/thoas/go-funk v0.7.1-0.20201128100912-5035611e402b h1:EBnJBNE7sw2Z7dg2inKlTQNO2se7PvDejZQjNaPAjg8= +github.com/thoas/go-funk v0.7.1-0.20201128100912-5035611e402b/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y= github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE= @@ -370,8 +372,8 @@ golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210224082022-3d97a244fca7 h1:OgUuv8lsRpBibGNbSizVwKWlysjaNzmC9gYMhPVfqFM= -golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 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= @@ -404,8 +406,8 @@ golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073 h1:8qxJSnu+7dRq6upnbntrmriWByIakBuct5OM/MdQC1M= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210227040730-b0d1d43c014d h1:9fH9JvLNoSpsDWcXJ4dSE3lZW99Z3OCUZLr07g60U6o= +golang.org/x/sys v0.0.0-20210227040730-b0d1d43c014d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M= diff --git a/http.go b/http.go index 2a721a8..4fc1fb9 100644 --- a/http.go +++ b/http.go @@ -77,13 +77,21 @@ func reloadRouter() error { return nil } +const paginationPath = "/page/{page:[0-9-]+}" +const feedPath = ".{feed:rss|json|atom}" + func buildHandler() (http.Handler, error) { - - paginationPath := "/page/{page:[0-9-]+}" - feedPath := ".{feed:rss|json|atom}" - r := chi.NewRouter() + // Private mode + privateMode := false + privateModeHandler := []func(http.Handler) http.Handler{} + if pm := appConfig.PrivateMode; pm != nil && pm.Enabled { + privateMode = true + privateModeHandler = append(privateModeHandler, authMiddleware) + } + + // Basic middleware if appConfig.Server.Logging { r.Use(logMiddleware) } @@ -96,29 +104,37 @@ func buildHandler() (http.Handler, error) { if !appConfig.Cache.Enable { r.Use(middleware.NoCache) } + + // No Index Header + if privateMode { + r.Use(noIndexHeader) + } + + // Login middleware etc. r.Use(checkIsLogin) r.Use(checkIsCaptcha) r.Use(checkLoggedIn) + r.Use(checkActivityStreamsRequest) // Logout r.With(authMiddleware).Get("/login", serveLogin) r.With(authMiddleware).Get("/logout", serveLogout) // Micropub - r.Route(micropubPath, func(mpRouter chi.Router) { - mpRouter.Use(checkIndieAuth) - mpRouter.Get("/", serveMicropubQuery) - mpRouter.Post("/", serveMicropubPost) - mpRouter.Post(micropubMediaSubPath, serveMicropubMedia) + r.Route(micropubPath, func(r chi.Router) { + r.Use(checkIndieAuth) + r.Get("/", serveMicropubQuery) + r.Post("/", serveMicropubPost) + r.Post(micropubMediaSubPath, serveMicropubMedia) }) // IndieAuth - r.Route("/indieauth", func(indieauthRouter chi.Router) { - indieauthRouter.Get("/", indieAuthRequest) - indieauthRouter.With(authMiddleware).Post("/accept", indieAuthAccept) - indieauthRouter.Post("/", indieAuthVerification) - indieauthRouter.Get("/token", indieAuthToken) - indieauthRouter.Post("/token", indieAuthToken) + r.Route("/indieauth", func(r chi.Router) { + r.Get("/", indieAuthRequest) + r.With(authMiddleware).Post("/accept", indieAuthAccept) + r.Post("/", indieAuthVerification) + r.Get("/token", indieAuthToken) + r.Post("/token", indieAuthToken) }) // ActivityPub and stuff @@ -132,9 +148,9 @@ func buildHandler() (http.Handler, error) { } // Webmentions - r.Route(webmentionPath, func(webmentionRouter chi.Router) { - webmentionRouter.Post("/", handleWebmention) - webmentionRouter.Group(func(r chi.Router) { + r.Route(webmentionPath, func(r chi.Router) { + r.Post("/", handleWebmention) + r.Group(func(r chi.Router) { // Authenticated routes r.Use(authMiddleware) r.Get("/", webmentionAdmin) @@ -159,7 +175,8 @@ func buildHandler() (http.Handler, error) { return nil, err } r.Group(func(r chi.Router) { - r.Use(manipulateAsPath, cacheMiddleware) + r.Use(privateModeHandler...) + r.Use(cacheMiddleware) for _, path := range pp { r.Get(path, servePost) } @@ -171,7 +188,7 @@ func buildHandler() (http.Handler, error) { return nil, err } r.Group(func(r chi.Router) { - r.Use(authMiddleware, cacheMiddleware) + r.Use(authMiddleware) for _, path := range dp { r.Get(path, servePost) } @@ -183,6 +200,7 @@ func buildHandler() (http.Handler, error) { return nil, err } r.Group(func(r chi.Router) { + r.Use(privateModeHandler...) r.Use(cacheMiddleware) for _, path := range allPostAliases { r.Get(path, servePostAlias) @@ -200,13 +218,13 @@ func buildHandler() (http.Handler, error) { } // Media files - r.Get(`/m/{file:[0-9a-fA-F]+(\.[0-9a-zA-Z]+)?}`, serveMediaFile) + r.With(privateModeHandler...).Get(`/m/{file:[0-9a-fA-F]+(\.[0-9a-zA-Z]+)?}`, serveMediaFile) // Captcha r.Handle("/captcha/*", captcha.Server(500, 250)) // Short paths - r.With(cacheMiddleware).Get("/s/{id:[0-9a-fA-F]+}", redirectToLongPath) + r.With(privateModeHandler...).With(cacheMiddleware).Get("/s/{id:[0-9a-fA-F]+}", redirectToLongPath) for blog, blogConfig := range appConfig.Blogs { @@ -216,12 +234,9 @@ func buildHandler() (http.Handler, error) { blogPath = "" } - r.Group(func(r chi.Router) { - - }) - // Sections r.Group(func(r chi.Router) { + r.Use(privateModeHandler...) r.Use(cacheMiddleware) for _, section := range blogConfig.Sections { if section.Name != "" { @@ -243,6 +258,7 @@ func buildHandler() (http.Handler, error) { return nil, err } r.Group(func(r chi.Router) { + r.Use(privateModeHandler...) r.Use(cacheMiddleware) r.Get(taxPath, serveTaxonomy(blog, taxonomy)) for _, tv := range taxValues { @@ -253,13 +269,13 @@ func buildHandler() (http.Handler, error) { r.Get(vPath+paginationPath, handler) } }) - } } // Photos if blogConfig.Photos != nil && blogConfig.Photos.Enabled { r.Group(func(r chi.Router) { + r.Use(privateModeHandler...) r.Use(cacheMiddleware) photoPath := blogPath + blogConfig.Photos.Path handler := servePhotos(blog, photoPath) @@ -272,6 +288,7 @@ func buildHandler() (http.Handler, error) { // Search if blogConfig.Search != nil && blogConfig.Search.Enabled { r.Group(func(r chi.Router) { + r.Use(privateModeHandler...) r.Use(cacheMiddleware) searchPath := blogPath + blogConfig.Search.Path handler := serveSearch(blog, searchPath) @@ -288,7 +305,7 @@ func buildHandler() (http.Handler, error) { // Stats if blogConfig.BlogStats != nil && blogConfig.BlogStats.Enabled { statsPath := blogPath + blogConfig.BlogStats.Path - r.With(cacheMiddleware).Get(statsPath, serveBlogStats(blog, statsPath)) + r.With(privateModeHandler...).With(cacheMiddleware).Get(statsPath, serveBlogStats(blog, statsPath)) } // Year / month archives @@ -297,6 +314,7 @@ func buildHandler() (http.Handler, error) { return nil, err } r.Group(func(r chi.Router) { + r.Use(privateModeHandler...) r.Use(cacheMiddleware) for _, d := range dates { // Year @@ -334,19 +352,23 @@ func buildHandler() (http.Handler, error) { // Blog if !blogConfig.PostAsHome { - handler := serveHome(blog, blogPath) - r.With(manipulateAsPath, cacheMiddleware).Get(fullBlogPath, handler) - r.With(cacheMiddleware).Get(fullBlogPath+feedPath, handler) - r.With(cacheMiddleware).Get(blogPath+paginationPath, handler) + r.Group(func(r chi.Router) { + r.Use(privateModeHandler...) + r.Use(cacheMiddleware) + handler := serveHome(blog, blogPath) + r.Get(fullBlogPath, handler) + r.Get(fullBlogPath+feedPath, handler) + r.Get(blogPath+paginationPath, handler) + }) } // Custom pages for _, cp := range blogConfig.CustomPages { handler := serveCustomPage(blogConfig, cp) if cp.Cache { - r.With(cacheMiddleware).Get(cp.Path, handler) + r.With(privateModeHandler...).With(cacheMiddleware).Get(cp.Path, handler) } else { - r.Get(cp.Path, handler) + r.With(privateModeHandler...).Get(cp.Path, handler) } } @@ -356,20 +378,21 @@ func buildHandler() (http.Handler, error) { if randomPath == "" { randomPath = "/random" } - r.Get(blogPath+randomPath, redirectToRandomPost(blog)) + r.With(privateModeHandler...).Get(blogPath+randomPath, redirectToRandomPost(blog)) } // Editor - r.Route(blogPath+"/editor", func(mpRouter chi.Router) { - mpRouter.Use(authMiddleware) - mpRouter.Get("/", serveEditor(blog)) - mpRouter.Post("/", serveEditorPost(blog)) + r.Route(blogPath+"/editor", func(r chi.Router) { + r.Use(authMiddleware) + r.Get("/", serveEditor(blog)) + r.Post("/", serveEditorPost(blog)) }) // Comments if commentsConfig := blogConfig.Comments; commentsConfig != nil && commentsConfig.Enabled { commentsPath := blogPath + "/comment" r.Route(commentsPath, func(cr chi.Router) { + cr.Use(privateModeHandler...) cr.With(cacheMiddleware).Get("/{id:[0-9]+}", serveComment(blog)) cr.With(captchaMiddleware).Post("/", createComment(blog, commentsPath)) // Admin @@ -385,10 +408,14 @@ func buildHandler() (http.Handler, error) { } // Sitemap - r.With(cacheMiddleware).Get(sitemapPath, serveSitemap) + r.With(privateModeHandler...).With(cacheMiddleware).Get(sitemapPath, serveSitemap) // Robots.txt - doesn't need cache, because it's too simple - r.Get("/robots.txt", serveRobotsTXT) + if !privateMode { + r.Get("/robots.txt", serveRobotsTXT) + } else { + r.Get("/robots.txt", servePrivateRobotsTXT) + } // Check redirects, then serve 404 r.With(cacheMiddleware, checkRegexRedirects).NotFound(serve404) @@ -411,12 +438,19 @@ func securityHeaders(next http.Handler) http.Handler { extraCSPDomains += " " + strings.Join(appConfig.Server.CSPDomains, " ") } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Strict-Transport-Security", "max-age=31536000;") - w.Header().Add("Referrer-Policy", "no-referrer") - w.Header().Add("X-Content-Type-Options", "nosniff") - w.Header().Add("X-Frame-Options", "SAMEORIGIN") - w.Header().Add("X-Xss-Protection", "1; mode=block") - w.Header().Add("Content-Security-Policy", "default-src 'self'"+extraCSPDomains) + 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'"+extraCSPDomains) + 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) }) } diff --git a/robotstxt.go b/robotstxt.go index 878b3dc..c33bf51 100644 --- a/robotstxt.go +++ b/robotstxt.go @@ -8,3 +8,7 @@ import ( func serveRobotsTXT(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(fmt.Sprintf("User-agent: *\nSitemap: %v", appConfig.Server.PublicAddress+sitemapPath))) } + +func servePrivateRobotsTXT(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("User-agent: *\nDisallow: /")) +} diff --git a/templates/login.gohtml b/templates/login.gohtml index dc75ac7..056cf01 100644 --- a/templates/login.gohtml +++ b/templates/login.gohtml @@ -14,6 +14,7 @@ + {{ include "author" . }} {{ end }} diff --git a/webmentionSending.go b/webmentionSending.go index 840b555..8a0d616 100644 --- a/webmentionSending.go +++ b/webmentionSending.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/PuerkitoBio/goquery" + "github.com/thoas/go-funk" "github.com/tomnomnom/linkheader" ) @@ -19,11 +20,12 @@ func (p *post) sendWebmentions() error { return err } links = append(links, contentLinks...) - links = append(links, p.firstParameter(appConfig.Micropub.LikeParam), p.firstParameter(appConfig.Micropub.ReplyParam), p.firstParameter(appConfig.Micropub.BookmarkParam)) - for _, link := range links { + links = append(links, p.firstParameter("link"), p.firstParameter(appConfig.Micropub.LikeParam), p.firstParameter(appConfig.Micropub.ReplyParam), p.firstParameter(appConfig.Micropub.BookmarkParam)) + for _, link := range funk.UniqString(links) { if link == "" { continue } + // Internal mention if strings.HasPrefix(link, appConfig.Server.PublicAddress) { // Save mention directly if err := createWebmention(p.fullURL(), link); err != nil { @@ -31,6 +33,11 @@ func (p *post) sendWebmentions() error { } continue } + // External mention + if pm := appConfig.PrivateMode; pm != nil && pm.Enabled { + // Private mode, don't send external mentions + continue + } endpoint := discoverEndpoint(link) if endpoint == "" { continue diff --git a/webmentionVerification.go b/webmentionVerification.go index 29b5af5..e905885 100644 --- a/webmentionVerification.go +++ b/webmentionVerification.go @@ -10,6 +10,7 @@ import ( "net/http" "net/url" "os" + "strings" "github.com/PuerkitoBio/goquery" "github.com/joncrlsn/dque" @@ -73,6 +74,10 @@ func (m *mention) verifyMention() error { return err } req.Header.Set(userAgent, appUserAgent) + if strings.HasPrefix(m.Source, appConfig.Server.PublicAddress) { + // Set authentication + req.SetBasicAuth(appConfig.User.Nick, appConfig.User.Password) + } resp, err := http.DefaultClient.Do(req) if err != nil { return err