Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor "route" related code, fix Safari cookie bug #24330

Merged
merged 8 commits into from
Apr 27, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions modules/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,13 +446,33 @@ func (ctx *Context) JSON(status int, content interface{}) {
}
}

func removeSessionCookieHeader(w http.ResponseWriter) {
cookies := w.Header()["Set-Cookie"]
w.Header().Del("Set-Cookie")
for _, cookie := range cookies {
if strings.HasPrefix(cookie, setting.SessionConfig.CookieName+"=") {
continue
}
w.Header().Add("Set-Cookie", cookie)
}
}

// Redirect redirects the request
func (ctx *Context) Redirect(location string, status ...int) {
code := http.StatusSeeOther
if len(status) == 1 {
code = status[0]
}

if strings.Contains(location, "://") || strings.HasPrefix(location, "//") {
// 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
// 2. some time later, the request to "/my-path" doesn't contain cookie (caused by Prevent web tracking)
// 3. Gitea's Sessioner doesn't see the session cookie, so it generates a new session id, and returns it to browser
// 4. then the browser accepts the empty session, then the user is logged out
// So in this case, we should remove the session cookie from the response header
removeSessionCookieHeader(ctx.Resp)
}
http.Redirect(ctx.Resp, ctx.Req, location, code)
}

Expand Down
40 changes: 40 additions & 0 deletions modules/context/context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package context

import (
"net/http"
"testing"

"code.gitea.io/gitea/modules/setting"

"github.com/stretchr/testify/assert"
)

type mockResponseWriter struct {
header http.Header
}

func (m *mockResponseWriter) Header() http.Header {
return m.header
}

func (m *mockResponseWriter) Write(bytes []byte) (int, error) {
panic("implement me")
}

func (m *mockResponseWriter) WriteHeader(statusCode int) {
panic("implement me")
}

func TestRemoveSessionCookieHeader(t *testing.T) {
w := &mockResponseWriter{}
w.header = http.Header{}
w.header.Add("Set-Cookie", (&http.Cookie{Name: setting.SessionConfig.CookieName, Value: "foo"}).String())
w.header.Add("Set-Cookie", (&http.Cookie{Name: "other", Value: "bar"}).String())
assert.Len(t, w.Header().Values("Set-Cookie"), 2)
removeSessionCookieHeader(w)
assert.Len(t, w.Header().Values("Set-Cookie"), 1)
assert.Contains(t, "other=bar", w.Header().Get("Set-Cookie"))
}
24 changes: 0 additions & 24 deletions modules/web/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"fmt"
"net/http"
"reflect"
"strings"

"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/web/routing"
Expand Down Expand Up @@ -175,26 +174,3 @@ func toHandlerProvider(handler any) func(next http.Handler) http.Handler {
provider(nil).ServeHTTP(nil, nil) // do a pre-check to make sure all arguments and return values are supported
return provider
}

