Skip to content

Commit 92fd3fc

Browse files
wxiaoguangGiteaBot
andauthored
Refactor "route" related code, fix Safari cookie bug (#24330)
Fix #24176 Clean some misuses of route package, clean some legacy FIXMEs --------- Co-authored-by: Giteabot <[email protected]>
1 parent 1c875ef commit 92fd3fc

File tree

14 files changed

+264
-253
lines changed

14 files changed

+264
-253
lines changed

modules/context/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ func RepoRefForAPI(next http.Handler) http.Handler {
343343
}
344344
ctx.Repo.Commit = commit
345345
ctx.Repo.TreePath = ctx.Params("*")
346+
next.ServeHTTP(w, req)
346347
return
347348
}
348349

modules/context/context.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,13 +446,33 @@ func (ctx *Context) JSON(status int, content interface{}) {
446446
}
447447
}
448448

449+
func removeSessionCookieHeader(w http.ResponseWriter) {
450+
cookies := w.Header()["Set-Cookie"]
451+
w.Header().Del("Set-Cookie")
452+
for _, cookie := range cookies {
453+
if strings.HasPrefix(cookie, setting.SessionConfig.CookieName+"=") {
454+
continue
455+
}
456+
w.Header().Add("Set-Cookie", cookie)
457+
}
458+
}
459+
449460
// Redirect redirects the request
450461
func (ctx *Context) Redirect(location string, status ...int) {
451462
code := http.StatusSeeOther
452463
if len(status) == 1 {
453464
code = status[0]
454465
}
455466

467+
if strings.Contains(location, "://") || strings.HasPrefix(location, "//") {
468+
// Some browsers (Safari) have buggy behavior for Cookie + Cache + External Redirection, eg: /my-path => https://other/path
469+
// 1. the first request to "/my-path" contains cookie
470+
// 2. some time later, the request to "/my-path" doesn't contain cookie (caused by Prevent web tracking)
471+
// 3. Gitea's Sessioner doesn't see the session cookie, so it generates a new session id, and returns it to browser
472+
// 4. then the browser accepts the empty session, then the user is logged out
473+
// So in this case, we should remove the session cookie from the response header
474+
removeSessionCookieHeader(ctx.Resp)
475+
}
456476
http.Redirect(ctx.Resp, ctx.Req, location, code)
457477
}
458478

modules/context/context_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package context
5+
6+
import (
7+
"net/http"
8+
"testing"
9+
10+
"code.gitea.io/gitea/modules/setting"
11+
12+
"github.com/stretchr/testify/assert"
13+
)
14+
15+
type mockResponseWriter struct {
16+
header http.Header
17+
}
18+
19+
func (m *mockResponseWriter) Header() http.Header {
20+
return m.header
21+
}
22+
23+
func (m *mockResponseWriter) Write(bytes []byte) (int, error) {
24+
panic("implement me")
25+
}
26+
27+
func (m *mockResponseWriter) WriteHeader(statusCode int) {
28+
panic("implement me")
29+
}
30+
31+
func TestRemoveSessionCookieHeader(t *testing.T) {
32+
w := &mockResponseWriter{}
33+
w.header = http.Header{}
34+
w.header.Add("Set-Cookie", (&http.Cookie{Name: setting.SessionConfig.CookieName, Value: "foo"}).String())
35+
w.header.Add("Set-Cookie", (&http.Cookie{Name: "other", Value: "bar"}).String())
36+
assert.Len(t, w.Header().Values("Set-Cookie"), 2)
37+
removeSessionCookieHeader(w)
38+
assert.Len(t, w.Header().Values("Set-Cookie"), 1)
39+
assert.Contains(t, "other=bar", w.Header().Get("Set-Cookie"))
40+
}

modules/web/handler.go

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"fmt"
99
"net/http"
1010
"reflect"
11-
"strings"
1211

