From c79adf00b830ea206d1c46ce298c86802c5404d9 Mon Sep 17 00:00:00 2001 From: Wesley van Tilburg Date: Mon, 27 Jan 2025 03:07:39 +0100 Subject: [PATCH] Add basic auth support to rss/atom feeds (#33371) Allows RSS readers to access private feeds using their basic auth capabilities. Not all clients feature the ability to add cookies or headers. fixes #32458 Tested with miniflux no credentials: ![image](https://github.com/user-attachments/assets/8c3369f2-1cf6-4ce3-ac6e-84447e454928) basic auth entered: ![image](https://github.com/user-attachments/assets/c93ff22c-1429-4a80-898f-91d9f35c7c61) ![image](https://github.com/user-attachments/assets/60d83afd-9dde-4973-a440-ff8138799e87) --------- Co-authored-by: wxiaoguang --- services/auth/auth.go | 20 ++++++++++++--- services/auth/auth_test.go | 51 +++++++++++++++++++++++++++----------- services/auth/basic.go | 5 ++-- 3 files changed, 57 insertions(+), 19 deletions(-) diff --git a/services/auth/auth.go b/services/auth/auth.go index eb90202d24..7deca9bc3d 100644 --- a/services/auth/auth.go +++ b/services/auth/auth.go @@ -26,13 +26,17 @@ type globalVarsStruct struct { gitRawOrAttachPathRe *regexp.Regexp lfsPathRe *regexp.Regexp archivePathRe *regexp.Regexp + feedPathRe *regexp.Regexp + feedRefPathRe *regexp.Regexp } var globalVars = sync.OnceValue(func() *globalVarsStruct { return &globalVarsStruct{ - gitRawOrAttachPathRe: regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/(?:(?:git-(?:(?:upload)|(?:receive))-pack$)|(?:info/refs$)|(?:HEAD$)|(?:objects/)|(?:raw/)|(?:releases/download/)|(?:attachments/))`), - lfsPathRe: regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/info/lfs/`), - archivePathRe: regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/archive/`), + gitRawOrAttachPathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/(?:(?:git-(?:(?:upload)|(?:receive))-pack$)|(?:info/refs$)|(?:HEAD$)|(?:objects/)|(?:raw/)|(?:releases/download/)|(?:attachments/))`), + lfsPathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/info/lfs/`), + archivePathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/archive/`), + feedPathRe: regexp.MustCompile(`^/[-.\w]+(/[-.\w]+)?\.(rss|atom)$`), // "/owner.rss" or "/owner/repo.atom" + feedRefPathRe: regexp.MustCompile(`^/[-.\w]+/[-.\w]+/(rss|atom)/`), // "/owner/repo/rss/branch/..." } }) @@ -61,6 +65,16 @@ func (a *authPathDetector) isAttachmentDownload() bool { return strings.HasPrefix(a.req.URL.Path, "/attachments/") && a.req.Method == "GET" } +func (a *authPathDetector) isFeedRequest(req *http.Request) bool { + if !setting.Other.EnableFeed { + return false + } + if req.Method != "GET" { + return false + } + return a.vars.feedPathRe.MatchString(req.URL.Path) || a.vars.feedRefPathRe.MatchString(req.URL.Path) +} + // isContainerPath checks if the request targets the container endpoint func (a *authPathDetector) isContainerPath() bool { return strings.HasPrefix(a.req.URL.Path, "/v2/") diff --git a/services/auth/auth_test.go b/services/auth/auth_test.go index 55ffdebe2d..b8d3396163 100644 --- a/services/auth/auth_test.go +++ b/services/auth/auth_test.go @@ -9,6 +9,7 @@ import ( "testing" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "github.com/stretchr/testify/assert" ) @@ -92,6 +93,19 @@ func Test_isGitRawOrLFSPath(t *testing.T) { true, }, } + + defer test.MockVariableValue(&setting.LFS.StartServer)() + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + req, _ := http.NewRequest("POST", "http://localhost"+tt.path, nil) + setting.LFS.StartServer = false + assert.Equal(t, tt.want, newAuthPathDetector(req).isGitRawOrAttachOrLFSPath()) + + setting.LFS.StartServer = true + assert.Equal(t, tt.want, newAuthPathDetector(req).isGitRawOrAttachOrLFSPath()) + }) + } + lfsTests := []string{ "/owner/repo/info/lfs/", "/owner/repo/info/lfs/objects/batch", @@ -103,19 +117,6 @@ func Test_isGitRawOrLFSPath(t *testing.T) { "/owner/repo/info/lfs/locks/verify", "/owner/repo/info/lfs/locks/123/unlock", } - - origLFSStartServer := setting.LFS.StartServer - - for _, tt := range tests { - t.Run(tt.path, func(t *testing.T) { - req, _ := http.NewRequest("POST", "http://localhost"+tt.path, nil) - setting.LFS.StartServer = false - assert.Equal(t, tt.want, newAuthPathDetector(req).isGitRawOrAttachOrLFSPath()) - - setting.LFS.StartServer = true - assert.Equal(t, tt.want, newAuthPathDetector(req).isGitRawOrAttachOrLFSPath()) - }) - } for _, tt := range lfsTests { t.Run(tt, func(t *testing.T) { req, _ := http.NewRequest("POST", tt, nil) @@ -128,5 +129,27 @@ func Test_isGitRawOrLFSPath(t *testing.T) { assert.Equalf(t, setting.LFS.StartServer, got, "isGitOrLFSPath(%q) = %v, want %v", tt, got, setting.LFS.StartServer) }) } - setting.LFS.StartServer = origLFSStartServer +} + +func Test_isFeedRequest(t *testing.T) { + tests := []struct { + want bool + path string + }{ + {true, "/user.rss"}, + {true, "/user/repo.atom"}, + {false, "/user/repo"}, + {false, "/use/repo/file.rss"}, + + {true, "/org/repo/rss/branch/xxx"}, + {true, "/org/repo/atom/tag/xxx"}, + {false, "/org/repo/branch/main/rss/any"}, + {false, "/org/atom/any"}, + } + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + req, _ := http.NewRequest("GET", "http://localhost"+tt.path, nil) + assert.Equal(t, tt.want, newAuthPathDetector(req).isFeedRequest(req)) + }) + } } diff --git a/services/auth/basic.go b/services/auth/basic.go index 67987206a7..e22b9e1eb7 100644 --- a/services/auth/basic.go +++ b/services/auth/basic.go @@ -47,9 +47,10 @@ func (b *Basic) Name() string { // name/token on successful validation. // Returns nil if header is empty or validation fails. func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { - // Basic authentication should only fire on API, Download or on Git or LFSPaths + // Basic authentication should only fire on API, Feed, Download or on Git or LFSPaths + // Not all feed (rss/atom) clients feature the ability to add cookies or headers, so we need to allow basic auth for feeds detector := newAuthPathDetector(req) - if !detector.isAPIPath() && !detector.isContainerPath() && !detector.isAttachmentDownload() && !detector.isGitRawOrAttachOrLFSPath() { + if !detector.isAPIPath() && !detector.isFeedRequest(req) && !detector.isContainerPath() && !detector.isAttachmentDownload() && !detector.isGitRawOrAttachOrLFSPath() { return nil, nil }