Add basic auth support to rss/atom feeds ()

Allows RSS readers to access private feeds using their basic auth
capabilities. Not all clients feature the ability to add cookies or
headers.

fixes  

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 <wxiaoguang@gmail.com>
pull/33012/head^2
Wesley van Tilburg 2025-01-27 03:07:39 +01:00 committed by GitHub
parent 26b51aa032
commit c79adf00b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 57 additions and 19 deletions

View File

@ -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/")

View File

@ -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))
})
}
}

View File

@ -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
}