aboutsummaryrefslogtreecommitdiffstats
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/auth/httpauth/httpauth.go47
-rw-r--r--modules/auth/httpauth/httpauth_test.go43
-rw-r--r--modules/base/tool.go16
-rw-r--r--modules/base/tool_test.go19
-rw-r--r--modules/structs/repo_file.go19
-rw-r--r--modules/util/string.go21
-rw-r--r--modules/web/router_path.go7
-rw-r--r--modules/web/router_test.go39
8 files changed, 154 insertions, 57 deletions
diff --git a/modules/auth/httpauth/httpauth.go b/modules/auth/httpauth/httpauth.go
new file mode 100644
index 0000000000..7f1f1ee152
--- /dev/null
+++ b/modules/auth/httpauth/httpauth.go
@@ -0,0 +1,47 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package httpauth
+
+import (
+ "encoding/base64"
+ "strings"
+
+ "code.gitea.io/gitea/modules/util"
+)
+
+type BasicAuth struct {
+ Username, Password string
+}
+
+type BearerToken struct {
+ Token string
+}
+
+type ParsedAuthorizationHeader struct {
+ BasicAuth *BasicAuth
+ BearerToken *BearerToken
+}
+
+func ParseAuthorizationHeader(header string) (ret ParsedAuthorizationHeader, _ bool) {
+ parts := strings.Fields(header)
+ if len(parts) != 2 {
+ return ret, false
+ }
+ if util.AsciiEqualFold(parts[0], "basic") {
+ s, err := base64.StdEncoding.DecodeString(parts[1])
+ if err != nil {
+ return ret, false
+ }
+ u, p, ok := strings.Cut(string(s), ":")
+ if !ok {
+ return ret, false
+ }
+ ret.BasicAuth = &BasicAuth{Username: u, Password: p}
+ return ret, true
+ } else if util.AsciiEqualFold(parts[0], "token") || util.AsciiEqualFold(parts[0], "bearer") {
+ ret.BearerToken = &BearerToken{Token: parts[1]}
+ return ret, true
+ }
+ return ret, false
+}
diff --git a/modules/auth/httpauth/httpauth_test.go b/modules/auth/httpauth/httpauth_test.go
new file mode 100644
index 0000000000..087b86917f
--- /dev/null
+++ b/modules/auth/httpauth/httpauth_test.go
@@ -0,0 +1,43 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package httpauth
+
+import (
+ "encoding/base64"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestParseAuthorizationHeader(t *testing.T) {
+ type parsed = ParsedAuthorizationHeader
+ type basic = BasicAuth
+ type bearer = BearerToken
+ cases := []struct {
+ headerValue string
+ expected parsed
+ ok bool
+ }{
+ {"", parsed{}, false},
+ {"?", parsed{}, false},
+ {"foo", parsed{}, false},
+ {"any value", parsed{}, false},
+
+ {"Basic ?", parsed{}, false},
+ {"Basic " + base64.StdEncoding.EncodeToString([]byte("foo")), parsed{}, false},
+ {"Basic " + base64.StdEncoding.EncodeToString([]byte("foo:bar")), parsed{BasicAuth: &basic{"foo", "bar"}}, true},
+ {"basic " + base64.StdEncoding.EncodeToString([]byte("foo:bar")), parsed{BasicAuth: &basic{"foo", "bar"}}, true},
+
+ {"token value", parsed{BearerToken: &bearer{"value"}}, true},
+ {"Token value", parsed{BearerToken: &bearer{"value"}}, true},
+ {"bearer value", parsed{BearerToken: &bearer{"value"}}, true},
+ {"Bearer value", parsed{BearerToken: &bearer{"value"}}, true},
+ {"Bearer wrong value", parsed{}, false},
+ }
+ for _, c := range cases {
+ ret, ok := ParseAuthorizationHeader(c.headerValue)
+ assert.Equal(t, c.ok, ok, "header %q", c.headerValue)
+ assert.Equal(t, c.expected, ret, "header %q", c.headerValue)
+ }
+}
diff --git a/modules/base/tool.go b/modules/base/tool.go
index 02ca85569e..ed94575e74 100644
--- a/modules/base/tool.go
+++ b/modules/base/tool.go
@@ -8,13 +8,10 @@ import (
"crypto/sha1"
"crypto/sha256"
"crypto/subtle"
- "encoding/base64"
"encoding/hex"
- "errors"
"fmt"
"hash"
"strconv"
- "strings"
"time"
"code.gitea.io/gitea/modules/setting"
@@ -36,19 +33,6 @@ func ShortSha(sha1 string) string {
return util.TruncateRunes(sha1, 10)
}
-// BasicAuthDecode decode basic auth string
-func BasicAuthDecode(encoded string) (string, string, error) {
- s, err := base64.StdEncoding.DecodeString(encoded)
- if err != nil {
- return "", "", err
- }
-
- if username, password, ok := strings.Cut(string(s), ":"); ok {
- return username, password, nil
- }
- return "", "", errors.New("invalid basic authentication")
-}
-
// VerifyTimeLimitCode verify time limit code
func VerifyTimeLimitCode(now time.Time, data string, minutes int, code string) bool {
if len(code) <= 18 {
diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go
index 7cebedb073..b7365e40c4 100644
--- a/modules/base/tool_test.go
+++ b/modules/base/tool_test.go
@@ -26,25 +26,6 @@ func TestShortSha(t *testing.T) {
assert.Equal(t, "veryverylo", ShortSha("veryverylong"))
}
-func TestBasicAuthDecode(t *testing.T) {
- _, _, err := BasicAuthDecode("?")
- assert.Equal(t, "illegal base64 data at input byte 0", err.Error())
-
- user, pass, err := BasicAuthDecode("Zm9vOmJhcg==")
- assert.NoError(t, err)
- assert.Equal(t, "foo", user)
- assert.Equal(t, "bar", pass)
-
- _, _, err = BasicAuthDecode("aW52YWxpZA==")
- assert.Error(t, err)
-
- _, _, err = BasicAuthDecode("invalid")
- assert.Error(t, err)
-
- _, _, err = BasicAuthDecode("YWxpY2U=") // "alice", no colon
- assert.Error(t, err)
-}
-
func TestVerifyTimeLimitCode(t *testing.T) {
defer test.MockVariableValue(&setting.InstallLock, true)()
initGeneralSecret := func(secret string) {
diff --git a/modules/structs/repo_file.go b/modules/structs/repo_file.go
index 91ee060d50..5a86db868b 100644
--- a/modules/structs/repo_file.go
+++ b/modules/structs/repo_file.go
@@ -116,14 +116,17 @@ type ContentsExtResponse struct {
// ContentsResponse contains information about a repo's entry's (dir, file, symlink, submodule) metadata and content
type ContentsResponse struct {
- Name string `json:"name"`
- Path string `json:"path"`
- SHA string `json:"sha"`
- LastCommitSHA string `json:"last_commit_sha"`
+ Name string `json:"name"`
+ Path string `json:"path"`
+ SHA string `json:"sha"`
+
+ LastCommitSHA *string `json:"last_commit_sha,omitempty"`
// swagger:strfmt date-time
- LastCommitterDate time.Time `json:"last_committer_date"`
+ LastCommitterDate *time.Time `json:"last_committer_date,omitempty"`
// swagger:strfmt date-time
- LastAuthorDate time.Time `json:"last_author_date"`
+ LastAuthorDate *time.Time `json:"last_author_date,omitempty"`
+ LastCommitMessage *string `json:"last_commit_message,omitempty"`
+
// `type` will be `file`, `dir`, `symlink`, or `submodule`
Type string `json:"type"`
Size int64 `json:"size"`
@@ -141,8 +144,8 @@ type ContentsResponse struct {
SubmoduleGitURL *string `json:"submodule_git_url"`
Links *FileLinksResponse `json:"_links"`
- LfsOid *string `json:"lfs_oid"`
- LfsSize *int64 `json:"lfs_size"`
+ LfsOid *string `json:"lfs_oid,omitempty"`
+ LfsSize *int64 `json:"lfs_size,omitempty"`
}
// FileCommitResponse contains information generated from a Git commit for a repo's file.
diff --git a/modules/util/string.go b/modules/util/string.go
index 03c0df96a3..b9b59df3ef 100644
--- a/modules/util/string.go
+++ b/modules/util/string.go
@@ -110,3 +110,24 @@ func SplitTrimSpace(input, sep string) []string {
}
return stringList
}
+
+func asciiLower(b byte) byte {
+ if 'A' <= b && b <= 'Z' {
+ return b + ('a' - 'A')
+ }
+ return b
+}
+
+// AsciiEqualFold is from Golang https://cs.opensource.google/go/go/+/refs/tags/go1.24.4:src/net/http/internal/ascii/print.go
+// ASCII only. In most cases for protocols, we should only use this but not [strings.EqualFold]
+func AsciiEqualFold(s, t string) bool { //nolint:revive // PascalCase
+ if len(s) != len(t) {
+ return false
+ }
+ for i := 0; i < len(s); i++ {
+ if asciiLower(s[i]) != asciiLower(t[i]) {
+ return false
+ }
+ }
+ return true
+}
diff --git a/modules/web/router_path.go b/modules/web/router_path.go
index ce041eedab..64154c34a5 100644
--- a/modules/web/router_path.go
+++ b/modules/web/router_path.go
@@ -26,6 +26,7 @@ func (g *RouterPathGroup) ServeHTTP(resp http.ResponseWriter, req *http.Request)
path := chiCtx.URLParam(g.pathParam)
for _, m := range g.matchers {
if m.matchPath(chiCtx, path) {
+ chiCtx.RoutePatterns = append(chiCtx.RoutePatterns, m.pattern)
handler := m.handlerFunc
for i := len(m.middlewares) - 1; i >= 0; i-- {
handler = m.middlewares[i](handler).ServeHTTP
@@ -38,6 +39,7 @@ func (g *RouterPathGroup) ServeHTTP(resp http.ResponseWriter, req *http.Request)
}
type RouterPathGroupPattern struct {
+ pattern string
re *regexp.Regexp
params []routerPathParam
middlewares []any
@@ -62,6 +64,7 @@ type routerPathParam struct {
type routerPathMatcher struct {
methods container.Set[string]
+ pattern string
re *regexp.Regexp
params []routerPathParam
middlewares []func(http.Handler) http.Handler
@@ -117,7 +120,7 @@ func newRouterPathMatcher(methods string, patternRegexp *RouterPathGroupPattern,
}
p.methods.Add(method)
}
- p.re, p.params = patternRegexp.re, patternRegexp.params
+ p.pattern, p.re, p.params = patternRegexp.pattern, patternRegexp.re, patternRegexp.params
return p
}
@@ -157,7 +160,7 @@ func patternRegexp(pattern string, h ...any) *RouterPathGroupPattern {
p.params = append(p.params, param)
}
re = append(re, '$')
- p.re = regexp.MustCompile(string(re))
+ p.pattern, p.re = pattern, regexp.MustCompile(string(re))
return p
}
diff --git a/modules/web/router_test.go b/modules/web/router_test.go
index 1cee2b879b..f216aa6180 100644
--- a/modules/web/router_test.go
+++ b/modules/web/router_test.go
@@ -56,17 +56,20 @@ func TestRouter(t *testing.T) {
recorder.Body = buff
type resultStruct struct {
- method string
- pathParams map[string]string
- handlerMarks []string
+ method string
+ pathParams map[string]string
+ handlerMarks []string
+ chiRoutePattern *string
}
var res resultStruct
h := func(optMark ...string) func(resp http.ResponseWriter, req *http.Request) {
mark := util.OptionalArg(optMark, "")
return func(resp http.ResponseWriter, req *http.Request) {
+ chiCtx := chi.RouteContext(req.Context())
res.method = req.Method
- res.pathParams = chiURLParamsToMap(chi.RouteContext(req.Context()))
+ res.pathParams = chiURLParamsToMap(chiCtx)
+ res.chiRoutePattern = util.ToPointer(chiCtx.RoutePattern())
if mark != "" {
res.handlerMarks = append(res.handlerMarks, mark)
}
@@ -125,21 +128,29 @@ func TestRouter(t *testing.T) {
req, err := http.NewRequest(methodPathFields[0], methodPathFields[1], nil)
assert.NoError(t, err)
r.ServeHTTP(recorder, req)
+ if expected.chiRoutePattern == nil {
+ res.chiRoutePattern = nil
+ }
assert.Equal(t, expected, res)
})
}
t.Run("RootRouter", func(t *testing.T) {
- testRoute(t, "GET /the-user/the-repo/other", resultStruct{method: "GET", handlerMarks: []string{"not-found:/"}})
+ testRoute(t, "GET /the-user/the-repo/other", resultStruct{
+ method: "GET",
+ handlerMarks: []string{"not-found:/"},
+ chiRoutePattern: util.ToPointer(""),
+ })
testRoute(t, "GET /the-user/the-repo/pulls", resultStruct{
method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "pulls"},
handlerMarks: []string{"list-issues-b"},
})
testRoute(t, "GET /the-user/the-repo/issues/123", resultStruct{
- method: "GET",
- pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"},
- handlerMarks: []string{"view-issue"},
+ method: "GET",
+ pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"},
+ handlerMarks: []string{"view-issue"},
+ chiRoutePattern: util.ToPointer("/{username}/{reponame}/{type:issues|pulls}/{index}"),
})
testRoute(t, "GET /the-user/the-repo/issues/123?stop=hijack", resultStruct{
method: "GET",
@@ -154,7 +165,10 @@ func TestRouter(t *testing.T) {
})
t.Run("Sub Router", func(t *testing.T) {
- testRoute(t, "GET /api/v1/other", resultStruct{method: "GET", handlerMarks: []string{"not-found:/api/v1"}})
+ testRoute(t, "GET /api/v1/other", resultStruct{
+ method: "GET",
+ handlerMarks: []string{"not-found:/api/v1"},
+ })
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches", resultStruct{
method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"},
@@ -211,9 +225,10 @@ func TestRouter(t *testing.T) {
})
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s3", resultStruct{
- method: "GET",
- pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"},
- handlerMarks: []string{"s1", "s2", "s3"},
+ method: "GET",
+ pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"},
+ handlerMarks: []string{"s1", "s2", "s3"},
+ chiRoutePattern: util.ToPointer("/api/v1/repos/{username}/{reponame}/branches/<dir:*>/<file:[a-z]{1,2}>"),
})
})
}