From af5b1503ec131d750778618ffcb50618fe6e9ce7 Mon Sep 17 00:00:00 2001 From: jannispl <838818+jannispl@users.noreply.github.com> Date: Tue, 19 Nov 2024 12:57:34 +0100 Subject: [PATCH 1/3] Add config option "USE_HOST_HEADER" Resolves #32554 This option can be set to make Gitea always use the "Host" request header for construction of absolute URLs. --- modules/httplib/url.go | 13 ++++++++++--- modules/httplib/url_test.go | 27 +++++++++++++++++++++++++++ modules/setting/server.go | 5 +++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/modules/httplib/url.go b/modules/httplib/url.go index e3bad1e5fba40..155fa59901657 100644 --- a/modules/httplib/url.go +++ b/modules/httplib/url.go @@ -69,11 +69,18 @@ func GuessCurrentHostURL(ctx context.Context) string { // 1. The reverse proxy is configured correctly, it passes "X-Forwarded-Proto/Host" headers. Perfect, Gitea can handle it correctly. // 2. The reverse proxy is not configured correctly, doesn't pass "X-Forwarded-Proto/Host" headers, eg: only one "proxy_pass http://gitea:3000" in Nginx. // 3. There is no reverse proxy. - // Without an extra config option, Gitea is impossible to distinguish between case 2 and case 3, - // then case 2 would result in wrong guess like guessed AppURL becomes "http://gitea:3000/", which is not accessible by end users. - // So in the future maybe it should introduce a new config option, to let site admin decide how to guess the AppURL. + // With the "USE_HOST_HEADER" config option disabled (default), Gitea is impossible to distinguish between case 2 and case 3, + // When enabling "USE_HOST_HEADER", any reverse proxies must be configured to properly pass "X-Forwarded-Proto/Host" headers, + // otherwise this would result in wrong guess like guessed AppURL becomes "http://gitea:3000/", which is not accessible by end users. reqScheme := getRequestScheme(req) if reqScheme == "" { + if setting.UseHostHeader && req.Host != "" { + if req.TLS != nil { + return "https://" + req.Host + } + return "http://" + req.Host + } + return strings.TrimSuffix(setting.AppURL, setting.AppSubURL+"/") } // X-Forwarded-Host has many problems: non-standard, not well-defined (X-Forwarded-Port or not), conflicts with Host header. diff --git a/modules/httplib/url_test.go b/modules/httplib/url_test.go index fc6c91cd3a3ea..3d19a507df5c3 100644 --- a/modules/httplib/url_test.go +++ b/modules/httplib/url_test.go @@ -5,6 +5,7 @@ package httplib import ( "context" + "crypto/tls" "net/http" "testing" @@ -39,6 +40,32 @@ func TestIsRelativeURL(t *testing.T) { } } +func TestGuessCurrentHostURL(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, "http://cfg-host/sub/")() + defer test.MockVariableValue(&setting.AppSubURL, "/sub")() + + ctx := context.Background() + assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(ctx)) + + ctx = context.WithValue(ctx, RequestContextKey, &http.Request{ + Host: "localhost:3000", + }) + assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(ctx)) + + defer test.MockVariableValue(&setting.UseHostHeader, true)() + + ctx = context.WithValue(ctx, RequestContextKey, &http.Request{ + Host: "localhost:3000", + }) + assert.Equal(t, "http://localhost:3000", GuessCurrentHostURL(ctx)) + + ctx = context.WithValue(ctx, RequestContextKey, &http.Request{ + Host: "localhost", + TLS: &tls.ConnectionState{}, + }) + assert.Equal(t, "https://localhost", GuessCurrentHostURL(ctx)) +} + func TestMakeAbsoluteURL(t *testing.T) { defer test.MockVariableValue(&setting.Protocol, "http")() defer test.MockVariableValue(&setting.AppURL, "http://cfg-host/sub/")() diff --git a/modules/setting/server.go b/modules/setting/server.go index d7a71578d4ab6..66635ce25f017 100644 --- a/modules/setting/server.go +++ b/modules/setting/server.go @@ -50,6 +50,10 @@ var ( AppSubURL string // UseSubURLPath makes Gitea handle requests with sub-path like "/sub-path/owner/repo/...", to make it easier to debug sub-path related problems without a reverse proxy. UseSubURLPath bool + // UseHostHeader makes Gitea always use the "Host" request header for construction of absolute URLs. + // This requires any reverse proxy to properly pass headers like "X-Forwarded-Proto" and "Host". + // It maps to ini:"USE_HOST_HEADER" in [server] and defaults to false + UseHostHeader bool // AppDataPath is the default path for storing data. // It maps to ini:"APP_DATA_PATH" in [server] and defaults to AppWorkPath + "/data" AppDataPath string @@ -277,6 +281,7 @@ func loadServerFrom(rootCfg ConfigProvider) { // This value is empty if site does not have sub-url. AppSubURL = strings.TrimSuffix(appURL.Path, "/") UseSubURLPath = sec.Key("USE_SUB_URL_PATH").MustBool(false) + UseHostHeader = sec.Key("USE_HOST_HEADER").MustBool(false) StaticURLPrefix = strings.TrimSuffix(sec.Key("STATIC_URL_PREFIX").MustString(AppSubURL), "/") // Check if Domain differs from AppURL domain than update it to AppURL's domain From 074a89ecf5c7356301b7a860f693db6388894050 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 20 Apr 2025 14:19:48 +0800 Subject: [PATCH 2/3] refactor --- custom/conf/app.example.ini | 37 ++++++++++++++--------------- modules/httplib/url.go | 14 +++++------ modules/httplib/url_test.go | 21 +++++------------ modules/setting/server.go | 46 +++++++++++++++++++++++++------------ 4 files changed, 62 insertions(+), 56 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index c10de9595393d..53e25a8c3bdb4 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -59,27 +59,16 @@ RUN_USER = ; git ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; -;; The protocol the server listens on. One of 'http', 'https', 'http+unix', 'fcgi' or 'fcgi+unix'. Defaults to 'http' -;; Note: Value must be lowercase. +;; The protocol the server listens on. One of "http", "https", "http+unix", "fcgi" or "fcgi+unix". ;PROTOCOL = http ;; -;; Expect PROXY protocol headers on connections -;USE_PROXY_PROTOCOL = false -;; -;; Use PROXY protocol in TLS Bridging mode -;PROXY_PROTOCOL_TLS_BRIDGING = false -;; -; Timeout to wait for PROXY protocol header (set to 0 to have no timeout) -;PROXY_PROTOCOL_HEADER_TIMEOUT=5s -;; -; Accept PROXY protocol headers with UNKNOWN type -;PROXY_PROTOCOL_ACCEPT_UNKNOWN=false -;; -;; Set the domain for the server +;; Set the domain for the server. +;; Most users should set it to the real website domain of their Gitea instance. ;DOMAIN = localhost ;; ;; The AppURL used by Gitea to generate absolute links, defaults to "{PROTOCOL}://{DOMAIN}:{HTTP_PORT}/". -;; Most users should set it to the real website URL of their Gitea instance. +;; Most users should set it to the real website URL of their Gitea instance when there is a reverse proxy. +;; When it is empty, Gitea will use HTTP "Host" header to generate ROOT_URL, and fall back to the default one if no "Host" header. ;ROOT_URL = ;; ;; For development purpose only. It makes Gitea handle sub-path ("/sub-path/owner/repo/...") directly when debugging without a reverse proxy. @@ -90,13 +79,25 @@ RUN_USER = ; git ;STATIC_URL_PREFIX = ;; ;; The address to listen on. Either a IPv4/IPv6 address or the path to a unix socket. -;; If PROTOCOL is set to `http+unix` or `fcgi+unix`, this should be the name of the Unix socket file to use. +;; If PROTOCOL is set to "http+unix" or "fcgi+unix", this should be the name of the Unix socket file to use. ;; Relative paths will be made absolute against the _`AppWorkPath`_. ;HTTP_ADDR = 0.0.0.0 ;; -;; The port to listen on. Leave empty when using a unix socket. +;; The port to listen on for "http" or "https" protocol. Leave empty when using a unix socket. ;HTTP_PORT = 3000 ;; +;; Expect PROXY protocol headers on connections +;USE_PROXY_PROTOCOL = false +;; +;; Use PROXY protocol in TLS Bridging mode +;PROXY_PROTOCOL_TLS_BRIDGING = false +;; +;; Timeout to wait for PROXY protocol header (set to 0 to have no timeout) +;PROXY_PROTOCOL_HEADER_TIMEOUT = 5s +;; +;; Accept PROXY protocol headers with UNKNOWN type +;PROXY_PROTOCOL_ACCEPT_UNKNOWN = false +;; ;; If REDIRECT_OTHER_PORT is true, and PROTOCOL is set to https an http server ;; will be started on PORT_TO_REDIRECT and it will redirect plain, non-secure http requests to the main ;; ROOT_URL. Defaults are false for REDIRECT_OTHER_PORT and 80 for diff --git a/modules/httplib/url.go b/modules/httplib/url.go index a5d6f6ab8a2f4..dabc1f5f450c5 100644 --- a/modules/httplib/url.go +++ b/modules/httplib/url.go @@ -70,18 +70,16 @@ func GuessCurrentHostURL(ctx context.Context) string { // 1. The reverse proxy is configured correctly, it passes "X-Forwarded-Proto/Host" headers. Perfect, Gitea can handle it correctly. // 2. The reverse proxy is not configured correctly, doesn't pass "X-Forwarded-Proto/Host" headers, eg: only one "proxy_pass http://gitea:3000" in Nginx. // 3. There is no reverse proxy. - // With the "USE_HOST_HEADER" config option disabled (default), Gitea is impossible to distinguish between case 2 and case 3, - // When enabling "USE_HOST_HEADER", any reverse proxies must be configured to properly pass "X-Forwarded-Proto/Host" headers, - // otherwise this would result in wrong guess like guessed AppURL becomes "http://gitea:3000/", which is not accessible by end users. + // Without more information, Gitea is impossible to distinguish between case 2 and case 3, then case 2 would result in + // wrong guess like guessed AppURL becomes "http://gitea:3000/" behind a "https" reverse proxy, which is not accessible by end users. + // So we introduced "UseHostHeader" option, it could be enabled by setting "ROOT_URL" to empty reqScheme := getRequestScheme(req) if reqScheme == "" { + // if no reverse proxy header, try to use "Host" header for absolute URL if setting.UseHostHeader && req.Host != "" { - if req.TLS != nil { - return "https://" + req.Host - } - return "http://" + req.Host + return util.Iif(req.TLS == nil, "http://", "https://") + req.Host } - + // fall back to default AppURL return strings.TrimSuffix(setting.AppURL, setting.AppSubURL+"/") } // X-Forwarded-Host has many problems: non-standard, not well-defined (X-Forwarded-Port or not), conflicts with Host header. diff --git a/modules/httplib/url_test.go b/modules/httplib/url_test.go index ebc86925dc84d..b62bb35e7246f 100644 --- a/modules/httplib/url_test.go +++ b/modules/httplib/url_test.go @@ -42,28 +42,19 @@ func TestIsRelativeURL(t *testing.T) { func TestGuessCurrentHostURL(t *testing.T) { defer test.MockVariableValue(&setting.AppURL, "http://cfg-host/sub/")() - defer test.MockVariableValue(&setting.AppSubURL, "/sub")() - ctx := context.Background() + ctx := t.Context() assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(ctx)) - ctx = context.WithValue(ctx, RequestContextKey, &http.Request{ - Host: "localhost:3000", - }) + ctx = context.WithValue(ctx, RequestContextKey, &http.Request{Host: "localhost:3000"}) assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(ctx)) defer test.MockVariableValue(&setting.UseHostHeader, true)() + ctx = context.WithValue(ctx, RequestContextKey, &http.Request{Host: "http-host:3000"}) + assert.Equal(t, "http://http-host:3000", GuessCurrentHostURL(ctx)) - ctx = context.WithValue(ctx, RequestContextKey, &http.Request{ - Host: "localhost:3000", - }) - assert.Equal(t, "http://localhost:3000", GuessCurrentHostURL(ctx)) - - ctx = context.WithValue(ctx, RequestContextKey, &http.Request{ - Host: "localhost", - TLS: &tls.ConnectionState{}, - }) - assert.Equal(t, "https://localhost", GuessCurrentHostURL(ctx)) + ctx = context.WithValue(ctx, RequestContextKey, &http.Request{Host: "http-host", TLS: &tls.ConnectionState{}}) + assert.Equal(t, "https://http-host", GuessCurrentHostURL(ctx)) } func TestMakeAbsoluteURL(t *testing.T) { diff --git a/modules/setting/server.go b/modules/setting/server.go index 902c669b61297..e523d8fc72485 100644 --- a/modules/setting/server.go +++ b/modules/setting/server.go @@ -46,29 +46,37 @@ var ( // AppURL is the Application ROOT_URL. It always has a '/' suffix // It maps to ini:"ROOT_URL" AppURL string - // AppSubURL represents the sub-url mounting point for gitea. It is either "" or starts with '/' and ends without '/', such as '/{subpath}'. + + // AppSubURL represents the sub-url mounting point for gitea, parsed from "ROOT_URL" + // It is either "" or starts with '/' and ends without '/', such as '/{sub-path}'. // This value is empty if site does not have sub-url. AppSubURL string - // UseSubURLPath makes Gitea handle requests with sub-path like "/sub-path/owner/repo/...", to make it easier to debug sub-path related problems without a reverse proxy. + + // UseSubURLPath makes Gitea handle requests with sub-path like "/sub-path/owner/repo/...", + // to make it easier to debug sub-path related problems without a reverse proxy. UseSubURLPath bool - // UseHostHeader makes Gitea always use the "Host" request header for construction of absolute URLs. - // This requires any reverse proxy to properly pass headers like "X-Forwarded-Proto" and "Host". - // It maps to ini:"USE_HOST_HEADER" in [server] and defaults to false + + // UseHostHeader makes Gitea prefer to use the "Host" request header for construction of absolute URLs. UseHostHeader bool + // AppDataPath is the default path for storing data. // It maps to ini:"APP_DATA_PATH" in [server] and defaults to AppWorkPath + "/data" AppDataPath string + // LocalURL is the url for locally running applications to contact Gitea. It always has a '/' suffix // It maps to ini:"LOCAL_ROOT_URL" in [server] LocalURL string - // AssetVersion holds a opaque value that is used for cache-busting assets + + // AssetVersion holds an opaque value that is used for cache-busting assets AssetVersion string - appTempPathInternal string // the temporary path for the app, it is only an internal variable, do not use it, always use AppDataTempDir + // appTempPathInternal is the temporary path for the app, it is only an internal variable + // DO NOT use it directly, always use AppDataTempDir + appTempPathInternal string Protocol Scheme - UseProxyProtocol bool // `ini:"USE_PROXY_PROTOCOL"` - ProxyProtocolTLSBridging bool //`ini:"PROXY_PROTOCOL_TLS_BRIDGING"` + UseProxyProtocol bool + ProxyProtocolTLSBridging bool ProxyProtocolHeaderTimeout time.Duration ProxyProtocolAcceptUnknown bool Domain string @@ -185,13 +193,14 @@ func loadServerFrom(rootCfg ConfigProvider) { EnableAcme = sec.Key("ENABLE_LETSENCRYPT").MustBool(false) } - Protocol = HTTP protocolCfg := sec.Key("PROTOCOL").String() if protocolCfg != "https" && EnableAcme { log.Fatal("ACME could only be used with HTTPS protocol") } switch protocolCfg { + case "http": + Protocol = HTTP case "https": Protocol = HTTPS if EnableAcme { @@ -247,7 +256,7 @@ func loadServerFrom(rootCfg ConfigProvider) { case "unix": log.Warn("unix PROTOCOL value is deprecated, please use http+unix") fallthrough - case "http+unix": + default: // "http+unix" Protocol = HTTPUnix } UnixSocketPermissionRaw := sec.Key("UNIX_SOCKET_PERMISSION").MustString("666") @@ -260,6 +269,8 @@ func loadServerFrom(rootCfg ConfigProvider) { if !filepath.IsAbs(HTTPAddr) { HTTPAddr = filepath.Join(AppWorkPath, HTTPAddr) } + default: + log.Fatal("Invalid PROTOCOL %q", Protocol) } UseProxyProtocol = sec.Key("USE_PROXY_PROTOCOL").MustBool(false) ProxyProtocolTLSBridging = sec.Key("PROXY_PROTOCOL_TLS_BRIDGING").MustBool(false) @@ -272,12 +283,16 @@ func loadServerFrom(rootCfg ConfigProvider) { PerWritePerKbTimeout = sec.Key("PER_WRITE_PER_KB_TIMEOUT").MustDuration(PerWritePerKbTimeout) defaultAppURL := string(Protocol) + "://" + Domain + ":" + HTTPPort - AppURL = sec.Key("ROOT_URL").MustString(defaultAppURL) + AppURL = sec.Key("ROOT_URL").String() + if AppURL == "" { + UseHostHeader = true + AppURL = defaultAppURL + } // Check validity of AppURL appURL, err := url.Parse(AppURL) if err != nil { - log.Fatal("Invalid ROOT_URL '%s': %s", AppURL, err) + log.Fatal("Invalid ROOT_URL %q: %s", AppURL, err) } // Remove default ports from AppURL. // (scheme-based URL normalization, RFC 3986 section 6.2.3) @@ -291,7 +306,6 @@ func loadServerFrom(rootCfg ConfigProvider) { // This value is empty if site does not have sub-url. AppSubURL = strings.TrimSuffix(appURL.Path, "/") UseSubURLPath = sec.Key("USE_SUB_URL_PATH").MustBool(false) - UseHostHeader = sec.Key("USE_HOST_HEADER").MustBool(false) StaticURLPrefix = strings.TrimSuffix(sec.Key("STATIC_URL_PREFIX").MustString(AppSubURL), "/") // Check if Domain differs from AppURL domain than update it to AppURL's domain @@ -314,13 +328,15 @@ func loadServerFrom(rootCfg ConfigProvider) { defaultLocalURL = AppURL case FCGIUnix: defaultLocalURL = AppURL - default: + case HTTP, HTTPS: defaultLocalURL = string(Protocol) + "://" if HTTPAddr == "0.0.0.0" { defaultLocalURL += net.JoinHostPort("localhost", HTTPPort) + "/" } else { defaultLocalURL += net.JoinHostPort(HTTPAddr, HTTPPort) + "/" } + default: + log.Fatal("Invalid PROTOCOL %q", Protocol) } LocalURL = sec.Key("LOCAL_ROOT_URL").MustString(defaultLocalURL) LocalURL = strings.TrimRight(LocalURL, "/") + "/" From bf7252330a1f700889e6516b94e3a4cbd5b55565 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 20 Apr 2025 18:31:09 +0800 Subject: [PATCH 3/3] fix test --- modules/httplib/url_test.go | 2 ++ modules/setting/server.go | 2 +- routers/web/admin/admin_test.go | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/httplib/url_test.go b/modules/httplib/url_test.go index b62bb35e7246f..0e198d7d732c8 100644 --- a/modules/httplib/url_test.go +++ b/modules/httplib/url_test.go @@ -42,6 +42,8 @@ func TestIsRelativeURL(t *testing.T) { func TestGuessCurrentHostURL(t *testing.T) { defer test.MockVariableValue(&setting.AppURL, "http://cfg-host/sub/")() + defer test.MockVariableValue(&setting.AppSubURL, "/sub")() + defer test.MockVariableValue(&setting.UseHostHeader, false)() ctx := t.Context() assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(ctx)) diff --git a/modules/setting/server.go b/modules/setting/server.go index e523d8fc72485..41b0ca89599a9 100644 --- a/modules/setting/server.go +++ b/modules/setting/server.go @@ -199,7 +199,7 @@ func loadServerFrom(rootCfg ConfigProvider) { } switch protocolCfg { - case "http": + case "", "http": Protocol = HTTP case "https": Protocol = HTTPS diff --git a/routers/web/admin/admin_test.go b/routers/web/admin/admin_test.go index a568c7c5c8186..04fad4663c4d3 100644 --- a/routers/web/admin/admin_test.go +++ b/routers/web/admin/admin_test.go @@ -76,6 +76,7 @@ func TestShadowPassword(t *testing.T) { func TestSelfCheckPost(t *testing.T) { defer test.MockVariableValue(&setting.AppURL, "http://config/sub/")() defer test.MockVariableValue(&setting.AppSubURL, "/sub")() + defer test.MockVariableValue(&setting.UseHostHeader, false)() ctx, resp := contexttest.MockContext(t, "GET http://host/sub/admin/self_check?location_origin=http://frontend") SelfCheckPost(ctx)