mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-06 08:08:37 +02:00
Merge branch 'main' into lunny/some_refactors
This commit is contained in:
commit
73279fb770
301 files changed
+72595
-201038
No files matched your search
@@ -31,7 +31,7 @@ func TestParseCommitWithSSHSignature(t *testing.T) {
|
||||
// AAAEDWqPHTH51xb4hy1y1f1VeWL/2A9Q0b6atOyv5fx8x5prpPrMXSg9qTx04jPNPWRcHs
|
||||
// utyxWjThIpzcaO68yWVnAAAAEXVzZXIyQGV4YW1wbGUuY29tAQIDBA==
|
||||
// -----END OPENSSH PRIVATE KEY-----
|
||||
sshPubKey, err := asymkey_model.AddPublicKey(t.Context(), 999, "user-ssh-key-any-name", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILpPrMXSg9qTx04jPNPWRcHsutyxWjThIpzcaO68yWVn", 0)
|
||||
sshPubKey, err := asymkey_model.AddPublicKey(t.Context(), 999, "user-ssh-key-any-name", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILpPrMXSg9qTx04jPNPWRcHsutyxWjThIpzcaO68yWVn", 0, false)
|
||||
require.NoError(t, err)
|
||||
_, err = db.GetEngine(t.Context()).ID(sshPubKey.ID).Cols("verified").Update(&asymkey_model.PublicKey{Verified: true})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -66,7 +66,7 @@ ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ib
|
||||
|
||||
for i, kase := range testCases {
|
||||
s.ID = int64(i) + 20
|
||||
asymkey_model.AddPublicKeysBySource(t.Context(), user, s, []string{kase.keyString})
|
||||
asymkey_model.AddPublicKeysBySource(t.Context(), user, s, []string{kase.keyString}, false)
|
||||
keys, err := db.Find[asymkey_model.PublicKey](t.Context(), asymkey_model.FindPublicKeyOptions{
|
||||
OwnerID: user.ID,
|
||||
LoginSourceID: s.ID,
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"code.gitea.io/gitea/modules/session"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
gitea_context "code.gitea.io/gitea/services/context"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
)
|
||||
|
||||
@@ -162,9 +161,4 @@ func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore
|
||||
}
|
||||
|
||||
middleware.SetLocaleCookie(resp, user.Language, 0)
|
||||
|
||||
// force to generate a new CSRF token
|
||||
if ctx := gitea_context.GetWebContext(req.Context()); ctx != nil {
|
||||
ctx.Csrf.PrepareForSessionUser(ctx)
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ type Source struct {
|
||||
AttributesInBind bool // fetch attributes in bind context (not user)
|
||||
AttributeSSHPublicKey string // LDAP SSH Public Key attribute
|
||||
AttributeAvatar string
|
||||
SSHKeysAreVerified bool // true if SSH keys in LDAP are verified
|
||||
SearchPageSize uint32 // Search with paging page size
|
||||
Filter string // Query filter to validate entry
|
||||
AdminFilter string // Query filter to check if user is admin
|
||||
|
||||
@@ -73,7 +73,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(ctx, user, source.AuthSource, sr.SSHPublicKey) {
|
||||
if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(ctx, user, source.AuthSource, sr.SSHPublicKey, source.SSHKeysAreVerified) {
|
||||
if err := asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
|
||||
return user, err
|
||||
}
|
||||
@@ -99,7 +99,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
|
||||
return user, err
|
||||
}
|
||||
|
||||
if isAttributeSSHPublicKeySet && asymkey_model.AddPublicKeysBySource(ctx, user, source.AuthSource, sr.SSHPublicKey) {
|
||||
if isAttributeSSHPublicKeySet && asymkey_model.AddPublicKeysBySource(ctx, user, source.AuthSource, sr.SSHPublicKey, source.SSHKeysAreVerified) {
|
||||
if err := asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
||||
|
||||
if err == nil && isAttributeSSHPublicKeySet {
|
||||
log.Trace("SyncExternalUsers[%s]: Adding LDAP Public SSH Keys for user %s", source.AuthSource.Name, usr.Name)
|
||||
if asymkey_model.AddPublicKeysBySource(ctx, usr, source.AuthSource, su.SSHPublicKey) {
|
||||
if asymkey_model.AddPublicKeysBySource(ctx, usr, source.AuthSource, su.SSHPublicKey, source.SSHKeysAreVerified) {
|
||||
sshKeysNeedUpdate = true
|
||||
}
|
||||
}
|
||||
@@ -145,7 +145,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
||||
}
|
||||
} else if updateExisting {
|
||||
// Synchronize SSH Public Key if that attribute is set
|
||||
if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(ctx, usr, source.AuthSource, su.SSHPublicKey) {
|
||||
if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(ctx, usr, source.AuthSource, su.SSHPublicKey, source.SSHKeysAreVerified) {
|
||||
sshKeysNeedUpdate = true
|
||||
}
|
||||
|
||||
|
||||
@@ -227,7 +227,7 @@ func APIContexter() func(http.Handler) http.Handler {
|
||||
|
||||
ctx.SetContextValue(apiContextKey, ctx)
|
||||
|
||||
// If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid.
|
||||
// FIXME: GLOBAL-PARSE-FORM: see more details in another FIXME comment
|
||||
if ctx.Req.Method == http.MethodPost && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") {
|
||||
if !ctx.ParseMultipartForm() {
|
||||
return
|
||||
|
||||
@@ -6,14 +6,12 @@ package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
@@ -48,7 +46,6 @@ type Context struct {
|
||||
PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData`
|
||||
|
||||
Cache cache.StringCache
|
||||
Csrf CSRFProtector
|
||||
Flash *middleware.Flash
|
||||
Session session.Store
|
||||
|
||||
@@ -143,18 +140,6 @@ func NewWebContext(base *Base, render Render, session session.Store) *Context {
|
||||
// Contexter initializes a classic context for a request.
|
||||
func Contexter() func(next http.Handler) http.Handler {
|
||||
rnd := templates.HTMLRenderer()
|
||||
csrfOpts := CsrfOptions{
|
||||
Secret: hex.EncodeToString(setting.GetGeneralTokenSigningSecret()),
|
||||
Cookie: setting.CSRFCookieName,
|
||||
Secure: setting.SessionConfig.Secure,
|
||||
CookieHTTPOnly: setting.CSRFCookieHTTPOnly,
|
||||
CookieDomain: setting.SessionConfig.Domain,
|
||||
CookiePath: setting.SessionConfig.CookiePath,
|
||||
SameSite: setting.SessionConfig.SameSite,
|
||||
}
|
||||
if !setting.IsProd {
|
||||
CsrfTokenRegenerationInterval = 5 * time.Second // in dev, re-generate the tokens more aggressively for debug purpose
|
||||
}
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
base := NewBaseContext(resp, req)
|
||||
@@ -167,8 +152,6 @@ func Contexter() func(next http.Handler) http.Handler {
|
||||
ctx.PageData = map[string]any{}
|
||||
ctx.Data["PageData"] = ctx.PageData
|
||||
|
||||
ctx.Csrf = NewCSRFProtector(csrfOpts)
|
||||
|
||||
// get the last flash message from cookie
|
||||
lastFlashCookie, lastFlashMsg := middleware.GetSiteCookieFlashMessage(ctx, ctx.Req, CookieNameFlash)
|
||||
if vals, _ := url.ParseQuery(lastFlashCookie); len(vals) > 0 {
|
||||
@@ -184,7 +167,10 @@ func Contexter() func(next http.Handler) http.Handler {
|
||||
}
|
||||
})
|
||||
|
||||
// If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid.
|
||||
// FIXME: GLOBAL-PARSE-FORM: this ParseMultipartForm was used for parsing the csrf token from multipart/form-data
|
||||
// We have dropped the csrf token, so ideally this global ParseMultipartForm should be removed.
|
||||
// When removing this, we need to avoid regressions in the handler functions because Golang's http framework is quite fragile
|
||||
// and developers sometimes need to manually parse the form before accessing some values.
|
||||
if ctx.Req.Method == http.MethodPost && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") {
|
||||
if !ctx.ParseMultipartForm() {
|
||||
return
|
||||
|
||||
@@ -25,13 +25,11 @@ func removeSessionCookieHeader(w http.ResponseWriter) {
|
||||
}
|
||||
|
||||
// SetSiteCookie convenience function to set most cookies consistently
|
||||
// CSRF and a few others are the exception here
|
||||
func (ctx *Context) SetSiteCookie(name, value string, maxAge int) {
|
||||
middleware.SetSiteCookie(ctx.Resp, name, value, maxAge)
|
||||
}
|
||||
|
||||
// DeleteSiteCookie convenience function to delete most cookies consistently
|
||||
// CSRF and a few others are the exception here
|
||||
func (ctx *Context) DeleteSiteCookie(name string) {
|
||||
middleware.SetSiteCookie(ctx.Resp, name, "", -1)
|
||||
}
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
// Copyright 2013 Martini Authors
|
||||
// Copyright 2014 The Macaron Authors
|
||||
// Copyright 2021 The Gitea Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// a middleware that generates and validates CSRF tokens.
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
const (
|
||||
CsrfHeaderName = "X-Csrf-Token"
|
||||
CsrfFormName = "_csrf"
|
||||
)
|
||||
|
||||
// CSRFProtector represents a CSRF protector and is used to get the current token and validate the token.
|
||||
type CSRFProtector interface {
|
||||
// PrepareForSessionUser prepares the csrf protector for the current session user.
|
||||
PrepareForSessionUser(ctx *Context)
|
||||
// Validate validates the csrf token in http context.
|
||||
Validate(ctx *Context)
|
||||
// DeleteCookie deletes the csrf cookie
|
||||
DeleteCookie(ctx *Context)
|
||||
}
|
||||
|
||||
type csrfProtector struct {
|
||||
opt CsrfOptions
|
||||
// id must be unique per user.
|
||||
id string
|
||||
// token is the valid one which will be used by end user and passed via header, cookie, or hidden form value.
|
||||
token string
|
||||
}
|
||||
|
||||
// CsrfOptions maintains options to manage behavior of Generate.
|
||||
type CsrfOptions struct {
|
||||
// The global secret value used to generate Tokens.
|
||||
Secret string
|
||||
// Cookie value used to set and get token.
|
||||
Cookie string
|
||||
// Cookie domain.
|
||||
CookieDomain string
|
||||
// Cookie path.
|
||||
CookiePath string
|
||||
CookieHTTPOnly bool
|
||||
// SameSite set the cookie SameSite type
|
||||
SameSite http.SameSite
|
||||
// Set the Secure flag to true on the cookie.
|
||||
Secure bool
|
||||
// sessionKey is the key used for getting the unique ID per user.
|
||||
sessionKey string
|
||||
// oldSessionKey saves old value corresponding to sessionKey.
|
||||
oldSessionKey string
|
||||
}
|
||||
|
||||
func newCsrfCookie(opt *CsrfOptions, value string) *http.Cookie {
|
||||
return &http.Cookie{
|
||||
Name: opt.Cookie,
|
||||
Value: value,
|
||||
Path: opt.CookiePath,
|
||||
Domain: opt.CookieDomain,
|
||||
MaxAge: int(CsrfTokenTimeout.Seconds()),
|
||||
Secure: opt.Secure,
|
||||
HttpOnly: opt.CookieHTTPOnly,
|
||||
SameSite: opt.SameSite,
|
||||
}
|
||||
}
|
||||
|
||||
func NewCSRFProtector(opt CsrfOptions) CSRFProtector {
|
||||
if opt.Secret == "" {
|
||||
panic("CSRF secret is empty but it must be set") // it shouldn't happen because it is always set in code
|
||||
}
|
||||
opt.Cookie = util.IfZero(opt.Cookie, "_csrf")
|
||||
opt.CookiePath = util.IfZero(opt.CookiePath, "/")
|
||||
opt.sessionKey = "uid"
|
||||
opt.oldSessionKey = "_old_" + opt.sessionKey
|
||||
return &csrfProtector{opt: opt}
|
||||
}
|
||||
|
||||
func (c *csrfProtector) PrepareForSessionUser(ctx *Context) {
|
||||
c.id = "0"
|
||||
if uidAny := ctx.Session.Get(c.opt.sessionKey); uidAny != nil {
|
||||
switch uidVal := uidAny.(type) {
|
||||
case string:
|
||||
c.id = uidVal
|
||||
case int64:
|
||||
c.id = strconv.FormatInt(uidVal, 10)
|
||||
default:
|
||||
log.Error("invalid uid type in session: %T", uidAny)
|
||||
}
|
||||
}
|
||||
|
||||
oldUID := ctx.Session.Get(c.opt.oldSessionKey)
|
||||
uidChanged := oldUID == nil || oldUID.(string) != c.id
|
||||
cookieToken := ctx.GetSiteCookie(c.opt.Cookie)
|
||||
|
||||
needsNew := true
|
||||
if uidChanged {
|
||||
_ = ctx.Session.Set(c.opt.oldSessionKey, c.id)
|
||||
} else if cookieToken != "" {
|
||||
// If cookie token present, re-use existing unexpired token, else generate a new one.
|
||||
if issueTime, ok := ParseCsrfToken(cookieToken); ok {
|
||||
dur := time.Since(issueTime) // issueTime is not a monotonic-clock, the server time may change a lot to an early time.
|
||||
if dur >= -CsrfTokenRegenerationInterval && dur <= CsrfTokenRegenerationInterval {
|
||||
c.token = cookieToken
|
||||
needsNew = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if needsNew {
|
||||
c.token = GenerateCsrfToken(c.opt.Secret, c.id, "POST", time.Now())
|
||||
ctx.Resp.Header().Add("Set-Cookie", newCsrfCookie(&c.opt, c.token).String())
|
||||
}
|
||||
|
||||
ctx.Data["CsrfToken"] = c.token
|
||||
ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + template.HTMLEscapeString(c.token) + `">`)
|
||||
}
|
||||
|
||||
func (c *csrfProtector) validateToken(ctx *Context, token string) {
|
||||
if !ValidCsrfToken(token, c.opt.Secret, c.id, "POST", time.Now()) {
|
||||
c.DeleteCookie(ctx)
|
||||
// currently, there should be no access to the APIPath with CSRF token. because templates shouldn't use the `/api/` endpoints.
|
||||
// FIXME: distinguish what the response is for: HTML (web page) or JSON (fetch)
|
||||
http.Error(ctx.Resp, "Invalid CSRF token.", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate should be used as a per route middleware. It attempts to get a token from an "X-Csrf-Token"
|
||||
// HTTP header and then a "_csrf" form value. If one of these is found, the token will be validated.
|
||||
// If this validation fails, http.StatusBadRequest is sent.
|
||||
func (c *csrfProtector) Validate(ctx *Context) {
|
||||
if token := ctx.Req.Header.Get(CsrfHeaderName); token != "" {
|
||||
c.validateToken(ctx, token)
|
||||
return
|
||||
}
|
||||
if token := ctx.Req.FormValue(CsrfFormName); token != "" {
|
||||
c.validateToken(ctx, token)
|
||||
return
|
||||
}
|
||||
c.validateToken(ctx, "") // no csrf token, use an empty token to respond error
|
||||
}
|
||||
|
||||
func (c *csrfProtector) DeleteCookie(ctx *Context) {
|
||||
cookie := newCsrfCookie(&c.opt, "")
|
||||
cookie.MaxAge = -1
|
||||
ctx.Resp.Header().Add("Set-Cookie", cookie.String())
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
// Copyright 2012 Google Inc. All Rights Reserved.
|
||||
// Copyright 2014 The Macaron Authors
|
||||
// Copyright 2020 The Gitea Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CsrfTokenTimeout represents the duration that XSRF tokens are valid.
|
||||
// It is exported so clients may set cookie timeouts that match generated tokens.
|
||||
const CsrfTokenTimeout = 24 * time.Hour
|
||||
|
||||
// CsrfTokenRegenerationInterval is the interval between token generations, old tokens are still valid before CsrfTokenTimeout
|
||||
var CsrfTokenRegenerationInterval = 10 * time.Minute
|
||||
|
||||
var csrfTokenSep = []byte(":")
|
||||
|
||||
// GenerateCsrfToken returns a URL-safe secure XSRF token that expires in CsrfTokenTimeout hours.
|
||||
// key is a secret key for your application.
|
||||
// userID is a unique identifier for the user.
|
||||
// actionID is the action the user is taking (e.g. POSTing to a particular path).
|
||||
func GenerateCsrfToken(key, userID, actionID string, now time.Time) string {
|
||||
nowUnixNano := now.UnixNano()
|
||||
nowUnixNanoStr := strconv.FormatInt(nowUnixNano, 10)
|
||||
h := hmac.New(sha1.New, []byte(key))
|
||||
h.Write([]byte(strings.ReplaceAll(userID, ":", "_")))
|
||||
h.Write(csrfTokenSep)
|
||||
h.Write([]byte(strings.ReplaceAll(actionID, ":", "_")))
|
||||
h.Write(csrfTokenSep)
|
||||
h.Write([]byte(nowUnixNanoStr))
|
||||
tok := fmt.Sprintf("%s:%s", h.Sum(nil), nowUnixNanoStr)
|
||||
return base64.RawURLEncoding.EncodeToString([]byte(tok))
|
||||
}
|
||||
|
||||
func ParseCsrfToken(token string) (issueTime time.Time, ok bool) {
|
||||
data, err := base64.RawURLEncoding.DecodeString(token)
|
||||
if err != nil {
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
pos := bytes.LastIndex(data, csrfTokenSep)
|
||||
if pos == -1 {
|
||||
return time.Time{}, false
|
||||
}
|
||||
nanos, err := strconv.ParseInt(string(data[pos+1:]), 10, 64)
|
||||
if err != nil {
|
||||
return time.Time{}, false
|
||||
}
|
||||
return time.Unix(0, nanos), true
|
||||
}
|
||||
|
||||
// ValidCsrfToken returns true if token is a valid and unexpired token returned by Generate.
|
||||
func ValidCsrfToken(token, key, userID, actionID string, now time.Time) bool {
|
||||
issueTime, ok := ParseCsrfToken(token)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check that the token is not expired.
|
||||
if now.Sub(issueTime) >= CsrfTokenTimeout {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check that the token is not from the future.
|
||||
// Allow 1-minute grace period in case the token is being verified on a
|
||||
// machine whose clock is behind the machine that issued the token.
|
||||
if issueTime.After(now.Add(1 * time.Minute)) {
|
||||
return false
|
||||
}
|
||||
|
||||
expected := GenerateCsrfToken(key, userID, actionID, issueTime)
|
||||
|
||||
// Check that the token matches the expected value.
|
||||
// Use constant time comparison to avoid timing attacks.
|
||||
return subtle.ConstantTimeCompare([]byte(token), []byte(expected)) == 1
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
// Copyright 2012 Google Inc. All Rights Reserved.
|
||||
// Copyright 2014 The Macaron Authors
|
||||
// Copyright 2020 The Gitea Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
key = "quay"
|
||||
userID = "12345678"
|
||||
actionID = "POST /form"
|
||||
)
|
||||
|
||||
var (
|
||||
now = time.Now()
|
||||
oneMinuteFromNow = now.Add(1 * time.Minute)
|
||||
)
|
||||
|
||||
func Test_ValidToken(t *testing.T) {
|
||||
t.Run("Validate token", func(t *testing.T) {
|
||||
tok := GenerateCsrfToken(key, userID, actionID, now)
|
||||
assert.True(t, ValidCsrfToken(tok, key, userID, actionID, oneMinuteFromNow))
|
||||
assert.True(t, ValidCsrfToken(tok, key, userID, actionID, now.Add(CsrfTokenTimeout-1*time.Nanosecond)))
|
||||
assert.True(t, ValidCsrfToken(tok, key, userID, actionID, now.Add(-1*time.Minute)))
|
||||
})
|
||||
}
|
||||
|
||||
// Test_SeparatorReplacement tests that separators are being correctly substituted
|
||||
func Test_SeparatorReplacement(t *testing.T) {
|
||||
t.Run("Test two separator replacements", func(t *testing.T) {
|
||||
assert.NotEqual(t, GenerateCsrfToken("foo:bar", "baz", "wah", now),
|
||||
GenerateCsrfToken("foo", "bar:baz", "wah", now))
|
||||
})
|
||||
}
|
||||
|
||||
func Test_InvalidToken(t *testing.T) {
|
||||
t.Run("Test invalid tokens", func(t *testing.T) {
|
||||
invalidTokenTests := []struct {
|
||||
name, key, userID, actionID string
|
||||
t time.Time
|
||||
}{
|
||||
{"Bad key", "foobar", userID, actionID, oneMinuteFromNow},
|
||||
{"Bad userID", key, "foobar", actionID, oneMinuteFromNow},
|
||||
{"Bad actionID", key, userID, "foobar", oneMinuteFromNow},
|
||||
{"Expired", key, userID, actionID, now.Add(CsrfTokenTimeout)},
|
||||
{"More than 1 minute from the future", key, userID, actionID, now.Add(-1*time.Nanosecond - 1*time.Minute)},
|
||||
}
|
||||
|
||||
tok := GenerateCsrfToken(key, userID, actionID, now)
|
||||
for _, itt := range invalidTokenTests {
|
||||
assert.False(t, ValidCsrfToken(tok, itt.key, itt.userID, itt.actionID, itt.t))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test_ValidateBadData primarily tests that no unexpected panics are triggered during parsing
|
||||
func Test_ValidateBadData(t *testing.T) {
|
||||
t.Run("Validate bad data", func(t *testing.T) {
|
||||
badDataTests := []struct {
|
||||
name, tok string
|
||||
}{
|
||||
{"Invalid Base64", "ASDab24(@)$*=="},
|
||||
{"No delimiter", base64.URLEncoding.EncodeToString([]byte("foobar12345678"))},
|
||||
{"Invalid time", base64.URLEncoding.EncodeToString([]byte("foobar:foobar"))},
|
||||
}
|
||||
|
||||
for _, bdt := range badDataTests {
|
||||
assert.False(t, ValidCsrfToken(bdt.tok, key, userID, actionID, oneMinuteFromNow))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -34,6 +34,7 @@ type AuthenticationForm struct {
|
||||
AttributeMail string
|
||||
AttributeSSHPublicKey string
|
||||
AttributeAvatar string
|
||||
SSHKeysAreVerified bool
|
||||
AttributesInBind bool
|
||||
UsePagedSearch bool
|
||||
SearchPageSize int
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pull
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -14,24 +14,38 @@ import (
|
||||
|
||||
// CompareInfo represents needed information for comparing references.
|
||||
type CompareInfo struct {
|
||||
MergeBase string
|
||||
BaseCommitID string
|
||||
HeadCommitID string
|
||||
Commits []*git.Commit
|
||||
NumFiles int
|
||||
BaseRepo *repo_model.Repository
|
||||
BaseRef git.RefName
|
||||
BaseCommitID string
|
||||
HeadRepo *repo_model.Repository
|
||||
HeadGitRepo *git.Repository
|
||||
HeadRef git.RefName
|
||||
HeadCommitID string
|
||||
DirectComparison bool
|
||||
MergeBase string
|
||||
Commits []*git.Commit
|
||||
NumFiles int
|
||||
}
|
||||
|
||||
func (ci *CompareInfo) IsSameRepository() bool {
|
||||
return ci.BaseRepo.ID == ci.HeadRepo.ID
|
||||
}
|
||||
|
||||
func (ci *CompareInfo) IsSameRef() bool {
|
||||
return ci.IsSameRepository() && ci.BaseRef == ci.HeadRef
|
||||
}
|
||||
|
||||
// GetCompareInfo generates and returns compare information between base and head branches of repositories.
|
||||
func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Repository, headGitRepo *git.Repository, baseBranch, headBranch string, directComparison, fileOnly bool) (_ *CompareInfo, err error) {
|
||||
func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Repository, headGitRepo *git.Repository, baseRef, headRef git.RefName, directComparison, fileOnly bool) (_ *CompareInfo, err error) {
|
||||
compareInfo := new(CompareInfo)
|
||||
|
||||
compareInfo.HeadCommitID, err = gitrepo.GetFullCommitID(ctx, headRepo, headBranch)
|
||||
compareInfo.HeadCommitID, err = gitrepo.GetFullCommitID(ctx, headRepo, headRef.String())
|
||||
if err != nil {
|
||||
compareInfo.HeadCommitID = headBranch
|
||||
compareInfo.HeadCommitID = headRef.String()
|
||||
}
|
||||
compareInfo.BaseCommitID, err = gitrepo.GetFullCommitID(ctx, baseRepo, baseBranch)
|
||||
compareInfo.BaseCommitID, err = gitrepo.GetFullCommitID(ctx, baseRepo, baseRef.String())
|
||||
if err != nil {
|
||||
compareInfo.BaseCommitID = baseBranch
|
||||
compareInfo.BaseCommitID = baseRef.String()
|
||||
}
|
||||
|
||||
compareInfo.MergeBase, err = gitrepo.MergeBaseFromRemote(ctx, baseRepo, headRepo, compareInfo.BaseCommitID, compareInfo.HeadCommitID)
|
||||
@@ -45,7 +59,7 @@ func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Reposito
|
||||
|
||||
// We have a common base - therefore we know that ... should work
|
||||
if !fileOnly {
|
||||
compareInfo.Commits, err = headGitRepo.ShowPrettyFormatLogToList(ctx, startCommitID+separator+headBranch)
|
||||
compareInfo.Commits, err = headGitRepo.ShowPrettyFormatLogToList(ctx, startCommitID+separator+headRef.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ShowPrettyFormatLogToList: %w", err)
|
||||
}
|
||||
+10
-6
@@ -14,7 +14,6 @@ import (
|
||||
"maps"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -28,6 +27,7 @@ import (
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/auth/httpauth"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
lfs_module "code.gitea.io/gitea/modules/lfs"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -44,6 +44,7 @@ type requestContext struct {
|
||||
Repo string
|
||||
Authorization string
|
||||
Method string
|
||||
RepoGitURL string
|
||||
}
|
||||
|
||||
// Claims is a JWT Token Claims
|
||||
@@ -83,17 +84,17 @@ func GetLFSAuthTokenWithBearer(opts AuthTokenOptions) (string, error) {
|
||||
|
||||
// DownloadLink builds a URL to download the object.
|
||||
func (rc *requestContext) DownloadLink(p lfs_module.Pointer) string {
|
||||
return setting.AppURL + path.Join(url.PathEscape(rc.User), url.PathEscape(rc.Repo+".git"), "info/lfs/objects", url.PathEscape(p.Oid))
|
||||
return rc.RepoGitURL + "/info/lfs/objects/" + url.PathEscape(p.Oid)
|
||||
}
|
||||
|
||||
// UploadLink builds a URL to upload the object.
|
||||
func (rc *requestContext) UploadLink(p lfs_module.Pointer) string {
|
||||
return setting.AppURL + path.Join(url.PathEscape(rc.User), url.PathEscape(rc.Repo+".git"), "info/lfs/objects", url.PathEscape(p.Oid), strconv.FormatInt(p.Size, 10))
|
||||
return rc.RepoGitURL + "/info/lfs/objects/" + url.PathEscape(p.Oid) + "/" + strconv.FormatInt(p.Size, 10)
|
||||
}
|
||||
|
||||
// VerifyLink builds a URL for verifying the object.
|
||||
func (rc *requestContext) VerifyLink(p lfs_module.Pointer) string {
|
||||
return setting.AppURL + path.Join(url.PathEscape(rc.User), url.PathEscape(rc.Repo+".git"), "info/lfs/verify")
|
||||
return rc.RepoGitURL + "/info/lfs/verify"
|
||||
}
|
||||
|
||||
// CheckAcceptMediaType checks if the client accepts the LFS media type.
|
||||
@@ -421,11 +422,14 @@ func decodeJSON(req *http.Request, v any) error {
|
||||
}
|
||||
|
||||
func getRequestContext(ctx *context.Context) *requestContext {
|
||||
ownerName := ctx.PathParam("username")
|
||||
repoName := strings.TrimSuffix(ctx.PathParam("reponame"), ".git")
|
||||
return &requestContext{
|
||||
User: ctx.PathParam("username"),
|
||||
Repo: strings.TrimSuffix(ctx.PathParam("reponame"), ".git"),
|
||||
User: ownerName,
|
||||
Repo: repoName,
|
||||
Authorization: ctx.Req.Header.Get("Authorization"),
|
||||
Method: ctx.Req.Method,
|
||||
RepoGitURL: httplib.GuessCurrentAppURL(ctx) + url.PathEscape(ownerName) + "/" + url.PathEscape(repoName+".git"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
git_service "code.gitea.io/gitea/services/git"
|
||||
issue_service "code.gitea.io/gitea/services/issue"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
)
|
||||
@@ -1059,14 +1060,14 @@ func GetPullCommits(ctx context.Context, baseGitRepo *git.Repository, doer *user
|
||||
if pull.HasMerged {
|
||||
baseBranch = pull.MergeBase
|
||||
}
|
||||
prInfo, err := GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo, baseBranch, pull.GetGitHeadRefName(), true, false)
|
||||
compareInfo, err := git_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo, git.RefNameFromBranch(baseBranch), git.RefName(pull.GetGitHeadRefName()), true, false)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
commits := make([]CommitInfo, 0, len(prInfo.Commits))
|
||||
commits := make([]CommitInfo, 0, len(compareInfo.Commits))
|
||||
|
||||
for _, commit := range prInfo.Commits {
|
||||
for _, commit := range compareInfo.Commits {
|
||||
var committerOrAuthorName string
|
||||
var commitTime time.Time
|
||||
if commit.Author != nil {
|
||||
|
||||
@@ -74,9 +74,9 @@ func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Reposi
|
||||
}
|
||||
|
||||
if err := gitrepo.PushFromLocal(ctx, tmpPath, repo, git.PushOptions{
|
||||
LocalBranch: "HEAD",
|
||||
Branch: defaultBranch,
|
||||
Env: repo_module.InternalPushingEnvironment(u, repo),
|
||||
LocalRefName: "HEAD",
|
||||
Branch: defaultBranch,
|
||||
Env: repo_module.InternalPushingEnvironment(u, repo),
|
||||
}); err != nil {
|
||||
log.Error("Failed to push back to HEAD Error: %v", err)
|
||||
return fmt.Errorf("git push: %w", err)
|
||||
|
||||
Reference in new issue
Block a user