mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-26 12:27:06 +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) { | ||||||
|  | 		defer test.MockVariableValue(&setting.PublicURLDetection, setting.PublicURLLegacy)() | ||||||
|  |  | ||||||
|  | 		assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(t.Context())) | ||||||
|  |  | ||||||
|  | 		// legacy: "Host" is not used when there is no "X-Forwarded-Proto" header | ||||||
|  | 		ctx := context.WithValue(t.Context(), RequestContextKey, &http.Request{Host: "req-host:3000"}) | ||||||
| 		assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(ctx)) | 		assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(ctx)) | ||||||
|  |  | ||||||
| 	ctx = context.WithValue(ctx, RequestContextKey, &http.Request{Host: "localhost:3000"}) | 		// if "X-Forwarded-Proto" exists, then use it and "Host" header | ||||||
| 	assert.Equal(t, "http://cfg-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)) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
| 	defer test.MockVariableValue(&setting.UseHostHeader, true)() | 	t.Run("Auto", func(t *testing.T) { | ||||||
| 	ctx = context.WithValue(ctx, RequestContextKey, &http.Request{Host: "http-host:3000"}) | 		defer test.MockVariableValue(&setting.PublicURLDetection, setting.PublicURLAuto)() | ||||||
| 	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, "http://cfg-host", GuessCurrentHostURL(t.Context())) | ||||||
| 	assert.Equal(t, "https://http-host", GuessCurrentHostURL(ctx)) |  | ||||||
|  | 		// 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
	 wxiaoguang
					wxiaoguang