mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	Make ROOT_URL support using request Host header (#32564)
Resolve #32554 --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
| @@ -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. | ||||
|   | ||||
| @@ -5,6 +5,7 @@ package httplib | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"crypto/tls" | ||||
| 	"net/http" | ||||
| 	"testing" | ||||
|  | ||||
| @@ -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/")() | ||||
|   | ||||
| @@ -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 | ||||
| @@ -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 { | ||||
| @@ -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") | ||||
| @@ -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) | ||||
| @@ -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) | ||||
| @@ -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, "/") + "/" | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user