feat: add comprehensive SSH agent management command
- Introduce `hero sshagent` for full SSH agent management - Implement `profile`, `push`, `auth`, `status` subcommands - Enable smart key loading and shell profile integration - Support remote key deployment and authorization verification - Use `~/.ssh/hero-agent.sock` and ensure secure permissions
This commit is contained in:
@@ -86,6 +86,7 @@ fn do() ! {
|
||||
herocmds.cmd_generator(mut cmd)
|
||||
herocmds.cmd_docusaurus(mut cmd)
|
||||
herocmds.cmd_web(mut cmd)
|
||||
herocmds.cmd_sshagent(mut cmd)
|
||||
|
||||
cmd.setup()
|
||||
cmd.parse(os.args)
|
||||
|
||||
255
examples/sshagent/hero_sshagent_examples.md
Normal file
255
examples/sshagent/hero_sshagent_examples.md
Normal file
@@ -0,0 +1,255 @@
|
||||
# Hero SSH Agent Management Tool
|
||||
|
||||
The Hero SSH Agent Management Tool provides comprehensive SSH agent lifecycle management with cross-platform compatibility for macOS and Linux. It integrates seamlessly with shell profiles and implements intelligent SSH agent lifecycle management, automatic key discovery and loading, and remote key deployment capabilities.
|
||||
|
||||
## Features
|
||||
|
||||
- 🔐 **Single SSH Agent Instance Enforcement**: Ensures exactly one SSH agent is running with persistent socket management
|
||||
- 🚀 **Smart Key Loading**: Auto-loads single keys from ~/.ssh/ directory with passphrase prompting
|
||||
- 🌐 **Remote Key Deployment**: Interactive SSH key deployment to remote machines
|
||||
- 🔄 **Agent Health Verification**: Comprehensive health checks through ssh-agent -l functionality
|
||||
- 📝 **Shell Profile Integration**: Automatic initialization in shell profiles
|
||||
- 🎯 **Cross-Platform**: Works on macOS and Linux
|
||||
|
||||
## Installation
|
||||
|
||||
The SSH agent functionality is built into the hero binary. After compiling hero:
|
||||
|
||||
```bash
|
||||
./cli/compile.vsh
|
||||
```
|
||||
|
||||
The hero binary will be available at `/Users/mahmoud/hero/bin/hero`
|
||||
|
||||
## Commands
|
||||
|
||||
### `hero sshagent profile`
|
||||
|
||||
Primary initialization command that ensures exactly one SSH agent is running on a consistent socket, performs health checks, manages agent lifecycle without losing existing keys, and automatically loads SSH keys when only one is present in ~/.ssh/ directory.
|
||||
|
||||
```bash
|
||||
# Initialize SSH agent with smart key loading
|
||||
hero sshagent profile
|
||||
```
|
||||
|
||||
**Features:**
|
||||
|
||||
- Ensures single SSH agent instance
|
||||
- Verifies agent health and responsiveness
|
||||
- Auto-loads single SSH keys
|
||||
- Updates shell profile for automatic initialization
|
||||
- Preserves existing loaded keys
|
||||
|
||||
### `hero sshagent list`
|
||||
|
||||
Lists all available SSH keys and their current status.
|
||||
|
||||
```bash
|
||||
# List all SSH keys
|
||||
hero sshagent list
|
||||
```
|
||||
|
||||
### `hero sshagent status`
|
||||
|
||||
Shows comprehensive SSH agent status and diagnostics.
|
||||
|
||||
```bash
|
||||
# Show agent status
|
||||
hero sshagent status
|
||||
```
|
||||
|
||||
### `hero sshagent generate`
|
||||
|
||||
Generates a new SSH key pair.
|
||||
|
||||
```bash
|
||||
# Generate new SSH key
|
||||
hero sshagent generate -n my_new_key
|
||||
|
||||
# Generate and immediately load
|
||||
hero sshagent generate -n my_new_key -l
|
||||
```
|
||||
|
||||
### `hero sshagent load`
|
||||
|
||||
Loads a specific SSH key into the agent.
|
||||
|
||||
```bash
|
||||
# Load specific key
|
||||
hero sshagent load -n my_key
|
||||
```
|
||||
|
||||
### `hero sshagent forget`
|
||||
|
||||
Removes a specific SSH key from the agent.
|
||||
|
||||
```bash
|
||||
# Remove key from agent
|
||||
hero sshagent forget -n my_key
|
||||
```
|
||||
|
||||
### `hero sshagent reset`
|
||||
|
||||
Removes all loaded SSH keys from the agent.
|
||||
|
||||
```bash
|
||||
# Reset agent (remove all keys)
|
||||
hero sshagent reset
|
||||
```
|
||||
|
||||
### `hero sshagent push`
|
||||
|
||||
Interactive SSH key deployment to remote machines with automatic key selection when multiple keys exist, target specification via user@hostname format, and streamlined single-key auto-selection.
|
||||
|
||||
```bash
|
||||
# Deploy SSH key to remote machine
|
||||
hero sshagent push -t user@hostname
|
||||
|
||||
# Deploy specific key to remote machine
|
||||
hero sshagent push -t user@hostname -k my_key
|
||||
|
||||
# Deploy to custom port
|
||||
hero sshagent push -t user@hostname:2222
|
||||
```
|
||||
|
||||
**Features:**
|
||||
|
||||
- Automatic key selection when only one key exists
|
||||
- Interactive key selection for multiple keys
|
||||
- Support for custom SSH ports
|
||||
- Uses ssh-copy-id when available, falls back to manual deployment
|
||||
|
||||
### `hero sshagent auth`
|
||||
|
||||
Remote SSH key authorization ensuring proper key installation on target machines, support for explicit key specification via -key parameter, and verification of successful key addition.
|
||||
|
||||
```bash
|
||||
# Verify SSH key authorization
|
||||
hero sshagent auth -t user@hostname
|
||||
|
||||
# Verify specific key authorization
|
||||
hero sshagent auth -t user@hostname -k my_key
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Workflow
|
||||
|
||||
```bash
|
||||
# 1. Initialize SSH agent
|
||||
hero sshagent profile
|
||||
|
||||
# 2. Check status
|
||||
hero sshagent status
|
||||
|
||||
# 3. Generate a new key if needed
|
||||
hero sshagent generate -n production_key
|
||||
|
||||
# 4. Load the key
|
||||
hero sshagent load -n production_key
|
||||
|
||||
# 5. Deploy to remote server
|
||||
hero sshagent push -t user@production-server.com
|
||||
|
||||
# 6. Verify authorization
|
||||
hero sshagent auth -t user@production-server.com
|
||||
```
|
||||
|
||||
### Advanced Usage
|
||||
|
||||
```bash
|
||||
# Deploy specific key to multiple servers
|
||||
hero sshagent push -t user@server1.com -k production_key
|
||||
hero sshagent push -t user@server2.com:2222 -k production_key
|
||||
|
||||
# Verify access to all servers
|
||||
hero sshagent auth -t user@server1.com -k production_key
|
||||
hero sshagent auth -t user@server2.com:2222 -k production_key
|
||||
|
||||
# List all keys and their status
|
||||
hero sshagent list
|
||||
|
||||
# Reset agent if needed
|
||||
hero sshagent reset
|
||||
```
|
||||
|
||||
## Technical Specifications
|
||||
|
||||
### Cross-Platform Compatibility
|
||||
|
||||
- **Socket Management**: Uses ~/.ssh/hero-agent.sock for consistent socket location
|
||||
- **Shell Integration**: Supports ~/.profile, ~/.bash_profile, ~/.bashrc, and ~/.zshrc
|
||||
- **Process Management**: Robust SSH agent lifecycle management
|
||||
- **Platform Support**: macOS and Linux (Windows not supported)
|
||||
|
||||
### Security Features
|
||||
|
||||
- **Single Agent Enforcement**: Prevents multiple conflicting agents
|
||||
- **Secure Socket Paths**: Uses user home directory for socket files
|
||||
- **Proper Permissions**: Ensures correct file permissions (0700 for .ssh, 0600 for keys)
|
||||
- **Input Validation**: Validates all user inputs and target specifications
|
||||
- **Connection Testing**: Verifies SSH connections before reporting success
|
||||
|
||||
### Error Handling
|
||||
|
||||
- **Network Connectivity**: Handles network failures gracefully
|
||||
- **Authentication Failures**: Provides clear error messages for auth issues
|
||||
- **Key Management**: Validates key existence and format
|
||||
- **Target Validation**: Ensures proper target format (user@hostname[:port])
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `HERO_DEBUG=1`: Enable debug output for troubleshooting
|
||||
|
||||
## Integration with Development Environments
|
||||
|
||||
The tool is designed to work seamlessly with development environments:
|
||||
|
||||
- **Preserves existing SSH agent state** during initialization
|
||||
- **Non-destructive operations** that don't interfere with existing workflows
|
||||
- **Shell profile integration** for automatic initialization
|
||||
- **Compatible with existing SSH configurations**
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Multiple SSH agents running**
|
||||
|
||||
```bash
|
||||
hero sshagent profile # Will cleanup and ensure single agent
|
||||
```
|
||||
|
||||
2. **Keys not loading**
|
||||
|
||||
```bash
|
||||
hero sshagent status # Check agent status
|
||||
hero sshagent reset # Reset if needed
|
||||
```
|
||||
|
||||
3. **Remote deployment failures**
|
||||
|
||||
```bash
|
||||
hero sshagent auth -t user@hostname # Verify connectivity
|
||||
```
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable debug output for detailed troubleshooting:
|
||||
|
||||
```bash
|
||||
HERO_DEBUG=1 hero sshagent profile
|
||||
```
|
||||
|
||||
## Shell Profile Integration
|
||||
|
||||
The `hero sshagent profile` command automatically adds initialization code to your shell profile:
|
||||
|
||||
```bash
|
||||
# Hero SSH Agent initialization
|
||||
if [ -f "/Users/username/.ssh/hero-agent.sock" ]; then
|
||||
export SSH_AUTH_SOCK="/Users/username/.ssh/hero-agent.sock"
|
||||
fi
|
||||
```
|
||||
|
||||
This ensures the SSH agent is available in new shell sessions.
|
||||
104
examples/sshagent/test_hero_sshagent.vsh
Executable file
104
examples/sshagent/test_hero_sshagent.vsh
Executable file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import os
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
|
||||
// Test script for Hero SSH Agent functionality
|
||||
// This script demonstrates the key features of the hero sshagent command
|
||||
|
||||
fn main() {
|
||||
console.print_header('🔑 Hero SSH Agent Test Suite')
|
||||
|
||||
hero_bin := '/Users/mahmoud/hero/bin/hero'
|
||||
|
||||
// Check if hero binary exists
|
||||
if !os.exists(hero_bin) {
|
||||
console.print_stderr('Hero binary not found at ${hero_bin}')
|
||||
console.print_stderr('Please compile hero first with: ./cli/compile.vsh')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
console.print_green('✓ Hero binary found at ${hero_bin}')
|
||||
|
||||
// Test 1: Profile initialization
|
||||
console.print_header('Test 1: Profile Initialization')
|
||||
result1 := os.execute('${hero_bin} sshagent profile')
|
||||
if result1.exit_code == 0 {
|
||||
console.print_green('✓ Profile initialization successful')
|
||||
} else {
|
||||
console.print_stderr('❌ Profile initialization failed: ${result1.output}')
|
||||
}
|
||||
|
||||
// Test 2: Status check
|
||||
console.print_header('Test 2: Status Check')
|
||||
result2 := os.execute('${hero_bin} sshagent status')
|
||||
if result2.exit_code == 0 {
|
||||
console.print_green('✓ Status check successful')
|
||||
println(result2.output)
|
||||
} else {
|
||||
console.print_stderr('❌ Status check failed: ${result2.output}')
|
||||
}
|
||||
|
||||
// Test 3: List keys
|
||||
console.print_header('Test 3: List SSH Keys')
|
||||
result3 := os.execute('${hero_bin} sshagent list')
|
||||
if result3.exit_code == 0 {
|
||||
console.print_green('✓ List keys successful')
|
||||
println(result3.output)
|
||||
} else {
|
||||
console.print_stderr('❌ List keys failed: ${result3.output}')
|
||||
}
|
||||
|
||||
// Test 4: Generate test key
|
||||
console.print_header('Test 4: Generate Test Key')
|
||||
test_key_name := 'hero_test_${os.getpid()}'
|
||||
result4 := os.execute('${hero_bin} sshagent generate -n ${test_key_name}')
|
||||
if result4.exit_code == 0 {
|
||||
console.print_green('✓ Key generation successful')
|
||||
println(result4.output)
|
||||
|
||||
// Cleanup: remove test key files
|
||||
test_key_path := '${os.home_dir()}/.ssh/${test_key_name}'
|
||||
test_pub_path := '${test_key_path}.pub'
|
||||
|
||||
if os.exists(test_key_path) {
|
||||
os.rm(test_key_path) or {}
|
||||
console.print_debug('Cleaned up test private key')
|
||||
}
|
||||
if os.exists(test_pub_path) {
|
||||
os.rm(test_pub_path) or {}
|
||||
console.print_debug('Cleaned up test public key')
|
||||
}
|
||||
} else {
|
||||
console.print_stderr('❌ Key generation failed: ${result4.output}')
|
||||
}
|
||||
|
||||
// Test 5: Help output
|
||||
console.print_header('Test 5: Help Output')
|
||||
result5 := os.execute('${hero_bin} sshagent')
|
||||
if result5.exit_code == 1 && result5.output.contains('Hero SSH Agent Management Tool') {
|
||||
console.print_green('✓ Help output is correct')
|
||||
} else {
|
||||
console.print_stderr('❌ Help output unexpected')
|
||||
}
|
||||
|
||||
console.print_header('🎉 Test Suite Complete')
|
||||
console.print_green('Hero SSH Agent is ready for use!')
|
||||
|
||||
// Show usage examples
|
||||
console.print_header('Usage Examples:')
|
||||
println('')
|
||||
println('Initialize SSH agent:')
|
||||
println(' ${hero_bin} sshagent profile')
|
||||
println('')
|
||||
println('Check status:')
|
||||
println(' ${hero_bin} sshagent status')
|
||||
println('')
|
||||
println('Deploy key to remote server:')
|
||||
println(' ${hero_bin} sshagent push -t user@server.com')
|
||||
println('')
|
||||
println('Verify authorization:')
|
||||
println(' ${hero_bin} sshagent auth -t user@server.com')
|
||||
println('')
|
||||
println('For more examples, see: examples/sshagent/hero_sshagent_examples.md')
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
module herocmds
|
||||
|
||||
import os
|
||||
import freeflowuniverse.herolib.osal.sshagent
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import freeflowuniverse.herolib.ui
|
||||
@@ -8,32 +9,87 @@ import cli { Command, Flag }
|
||||
pub fn cmd_sshagent(mut cmdroot Command) {
|
||||
mut cmd_run := Command{
|
||||
name: 'sshagent'
|
||||
description: 'Work with SSHAgent'
|
||||
// required_args: 1
|
||||
usage: 'sub commands of generate are list, generate, unload, load'
|
||||
description: 'Comprehensive SSH Agent Management'
|
||||
usage: '
|
||||
Hero SSH Agent Management Tool
|
||||
|
||||
COMMANDS:
|
||||
profile Initialize SSH agent with smart key loading
|
||||
list List available SSH keys
|
||||
generate <name> Generate new SSH key
|
||||
load <name> Load SSH key into agent
|
||||
forget <name> Remove SSH key from agent
|
||||
reset Remove all loaded SSH keys
|
||||
push <target> [key] Deploy SSH key to remote machine
|
||||
auth <target> [key] Verify SSH key authorization
|
||||
status Show SSH agent status
|
||||
|
||||
EXAMPLES:
|
||||
hero sshagent profile
|
||||
hero sshagent push user@server.com
|
||||
hero sshagent push user@server.com:2222 my_key
|
||||
hero sshagent auth user@server.com
|
||||
hero sshagent load my_key
|
||||
hero sshagent status
|
||||
|
||||
TARGET FORMAT:
|
||||
user@hostname[:port] # Port defaults to 22
|
||||
'
|
||||
execute: cmd_sshagent_execute
|
||||
sort_commands: true
|
||||
}
|
||||
|
||||
// Profile command - primary initialization
|
||||
mut sshagent_command_profile := Command{
|
||||
sort_flags: true
|
||||
name: 'profile'
|
||||
execute: cmd_sshagent_execute
|
||||
description: 'Initialize SSH agent with smart key loading and shell integration'
|
||||
}
|
||||
|
||||
mut sshagent_command_list := Command{
|
||||
sort_flags: true
|
||||
name: 'list'
|
||||
execute: cmd_sshagent_execute
|
||||
description: 'list ssh-keys.'
|
||||
description: 'List available SSH keys and their status'
|
||||
}
|
||||
|
||||
mut sshagent_command_generate := Command{
|
||||
sort_flags: true
|
||||
name: 'generate'
|
||||
execute: cmd_sshagent_execute
|
||||
description: 'generate ssh-key.'
|
||||
description: 'Generate new SSH key pair'
|
||||
}
|
||||
|
||||
mut sshagent_command_add := Command{
|
||||
sort_flags: true
|
||||
name: 'add'
|
||||
execute: cmd_sshagent_execute
|
||||
description: 'add a key starting from private key, only works interactive for nows.'
|
||||
description: 'Add existing private key to SSH agent'
|
||||
}
|
||||
|
||||
// Status command
|
||||
mut sshagent_command_status := Command{
|
||||
sort_flags: true
|
||||
name: 'status'
|
||||
execute: cmd_sshagent_execute
|
||||
description: 'Show SSH agent status and diagnostics'
|
||||
}
|
||||
|
||||
// Push command for remote deployment
|
||||
mut sshagent_command_push := Command{
|
||||
sort_flags: true
|
||||
name: 'push'
|
||||
execute: cmd_sshagent_execute
|
||||
description: 'Deploy SSH key to remote machine'
|
||||
}
|
||||
|
||||
// Auth command for verification
|
||||
mut sshagent_command_auth := Command{
|
||||
sort_flags: true
|
||||
name: 'auth'
|
||||
execute: cmd_sshagent_execute
|
||||
description: 'Verify SSH key authorization on remote machine'
|
||||
}
|
||||
|
||||
sshagent_command_generate.add_flag(Flag{
|
||||
@@ -43,6 +99,37 @@ pub fn cmd_sshagent(mut cmdroot Command) {
|
||||
description: 'should key be loaded'
|
||||
})
|
||||
|
||||
// Add target flag for push and auth commands
|
||||
sshagent_command_push.add_flag(Flag{
|
||||
flag: .string
|
||||
name: 'target'
|
||||
abbrev: 't'
|
||||
required: true
|
||||
description: 'target in format user@hostname[:port]'
|
||||
})
|
||||
|
||||
sshagent_command_push.add_flag(Flag{
|
||||
flag: .string
|
||||
name: 'key'
|
||||
abbrev: 'k'
|
||||
description: 'specific key name to deploy (optional)'
|
||||
})
|
||||
|
||||
sshagent_command_auth.add_flag(Flag{
|
||||
flag: .string
|
||||
name: 'target'
|
||||
abbrev: 't'
|
||||
required: true
|
||||
description: 'target in format user@hostname[:port]'
|
||||
})
|
||||
|
||||
sshagent_command_auth.add_flag(Flag{
|
||||
flag: .string
|
||||
name: 'key'
|
||||
abbrev: 'k'
|
||||
description: 'specific key name to verify (optional)'
|
||||
})
|
||||
|
||||
mut sshagent_command_load := Command{
|
||||
sort_flags: true
|
||||
name: 'load'
|
||||
@@ -64,6 +151,7 @@ pub fn cmd_sshagent(mut cmdroot Command) {
|
||||
description: 'Reset all keys, means unload them all.'
|
||||
}
|
||||
|
||||
// Commands that require a name parameter
|
||||
mut allcmdsref_gen0 := [&sshagent_command_generate, &sshagent_command_load, &sshagent_command_unload,
|
||||
&sshagent_command_reset, &sshagent_command_add]
|
||||
for mut d in allcmdsref_gen0 {
|
||||
@@ -76,63 +164,486 @@ pub fn cmd_sshagent(mut cmdroot Command) {
|
||||
})
|
||||
}
|
||||
|
||||
// Commands that support script mode
|
||||
mut allcmdsref_gen := [&sshagent_command_list, &sshagent_command_generate, &sshagent_command_load,
|
||||
&sshagent_command_unload, &sshagent_command_reset]
|
||||
&sshagent_command_unload, &sshagent_command_reset, &sshagent_command_status]
|
||||
|
||||
for mut c in allcmdsref_gen {
|
||||
// c.add_flag(Flag{
|
||||
// flag: .bool
|
||||
// name: 'reset'
|
||||
// abbrev: 'r'
|
||||
// description: 'do you want to reset all? Dangerous!'
|
||||
// })
|
||||
c.add_flag(Flag{
|
||||
flag: .bool
|
||||
name: 'script'
|
||||
abbrev: 's'
|
||||
description: 'runs non interactive!'
|
||||
})
|
||||
|
||||
cmd_run.add_command(*c)
|
||||
}
|
||||
|
||||
// Add all commands to the main sshagent command
|
||||
cmd_run.add_command(sshagent_command_profile)
|
||||
cmd_run.add_command(sshagent_command_list)
|
||||
cmd_run.add_command(sshagent_command_generate)
|
||||
cmd_run.add_command(sshagent_command_add)
|
||||
cmd_run.add_command(sshagent_command_load)
|
||||
cmd_run.add_command(sshagent_command_unload)
|
||||
cmd_run.add_command(sshagent_command_reset)
|
||||
cmd_run.add_command(sshagent_command_status)
|
||||
cmd_run.add_command(sshagent_command_push)
|
||||
cmd_run.add_command(sshagent_command_auth)
|
||||
|
||||
cmdroot.add_command(cmd_run)
|
||||
}
|
||||
|
||||
fn cmd_sshagent_execute(cmd Command) ! {
|
||||
// mut reset := cmd.flags.get_bool('reset') or {false }
|
||||
mut isscript := cmd.flags.get_bool('script') or { false }
|
||||
mut load := cmd.flags.get_bool('load') or { false }
|
||||
mut name := cmd.flags.get_string('name') or { '' }
|
||||
mut target := cmd.flags.get_string('target') or { '' }
|
||||
mut key := cmd.flags.get_string('key') or { '' }
|
||||
|
||||
mut agent := sshagent.new()!
|
||||
|
||||
if cmd.name == 'list' {
|
||||
match cmd.name {
|
||||
'profile' {
|
||||
cmd_profile_execute(mut agent, isscript)!
|
||||
}
|
||||
'list' {
|
||||
cmd_list_execute(mut agent, isscript)!
|
||||
}
|
||||
'generate' {
|
||||
cmd_generate_execute(mut agent, name, load)!
|
||||
}
|
||||
'load' {
|
||||
cmd_load_execute(mut agent, name)!
|
||||
}
|
||||
'forget' {
|
||||
cmd_forget_execute(mut agent, name)!
|
||||
}
|
||||
'reset' {
|
||||
cmd_reset_execute(mut agent, isscript)!
|
||||
}
|
||||
'add' {
|
||||
cmd_add_execute(mut agent, name)!
|
||||
}
|
||||
'status' {
|
||||
cmd_status_execute(mut agent)!
|
||||
}
|
||||
'push' {
|
||||
cmd_push_execute(mut agent, target, key)!
|
||||
}
|
||||
'auth' {
|
||||
cmd_auth_execute(mut agent, target, key)!
|
||||
}
|
||||
else {
|
||||
return error(cmd.help_message())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Profile command - comprehensive SSH agent initialization
|
||||
fn cmd_profile_execute(mut agent sshagent.SSHAgent, isscript bool) ! {
|
||||
console.print_header('🔑 Hero SSH Agent Profile Initialization')
|
||||
|
||||
// Ensure single agent instance
|
||||
agent.ensure_single_agent()!
|
||||
console.print_green('✓ SSH agent instance verified')
|
||||
|
||||
// Smart key loading
|
||||
available_keys := agent.keys
|
||||
loaded_keys := agent.keys_loaded()!
|
||||
|
||||
console.print_debug('Found ${available_keys.len} available keys, ${loaded_keys.len} loaded')
|
||||
|
||||
// If only one key and none loaded, auto-load it
|
||||
if available_keys.len == 1 && loaded_keys.len == 0 {
|
||||
key_name := available_keys[0].name
|
||||
console.print_debug('Auto-loading single key: ${key_name}')
|
||||
|
||||
mut key := agent.get(name: key_name) or {
|
||||
console.print_stderr('Failed to get key: ${err}')
|
||||
return
|
||||
}
|
||||
|
||||
key.load() or { console.print_debug('Key loading failed (may need passphrase): ${err}') }
|
||||
}
|
||||
|
||||
// Update shell profile
|
||||
update_shell_profile()!
|
||||
|
||||
console.print_green('✅ SSH agent profile initialized successfully')
|
||||
cmd_status_execute(mut agent)!
|
||||
}
|
||||
|
||||
// Update shell profile with SSH agent initialization
|
||||
fn update_shell_profile() ! {
|
||||
home := os.home_dir()
|
||||
ssh_dir := '${home}/.ssh'
|
||||
socket_path := '${ssh_dir}/hero-agent.sock'
|
||||
|
||||
// Find appropriate profile file
|
||||
profile_candidates := [
|
||||
'${home}/.profile',
|
||||
'${home}/.bash_profile',
|
||||
'${home}/.bashrc',
|
||||
'${home}/.zshrc',
|
||||
]
|
||||
|
||||
mut profile_file := '${home}/.profile'
|
||||
for candidate in profile_candidates {
|
||||
if os.exists(candidate) {
|
||||
profile_file = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
profile_content := if os.exists(profile_file) {
|
||||
os.read_file(profile_file)!
|
||||
} else {
|
||||
''
|
||||
}
|
||||
|
||||
hero_init_block := '
|
||||
# Hero SSH Agent initialization
|
||||
if [ -f "${socket_path}" ]; then
|
||||
export SSH_AUTH_SOCK="${socket_path}"
|
||||
fi'
|
||||
|
||||
// Check if already present
|
||||
if profile_content.contains('Hero SSH Agent initialization') {
|
||||
console.print_debug('Hero initialization already present in profile')
|
||||
return
|
||||
}
|
||||
|
||||
// Add hero initialization
|
||||
updated_content := profile_content + hero_init_block
|
||||
os.write_file(profile_file, updated_content)!
|
||||
|
||||
console.print_green('✓ Updated shell profile: ${profile_file}')
|
||||
}
|
||||
|
||||
// List command
|
||||
fn cmd_list_execute(mut agent sshagent.SSHAgent, isscript bool) ! {
|
||||
if !isscript {
|
||||
console.clear()
|
||||
}
|
||||
console.print_debug(agent.str())
|
||||
} else if cmd.name == 'generate' {
|
||||
agent.generate(name, '')!
|
||||
if load {
|
||||
agent.load(name)!
|
||||
|
||||
console.print_header('SSH Keys Status')
|
||||
println(agent.str())
|
||||
|
||||
loaded_keys := agent.keys_loaded()!
|
||||
if loaded_keys.len > 0 {
|
||||
console.print_header('Currently Loaded Keys:')
|
||||
for key in loaded_keys {
|
||||
console.print_item('- ${key.name} (${key.cat})')
|
||||
}
|
||||
} else if cmd.name == 'load' {
|
||||
agent.load(name)!
|
||||
} else if cmd.name == 'forget' {
|
||||
agent.forget(name)!
|
||||
} else if cmd.name == 'reset' {
|
||||
agent.reset()!
|
||||
} else if cmd.name == 'add' {
|
||||
panic("can't work, no support for multiline yet")
|
||||
mut myui := ui.new()!
|
||||
privkey := myui.ask_question(
|
||||
question: 'private key of your ssh key'
|
||||
)!
|
||||
agent.add(name, privkey)!
|
||||
} else {
|
||||
// console.print_debug(1)
|
||||
return error(cmd.help_message())
|
||||
// console.print_debug(" Supported commands are: ${gentools.gencmds}")
|
||||
// return error('unknown subcmd')
|
||||
console.print_debug('No keys currently loaded in agent')
|
||||
}
|
||||
}
|
||||
|
||||
// Generate command
|
||||
fn cmd_generate_execute(mut agent sshagent.SSHAgent, name string, load bool) ! {
|
||||
if name == '' {
|
||||
return error('Key name is required for generate command')
|
||||
}
|
||||
|
||||
console.print_debug('Generating SSH key: ${name}')
|
||||
mut key := agent.generate(name, '')!
|
||||
console.print_green('✓ Generated SSH key: ${name}')
|
||||
|
||||
if load {
|
||||
console.print_debug('Loading key into agent...')
|
||||
key.load() or {
|
||||
console.print_stderr('Failed to load key: ${err}')
|
||||
return
|
||||
}
|
||||
console.print_green('✓ Key loaded into agent')
|
||||
}
|
||||
}
|
||||
|
||||
// Load command
|
||||
fn cmd_load_execute(mut agent sshagent.SSHAgent, name string) ! {
|
||||
if name == '' {
|
||||
return error('Key name is required for load command')
|
||||
}
|
||||
|
||||
mut key := agent.get(name: name) or { return error('SSH key "${name}" not found') }
|
||||
|
||||
console.print_debug('Loading SSH key: ${name}')
|
||||
key.load()!
|
||||
console.print_green('✓ SSH key "${name}" loaded successfully')
|
||||
}
|
||||
|
||||
// Forget command
|
||||
fn cmd_forget_execute(mut agent sshagent.SSHAgent, name string) ! {
|
||||
if name == '' {
|
||||
return error('Key name is required for forget command')
|
||||
}
|
||||
|
||||
console.print_debug('Removing SSH key from agent: ${name}')
|
||||
agent.forget(name)!
|
||||
console.print_green('✓ SSH key "${name}" removed from agent')
|
||||
}
|
||||
|
||||
// Reset command
|
||||
fn cmd_reset_execute(mut agent sshagent.SSHAgent, isscript bool) ! {
|
||||
if !isscript {
|
||||
print('This will remove all loaded SSH keys. Continue? (y/N): ')
|
||||
input := os.input('')
|
||||
if input.trim_space().to_lower() != 'y' {
|
||||
console.print_debug('Reset cancelled')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
console.print_debug('Resetting SSH agent - removing all keys')
|
||||
agent.reset()!
|
||||
console.print_green('✓ All SSH keys removed from agent')
|
||||
}
|
||||
|
||||
// Add command
|
||||
fn cmd_add_execute(mut agent sshagent.SSHAgent, name string) ! {
|
||||
if name == '' {
|
||||
return error('Key name is required for add command')
|
||||
}
|
||||
|
||||
mut myui := ui.new()!
|
||||
privkey := myui.ask_question(
|
||||
question: 'Enter the private key content:'
|
||||
)!
|
||||
|
||||
console.print_debug('Adding SSH key: ${name}')
|
||||
agent.add(name, privkey)!
|
||||
console.print_green('✓ SSH key "${name}" added successfully')
|
||||
}
|
||||
|
||||
// Status command
|
||||
fn cmd_status_execute(mut agent sshagent.SSHAgent) ! {
|
||||
console.print_header('SSH Agent Status')
|
||||
|
||||
diag := agent.diagnostics()
|
||||
for key, value in diag {
|
||||
console.print_item('${key}: ${value}')
|
||||
}
|
||||
|
||||
loaded_keys := agent.keys_loaded()!
|
||||
if loaded_keys.len > 0 {
|
||||
console.print_header('Loaded Keys:')
|
||||
for key in loaded_keys {
|
||||
console.print_item('- ${key.name} (${key.cat})')
|
||||
}
|
||||
} else {
|
||||
console.print_debug('No keys currently loaded')
|
||||
}
|
||||
}
|
||||
|
||||
// Push command - deploy SSH key to remote machine
|
||||
fn cmd_push_execute(mut agent sshagent.SSHAgent, target string, key_name string) ! {
|
||||
if target == '' {
|
||||
return error('Target is required for push command (format: user@hostname[:port])')
|
||||
}
|
||||
|
||||
console.print_header('🚀 SSH Key Deployment')
|
||||
|
||||
// Parse target
|
||||
parsed_target := parse_target(target)!
|
||||
console.print_debug('Target: ${parsed_target.user}@${parsed_target.hostname}:${parsed_target.port}')
|
||||
|
||||
// Select key to deploy
|
||||
mut selected_key := select_key_for_deployment(mut agent, key_name)!
|
||||
console.print_debug('Selected key: ${selected_key.name}')
|
||||
|
||||
// Deploy key
|
||||
deploy_key_to_target(mut selected_key, parsed_target)!
|
||||
|
||||
console.print_green('✅ SSH key deployed successfully to ${target}')
|
||||
}
|
||||
|
||||
// Auth command - verify SSH key authorization
|
||||
fn cmd_auth_execute(mut agent sshagent.SSHAgent, target string, key_name string) ! {
|
||||
if target == '' {
|
||||
return error('Target is required for auth command (format: user@hostname[:port])')
|
||||
}
|
||||
|
||||
console.print_header('🔐 SSH Key Authorization Verification')
|
||||
|
||||
// Parse target
|
||||
parsed_target := parse_target(target)!
|
||||
|
||||
// Select key to verify
|
||||
mut selected_key := select_key_for_deployment(mut agent, key_name)!
|
||||
|
||||
// Verify authorization
|
||||
verify_key_authorization(mut selected_key, parsed_target)!
|
||||
|
||||
console.print_green('✅ SSH key authorization verified for ${target}')
|
||||
}
|
||||
|
||||
// Helper structures and functions for remote operations
|
||||
struct RemoteTarget {
|
||||
mut:
|
||||
user string
|
||||
hostname string
|
||||
port int = 22
|
||||
}
|
||||
|
||||
// Parse target string in format user@hostname[:port]
|
||||
fn parse_target(target_str string) !RemoteTarget {
|
||||
if !target_str.contains('@') {
|
||||
return error('Target must be in format user@hostname[:port]')
|
||||
}
|
||||
|
||||
parts := target_str.split('@')
|
||||
if parts.len != 2 {
|
||||
return error('Invalid target format: ${target_str}')
|
||||
}
|
||||
|
||||
user := parts[0]
|
||||
mut hostname := parts[1]
|
||||
mut port := 22
|
||||
|
||||
// Check for port specification
|
||||
if hostname.contains(':') {
|
||||
host_port := hostname.split(':')
|
||||
if host_port.len != 2 {
|
||||
return error('Invalid hostname:port format: ${hostname}')
|
||||
}
|
||||
hostname = host_port[0]
|
||||
port = host_port[1].int()
|
||||
}
|
||||
|
||||
return RemoteTarget{
|
||||
user: user
|
||||
hostname: hostname
|
||||
port: port
|
||||
}
|
||||
}
|
||||
|
||||
// Select appropriate key for deployment
|
||||
fn select_key_for_deployment(mut agent sshagent.SSHAgent, key_name string) !sshagent.SSHKey {
|
||||
available_keys := agent.keys
|
||||
|
||||
if available_keys.len == 0 {
|
||||
return error('No SSH keys found. Generate a key first with: hero sshagent generate <name>')
|
||||
}
|
||||
|
||||
// If specific key requested
|
||||
if key_name.len > 0 {
|
||||
for key in available_keys {
|
||||
if key.name == key_name {
|
||||
return key
|
||||
}
|
||||
}
|
||||
return error('SSH key "${key_name}" not found')
|
||||
}
|
||||
|
||||
// Auto-select if only one key
|
||||
if available_keys.len == 1 {
|
||||
console.print_debug('Auto-selecting single available key: ${available_keys[0].name}')
|
||||
return available_keys[0]
|
||||
}
|
||||
|
||||
// Interactive selection for multiple keys
|
||||
return interactive_key_selection(available_keys)!
|
||||
}
|
||||
|
||||
// Interactive key selection when multiple keys are available
|
||||
fn interactive_key_selection(keys []sshagent.SSHKey) !sshagent.SSHKey {
|
||||
console.print_header('Multiple SSH keys available:')
|
||||
|
||||
for i, key in keys {
|
||||
console.print_item('${i + 1}. ${key.name} (${key.cat})')
|
||||
}
|
||||
|
||||
print('Select key number (1-${keys.len}): ')
|
||||
input := os.input('')
|
||||
|
||||
selection := input.trim_space().int() - 1
|
||||
if selection < 0 || selection >= keys.len {
|
||||
return error('Invalid selection: ${input}')
|
||||
}
|
||||
|
||||
return keys[selection]
|
||||
}
|
||||
|
||||
// Deploy key to remote target
|
||||
fn deploy_key_to_target(mut key sshagent.SSHKey, target RemoteTarget) ! {
|
||||
console.print_debug('Deploying key ${key.name} to ${target.user}@${target.hostname}')
|
||||
|
||||
// Get public key content
|
||||
pub_key_content := key.keypub()!
|
||||
|
||||
// Use ssh-copy-id if available, otherwise manual deployment
|
||||
if has_ssh_copy_id() {
|
||||
deploy_with_ssh_copy_id(mut key, target)!
|
||||
} else {
|
||||
deploy_manually(pub_key_content, target)!
|
||||
}
|
||||
}
|
||||
|
||||
// Check if ssh-copy-id is available
|
||||
fn has_ssh_copy_id() bool {
|
||||
result := os.execute('which ssh-copy-id')
|
||||
return result.exit_code == 0
|
||||
}
|
||||
|
||||
// Deploy using ssh-copy-id
|
||||
fn deploy_with_ssh_copy_id(mut key sshagent.SSHKey, target RemoteTarget) ! {
|
||||
mut key_path := key.keypath()!
|
||||
|
||||
mut cmd := 'ssh-copy-id -i ${key_path.path}'
|
||||
if target.port != 22 {
|
||||
cmd += ' -p ${target.port}'
|
||||
}
|
||||
cmd += ' ${target.user}@${target.hostname}'
|
||||
|
||||
console.print_debug('Executing: ${cmd}')
|
||||
result := os.execute(cmd)
|
||||
|
||||
if result.exit_code != 0 {
|
||||
return error('ssh-copy-id failed: ${result.output}')
|
||||
}
|
||||
}
|
||||
|
||||
// Manual deployment by appending to authorized_keys
|
||||
fn deploy_manually(pub_key_content string, target RemoteTarget) ! {
|
||||
mut ssh_cmd := 'ssh'
|
||||
if target.port != 22 {
|
||||
ssh_cmd += ' -p ${target.port}'
|
||||
}
|
||||
|
||||
// Command to append key to authorized_keys
|
||||
remote_cmd := 'mkdir -p ~/.ssh && echo "${pub_key_content}" >> ~/.ssh/authorized_keys && chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys'
|
||||
|
||||
full_cmd := '${ssh_cmd} ${target.user}@${target.hostname} "${remote_cmd}"'
|
||||
|
||||
console.print_debug('Executing manual deployment')
|
||||
result := os.execute(full_cmd)
|
||||
|
||||
if result.exit_code != 0 {
|
||||
return error('Manual key deployment failed: ${result.output}')
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that key is properly authorized on remote target
|
||||
fn verify_key_authorization(mut key sshagent.SSHKey, target RemoteTarget) ! {
|
||||
console.print_debug('Verifying key authorization for ${key.name}')
|
||||
|
||||
// Test SSH connection
|
||||
mut ssh_cmd := 'ssh -o BatchMode=yes -o ConnectTimeout=10'
|
||||
if target.port != 22 {
|
||||
ssh_cmd += ' -p ${target.port}'
|
||||
}
|
||||
ssh_cmd += ' ${target.user}@${target.hostname} "echo SSH_CONNECTION_SUCCESS"'
|
||||
|
||||
console.print_debug('Testing SSH connection...')
|
||||
result := os.execute(ssh_cmd)
|
||||
|
||||
if result.exit_code != 0 {
|
||||
return error('SSH connection failed: ${result.output}')
|
||||
}
|
||||
|
||||
if !result.output.contains('SSH_CONNECTION_SUCCESS') {
|
||||
return error('SSH connection test failed - unexpected output')
|
||||
}
|
||||
|
||||
console.print_green('✓ SSH key is properly authorized and working')
|
||||
}
|
||||
|
||||
@@ -34,9 +34,18 @@ pub fn (mut agent SSHAgent) ensure_single_agent() ! {
|
||||
agent.active = true
|
||||
}
|
||||
|
||||
// get consistent socket path per user
|
||||
// get consistent socket path per user in home directory
|
||||
fn get_agent_socket_path(user string) string {
|
||||
return '/tmp/ssh-agent-${user}.sock'
|
||||
home := os.home_dir()
|
||||
ssh_dir := '${home}/.ssh'
|
||||
|
||||
// Ensure SSH directory exists with correct permissions
|
||||
if !os.exists(ssh_dir) {
|
||||
os.mkdir_all(ssh_dir) or { return '/tmp/ssh-agent-${user}.sock' }
|
||||
os.chmod(ssh_dir, 0o700) or {}
|
||||
}
|
||||
|
||||
return '${ssh_dir}/hero-agent.sock'
|
||||
}
|
||||
|
||||
// check if current agent is responsive
|
||||
|
||||
Reference in New Issue
Block a user