diff --git a/app.go b/app.go index 6e8ae25..b11281d 100644 --- a/app.go +++ b/app.go @@ -71,5 +71,6 @@ type goBlog struct { // Template strings ts *ts.TemplateStrings // Tor - torAddress string + torAddress string + torHostname string } diff --git a/config.go b/config.go index 821e2b1..123eb9f 100644 --- a/config.go +++ b/config.go @@ -32,6 +32,7 @@ type configServer struct { Port int `mapstructure:"port"` PublicAddress string `mapstructure:"publicAddress"` ShortPublicAddress string `mapstructure:"shortPublicAddress"` + MediaAddress string `mapstructure:"mediaAddress"` PublicHTTPS bool `mapstructure:"publicHttps"` Tor bool `mapstructure:"tor"` SecurityHeaders bool `mapstructure:"securityHeaders"` @@ -39,6 +40,7 @@ type configServer struct { JWTSecret string `mapstructure:"jwtSecret"` publicHostname string shortPublicHostname string + mediaHostname string } type configDb struct { @@ -304,13 +306,20 @@ func (a *goBlog) initConfig() error { return err } a.cfg.Server.publicHostname = publicURL.Hostname() - if a.cfg.Server.ShortPublicAddress != "" { - shortPublicURL, err := url.Parse(a.cfg.Server.ShortPublicAddress) + if sa := a.cfg.Server.ShortPublicAddress; sa != "" { + shortPublicURL, err := url.Parse(sa) if err != nil { return err } a.cfg.Server.shortPublicHostname = shortPublicURL.Hostname() } + if ma := a.cfg.Server.MediaAddress; ma != "" { + mediaUrl, err := url.Parse(ma) + if err != nil { + return err + } + a.cfg.Server.mediaHostname = mediaUrl.Hostname() + } if a.cfg.Server.JWTSecret == "" { return errors.New("no JWT secret configured") } diff --git a/example-config.yml b/example-config.yml index 92e1afd..570f9c5 100644 --- a/example-config.yml +++ b/example-config.yml @@ -17,6 +17,7 @@ server: port: 8080 publicAddress: https://example.com # Public address to use for the blog shortPublicAddress: https://short.example.com # Optional short address, will redirect to main address + mediaAddress: https://media.example.com # Optional domain to use for serving media files # Security publicHttps: true # Use Let's Encrypt and serve site with HTTPS securityHeaders: true # Set security HTTP headers (to always use HTTPS etc.) diff --git a/http.go b/http.go index ba29260..050025c 100644 --- a/http.go +++ b/http.go @@ -15,6 +15,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "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" @@ -78,8 +79,11 @@ func (a *goBlog) startServer() (err error) { // Start HTTPS s.Addr = ":https" hosts := []string{a.cfg.Server.publicHostname} - if a.cfg.Server.shortPublicHostname != "" { - hosts = append(hosts, a.cfg.Server.shortPublicHostname) + 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" @@ -127,11 +131,29 @@ const ( ) 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() // Basic middleware r.Use(fixHTTPHandler) - r.Use(a.redirectShortDomain) r.Use(middleware.RedirectSlashes) r.Use(middleware.CleanPath) r.Use(middleware.GetHead) @@ -173,7 +195,7 @@ func (a *goBlog) buildRouter() (http.Handler, error) { r.Group(a.staticFilesRouter) // Media files - r.With(a.privateModeHandler).Get(`/m/{file:[0-9a-fA-F]+(\.[0-9a-zA-Z]+)?}`, a.serveMediaFile) + r.Route("/m", a.mediaFilesRouter) // Captcha r.Handle("/captcha/*", captcha.Server(500, 250)) @@ -196,7 +218,8 @@ func (a *goBlog) buildRouter() (http.Handler, error) { r.MethodNotAllowed(a.serveNotAllowed) - return r, nil + mapRouter.DefaultHandler = r + return mapRouter, nil } func (a *goBlog) servePostsAliasesRedirects() http.HandlerFunc { diff --git a/httpMiddlewares.go b/httpMiddlewares.go index 40689f8..247d23e 100644 --- a/httpMiddlewares.go +++ b/httpMiddlewares.go @@ -1,7 +1,6 @@ package main import ( - "fmt" "net/http" "net/url" "strings" @@ -43,8 +42,8 @@ func (a *goBlog) securityHeaders(next http.Handler) http.Handler { 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)) + if a.torAddress != "" { + w.Header().Set("Onion-Location", a.torAddress+r.RequestURI) } next.ServeHTTP(w, r) }) diff --git a/httpRouters.go b/httpRouters.go index 33d8a7e..beb1bf6 100644 --- a/httpRouters.go +++ b/httpRouters.go @@ -92,6 +92,12 @@ func (a *goBlog) staticFilesRouter(r chi.Router) { } } +// Media files +func (a *goBlog) mediaFilesRouter(r chi.Router) { + r.Use(a.privateModeHandler) + r.Get(mediaFileRoute, a.serveMediaFile) +} + // Blog func (a *goBlog) blogRouter(blog string, conf *configBlog) func(r chi.Router) { return func(r chi.Router) { diff --git a/media.go b/media.go index 0b99a80..45f6361 100644 --- a/media.go +++ b/media.go @@ -8,13 +8,17 @@ import ( "github.com/go-chi/chi/v5" ) -const mediaFilePath = "data/media" +const ( + mediaFilePath = "data/media" + mediaFileRoute = `/{file:[0-9a-fA-F]+(\.[0-9a-zA-Z]+)?}` +) func (a *goBlog) serveMediaFile(w http.ResponseWriter, r *http.Request) { f := filepath.Join(mediaFilePath, chi.URLParam(r, "file")) _, err := os.Stat(f) if err != nil { - a.serve404(w, r) + // Serve 404, but don't use normal serve404 method because of media domain + http.NotFound(w, r) return } w.Header().Add("Cache-Control", "public,max-age=31536000,immutable") diff --git a/pkgs/maprouter/maprouter.go b/pkgs/maprouter/maprouter.go new file mode 100644 index 0000000..80010ac --- /dev/null +++ b/pkgs/maprouter/maprouter.go @@ -0,0 +1,40 @@ +package maprouter + +import ( + "net/http" +) + +// Make sure interface is satisfied +var _ http.Handler = &MapRouter{} + +// Routes requests based on a map with routers +type MapRouter struct { + // Default http.Handler + DefaultHandler http.Handler + // Handlers mapped by prefix + Handlers map[string]http.Handler + // Optional function to find key for handler, default uses hostname + KeyFunc func(r *http.Request) string +} + +// Serve the HTTP request +func (ar *MapRouter) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + if len(ar.Handlers) > 0 { + var key string + if ar.KeyFunc != nil { + key = ar.KeyFunc(r) + } else { + key = defaultKey(r) + } + if h, ok := ar.Handlers[key]; ok { + h.ServeHTTP(rw, r) + return + } + } + ar.DefaultHandler.ServeHTTP(rw, r) +} + +// Gets the default key for the router +func defaultKey(r *http.Request) string { + return r.Host +} diff --git a/render.go b/render.go index 1015f16..1f56d17 100644 --- a/render.go +++ b/render.go @@ -3,7 +3,6 @@ package main import ( "bytes" "errors" - "fmt" "html/template" "net/http" "os" @@ -174,7 +173,7 @@ func (a *goBlog) checkRenderData(r *http.Request, data *renderData) { } // Tor if a.cfg.Server.Tor && a.torAddress != "" { - data.TorAddress = fmt.Sprintf("http://%v%v", a.torAddress, r.RequestURI) + data.TorAddress = a.torAddress + r.RequestURI } if torUsed, ok := r.Context().Value(torUsedKey).(bool); ok && torUsed { data.TorUsed = true diff --git a/shortDomain.go b/shortDomain.go index 0244881..573cf55 100644 --- a/shortDomain.go +++ b/shortDomain.go @@ -4,12 +4,6 @@ import ( "net/http" ) -func (a *goBlog) redirectShortDomain(next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - if a.cfg.Server.shortPublicHostname != "" && r.Host == a.cfg.Server.shortPublicHostname { - http.Redirect(rw, r, a.getFullAddress(r.RequestURI), http.StatusMovedPermanently) - return - } - next.ServeHTTP(rw, r) - }) +func (a *goBlog) redirectShortDomain(rw http.ResponseWriter, r *http.Request) { + http.Redirect(rw, r, a.getFullAddress(r.RequestURI), http.StatusMovedPermanently) } diff --git a/tor.go b/tor.go index 372f5cd..7b1d917 100644 --- a/tor.go +++ b/tor.go @@ -8,6 +8,7 @@ import ( "encoding/pem" "log" "net/http" + "net/url" "os" "path/filepath" "time" @@ -72,8 +73,10 @@ func (a *goBlog) startOnionService(h http.Handler) error { return err } defer onion.Close() - a.torAddress = onion.String() - log.Println("Onion service published on http://" + a.torAddress) + a.torAddress = "http://" + onion.String() + torUrl, _ := url.Parse(a.torAddress) + a.torHostname = torUrl.Hostname() + log.Println("Onion service published on " + a.torAddress) // Clear cache a.cache.purge() // Serve handler