2023-06-16 14:32:43 +08:00
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package httplib
import (
2024-05-07 16:26:13 +08:00
"context"
2025-01-07 13:17:44 +08:00
"net"
2024-05-07 16:26:13 +08:00
"net/http"
2023-06-16 14:32:43 +08:00
"net/url"
"strings"
"code.gitea.io/gitea/modules/setting"
2024-03-21 20:02:34 +08:00
"code.gitea.io/gitea/modules/util"
2023-06-16 14:32:43 +08:00
)
2024-05-07 16:26:13 +08:00
type RequestContextKeyStruct struct { }
var RequestContextKey = RequestContextKeyStruct { }
2024-03-21 20:02:34 +08:00
func urlIsRelative ( s string , u * url . URL ) bool {
2023-06-16 14:32:43 +08: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 20:02:34 +08:00
return false
2023-06-16 14:32:43 +08:00
}
2024-03-21 20:02:34 +08:00
return u != nil && u . Scheme == "" && u . Host == ""
}
2023-06-16 14:32:43 +08:00
2024-03-21 20:02:34 +08:00
// IsRelativeURL detects if a URL is relative (no scheme or host)
func IsRelativeURL ( s string ) bool {
2023-06-16 14:32:43 +08:00
u , err := url . Parse ( s )
2024-03-21 20:02:34 +08:00
return err == nil && urlIsRelative ( s , u )
}
2023-06-16 14:32:43 +08:00
2024-05-19 22:56:08 +08:00
func getRequestScheme ( req * http . Request ) string {
2024-05-07 16:26:13 +08: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 22:56:08 +08:00
return ""
2024-05-07 16:26:13 +08:00
}
2025-04-22 06:49:37 +08:00
// 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
2024-05-07 16:26:13 +08:00
func GuessCurrentAppURL ( ctx context . Context ) string {
2024-06-15 11:43:57 +08: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 {
2025-04-22 06:49:37 +08:00
// Try the best guess to get the current host URL (will be used for public URL) by http headers.
2024-05-19 22:56:08 +08:00
// 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.
2025-04-20 13:43:48 +02:00
// Without more information, Gitea is impossible to distinguish between case 2 and case 3, then case 2 would result in
2025-04-22 06:49:37 +08:00
// 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 "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 + "/" )
}
2024-05-19 22:56:08 +08:00
reqScheme := getRequestScheme ( req )
if reqScheme == "" {
2025-04-20 13:43:48 +02:00
// if no reverse proxy header, try to use "Host" header for absolute URL
2025-04-22 06:49:37 +08:00
if setting . PublicURLDetection == setting . PublicURLAuto && req . Host != "" {
2025-04-20 13:43:48 +02:00
return util . Iif ( req . TLS == nil , "http://" , "https://" ) + req . Host
}
// fall back to default AppURL
2024-06-15 11:43:57 +08:00
return strings . TrimSuffix ( setting . AppURL , setting . AppSubURL + "/" )
2024-05-19 22:56:08 +08:00
}
2024-09-20 22:57:55 +08: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 16:26:13 +08:00
}
2025-01-07 13:17:44 +08:00
func GuessCurrentHostDomain ( ctx context . Context ) string {
_ , host , _ := strings . Cut ( GuessCurrentHostURL ( ctx ) , "://" )
domain , _ , _ := net . SplitHostPort ( host )
return util . IfZero ( domain , host )
}
2025-04-22 06:49:37 +08:00
// MakeAbsoluteURL tries to make a link to an absolute public URL:
// * If link is empty, it returns the current public URL.
2024-06-15 11:43:57 +08:00
// * 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 16:26:13 +08:00
}
2024-06-15 11:43:57 +08:00
return GuessCurrentHostURL ( ctx ) + "/" + strings . TrimPrefix ( link , "/" )
2024-05-07 16:26:13 +08:00
}
2025-03-05 17:29:29 +01:00
type urlType int
const (
urlTypeGiteaAbsolute urlType = iota + 1 // "http://gitea/subpath"
urlTypeGiteaPageRelative // "/subpath"
urlTypeGiteaSiteRelative // "?key=val"
urlTypeUnknown // "http://other"
)
func detectURLRoutePath ( ctx context . Context , s string ) ( routePath string , ut urlType ) {
2024-03-21 20:02:34 +08:00
u , err := url . Parse ( s )
if err != nil {
2025-03-05 17:29:29 +01:00
return "" , urlTypeUnknown
2024-03-21 20:02:34 +08:00
}
2025-03-05 17:29:29 +01:00
cleanedPath := ""
2024-03-21 20:02:34 +08:00
if u . Path != "" {
2025-03-05 17:29:29 +01:00
cleanedPath = util . PathJoinRelX ( u . Path )
cleanedPath = util . Iif ( cleanedPath == "." , "" , "/" + cleanedPath )
2024-03-21 20:02:34 +08:00
}
if urlIsRelative ( s , u ) {
2025-03-05 17:29:29 +01:00
if u . Path == "" {
return "" , urlTypeGiteaPageRelative
}
if strings . HasPrefix ( strings . ToLower ( cleanedPath + "/" ) , strings . ToLower ( setting . AppSubURL + "/" ) ) {
return cleanedPath [ len ( setting . AppSubURL ) : ] , urlTypeGiteaSiteRelative
}
return "" , urlTypeUnknown
2024-03-21 20:02:34 +08:00
}
2025-03-05 17:29:29 +01:00
u . Path = cleanedPath + "/"
2024-05-07 16:26:13 +08:00
urlLower := strings . ToLower ( u . String ( ) )
2025-03-05 17:29:29 +01:00
if strings . HasPrefix ( urlLower , strings . ToLower ( setting . AppURL ) ) {
return cleanedPath [ len ( setting . AppSubURL ) : ] , urlTypeGiteaAbsolute
}
guessedCurURL := GuessCurrentAppURL ( ctx )
if strings . HasPrefix ( urlLower , strings . ToLower ( guessedCurURL ) ) {
return cleanedPath [ len ( setting . AppSubURL ) : ] , urlTypeGiteaAbsolute
}
return "" , urlTypeUnknown
}
func IsCurrentGiteaSiteURL ( ctx context . Context , s string ) bool {
_ , ut := detectURLRoutePath ( ctx , s )
return ut != urlTypeUnknown
}
type GiteaSiteURL struct {
RoutePath string
OwnerName string
RepoName string
RepoSubPath string
}
func ParseGiteaSiteURL ( ctx context . Context , s string ) * GiteaSiteURL {
routePath , ut := detectURLRoutePath ( ctx , s )
if ut == urlTypeUnknown || ut == urlTypeGiteaPageRelative {
return nil
}
ret := & GiteaSiteURL { RoutePath : routePath }
fields := strings . SplitN ( strings . TrimPrefix ( ret . RoutePath , "/" ) , "/" , 3 )
// TODO: now it only does a quick check for some known reserved paths, should do more strict checks in the future
if fields [ 0 ] == "attachments" {
return ret
}
if len ( fields ) < 2 {
return ret
}
ret . OwnerName = fields [ 0 ]
ret . RepoName = fields [ 1 ]
if len ( fields ) == 3 {
ret . RepoSubPath = "/" + fields [ 2 ]
}
return ret
2023-06-16 14:32:43 +08:00
}