code.gitea.io/gitea@v1.22.3/modules/httplib/url.go (about)

     1  // Copyright 2023 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package httplib
     5  
     6  import (
     7  	"context"
     8  	"net/http"
     9  	"net/url"
    10  	"strings"
    11  
    12  	"code.gitea.io/gitea/modules/setting"
    13  	"code.gitea.io/gitea/modules/util"
    14  )
    15  
    16  type RequestContextKeyStruct struct{}
    17  
    18  var RequestContextKey = RequestContextKeyStruct{}
    19  
    20  func urlIsRelative(s string, u *url.URL) bool {
    21  	// Unfortunately browsers consider a redirect Location with preceding "//", "\\", "/\" and "\/" as meaning redirect to "http(s)://REST_OF_PATH"
    22  	// Therefore we should ignore these redirect locations to prevent open redirects
    23  	if len(s) > 1 && (s[0] == '/' || s[0] == '\\') && (s[1] == '/' || s[1] == '\\') {
    24  		return false
    25  	}
    26  	return u != nil && u.Scheme == "" && u.Host == ""
    27  }
    28  
    29  // IsRelativeURL detects if a URL is relative (no scheme or host)
    30  func IsRelativeURL(s string) bool {
    31  	u, err := url.Parse(s)
    32  	return err == nil && urlIsRelative(s, u)
    33  }
    34  
    35  func getRequestScheme(req *http.Request) string {
    36  	// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto
    37  	if s := req.Header.Get("X-Forwarded-Proto"); s != "" {
    38  		return s
    39  	}
    40  	if s := req.Header.Get("X-Forwarded-Protocol"); s != "" {
    41  		return s
    42  	}
    43  	if s := req.Header.Get("X-Url-Scheme"); s != "" {
    44  		return s
    45  	}
    46  	if s := req.Header.Get("Front-End-Https"); s != "" {
    47  		return util.Iif(s == "on", "https", "http")
    48  	}
    49  	if s := req.Header.Get("X-Forwarded-Ssl"); s != "" {
    50  		return util.Iif(s == "on", "https", "http")
    51  	}
    52  	return ""
    53  }
    54  
    55  func getForwardedHost(req *http.Request) string {
    56  	// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host
    57  	return req.Header.Get("X-Forwarded-Host")
    58  }
    59  
    60  // 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
    61  func GuessCurrentAppURL(ctx context.Context) string {
    62  	return GuessCurrentHostURL(ctx) + setting.AppSubURL + "/"
    63  }
    64  
    65  // GuessCurrentHostURL tries to guess the current full host URL (no sub-path) by http headers, there is no trailing slash.
    66  func GuessCurrentHostURL(ctx context.Context) string {
    67  	req, ok := ctx.Value(RequestContextKey).(*http.Request)
    68  	if !ok {
    69  		return strings.TrimSuffix(setting.AppURL, setting.AppSubURL+"/")
    70  	}
    71  	// If no scheme provided by reverse proxy, then do not guess the AppURL, use the configured one.
    72  	// At the moment, if site admin doesn't configure the proxy headers correctly, then Gitea would guess wrong.
    73  	// There are some cases:
    74  	// 1. The reverse proxy is configured correctly, it passes "X-Forwarded-Proto/Host" headers. Perfect, Gitea can handle it correctly.
    75  	// 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.
    76  	// 3. There is no reverse proxy.
    77  	// Without an extra config option, Gitea is impossible to distinguish between case 2 and case 3,
    78  	// then case 2 would result in wrong guess like guessed AppURL becomes "http://gitea:3000/", which is not accessible by end users.
    79  	// So in the future maybe it should introduce a new config option, to let site admin decide how to guess the AppURL.
    80  	reqScheme := getRequestScheme(req)
    81  	if reqScheme == "" {
    82  		return strings.TrimSuffix(setting.AppURL, setting.AppSubURL+"/")
    83  	}
    84  	reqHost := getForwardedHost(req)
    85  	if reqHost == "" {
    86  		reqHost = req.Host
    87  	}
    88  	return reqScheme + "://" + reqHost
    89  }
    90  
    91  // MakeAbsoluteURL tries to make a link to an absolute URL:
    92  // * If link is empty, it returns the current app URL.
    93  // * If link is absolute, it returns the link.
    94  // * Otherwise, it returns the current host URL + link, the link itself should have correct sub-path (AppSubURL) if needed.
    95  func MakeAbsoluteURL(ctx context.Context, link string) string {
    96  	if link == "" {
    97  		return GuessCurrentAppURL(ctx)
    98  	}
    99  	if !IsRelativeURL(link) {
   100  		return link
   101  	}
   102  	return GuessCurrentHostURL(ctx) + "/" + strings.TrimPrefix(link, "/")
   103  }
   104  
   105  func IsCurrentGiteaSiteURL(ctx context.Context, s string) bool {
   106  	u, err := url.Parse(s)
   107  	if err != nil {
   108  		return false
   109  	}
   110  	if u.Path != "" {
   111  		cleanedPath := util.PathJoinRelX(u.Path)
   112  		if cleanedPath == "" || cleanedPath == "." {
   113  			u.Path = "/"
   114  		} else {
   115  			u.Path += "/" + cleanedPath + "/"
   116  		}
   117  	}
   118  	if urlIsRelative(s, u) {
   119  		return u.Path == "" || strings.HasPrefix(strings.ToLower(u.Path), strings.ToLower(setting.AppSubURL+"/"))
   120  	}
   121  	if u.Path == "" {
   122  		u.Path = "/"
   123  	}
   124  	urlLower := strings.ToLower(u.String())
   125  	return strings.HasPrefix(urlLower, strings.ToLower(setting.AppURL)) || strings.HasPrefix(urlLower, strings.ToLower(GuessCurrentAppURL(ctx)))
   126  }