Skip to content

Commit ca30f77

Browse files
committed
fix
1 parent 1761459 commit ca30f77

File tree

10 files changed

+123
-26
lines changed

10 files changed

+123
-26
lines changed

custom/conf/app.example.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ RUN_USER = ; git
8181
;; Overwrite the automatically generated public URL. Necessary for proxies and docker.
8282
;ROOT_URL = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s/
8383
;;
84+
;; For development purpose only.
85+
;; DO NOT USE IT IN PRODUCTION!!!
86+
;USE_SUB_URL_PATH = false
87+
;;
8488
;; when STATIC_URL_PREFIX is empty it will follow ROOT_URL
8589
;STATIC_URL_PREFIX =
8690
;;

models/repo/avatar.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"image/png"
1010
"io"
1111
"net/url"
12+
"strings"
1213

1314
"code.gitea.io/gitea/models/db"
1415
"code.gitea.io/gitea/modules/avatar"
@@ -90,7 +91,7 @@ func (repo *Repository) relAvatarLink(ctx context.Context) string {
9091
func (repo *Repository) AvatarLink(ctx context.Context) string {
9192
relLink := repo.relAvatarLink(ctx)
9293
if relLink != "" {
93-
return httplib.MakeAbsoluteURL(ctx, relLink)
94+
return httplib.MakeAbsoluteURL(ctx, strings.TrimPrefix(relLink, setting.AppSubURL+"/"))
9495
}
9596
return ""
9697
}

models/repo/avatar_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package repo
5+
6+
import (
7+
"testing"
8+
9+
"code.gitea.io/gitea/models/db"
10+
"code.gitea.io/gitea/modules/setting"
11+
"code.gitea.io/gitea/modules/test"
12+
13+
"github.com/stretchr/testify/assert"
14+
)
15+
16+
func TestRepoAvatarLink(t *testing.T) {
17+
defer test.MockVariableValue(&setting.AppURL, "https://localhost/")()
18+
defer test.MockVariableValue(&setting.AppSubURL, "")()
19+
20+
repo := &Repository{ID: 1, Avatar: "avatar.png"}
21+
link := repo.AvatarLink(db.DefaultContext)
22+
assert.Equal(t, "https://localhost/repo-avatars/avatar.png", link)
23+
24+
setting.AppURL = "https://localhost/sub-path/"
25+
setting.AppSubURL = "/sub-path"
26+
link = repo.AvatarLink(db.DefaultContext)
27+
assert.Equal(t, "https://localhost/sub-path/repo-avatars/avatar.png", link)
28+
}

models/user/avatar.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"image/png"
1111
"io"
12+
"strings"
1213

1314
"code.gitea.io/gitea/models/avatars"
1415
"code.gitea.io/gitea/models/db"
@@ -89,9 +90,11 @@ func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
8990
return avatars.GenerateEmailAvatarFastLink(ctx, u.AvatarEmail, size)
9091
}
9192

92-
// AvatarLink returns the full avatar url with http host. TODO: refactor it to a relative URL, but it is still used in API response at the moment
93+
// AvatarLink returns the full avatar url with http host.
94+
// TODO: refactor it to a relative URL, but it is still used in API response at the moment
9395
func (u *User) AvatarLink(ctx context.Context) string {
94-
return httplib.MakeAbsoluteURL(ctx, u.AvatarLinkWithSize(ctx, 0))
96+
relLink := u.AvatarLinkWithSize(ctx, 0) // it can't be empty
97+
return httplib.MakeAbsoluteURL(ctx, strings.TrimPrefix(relLink, setting.AppSubURL+"/"))
9598
}
9699

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

