Skip to content

Commit 94eba1b

Browse files
committed
fix
1 parent 9c08637 commit 94eba1b

File tree

13 files changed

+95
-37
lines changed

13 files changed

+95
-37
lines changed

models/repo/avatar.go

+2-8
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import (
99
"image/png"
1010
"io"
1111
"net/url"
12-
"strings"
1312

1413
"code.gitea.io/gitea/models/db"
1514
"code.gitea.io/gitea/modules/avatar"
15+
"code.gitea.io/gitea/modules/httplib"
1616
"code.gitea.io/gitea/modules/log"
1717
"code.gitea.io/gitea/modules/setting"
1818
"code.gitea.io/gitea/modules/storage"
@@ -86,11 +86,5 @@ func (repo *Repository) relAvatarLink(ctx context.Context) string {
8686

8787
// AvatarLink returns a link to the repository's avatar.
8888
func (repo *Repository) AvatarLink(ctx context.Context) string {
89-
link := repo.relAvatarLink(ctx)
90-
// we only prepend our AppURL to our known (relative, internal) avatar link to get an absolute URL
91-
if strings.HasPrefix(link, "/") && !strings.HasPrefix(link, "//") {
92-
return setting.AppURL + strings.TrimPrefix(link, setting.AppSubURL)[1:]
93-
}
94-
// otherwise, return the link as it is
95-
return link
89+
return httplib.MakeAbsoluteURL(ctx, repo.relAvatarLink(ctx))
9690
}

models/user/avatar.go

+2-6
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ import (
99
"fmt"
1010
"image/png"
1111
"io"
12-
"strings"
1312

1413
"code.gitea.io/gitea/models/avatars"
1514
"code.gitea.io/gitea/models/db"
1615
"code.gitea.io/gitea/modules/avatar"
16+
"code.gitea.io/gitea/modules/httplib"
1717
"code.gitea.io/gitea/modules/log"
1818
"code.gitea.io/gitea/modules/setting"
1919
"code.gitea.io/gitea/modules/storage"
@@ -91,11 +91,7 @@ func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
9191

9292
// AvatarLink returns the full avatar link with http host
9393
func (u *User) AvatarLink(ctx context.Context) string {
94-
link := u.AvatarLinkWithSize(ctx, 0)
95-
if !strings.HasPrefix(link, "//") && !strings.Contains(link, "://") {
96-
return setting.AppURL + strings.TrimPrefix(link, setting.AppSubURL+"/")
97-
}
98-
return link
94+
return httplib.MakeAbsoluteURL(ctx, u.AvatarLinkWithSize(ctx, 0))
9995
}
10096

10197
// IsUploadAvatarChanged returns true if the current user's avatar would be changed with the provided data

modules/httplib/url.go

+62-2
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@
44
package httplib
55

66
import (
7+
"context"
8+
"net/http"
79
"net/url"
810
"strings"
911

1012
"code.gitea.io/gitea/modules/setting"
1113
"code.gitea.io/gitea/modules/util"
1214
)
1315

16+
type httpRequestContextKeyStruct struct{}
17+
18+
var HttpRequestContextKey = httpRequestContextKeyStruct{}
19+
1420
func urlIsRelative(s string, u *url.URL) bool {
1521
// Unfortunately browsers consider a redirect Location with preceding "//", "\\", "/\" and "\/" as meaning redirect to "http(s)://REST_OF_PATH"
1622
// Therefore we should ignore these redirect locations to prevent open redirects
@@ -26,7 +32,60 @@ func IsRelativeURL(s string) bool {
2632
return err == nil && urlIsRelative(s, u)
2733
}
2834

29-
func IsCurrentGiteaSiteURL(s string) bool {
35+
func guessHttpRequestScheme(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+
if req.TLS != nil {
53+
return "https"
54+
}
55+
return "http"
56+
}
57+
58+
func guessHttpRequestHost(req *http.Request) string {
59+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host
60+
if s := req.Header.Get("X-Forwarded-Host"); s != "" {
61+
return s
62+
}
63+
if req.Host != "" {
64+
return req.Host // Golang: the Host header is promoted to the Request.Host field and removed from the Header map.
65+
}
66+
return ""
67+
}
68+
69+
// GuessCurrentAppURL tries to guess the current full URL by http headers. It always has a '/' suffix, exactly the same as setting.AppURL
70+
func GuessCurrentAppURL(ctx context.Context) string {
71+
req, ok := ctx.Value(HttpRequestContextKey).(*http.Request)
72+
if !ok {
73+
return setting.AppURL
74+
}
75+
if host := guessHttpRequestHost(req); host != "" {
76+
return guessHttpRequestScheme(req) + "://" + host + setting.AppSubURL + "/"
77+
}
78+
return setting.AppURL
79+
}
80+
81+
func MakeAbsoluteURL(ctx context.Context, s string) string {
82+
if IsRelativeURL(s) {
83+
return GuessCurrentAppURL(ctx) + strings.TrimPrefix(s, "/")
84+
}
85+
return s
86+
}
87+
88+
func IsCurrentGiteaSiteURL(ctx context.Context, s string) bool {
3089
u, err := url.Parse(s)
3190
if err != nil {
3291
return false
@@ -45,5 +104,6 @@ func IsCurrentGiteaSiteURL(s string) bool {
45104
if u.Path == "" {
46105
u.Path = "/"
47106
}
48-
return strings.HasPrefix(strings.ToLower(u.String()), strings.ToLower(setting.AppURL))
107+
urlLower := strings.ToLower(u.String())
108+
return strings.HasPrefix(urlLower, strings.ToLower(setting.AppURL)) || strings.HasPrefix(urlLower, strings.ToLower(GuessCurrentAppURL(ctx)))
49109
}

modules/httplib/url_test.go

+8-6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package httplib
55

66
import (
7+
"context"
78
"testing"
89

910
"code.gitea.io/gitea/modules/setting"
@@ -40,6 +41,7 @@ func TestIsRelativeURL(t *testing.T) {
4041
func TestIsCurrentGiteaSiteURL(t *testing.T) {
4142
defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/sub/")()
4243
defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
44+
ctx := context.Background()
4345
good := []string{
4446
"?key=val",
4547
"/sub",
@@ -50,7 +52,7 @@ func TestIsCurrentGiteaSiteURL(t *testing.T) {
5052
"http://localhost:3000/sub/",
5153
}
5254
for _, s := range good {
53-
assert.True(t, IsCurrentGiteaSiteURL(s), "good = %q", s)
55+
assert.True(t, IsCurrentGiteaSiteURL(ctx, s), "good = %q", s)
5456
}
5557
bad := []string{
5658
".",
@@ -64,13 +66,13 @@ func TestIsCurrentGiteaSiteURL(t *testing.T) {
6466
"http://other/",
6567
}
6668
for _, s := range bad {
67-
assert.False(t, IsCurrentGiteaSiteURL(s), "bad = %q", s)
69+
assert.False(t, IsCurrentGiteaSiteURL(ctx, s), "bad = %q", s)
6870
}
6971

7072
setting.AppURL = "http://localhost:3000/"
7173
setting.AppSubURL = ""
72-
assert.False(t, IsCurrentGiteaSiteURL("//"))
73-
assert.False(t, IsCurrentGiteaSiteURL("\\\\"))
74-
assert.False(t, IsCurrentGiteaSiteURL("http://localhost"))
75-
assert.True(t, IsCurrentGiteaSiteURL("http://localhost:3000?key=val"))
74+
assert.False(t, IsCurrentGiteaSiteURL(ctx, "//"))
75+
assert.False(t, IsCurrentGiteaSiteURL(ctx, "\\\\"))
76+
assert.False(t, IsCurrentGiteaSiteURL(ctx, "http://localhost"))
77+
assert.True(t, IsCurrentGiteaSiteURL(ctx, "http://localhost:3000?key=val"))
7678
}

modules/markup/html_codepreview.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func renderCodeBlock(ctx *RenderContext, node *html.Node) (urlPosStart, urlPosSt
4242
CommitID: node.Data[m[6]:m[7]],
4343
FilePath: node.Data[m[8]:m[9]],
4444
}
45-
if !httplib.IsCurrentGiteaSiteURL(opts.FullURL) {
45+
if !httplib.IsCurrentGiteaSiteURL(ctx.Ctx, opts.FullURL) {
4646
return 0, 0, "", nil
4747
}
4848
u, err := url.Parse(opts.FilePath)

routers/api/actions/artifacts.go

+6-5
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ import (
7171

7272
"code.gitea.io/gitea/models/actions"
7373
"code.gitea.io/gitea/models/db"
74+
"code.gitea.io/gitea/modules/httplib"
7475
"code.gitea.io/gitea/modules/json"
7576
"code.gitea.io/gitea/modules/log"
7677
"code.gitea.io/gitea/modules/setting"
@@ -184,8 +185,8 @@ type artifactRoutes struct {
184185
fs storage.ObjectStorage
185186
}
186187

187-
func (ar artifactRoutes) buildArtifactURL(runID int64, artifactHash, suffix string) string {
188-
uploadURL := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(ar.prefix, "/") +
188+
func (ar artifactRoutes) buildArtifactURL(ctx *ArtifactContext, runID int64, artifactHash, suffix string) string {
189+
uploadURL := strings.TrimSuffix(httplib.GuessCurrentAppURL(ctx), "/") + strings.TrimSuffix(ar.prefix, "/") +
189190
strings.ReplaceAll(artifactRouteBase, "{run_id}", strconv.FormatInt(runID, 10)) +
190191
"/" + artifactHash + "/" + suffix
191192
return uploadURL
@@ -224,7 +225,7 @@ func (ar artifactRoutes) getUploadArtifactURL(ctx *ArtifactContext) {
224225
// use md5(artifact_name) to create upload url
225226
artifactHash := fmt.Sprintf("%x", md5.Sum([]byte(req.Name)))
226227
resp := getUploadArtifactResponse{
227-
FileContainerResourceURL: ar.buildArtifactURL(runID, artifactHash, "upload"+retentionQuery),
228+
FileContainerResourceURL: ar.buildArtifactURL(ctx, runID, artifactHash, "upload"+retentionQuery),
228229
}
229230
log.Debug("[artifact] get upload url: %s", resp.FileContainerResourceURL)
230231
ctx.JSON(http.StatusOK, resp)
@@ -365,7 +366,7 @@ func (ar artifactRoutes) listArtifacts(ctx *ArtifactContext) {
365366
artifactHash := fmt.Sprintf("%x", md5.Sum([]byte(art.ArtifactName)))
366367
item := listArtifactsResponseItem{
367368
Name: art.ArtifactName,
368-
FileContainerResourceURL: ar.buildArtifactURL(runID, artifactHash, "download_url"),
369+
FileContainerResourceURL: ar.buildArtifactURL(ctx, runID, artifactHash, "download_url"),
369370
}
370371
items = append(items, item)
371372
values[art.ArtifactName] = true
@@ -437,7 +438,7 @@ func (ar artifactRoutes) getDownloadArtifactURL(ctx *ArtifactContext) {
437438
}
438439
}
439440
if downloadURL == "" {
440-
downloadURL = ar.buildArtifactURL(runID, strconv.FormatInt(artifact.ID, 10), "download")
441+
downloadURL = ar.buildArtifactURL(ctx, runID, strconv.FormatInt(artifact.ID, 10), "download")
441442
}
442443
item := downloadArtifactResponseItem{
443444
Path: util.PathJoinRel(itemPath, artifact.ArtifactPath),

routers/api/actions/artifactsv4.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ import (
9292

9393
"code.gitea.io/gitea/models/actions"
9494
"code.gitea.io/gitea/models/db"
95+
"code.gitea.io/gitea/modules/httplib"
9596
"code.gitea.io/gitea/modules/log"
9697
"code.gitea.io/gitea/modules/setting"
9798
"code.gitea.io/gitea/modules/storage"
@@ -160,9 +161,9 @@ func (r artifactV4Routes) buildSignature(endp, expires, artifactName string, tas
160161
return mac.Sum(nil)
161162
}
162163

163-
func (r artifactV4Routes) buildArtifactURL(endp, artifactName string, taskID int64) string {
164+
func (r artifactV4Routes) buildArtifactURL(ctx *ArtifactContext, endp, artifactName string, taskID int64) string {
164165
expires := time.Now().Add(60 * time.Minute).Format("2006-01-02 15:04:05.999999999 -0700 MST")
165-
uploadURL := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(r.prefix, "/") +
166+
uploadURL := strings.TrimSuffix(httplib.GuessCurrentAppURL(ctx), "/") + strings.TrimSuffix(r.prefix, "/") +
166167
"/" + endp + "?sig=" + base64.URLEncoding.EncodeToString(r.buildSignature(endp, expires, artifactName, taskID)) + "&expires=" + url.QueryEscape(expires) + "&artifactName=" + url.QueryEscape(artifactName) + "&taskID=" + fmt.Sprint(taskID)
167168
return uploadURL
168169
}
@@ -278,7 +279,7 @@ func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) {
278279

279280
respData := CreateArtifactResponse{
280281
Ok: true,
281-
SignedUploadUrl: r.buildArtifactURL("UploadArtifact", artifactName, ctx.ActionTask.ID),
282+
SignedUploadUrl: r.buildArtifactURL(ctx, "UploadArtifact", artifactName, ctx.ActionTask.ID),
282283
}
283284
r.sendProtbufBody(ctx, &respData)
284285
}
@@ -454,7 +455,7 @@ func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) {
454455
}
455456
}
456457
if respData.SignedUrl == "" {
457-
respData.SignedUrl = r.buildArtifactURL("DownloadArtifact", artifactName, ctx.ActionTask.ID)
458+
respData.SignedUrl = r.buildArtifactURL(ctx, "DownloadArtifact", artifactName, ctx.ActionTask.ID)
458459
}
459460
r.sendProtbufBody(ctx, &respData)
460461
}

routers/api/packages/container/container.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
packages_model "code.gitea.io/gitea/models/packages"
1818
container_model "code.gitea.io/gitea/models/packages/container"
1919
user_model "code.gitea.io/gitea/models/user"
20+
"code.gitea.io/gitea/modules/httplib"
2021
"code.gitea.io/gitea/modules/json"
2122
"code.gitea.io/gitea/modules/log"
2223
packages_module "code.gitea.io/gitea/modules/packages"
@@ -115,7 +116,7 @@ func apiErrorDefined(ctx *context.Context, err *namedError) {
115116
}
116117

117118
func apiUnauthorizedError(ctx *context.Context) {
118-
ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+setting.AppURL+`v2/token",service="container_registry",scope="*"`)
119+
ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+httplib.GuessCurrentAppURL(ctx)+`v2/token",service="container_registry",scope="*"`)
119120
apiErrorDefined(ctx, errUnauthorized)
120121
}
121122

routers/common/middleware.go

+3
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
package common
55

66
import (
7+
go_context "context"
78
"fmt"
89
"net/http"
910
"strings"
1011

1112
"code.gitea.io/gitea/modules/cache"
13+
"code.gitea.io/gitea/modules/httplib"
1214
"code.gitea.io/gitea/modules/process"
1315
"code.gitea.io/gitea/modules/setting"
1416
"code.gitea.io/gitea/modules/web/middleware"
@@ -34,6 +36,7 @@ func ProtocolMiddlewares() (handlers []any) {
3436
}
3537
}()
3638
req = req.WithContext(middleware.WithContextData(req.Context()))
39+
req = req.WithContext(go_context.WithValue(req.Context(), httplib.HttpRequestContextKey, req))
3740
next.ServeHTTP(resp, req)
3841
})
3942
})

routers/common/redirect.go

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

routers/web/auth/auth.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
368368
return setting.AppSubURL + "/"
369369
}
370370

371-
if redirectTo := ctx.GetSiteCookie("redirect_to"); redirectTo != "" && httplib.IsCurrentGiteaSiteURL(redirectTo) {
371+
if redirectTo := ctx.GetSiteCookie("redirect_to"); redirectTo != "" && httplib.IsCurrentGiteaSiteURL(ctx, redirectTo) {
372372
middleware.DeleteRedirectToCookie(ctx.Resp)
373373
if obeyRedirect {
374374
ctx.RedirectToCurrentSite(redirectTo)

services/context/base.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ func (b *Base) Redirect(location string, status ...int) {
254254
code = status[0]
255255
}
256256

257-
if strings.HasPrefix(location, "http://") || strings.HasPrefix(location, "https://") || strings.HasPrefix(location, "//") {
257+
if !httplib.IsRelativeURL(location) {
258258
// Some browsers (Safari) have buggy behavior for Cookie + Cache + External Redirection, eg: /my-path => https://other/path
259259
// 1. the first request to "/my-path" contains cookie
260260
// 2. some time later, the request to "/my-path" doesn't contain cookie (caused by Prevent web tracking)

services/context/context_response.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func (ctx *Context) RedirectToCurrentSite(location ...string) {
5252
continue
5353
}
5454

55-
if !httplib.IsCurrentGiteaSiteURL(loc) {
55+
if !httplib.IsCurrentGiteaSiteURL(ctx, loc) {
5656
continue
5757
}
5858

0 commit comments

Comments
 (0)