// MiddlewareWithPrefix wraps a handler function at a prefix, and make it as a middleware
// TODO: this design is incorrect, the asset handler should not be a middleware
func MiddlewareWithPrefix(pathPrefix string, middleware func(handler http.Handler) http.Handler, handlerFunc http.HandlerFunc) func(next http.Handler) http.Handler {
funcInfo := routing.GetFuncInfo(handlerFunc)
handler := http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
routing.UpdateFuncInfo(req.Context(), funcInfo)
handlerFunc(resp, req)
})
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
if !strings.HasPrefix(req.URL.Path, pathPrefix) {
next.ServeHTTP(resp, req)
return
}
if middleware != nil {
middleware(handler).ServeHTTP(resp, req)
} else {
handler.ServeHTTP(resp, req)
}
})
}
}
20 changes: 4 additions & 16 deletions modules/web/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,23 +44,13 @@ type Route struct {
// NewRoute creates a new route
func NewRoute() *Route {
r := chi.NewRouter()
return &Route{
R: r,
curGroupPrefix: "",
curMiddlewares: []interface{}{},
}
return &Route{R: r}
}

// Use supports two middlewares
func (r *Route) Use(middlewares ...interface{}) {
if r.curGroupPrefix != "" {
// FIXME: this behavior is incorrect, should use "With" instead
r.curMiddlewares = append(r.curMiddlewares, middlewares...)
} else {
// FIXME: another misuse, the "Use" with empty middlewares is called after "Mount"
for _, m := range middlewares {
r.R.Use(toHandlerProvider(m))
}
for _, m := range middlewares {
r.R.Use(toHandlerProvider(m))
}
}

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

// Mount attaches another Route along ./pattern/*
func (r *Route) Mount(pattern string, subR *Route) {
middlewares := make([]interface{}, len(r.curMiddlewares))
copy(middlewares, r.curMiddlewares)
subR.Use(middlewares...)
subR.Use(r.curMiddlewares...)
r.R.Mount(r.getPattern(pattern), subR.R)
}

Expand Down
39 changes: 26 additions & 13 deletions routers/common/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,23 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/routing"

"gitea.com/go-chi/session"
"github.com/chi-middleware/proxy"
chi "github.com/go-chi/chi/v5"
)

// Middlewares returns common middlewares
func Middlewares() []func(http.Handler) http.Handler {
handlers := []func(http.Handler) http.Handler{
func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
// First of all escape the URL RawPath to ensure that all routing is done using a correctly escaped URL
req.URL.RawPath = req.URL.EscapedPath()
// ProtocolMiddlewares returns HTTP protocol related middlewares
func ProtocolMiddlewares() (handlers []any) {
wxiaoguang marked this conversation as resolved.
Show resolved Hide resolved
handlers = append(handlers, func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
// First of all escape the URL RawPath to ensure that all routing is done using a correctly escaped URL
req.URL.RawPath = req.URL.EscapedPath()

ctx, _, finished := process.GetManager().AddTypedContext(req.Context(), fmt.Sprintf("%s: %s", req.Method, req.RequestURI), process.RequestProcessType, true)
defer finished()
next.ServeHTTP(context.NewResponse(resp), req.WithContext(cache.WithCacheContext(ctx)))
})
},
}
ctx, _, finished := process.GetManager().AddTypedContext(req.Context(), fmt.Sprintf("%s: %s", req.Method, req.RequestURI), process.RequestProcessType, true)
defer finished()
next.ServeHTTP(context.NewResponse(resp), req.WithContext(cache.WithCacheContext(ctx)))
})
})

if setting.ReverseProxyLimit > 0 {
opt := proxy.NewForwardedHeadersOptions().
Expand Down Expand Up @@ -112,3 +111,17 @@ func stripSlashesMiddleware(next http.Handler) http.Handler {
next.ServeHTTP(resp, req)
})
}