models/user/avatar_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package user
5+
6+
import (
7+
"testing"
8+
9+
"code.gitea.io/gitea/models/db"
10+
"code.gitea.io/gitea/modules/setting"
11+
"code.gitea.io/gitea/modules/test"
12+
13+
"github.com/stretchr/testify/assert"
14+
)
15+
16+
func TestUserAvatarLink(t *testing.T) {
17+
defer test.MockVariableValue(&setting.AppURL, "https://localhost/")()
18+
defer test.MockVariableValue(&setting.AppSubURL, "")()
19+
20+
u := &User{ID: 1, Avatar: "avatar.png"}
21+
link := u.AvatarLink(db.DefaultContext)
22+
assert.Equal(t, "https://localhost/avatars/avatar.png", link)
23+
24+
setting.AppURL = "https://localhost/sub-path/"
25+
setting.AppSubURL = "/sub-path"
26+
link = u.AvatarLink(db.DefaultContext)
27+
assert.Equal(t, "https://localhost/sub-path/avatars/avatar.png", link)
28+
}

modules/setting/global.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package setting
5+
6+
// Global settings
7+
var (
8+
// RunUser is the OS user that Gitea is running as. ini:"RUN_USER"
9+
RunUser string
10+
// RunMode is the running mode of Gitea, it only accepts two values: "dev" and "prod".
11+
// Non-dev values will be replaced by "prod". ini: "RUN_MODE"
12+
RunMode string
13+
// IsProd is true if RunMode is not "dev"
14+
IsProd bool
15+
16+
// AppName is the Application name, used in the page title. ini: "APP_NAME"
17+
AppName string
18+
)

modules/setting/server.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,16 @@ const (
4040
LandingPageLogin LandingPage = "/user/login"
4141
)
4242

