This commit is contained in:
wxiaoguang 2024-05-07 13:02:29 +08:00
parent 9c08637eae
commit 94eba1bf25
13 changed files with 95 additions and 37 deletions

View File

@ -9,10 +9,10 @@ import (
"image/png" "image/png"
"io" "io"
"net/url" "net/url"
"strings"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/avatar" "code.gitea.io/gitea/modules/avatar"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/storage"
@ -86,11 +86,5 @@ func (repo *Repository) relAvatarLink(ctx context.Context) string {
// AvatarLink returns a link to the repository's avatar. // AvatarLink returns a link to the repository's avatar.
func (repo *Repository) AvatarLink(ctx context.Context) string { func (repo *Repository) AvatarLink(ctx context.Context) string {
link := repo.relAvatarLink(ctx) return httplib.MakeAbsoluteURL(ctx, repo.relAvatarLink(ctx))
// we only prepend our AppURL to our known (relative, internal) avatar link to get an absolute URL
if strings.HasPrefix(link, "/") && !strings.HasPrefix(link, "//") {
return setting.AppURL + strings.TrimPrefix(link, setting.AppSubURL)[1:]
}
// otherwise, return the link as it is
return link
} }

View File

@ -9,11 +9,11 @@ import (
"fmt" "fmt"
"image/png" "image/png"
"io" "io"
"strings"
"code.gitea.io/gitea/models/avatars" "code.gitea.io/gitea/models/avatars"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/avatar" "code.gitea.io/gitea/modules/avatar"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/storage"
@ -91,11 +91,7 @@ func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
// AvatarLink returns the full avatar link with http host // AvatarLink returns the full avatar link with http host
func (u *User) AvatarLink(ctx context.Context) string { func (u *User) AvatarLink(ctx context.Context) string {
link := u.AvatarLinkWithSize(ctx, 0) return httplib.MakeAbsoluteURL(ctx, u.AvatarLinkWithSize(ctx, 0))
if !strings.HasPrefix(link, "//") && !strings.Contains(link, "://") {
return setting.AppURL + strings.TrimPrefix(link, setting.AppSubURL+"/")
}
return link
} }
// IsUploadAvatarChanged returns true if the current user's avatar would be changed with the provided data // IsUploadAvatarChanged returns true if the current user's avatar would be changed with the provided data

View File

@ -4,6 +4,8 @@
package httplib package httplib
import ( import (
"context"
"net/http"
"net/url" "net/url"
"strings" "strings"
@ -11,6 +13,10 @@ import (
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
) )
type httpRequestContextKeyStruct struct{}
var HttpRequestContextKey = httpRequestContextKeyStruct{}
func urlIsRelative(s string, u *url.URL) bool { func urlIsRelative(s string, u *url.URL) bool {
// Unfortunately browsers consider a redirect Location with preceding "//", "\\", "/\" and "\/" as meaning redirect to "http(s)://REST_OF_PATH" // 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 // Therefore we should ignore these redirect locations to prevent open redirects
@ -26,7 +32,60 @@ func IsRelativeURL(s string) bool {
return err == nil && urlIsRelative(s, u) return err == nil && urlIsRelative(s, u)
} }
func IsCurrentGiteaSiteURL(s string) bool { func guessHttpRequestScheme(req *http.Request) string {
// 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")
}
if req.TLS != nil {
return "https"
}
return "http"
}
func guessHttpRequestHost(req *http.Request) string {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host
if s := req.Header.Get("X-Forwarded-Host"); s != "" {
return s
}
if req.Host != "" {
return req.Host // Golang: the Host header is promoted to the Request.Host field and removed from the Header map.
}
return ""
}
// GuessCurrentAppURL tries to guess the current full URL by http headers. It always has a '/' suffix, exactly the same as setting.AppURL
func GuessCurrentAppURL(ctx context.Context) string {
req, ok := ctx.Value(HttpRequestContextKey).(*http.Request)
if !ok {
return setting.AppURL
}
if host := guessHttpRequestHost(req); host != "" {
return guessHttpRequestScheme(req) + "://" + host + setting.AppSubURL + "/"
}
return setting.AppURL
}
func MakeAbsoluteURL(ctx context.Context, s string) string {
if IsRelativeURL(s) {
return GuessCurrentAppURL(ctx) + strings.TrimPrefix(s, "/")
}
return s
}
func IsCurrentGiteaSiteURL(ctx context.Context, s string) bool {
u, err := url.Parse(s) u, err := url.Parse(s)
if err != nil { if err != nil {
return false return false
@ -45,5 +104,6 @@ func IsCurrentGiteaSiteURL(s string) bool {
if u.Path == "" { if u.Path == "" {
u.Path = "/" u.Path = "/"
} }
return strings.HasPrefix(strings.ToLower(u.String()), strings.ToLower(setting.AppURL)) urlLower := strings.ToLower(u.String())
return strings.HasPrefix(urlLower, strings.ToLower(setting.AppURL)) || strings.HasPrefix(urlLower, strings.ToLower(GuessCurrentAppURL(ctx)))
} }

View File

@ -4,6 +4,7 @@
package httplib package httplib
import ( import (
"context"
"testing" "testing"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -40,6 +41,7 @@ func TestIsRelativeURL(t *testing.T) {
func TestIsCurrentGiteaSiteURL(t *testing.T) { func TestIsCurrentGiteaSiteURL(t *testing.T) {
defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/sub/")() defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/sub/")()
defer test.MockVariableValue(&setting.AppSubURL, "/sub")() defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
ctx := context.Background()
good := []string{ good := []string{
"?key=val", "?key=val",
"/sub", "/sub",
@ -50,7 +52,7 @@ func TestIsCurrentGiteaSiteURL(t *testing.T) {
"http://localhost:3000/sub/", "http://localhost:3000/sub/",
} }
for _, s := range good { for _, s := range good {
assert.True(t, IsCurrentGiteaSiteURL(s), "good = %q", s) assert.True(t, IsCurrentGiteaSiteURL(ctx, s), "good = %q", s)
} }
bad := []string{ bad := []string{
".", ".",
@ -64,13 +66,13 @@ func TestIsCurrentGiteaSiteURL(t *testing.T) {
"http://other/", "http://other/",
} }
for _, s := range bad { for _, s := range bad {
assert.False(t, IsCurrentGiteaSiteURL(s), "bad = %q", s) assert.False(t, IsCurrentGiteaSiteURL(ctx, s), "bad = %q", s)
} }
setting.AppURL = "http://localhost:3000/" setting.AppURL = "http://localhost:3000/"
setting.AppSubURL = "" setting.AppSubURL = ""
assert.False(t, IsCurrentGiteaSiteURL("//")) assert.False(t, IsCurrentGiteaSiteURL(ctx, "//"))
assert.False(t, IsCurrentGiteaSiteURL("\\\\")) assert.False(t, IsCurrentGiteaSiteURL(ctx, "\\\\"))
assert.False(t, IsCurrentGiteaSiteURL("http://localhost")) assert.False(t, IsCurrentGiteaSiteURL(ctx, "http://localhost"))
assert.True(t, IsCurrentGiteaSiteURL("http://localhost:3000?key=val")) assert.True(t, IsCurrentGiteaSiteURL(ctx, "http://localhost:3000?key=val"))
} }

View File

@ -42,7 +42,7 @@ func renderCodeBlock(ctx *RenderContext, node *html.Node) (urlPosStart, urlPosSt
CommitID: node.Data[m[6]:m[7]], CommitID: node.Data[m[6]:m[7]],
FilePath: node.Data[m[8]:m[9]], FilePath: node.Data[m[8]:m[9]],
} }
if !httplib.IsCurrentGiteaSiteURL(opts.FullURL) { if !httplib.IsCurrentGiteaSiteURL(ctx.Ctx, opts.FullURL) {
return 0, 0, "", nil return 0, 0, "", nil
} }
u, err := url.Parse(opts.FilePath) u, err := url.Parse(opts.FilePath)

View File

@ -71,6 +71,7 @@ import (
"code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -184,8 +185,8 @@ type artifactRoutes struct {
fs storage.ObjectStorage fs storage.ObjectStorage
} }
func (ar artifactRoutes) buildArtifactURL(runID int64, artifactHash, suffix string) string { func (ar artifactRoutes) buildArtifactURL(ctx *ArtifactContext, runID int64, artifactHash, suffix string) string {
uploadURL := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(ar.prefix, "/") + uploadURL := strings.TrimSuffix(httplib.GuessCurrentAppURL(ctx), "/") + strings.TrimSuffix(ar.prefix, "/") +
strings.ReplaceAll(artifactRouteBase, "{run_id}", strconv.FormatInt(runID, 10)) + strings.ReplaceAll(artifactRouteBase, "{run_id}", strconv.FormatInt(runID, 10)) +
"/" + artifactHash + "/" + suffix "/" + artifactHash + "/" + suffix
return uploadURL return uploadURL
@ -224,7 +225,7 @@ func (ar artifactRoutes) getUploadArtifactURL(ctx *ArtifactContext) {
// use md5(artifact_name) to create upload url // use md5(artifact_name) to create upload url
artifactHash := fmt.Sprintf("%x", md5.Sum([]byte(req.Name))) artifactHash := fmt.Sprintf("%x", md5.Sum([]byte(req.Name)))
resp := getUploadArtifactResponse{ resp := getUploadArtifactResponse{
FileContainerResourceURL: ar.buildArtifactURL(runID, artifactHash, "upload"+retentionQuery), FileContainerResourceURL: ar.buildArtifactURL(ctx, runID, artifactHash, "upload"+retentionQuery),
} }
log.Debug("[artifact] get upload url: %s", resp.FileContainerResourceURL) log.Debug("[artifact] get upload url: %s", resp.FileContainerResourceURL)
ctx.JSON(http.StatusOK, resp) ctx.JSON(http.StatusOK, resp)
@ -365,7 +366,7 @@ func (ar artifactRoutes) listArtifacts(ctx *ArtifactContext) {
artifactHash := fmt.Sprintf("%x", md5.Sum([]byte(art.ArtifactName))) artifactHash := fmt.Sprintf("%x", md5.Sum([]byte(art.ArtifactName)))
item := listArtifactsResponseItem{ item := listArtifactsResponseItem{
Name: art.ArtifactName, Name: art.ArtifactName,
FileContainerResourceURL: ar.buildArtifactURL(runID, artifactHash, "download_url"), FileContainerResourceURL: ar.buildArtifactURL(ctx, runID, artifactHash, "download_url"),
} }
items = append(items, item) items = append(items, item)
values[art.ArtifactName] = true values[art.ArtifactName] = true
@ -437,7 +438,7 @@ func (ar artifactRoutes) getDownloadArtifactURL(ctx *ArtifactContext) {
} }
} }
if downloadURL == "" { if downloadURL == "" {
downloadURL = ar.buildArtifactURL(runID, strconv.FormatInt(artifact.ID, 10), "download") downloadURL = ar.buildArtifactURL(ctx, runID, strconv.FormatInt(artifact.ID, 10), "download")
} }
item := downloadArtifactResponseItem{ item := downloadArtifactResponseItem{
Path: util.PathJoinRel(itemPath, artifact.ArtifactPath), Path: util.PathJoinRel(itemPath, artifact.ArtifactPath),

View File

@ -92,6 +92,7 @@ import (
"code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/storage"
@ -160,9 +161,9 @@ func (r artifactV4Routes) buildSignature(endp, expires, artifactName string, tas
return mac.Sum(nil) return mac.Sum(nil)
} }
func (r artifactV4Routes) buildArtifactURL(endp, artifactName string, taskID int64) string { func (r artifactV4Routes) buildArtifactURL(ctx *ArtifactContext, endp, artifactName string, taskID int64) string {
expires := time.Now().Add(60 * time.Minute).Format("2006-01-02 15:04:05.999999999 -0700 MST") expires := time.Now().Add(60 * time.Minute).Format("2006-01-02 15:04:05.999999999 -0700 MST")
uploadURL := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(r.prefix, "/") + uploadURL := strings.TrimSuffix(httplib.GuessCurrentAppURL(ctx), "/") + strings.TrimSuffix(r.prefix, "/") +
"/" + endp + "?sig=" + base64.URLEncoding.EncodeToString(r.buildSignature(endp, expires, artifactName, taskID)) + "&expires=" + url.QueryEscape(expires) + "&artifactName=" + url.QueryEscape(artifactName) + "&taskID=" + fmt.Sprint(taskID) "/" + endp + "?sig=" + base64.URLEncoding.EncodeToString(r.buildSignature(endp, expires, artifactName, taskID)) + "&expires=" + url.QueryEscape(expires) + "&artifactName=" + url.QueryEscape(artifactName) + "&taskID=" + fmt.Sprint(taskID)
return uploadURL return uploadURL
} }
@ -278,7 +279,7 @@ func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) {
respData := CreateArtifactResponse{ respData := CreateArtifactResponse{
Ok: true, Ok: true,
SignedUploadUrl: r.buildArtifactURL("UploadArtifact", artifactName, ctx.ActionTask.ID), SignedUploadUrl: r.buildArtifactURL(ctx, "UploadArtifact", artifactName, ctx.ActionTask.ID),
} }
r.sendProtbufBody(ctx, &respData) r.sendProtbufBody(ctx, &respData)
} }
@ -454,7 +455,7 @@ func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) {
} }
} }
if respData.SignedUrl == "" { if respData.SignedUrl == "" {
respData.SignedUrl = r.buildArtifactURL("DownloadArtifact", artifactName, ctx.ActionTask.ID) respData.SignedUrl = r.buildArtifactURL(ctx, "DownloadArtifact", artifactName, ctx.ActionTask.ID)
} }
r.sendProtbufBody(ctx, &respData) r.sendProtbufBody(ctx, &respData)
} }

View File

@ -17,6 +17,7 @@ import (
packages_model "code.gitea.io/gitea/models/packages" packages_model "code.gitea.io/gitea/models/packages"
container_model "code.gitea.io/gitea/models/packages/container" container_model "code.gitea.io/gitea/models/packages/container"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
packages_module "code.gitea.io/gitea/modules/packages" packages_module "code.gitea.io/gitea/modules/packages"
@ -115,7 +116,7 @@ func apiErrorDefined(ctx *context.Context, err *namedError) {
} }
func apiUnauthorizedError(ctx *context.Context) { func apiUnauthorizedError(ctx *context.Context) {
ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+setting.AppURL+`v2/token",service="container_registry",scope="*"`) ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+httplib.GuessCurrentAppURL(ctx)+`v2/token",service="container_registry",scope="*"`)
apiErrorDefined(ctx, errUnauthorized) apiErrorDefined(ctx, errUnauthorized)
} }

View File

@ -4,11 +4,13 @@
package common package common
import ( import (
go_context "context"
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
"code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
@ -34,6 +36,7 @@ func ProtocolMiddlewares() (handlers []any) {
} }
}() }()
req = req.WithContext(middleware.WithContextData(req.Context())) req = req.WithContext(middleware.WithContextData(req.Context()))
req = req.WithContext(go_context.WithValue(req.Context(), httplib.HttpRequestContextKey, req))
next.ServeHTTP(resp, req) next.ServeHTTP(resp, req)
}) })
}) })

