Files
herolib/lib/osal/sshagent/sshagent.v
2025-08-25 06:28:42 +02:00

317 lines
8.3 KiB
V

module sshagent
import os
import freeflowuniverse.herolib.core.pathlib
import freeflowuniverse.herolib.ui.console
@[heap]
pub struct SSHAgent {
pub mut:
keys []SSHKey
active bool
homepath pathlib.Path
}
// ensure only one SSH agent is running for the current user
pub fn (mut agent SSHAgent) ensure_single_agent() ! {
user := os.getenv('USER')
socket_path := get_agent_socket_path(user)
// Check if we have a valid agent already
if agent.is_agent_responsive() {
console.print_debug('SSH agent already running and responsive')
return
}
// Kill any orphaned agents
agent.cleanup_orphaned_agents()!
// Start new agent with consistent socket
agent.start_agent_with_socket(socket_path)!
// Set environment variables
os.setenv('SSH_AUTH_SOCK', socket_path, true)
agent.active = true
}
// get consistent socket path per user
fn get_agent_socket_path(user string) string {
return '/tmp/ssh-agent-${user}.sock'
}
// check if current agent is responsive
pub fn (mut agent SSHAgent) is_agent_responsive() bool {
if os.getenv('SSH_AUTH_SOCK') == '' {
return false
}
res := os.execute('ssh-add -l 2>/dev/null')
return res.exit_code == 0 || res.exit_code == 1 // 1 means no keys, but agent is running
}
// cleanup orphaned ssh-agent processes
pub fn (mut agent SSHAgent) cleanup_orphaned_agents() ! {
user := os.getenv('USER')
// Find ssh-agent processes for current user
res := os.execute('pgrep -u ${user} ssh-agent')
if res.exit_code == 0 && res.output.len > 0 {
pids := res.output.trim_space().split('\n')
for pid in pids {
if pid.trim_space() != '' {
// Check if this agent has a valid socket
if !agent.is_agent_pid_valid(pid.int()) {
console.print_debug('Killing orphaned ssh-agent PID: ${pid}')
os.execute('kill ${pid}')
}
}
}
}
}
// check if specific agent PID is valid and responsive
fn (mut agent SSHAgent) is_agent_pid_valid(pid int) bool {
// Try to find socket for this PID
res := os.execute('find /tmp -name "agent.*" -user ${os.getenv('USER')} 2>/dev/null | head -10')
if res.exit_code != 0 {
return false
}
for socket_path in res.output.split('\n') {
if socket_path.trim_space() != '' {
// Test if this socket responds
old_sock := os.getenv('SSH_AUTH_SOCK')
os.setenv('SSH_AUTH_SOCK', socket_path, true)
test_res := os.execute('ssh-add -l 2>/dev/null')
os.setenv('SSH_AUTH_SOCK', old_sock, true)
if test_res.exit_code == 0 || test_res.exit_code == 1 {
return true
}
}
}
return false
}
// start new ssh-agent with specific socket path
pub fn (mut agent SSHAgent) start_agent_with_socket(socket_path string) ! {
// Remove existing socket if it exists
if os.exists(socket_path) {
os.rm(socket_path)!
}
// Start ssh-agent with specific socket
cmd := 'ssh-agent -a ${socket_path}'
res := os.execute(cmd)
if res.exit_code != 0 {
return error('Failed to start ssh-agent: ${res.output}')
}
// Verify socket was created
if !os.exists(socket_path) {
return error('SSH agent socket was not created at ${socket_path}')
}
// Set environment variable
os.setenv('SSH_AUTH_SOCK', socket_path, true)
// Verify agent is responsive
if !agent.is_agent_responsive() {
return error('SSH agent started but is not responsive')
}
console.print_debug('SSH agent started with socket: ${socket_path}')
}
// get agent status and diagnostics
pub fn (mut agent SSHAgent) diagnostics() map[string]string {
mut diag := map[string]string{}
diag['socket_path'] = os.getenv('SSH_AUTH_SOCK')
diag['socket_exists'] = os.exists(diag['socket_path']).str()
diag['agent_responsive'] = agent.is_agent_responsive().str()
diag['loaded_keys_count'] = agent.keys.filter(it.loaded).len.str()
diag['total_keys_count'] = agent.keys.len.str()
// Count running ssh-agent processes
user := os.getenv('USER')
res := os.execute('pgrep -u ${user} ssh-agent | wc -l')
diag['agent_processes'] = if res.exit_code == 0 { res.output.trim_space() } else { '0' }
return diag
}
// get all keys from sshagent and from the local .ssh dir
pub fn (mut agent SSHAgent) init() ! {
// first get keys out of ssh-add
agent.keys = []SSHKey{}
res := os.execute('ssh-add -L')
if res.exit_code == 0 {
for line in res.output.split('\n') {
if line.trim(' ') == '' {
continue
}
if line.contains(' ') {
splitted := line.split(' ')
if splitted.len < 2 {
panic('bug')
}
pubkey := splitted[1]
mut sshkey := SSHKey{
pubkey: pubkey
agent: &agent
loaded: true
}
if splitted[0].contains('ed25519') {
sshkey.cat = .ed25519
if splitted.len > 2 {
sshkey.email = splitted[2] or { panic('bug') }
}
} else if splitted[0].contains('rsa') {
sshkey.cat = .rsa
} else {
panic('bug: implement other cat for ssh-key.\n${line}')
}
if !(agent.exists(pubkey: pubkey)) {
// $if debug{console.print_debug("- add from agent: ${sshkey}")}
agent.keys << sshkey
}
}
}
}
// now get them from the filesystem
mut fl := agent.homepath.list()!
mut sshfiles := fl.paths.clone()
mut pubkeypaths := sshfiles.filter(it.path.ends_with('.pub'))
for mut pkp in pubkeypaths {
mut c := pkp.read()!
c = c.replace(' ', ' ').replace(' ', ' ') // deal with double spaces, or tripple (need to do this 2x
splitted := c.trim_space().split(' ')
if splitted.len < 2 {
panic('bug')
}
mut name := pkp.name()
name = name[0..(name.len - 4)]
pubkey2 := splitted[1]
// the pop makes sure the key is removed from keys in agent, this means we can add later
mut sshkey2 := agent.get(pubkey: pubkey2) or {
SSHKey{
name: name
pubkey: pubkey2
agent: &agent
}
}
agent.pop(sshkey2.pubkey)
sshkey2.name = name
if splitted[0].contains('ed25519') {
sshkey2.cat = .ed25519
} else if splitted[0].contains('rsa') {
sshkey2.cat = .rsa
} else {
panic('bug: implement other cat for ssh-key')
}
if splitted.len > 2 {
sshkey2.email = splitted[2]
}
// $if debug{console.print_debug("- add from fs: ${sshkey2}")}
agent.keys << sshkey2
}
}
// returns path to sshkey
pub fn (mut agent SSHAgent) generate(name string, passphrase string) !SSHKey {
dest := '${agent.homepath.path}/${name}'
if os.exists(dest) {
os.rm(dest)!
}
cmd := 'ssh-keygen -t ed25519 -f ${dest} -P ${passphrase} -q'
// console.print_debug(cmd)
rc := os.execute(cmd)
if !(rc.exit_code == 0) {
return error('Could not generated sshkey,\n${rc}')
}
agent.init()!
return agent.get(name: name) or { panic(err) }
}
// unload all ssh keys
pub fn (mut agent SSHAgent) reset() ! {
if true {
panic('reset_ssh')
}
res := os.execute('ssh-add -D')
if res.exit_code > 0 {
return error('cannot reset sshkeys.')
}
agent.init()! // should now be empty for loaded keys
}
// load the key, they key is content (private key) .
// a name is required
pub fn (mut agent SSHAgent) add(name string, privkey_ string) !SSHKey {
mut privkey := privkey_
path := '${agent.homepath.path}/${name}'
if os.exists(path) {
os.rm(path)!
}
if os.exists('${path}.pub') {
os.rm('${path}.pub')!
}
if !privkey.ends_with('\n') {
privkey += '\n'
}
os.write_file(path, privkey)!
os.chmod(path, 0o600)!
res4 := os.execute('ssh-keygen -y -f ${path} > ${path}.pub')
if res4.exit_code > 0 {
return error('cannot generate pubkey ${path}.\n${res4.output}')
}
return agent.load(path)!
}
// load key starting from path to private key
pub fn (mut agent SSHAgent) load(keypath string) !SSHKey {
if !os.exists(keypath) {
return error('cannot find sshkey: ${keypath}')
}
if keypath.ends_with('.pub') {
return error('can only load private keys')
}
name := keypath.split('/').last()
os.chmod(keypath, 0o600)!
res := os.execute('ssh-add ${keypath}')
if res.exit_code > 0 {
return error('cannot add ssh-key with path ${keypath}.\n${res.output}')
}
agent.init()!
return agent.get(name: name) or {
panic("can't find sshkey with name:'${name}' from agent.\n${err}")
}
}
// forget the specified key
pub fn (mut agent SSHAgent) forget(name string) ! {
if true {
panic('reset_ssh')
}
mut key := agent.get(name: name) or { return }
agent.pop(key.pubkey)
key.forget()!
}
pub fn (mut agent SSHAgent) str() string {
mut out := []string{}
out << '\n## SSHAGENT:\n'
for mut key in agent.keys {
out << key.str()
}
return out.join_lines() + '\n'
}
pub fn (mut agent SSHAgent) keys_loaded() ![]SSHKey {
return agent.keys.filter(it.loaded)
}