43+
// Server settings
4344
var (
44-
// AppName is the Application name, used in the page title.
45-
// It maps to ini:"APP_NAME"
46-
AppName string
4745
// AppURL is the Application ROOT_URL. It always has a '/' suffix
4846
// It maps to ini:"ROOT_URL"
4947
AppURL string
5048
// AppSubURL represents the sub-url mounting point for gitea. It is either "" or starts with '/' and ends without '/', such as '/{subpath}'.
5149
// This value is empty if site does not have sub-url.
5250
AppSubURL string
51+
// UseSubURLPath makes Gitea handles requests with sub-path like "/sub-path/owner/repo/...", to make it easier to debug sub-path related problems.
52+
UseSubURLPath bool
5353
// AppDataPath is the default path for storing data.
5454
// It maps to ini:"APP_DATA_PATH" in [server] and defaults to AppWorkPath + "/data"
5555
AppDataPath string
@@ -59,8 +59,6 @@ var (
5959
// AssetVersion holds a opaque value that is used for cache-busting assets
6060
AssetVersion string
6161

62-
// Server settings
63-
6462
Protocol Scheme
6563
UseProxyProtocol bool // `ini:"USE_PROXY_PROTOCOL"`
6664
ProxyProtocolTLSBridging bool //`ini:"PROXY_PROTOCOL_TLS_BRIDGING"`
@@ -275,9 +273,10 @@ func loadServerFrom(rootCfg ConfigProvider) {
275273
// This should be TrimRight to ensure that there is only a single '/' at the end of AppURL.
276274
AppURL = strings.TrimRight(appURL.String(), "/") + "/"
277275

278-
// Suburl should start with '/' and end without '/', such as '/{subpath}'.
276+
// AppSubURL should start with '/' and end without '/', such as '/{subpath}'.
279277
// This value is empty if site does not have sub-url.
280278
AppSubURL = strings.TrimSuffix(appURL.Path, "/")
279+
UseSubURLPath = sec.Key("USE_SUB_URL_PATH").MustBool(false)
281280
StaticURLPrefix = strings.TrimSuffix(sec.Key("STATIC_URL_PREFIX").MustString(AppSubURL), "/")
282281

283282
// Check if Domain differs from AppURL domain than update it to AppURL's domain

modules/setting/setting.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,7 @@ var (
2525
// AppStartTime store time gitea has started
2626
AppStartTime time.Time
2727

28-
// Other global setting objects
29-
3028
CfgProvider ConfigProvider
31-
RunMode string
32-
RunUser string
33-
IsProd bool
3429
IsWindows bool
3530

3631
// IsInTesting indicates whether the testing is running. A lot of unreliable code causes a lot of nonsense error logs during testing

routers/common/middleware.go

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import (
2525
// ProtocolMiddlewares returns HTTP protocol related middlewares, and it provides a global panic recovery
2626
func ProtocolMiddlewares() (handlers []any) {
2727
// first, normalize the URL path
28-
handlers = append(handlers, stripSlashesMiddleware)
28+
handlers = append(handlers, normalizeRequestPathMiddleware)
2929

3030
// prepare the ContextData and panic recovery
3131
handlers = append(handlers, func(next http.Handler) http.Handler {
@@ -75,9 +75,9 @@ func ProtocolMiddlewares() (handlers []any) {
7575
return handlers
7676
}
7777

78-
func stripSlashesMiddleware(next http.Handler) http.Handler {
78+
func normalizeRequestPathMiddleware(next http.Handler) http.Handler {
7979
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
80-
// First of all escape the URL RawPath to ensure that all routing is done using a correctly escaped URL
80+
// escape the URL RawPath to ensure that all routing is done using a correctly escaped URL
8181
req.URL.RawPath = req.URL.EscapedPath()
8282

8383
urlPath := req.URL.RawPath
@@ -86,19 +86,40 @@ func stripSlashesMiddleware(next http.Handler) http.Handler {
8686
urlPath = rctx.RoutePath
8787
}
8888

89-
sanitizedPath := &strings.Builder{}
90-
prevWasSlash := false
91-
for _, chr := range strings.TrimRight(urlPath, "/") {
92-
if chr != '/' || !prevWasSlash {
93-
sanitizedPath.WriteRune(chr)
89+
normalizedPath := strings.TrimRight(urlPath, "/")
90+
if strings.Contains(urlPath, "//") {
91+
buf := &strings.Builder{}
92+
prevWasSlash := false
93+
for _, chr := range normalizedPath {
94+
if chr != '/' || !prevWasSlash {
95+
buf.WriteRune(chr)
96+
}
97+
prevWasSlash = chr == '/'
98+
}
99+
normalizedPath = buf.String()
100+
}
101+
102+
if setting.UseSubURLPath {
103+
remainingPath, ok := strings.CutPrefix(normalizedPath, setting.AppSubURL+"/")
104+
if ok {
105+
normalizedPath = "/" + remainingPath
106+
} else if normalizedPath == setting.AppSubURL {
107+
normalizedPath = "/"
108+
} else if !strings.HasPrefix(normalizedPath+"/", "/v2/") {
109+
// do not respond to other requests, to simulate a real sub-path environment
110+
http.Error(resp, "404 page not found, sub-path is: "+setting.AppSubURL, http.StatusNotFound)
111+
return
94112
}
95-
prevWasSlash = chr == '/'
113+
// TODO: it's not quite clear about how req.URL and rctx.RoutePath work together.
114+
// Fortunately, it is only used for debug purpose, we have enough time to figure it out in the future.
115+
req.URL.RawPath = normalizedPath
116+
req.URL.Path = normalizedPath
96117
}
97118

98119
if rctx == nil {
99-
req.URL.Path = sanitizedPath.String()
120+
req.URL.Path = normalizedPath
100121
} else {
101-
rctx.RoutePath = sanitizedPath.String()
122+
rctx.RoutePath = normalizedPath
102123
}
103124
next.ServeHTTP(resp, req)
104125
})

routers/common/middleware_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func TestStripSlashesMiddleware(t *testing.T) {
6161
})
6262

6363
// pass the test middleware to validate the changes
64-
handlerToTest := stripSlashesMiddleware(testMiddleware)
64+
handlerToTest := normalizeRequestPathMiddleware(testMiddleware)
6565
// create a mock request to use
6666
req := httptest.NewRequest("GET", tt.inputPath, nil)
6767
// call the handler using a mock response recorder

0 commit comments

Comments
 (0)