update based on feedback

This commit is contained in:
techknowlogick
2026-03-23 19:36:52 -04:00
parent 2654fa3bfa
commit 770663b23d
9 changed files with 168 additions and 122 deletions
+6 -69
View File
@@ -7,11 +7,7 @@ import (
"crypto/ed25519"
"fmt"
"net"
"os"
"path/filepath"
"runtime"
"sync"
"time"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
@@ -33,61 +29,16 @@ type Agent struct {
// NewSSHAgent creates a new SSH agent with the given private key
func NewSSHAgent(privateKey ed25519.PrivateKey) (*Agent, error) {
var listener net.Listener
var socketPath string
var tempDir string
var err error
// Setup cleanup function for early returns
var cleanup func()
listener, socketPath, cleanup, err := createAgentListener()
if err != nil {
return nil, err
}
defer func() {
if cleanup != nil {
cleanup()
}
}()
if runtime.GOOS == "windows" {
// On Windows, use named pipes
agentID, err := util.CryptoRandomString(16)
if err != nil {
return nil, fmt.Errorf("failed to generate agent ID: %w", err)
}
socketPath = `\\.\pipe\gitea-ssh-agent-` + agentID
listener, err = net.Listen("pipe", socketPath)
if err != nil {
return nil, fmt.Errorf("failed to create named pipe: %w", err)
}
cleanup = func() {
listener.Close()
}
} else {
tempDir, err = os.MkdirTemp("", "gitea-ssh-agent-")
if err != nil {
return nil, fmt.Errorf("failed to create temporary directory: %w", err)
}
cleanup = func() {
os.RemoveAll(tempDir)
}
if err := os.Chmod(tempDir, 0o700); err != nil {
return nil, fmt.Errorf("failed to set temporary directory permissions: %w", err)
}
socketPath = filepath.Join(tempDir, "agent.sock")
listener, err = net.Listen("unix", socketPath)
if err != nil {
return nil, fmt.Errorf("failed to create Unix socket: %w", err)
}
cleanup = func() {
listener.Close()
os.RemoveAll(tempDir)
}
if err := os.Chmod(socketPath, 0o600); err != nil {
return nil, fmt.Errorf("failed to set socket permissions: %w", err)
}
}
sshAgent := agent.NewKeyring()
if len(privateKey) != ed25519.PrivateKeySize {
@@ -136,14 +87,7 @@ func (sa *Agent) serve() {
return
default:
// Set a timeout for Accept to avoid blocking indefinitely
if runtime.GOOS != "windows" {
// On Windows, named pipes don't support SetDeadline in the same way
if listener, ok := sa.listener.(*net.UnixListener); ok {
if err := listener.SetDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
log.Debug("Failed to set listener deadline: %v", err)
}
}
}
setListenerAcceptDeadline(sa.listener)
conn, err := sa.listener.Accept()
if err != nil {
@@ -175,14 +119,7 @@ func (sa *Agent) serve() {
// cleanup removes the socket file and temporary directory
func (sa *Agent) cleanup() {
if sa.socketPath != "" {
if runtime.GOOS != "windows" {
// On Windows, named pipes are automatically cleaned up when closed
// On Unix-like systems, remove the temporary directory
tempDir := filepath.Dir(sa.socketPath)
os.RemoveAll(tempDir)
}
}
cleanupAgentSocket(sa.socketPath)
}
// GetSocketPath returns the path to the SSH agent socket
+65
View File
@@ -0,0 +1,65 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !windows
package ssh
import (
"fmt"
"net"
"os"
"path/filepath"
"time"
)
// createAgentListener creates a Unix domain socket listener for the SSH agent.
// Returns the listener, socket path, and a cleanup function for early-return error paths.
func createAgentListener() (net.Listener, string, func(), error) {
tempDir, err := os.MkdirTemp("", "gitea-ssh-agent-")
if err != nil {
return nil, "", nil, fmt.Errorf("failed to create temporary directory: %w", err)
}
cleanupDir := func() {
os.RemoveAll(tempDir)
}
if err := os.Chmod(tempDir, 0o700); err != nil {
cleanupDir()
return nil, "", nil, fmt.Errorf("failed to set temporary directory permissions: %w", err)
}
socketPath := filepath.Join(tempDir, "agent.sock")
listener, err := net.Listen("unix", socketPath)
if err != nil {
cleanupDir()
return nil, "", nil, fmt.Errorf("failed to create Unix socket: %w", err)
}
cleanup := func() {
listener.Close()
os.RemoveAll(tempDir)
}
if err := os.Chmod(socketPath, 0o600); err != nil {
cleanup()
return nil, "", nil, fmt.Errorf("failed to set socket permissions: %w", err)
}
return listener, socketPath, cleanup, nil
}
// setListenerAcceptDeadline sets a short deadline on the listener for non-blocking accept loops.
func setListenerAcceptDeadline(listener net.Listener) {
if unixListener, ok := listener.(*net.UnixListener); ok {
_ = unixListener.SetDeadline(time.Now().Add(100 * time.Millisecond))
}
}
// cleanupAgentSocket removes the socket file and its temporary directory.
func cleanupAgentSocket(socketPath string) {
if socketPath != "" {
tempDir := filepath.Dir(socketPath)
os.RemoveAll(tempDir)
}
}
+42
View File
@@ -0,0 +1,42 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build windows
package ssh
import (
"fmt"
"net"
"code.gitea.io/gitea/modules/util"
"github.com/Microsoft/go-winio"
)
// createAgentListener creates a Windows named pipe listener for the SSH agent.
// Returns the listener, pipe path, and a cleanup function for early-return error paths.
func createAgentListener() (net.Listener, string, func(), error) {
agentID, err := util.CryptoRandomString(16)
if err != nil {
return nil, "", nil, fmt.Errorf("failed to generate agent ID: %w", err)
}
pipePath := `\\.\pipe\gitea-ssh-agent-` + agentID
listener, err := winio.ListenPipe(pipePath, nil)
if err != nil {
return nil, "", nil, fmt.Errorf("failed to create named pipe: %w", err)
}
cleanup := func() {
listener.Close()
}
return listener, pipePath, cleanup, nil
}
// setListenerAcceptDeadline is a no-op on Windows; named pipes don't support SetDeadline.
func setListenerAcceptDeadline(_ net.Listener) {}
// cleanupAgentSocket is a no-op on Windows; named pipes are automatically cleaned up when closed.
func cleanupAgentSocket(_ string) {}
+1 -1
View File
@@ -67,7 +67,7 @@ func GetSSHKeypairForRepository(ctx context.Context, repo *repo_model.Repository
// Returns nil if the URL is not an SSH URL
func GetSSHKeypairForURL(ctx context.Context, repo *repo_model.Repository, url string) (*repo_model.UserSSHKeypair, error) {
if !IsSSHURL(url) {
return nil, nil
return nil, nil //nolint:nilnil // non-SSH URLs don't need a keypair
}
return GetSSHKeypairForRepository(ctx, repo)
}