View File

@ -17,7 +17,7 @@ func FetchRedirectDelegate(resp http.ResponseWriter, req *http.Request) {
// The typical page is "issue comment" page. The backend responds "/owner/repo/issues/1#comment-2", // The typical page is "issue comment" page. The backend responds "/owner/repo/issues/1#comment-2",
// then frontend needs this delegate to redirect to the new location with hash correctly. // then frontend needs this delegate to redirect to the new location with hash correctly.
redirect := req.PostFormValue("redirect") redirect := req.PostFormValue("redirect")
if !httplib.IsCurrentGiteaSiteURL(redirect) { if !httplib.IsCurrentGiteaSiteURL(req.Context(), redirect) {
resp.WriteHeader(http.StatusBadRequest) resp.WriteHeader(http.StatusBadRequest)
return return
} }

View File

@ -368,7 +368,7 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
return setting.AppSubURL + "/" return setting.AppSubURL + "/"
} }
if redirectTo := ctx.GetSiteCookie("redirect_to"); redirectTo != "" && httplib.IsCurrentGiteaSiteURL(redirectTo) { if redirectTo := ctx.GetSiteCookie("redirect_to"); redirectTo != "" && httplib.IsCurrentGiteaSiteURL(ctx, redirectTo) {
middleware.DeleteRedirectToCookie(ctx.Resp) middleware.DeleteRedirectToCookie(ctx.Resp)
if obeyRedirect { if obeyRedirect {
ctx.RedirectToCurrentSite(redirectTo) ctx.RedirectToCurrentSite(redirectTo)

View File

@ -254,7 +254,7 @@ func (b *Base) Redirect(location string, status ...int) {
code = status[0] code = status[0]
} }
if strings.HasPrefix(location, "http://") || strings.HasPrefix(location, "https://") || strings.HasPrefix(location, "//") { if !httplib.IsRelativeURL(location) {
// Some browsers (Safari) have buggy behavior for Cookie + Cache + External Redirection, eg: /my-path => https://other/path // Some browsers (Safari) have buggy behavior for Cookie + Cache + External Redirection, eg: /my-path => https://other/path
// 1. the first request to "/my-path" contains cookie // 1. the first request to "/my-path" contains cookie
// 2. some time later, the request to "/my-path" doesn't contain cookie (caused by Prevent web tracking) // 2. some time later, the request to "/my-path" doesn't contain cookie (caused by Prevent web tracking)

View File

@ -52,7 +52,7 @@ func (ctx *Context) RedirectToCurrentSite(location ...string) {
continue continue
} }
if !httplib.IsCurrentGiteaSiteURL(loc) { if !httplib.IsCurrentGiteaSiteURL(ctx, loc) {
continue continue
} }