1312
"code.gitea.io/gitea/modules/context"
1413
"code.gitea.io/gitea/modules/web/routing"
@@ -131,16 +130,22 @@ func hasResponseBeenWritten(argsIn []reflect.Value) bool {
131130
// toHandlerProvider converts a handler to a handler provider
132131
// A handler provider is a function that takes a "next" http.Handler, it can be used as a middleware
133132
func toHandlerProvider(handler any) func(next http.Handler) http.Handler {
134-
if hp, ok := handler.(func(next http.Handler) http.Handler); ok {
135-
return hp
136-
}
137-
138133
funcInfo := routing.GetFuncInfo(handler)
139134
fn := reflect.ValueOf(handler)
140135
if fn.Type().Kind() != reflect.Func {
141136
panic(fmt.Sprintf("handler must be a function, but got %s", fn.Type()))
142137
}
143138

139+
if hp, ok := handler.(func(next http.Handler) http.Handler); ok {
140+
return func(next http.Handler) http.Handler {
141+
h := hp(next) // this handle could be dynamically generated, so we can't use it for debug info
142+
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
143+
routing.UpdateFuncInfo(req.Context(), funcInfo)
144+
h.ServeHTTP(resp, req)
145+
})
146+
}
147+
}
148+
144149
provider := func(next http.Handler) http.Handler {
145150
return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) {
146151
// wrap the response writer to check whether the response has been written
@@ -175,26 +180,3 @@ func toHandlerProvider(handler any) func(next http.Handler) http.Handler {
175180
provider(nil).ServeHTTP(nil, nil) // do a pre-check to make sure all arguments and return values are supported
176181
return provider
177182
}
178-
179-
// MiddlewareWithPrefix wraps a handler function at a prefix, and make it as a middleware
180-
// TODO: this design is incorrect, the asset handler should not be a middleware
181-
func MiddlewareWithPrefix(pathPrefix string, middleware func(handler http.Handler) http.Handler, handlerFunc http.HandlerFunc) func(next http.Handler) http.Handler {
182-
funcInfo := routing.GetFuncInfo(handlerFunc)
183-
handler := http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
184-
routing.UpdateFuncInfo(req.Context(), funcInfo)
185-
handlerFunc(resp, req)
186-
})
187-
return func(next http.Handler) http.Handler {
188-
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
189-
if !strings.HasPrefix(req.URL.Path, pathPrefix) {
190-
next.ServeHTTP(resp, req)
191-
return
192-
}
193-
if middleware != nil {
194-
middleware(handler).ServeHTTP(resp, req)
195-
} else {
196-
handler.ServeHTTP(resp, req)
197-
}
198-
})
199-
}
200-
}

