Skip to content

Commit

Permalink
Add -base-path flag
Browse files Browse the repository at this point in the history
This allows self-hosting GoatCounter and reverse proxying it into a
subdirectory instead of hosting it on its own subdomain.

Fixes #707, #750
  • Loading branch information
mk12 committed Aug 18, 2024
1 parent 57c95db commit b9ed19f
Show file tree
Hide file tree
Showing 59 changed files with 272 additions and 216 deletions.
20 changes: 18 additions & 2 deletions cmd/goatcounter/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ Flags:
-public-port Port your site is publicly accessible on. Only needed if it's
not 80 or 443.
-base-path Base path to use in URLs sent back to the client. Default: "/".
You need this if you are reverse proxying GoatCounter into a
subdirectory of your website. This does not affect routing, since
it is assumed the reverse proxy will strip off the base path.
-automigrate Automatically run all pending migrations on startup.
-smtp SMTP relay server, as URL (e.g. "smtp://user:pass@server").
Expand Down Expand Up @@ -164,18 +169,26 @@ func cmdServe(f zli.Flags, ready chan<- struct{}, stop chan struct{}) error {
var (
// TODO(depr): -port is for compat with <2.0
port = f.Int(0, "public-port", "port").Pointer()
basePath = f.String("/", "base-path").Pointer()
domainStatic = f.String("", "static").Pointer()
)
dbConnect, dbConn, dev, automigrate, listen, flagTLS, from, websocket, apiMax, err := flagsServe(f, &v)
if err != nil {
return err
}

return func(port int, domainStatic string) error {
return func(port int, basePath, domainStatic string) error {
if flagTLS == "" {
flagTLS = map[bool]string{true: "http", false: "acme,rdr"}[dev]
}

if !strings.HasPrefix(basePath, "/") {
return fmt.Errorf("invalid -base-path flag: %q: must start with a slash", basePath)
}
if i := len(basePath) - 1; basePath[i] == '/' {
basePath = basePath[:i]
}

var domainCount, urlStatic string
if domainStatic != "" {
if p := strings.Index(domainStatic, ":"); p > -1 {
Expand All @@ -185,6 +198,8 @@ func cmdServe(f zli.Flags, ready chan<- struct{}, stop chan struct{}) error {
}
urlStatic = "//" + domainStatic
domainCount = domainStatic
} else {
urlStatic = basePath
}

//from := flagFrom(from, "cfg.Domain", &v)
Expand All @@ -206,6 +221,7 @@ func cmdServe(f zli.Flags, ready chan<- struct{}, stop chan struct{}) error {
c.DomainStatic = domainStatic
c.Dev = dev
c.URLStatic = urlStatic
c.BasePath = basePath
c.DomainCount = domainCount
c.Websocket = websocket

Expand Down Expand Up @@ -238,7 +254,7 @@ func cmdServe(f zli.Flags, ready chan<- struct{}, stop chan struct{}) error {
}
ready <- struct{}{}
})
}(*port, *domainStatic)
}(*port, *basePath, *domainStatic)
}

