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 }