modules/web/route.go

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -44,23 +44,13 @@ type Route struct {
4444
// NewRoute creates a new route
4545
func NewRoute() *Route {
4646
r := chi.NewRouter()
47-
return &Route{
48-
R: r,
49-
curGroupPrefix: "",
50-
curMiddlewares: []interface{}{},
51-
}
47+
return &Route{R: r}
5248
}
5349

5450
// Use supports two middlewares
5551
func (r *Route) Use(middlewares ...interface{}) {
56-
if r.curGroupPrefix != "" {
57-
// FIXME: this behavior is incorrect, should use "With" instead
58-
r.curMiddlewares = append(r.curMiddlewares, middlewares...)
59-
} else {
60-
// FIXME: another misuse, the "Use" with empty middlewares is called after "Mount"
61-
for _, m := range middlewares {
62-
r.R.Use(toHandlerProvider(m))
63-
}
52+
for _, m := range middlewares {
53+
r.R.Use(toHandlerProvider(m))
6454
}
6555
}
6656

@@ -116,9 +106,7 @@ func (r *Route) Methods(method, pattern string, h []any) {
116106

117107
// Mount attaches another Route along ./pattern/*
118108
func (r *Route) Mount(pattern string, subR *Route) {
119-
middlewares := make([]interface{}, len(r.curMiddlewares))
120-
copy(middlewares, r.curMiddlewares)
121-
subR.Use(middlewares...)
109+
subR.Use(r.curMiddlewares...)
122110
r.R.Mount(r.getPattern(pattern), subR.R)
123111
}
124112

routers/common/middleware.go

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,23 @@ import (
1515
"code.gitea.io/gitea/modules/setting"
1616
"code.gitea.io/gitea/modules/web/routing"
1717

18+
"gitea.com/go-chi/session"
1819
"github.com/chi-middleware/proxy"
1920
chi "github.com/go-chi/chi/v5"
2021
)
2122

22-
// Middlewares returns common middlewares
23-
func Middlewares() []func(http.Handler) http.Handler {
24-
handlers := []func(http.Handler) http.Handler{
25-
func(next http.Handler) http.Handler {
26-
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
27-
// First of all escape the URL RawPath to ensure that all routing is done using a correctly escaped URL
28-
req.URL.RawPath = req.URL.EscapedPath()
23+
// ProtocolMiddlewares returns HTTP protocol related middlewares
24+
func ProtocolMiddlewares() (handlers []any) {
25+
handlers = append(handlers, func(next http.Handler) http.Handler {
26+
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
27+
// First of all escape the URL RawPath to ensure that all routing is done using a correctly escaped URL
28+
req.URL.RawPath = req.URL.EscapedPath()
2929

30-
ctx, _, finished := process.GetManager().AddTypedContext(req.Context(), fmt.Sprintf("%s: %s", req.Method, req.RequestURI), process.RequestProcessType, true)
31-
defer finished()
32-
next.ServeHTTP(context.NewResponse(resp), req.WithContext(cache.WithCacheContext(ctx)))
33-
})
34-
},
35-
}
30+
ctx, _, finished := process.GetManager().AddTypedContext(req.Context(), fmt.Sprintf("%s: %s", req.Method, req.RequestURI), process.RequestProcessType, true)
31+
defer finished()
32+
next.ServeHTTP(context.NewResponse(resp), req.WithContext(cache.WithCacheContext(ctx)))
33+
})
34+
})
3635

3736
if setting.ReverseProxyLimit > 0 {
3837
opt := proxy.NewForwardedHeadersOptions().
@@ -112,3 +111,17 @@ func stripSlashesMiddleware(next http.Handler) http.Handler {
112111
next.ServeHTTP(resp, req)
113112
})
114113
}
114+
115+
func Sessioner() func(next http.Handler) http.Handler {
116+
return session.Sessioner(session.Options{
117+
Provider: setting.SessionConfig.Provider,
118+
ProviderConfig: setting.SessionConfig.ProviderConfig,
119+
CookieName: setting.SessionConfig.CookieName,
120+
CookiePath: setting.SessionConfig.CookiePath,
121+
Gclifetime: setting.SessionConfig.Gclifetime,
122+
Maxlifetime: setting.SessionConfig.Maxlifetime,
123+
Secure: setting.SessionConfig.Secure,
124+
SameSite: setting.SessionConfig.SameSite,
125+
Domain: setting.SessionConfig.Domain,
126+
})
127+
}