func doServe(ctx context.Context, db zdb.DB,
Expand Down
1 change: 1 addition & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type GlobalConfig struct {
Domain string
DomainStatic string
DomainCount string
BasePath string
URLStatic string
Dev bool
GoatcounterCom bool
Expand Down
4 changes: 2 additions & 2 deletions handlers/bosmang.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func (h bosmang) mount(r chi.Router, db zdb.DB) {
a := r.With(mware.RequestLog(nil), requireAccess(goatcounter.AccessSuperuser))

r.Get("/bosmang", zhttp.Wrap(func(w http.ResponseWriter, r *http.Request) error {
return zhttp.MovedPermanently(w, "/settings/server")
return MovedPermanently(w, r, "/settings/server")
}))

a.Get("/bosmang/cache", zhttp.Wrap(h.cache))
Expand Down Expand Up @@ -98,7 +98,7 @@ func (h bosmang) runTask(w http.ResponseWriter, r *http.Request) error {
})

zhttp.Flash(w, "Task %q started", id)
return zhttp.SeeOther(w, "/bosmang/bgrun")
return SeeOther(w, r, "/bosmang/bgrun")
}

func (h bosmang) metrics(w http.ResponseWriter, r *http.Request) error {
Expand Down
5 changes: 1 addition & 4 deletions handlers/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,7 @@ func (h backend) dashboard(w http.ResponseWriter, r *http.Request) error {

cd := goatcounter.Config(r.Context()).DomainCount
if cd == "" {
cd = Site(r.Context()).Domain(r.Context())
if goatcounter.Config(r.Context()).Port != "" {
cd += ":" + goatcounter.Config(r.Context()).Port
}
cd = Site(r.Context()).SchemelessURL(r.Context())
}

args := widgets.Args{
Expand Down
2 changes: 2 additions & 0 deletions handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type Globals struct {
User *goatcounter.User
Site *goatcounter.Site
Path string
Base string
Flash *zhttp.FlashMessage
Static string
StaticDomain string
Expand All @@ -84,6 +85,7 @@ func newGlobals(w http.ResponseWriter, r *http.Request) Globals {
User: goatcounter.GetUser(ctx),
Site: goatcounter.GetSite(ctx),
Path: r.URL.Path,
Base: goatcounter.Config(ctx).BasePath,
Flash: zhttp.ReadFlash(w, r),
Static: goatcounter.Config(ctx).URLStatic,
Domain: goatcounter.Config(ctx).Domain,
Expand Down
8 changes: 4 additions & 4 deletions handlers/i18n.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ func (h i18n) show(w http.ResponseWriter, r *http.Request) error {

return zhttp.Template(w, "i18n_show.gohtml", struct {
Globals
Base msgfile.File
BaseFile msgfile.File
File msgfile.File
TOMLFile string
FormatLink func(string) string
Expand Down Expand Up @@ -187,7 +187,7 @@ func (h i18n) new(w http.ResponseWriter, r *http.Request) error {
}

zhttp.Flash(w, "%q added", args.Language)
return zhttp.SeeOther(w, "/i18n")
return SeeOther(w, r, "/i18n")
}

func (h i18n) save(w http.ResponseWriter, r *http.Request) error {
Expand Down Expand Up @@ -266,7 +266,7 @@ func (h i18n) set(w http.ResponseWriter, r *http.Request) error {
}

zhttp.Flash(w, "language set to %q", lang)
return zhttp.SeeOther(w, "/i18n")
return SeeOther(w, r, "/i18n")
}

func (h i18n) submit(w http.ResponseWriter, r *http.Request) error {
Expand Down Expand Up @@ -294,5 +294,5 @@ func (h i18n) submit(w http.ResponseWriter, r *http.Request) error {
}()

zhttp.Flash(w, "email sent to [email protected]; I'll take a look as soon as possible.")
return zhttp.SeeOther(w, "/i18n/"+file)
return SeeOther(w, r, "/i18n/"+file)
}
11 changes: 6 additions & 5 deletions handlers/mw.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ var Started time.Time
var (
redirect = func(w http.ResponseWriter, r *http.Request) error {
zhttp.Flash(w, "Need to log in")
return guru.Errorf(303, "/user/new")
return guru.Errorf(303, goatcounter.Config(r.Context()).BasePath+"/user/new")
}

loggedIn = auth.Filter(func(w http.ResponseWriter, r *http.Request) error {
Expand Down Expand Up @@ -76,7 +76,7 @@ var (
Secure: zhttp.CookieSecure,
SameSite: zhttp.CookieSameSite,
})
return guru.Errorf(303, "/")
return guru.Errorf(303, goatcounter.Config(r.Context()).BasePath+"/")
}
if c, err := r.Cookie("access-token"); err == nil && s.Settings.CanView(c.Value) {
return nil
Expand Down Expand Up @@ -220,7 +220,7 @@ func addctx(db zdb.DB, loadSite bool, dashTimeout int) func(http.Handler) http.H

func noSites(db zdb.DB, w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
w.Header().Set("Location", "/")
w.Header().Set("Location", goatcounter.Config(r.Context()).BasePath+"/")
w.WriteHeader(307)
return
}
Expand Down Expand Up @@ -290,7 +290,7 @@ func noSites(db zdb.DB, w http.ResponseWriter, r *http.Request) {
tplErr = errors.Unwrap(tplErr) // Remove "zdb.TX fn: "
}
if tplErr == nil && !v.HasErrors() {
zhttp.SeeOther(w, "/")
SeeOther(w, r, "/")
}
}

Expand All @@ -302,11 +302,12 @@ func noSites(db zdb.DB, w http.ResponseWriter, r *http.Request) {
}

err := zhttp.Template(w, "serve_newsite.gohtml", struct {
Globals
Validate *zvalidate.Validator
Error error
Email string
Cname string
}{&v, tplErr, args.Email, args.Cname})
}{newGlobals(w, r), &v, tplErr, args.Email, args.Cname})
if err != nil {
zlog.Error(err)
}
Expand Down
27 changes: 27 additions & 0 deletions handlers/redirect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright © Martin Tournoij – This file is part of GoatCounter and published
// under the terms of a slightly modified EUPL v1.2 license, which can be found
// in the LICENSE file or at https://license.goatcounter.com

package handlers

import (
"fmt"
"net/http"

"zgo.at/goatcounter/v2"
"zgo.at/zhttp"
)

func MovedPermanently(w http.ResponseWriter, r *http.Request, path string) error {
if path == "" || path[0] != '/' {
panic(fmt.Sprintf("handlers.MovedPermantly: %q does not start with slash", path))
}
return zhttp.MovedPermanently(w, goatcounter.Config(r.Context()).BasePath+path)
}

func SeeOther(w http.ResponseWriter, r *http.Request, path string) error {
if path == "" || path[0] != '/' {
panic(fmt.Sprintf("handlers.SeeOther: %q does not start with slash", path))
}
return zhttp.SeeOther(w, goatcounter.Config(r.Context()).BasePath+path)
}
38 changes: 19 additions & 19 deletions handlers/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ type settings struct{}
func (h settings) mount(r chi.Router) {
{ // User settings.
r.Get("/user", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
zhttp.SeeOther(w, "/user/pref")
SeeOther(w, r, "/user/pref")
}))

r.Get("/user/pref", zhttp.Wrap(h.userPref(nil)))
Expand All @@ -63,7 +63,7 @@ func (h settings) mount(r chi.Router) {
set := r.With(requireAccess(goatcounter.AccessSettings))

set.Get("/settings", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
zhttp.SeeOther(w, "/settings/main")
SeeOther(w, r, "/settings/main")
}))
set.Get("/settings/main", zhttp.Wrap(func(w http.ResponseWriter, r *http.Request) error {
return h.main(nil)(w, r)
Expand Down Expand Up @@ -209,7 +209,7 @@ func (h settings) mainSave(w http.ResponseWriter, r *http.Request) error {
}

zhttp.Flash(w, T(r.Context(), "notify/saved|Saved!"))
return zhttp.SeeOther(w, "/settings")
return SeeOther(w, r, "/settings")
}

func (h settings) changeCode(w http.ResponseWriter, r *http.Request) error {
Expand Down Expand Up @@ -299,7 +299,7 @@ func (h settings) sitesAdd(w http.ResponseWriter, r *http.Request) error {
}

zhttp.Flash(w, T(r.Context(), "notify/restored-previously-deleted-site|Site ‘%(url)’ was previously deleted; restored site with all data.", newSite.URL(r.Context())))
return zhttp.SeeOther(w, "/settings/sites")
return SeeOther(w, r, "/settings/sites")
}

// Create new site.
Expand All @@ -317,11 +317,11 @@ func (h settings) sitesAdd(w http.ResponseWriter, r *http.Request) error {
})
if err != nil {
zhttp.FlashError(w, err.Error())
return zhttp.SeeOther(w, "/settings/sites")
return SeeOther(w, r, "/settings/sites")
}

zhttp.Flash(w, T(r.Context(), "notify/site-added|Site ‘%(url)’ added.", newSite.URL(r.Context())))
return zhttp.SeeOther(w, "/settings/sites")
return SeeOther(w, r, "/settings/sites")
}

func (h settings) getSite(ctx context.Context, id int64) (*goatcounter.Site, error) {
Expand Down Expand Up @@ -388,11 +388,11 @@ func (h settings) sitesRemove(w http.ResponseWriter, r *http.Request) error {
err = parent.ByID(r.Context(), *s.Parent)
if err != nil {
zlog.Error(err)
return zhttp.SeeOther(w, "/")
return SeeOther(w, r, "/")
}
return zhttp.SeeOther(w, parent.URL(r.Context()))
return SeeOther(w, r, parent.URL(r.Context()))
}
return zhttp.SeeOther(w, "/settings/sites")
return SeeOther(w, r, "/settings/sites")
}

func (h settings) sitesCopySettings(w http.ResponseWriter, r *http.Request) error {
Expand Down Expand Up @@ -436,7 +436,7 @@ func (h settings) sitesCopySettings(w http.ResponseWriter, r *http.Request) erro
}

zhttp.Flash(w, T(r.Context(), "notify/settings-copied-to-site|Settings copied to the selected sites."))
return zhttp.SeeOther(w, "/settings/sites")
return SeeOther(w, r, "/settings/sites")
}

func (h settings) purge(w http.ResponseWriter, r *http.Request) error {
Expand Down Expand Up @@ -486,7 +486,7 @@ func (h settings) purgeDo(w http.ResponseWriter, r *http.Request) error {
})

zhttp.Flash(w, T(r.Context(), "notify/started-background-process|Started in the background; may take about 10-20 seconds to fully process."))
return zhttp.SeeOther(w, "/settings/purge")
return SeeOther(w, r, "/settings/purge")
}

func (h settings) merge(w http.ResponseWriter, r *http.Request) error {
Expand All @@ -512,7 +512,7 @@ func (h settings) merge(w http.ResponseWriter, r *http.Request) error {
})

zhttp.Flash(w, T(r.Context(), "notify/started-background-process|Started in the background; may take about 10-20 seconds to fully process."))
return zhttp.SeeOther(w, "/settings/purge")
return SeeOther(w, r, "/settings/purge")
}

func (h settings) export(verr *zvalidate.Validator) zhttp.HandlerFunc {
Expand Down Expand Up @@ -548,7 +548,7 @@ func (h settings) exportDownload(w http.ResponseWriter, r *http.Request) error {
if err != nil {
if os.IsNotExist(err) {
zhttp.FlashError(w, T(r.Context(), "error/export-expired|It looks like there is no export yet or the export has expired."))
return zhttp.SeeOther(w, "/settings/export")
return SeeOther(w, r, "/settings/export")
}

return err
Expand Down Expand Up @@ -633,7 +633,7 @@ func (h settings) exportImport(w http.ResponseWriter, r *http.Request) error {
})

zhttp.Flash(w, T(r.Context(), "notify/import-started-in-background|Import started in the background; you’ll get an email when it’s done."))
return zhttp.SeeOther(w, "/settings/export")
return SeeOther(w, r, "/settings/export")
}

func (h settings) exportStart(w http.ResponseWriter, r *http.Request) error {
Expand All @@ -656,7 +656,7 @@ func (h settings) exportStart(w http.ResponseWriter, r *http.Request) error {
func() { export.Run(ctx, fp, true) })

zhttp.Flash(w, T(r.Context(), "notify/export-started-in-background|Export started in the background; you’ll get an email with a download link when it’s done."))
return zhttp.SeeOther(w, "/settings/export")
return SeeOther(w, r, "/settings/export")
}

func (h settings) delete(verr *zvalidate.Validator) zhttp.HandlerFunc {
Expand Down Expand Up @@ -713,7 +713,7 @@ func (h settings) deleteDo(w http.ResponseWriter, r *http.Request) error {
if err != nil {
return err
}
return zhttp.SeeOther(w, "https://"+goatcounter.Config(r.Context()).Domain)
return SeeOther(w, r, "https://"+goatcounter.Config(r.Context()).Domain)
}

func (h settings) users(verr *zvalidate.Validator) zhttp.HandlerFunc {
Expand Down Expand Up @@ -829,7 +829,7 @@ func (h settings) usersAdd(w http.ResponseWriter, r *http.Request) error {
})

zhttp.Flash(w, T(r.Context(), "notify/user-added|User ‘%(email)’ added.", newUser.Email))
return zhttp.SeeOther(w, "/settings/users")
return SeeOther(w, r, "/settings/users")
}

func (h settings) usersEdit(w http.ResponseWriter, r *http.Request) error {
Expand Down Expand Up @@ -887,7 +887,7 @@ func (h settings) usersEdit(w http.ResponseWriter, r *http.Request) error {
}

zhttp.Flash(w, T(r.Context(), "notify/users-edited|User ‘%(email)’ edited.", editUser.Email))
return zhttp.SeeOther(w, "/settings/users")
return SeeOther(w, r, "/settings/users")
}

func (h settings) usersRemove(w http.ResponseWriter, r *http.Request) error {
Expand Down Expand Up @@ -915,7 +915,7 @@ func (h settings) usersRemove(w http.ResponseWriter, r *http.Request) error {
}

zhttp.Flash(w, T(r.Context(), "notify/user-removed|User ‘%(email)’ removed.", user.Email))
return zhttp.SeeOther(w, "/settings/users")
return SeeOther(w, r, "/settings/users")
}

func (h settings) bosmang(w http.ResponseWriter, r *http.Request) error {
Expand Down
Loading

0 comments on commit b9ed19f

Please sign in to comment.