191 lines
4.7 KiB
Go
191 lines
4.7 KiB
Go
package git
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/go-git/go-git/v5/plumbing/transport"
|
|
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
|
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
|
|
)
|
|
|
|
// AuthProvider handles authentication for Git operations
|
|
type AuthProvider struct {
|
|
cache map[string]transport.AuthMethod
|
|
}
|
|
|
|
// NewAuthProvider creates a new AuthProvider
|
|
func NewAuthProvider() *AuthProvider {
|
|
return &AuthProvider{
|
|
cache: make(map[string]transport.AuthMethod),
|
|
}
|
|
}
|
|
|
|
// GetAuthMethod returns the appropriate authentication method for a URL
|
|
func (ap *AuthProvider) GetAuthMethod(url string) (transport.AuthMethod, error) {
|
|
// Check cache first
|
|
if auth, ok := ap.cache[url]; ok {
|
|
return auth, nil
|
|
}
|
|
|
|
var auth transport.AuthMethod
|
|
var err error
|
|
|
|
// Detect transport type from URL
|
|
if strings.HasPrefix(url, "git@") || strings.HasPrefix(url, "ssh://") {
|
|
// SSH authentication
|
|
auth, err = ap.getSSHAuth()
|
|
} else if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
|
|
// HTTPS authentication
|
|
auth, err = ap.getHTTPSAuth(url)
|
|
} else {
|
|
return nil, fmt.Errorf("unsupported URL scheme: %s", url)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Cache the auth method
|
|
ap.cache[url] = auth
|
|
|
|
return auth, nil
|
|
}
|
|
|
|
// getSSHAuth attempts to get SSH authentication
|
|
func (ap *AuthProvider) getSSHAuth() (transport.AuthMethod, error) {
|
|
// Try SSH agent first
|
|
auth, err := ssh.NewSSHAgentAuth("git")
|
|
if err == nil {
|
|
return auth, nil
|
|
}
|
|
|
|
// Fallback to loading SSH keys from default locations
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get home directory: %w", err)
|
|
}
|
|
|
|
sshDir := filepath.Join(homeDir, ".ssh")
|
|
|
|
// Try common key files
|
|
keyFiles := []string{
|
|
"id_ed25519",
|
|
"id_rsa",
|
|
"id_ecdsa",
|
|
"id_dsa",
|
|
}
|
|
|
|
for _, keyFile := range keyFiles {
|
|
keyPath := filepath.Join(sshDir, keyFile)
|
|
if _, err := os.Stat(keyPath); err == nil {
|
|
// Try loading without passphrase first
|
|
auth, err := ssh.NewPublicKeysFromFile("git", keyPath, "")
|
|
if err == nil {
|
|
return auth, nil
|
|
}
|
|
|
|
// If that fails, it might need a passphrase
|
|
// For now, we'll skip passphrase-protected keys
|
|
// In the future, we could prompt for the passphrase
|
|
continue
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("no SSH authentication method available (tried ssh-agent and ~/.ssh keys)")
|
|
}
|
|
|
|
// getHTTPSAuth attempts to get HTTPS authentication
|
|
func (ap *AuthProvider) getHTTPSAuth(url string) (transport.AuthMethod, error) {
|
|
// Try git credential helper first
|
|
auth, err := ap.tryGitCredentialHelper(url)
|
|
if err == nil && auth != nil {
|
|
return auth, nil
|
|
}
|
|
|
|
// Try environment variables
|
|
username := os.Getenv("GIT_USERNAME")
|
|
password := os.Getenv("GIT_PASSWORD")
|
|
token := os.Getenv("GIT_TOKEN")
|
|
|
|
if token != "" {
|
|
// Use token as password (common for GitHub, GitLab, etc.)
|
|
return &http.BasicAuth{
|
|
Username: "git", // Token usually goes in password field
|
|
Password: token,
|
|
}, nil
|
|
}
|
|
|
|
if username != "" && password != "" {
|
|
return &http.BasicAuth{
|
|
Username: username,
|
|
Password: password,
|
|
}, nil
|
|
}
|
|
|
|
// No credentials available - return nil to let go-git try anonymous
|
|
// (this will fail for private repos but that's expected)
|
|
return nil, fmt.Errorf("no HTTPS credentials available (tried git credential helper and environment variables)")
|
|
}
|
|
|
|
// tryGitCredentialHelper attempts to use git's credential helper
|
|
func (ap *AuthProvider) tryGitCredentialHelper(url string) (*http.BasicAuth, error) {
|
|
// Build the credential request
|
|
input := fmt.Sprintf("protocol=https\nhost=%s\n\n", extractHost(url))
|
|
|
|
// Call git credential fill
|
|
cmd := exec.Command("git", "credential", "fill")
|
|
cmd.Stdin = strings.NewReader(input)
|
|
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("git credential helper failed: %w", err)
|
|
}
|
|
|
|
// Parse the output
|
|
lines := strings.Split(string(output), "\n")
|
|
auth := &http.BasicAuth{}
|
|
|
|
for _, line := range lines {
|
|
parts := strings.SplitN(line, "=", 2)
|
|
if len(parts) != 2 {
|
|
continue
|
|
}
|
|
|
|
key := strings.TrimSpace(parts[0])
|
|
value := strings.TrimSpace(parts[1])
|
|
|
|
switch key {
|
|
case "username":
|
|
auth.Username = value
|
|
case "password":
|
|
auth.Password = value
|
|
}
|
|
}
|
|
|
|
if auth.Username == "" || auth.Password == "" {
|
|
return nil, fmt.Errorf("git credential helper did not return username and password")
|
|
}
|
|
|
|
return auth, nil
|
|
}
|
|
|
|
// extractHost extracts the host from a URL
|
|
func extractHost(url string) string {
|
|
// Remove protocol
|
|
url = strings.TrimPrefix(url, "https://")
|
|
url = strings.TrimPrefix(url, "http://")
|
|
|
|
// Extract host (everything before the first /)
|
|
parts := strings.SplitN(url, "/", 2)
|
|
return parts[0]
|
|
}
|
|
|
|
// ClearCache clears the authentication cache
|
|
func (ap *AuthProvider) ClearCache() {
|
|
ap.cache = make(map[string]transport.AuthMethod)
|
|
}
|