Files
DankMaterialShell/core/internal/gitssh/gitssh.go
T

130 lines
3.3 KiB
Go

package gitssh
import (
"bytes"
"context"
"errors"
"fmt"
"io/fs"
"net"
"net/url"
"os"
"path/filepath"
"strconv"
"github.com/go-git/go-git/v6/plumbing/client"
"github.com/go-git/go-git/v6/plumbing/transport"
xssh "github.com/go-git/go-git/v6/plumbing/transport/ssh"
"github.com/go-git/go-git/v6/plumbing/transport/ssh/knownhosts"
"github.com/kevinburke/ssh_config"
gossh "golang.org/x/crypto/ssh"
)
func ClientOptions() []client.Option {
return []client.Option{client.WithSSHAuth(agentAuth{})}
}
type agentAuth struct{}
func (agentAuth) ClientConfig(ctx context.Context, req *transport.Request) (*gossh.ClientConfig, error) {
auth, err := xssh.NewSSHAgentAuth(username(req.URL))
if err != nil {
return nil, err
}
db, err := knownHostsDB()
if err != nil {
return nil, err
}
auth.HostKeyCallback = db.HostKeyCallback()
cfg, err := auth.ClientConfig(ctx, req)
if err != nil {
return nil, err
}
cfg.HostKeyAlgorithms = db.HostKeyAlgorithms(hostWithPort(req.URL))
if len(cfg.HostKeyAlgorithms) == 0 {
cfg.HostKeyAlgorithms = gossh.SupportedAlgorithms().HostKeys
}
return cfg, nil
}
func username(u *url.URL) string {
if u.User == nil || u.User.Username() == "" {
return xssh.DefaultUsername
}
return u.User.Username()
}
func hostWithPort(u *url.URL) string {
hostname := u.Hostname()
port := u.Port()
if configHost := ssh_config.Get(hostname, "Hostname"); configHost != "" {
hostname = configHost
}
if port == "" {
if configPort := ssh_config.Get(u.Hostname(), "Port"); configPort != "" {
if _, err := strconv.Atoi(configPort); err == nil {
port = configPort
}
}
}
if port == "" {
port = strconv.Itoa(xssh.DefaultPort)
}
return net.JoinHostPort(hostname, port)
}
// x/crypto rejects a whole known_hosts file over one key type it cannot parse,
// so entries like ssh-mldsa44-ed25519 are blanked before the file is loaded.
func knownHostsDB() (*knownhosts.HostKeyDB, error) {
dir, err := os.MkdirTemp("", "dms-known-hosts-")
if err != nil {
return nil, err
}
defer os.RemoveAll(dir)
var filtered []string
for i, file := range knownHostsFiles() {
content, err := os.ReadFile(file)
if errors.Is(err, fs.ErrNotExist) {
continue
}
if err != nil {
return nil, err
}
path := filepath.Join(dir, fmt.Sprintf("%d-%s", i, filepath.Base(file)))
if err := os.WriteFile(path, blankUnparseableLines(content), 0o600); err != nil {
return nil, err
}
filtered = append(filtered, path)
}
if len(filtered) == 0 {
return nil, errors.New("unable to find any valid known_hosts file, set SSH_KNOWN_HOSTS env variable")
}
return knownhosts.NewDB(filtered...)
}
func knownHostsFiles() []string {
if files := filepath.SplitList(os.Getenv("SSH_KNOWN_HOSTS")); len(files) != 0 {
return files
}
home, err := os.UserHomeDir()
if err != nil {
return []string{"/etc/ssh/ssh_known_hosts"}
}
return []string{filepath.Join(home, ".ssh", "known_hosts"), "/etc/ssh/ssh_known_hosts"}
}
func blankUnparseableLines(content []byte) []byte {
lines := bytes.Split(content, []byte("\n"))
for i, line := range lines {
trimmed := bytes.TrimSpace(line)
if len(trimmed) == 0 || trimmed[0] == '#' {
continue
}
if _, _, _, _, _, err := gossh.ParseKnownHosts(trimmed); err != nil {
lines[i] = nil
}
}
return bytes.Join(lines, []byte("\n"))
}