From a58498cc430fdc5756b8a8f893eb865c5e7080b6 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Sun, 19 May 2024 23:22:54 +0800 Subject: [PATCH] Improve reverse proxy documents and clarify the AppURL guessing behavior (#31003) (#31020) Backport #31003 by wxiaoguang Fix #31002 1. Mention Make sure `Host` and `X-Fowarded-Proto` headers are correctly passed to Gitea 2. Clarify the basic requirements and move the "general configuration" to the top 3. Add a comment for the "container registry" 4. Use 1.21 behavior if the reverse proxy is not correctly configured Co-authored-by: wxiaoguang Co-authored-by: KN4CK3R --- .../administration/reverse-proxies.en-us.md | 92 ++++++++++--------- modules/httplib/url.go | 31 ++++--- modules/httplib/url_test.go | 12 +-- routers/api/packages/container/container.go | 2 + routers/web/admin/admin_test.go | 2 +- 5 files changed, 78 insertions(+), 61 deletions(-) diff --git a/docs/content/administration/reverse-proxies.en-us.md b/docs/content/administration/reverse-proxies.en-us.md index fe54c67d02..5fbd0eb0b7 100644 --- a/docs/content/administration/reverse-proxies.en-us.md +++ b/docs/content/administration/reverse-proxies.en-us.md @@ -17,15 +17,35 @@ menu: # Reverse Proxies +## General configuration + +1. Set `[server] ROOT_URL = https://git.example.com/` in your `app.ini` file. +2. Make the reverse-proxy pass `https://git.example.com/foo` to `http://gitea:3000/foo`. +3. Make sure the reverse-proxy does not decode the URI. The request `https://git.example.com/a%2Fb` should be passed as `http://gitea:3000/a%2Fb`. +4. Make sure `Host` and `X-Fowarded-Proto` headers are correctly passed to Gitea to make Gitea see the real URL being visited. + +### Use a sub-path + +Usually it's **not recommended** to put Gitea in a sub-path, it's not widely used and may have some issues in rare cases. + +To make Gitea work with a sub-path (eg: `https://common.example.com/gitea/`), +there are some extra requirements besides the general configuration above: + +1. Use `[server] ROOT_URL = https://common.example.com/gitea/` in your `app.ini` file. +2. Make the reverse-proxy pass `https://common.example.com/gitea/foo` to `http://gitea:3000/foo`. +3. The container registry requires a fixed sub-path `/v2` at the root level which must be configured: + - Make the reverse-proxy pass `https://common.example.com/v2` to `http://gitea:3000/v2`. + - Make sure the URI and headers are also correctly passed (see the general configuration above). + ## Nginx -If you want Nginx to serve your Gitea instance, add the following `server` section to the `http` section of `nginx.conf`: +If you want Nginx to serve your Gitea instance, add the following `server` section to the `http` section of `nginx.conf`. -``` +Make sure `client_max_body_size` is large enough, otherwise there would be "413 Request Entity Too Large" error when uploading large files. + +```nginx server { - listen 80; - server_name git.example.com; - + ... location / { client_max_body_size 512M; proxy_pass http://localhost:3000; @@ -39,37 +59,35 @@ server { } ``` -### Resolving Error: 413 Request Entity Too Large - -This error indicates nginx is configured to restrict the file upload size, -it affects attachment uploading, form posting, package uploading and LFS pushing, etc. -You can fine tune the `client_max_body_size` option according to [nginx document](http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size). - ## Nginx with a sub-path -In case you already have a site, and you want Gitea to share the domain name, you can setup Nginx to serve Gitea under a sub-path by adding the following `server` section inside the `http` section of `nginx.conf`: +In case you already have a site, and you want Gitea to share the domain name, +you can setup Nginx to serve Gitea under a sub-path by adding the following `server` section +into the `http` section of `nginx.conf`: -``` +```nginx server { - listen 80; - server_name git.example.com; - - # Note: Trailing slash - location /gitea/ { + ... + location ~ ^/(gitea|v2)($|/) { client_max_body_size 512M; - # make nginx use unescaped URI, keep "%2F" as is + # make nginx use unescaped URI, keep "%2F" as-is, remove the "/gitea" sub-path prefix, pass "/v2" as-is. rewrite ^ $request_uri; - rewrite ^/gitea(/.*) $1 break; + rewrite ^(/gitea)?(/.*) $2 break; proxy_pass http://127.0.0.1:3000$uri; # other common HTTP headers, see the "Nginx" config section above - proxy_set_header ... + proxy_set_header Connection $http_connection; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; } } ``` -Then you **MUST** set something like `[server] ROOT_URL = http://git.example.com/git/` correctly in your configuration. +Then you **MUST** set something like `[server] ROOT_URL = http://git.example.com/gitea/` correctly in your configuration. ## Nginx and serve static resources directly @@ -93,7 +111,7 @@ or use a cdn for the static files. Set `[server] STATIC_URL_PREFIX = /_/static` in your configuration. -```apacheconf +```nginx server { listen 80; server_name git.example.com; @@ -112,7 +130,7 @@ server { Set `[server] STATIC_URL_PREFIX = http://cdn.example.com/gitea` in your configuration. -```apacheconf +```nginx # application server running Gitea server { listen 80; @@ -124,7 +142,7 @@ server { } ``` -```apacheconf +```nginx # static content delivery server server { listen 80; @@ -151,6 +169,8 @@ If you want Apache HTTPD to serve your Gitea instance, you can add the following ProxyRequests off AllowEncodedSlashes NoDecode ProxyPass / http://localhost:3000/ nocanon + ProxyPreserveHost On + RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME} ``` @@ -172,6 +192,8 @@ In case you already have a site, and you want Gitea to share the domain name, yo AllowEncodedSlashes NoDecode # Note: no trailing slash after either /git or port ProxyPass /git http://localhost:3000 nocanon + ProxyPreserveHost On + RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME} ``` @@ -183,7 +205,7 @@ Note: The following Apache HTTPD mods must be enabled: `proxy`, `proxy_http`. If you want Caddy to serve your Gitea instance, you can add the following server block to your Caddyfile: -```apacheconf +``` git.example.com { reverse_proxy localhost:3000 } @@ -193,7 +215,7 @@ git.example.com { In case you already have a site, and you want Gitea to share the domain name, you can setup Caddy to serve Gitea under a sub-path by adding the following to your server block in your Caddyfile: -```apacheconf +``` git.example.com { route /git/* { uri strip_prefix /git @@ -371,19 +393,3 @@ gitea: This config assumes that you are handling HTTPS on the traefik side and using HTTP between Gitea and traefik. Then you **MUST** set something like `[server] ROOT_URL = http://example.com/gitea/` correctly in your configuration. - -## General sub-path configuration - -Usually it's not recommended to put Gitea in a sub-path, it's not widely used and may have some issues in rare cases. - -If you really need to do so, to make Gitea works with sub-path (eg: `http://example.com/gitea/`), here are the requirements: - -1. Set `[server] ROOT_URL = http://example.com/gitea/` in your `app.ini` file. -2. Make the reverse-proxy pass `http://example.com/gitea/foo` to `http://gitea-server:3000/foo`. -3. Make sure the reverse-proxy not decode the URI, the request `http://example.com/gitea/a%2Fb` should be passed as `http://gitea-server:3000/a%2Fb`. - -## Docker / Container Registry - -The container registry uses a fixed sub-path `/v2` which can't be changed. -Even if you deploy Gitea with a different sub-path, `/v2` will be used by the `docker` client. -Therefore you may need to add an additional route to your reverse proxy configuration. diff --git a/modules/httplib/url.go b/modules/httplib/url.go index 541c4f325b..8dc5b71181 100644 --- a/modules/httplib/url.go +++ b/modules/httplib/url.go @@ -32,7 +32,7 @@ func IsRelativeURL(s string) bool { return err == nil && urlIsRelative(s, u) } -func guessRequestScheme(req *http.Request, def string) string { +func getRequestScheme(req *http.Request) string { // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto if s := req.Header.Get("X-Forwarded-Proto"); s != "" { return s @@ -49,10 +49,10 @@ func guessRequestScheme(req *http.Request, def string) string { if s := req.Header.Get("X-Forwarded-Ssl"); s != "" { return util.Iif(s == "on", "https", "http") } - return def + return "" } -func guessForwardedHost(req *http.Request) string { +func getForwardedHost(req *http.Request) string { // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host return req.Header.Get("X-Forwarded-Host") } @@ -63,15 +63,24 @@ func GuessCurrentAppURL(ctx context.Context) string { if !ok { return setting.AppURL } - if host := guessForwardedHost(req); host != "" { - // if it is behind a reverse proxy, use "https" as default scheme in case the site admin forgets to set the correct forwarded-protocol headers - return guessRequestScheme(req, "https") + "://" + host + setting.AppSubURL + "/" - } else if req.Host != "" { - // if it is not behind a reverse proxy, use the scheme from config options, meanwhile use "https" as much as possible - defaultScheme := util.Iif(setting.Protocol == "http", "http", "https") - return guessRequestScheme(req, defaultScheme) + "://" + req.Host + 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. + // There are some cases: + // 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. + reqScheme := getRequestScheme(req) + if reqScheme == "" { + return setting.AppURL } - return setting.AppURL + reqHost := getForwardedHost(req) + if reqHost == "" { + reqHost = req.Host + } + return reqScheme + "://" + reqHost + setting.AppSubURL + "/" } func MakeAbsoluteURL(ctx context.Context, s string) string { diff --git a/modules/httplib/url_test.go b/modules/httplib/url_test.go index e021cd610d..9980cb74e8 100644 --- a/modules/httplib/url_test.go +++ b/modules/httplib/url_test.go @@ -41,19 +41,19 @@ func TestIsRelativeURL(t *testing.T) { func TestMakeAbsoluteURL(t *testing.T) { defer test.MockVariableValue(&setting.Protocol, "http")() - defer test.MockVariableValue(&setting.AppURL, "http://the-host/sub/")() + defer test.MockVariableValue(&setting.AppURL, "http://cfg-host/sub/")() defer test.MockVariableValue(&setting.AppSubURL, "/sub")() ctx := context.Background() - assert.Equal(t, "http://the-host/sub/", MakeAbsoluteURL(ctx, "")) - assert.Equal(t, "http://the-host/sub/foo", MakeAbsoluteURL(ctx, "foo")) - assert.Equal(t, "http://the-host/sub/foo", MakeAbsoluteURL(ctx, "/foo")) + assert.Equal(t, "http://cfg-host/sub/", MakeAbsoluteURL(ctx, "")) + assert.Equal(t, "http://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "foo")) + assert.Equal(t, "http://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "/foo")) assert.Equal(t, "http://other/foo", MakeAbsoluteURL(ctx, "http://other/foo")) ctx = context.WithValue(ctx, RequestContextKey, &http.Request{ Host: "user-host", }) - assert.Equal(t, "http://user-host/sub/foo", MakeAbsoluteURL(ctx, "/foo")) + assert.Equal(t, "http://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "/foo")) ctx = context.WithValue(ctx, RequestContextKey, &http.Request{ Host: "user-host", @@ -61,7 +61,7 @@ func TestMakeAbsoluteURL(t *testing.T) { "X-Forwarded-Host": {"forwarded-host"}, }, }) - assert.Equal(t, "https://forwarded-host/sub/foo", MakeAbsoluteURL(ctx, "/foo")) + assert.Equal(t, "http://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "/foo")) ctx = context.WithValue(ctx, RequestContextKey, &http.Request{ Host: "user-host", diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index 1efd166eb3..2a6d44ba08 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -116,6 +116,8 @@ func apiErrorDefined(ctx *context.Context, err *namedError) { } func apiUnauthorizedError(ctx *context.Context) { + // TODO: it doesn't seem quite right but it doesn't really cause problem at the moment. + // container registry requires that the "/v2" must be in the root, so the sub-path in AppURL should be removed, ideally. ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+httplib.GuessCurrentAppURL(ctx)+`v2/token",service="container_registry",scope="*"`) apiErrorDefined(ctx, errUnauthorized) } diff --git a/routers/web/admin/admin_test.go b/routers/web/admin/admin_test.go index 782126adf5..6c38f0b509 100644 --- a/routers/web/admin/admin_test.go +++ b/routers/web/admin/admin_test.go @@ -87,6 +87,6 @@ func TestSelfCheckPost(t *testing.T) { err := json.Unmarshal(resp.Body.Bytes(), &data) assert.NoError(t, err) assert.Equal(t, []string{ - ctx.Locale.TrString("admin.self_check.location_origin_mismatch", "http://frontend/sub/", "http://host/sub/"), + ctx.Locale.TrString("admin.self_check.location_origin_mismatch", "http://frontend/sub/", "http://config/sub/"), }, data.Problems) }