gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. // Copyright 2023 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package httplib
  4. import (
  5. "context"
  6. "net"
  7. "net/http"
  8. "net/url"
  9. "strings"
  10. "code.gitea.io/gitea/modules/setting"
  11. "code.gitea.io/gitea/modules/util"
  12. )
  13. type RequestContextKeyStruct struct{}
  14. var RequestContextKey = RequestContextKeyStruct{}
  15. func urlIsRelative(s string, u *url.URL) bool {
  16. // Unfortunately, browsers consider a redirect Location with preceding "//", "\\", "/\" and "\/" as meaning redirect to "http(s)://REST_OF_PATH"
  17. // Therefore we should ignore these redirect locations to prevent open redirects
  18. if len(s) > 1 && (s[0] == '/' || s[0] == '\\') && (s[1] == '/' || s[1] == '\\') {
  19. return false
  20. }
  21. return u != nil && u.Scheme == "" && u.Host == ""
  22. }
  23. // IsRelativeURL detects if a URL is relative (no scheme or host)
  24. func IsRelativeURL(s string) bool {
  25. u, err := url.Parse(s)
  26. return err == nil && urlIsRelative(s, u)
  27. }
  28. func getRequestScheme(req *http.Request) string {
  29. // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto
  30. if s := req.Header.Get("X-Forwarded-Proto"); s != "" {
  31. return s
  32. }
  33. if s := req.Header.Get("X-Forwarded-Protocol"); s != "" {
  34. return s
  35. }
  36. if s := req.Header.Get("X-Url-Scheme"); s != "" {
  37. return s
  38. }
  39. if s := req.Header.Get("Front-End-Https"); s != "" {
  40. return util.Iif(s == "on", "https", "http")
  41. }
  42. if s := req.Header.Get("X-Forwarded-Ssl"); s != "" {
  43. return util.Iif(s == "on", "https", "http")
  44. }
  45. return ""
  46. }
  47. // 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
  48. // TODO: should rename it to GuessCurrentPublicURL in the future
  49. func GuessCurrentAppURL(ctx context.Context) string {
  50. return GuessCurrentHostURL(ctx) + setting.AppSubURL + "/"
  51. }
  52. // GuessCurrentHostURL tries to guess the current full host URL (no sub-path) by http headers, there is no trailing slash.
  53. func GuessCurrentHostURL(ctx context.Context) string {
  54. // Try the best guess to get the current host URL (will be used for public URL) by http headers.
  55. // At the moment, if site admin doesn't configure the proxy headers correctly, then Gitea would guess wrong.
  56. // There are some cases:
  57. // 1. The reverse proxy is configured correctly, it passes "X-Forwarded-Proto/Host" headers. Perfect, Gitea can handle it correctly.
  58. // 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.
  59. // 3. There is no reverse proxy.
  60. // Without more information, Gitea is impossible to distinguish between case 2 and case 3, then case 2 would result in
  61. // wrong guess like guessed public URL becomes "http://gitea:3000/" behind a "https" reverse proxy, which is not accessible by end users.
  62. // So we introduced "PUBLIC_URL_DETECTION" option, to control the guessing behavior to satisfy different use cases.
  63. req, ok := ctx.Value(RequestContextKey).(*http.Request)
  64. if !ok {
  65. return strings.TrimSuffix(setting.AppURL, setting.AppSubURL+"/")
  66. }
  67. reqScheme := getRequestScheme(req)
  68. if reqScheme == "" {
  69. // if no reverse proxy header, try to use "Host" header for absolute URL
  70. if setting.PublicURLDetection == setting.PublicURLAuto && req.Host != "" {
  71. return util.Iif(req.TLS == nil, "http://", "https://") + req.Host
  72. }
  73. // fall back to default AppURL
  74. return strings.TrimSuffix(setting.AppURL, setting.AppSubURL+"/")
  75. }
  76. // X-Forwarded-Host has many problems: non-standard, not well-defined (X-Forwarded-Port or not), conflicts with Host header.
  77. // So do not use X-Forwarded-Host, just use Host header directly.
  78. return reqScheme + "://" + req.Host
  79. }
  80. func GuessCurrentHostDomain(ctx context.Context) string {
  81. _, host, _ := strings.Cut(GuessCurrentHostURL(ctx), "://")
  82. domain, _, _ := net.SplitHostPort(host)
  83. return util.IfZero(domain, host)
  84. }
  85. // MakeAbsoluteURL tries to make a link to an absolute public URL:
  86. // * If link is empty, it returns the current public URL.
  87. // * If link is absolute, it returns the link.
  88. // * Otherwise, it returns the current host URL + link, the link itself should have correct sub-path (AppSubURL) if needed.
  89. func MakeAbsoluteURL(ctx context.Context, link string) string {
  90. if link == "" {
  91. return GuessCurrentAppURL(ctx)
  92. }
  93. if !IsRelativeURL(link) {
  94. return link
  95. }
  96. return GuessCurrentHostURL(ctx) + "/" + strings.TrimPrefix(link, "/")
  97. }
  98. type urlType int
  99. const (
  100. urlTypeGiteaAbsolute urlType = iota + 1 // "http://gitea/subpath"
  101. urlTypeGiteaPageRelative // "/subpath"
  102. urlTypeGiteaSiteRelative // "?key=val"
  103. urlTypeUnknown // "http://other"
  104. )
  105. func detectURLRoutePath(ctx context.Context, s string) (routePath string, ut urlType) {
  106. u, err := url.Parse(s)
  107. if err != nil {
  108. return "", urlTypeUnknown
  109. }
  110. cleanedPath := ""
  111. if u.Path != "" {
  112. cleanedPath = util.PathJoinRelX(u.Path)
  113. cleanedPath = util.Iif(cleanedPath == ".", "", "/"+cleanedPath)
  114. }
  115. if urlIsRelative(s, u) {
  116. if u.Path == "" {
  117. return "", urlTypeGiteaPageRelative
  118. }
  119. if strings.HasPrefix(strings.ToLower(cleanedPath+"/"), strings.ToLower(setting.AppSubURL+"/")) {
  120. return cleanedPath[len(setting.AppSubURL):], urlTypeGiteaSiteRelative
  121. }
  122. return "", urlTypeUnknown
  123. }
  124. u.Path = cleanedPath + "/"
  125. urlLower := strings.ToLower(u.String())
  126. if strings.HasPrefix(urlLower, strings.ToLower(setting.AppURL)) {
  127. return cleanedPath[len(setting.AppSubURL):], urlTypeGiteaAbsolute
  128. }
  129. guessedCurURL := GuessCurrentAppURL(ctx)
  130. if strings.HasPrefix(urlLower, strings.ToLower(guessedCurURL)) {
  131. return cleanedPath[len(setting.AppSubURL):], urlTypeGiteaAbsolute
  132. }
  133. return "", urlTypeUnknown
  134. }
  135. func IsCurrentGiteaSiteURL(ctx context.Context, s string) bool {
  136. _, ut := detectURLRoutePath(ctx, s)
  137. return ut != urlTypeUnknown
  138. }
  139. type GiteaSiteURL struct {
  140. RoutePath string
  141. OwnerName string
  142. RepoName string
  143. RepoSubPath string
  144. }
  145. func ParseGiteaSiteURL(ctx context.Context, s string) *GiteaSiteURL {
  146. routePath, ut := detectURLRoutePath(ctx, s)
  147. if ut == urlTypeUnknown || ut == urlTypeGiteaPageRelative {
  148. return nil
  149. }
  150. ret := &GiteaSiteURL{RoutePath: routePath}
  151. fields := strings.SplitN(strings.TrimPrefix(ret.RoutePath, "/"), "/", 3)
  152. // TODO: now it only does a quick check for some known reserved paths, should do more strict checks in the future
  153. if fields[0] == "attachments" {
  154. return ret
  155. }
  156. if len(fields) < 2 {
  157. return ret
  158. }
  159. ret.OwnerName = fields[0]
  160. ret.RepoName = fields[1]
  161. if len(fields) == 3 {
  162. ret.RepoSubPath = "/" + fields[2]
  163. }
  164. return ret
  165. }