2023-06-16 00:32:43 -06:00
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package httplib
import (
2024-05-07 02:26:13 -06:00
"context"
"net/http"
2023-06-16 00:32:43 -06:00
"net/url"
"strings"
"code.gitea.io/gitea/modules/setting"
2024-03-21 06:02:34 -06:00
"code.gitea.io/gitea/modules/util"
2023-06-16 00:32:43 -06:00
)
2024-05-07 02:26:13 -06:00
type RequestContextKeyStruct struct { }
var RequestContextKey = RequestContextKeyStruct { }
2024-03-21 06:02:34 -06:00
func urlIsRelative ( s string , u * url . URL ) bool {
2023-06-16 00:32:43 -06:00
// Unfortunately browsers consider a redirect Location with preceding "//", "\\", "/\" and "\/" as meaning redirect to "http(s)://REST_OF_PATH"
// Therefore we should ignore these redirect locations to prevent open redirects
if len ( s ) > 1 && ( s [ 0 ] == '/' || s [ 0 ] == '\\' ) && ( s [ 1 ] == '/' || s [ 1 ] == '\\' ) {
2024-03-21 06:02:34 -06:00
return false
2023-06-16 00:32:43 -06:00
}
2024-03-21 06:02:34 -06:00
return u != nil && u . Scheme == "" && u . Host == ""
}
2023-06-16 00:32:43 -06:00
2024-03-21 06:02:34 -06:00
// IsRelativeURL detects if a URL is relative (no scheme or host)
func IsRelativeURL ( s string ) bool {
2023-06-16 00:32:43 -06:00
u , err := url . Parse ( s )
2024-03-21 06:02:34 -06:00
return err == nil && urlIsRelative ( s , u )
}
2023-06-16 00:32:43 -06:00
2024-05-19 08:56:08 -06:00
func getRequestScheme ( req * http . Request ) string {
2024-05-07 02:26:13 -06:00
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto
if s := req . Header . Get ( "X-Forwarded-Proto" ) ; s != "" {
return s
}
if s := req . Header . Get ( "X-Forwarded-Protocol" ) ; s != "" {
return s
}
if s := req . Header . Get ( "X-Url-Scheme" ) ; s != "" {
return s
}
if s := req . Header . Get ( "Front-End-Https" ) ; s != "" {
return util . Iif ( s == "on" , "https" , "http" )
}
if s := req . Header . Get ( "X-Forwarded-Ssl" ) ; s != "" {
return util . Iif ( s == "on" , "https" , "http" )
}
2024-05-19 08:56:08 -06:00
return ""
2024-05-07 02:26:13 -06:00
}
2024-06-14 21:43:57 -06:00
// 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
2024-05-07 02:26:13 -06:00
func GuessCurrentAppURL ( ctx context . Context ) string {
2024-06-14 21:43:57 -06:00
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.
func GuessCurrentHostURL ( ctx context . Context ) string {
2024-05-07 02:26:13 -06:00
req , ok := ctx . Value ( RequestContextKey ) . ( * http . Request )
if ! ok {
2024-06-14 21:43:57 -06:00
return strings . TrimSuffix ( setting . AppURL , setting . AppSubURL + "/" )
2024-05-07 02:26:13 -06:00
}
2024-05-19 08:56:08 -06:00
// 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 == "" {
2024-06-14 21:43:57 -06:00
return strings . TrimSuffix ( setting . AppURL , setting . AppSubURL + "/" )
2024-05-19 08:56:08 -06:00
}
2024-09-20 08:57:55 -06:00
// X-Forwarded-Host has many problems: non-standard, not well-defined (X-Forwarded-Port or not), conflicts with Host header.
// So do not use X-Forwarded-Host, just use Host header directly.
return reqScheme + "://" + req . Host
2024-05-07 02:26:13 -06:00
}
2024-06-14 21:43:57 -06:00
// MakeAbsoluteURL tries to make a link to an absolute URL:
// * If link is empty, it returns the current app URL.
// * 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.
func MakeAbsoluteURL ( ctx context . Context , link string ) string {
if link == "" {
return GuessCurrentAppURL ( ctx )
}
if ! IsRelativeURL ( link ) {
return link
2024-05-07 02:26:13 -06:00
}
2024-06-14 21:43:57 -06:00
return GuessCurrentHostURL ( ctx ) + "/" + strings . TrimPrefix ( link , "/" )
2024-05-07 02:26:13 -06:00
}
func IsCurrentGiteaSiteURL ( ctx context . Context , s string ) bool {
2024-03-21 06:02:34 -06:00
u , err := url . Parse ( s )
if err != nil {
return false
}
if u . Path != "" {
2024-03-21 14:32:40 -06:00
cleanedPath := util . PathJoinRelX ( u . Path )
if cleanedPath == "" || cleanedPath == "." {
u . Path = "/"
} else {
u . Path += "/" + cleanedPath + "/"
2024-03-21 06:02:34 -06:00
}
}
if urlIsRelative ( s , u ) {
return u . Path == "" || strings . HasPrefix ( strings . ToLower ( u . Path ) , strings . ToLower ( setting . AppSubURL + "/" ) )
}
if u . Path == "" {
u . Path = "/"
}
2024-05-07 02:26:13 -06:00
urlLower := strings . ToLower ( u . String ( ) )
return strings . HasPrefix ( urlLower , strings . ToLower ( setting . AppURL ) ) || strings . HasPrefix ( urlLower , strings . ToLower ( GuessCurrentAppURL ( ctx ) ) )
2023-06-16 00:32:43 -06:00
}