Improve reverse proxy documents and clarify the AppURL guessing behavior (#31003)

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: KN4CK3R <admin@oldschoolhack.me>
This commit is contained in:
wxiaoguang 2024-05-19 22:56:08 +08:00 committed by GitHub
parent 58a03e9fad
commit 339bc8bc8f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 78 additions and 61 deletions

View File

@ -17,15 +17,35 @@ menu:
# Reverse Proxies # 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 ## 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 { server {
listen 80; ...
server_name git.example.com;
location / { location / {
client_max_body_size 512M; client_max_body_size 512M;
proxy_pass http://localhost:3000; 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 ## 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 { server {
listen 80; ...
server_name git.example.com; location ~ ^/(gitea|v2)($|/) {
# Note: Trailing slash
location /gitea/ {
client_max_body_size 512M; 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 ^ $request_uri;
rewrite ^/gitea(/.*) $1 break; rewrite ^(/gitea)?(/.*) $2 break;
proxy_pass http://127.0.0.1:3000$uri; proxy_pass http://127.0.0.1:3000$uri;
# other common HTTP headers, see the "Nginx" config section above # 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 ## 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. Set `[server] STATIC_URL_PREFIX = /_/static` in your configuration.
```apacheconf ```nginx
server { server {
listen 80; listen 80;
server_name git.example.com; server_name git.example.com;
@ -112,7 +130,7 @@ server {
Set `[server] STATIC_URL_PREFIX = http://cdn.example.com/gitea` in your configuration. Set `[server] STATIC_URL_PREFIX = http://cdn.example.com/gitea` in your configuration.
```apacheconf ```nginx
# application server running Gitea # application server running Gitea
server { server {
listen 80; listen 80;
@ -124,7 +142,7 @@ server {
} }
``` ```
```apacheconf ```nginx
# static content delivery server # static content delivery server
server { server {
listen 80; listen 80;
@ -151,6 +169,8 @@ If you want Apache HTTPD to serve your Gitea instance, you can add the following
ProxyRequests off ProxyRequests off
AllowEncodedSlashes NoDecode AllowEncodedSlashes NoDecode
ProxyPass / http://localhost:3000/ nocanon ProxyPass / http://localhost:3000/ nocanon
ProxyPreserveHost On
RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
</VirtualHost> </VirtualHost>
``` ```
@ -172,6 +192,8 @@ In case you already have a site, and you want Gitea to share the domain name, yo
AllowEncodedSlashes NoDecode AllowEncodedSlashes NoDecode
# Note: no trailing slash after either /git or port # Note: no trailing slash after either /git or port
ProxyPass /git http://localhost:3000 nocanon ProxyPass /git http://localhost:3000 nocanon
ProxyPreserveHost On
RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
</VirtualHost> </VirtualHost>
``` ```
@ -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: If you want Caddy to serve your Gitea instance, you can add the following server block to your Caddyfile:
```apacheconf ```
git.example.com { git.example.com {
reverse_proxy localhost:3000 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: 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 { git.example.com {
route /git/* { route /git/* {
uri strip_prefix /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. 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. 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.

View File

@ -32,7 +32,7 @@ func IsRelativeURL(s string) bool {
return err == nil && urlIsRelative(s, u) 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 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto
if s := req.Header.Get("X-Forwarded-Proto"); s != "" { if s := req.Header.Get("X-Forwarded-Proto"); s != "" {
return s return s
@ -49,10 +49,10 @@ func guessRequestScheme(req *http.Request, def string) string {
if s := req.Header.Get("X-Forwarded-Ssl"); s != "" { if s := req.Header.Get("X-Forwarded-Ssl"); s != "" {
return util.Iif(s == "on", "https", "http") 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 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host
return req.Header.Get("X-Forwarded-Host") return req.Header.Get("X-Forwarded-Host")
} }
@ -63,15 +63,24 @@ func GuessCurrentAppURL(ctx context.Context) string {
if !ok { if !ok {
return setting.AppURL return setting.AppURL
} }
if host := guessForwardedHost(req); host != "" { // If no scheme provided by reverse proxy, then do not guess the AppURL, use the configured one.
// 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 // At the moment, if site admin doesn't configure the proxy headers correctly, then Gitea would guess wrong.
return guessRequestScheme(req, "https") + "://" + host + setting.AppSubURL + "/" // There are some cases:
} else if req.Host != "" { // 1. The reverse proxy is configured correctly, it passes "X-Forwarded-Proto/Host" headers. Perfect, Gitea can handle it correctly.
// if it is not behind a reverse proxy, use the scheme from config options, meanwhile use "https" as much as possible // 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.
defaultScheme := util.Iif(setting.Protocol == "http", "http", "https") // 3. There is no reverse proxy.
return guessRequestScheme(req, defaultScheme) + "://" + req.Host + setting.AppSubURL + "/" // 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 { func MakeAbsoluteURL(ctx context.Context, s string) string {

View File

@ -41,19 +41,19 @@ func TestIsRelativeURL(t *testing.T) {
func TestMakeAbsoluteURL(t *testing.T) { func TestMakeAbsoluteURL(t *testing.T) {
defer test.MockVariableValue(&setting.Protocol, "http")() 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")() defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
ctx := context.Background() ctx := context.Background()
assert.Equal(t, "http://the-host/sub/", MakeAbsoluteURL(ctx, "")) assert.Equal(t, "http://cfg-host/sub/", MakeAbsoluteURL(ctx, ""))
assert.Equal(t, "http://the-host/sub/foo", MakeAbsoluteURL(ctx, "foo")) assert.Equal(t, "http://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "foo"))
assert.Equal(t, "http://the-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")) assert.Equal(t, "http://other/foo", MakeAbsoluteURL(ctx, "http://other/foo"))
ctx = context.WithValue(ctx, RequestContextKey, &http.Request{ ctx = context.WithValue(ctx, RequestContextKey, &http.Request{
Host: "user-host", 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{ ctx = context.WithValue(ctx, RequestContextKey, &http.Request{
Host: "user-host", Host: "user-host",
@ -61,7 +61,7 @@ func TestMakeAbsoluteURL(t *testing.T) {
"X-Forwarded-Host": {"forwarded-host"}, "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{ ctx = context.WithValue(ctx, RequestContextKey, &http.Request{
Host: "user-host", Host: "user-host",

View File

@ -116,6 +116,8 @@ func apiErrorDefined(ctx *context.Context, err *namedError) {
} }
func apiUnauthorizedError(ctx *context.Context) { 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="*"`) ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+httplib.GuessCurrentAppURL(ctx)+`v2/token",service="container_registry",scope="*"`)
apiErrorDefined(ctx, errUnauthorized) apiErrorDefined(ctx, errUnauthorized)
} }

View File

@ -87,6 +87,6 @@ func TestSelfCheckPost(t *testing.T) {
err := json.Unmarshal(resp.Body.Bytes(), &data) err := json.Unmarshal(resp.Body.Bytes(), &data)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []string{ 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) }, data.Problems)
} }