mirror of
https://github.com/go-gitea/gitea.git
synced 2025-10-22 08:04:37 +02:00
Refactor legacy code (#35708)
And by the way, remove the legacy TODO, split large functions into small ones, and add more tests
This commit is contained in:
parent
897e48dde3
commit
b2ee5be52e
27
cmd/serv.go
27
cmd/serv.go
@ -13,7 +13,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||||
@ -32,7 +31,6 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/services/lfs"
|
"code.gitea.io/gitea/services/lfs"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
|
||||||
"github.com/kballard/go-shellquote"
|
"github.com/kballard/go-shellquote"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
@ -133,27 +131,6 @@ func getAccessMode(verb, lfsVerb string) perm.AccessMode {
|
|||||||
return perm.AccessModeNone
|
return perm.AccessModeNone
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLFSAuthToken(ctx context.Context, lfsVerb string, results *private.ServCommandResults) (string, error) {
|
|
||||||
now := time.Now()
|
|
||||||
claims := lfs.Claims{
|
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
|
||||||
ExpiresAt: jwt.NewNumericDate(now.Add(setting.LFS.HTTPAuthExpiry)),
|
|
||||||
NotBefore: jwt.NewNumericDate(now),
|
|
||||||
},
|
|
||||||
RepoID: results.RepoID,
|
|
||||||
Op: lfsVerb,
|
|
||||||
UserID: results.UserID,
|
|
||||||
}
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
||||||
|
|
||||||
// Sign and get the complete encoded token as a string using the secret
|
|
||||||
tokenString, err := token.SignedString(setting.LFS.JWTSecretBytes)
|
|
||||||
if err != nil {
|
|
||||||
return "", fail(ctx, "Failed to sign JWT Token", "Failed to sign JWT token: %v", err)
|
|
||||||
}
|
|
||||||
return "Bearer " + tokenString, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func runServ(ctx context.Context, c *cli.Command) error {
|
func runServ(ctx context.Context, c *cli.Command) error {
|
||||||
// FIXME: This needs to internationalised
|
// FIXME: This needs to internationalised
|
||||||
setup(ctx, c.Bool("debug"))
|
setup(ctx, c.Bool("debug"))
|
||||||
@ -283,7 +260,7 @@ func runServ(ctx context.Context, c *cli.Command) error {
|
|||||||
|
|
||||||
// LFS SSH protocol
|
// LFS SSH protocol
|
||||||
if verb == git.CmdVerbLfsTransfer {
|
if verb == git.CmdVerbLfsTransfer {
|
||||||
token, err := getLFSAuthToken(ctx, lfsVerb, results)
|
token, err := lfs.GetLFSAuthTokenWithBearer(lfs.AuthTokenOptions{Op: lfsVerb, UserID: results.UserID, RepoID: results.RepoID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -294,7 +271,7 @@ func runServ(ctx context.Context, c *cli.Command) error {
|
|||||||
if verb == git.CmdVerbLfsAuthenticate {
|
if verb == git.CmdVerbLfsAuthenticate {
|
||||||
url := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, url.PathEscape(results.OwnerName), url.PathEscape(results.RepoName))
|
url := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, url.PathEscape(results.OwnerName), url.PathEscape(results.RepoName))
|
||||||
|
|
||||||
token, err := getLFSAuthToken(ctx, lfsVerb, results)
|
token, err := lfs.GetLFSAuthTokenWithBearer(lfs.AuthTokenOptions{Op: lfsVerb, UserID: results.UserID, RepoID: results.RepoID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -67,13 +67,6 @@ func (key *PublicKey) OmitEmail() string {
|
|||||||
return strings.Join(strings.Split(key.Content, " ")[:2], " ")
|
return strings.Join(strings.Split(key.Content, " ")[:2], " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizedString returns formatted public key string for authorized_keys file.
|
|
||||||
//
|
|
||||||
// TODO: Consider dropping this function
|
|
||||||
func (key *PublicKey) AuthorizedString() string {
|
|
||||||
return AuthorizedStringForKey(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func addKey(ctx context.Context, key *PublicKey) (err error) {
|
func addKey(ctx context.Context, key *PublicKey) (err error) {
|
||||||
if len(key.Fingerprint) == 0 {
|
if len(key.Fingerprint) == 0 {
|
||||||
key.Fingerprint, err = CalcFingerprint(key.Content)
|
key.Fingerprint, err = CalcFingerprint(key.Content)
|
||||||
|
@ -17,29 +17,13 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
// _____ __ .__ .__ .___
|
// AuthorizedStringCommentPrefix is a magic tag
|
||||||
// / _ \ __ ___/ |_| |__ ___________|__|_______ ____ __| _/
|
// some functions like RegeneratePublicKeys needs this tag to skip the keys generated by Gitea, while keep other keys
|
||||||
// / /_\ \| | \ __\ | \ / _ \_ __ \ \___ // __ \ / __ |
|
const AuthorizedStringCommentPrefix = `# gitea public key`
|
||||||
// / | \ | /| | | Y ( <_> ) | \/ |/ /\ ___// /_/ |
|
|
||||||
// \____|__ /____/ |__| |___| /\____/|__| |__/_____ \\___ >____ |
|
|
||||||
// \/ \/ \/ \/ \/
|
|
||||||
// ____ __.
|
|
||||||
// | |/ _|____ ___.__. ______
|
|
||||||
// | <_/ __ < | |/ ___/
|
|
||||||
// | | \ ___/\___ |\___ \
|
|
||||||
// |____|__ \___ > ____/____ >
|
|
||||||
// \/ \/\/ \/
|
|
||||||
//
|
|
||||||
// This file contains functions for creating authorized_keys files
|
|
||||||
//
|
|
||||||
// There is a dependence on the database within RegeneratePublicKeys however most of these functions probably belong in a module
|
|
||||||
|
|
||||||
const (
|
|
||||||
tplCommentPrefix = `# gitea public key`
|
|
||||||
tplPublicKey = tplCommentPrefix + "\n" + `command=%s,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict %s` + "\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
var sshOpLocker sync.Mutex
|
var sshOpLocker sync.Mutex
|
||||||
|
|
||||||
@ -50,17 +34,45 @@ func WithSSHOpLocker(f func() error) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizedStringForKey creates the authorized keys string appropriate for the provided key
|
// AuthorizedStringForKey creates the authorized keys string appropriate for the provided key
|
||||||
func AuthorizedStringForKey(key *PublicKey) string {
|
func AuthorizedStringForKey(key *PublicKey) (string, error) {
|
||||||
sb := &strings.Builder{}
|
sb := &strings.Builder{}
|
||||||
_ = setting.SSH.AuthorizedKeysCommandTemplateTemplate.Execute(sb, map[string]any{
|
_, err := writeAuthorizedStringForKey(key, sb)
|
||||||
|
return sb.String(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteAuthorizedStringForValidKey writes the authorized key for the provided key. If the key is invalid, it does nothing.
|
||||||
|
func WriteAuthorizedStringForValidKey(key *PublicKey, w io.Writer) error {
|
||||||
|
validKey, err := writeAuthorizedStringForKey(key, w)
|
||||||
|
if !validKey {
|
||||||
|
log.Debug("WriteAuthorizedStringForValidKey: key %s is not valid: %v", key, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeAuthorizedStringForKey(key *PublicKey, w io.Writer) (keyValid bool, err error) {
|
||||||
|
const tpl = AuthorizedStringCommentPrefix + "\n" + `command=%s,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict %s %s` + "\n"
|
||||||
|
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
// now the key is valid, the code below could only return template/IO related errors
|
||||||
|
sbCmd := &strings.Builder{}
|
||||||
|
err = setting.SSH.AuthorizedKeysCommandTemplateTemplate.Execute(sbCmd, map[string]any{
|
||||||
"AppPath": util.ShellEscape(setting.AppPath),
|
"AppPath": util.ShellEscape(setting.AppPath),
|
||||||
"AppWorkPath": util.ShellEscape(setting.AppWorkPath),
|
"AppWorkPath": util.ShellEscape(setting.AppWorkPath),
|
||||||
"CustomConf": util.ShellEscape(setting.CustomConf),
|
"CustomConf": util.ShellEscape(setting.CustomConf),
|
||||||
"CustomPath": util.ShellEscape(setting.CustomPath),
|
"CustomPath": util.ShellEscape(setting.CustomPath),
|
||||||
"Key": key,
|
"Key": key,
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
return fmt.Sprintf(tplPublicKey, util.ShellEscape(sb.String()), key.Content)
|
return true, err
|
||||||
|
}
|
||||||
|
sshCommandEscaped := util.ShellEscape(sbCmd.String())
|
||||||
|
sshKeyMarshalled := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pubKey)))
|
||||||
|
sshKeyComment := fmt.Sprintf("user-%d", key.OwnerID)
|
||||||
|
_, err = fmt.Fprintf(w, tpl, sshCommandEscaped, sshKeyMarshalled, sshKeyComment)
|
||||||
|
return true, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// appendAuthorizedKeysToFile appends new SSH keys' content to authorized_keys file.
|
// appendAuthorizedKeysToFile appends new SSH keys' content to authorized_keys file.
|
||||||
@ -112,7 +124,7 @@ func appendAuthorizedKeysToFile(keys ...*PublicKey) error {
|
|||||||
if key.Type == KeyTypePrincipal {
|
if key.Type == KeyTypePrincipal {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, err = f.WriteString(key.AuthorizedString()); err != nil {
|
if err = WriteAuthorizedStringForValidKey(key, f); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -120,10 +132,9 @@ func appendAuthorizedKeysToFile(keys ...*PublicKey) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RegeneratePublicKeys regenerates the authorized_keys file
|
// RegeneratePublicKeys regenerates the authorized_keys file
|
||||||
func RegeneratePublicKeys(ctx context.Context, t io.StringWriter) error {
|
func RegeneratePublicKeys(ctx context.Context, t io.Writer) error {
|
||||||
if err := db.GetEngine(ctx).Where("type != ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean any) (err error) {
|
if err := db.GetEngine(ctx).Where("type != ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean any) (err error) {
|
||||||
_, err = t.WriteString((bean.(*PublicKey)).AuthorizedString())
|
return WriteAuthorizedStringForValidKey(bean.(*PublicKey), t)
|
||||||
return err
|
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -144,11 +155,11 @@ func RegeneratePublicKeys(ctx context.Context, t io.StringWriter) error {
|
|||||||
scanner := bufio.NewScanner(f)
|
scanner := bufio.NewScanner(f)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
if strings.HasPrefix(line, tplCommentPrefix) {
|
if strings.HasPrefix(line, AuthorizedStringCommentPrefix) {
|
||||||
scanner.Scan()
|
scanner.Scan()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
_, err = t.WriteString(line + "\n")
|
_, err = io.WriteString(t, line+"\n")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -127,16 +127,9 @@ func DeleteUploads(ctx context.Context, uploads ...*Upload) (err error) {
|
|||||||
|
|
||||||
for _, upload := range uploads {
|
for _, upload := range uploads {
|
||||||
localPath := upload.LocalPath()
|
localPath := upload.LocalPath()
|
||||||
isFile, err := util.IsFile(localPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to check if %s is a file. Error: %v", localPath, err)
|
|
||||||
}
|
|
||||||
if !isFile {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := util.Remove(localPath); err != nil {
|
if err := util.Remove(localPath); err != nil {
|
||||||
return fmt.Errorf("remove upload: %w", err)
|
// just continue, don't fail the whole operation if a file is missing (removed by others)
|
||||||
|
log.Error("unable to remove upload file %s: %v", localPath, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,30 +47,16 @@ func GetHook(repoPath, name string) (*Hook, error) {
|
|||||||
name: name,
|
name: name,
|
||||||
path: filepath.Join(repoPath, filepath.Join("hooks", name+".d", name)),
|
path: filepath.Join(repoPath, filepath.Join("hooks", name+".d", name)),
|
||||||
}
|
}
|
||||||
isFile, err := util.IsFile(h.path)
|
if data, err := os.ReadFile(h.path); err == nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if isFile {
|
|
||||||
data, err := os.ReadFile(h.path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
h.IsActive = true
|
h.IsActive = true
|
||||||
h.Content = string(data)
|
h.Content = string(data)
|
||||||
return h, nil
|
return h, nil
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
samplePath := filepath.Join(repoPath, "hooks", name+".sample")
|
samplePath := filepath.Join(repoPath, "hooks", name+".sample")
|
||||||
isFile, err = util.IsFile(samplePath)
|
if data, err := os.ReadFile(samplePath); err == nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if isFile {
|
|
||||||
data, err := os.ReadFile(samplePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
h.Sample = string(data)
|
h.Sample = string(data)
|
||||||
}
|
}
|
||||||
return h, nil
|
return h, nil
|
||||||
|
@ -202,11 +202,11 @@ func NewConfigProviderFromFile(file string) (ConfigProvider, error) {
|
|||||||
loadedFromEmpty := true
|
loadedFromEmpty := true
|
||||||
|
|
||||||
if file != "" {
|
if file != "" {
|
||||||
isFile, err := util.IsFile(file)
|
isExist, err := util.IsExist(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to check if %q is a file. Error: %v", file, err)
|
return nil, fmt.Errorf("unable to check if %q exists: %v", file, err)
|
||||||
}
|
}
|
||||||
if isFile {
|
if isExist {
|
||||||
if err = cfg.Append(file); err != nil {
|
if err = cfg.Append(file); err != nil {
|
||||||
return nil, fmt.Errorf("failed to load config file %q: %v", file, err)
|
return nil, fmt.Errorf("failed to load config file %q: %v", file, err)
|
||||||
}
|
}
|
||||||
|
@ -115,15 +115,10 @@ func IsDir(dir string) (bool, error) {
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsFile returns true if given path is a file,
|
func IsRegularFile(filePath string) (bool, error) {
|
||||||
// or returns false when it's a directory or does not exist.
|
f, err := os.Lstat(filePath)
|
||||||
func IsFile(filePath string) (bool, error) {
|
|
||||||
f, err := os.Stat(filePath)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return !f.IsDir(), nil
|
return f.Mode().IsRegular(), nil
|
||||||
}
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return false, nil
|
|
||||||
}
|
}
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ func UpdatePublicKeyInRepo(ctx *context.PrivateContext) {
|
|||||||
ctx.PlainText(http.StatusOK, "success")
|
ctx.PlainText(http.StatusOK, "success")
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizedPublicKeyByContent searches content as prefix (leak e-mail part)
|
// AuthorizedPublicKeyByContent searches content as prefix (without comment part)
|
||||||
// and returns public key found.
|
// and returns public key found.
|
||||||
func AuthorizedPublicKeyByContent(ctx *context.PrivateContext) {
|
func AuthorizedPublicKeyByContent(ctx *context.PrivateContext) {
|
||||||
content := ctx.FormString("content")
|
content := ctx.FormString("content")
|
||||||
@ -57,5 +57,14 @@ func AuthorizedPublicKeyByContent(ctx *context.PrivateContext) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.PlainText(http.StatusOK, publicKey.AuthorizedString())
|
|
||||||
|
authorizedString, err := asymkey_model.AuthorizedStringForKey(publicKey)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||||
|
Err: err.Error(),
|
||||||
|
UserMsg: "invalid public key",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.PlainText(http.StatusOK, authorizedString)
|
||||||
}
|
}
|
||||||
|
@ -25,10 +25,7 @@ import (
|
|||||||
// There is a dependence on the database within RewriteAllPrincipalKeys & RegeneratePrincipalKeys
|
// There is a dependence on the database within RewriteAllPrincipalKeys & RegeneratePrincipalKeys
|
||||||
// The sshOpLocker is used from ssh_key_authorized_keys.go
|
// The sshOpLocker is used from ssh_key_authorized_keys.go
|
||||||
|
|
||||||
const (
|
const authorizedPrincipalsFile = "authorized_principals"
|
||||||
authorizedPrincipalsFile = "authorized_principals"
|
|
||||||
tplCommentPrefix = `# gitea public key`
|
|
||||||
)
|
|
||||||
|
|
||||||
// RewriteAllPrincipalKeys removes any authorized principal and rewrite all keys from database again.
|
// RewriteAllPrincipalKeys removes any authorized principal and rewrite all keys from database again.
|
||||||
// Note: db.GetEngine(ctx).Iterate does not get latest data after insert/delete, so we have to call this function
|
// Note: db.GetEngine(ctx).Iterate does not get latest data after insert/delete, so we have to call this function
|
||||||
@ -90,10 +87,9 @@ func rewriteAllPrincipalKeys(ctx context.Context) error {
|
|||||||
return util.Rename(tmpPath, fPath)
|
return util.Rename(tmpPath, fPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func regeneratePrincipalKeys(ctx context.Context, t io.StringWriter) error {
|
func regeneratePrincipalKeys(ctx context.Context, t io.Writer) error {
|
||||||
if err := db.GetEngine(ctx).Where("type = ?", asymkey_model.KeyTypePrincipal).Iterate(new(asymkey_model.PublicKey), func(idx int, bean any) (err error) {
|
if err := db.GetEngine(ctx).Where("type = ?", asymkey_model.KeyTypePrincipal).Iterate(new(asymkey_model.PublicKey), func(idx int, bean any) (err error) {
|
||||||
_, err = t.WriteString((bean.(*asymkey_model.PublicKey)).AuthorizedString())
|
return asymkey_model.WriteAuthorizedStringForValidKey(bean.(*asymkey_model.PublicKey), t)
|
||||||
return err
|
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -114,11 +110,11 @@ func regeneratePrincipalKeys(ctx context.Context, t io.StringWriter) error {
|
|||||||
scanner := bufio.NewScanner(f)
|
scanner := bufio.NewScanner(f)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
if strings.HasPrefix(line, tplCommentPrefix) {
|
if strings.HasPrefix(line, asymkey_model.AuthorizedStringCommentPrefix) {
|
||||||
scanner.Scan()
|
scanner.Scan()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
_, err = t.WriteString(line + "\n")
|
_, err = io.WriteString(t, line+"\n")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -20,8 +20,6 @@ import (
|
|||||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||||
)
|
)
|
||||||
|
|
||||||
const tplCommentPrefix = `# gitea public key`
|
|
||||||
|
|
||||||
func checkAuthorizedKeys(ctx context.Context, logger log.Logger, autofix bool) error {
|
func checkAuthorizedKeys(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||||
if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile {
|
if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile {
|
||||||
return nil
|
return nil
|
||||||
@ -47,7 +45,7 @@ func checkAuthorizedKeys(ctx context.Context, logger log.Logger, autofix bool) e
|
|||||||
scanner := bufio.NewScanner(f)
|
scanner := bufio.NewScanner(f)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
if strings.HasPrefix(line, tplCommentPrefix) {
|
if strings.HasPrefix(line, asymkey_model.AuthorizedStringCommentPrefix) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
linesInAuthorizedKeys.Add(line)
|
linesInAuthorizedKeys.Add(line)
|
||||||
@ -67,7 +65,7 @@ func checkAuthorizedKeys(ctx context.Context, logger log.Logger, autofix bool) e
|
|||||||
scanner = bufio.NewScanner(regenerated)
|
scanner = bufio.NewScanner(regenerated)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
if strings.HasPrefix(line, tplCommentPrefix) {
|
if strings.HasPrefix(line, asymkey_model.AuthorizedStringCommentPrefix) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if linesInAuthorizedKeys.Contains(line) {
|
if linesInAuthorizedKeys.Contains(line) {
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
actions_model "code.gitea.io/gitea/models/actions"
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
@ -54,6 +55,33 @@ type Claims struct {
|
|||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthTokenOptions struct {
|
||||||
|
Op string
|
||||||
|
UserID int64
|
||||||
|
RepoID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLFSAuthTokenWithBearer(opts AuthTokenOptions) (string, error) {
|
||||||
|
now := time.Now()
|
||||||
|
claims := Claims{
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(now.Add(setting.LFS.HTTPAuthExpiry)),
|
||||||
|
NotBefore: jwt.NewNumericDate(now),
|
||||||
|
},
|
||||||
|
RepoID: opts.RepoID,
|
||||||
|
Op: opts.Op,
|
||||||
|
UserID: opts.UserID,
|
||||||
|
}
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
|
||||||
|
// Sign and get the complete encoded token as a string using the secret
|
||||||
|
tokenString, err := token.SignedString(setting.LFS.JWTSecretBytes)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to sign LFS JWT token: %w", err)
|
||||||
|
}
|
||||||
|
return "Bearer " + tokenString, nil
|
||||||
|
}
|
||||||
|
|
||||||
// DownloadLink builds a URL to download the object.
|
// DownloadLink builds a URL to download the object.
|
||||||
func (rc *requestContext) DownloadLink(p lfs_module.Pointer) string {
|
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 setting.AppURL + path.Join(url.PathEscape(rc.User), url.PathEscape(rc.Repo+".git"), "info/lfs/objects", url.PathEscape(p.Oid))
|
||||||
@ -559,9 +587,6 @@ func authenticate(ctx *context.Context, repository *repo_model.Repository, autho
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleLFSToken(ctx stdCtx.Context, tokenSHA string, target *repo_model.Repository, mode perm_model.AccessMode) (*user_model.User, error) {
|
func handleLFSToken(ctx stdCtx.Context, tokenSHA string, target *repo_model.Repository, mode perm_model.AccessMode) (*user_model.User, error) {
|
||||||
if !strings.Contains(tokenSHA, ".") {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
token, err := jwt.ParseWithClaims(tokenSHA, &Claims{}, func(t *jwt.Token) (any, error) {
|
token, err := jwt.ParseWithClaims(tokenSHA, &Claims{}, func(t *jwt.Token) (any, error) {
|
||||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||||
@ -569,7 +594,7 @@ func handleLFSToken(ctx stdCtx.Context, tokenSHA string, target *repo_model.Repo
|
|||||||
return setting.LFS.JWTSecretBytes, nil
|
return setting.LFS.JWTSecretBytes, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil
|
return nil, errors.New("invalid token")
|
||||||
}
|
}
|
||||||
|
|
||||||
claims, claimsOk := token.Claims.(*Claims)
|
claims, claimsOk := token.Claims.(*Claims)
|
||||||
|
51
services/lfs/server_test.go
Normal file
51
services/lfs/server_test.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package lfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
perm_model "code.gitea.io/gitea/models/perm"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
"code.gitea.io/gitea/services/contexttest"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
unittest.MainTest(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthenticate(t *testing.T) {
|
||||||
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
|
|
||||||
|
token2, _ := GetLFSAuthTokenWithBearer(AuthTokenOptions{Op: "download", UserID: 2, RepoID: 1})
|
||||||
|
_, token2, _ = strings.Cut(token2, " ")
|
||||||
|
ctx, _ := contexttest.MockContext(t, "/")
|
||||||
|
|
||||||
|
t.Run("handleLFSToken", func(t *testing.T) {
|
||||||
|
u, err := handleLFSToken(ctx, "", repo1, perm_model.AccessModeRead)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, u)
|
||||||
|
|
||||||
|
u, err = handleLFSToken(ctx, "invalid", repo1, perm_model.AccessModeRead)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, u)
|
||||||
|
|
||||||
|
u, err = handleLFSToken(ctx, token2, repo1, perm_model.AccessModeRead)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.EqualValues(t, 2, u.ID)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("authenticate", func(t *testing.T) {
|
||||||
|
const prefixBearer = "Bearer "
|
||||||
|
assert.False(t, authenticate(ctx, repo1, "", true, false))
|
||||||
|
assert.False(t, authenticate(ctx, repo1, prefixBearer+"invalid", true, false))
|
||||||
|
assert.True(t, authenticate(ctx, repo1, prefixBearer+token2, true, false))
|
||||||
|
})
|
||||||
|
}
|
@ -13,6 +13,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
@ -40,29 +41,41 @@ type expansion struct {
|
|||||||
Transformers []transformer
|
Transformers []transformer
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultTransformers = []transformer{
|
var globalVars = sync.OnceValue(func() (ret struct {
|
||||||
{Name: "SNAKE", Transform: xstrings.ToSnakeCase},
|
defaultTransformers []transformer
|
||||||
{Name: "KEBAB", Transform: xstrings.ToKebabCase},
|
fileNameSanitizeRegexp *regexp.Regexp
|
||||||
{Name: "CAMEL", Transform: xstrings.ToCamelCase},
|
},
|
||||||
{Name: "PASCAL", Transform: xstrings.ToPascalCase},
|
) {
|
||||||
{Name: "LOWER", Transform: strings.ToLower},
|
ret.defaultTransformers = []transformer{
|
||||||
{Name: "UPPER", Transform: strings.ToUpper},
|
{Name: "SNAKE", Transform: xstrings.ToSnakeCase},
|
||||||
{Name: "TITLE", Transform: util.ToTitleCase},
|
{Name: "KEBAB", Transform: xstrings.ToKebabCase},
|
||||||
}
|
{Name: "CAMEL", Transform: xstrings.ToCamelCase},
|
||||||
|
{Name: "PASCAL", Transform: xstrings.ToPascalCase},
|
||||||
|
{Name: "LOWER", Transform: strings.ToLower},
|
||||||
|
{Name: "UPPER", Transform: strings.ToUpper},
|
||||||
|
{Name: "TITLE", Transform: util.ToTitleCase},
|
||||||
|
}
|
||||||
|
|
||||||
func generateExpansion(ctx context.Context, src string, templateRepo, generateRepo *repo_model.Repository, sanitizeFileName bool) string {
|
// invalid filename contents, based on https://github.com/sindresorhus/filename-reserved-regex
|
||||||
|
// "COM10" needs to be opened with UNC "\\.\COM10" on Windows, so itself is valid
|
||||||
|
ret.fileNameSanitizeRegexp = regexp.MustCompile(`(?i)[<>:"/\\|?*\x{0000}-\x{001F}]|^(con|prn|aux|nul|com\d|lpt\d)$`)
|
||||||
|
return ret
|
||||||
|
})
|
||||||
|
|
||||||
|
func generateExpansion(ctx context.Context, src string, templateRepo, generateRepo *repo_model.Repository) string {
|
||||||
|
transformers := globalVars().defaultTransformers
|
||||||
year, month, day := time.Now().Date()
|
year, month, day := time.Now().Date()
|
||||||
expansions := []expansion{
|
expansions := []expansion{
|
||||||
{Name: "YEAR", Value: strconv.Itoa(year), Transformers: nil},
|
{Name: "YEAR", Value: strconv.Itoa(year), Transformers: nil},
|
||||||
{Name: "MONTH", Value: fmt.Sprintf("%02d", int(month)), Transformers: nil},
|
{Name: "MONTH", Value: fmt.Sprintf("%02d", int(month)), Transformers: nil},
|
||||||
{Name: "MONTH_ENGLISH", Value: month.String(), Transformers: defaultTransformers},
|
{Name: "MONTH_ENGLISH", Value: month.String(), Transformers: transformers},
|
||||||
{Name: "DAY", Value: fmt.Sprintf("%02d", day), Transformers: nil},
|
{Name: "DAY", Value: fmt.Sprintf("%02d", day), Transformers: nil},
|
||||||
{Name: "REPO_NAME", Value: generateRepo.Name, Transformers: defaultTransformers},
|
{Name: "REPO_NAME", Value: generateRepo.Name, Transformers: transformers},
|
||||||
{Name: "TEMPLATE_NAME", Value: templateRepo.Name, Transformers: defaultTransformers},
|
{Name: "TEMPLATE_NAME", Value: templateRepo.Name, Transformers: transformers},
|
||||||
{Name: "REPO_DESCRIPTION", Value: generateRepo.Description, Transformers: nil},
|
{Name: "REPO_DESCRIPTION", Value: generateRepo.Description, Transformers: nil},
|
||||||
{Name: "TEMPLATE_DESCRIPTION", Value: templateRepo.Description, Transformers: nil},
|
{Name: "TEMPLATE_DESCRIPTION", Value: templateRepo.Description, Transformers: nil},
|
||||||
{Name: "REPO_OWNER", Value: generateRepo.OwnerName, Transformers: defaultTransformers},
|
{Name: "REPO_OWNER", Value: generateRepo.OwnerName, Transformers: transformers},
|
||||||
{Name: "TEMPLATE_OWNER", Value: templateRepo.OwnerName, Transformers: defaultTransformers},
|
{Name: "TEMPLATE_OWNER", Value: templateRepo.OwnerName, Transformers: transformers},
|
||||||
{Name: "REPO_LINK", Value: generateRepo.Link(), Transformers: nil},
|
{Name: "REPO_LINK", Value: generateRepo.Link(), Transformers: nil},
|
||||||
{Name: "TEMPLATE_LINK", Value: templateRepo.Link(), Transformers: nil},
|
{Name: "TEMPLATE_LINK", Value: templateRepo.Link(), Transformers: nil},
|
||||||
{Name: "REPO_HTTPS_URL", Value: generateRepo.CloneLinkGeneral(ctx).HTTPS, Transformers: nil},
|
{Name: "REPO_HTTPS_URL", Value: generateRepo.CloneLinkGeneral(ctx).HTTPS, Transformers: nil},
|
||||||
@ -80,32 +93,23 @@ func generateExpansion(ctx context.Context, src string, templateRepo, generateRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
return os.Expand(src, func(key string) string {
|
return os.Expand(src, func(key string) string {
|
||||||
if expansion, ok := expansionMap[key]; ok {
|
if val, ok := expansionMap[key]; ok {
|
||||||
if sanitizeFileName {
|
return val
|
||||||
return fileNameSanitize(expansion)
|
|
||||||
}
|
|
||||||
return expansion
|
|
||||||
}
|
}
|
||||||
return key
|
return key
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GiteaTemplate holds information about a .gitea/template file
|
// giteaTemplateFileMatcher holds information about a .gitea/template file
|
||||||
type GiteaTemplate struct {
|
type giteaTemplateFileMatcher struct {
|
||||||
Path string
|
LocalFullPath string
|
||||||
Content []byte
|
globs []glob.Glob
|
||||||
|
|
||||||
globs []glob.Glob
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Globs parses the .gitea/template globs or returns them if they were already parsed
|
func newGiteaTemplateFileMatcher(fullPath string, content []byte) *giteaTemplateFileMatcher {
|
||||||
func (gt *GiteaTemplate) Globs() []glob.Glob {
|
gt := &giteaTemplateFileMatcher{LocalFullPath: fullPath}
|
||||||
if gt.globs != nil {
|
|
||||||
return gt.globs
|
|
||||||
}
|
|
||||||
|
|
||||||
gt.globs = make([]glob.Glob, 0)
|
gt.globs = make([]glob.Glob, 0)
|
||||||
scanner := bufio.NewScanner(bytes.NewReader(gt.Content))
|
scanner := bufio.NewScanner(bytes.NewReader(content))
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := strings.TrimSpace(scanner.Text())
|
line := strings.TrimSpace(scanner.Text())
|
||||||
if line == "" || strings.HasPrefix(line, "#") {
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
@ -113,73 +117,91 @@ func (gt *GiteaTemplate) Globs() []glob.Glob {
|
|||||||
}
|
}
|
||||||
g, err := glob.Compile(line, '/')
|
g, err := glob.Compile(line, '/')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Info("Invalid glob expression '%s' (skipped): %v", line, err)
|
log.Debug("Invalid glob expression '%s' (skipped): %v", line, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
gt.globs = append(gt.globs, g)
|
gt.globs = append(gt.globs, g)
|
||||||
}
|
}
|
||||||
return gt.globs
|
return gt
|
||||||
}
|
}
|
||||||
|
|
||||||
func readGiteaTemplateFile(tmpDir string) (*GiteaTemplate, error) {
|
func (gt *giteaTemplateFileMatcher) HasRules() bool {
|
||||||
gtPath := filepath.Join(tmpDir, ".gitea", "template")
|
return len(gt.globs) != 0
|
||||||
if _, err := os.Stat(gtPath); os.IsNotExist(err) {
|
}
|
||||||
|
|
||||||
|
func (gt *giteaTemplateFileMatcher) Match(s string) bool {
|
||||||
|
for _, g := range gt.globs {
|
||||||
|
if g.Match(s) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func readGiteaTemplateFile(tmpDir string) (*giteaTemplateFileMatcher, error) {
|
||||||
|
localPath := filepath.Join(tmpDir, ".gitea", "template")
|
||||||
|
if _, err := os.Stat(localPath); os.IsNotExist(err) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
content, err := os.ReadFile(gtPath)
|
content, err := os.ReadFile(localPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &GiteaTemplate{Path: gtPath, Content: content}, nil
|
return newGiteaTemplateFileMatcher(localPath, content), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func processGiteaTemplateFile(ctx context.Context, tmpDir string, templateRepo, generateRepo *repo_model.Repository, giteaTemplateFile *GiteaTemplate) error {
|
func substGiteaTemplateFile(ctx context.Context, tmpDir, tmpDirSubPath string, templateRepo, generateRepo *repo_model.Repository) error {
|
||||||
if err := util.Remove(giteaTemplateFile.Path); err != nil {
|
tmpFullPath := filepath.Join(tmpDir, tmpDirSubPath)
|
||||||
return fmt.Errorf("remove .giteatemplate: %w", err)
|
if ok, err := util.IsRegularFile(tmpFullPath); !ok {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
if len(giteaTemplateFile.Globs()) == 0 {
|
|
||||||
|
content, err := os.ReadFile(tmpFullPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := util.Remove(tmpFullPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
generatedContent := generateExpansion(ctx, string(content), templateRepo, generateRepo)
|
||||||
|
substSubPath := filepath.Clean(filePathSanitize(generateExpansion(ctx, tmpDirSubPath, templateRepo, generateRepo)))
|
||||||
|
newLocalPath := filepath.Join(tmpDir, substSubPath)
|
||||||
|
regular, err := util.IsRegularFile(newLocalPath)
|
||||||
|
if canWrite := regular || os.IsNotExist(err); !canWrite {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(newLocalPath), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(newLocalPath, []byte(generatedContent), 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func processGiteaTemplateFile(ctx context.Context, tmpDir string, templateRepo, generateRepo *repo_model.Repository, fileMatcher *giteaTemplateFileMatcher) error {
|
||||||
|
if err := util.Remove(fileMatcher.LocalFullPath); err != nil {
|
||||||
|
return fmt.Errorf("unable to remove .gitea/template: %w", err)
|
||||||
|
}
|
||||||
|
if !fileMatcher.HasRules() {
|
||||||
return nil // Avoid walking tree if there are no globs
|
return nil // Avoid walking tree if there are no globs
|
||||||
}
|
}
|
||||||
tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/"
|
|
||||||
return filepath.WalkDir(tmpDirSlash, func(path string, d os.DirEntry, walkErr error) error {
|
return filepath.WalkDir(tmpDir, func(fullPath string, d os.DirEntry, walkErr error) error {
|
||||||
if walkErr != nil {
|
if walkErr != nil {
|
||||||
return walkErr
|
return walkErr
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.IsDir() {
|
if d.IsDir() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
tmpDirSubPath, err := filepath.Rel(tmpDir, fullPath)
|
||||||
base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash)
|
if err != nil {
|
||||||
for _, g := range giteaTemplateFile.Globs() {
|
return err
|
||||||
if g.Match(base) {
|
}
|
||||||
content, err := os.ReadFile(path)
|
if fileMatcher.Match(filepath.ToSlash(tmpDirSubPath)) {
|
||||||
if err != nil {
|
return substGiteaTemplateFile(ctx, tmpDir, tmpDirSubPath, templateRepo, generateRepo)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
generatedContent := []byte(generateExpansion(ctx, string(content), templateRepo, generateRepo, false))
|
|
||||||
if err := os.WriteFile(path, generatedContent, 0o644); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
substPath := filepath.FromSlash(filepath.Join(tmpDirSlash, generateExpansion(ctx, base, templateRepo, generateRepo, true)))
|
|
||||||
|
|
||||||
// Create parent subdirectories if needed or continue silently if it exists
|
|
||||||
if err = os.MkdirAll(filepath.Dir(substPath), 0o755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Substitute filename variables
|
|
||||||
if err = os.Rename(path, substPath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}) // end: WalkDir
|
}) // end: WalkDir
|
||||||
@ -219,13 +241,13 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Variable expansion
|
// Variable expansion
|
||||||
giteaTemplateFile, err := readGiteaTemplateFile(tmpDir)
|
fileMatcher, err := readGiteaTemplateFile(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("readGiteaTemplateFile: %w", err)
|
return fmt.Errorf("readGiteaTemplateFile: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if giteaTemplateFile != nil {
|
if fileMatcher != nil {
|
||||||
err = processGiteaTemplateFile(ctx, tmpDir, templateRepo, generateRepo, giteaTemplateFile)
|
err = processGiteaTemplateFile(ctx, tmpDir, templateRepo, generateRepo, fileMatcher)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -317,12 +339,17 @@ func (gro GenerateRepoOptions) IsValid() bool {
|
|||||||
gro.IssueLabels || gro.ProtectedBranch // or other items as they are added
|
gro.IssueLabels || gro.ProtectedBranch // or other items as they are added
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileNameSanitizeRegexp = regexp.MustCompile(`(?i)\.\.|[<>:\"/\\|?*\x{0000}-\x{001F}]|^(con|prn|aux|nul|com\d|lpt\d)$`)
|
func filePathSanitize(s string) string {
|
||||||
|
fields := strings.Split(filepath.ToSlash(s), "/")
|
||||||
// Sanitize user input to valid OS filenames
|
for i, field := range fields {
|
||||||
//
|
field = strings.TrimSpace(strings.TrimSpace(globalVars().fileNameSanitizeRegexp.ReplaceAllString(field, "_")))
|
||||||
// Based on https://github.com/sindresorhus/filename-reserved-regex
|
if strings.HasPrefix(field, "..") {
|
||||||
// Adds ".." to prevent directory traversal
|
field = "__" + field[2:]
|
||||||
func fileNameSanitize(s string) string {
|
}
|
||||||
return strings.TrimSpace(fileNameSanitizeRegexp.ReplaceAllString(s, "_"))
|
if strings.EqualFold(field, ".git") {
|
||||||
|
field = "_" + field[1:]
|
||||||
|
}
|
||||||
|
fields[i] = field
|
||||||
|
}
|
||||||
|
return filepath.FromSlash(strings.Join(fields, "/"))
|
||||||
}
|
}
|
||||||
|
@ -4,13 +4,18 @@
|
|||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
var giteaTemplate = []byte(`
|
func TestGiteaTemplate(t *testing.T) {
|
||||||
|
giteaTemplate := []byte(`
|
||||||
# Header
|
# Header
|
||||||
|
|
||||||
# All .go files
|
# All .go files
|
||||||
@ -23,48 +28,153 @@ text/*.txt
|
|||||||
**/modules/*
|
**/modules/*
|
||||||
`)
|
`)
|
||||||
|
|
||||||
func TestGiteaTemplate(t *testing.T) {
|
gt := newGiteaTemplateFileMatcher("", giteaTemplate)
|
||||||
gt := GiteaTemplate{Content: giteaTemplate}
|
assert.Len(t, gt.globs, 3)
|
||||||
assert.Len(t, gt.Globs(), 3)
|
|
||||||
|
|
||||||
tt := []struct {
|
tt := []struct {
|
||||||
Path string
|
Path string
|
||||||
Match bool
|
Match bool
|
||||||
}{
|
}{
|
||||||
{Path: "main.go", Match: true},
|
{Path: "main.go", Match: true},
|
||||||
{Path: "a/b/c/d/e.go", Match: true},
|
{Path: "sub/sub/foo.go", Match: true},
|
||||||
{Path: "main.txt", Match: false},
|
|
||||||
{Path: "a/b.txt", Match: false},
|
{Path: "a.txt", Match: false},
|
||||||
{Path: "text/a.txt", Match: true},
|
{Path: "text/a.txt", Match: true},
|
||||||
{Path: "text/b.txt", Match: true},
|
{Path: "sub/text/a.txt", Match: false},
|
||||||
{Path: "text/c.json", Match: false},
|
{Path: "text/a.json", Match: false},
|
||||||
|
|
||||||
{Path: "a/b/c/modules/README.md", Match: true},
|
{Path: "a/b/c/modules/README.md", Match: true},
|
||||||
{Path: "a/b/c/modules/d/README.md", Match: false},
|
{Path: "a/b/c/modules/d/README.md", Match: false},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tt {
|
for _, tc := range tt {
|
||||||
t.Run(tc.Path, func(t *testing.T) {
|
assert.Equal(t, tc.Match, gt.Match(tc.Path), "path: %s", tc.Path)
|
||||||
match := false
|
|
||||||
for _, g := range gt.Globs() {
|
|
||||||
if g.Match(tc.Path) {
|
|
||||||
match = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.Equal(t, tc.Match, match)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFileNameSanitize(t *testing.T) {
|
func TestFilePathSanitize(t *testing.T) {
|
||||||
assert.Equal(t, "test_CON", fileNameSanitize("test_CON"))
|
assert.Equal(t, "test_CON", filePathSanitize("test_CON"))
|
||||||
assert.Equal(t, "test CON", fileNameSanitize("test CON "))
|
assert.Equal(t, "test CON", filePathSanitize("test CON "))
|
||||||
assert.Equal(t, "__traverse__", fileNameSanitize("../traverse/.."))
|
assert.Equal(t, "__/traverse/__", filePathSanitize(".. /traverse/ .."))
|
||||||
assert.Equal(t, "http___localhost_3003_user_test.git", fileNameSanitize("http://localhost:3003/user/test.git"))
|
assert.Equal(t, "./__/a/_git/b_", filePathSanitize("./../a/.git/ b: "))
|
||||||
assert.Equal(t, "_", fileNameSanitize("CON"))
|
assert.Equal(t, "_", filePathSanitize("CoN"))
|
||||||
assert.Equal(t, "_", fileNameSanitize("con"))
|
assert.Equal(t, "_", filePathSanitize("LpT1"))
|
||||||
assert.Equal(t, "_", fileNameSanitize("\u0000"))
|
assert.Equal(t, "_", filePathSanitize("CoM1"))
|
||||||
assert.Equal(t, "目标", fileNameSanitize("目标"))
|
assert.Equal(t, "_", filePathSanitize("\u0000"))
|
||||||
|
assert.Equal(t, "目标", filePathSanitize("目标"))
|
||||||
|
// unlike filepath.Clean, it only sanitizes, doesn't change the separator layout
|
||||||
|
assert.Equal(t, "", filePathSanitize("")) //nolint:testifylint // for easy reading
|
||||||
|
assert.Equal(t, ".", filePathSanitize("."))
|
||||||
|
assert.Equal(t, "/", filePathSanitize("/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessGiteaTemplateFile(t *testing.T) {
|
||||||
|
tmpDir := filepath.Join(t.TempDir(), "gitea-template-test")
|
||||||
|
|
||||||
|
assertFileContent := func(path, expected string) {
|
||||||
|
data, err := os.ReadFile(filepath.Join(tmpDir, path))
|
||||||
|
if expected == "" {
|
||||||
|
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, expected, string(data), "file content mismatch for %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertSymLink := func(path, expected string) {
|
||||||
|
link, err := os.Readlink(filepath.Join(tmpDir, path))
|
||||||
|
if expected == "" {
|
||||||
|
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, expected, link, "symlink target mismatch for %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, os.MkdirAll(tmpDir+"/.gitea", 0o755))
|
||||||
|
require.NoError(t, os.WriteFile(tmpDir+"/.gitea/template", []byte("*\ninclude/**"), 0o644))
|
||||||
|
require.NoError(t, os.MkdirAll(tmpDir+"/sub", 0o755))
|
||||||
|
require.NoError(t, os.MkdirAll(tmpDir+"/include/foo/bar", 0o755))
|
||||||
|
|
||||||
|
require.NoError(t, os.WriteFile(tmpDir+"/sub/link-target", []byte("link target content from ${TEMPLATE_NAME}"), 0o644))
|
||||||
|
require.NoError(t, os.WriteFile(tmpDir+"/include/foo/bar/test.txt", []byte("include subdir ${TEMPLATE_NAME}"), 0o644))
|
||||||
|
|
||||||
|
// case-1
|
||||||
|
{
|
||||||
|
require.NoError(t, os.WriteFile(tmpDir+"/normal", []byte("normal content"), 0o644))
|
||||||
|
require.NoError(t, os.WriteFile(tmpDir+"/template", []byte("template from ${TEMPLATE_NAME}"), 0o644))
|
||||||
|
}
|
||||||
|
|
||||||
|
// case-2
|
||||||
|
{
|
||||||
|
require.NoError(t, os.Symlink(tmpDir+"/sub/link-target", tmpDir+"/link"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// case-3
|
||||||
|
{
|
||||||
|
require.NoError(t, os.WriteFile(tmpDir+"/subst-${REPO_NAME}", []byte("dummy subst repo name"), 0o644))
|
||||||
|
}
|
||||||
|
|
||||||
|
// case-4
|
||||||
|
assertSubstTemplateName := func(normalContent, toLinkContent, fromLinkContent string) {
|
||||||
|
assertFileContent("subst-${TEMPLATE_NAME}-normal", normalContent)
|
||||||
|
assertFileContent("subst-${TEMPLATE_NAME}-to-link", toLinkContent)
|
||||||
|
assertFileContent("subst-${TEMPLATE_NAME}-from-link", fromLinkContent)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// will succeed
|
||||||
|
require.NoError(t, os.WriteFile(tmpDir+"/subst-${TEMPLATE_NAME}-normal", []byte("dummy subst template name normal"), 0o644))
|
||||||
|
// will skil if the path subst result is a link
|
||||||
|
require.NoError(t, os.WriteFile(tmpDir+"/subst-${TEMPLATE_NAME}-to-link", []byte("dummy subst template name to link"), 0o644))
|
||||||
|
require.NoError(t, os.Symlink(tmpDir+"/sub/link-target", tmpDir+"/subst-TemplateRepoName-to-link"))
|
||||||
|
// will be skipped since the source is a symlink
|
||||||
|
require.NoError(t, os.Symlink(tmpDir+"/sub/link-target", tmpDir+"/subst-${TEMPLATE_NAME}-from-link"))
|
||||||
|
// pre-check
|
||||||
|
assertSubstTemplateName("dummy subst template name normal", "dummy subst template name to link", "link target content from ${TEMPLATE_NAME}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// process the template files
|
||||||
|
{
|
||||||
|
templateRepo := &repo_model.Repository{Name: "TemplateRepoName"}
|
||||||
|
generatedRepo := &repo_model.Repository{Name: "/../.gIt/name"}
|
||||||
|
fileMatcher, _ := readGiteaTemplateFile(tmpDir)
|
||||||
|
err := processGiteaTemplateFile(t.Context(), tmpDir, templateRepo, generatedRepo, fileMatcher)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assertFileContent("include/foo/bar/test.txt", "include subdir TemplateRepoName")
|
||||||
|
}
|
||||||
|
|
||||||
|
// the lin target should never be modified, and since it is in a subdirectory, it is not affected by the template either
|
||||||
|
assertFileContent("sub/link-target", "link target content from ${TEMPLATE_NAME}")
|
||||||
|
|
||||||
|
// case-1
|
||||||
|
{
|
||||||
|
assertFileContent("no-such", "")
|
||||||
|
assertFileContent("normal", "normal content")
|
||||||
|
assertFileContent("template", "template from TemplateRepoName")
|
||||||
|
}
|
||||||
|
|
||||||
|
// case-2
|
||||||
|
{
|
||||||
|
// symlink with templates should be preserved (not read or write)
|
||||||
|
assertSymLink("link", tmpDir+"/sub/link-target")
|
||||||
|
}
|
||||||
|
|
||||||
|
// case-3
|
||||||
|
{
|
||||||
|
assertFileContent("subst-${REPO_NAME}", "")
|
||||||
|
assertFileContent("subst-/__/_gIt/name", "dummy subst repo name")
|
||||||
|
}
|
||||||
|
|
||||||
|
// case-4
|
||||||
|
{
|
||||||
|
// the paths with templates should have been removed, subst to a regular file, succeed, the link is preserved
|
||||||
|
assertSubstTemplateName("", "", "link target content from ${TEMPLATE_NAME}")
|
||||||
|
assertFileContent("subst-TemplateRepoName-normal", "dummy subst template name normal")
|
||||||
|
// subst to a link, skip, and the target is unchanged
|
||||||
|
assertSymLink("subst-TemplateRepoName-to-link", tmpDir+"/sub/link-target")
|
||||||
|
// subst from a link, skip, and the target is unchanged
|
||||||
|
assertSymLink("subst-${TEMPLATE_NAME}-from-link", tmpDir+"/sub/link-target")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTransformers(t *testing.T) {
|
func TestTransformers(t *testing.T) {
|
||||||
@ -82,9 +192,9 @@ func TestTransformers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input := "Abc_Def-XYZ"
|
input := "Abc_Def-XYZ"
|
||||||
assert.Len(t, defaultTransformers, len(cases))
|
assert.Len(t, globalVars().defaultTransformers, len(cases))
|
||||||
for i, c := range cases {
|
for i, c := range cases {
|
||||||
tf := defaultTransformers[i]
|
tf := globalVars().defaultTransformers[i]
|
||||||
require.Equal(t, c.name, tf.Name)
|
require.Equal(t, c.name, tf.Name)
|
||||||
assert.Equal(t, c.expected, tf.Transform(input), "case %s", c.name)
|
assert.Equal(t, c.expected, tf.Transform(input), "case %s", c.name)
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ func Test_CmdKeys(t *testing.T) {
|
|||||||
"with_key",
|
"with_key",
|
||||||
[]string{"keys", "-e", "git", "-u", "git", "-t", "ssh-rsa", "-k", "AAAAB3NzaC1yc2EAAAADAQABAAABgQDWVj0fQ5N8wNc0LVNA41wDLYJ89ZIbejrPfg/avyj3u/ZohAKsQclxG4Ju0VirduBFF9EOiuxoiFBRr3xRpqzpsZtnMPkWVWb+akZwBFAx8p+jKdy4QXR/SZqbVobrGwip2UjSrri1CtBxpJikojRIZfCnDaMOyd9Jp6KkujvniFzUWdLmCPxUE9zhTaPu0JsEP7MW0m6yx7ZUhHyfss+NtqmFTaDO+QlMR7L2QkDliN2Jl3Xa3PhuWnKJfWhdAq1Cw4oraKUOmIgXLkuiuxVQ6mD3AiFupkmfqdHq6h+uHHmyQqv3gU+/sD8GbGAhf6ftqhTsXjnv1Aj4R8NoDf9BS6KRkzkeun5UisSzgtfQzjOMEiJtmrep2ZQrMGahrXa+q4VKr0aKJfm+KlLfwm/JztfsBcqQWNcTURiCFqz+fgZw0Ey/de0eyMzldYTdXXNRYCKjs9bvBK+6SSXRM7AhftfQ0ZuoW5+gtinPrnmoOaSCEJbAiEiTO/BzOHgowiM="},
|
[]string{"keys", "-e", "git", "-u", "git", "-t", "ssh-rsa", "-k", "AAAAB3NzaC1yc2EAAAADAQABAAABgQDWVj0fQ5N8wNc0LVNA41wDLYJ89ZIbejrPfg/avyj3u/ZohAKsQclxG4Ju0VirduBFF9EOiuxoiFBRr3xRpqzpsZtnMPkWVWb+akZwBFAx8p+jKdy4QXR/SZqbVobrGwip2UjSrri1CtBxpJikojRIZfCnDaMOyd9Jp6KkujvniFzUWdLmCPxUE9zhTaPu0JsEP7MW0m6yx7ZUhHyfss+NtqmFTaDO+QlMR7L2QkDliN2Jl3Xa3PhuWnKJfWhdAq1Cw4oraKUOmIgXLkuiuxVQ6mD3AiFupkmfqdHq6h+uHHmyQqv3gU+/sD8GbGAhf6ftqhTsXjnv1Aj4R8NoDf9BS6KRkzkeun5UisSzgtfQzjOMEiJtmrep2ZQrMGahrXa+q4VKr0aKJfm+KlLfwm/JztfsBcqQWNcTURiCFqz+fgZw0Ey/de0eyMzldYTdXXNRYCKjs9bvBK+6SSXRM7AhftfQ0ZuoW5+gtinPrnmoOaSCEJbAiEiTO/BzOHgowiM="},
|
||||||
false,
|
false,
|
||||||
"# gitea public key\ncommand=\"" + setting.AppPath + " --config=" + util.ShellEscape(setting.CustomConf) + " serv key-1\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDWVj0fQ5N8wNc0LVNA41wDLYJ89ZIbejrPfg/avyj3u/ZohAKsQclxG4Ju0VirduBFF9EOiuxoiFBRr3xRpqzpsZtnMPkWVWb+akZwBFAx8p+jKdy4QXR/SZqbVobrGwip2UjSrri1CtBxpJikojRIZfCnDaMOyd9Jp6KkujvniFzUWdLmCPxUE9zhTaPu0JsEP7MW0m6yx7ZUhHyfss+NtqmFTaDO+QlMR7L2QkDliN2Jl3Xa3PhuWnKJfWhdAq1Cw4oraKUOmIgXLkuiuxVQ6mD3AiFupkmfqdHq6h+uHHmyQqv3gU+/sD8GbGAhf6ftqhTsXjnv1Aj4R8NoDf9BS6KRkzkeun5UisSzgtfQzjOMEiJtmrep2ZQrMGahrXa+q4VKr0aKJfm+KlLfwm/JztfsBcqQWNcTURiCFqz+fgZw0Ey/de0eyMzldYTdXXNRYCKjs9bvBK+6SSXRM7AhftfQ0ZuoW5+gtinPrnmoOaSCEJbAiEiTO/BzOHgowiM= user2@localhost\n",
|
"# gitea public key\ncommand=\"" + setting.AppPath + " --config=" + util.ShellEscape(setting.CustomConf) + " serv key-1\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDWVj0fQ5N8wNc0LVNA41wDLYJ89ZIbejrPfg/avyj3u/ZohAKsQclxG4Ju0VirduBFF9EOiuxoiFBRr3xRpqzpsZtnMPkWVWb+akZwBFAx8p+jKdy4QXR/SZqbVobrGwip2UjSrri1CtBxpJikojRIZfCnDaMOyd9Jp6KkujvniFzUWdLmCPxUE9zhTaPu0JsEP7MW0m6yx7ZUhHyfss+NtqmFTaDO+QlMR7L2QkDliN2Jl3Xa3PhuWnKJfWhdAq1Cw4oraKUOmIgXLkuiuxVQ6mD3AiFupkmfqdHq6h+uHHmyQqv3gU+/sD8GbGAhf6ftqhTsXjnv1Aj4R8NoDf9BS6KRkzkeun5UisSzgtfQzjOMEiJtmrep2ZQrMGahrXa+q4VKr0aKJfm+KlLfwm/JztfsBcqQWNcTURiCFqz+fgZw0Ey/de0eyMzldYTdXXNRYCKjs9bvBK+6SSXRM7AhftfQ0ZuoW5+gtinPrnmoOaSCEJbAiEiTO/BzOHgowiM= user-2\n",
|
||||||
},
|
},
|
||||||
{"invalid", []string{"keys", "--not-a-flag=git"}, true, "Incorrect Usage: flag provided but not defined: -not-a-flag\n\n"},
|
{"invalid", []string{"keys", "--not-a-flag=git"}, true, "Incorrect Usage: flag provided but not defined: -not-a-flag\n\n"},
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user