Skip to content

Make ROOT_URL support using request Host header #32564

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 19 additions & 18 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
11 changes: 8 additions & 3 deletions modules/httplib/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +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.
// 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.
// 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 != "" {
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.
Expand Down
20 changes: 20 additions & 0 deletions modules/httplib/url_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package httplib

import (
"context"
"crypto/tls"
"net/http"
"testing"

Expand Down Expand Up @@ -39,6 +40,25 @@ 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))

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: "http-host", TLS: &tls.ConnectionState{}})
assert.Equal(t, "https://http-host", GuessCurrentHostURL(ctx))
}

func TestMakeAbsoluteURL(t *testing.T) {
defer test.MockVariableValue(&setting.Protocol, "http")()
defer test.MockVariableValue(&setting.AppURL, "http://cfg-host/sub/")()
Expand Down
43 changes: 32 additions & 11 deletions modules/setting/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,25 +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 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
Expand Down Expand Up @@ -181,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 {
Expand Down Expand Up @@ -243,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")
Expand All @@ -256,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)
Expand All @@ -268,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)
Expand Down Expand Up @@ -309,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, "/") + "/"
Expand Down
1 change: 1 addition & 0 deletions routers/web/admin/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down