routers/init.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -177,20 +177,15 @@ func GlobalInitInstalled(ctx context.Context) {
177177
func NormalRoutes(ctx context.Context) *web.Route {
178178
ctx, _ = templates.HTMLRenderer(ctx)
179179
r := web.NewRoute()
180-
for _, middle := range common.Middlewares() {
181-
r.Use(middle)
182-
}
180+
r.Use(common.ProtocolMiddlewares()...)
183181

184182
r.Mount("/", web_routers.Routes(ctx))
185183
r.Mount("/api/v1", apiv1.Routes(ctx))
186184
r.Mount("/api/internal", private.Routes())
187185

188186
if setting.Packages.Enabled {
189-
// Add endpoints to match common package manager APIs
190-
191187
// This implements package support for most package managers
192188
r.Mount("/api/packages", packages_router.CommonRoutes(ctx))
193-
194189
// This implements the OCI API (Note this is not preceded by /api but is instead /v2)
195190
r.Mount("/v2", packages_router.ContainerRoutes(ctx))
196191
}

routers/install/routes.go

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ import (
1919
"code.gitea.io/gitea/routers/common"
2020
"code.gitea.io/gitea/routers/web/healthcheck"
2121
"code.gitea.io/gitea/services/forms"
22-
23-
"gitea.com/go-chi/session"
2422
)
2523

2624
type dataStore map[string]interface{}
@@ -30,7 +28,6 @@ func (d *dataStore) GetData() map[string]interface{} {
3028
}
3129

3230
func installRecovery(ctx goctx.Context) func(next http.Handler) http.Handler {
33-
_, rnd := templates.HTMLRenderer(ctx)
3431
return func(next http.Handler) http.Handler {
3532
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
3633
defer func() {
@@ -69,6 +66,7 @@ func installRecovery(ctx goctx.Context) func(next http.Handler) http.Handler {
6966
if !setting.IsProd {
7067
store["ErrorMsg"] = combinedErr
7168
}
69+
_, rnd := templates.HTMLRenderer(ctx)
7270
err = rnd.HTML(w, http.StatusInternalServerError, "status/500", templates.BaseVars().Merge(store))
7371
if err != nil {
7472
log.Error("%v", err)
@@ -83,34 +81,22 @@ func installRecovery(ctx goctx.Context) func(next http.Handler) http.Handler {
8381

8482
// Routes registers the installation routes
8583
func Routes(ctx goctx.Context) *web.Route {
86-
r := web.NewRoute()
87-
for _, middle := range common.Middlewares() {
88-
r.Use(middle)
89-
}
90-
91-
r.Use(web.MiddlewareWithPrefix("/assets/", nil, public.AssetsHandlerFunc("/assets/")))
92-
93-
r.Use(session.Sessioner(session.Options{
94-
Provider: setting.SessionConfig.Provider,
95-
ProviderConfig: setting.SessionConfig.ProviderConfig,
96-
CookieName: setting.SessionConfig.CookieName,
97-
CookiePath: setting.SessionConfig.CookiePath,
98-
Gclifetime: setting.SessionConfig.Gclifetime,
99-
Maxlifetime: setting.SessionConfig.Maxlifetime,
100-
Secure: setting.SessionConfig.Secure,
101-
SameSite: setting.SessionConfig.SameSite,
102-
Domain: setting.SessionConfig.Domain,
103-
}))
84+
base := web.NewRoute()
85+
base.Use(common.ProtocolMiddlewares()...)
86+
base.RouteMethods("/assets/*", "GET, HEAD", public.AssetsHandlerFunc("/assets/"))
10487

88+
r := web.NewRoute()
89+
r.Use(common.Sessioner())
10590
r.Use(installRecovery(ctx))
10691
r.Use(Init(ctx))
10792
r.Get("/", Install) // it must be on the root, because the "install.js" use the window.location to replace the "localhost" AppURL
10893
r.Post("/", web.Bind(forms.InstallForm{}), SubmitInstall)
10994
r.Get("/post-install", InstallDone)
11095
r.Get("/api/healthz", healthcheck.Check)
111-
11296
r.NotFound(installNotFound)
113-
return r
97+
98+
base.Mount("", r)
99+
return base
114100
}
115101

116102
func installNotFound(w http.ResponseWriter, req *http.Request) {

routers/install/routes_test.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ import (
1111
)
1212

1313
func TestRoutes(t *testing.T) {
14+
// TODO: this test seems not really testing the handlers
1415
ctx, cancel := context.WithCancel(context.Background())
1516
defer cancel()
16-
routes := Routes(ctx)
17-
assert.NotNil(t, routes)
18-
assert.EqualValues(t, "/", routes.R.Routes()[0].Pattern)
19-
assert.Nil(t, routes.R.Routes()[0].SubRoutes)
20-
assert.Len(t, routes.R.Routes()[0].Handlers, 2)
17+
base := Routes(ctx)
18+
assert.NotNil(t, base)
19+
r := base.R.Routes()[1]
20+
routes := r.SubRoutes.Routes()[0]
21+
assert.EqualValues(t, "/", routes.Pattern)
22+
assert.Nil(t, routes.SubRoutes)
23+
assert.Len(t, routes.Handlers, 2)
2124
}

routers/web/base.go

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,7 @@ func storageHandler(storageSetting setting.Storage, prefix string, objStore stor
5959
return
6060
}
6161

62-
http.Redirect(
63-
w,
64-
req,
65-
u.String(),
66-
http.StatusTemporaryRedirect,
67-
)
62+
http.Redirect(w, req, u.String(), http.StatusTemporaryRedirect)
6863
})
6964
}
7065

@@ -122,9 +117,9 @@ func (d *dataStore) GetData() map[string]interface{} {
122117
return *d
123118
}
124119

125-
// Recovery returns a middleware that recovers from any panics and writes a 500 and a log if so.
120+
// RecoveryWith500Page returns a middleware that recovers from any panics and writes a 500 and a log if so.
126121
// This error will be created with the gitea 500 page.
127-
func Recovery(ctx goctx.Context) func(next http.Handler) http.Handler {
122+
func RecoveryWith500Page(ctx goctx.Context) func(next http.Handler) http.Handler {
128123
_, rnd := templates.HTMLRenderer(ctx)
129124
return func(next http.Handler) http.Handler {
130125
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {

0 commit comments

Comments
 (0)