From 7cb66e26e5b33e8e8898d1c7e353b385a9bb8b4b Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Wed, 16 Jul 2025 15:00:46 +0100 Subject: [PATCH] http: add "Use reverse proxy host" option added "Use reverse-proxy reported "Host" when possible" option, which will prefer using the "Host" or "X-Forwarded-Host" values instead of "External jfa-go URL" in the web app. To do so, app.ExternalDomain/URI are now functions which take *gin.Context (the latter optionally). The protocol for the request is determined from X-Forwarded-Proto(col), so make sure your proxy includes it. The wiki will have been updated to mention the new option. --- auth.go | 5 ++-- config.go | 48 +++++++++++++++++++++++++++++++++----- config/config-base.yaml | 9 ++++--- email.go | 4 ++-- logmessages/logmessages.go | 19 ++++++++------- main.go | 3 ++- pwreset.go | 2 +- user-auth.go | 4 ++-- views.go | 4 ++-- 9 files changed, 69 insertions(+), 29 deletions(-) diff --git a/auth.go b/auth.go index 1296630..1c065df 100644 --- a/auth.go +++ b/auth.go @@ -266,8 +266,7 @@ func (app *appContext) getTokenLogin(gc *gin.Context) { respond(500, "Couldn't generate token", gc) return } - // host := gc.Request.URL.Hostname() - host := app.ExternalDomain + host := app.ExternalDomain(gc) // Before you think this is broken: the first "true" arg is for "secure", i.e. only HTTPS! gc.SetCookie("refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, "/", host, true, true) @@ -329,7 +328,7 @@ func (app *appContext) getTokenRefresh(gc *gin.Context) { return } // host := gc.Request.URL.Hostname() - host := app.ExternalDomain + host := app.ExternalDomain(gc) gc.SetCookie("refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, "/", host, true, true) gc.JSON(200, getTokenDTO{jwt}) } diff --git a/config.go b/config.go index 9ad5b59..df3b1ee 100644 --- a/config.go +++ b/config.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/gin-gonic/gin" "github.com/hrfee/jfa-go/common" "github.com/hrfee/jfa-go/easyproxy" lm "github.com/hrfee/jfa-go/logmessages" @@ -68,6 +69,35 @@ func (app *appContext) MustCorrectURL(section, key, value string) { app.config.Section(section).Key(key).SetValue(v) } +// ExternalDomain returns the Host for the request, using the fixed app.externalDomain value unless app.UseProxyHost is true. +func (app *appContext) ExternalDomain(gc *gin.Context) string { + if !app.UseProxyHost || gc.Request.Host == "" { + return app.externalDomain + } + return gc.Request.Host +} + +// ExternalURI returns the External URI of jfa-go's root directory (by default, where the admin page is), using the fixed app.externalURI value unless app.UseProxyHost is true and gc is not nil. +// When nil is passed, app.externalURI is returned. +func (app *appContext) ExternalURI(gc *gin.Context) string { + if gc == nil { + return app.externalURI + } + + var proto string + if gc.Request.TLS != nil || gc.Request.Header.Get("X-Forwarded-Proto") == "https" || gc.Request.Header.Get("X-Forwarded-Protocol") == "https" { + proto = "https://" + } else { + proto = "http://" + } + + // app.debug.Printf("Request: %+v\n", gc.Request) + if app.UseProxyHost && gc.Request.Host != "" { + return proto + gc.Request.Host + PAGES.Base + } + return app.externalURI +} + func (app *appContext) loadConfig() error { var err error app.config, err = ini.ShadowLoad(app.configPath) @@ -108,16 +138,22 @@ func (app *appContext) loadConfig() error { app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".db")))) } - app.ExternalURI = strings.TrimSuffix(strings.TrimSuffix(app.config.Section("ui").Key("jfa_url").MustString(""), "/invite"), "/") - if !strings.HasSuffix(app.ExternalURI, PAGES.Base) { + // If true, app.ExternalDomain() will return one based on the reported Host (ideally reported in "Host" or "X-Forwarded-Host" by the reverse proxy), falling back to app.externalDomain if not set. + app.UseProxyHost = app.config.Section("ui").Key("use_proxy_host").MustBool(false) + app.externalURI = strings.TrimSuffix(strings.TrimSuffix(app.config.Section("ui").Key("jfa_url").MustString(""), "/invite"), "/") + if !strings.HasSuffix(app.externalURI, PAGES.Base) { app.err.Println(lm.NoURLSuffix) } - if app.ExternalURI == "" { - app.err.Println(lm.NoExternalHost + lm.LoginWontSave) + if app.externalURI == "" { + if app.UseProxyHost { + app.err.Println(lm.NoExternalHost + lm.LoginWontSave + lm.SetExternalHostDespiteUseProxyHost) + } else { + app.err.Println(lm.NoExternalHost + lm.LoginWontSave) + } } - u, err := url.Parse(app.ExternalURI) + u, err := url.Parse(app.externalURI) if err == nil { - app.ExternalDomain = u.Hostname() + app.externalDomain = u.Hostname() } app.config.Section("email").Key("no_username").SetValue(strconv.FormatBool(app.config.Section("email").Key("no_username").MustBool(false))) diff --git a/config/config-base.yaml b/config/config-base.yaml index 2084ec6..f25ac58 100644 --- a/config/config-base.yaml +++ b/config/config-base.yaml @@ -215,12 +215,15 @@ sections: - setting: jfa_url name: External jfa-go URL required: true - depends_true: enabled type: text value: http://accounts.jellyf.in:8056 description: The URL at which the jfa-go root (usually the admin page) is accessible, including - the subfolder if you use one. This is necessary because using a reverse proxy - means the program has no way of knowing the URL itself. + the subfolder if you use one. While your reverse proxy should report this anyway, server-side actions like sending invite messages don't receive such wisdom. + - setting: use_proxy_host + name: Use reverse-proxy reported "Host" when possible + type: bool + value: false + description: If enabled, the "Host" reported by your reverse proxy will be used in the web app, rather than the "External jfa-go URL" value. Useful if you regularly access jfa-go from more than one host/domain. Also, make sure your proxy passes X-Forwarded-Proto/X-Forwarded-Protocol. - setting: url_base name: Reverse Proxy subfolder requires_restart: true diff --git a/email.go b/email.go index a66511a..0e447c8 100644 --- a/email.go +++ b/email.go @@ -326,7 +326,7 @@ func (emailer *Emailer) confirmationValues(code, username, key string, app *appC } } else { message := app.config.Section("messages").Key("message").String() - inviteLink := app.ExternalURI + inviteLink := app.ExternalURI(nil) if code == "" { // Personal email change inviteLink = fmt.Sprintf("%s/my/confirm/%s", inviteLink, url.PathEscape(key)) } else { // Invite email confirmation @@ -394,7 +394,7 @@ func (emailer *Emailer) inviteValues(code string, invite Invite, app *appContext expiry := invite.ValidTill d, t, expiresIn := emailer.formatExpiry(expiry, false, app.datePattern, app.timePattern) message := app.config.Section("messages").Key("message").String() - inviteLink := fmt.Sprintf("%s%s/%s", app.ExternalURI, PAGES.Form, code) + inviteLink := fmt.Sprintf("%s%s/%s", app.ExternalURI(nil), PAGES.Form, code) template := map[string]interface{}{ "hello": emailer.lang.InviteEmail.get("hello"), "youHaveBeenInvited": emailer.lang.InviteEmail.get("youHaveBeenInvited"), diff --git a/logmessages/logmessages.go b/logmessages/logmessages.go index 160dabe..e3f7039 100644 --- a/logmessages/logmessages.go +++ b/logmessages/logmessages.go @@ -207,15 +207,16 @@ const ( FailedRestoreDB = "Failed to resotre database from \"%s\": %v" // config.go - EnableAllPWRMethods = "No PWR method preferences set in [user_page], all will be enabled" - InitProxy = "Initialized proxy @ \"%s\"" - FailedInitProxy = "Failed to initialize proxy @ \"%s\": %v\nStartup will pause for a bit to grab your attention." - NoURLSuffix = `Warning: Given "jfa_url"/"External jfa-go URL" value does not include "url_base" value!` - BadURLBase = `Warning: Given reverse proxy subfolder "%s" may conflict with the applications subpaths.` - RouteCollision = `Route Collision! Given reverse proxy subfolder "%s" or "URL Paths" settings likely conflict with the applications subpaths. Culprit: %v` - NoExternalHost = `No "External jfa-go URL" provided, set one in Settings > General.` - LoginWontSave = ` Your login won't save until you do.` - SubpathBlockMessage = `URLs: Root subfolder = "%s", Admin = "%s", My Account = "%s", Invite forms = "%s"` + EnableAllPWRMethods = "No PWR method preferences set in [user_page], all will be enabled" + InitProxy = "Initialized proxy @ \"%s\"" + FailedInitProxy = "Failed to initialize proxy @ \"%s\": %v\nStartup will pause for a bit to grab your attention." + NoURLSuffix = `Warning: Given "jfa_url"/"External jfa-go URL" value does not include "url_base" value!` + BadURLBase = `Warning: Given reverse proxy subfolder "%s" may conflict with the applications subpaths.` + RouteCollision = `Route Collision! Given reverse proxy subfolder "%s" or "URL Paths" settings likely conflict with the applications subpaths. Culprit: %v` + NoExternalHost = `No "External jfa-go URL" provided, set one in Settings > General.` + LoginWontSave = ` Logins may not save until you do.` + SetExternalHostDespiteUseProxyHost = ` This needs to be set even though use_proxy_host is enabled.` + SubpathBlockMessage = `URLs: Root subfolder = "%s", Admin = "%s", My Account = "%s", Invite forms = "%s"` // discord.go StartDaemon = "Started %s daemon" diff --git a/main.go b/main.go index 2ec8f7c..04ac95a 100644 --- a/main.go +++ b/main.go @@ -121,7 +121,8 @@ type appContext struct { host string port int version string - ExternalURI, ExternalDomain string + externalURI, externalDomain string // The latter lower-case as should be accessed through app.ExternalDomain() + UseProxyHost bool updater *Updater webhooks *WebhookSender newUpdate bool // Whether whatever's in update is new. diff --git a/pwreset.go b/pwreset.go index 5897b80..961b64a 100644 --- a/pwreset.go +++ b/pwreset.go @@ -30,7 +30,7 @@ func (app *appContext) GenInternalReset(userID string) (InternalPWR, error) { // GenResetLink generates and returns a password reset link. func (app *appContext) GenResetLink(pin string) (string, error) { - url := app.ExternalURI + url := app.ExternalURI(nil) var pinLink string if url == "" { return pinLink, errors.New(lm.NoExternalHost) diff --git a/user-auth.go b/user-auth.go index c80e213..c8581f2 100644 --- a/user-auth.go +++ b/user-auth.go @@ -65,7 +65,7 @@ func (app *appContext) getUserTokenLogin(gc *gin.Context) { } // host := gc.Request.URL.Hostname() - host := app.ExternalDomain + host := app.ExternalDomain(gc) uri := "/my" // FIXME: This seems like a bad idea? I think it's to deal with people having Reverse proxy subfolder/URL base set to /accounts. if strings.HasPrefix(gc.Request.RequestURI, PAGES.Base) { @@ -105,7 +105,7 @@ func (app *appContext) getUserTokenRefresh(gc *gin.Context) { } // host := gc.Request.URL.Hostname() - host := app.ExternalDomain + host := app.ExternalDomain(gc) gc.SetCookie("user-refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, "/my", host, true, true) gc.JSON(200, getTokenDTO{jwt}) } diff --git a/views.go b/views.go index 30d2abb..e0a284f 100644 --- a/views.go +++ b/views.go @@ -88,7 +88,7 @@ func (app *appContext) BasePageTemplateValues(gc *gin.Context, page Page, base g pages := PagePathsDTO{ PagePaths: PAGES, - ExternalURI: app.ExternalURI, + ExternalURI: app.ExternalURI(gc), TrueBase: PAGES.Base, } pages.Base = app.getURLBase(gc) @@ -742,7 +742,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) { discord := discordEnabled && app.config.Section("discord").Key("show_on_reg").MustBool(true) matrix := matrixEnabled && app.config.Section("matrix").Key("show_on_reg").MustBool(true) - userPageAddress := app.ExternalURI + PAGES.MyAccount + userPageAddress := app.ExternalURI(gc) + PAGES.MyAccount fromUser := "" if invite.ReferrerJellyfinID != "" {