func Sessioner() func(next http.Handler) http.Handler {
return session.Sessioner(session.Options{
Provider: setting.SessionConfig.Provider,
ProviderConfig: setting.SessionConfig.ProviderConfig,
CookieName: setting.SessionConfig.CookieName,
CookiePath: setting.SessionConfig.CookiePath,
Gclifetime: setting.SessionConfig.Gclifetime,
Maxlifetime: setting.SessionConfig.Maxlifetime,
Secure: setting.SessionConfig.Secure,
SameSite: setting.SessionConfig.SameSite,
Domain: setting.SessionConfig.Domain,
})
}
7 changes: 1 addition & 6 deletions routers/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,20 +177,15 @@ func GlobalInitInstalled(ctx context.Context) {
func NormalRoutes(ctx context.Context) *web.Route {
ctx, _ = templates.HTMLRenderer(ctx)
r := web.NewRoute()
for _, middle := range common.Middlewares() {
r.Use(middle)
}
r.Use(common.ProtocolMiddlewares()...)

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

if setting.Packages.Enabled {
// Add endpoints to match common package manager APIs

// This implements package support for most package managers
r.Mount("/api/packages", packages_router.CommonRoutes(ctx))

// This implements the OCI API (Note this is not preceded by /api but is instead /v2)
r.Mount("/v2", packages_router.ContainerRoutes(ctx))
}
Expand Down
32 changes: 9 additions & 23 deletions routers/install/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ import (
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/routers/web/healthcheck"
"code.gitea.io/gitea/services/forms"

"gitea.com/go-chi/session"
)

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

func installRecovery(ctx goctx.Context) func(next http.Handler) http.Handler {
_, rnd := templates.HTMLRenderer(ctx)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
defer func() {
Expand Down Expand Up @@ -69,6 +66,7 @@ func installRecovery(ctx goctx.Context) func(next http.Handler) http.Handler {
if !setting.IsProd {
store["ErrorMsg"] = combinedErr
}
_, rnd := templates.HTMLRenderer(ctx)
err = rnd.HTML(w, http.StatusInternalServerError, "status/500", templates.BaseVars().Merge(store))
if err != nil {
log.Error("%v", err)
Expand All @@ -83,34 +81,22 @@ func installRecovery(ctx goctx.Context) func(next http.Handler) http.Handler {

// Routes registers the installation routes
func Routes(ctx goctx.Context) *web.Route {
r := web.NewRoute()
for _, middle := range common.Middlewares() {
r.Use(middle)
}

r.Use(web.MiddlewareWithPrefix("/assets/", nil, public.AssetsHandlerFunc("/assets/")))

r.Use(session.Sessioner(session.Options{
Provider: setting.SessionConfig.Provider,
ProviderConfig: setting.SessionConfig.ProviderConfig,
CookieName: setting.SessionConfig.CookieName,
CookiePath: setting.SessionConfig.CookiePath,
Gclifetime: setting.SessionConfig.Gclifetime,
Maxlifetime: setting.SessionConfig.Maxlifetime,
Secure: setting.SessionConfig.Secure,
SameSite: setting.SessionConfig.SameSite,
Domain: setting.SessionConfig.Domain,
}))
base := web.NewRoute()
base.Use(common.ProtocolMiddlewares()...)
base.RouteMethods("/assets/*", "GET, HEAD", public.AssetsHandlerFunc("/assets/"))

r := web.NewRoute()
r.Use(common.Sessioner())
r.Use(installRecovery(ctx))
r.Use(Init(ctx))
r.Get("/", Install) // it must be on the root, because the "install.js" use the window.location to replace the "localhost" AppURL
r.Post("/", web.Bind(forms.InstallForm{}), SubmitInstall)
r.Get("/post-install", InstallDone)
r.Get("/api/healthz", healthcheck.Check)

r.NotFound(installNotFound)
return r

base.Mount("", r)
return base
}

func installNotFound(w http.ResponseWriter, req *http.Request) {
Expand Down
13 changes: 8 additions & 5 deletions routers/install/routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ import (
)

func TestRoutes(t *testing.T) {
// TODO: this test seems not really testing the handlers
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
routes := Routes(ctx)
assert.NotNil(t, routes)
assert.EqualValues(t, "/", routes.R.Routes()[0].Pattern)
assert.Nil(t, routes.R.Routes()[0].SubRoutes)
assert.Len(t, routes.R.Routes()[0].Handlers, 2)
base := Routes(ctx)
assert.NotNil(t, base)
r := base.R.Routes()[1]
routes := r.SubRoutes.Routes()[0]
assert.EqualValues(t, "/", routes.Pattern)
assert.Nil(t, routes.SubRoutes)
assert.Len(t, routes.Handlers, 2)
}
11 changes: 3 additions & 8 deletions routers/web/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,7 @@ func storageHandler(storageSetting setting.Storage, prefix string, objStore stor
return
}

http.Redirect(
w,
req,
u.String(),
http.StatusTemporaryRedirect,
)
http.Redirect(w, req, u.String(), http.StatusTemporaryRedirect)
})
}

Expand Down Expand Up @@ -122,9 +117,9 @@ func (d *dataStore) GetData() map[string]interface{} {
return *d
}

// Recovery returns a middleware that recovers from any panics and writes a 500 and a log if so.
// RecoveryWith500Page returns a middleware that recovers from any panics and writes a 500 and a log if so.
// This error will be created with the gitea 500 page.
func Recovery(ctx goctx.Context) func(next http.Handler) http.Handler {
func RecoveryWith500Page(ctx goctx.Context) func(next http.Handler) http.Handler {
_, rnd := templates.HTMLRenderer(ctx)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
Expand Down
49 changes: 49 additions & 0 deletions routers/web/misc/misc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package misc

import (
"net/http"
"os"
"path"

"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)

func SSHInfo(rw http.ResponseWriter, req *http.Request) {
if !git.SupportProcReceive {
rw.WriteHeader(http.StatusNotFound)
return
}
rw.Header().Set("content-type", "text/json;charset=UTF-8")
_, err := rw.Write([]byte(`{"type":"gitea","version":1}`))
if err != nil {
log.Error("fail to write result: err: %v", err)
rw.WriteHeader(http.StatusInternalServerError)
return
}
rw.WriteHeader(http.StatusOK)
}

func DummyOK(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusOK)
}

func RobotsTxt(w http.ResponseWriter, req *http.Request) {
filePath := path.Join(setting.CustomPath, "robots.txt")
fi, err := os.Stat(filePath)
if err == nil && httpcache.HandleTimeCache(req, w, fi) {
return
}
http.ServeFile(w, req, filePath)
}

func StaticRedirect(target string) func(w http.ResponseWriter, req *http.Request) {
return func(w http.ResponseWriter, req *http.Request) {
http.Redirect(w, req, path.Join(setting.StaticURLPrefix, target), http.StatusMovedPermanently)
}
}
Loading