mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-04 01:30:53 +00:00 
			
		
		
		
	Make public URL generation configurable (#34250)
Follow up #32564 Co-authored-by: Jannis Pohl <838818+jannispl@users.noreply.github.com> Co-authored-by: Denys Konovalov <kontakt@denyskon.de>
This commit is contained in:
		@@ -63,14 +63,19 @@ RUN_USER = ; git
 | 
				
			|||||||
;PROTOCOL = http
 | 
					;PROTOCOL = http
 | 
				
			||||||
;;
 | 
					;;
 | 
				
			||||||
;; 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
 | 
					;DOMAIN = localhost
 | 
				
			||||||
;;
 | 
					;;
 | 
				
			||||||
;; The AppURL used by Gitea to generate absolute links, defaults to "{PROTOCOL}://{DOMAIN}:{HTTP_PORT}/".
 | 
					;; The AppURL is used to generate public URL links, defaults to "{PROTOCOL}://{DOMAIN}:{HTTP_PORT}/".
 | 
				
			||||||
;; Most users should set it to the real website URL of their Gitea instance when there is a reverse proxy.
 | 
					;; 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 =
 | 
					;ROOT_URL =
 | 
				
			||||||
;;
 | 
					;;
 | 
				
			||||||
 | 
					;; Controls how to detect the public URL.
 | 
				
			||||||
 | 
					;; Although it defaults to "legacy" (to avoid breaking existing users), most instances should use the "auto" behavior,
 | 
				
			||||||
 | 
					;; especially when the Gitea instance needs to be accessed in a container network.
 | 
				
			||||||
 | 
					;; * legacy: detect the public URL from "Host" header if "X-Forwarded-Proto" header exists, otherwise use "ROOT_URL".
 | 
				
			||||||
 | 
					;; * auto: always use "Host" header, and also use "X-Forwarded-Proto" header if it exists. If no "Host" header, use "ROOT_URL".
 | 
				
			||||||
 | 
					;PUBLIC_URL_DETECTION = legacy
 | 
				
			||||||
 | 
					;;
 | 
				
			||||||
;; For development purpose only. It makes Gitea handle sub-path ("/sub-path/owner/repo/...") directly when debugging without a reverse proxy.
 | 
					;; For development purpose only. It makes Gitea handle sub-path ("/sub-path/owner/repo/...") directly when debugging without a reverse proxy.
 | 
				
			||||||
;; DO NOT USE IT IN PRODUCTION!!!
 | 
					;; DO NOT USE IT IN PRODUCTION!!!
 | 
				
			||||||
;USE_SUB_URL_PATH = false
 | 
					;USE_SUB_URL_PATH = false
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -53,30 +53,31 @@ func getRequestScheme(req *http.Request) string {
 | 
				
			|||||||
	return ""
 | 
						return ""
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GuessCurrentAppURL tries to guess the current full app URL (with sub-path) by http headers. It always has a '/' suffix, exactly the same as setting.AppURL
 | 
					// GuessCurrentAppURL tries to guess the current full public URL (with sub-path) by http headers. It always has a '/' suffix, exactly the same as setting.AppURL
 | 
				
			||||||
 | 
					// TODO: should rename it to GuessCurrentPublicURL in the future
 | 
				
			||||||
func GuessCurrentAppURL(ctx context.Context) string {
 | 
					func GuessCurrentAppURL(ctx context.Context) string {
 | 
				
			||||||
	return GuessCurrentHostURL(ctx) + setting.AppSubURL + "/"
 | 
						return GuessCurrentHostURL(ctx) + setting.AppSubURL + "/"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GuessCurrentHostURL tries to guess the current full host URL (no sub-path) by http headers, there is no trailing slash.
 | 
					// GuessCurrentHostURL tries to guess the current full host URL (no sub-path) by http headers, there is no trailing slash.
 | 
				
			||||||
func GuessCurrentHostURL(ctx context.Context) string {
 | 
					func GuessCurrentHostURL(ctx context.Context) string {
 | 
				
			||||||
	req, ok := ctx.Value(RequestContextKey).(*http.Request)
 | 
						// Try the best guess to get the current host URL (will be used for public URL) by http headers.
 | 
				
			||||||
	if !ok {
 | 
					 | 
				
			||||||
		return strings.TrimSuffix(setting.AppURL, setting.AppSubURL+"/")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	// If no scheme provided by reverse proxy, then do not guess the AppURL, use the configured one.
 | 
					 | 
				
			||||||
	// At the moment, if site admin doesn't configure the proxy headers correctly, then Gitea would guess wrong.
 | 
						// At the moment, if site admin doesn't configure the proxy headers correctly, then Gitea would guess wrong.
 | 
				
			||||||
	// There are some cases:
 | 
						// There are some cases:
 | 
				
			||||||
	// 1. The reverse proxy is configured correctly, it passes "X-Forwarded-Proto/Host" headers. Perfect, Gitea can handle it correctly.
 | 
						// 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.
 | 
						// 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.
 | 
						// 3. There is no reverse proxy.
 | 
				
			||||||
	// Without more information, Gitea is impossible to distinguish between case 2 and case 3, then case 2 would result in
 | 
						// 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.
 | 
						// wrong guess like guessed public URL 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
 | 
						// So we introduced "PUBLIC_URL_DETECTION" option, to control the guessing behavior to satisfy different use cases.
 | 
				
			||||||
 | 
						req, ok := ctx.Value(RequestContextKey).(*http.Request)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return strings.TrimSuffix(setting.AppURL, setting.AppSubURL+"/")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	reqScheme := getRequestScheme(req)
 | 
						reqScheme := getRequestScheme(req)
 | 
				
			||||||
	if reqScheme == "" {
 | 
						if reqScheme == "" {
 | 
				
			||||||
		// if no reverse proxy header, try to use "Host" header for absolute URL
 | 
							// if no reverse proxy header, try to use "Host" header for absolute URL
 | 
				
			||||||
		if setting.UseHostHeader && req.Host != "" {
 | 
							if setting.PublicURLDetection == setting.PublicURLAuto && req.Host != "" {
 | 
				
			||||||
			return util.Iif(req.TLS == nil, "http://", "https://") + req.Host
 | 
								return util.Iif(req.TLS == nil, "http://", "https://") + req.Host
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		// fall back to default AppURL
 | 
							// fall back to default AppURL
 | 
				
			||||||
@@ -93,8 +94,8 @@ func GuessCurrentHostDomain(ctx context.Context) string {
 | 
				
			|||||||
	return util.IfZero(domain, host)
 | 
						return util.IfZero(domain, host)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// MakeAbsoluteURL tries to make a link to an absolute URL:
 | 
					// MakeAbsoluteURL tries to make a link to an absolute public URL:
 | 
				
			||||||
// * If link is empty, it returns the current app URL.
 | 
					// * If link is empty, it returns the current public URL.
 | 
				
			||||||
// * If link is absolute, it returns the link.
 | 
					// * If link is absolute, it returns the link.
 | 
				
			||||||
// * Otherwise, it returns the current host URL + link, the link itself should have correct sub-path (AppSubURL) if needed.
 | 
					// * Otherwise, it returns the current host URL + link, the link itself should have correct sub-path (AppSubURL) if needed.
 | 
				
			||||||
func MakeAbsoluteURL(ctx context.Context, link string) string {
 | 
					func MakeAbsoluteURL(ctx context.Context, link string) string {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -43,20 +43,37 @@ func TestIsRelativeURL(t *testing.T) {
 | 
				
			|||||||
func TestGuessCurrentHostURL(t *testing.T) {
 | 
					func TestGuessCurrentHostURL(t *testing.T) {
 | 
				
			||||||
	defer test.MockVariableValue(&setting.AppURL, "http://cfg-host/sub/")()
 | 
						defer test.MockVariableValue(&setting.AppURL, "http://cfg-host/sub/")()
 | 
				
			||||||
	defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
 | 
						defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
 | 
				
			||||||
	defer test.MockVariableValue(&setting.UseHostHeader, false)()
 | 
						headersWithProto := http.Header{"X-Forwarded-Proto": {"https"}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx := t.Context()
 | 
						t.Run("Legacy", func(t *testing.T) {
 | 
				
			||||||
	assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(ctx))
 | 
							defer test.MockVariableValue(&setting.PublicURLDetection, setting.PublicURLLegacy)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx = context.WithValue(ctx, RequestContextKey, &http.Request{Host: "localhost:3000"})
 | 
							assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(t.Context()))
 | 
				
			||||||
	assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(ctx))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	defer test.MockVariableValue(&setting.UseHostHeader, true)()
 | 
							// legacy: "Host" is not used when there is no "X-Forwarded-Proto" header
 | 
				
			||||||
	ctx = context.WithValue(ctx, RequestContextKey, &http.Request{Host: "http-host:3000"})
 | 
							ctx := context.WithValue(t.Context(), RequestContextKey, &http.Request{Host: "req-host:3000"})
 | 
				
			||||||
	assert.Equal(t, "http://http-host:3000", GuessCurrentHostURL(ctx))
 | 
							assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(ctx))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx = context.WithValue(ctx, RequestContextKey, &http.Request{Host: "http-host", TLS: &tls.ConnectionState{}})
 | 
							// if "X-Forwarded-Proto" exists, then use it and "Host" header
 | 
				
			||||||
	assert.Equal(t, "https://http-host", GuessCurrentHostURL(ctx))
 | 
							ctx = context.WithValue(t.Context(), RequestContextKey, &http.Request{Host: "req-host:3000", Header: headersWithProto})
 | 
				
			||||||
 | 
							assert.Equal(t, "https://req-host:3000", GuessCurrentHostURL(ctx))
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("Auto", func(t *testing.T) {
 | 
				
			||||||
 | 
							defer test.MockVariableValue(&setting.PublicURLDetection, setting.PublicURLAuto)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(t.Context()))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// auto: always use "Host" header, the scheme is determined by "X-Forwarded-Proto" header, or TLS config if no "X-Forwarded-Proto" header
 | 
				
			||||||
 | 
							ctx := context.WithValue(t.Context(), RequestContextKey, &http.Request{Host: "req-host:3000"})
 | 
				
			||||||
 | 
							assert.Equal(t, "http://req-host:3000", GuessCurrentHostURL(ctx))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							ctx = context.WithValue(t.Context(), RequestContextKey, &http.Request{Host: "req-host", TLS: &tls.ConnectionState{}})
 | 
				
			||||||
 | 
							assert.Equal(t, "https://req-host", GuessCurrentHostURL(ctx))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							ctx = context.WithValue(t.Context(), RequestContextKey, &http.Request{Host: "req-host:3000", Header: headersWithProto})
 | 
				
			||||||
 | 
							assert.Equal(t, "https://req-host:3000", GuessCurrentHostURL(ctx))
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestMakeAbsoluteURL(t *testing.T) {
 | 
					func TestMakeAbsoluteURL(t *testing.T) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -41,12 +41,20 @@ const (
 | 
				
			|||||||
	LandingPageLogin         LandingPage = "/user/login"
 | 
						LandingPageLogin         LandingPage = "/user/login"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						PublicURLAuto   = "auto"
 | 
				
			||||||
 | 
						PublicURLLegacy = "legacy"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Server settings
 | 
					// Server settings
 | 
				
			||||||
var (
 | 
					var (
 | 
				
			||||||
	// AppURL is the Application ROOT_URL. It always has a '/' suffix
 | 
						// AppURL is the Application ROOT_URL. It always has a '/' suffix
 | 
				
			||||||
	// It maps to ini:"ROOT_URL"
 | 
						// It maps to ini:"ROOT_URL"
 | 
				
			||||||
	AppURL string
 | 
						AppURL string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// PublicURLDetection controls how to use the HTTP request headers to detect public URL
 | 
				
			||||||
 | 
						PublicURLDetection string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// AppSubURL represents the sub-url mounting point for gitea, parsed from "ROOT_URL"
 | 
						// 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}'.
 | 
						// It is either "" or starts with '/' and ends without '/', such as '/{sub-path}'.
 | 
				
			||||||
	// This value is empty if site does not have sub-url.
 | 
						// This value is empty if site does not have sub-url.
 | 
				
			||||||
@@ -56,9 +64,6 @@ var (
 | 
				
			|||||||
	// to make it easier to debug sub-path related problems without a reverse proxy.
 | 
						// to make it easier to debug sub-path related problems without a reverse proxy.
 | 
				
			||||||
	UseSubURLPath bool
 | 
						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.
 | 
						// AppDataPath is the default path for storing data.
 | 
				
			||||||
	// It maps to ini:"APP_DATA_PATH" in [server] and defaults to AppWorkPath + "/data"
 | 
						// It maps to ini:"APP_DATA_PATH" in [server] and defaults to AppWorkPath + "/data"
 | 
				
			||||||
	AppDataPath string
 | 
						AppDataPath string
 | 
				
			||||||
@@ -283,10 +288,10 @@ func loadServerFrom(rootCfg ConfigProvider) {
 | 
				
			|||||||
	PerWritePerKbTimeout = sec.Key("PER_WRITE_PER_KB_TIMEOUT").MustDuration(PerWritePerKbTimeout)
 | 
						PerWritePerKbTimeout = sec.Key("PER_WRITE_PER_KB_TIMEOUT").MustDuration(PerWritePerKbTimeout)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	defaultAppURL := string(Protocol) + "://" + Domain + ":" + HTTPPort
 | 
						defaultAppURL := string(Protocol) + "://" + Domain + ":" + HTTPPort
 | 
				
			||||||
	AppURL = sec.Key("ROOT_URL").String()
 | 
						AppURL = sec.Key("ROOT_URL").MustString(defaultAppURL)
 | 
				
			||||||
	if AppURL == "" {
 | 
						PublicURLDetection = sec.Key("PUBLIC_URL_DETECTION").MustString(PublicURLLegacy)
 | 
				
			||||||
		UseHostHeader = true
 | 
						if PublicURLDetection != PublicURLAuto && PublicURLDetection != PublicURLLegacy {
 | 
				
			||||||
		AppURL = defaultAppURL
 | 
							log.Fatal("Invalid PUBLIC_URL_DETECTION value: %s", PublicURLDetection)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Check validity of AppURL
 | 
						// Check validity of AppURL
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -76,7 +76,6 @@ func TestShadowPassword(t *testing.T) {
 | 
				
			|||||||
func TestSelfCheckPost(t *testing.T) {
 | 
					func TestSelfCheckPost(t *testing.T) {
 | 
				
			||||||
	defer test.MockVariableValue(&setting.AppURL, "http://config/sub/")()
 | 
						defer test.MockVariableValue(&setting.AppURL, "http://config/sub/")()
 | 
				
			||||||
	defer test.MockVariableValue(&setting.AppSubURL, "/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")
 | 
						ctx, resp := contexttest.MockContext(t, "GET http://host/sub/admin/self_check?location_origin=http://frontend")
 | 
				
			||||||
	SelfCheckPost(ctx)
 | 
						SelfCheckPost(ctx)
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user