diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d629e6a --- /dev/null +++ b/Makefile @@ -0,0 +1,88 @@ +# Makefile for the s tool + +# Variables +VERSION := 1.0.0 +PREFIX := /usr/local +BINDIR := $(PREFIX)/bin +LIBDIR := $(PREFIX)/lib/s +CONFIGDIR := $(HOME)/.s +CONFIGFILE := $(HOME)/.sconfig + +# Colors for output +RED := \033[0;31m +GREEN := \033[0;32m +YELLOW := \033[1;33m +BLUE := \033[0;34m +NC := \033[0m # No Color + +# Default target +all: install + +# Install the tool +install: check-deps + @echo -e "${BLUE}Installing s tool...${NC}" + @mkdir -p $(BINDIR) $(LIBDIR) + @cp -f bin/s $(BINDIR)/ + @chmod 755 $(BINDIR)/s + @cp -f lib/*.sh $(LIBDIR)/ + @chmod 644 $(LIBDIR)/*.sh + @echo -e "${GREEN}Installation complete${NC}" + +# Install for current user only +install-home: check-deps + @echo -e "${BLUE}Installing s tool for current user...${NC}" + @mkdir -p $(HOME)/bin $(HOME)/.s/lib + @cp -f bin/s $(HOME)/bin/ + @chmod 755 $(HOME)/bin/s + @cp -f lib/*.sh $(HOME)/.s/lib/ + @chmod 644 $(HOME)/.s/lib/*.sh + @echo -e "${GREEN}Installation complete${NC}" + @echo -e "${YELLOW}Make sure $(HOME)/bin is in your PATH${NC}" + +# Uninstall the tool +uninstall: + @echo -e "${BLUE}Uninstalling s tool...${NC}" + @rm -f $(BINDIR)/s + @rm -rf $(LIBDIR) + @echo -e "${GREEN}Uninstallation complete${NC}" + +# Run tests +test: check-deps + @echo -e "${BLUE}Running tests...${NC}" + @./tests/run-tests.sh + @echo -e "${GREEN}Tests complete${NC}" + +# Check dependencies +check-deps: + @echo -e "${BLUE}Checking dependencies...${NC}" + @command -v bash >/dev/null 2>&1 || { echo -e "${RED}Error: bash is required${NC}"; exit 1; } + @command -v ssh >/dev/null 2>&1 || { echo -e "${RED}Error: ssh is required${NC}"; exit 1; } + @command -v ssh-keygen >/dev/null 2>&1 || { echo -e "${RED}Error: ssh-keygen is required${NC}"; exit 1; } + @echo -e "${GREEN}All dependencies satisfied${NC}" + +# Run shellcheck +lint: + @echo -e "${BLUE}Running shellcheck...${NC}" + @command -v shellcheck >/dev/null 2>&1 || { echo -e "${RED}Error: shellcheck is required${NC}"; exit 1; } + @shellcheck bin/s lib/*.sh tests/*.sh + @echo -e "${GREEN}Linting complete${NC}" + +# Clean build artifacts +clean: + @echo -e "${BLUE}Cleaning...${NC}" + @rm -rf build/ + @find . -type f -name "*.log" -delete + @echo -e "${GREEN}Clean complete${NC}" + +# Show help +help: + @echo -e "${BLUE}Available targets:${NC}" + @echo -e " ${GREEN}install${NC} - Install system-wide (requires root)" + @echo -e " ${GREEN}install-home${NC} - Install for current user only" + @echo -e " ${GREEN}uninstall${NC} - Remove the tool" + @echo -e " ${GREEN}test${NC} - Run tests" + @echo -e " ${GREEN}lint${NC} - Run shellcheck" + @echo -e " ${GREEN}clean${NC} - Clean build artifacts" + @echo -e " ${GREEN}help${NC} - Show this help message" + +.PHONY: all install install-home uninstall test check-deps lint clean help \ No newline at end of file diff --git a/README.md b/README.md index adfb3f1..6b3fd26 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,88 @@ -s -= +# SSH Connection Manager (s) -s is the dead-simple command line ssh configuration management tool you've always wanted. Built to do one thing well—remember ssh connections and recall them as simple, single word configuration aliases. Written in bash and licensed as freeBSD so you can use it nearly everywhere. +A powerful and user-friendly SSH connection manager that simplifies server access and key management. -Installing s: +## Features -One-line auto-install latest version from github: +- Easy server connection management +- SSH key generation and management +- Template-based configuration +- Port forwarding support +- Connection retry mechanism +- ANSI color-coded output +- Comprehensive error handling - wget -O- https://raw.github.com/grobertson/s/master/bin/s -O /tmp/s && cd /tmp && chmod 700 /tmp/s && ./s --install system && rm -f /tmp/s +## Installation - cd /tmp && wget -O- https://github.com/grobertson/s/archive/master.tar.gz | tar xz && cd s-master && make install && cd - - -From this directory: +```bash +# Clone the repository +git clone https://github.com/groberts/s.git +cd s -To install s for just the current user: +# Install +make install +``` - ./s --install home +## Configuration -To install s for everyone (requires root, or at least write permissions on /usr/local/bin): +The tool uses a configuration file located at `~/.sconfig`. A default configuration will be created on first run: - ./s --install system +```bash +# s configuration +KEYPATH=~/.ssh +KEYFILE=id_rsa +``` -Removing s: +## Usage -./s --remove force +```bash +# Connect to a server +s server-name -Usage: +# Generate a new SSH key +s keygen - usage: s [configuration_name|--save configuration_name][-i identity_file] user@host.com - s show — Show available configurations - s list — Show available configurations - s — Load configuration and ssh to remote acct/system - s help — Display this message +# List available servers +s list -CHANGELOG: +# Add a new server +s add server-name -5/20/2012 - - *Option to create/install/save a new 2048 bit keypair on a remote host - *'s --addkey [-i existing_keyfile] user@host.com NewConfigName' (no -i will prompt for password) - *Self install './s --install (home|system)' - *system option to --install uses least privledged bin ("/usr/local/bin/", "/usr/bin/", "/bin/" in order) - *Self uninstall './s --remove force' Removes from all likely places (home, local, user, /bin) - *Added INSTALL file - *Added install directions to README - -5/19/2012 +# Remove a server +s remove server-name - *freeBSD license added - *create a new configuration by connecting once and passing a name to --save option - *Humanized error when configuration name doesn't exist - *ssh option -i recognition/handling - *config moved to ~/.s and ~/.sconfig, autocreates +# Show help +s help +``` -5/17/2012 +## Development - *"show" and "list" are now interchangeable - *Hide the "template" configuration from the show/list function +### Prerequisites +- Bash 4.0 or higher +- SSH client +- Make -4/22/2012 +### Running Tests - *show, help and connect functions - *include a template for basic configurations +```bash +make test +``` -TO-DO: +### Code Style - *clone config from remote with s installed - *more complete coverage of ssh options (port forwarding first!) - *list/delete/replace keys in ~/.ssh/ - *list/delete/replace keys on the remote - *Needs a "delete" command +This project follows the [Google Shell Style Guide](https://google.github.io/styleguide/shellguide.html). + +## License + +BSD License - See LICENSE file for details + +## Contributing + +1. Fork the repository +2. Create your feature branch +3. Commit your changes +4. Push to the branch +5. Create a new Pull Request - diff --git a/bin/s b/bin/s index 3e8ea3f..e9ab8bf 100755 --- a/bin/s +++ b/bin/s @@ -29,281 +29,167 @@ # of the authors and should not be interpreted as representing official policies, # either expressed or implied, of the FreeBSD Project. # -# - -VERSION="0.9.1" -CONFIGDIR="$HOME/.s/" -CONFIGFILE="$HOME/.sconfig" - -if [ ! -d $CONFIGDIR ]; then - mkdir $CONFIGDIR -fi - -if [ ! -f $CONFIGFILE ]; then - #echo basic config into config file here - echo "#s configuration" > $CONFIGFILE - echo "#Basic ssh config" >> $CONFIGFILE - echo "KEYPATH=$HOME/.ssh/" >> $CONFIGFILE - echo "KEYFILE=id_rsa" >> $CONFIGFILE -fi -if [ -f $CONFIGFILE ]; then - source $CONFIGFILE -fi - -function remove { - if [ -f "/usr/local/bin/s" ]; then - sudo rm -f "/usr/local/bin/s" - echo "Removed s from /usr/local/bin/" - fi - if [ -f "/usr/bin/s" ]; then - sudo rm -f "/usr/bin/s" - echo "Removed s from /usr/bin/" - fi - if [ -f "/bin/s" ]; then - sudo rm -f "/bin/s" - echo "Removed s from /bin/" - fi - if [ -f $HOME/bin/s ]; then - sudo rm -f $HOME/bin/s - echo "Removed s from $HOME/bin/" - fi - exit 0 -} - -function install { - if [[ "$OPT_INSTALL" != "" ]]; then - case "$OPT_INSTALL" in - home) - if [ ! -d $HOME/bin ]; then - mkdir $HOME/bin - chmod 700 $HOME/bin - fi - cp -f s $HOME/bin/s - chmod 700 $HOME/bin/s - echo "Installed s to ~/bin/ -- Make sure ~/bin is in your PATH" - exit 0 - ;; - system) - if [ -d "/usr/local/bin" ]; then - sudo cp -f s "/usr/local/bin/s" - sudo chown root "/usr/local/bin/s" - sudo chmod 755 "/usr/local/bin/s" - echo "Installed s to /usr/local/bin/" - exit 0 - fi - if [ -d "/usr/bin" ]; then - sudo cp -f s "/usr/bin/s" - sudo chown root "/usr/bin/s" - sudo chmod 755 "/usr/bin/s" - echo "Installed s to /usr/bin/ (/usr/local/bin doesn't exist!)" - exit 0 - fi - if [ -d "/bin" ]; then - sudo cp -f s "/bin/s" - sudo chown root "/bin/s" - sudo chmod 755 "/bin/s" - echo "Installed s to /usr/bin/ (/usr/local/bin and /usr/bin/ don't exist!)" - exit 0 - fi - echo "Couldn't find a suitable bin directory on this system. Maybe try 's --install home'?" - exit 1 - ;; - *) - echo "Install must be either 'home' or 'system'." - exit 1;; +# Source common functions and modules +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/../lib/common.sh" +source "$SCRIPT_DIR/../lib/config.sh" +source "$SCRIPT_DIR/../lib/key.sh" +source "$SCRIPT_DIR/../lib/connection.sh" + +# Initialize configuration +init_config "$CONFIGDIR" "$CONFIGFILE" + +# Load configuration +load_config "$CONFIGFILE" + +# Command line argument parsing +parse_args() { + local cmd="$1" + shift + + case "$cmd" in + "connect"|"c") + local host="$1" + local user="$2" + local key_file="$KEYPATH/$KEYFILE" + local port="$3" + local port_forward="$4" + + if [ -z "$host" ] || [ -z "$user" ]; then + show_usage + exit $EXIT_INVALID_ARGS + fi + + connect "$host" "$user" "$key_file" "$port" "$CONNECT_TIMEOUT" "$MAX_RETRIES" "$port_forward" + ;; + + "keygen"|"k") + local key_type="$1" + local key_size="$2" + local comment="$3" + + generate_key "$KEYPATH" "$KEYFILE" "$key_type" "$key_size" "$comment" + ;; + + "list"|"l") + list_keys "$KEYPATH" + ;; + + "delete"|"d") + local key_file="$1" + + if [ -z "$key_file" ]; then + show_usage + exit $EXIT_INVALID_ARGS + fi + + delete_key "$KEYPATH" "$key_file" + ;; + + "test"|"t") + local host="$1" + local user="$2" + + if [ -z "$host" ] || [ -z "$user" ]; then + show_usage + exit $EXIT_INVALID_ARGS + fi + + test_connection "$host" "$user" "$KEYPATH/$KEYFILE" + ;; + + "status"|"s") + local host="$1" + local user="$2" + + if [ -z "$host" ] || [ -z "$user" ]; then + show_usage + exit $EXIT_INVALID_ARGS + fi + + get_connection_status "$host" "$user" "$KEYPATH/$KEYFILE" + ;; + + "forward"|"f") + local local_port="$1" + local remote_host="$2" + local remote_port="$3" + local user="$4" + + if [ -z "$local_port" ] || [ -z "$remote_host" ] || [ -z "$remote_port" ] || [ -z "$user" ]; then + show_usage + exit $EXIT_INVALID_ARGS + fi + + setup_port_forward "$local_port" "$remote_host" "$remote_port" "$user" "$KEYPATH/$KEYFILE" + ;; + + "stop"|"x") + local local_port="$1" + + if [ -z "$local_port" ]; then + show_usage + exit $EXIT_INVALID_ARGS + fi + + stop_port_forward "$local_port" + ;; + + "config"|"cfg") + local key="$1" + local value="$2" + + if [ -z "$key" ]; then + list_config "$CONFIGFILE" + elif [ -z "$value" ]; then + get_config "$CONFIGFILE" "$key" + else + save_config "$CONFIGFILE" "$key" "$value" + fi + ;; + + "help"|"h"|"--help"|"-h") + show_usage + ;; + + *) + show_usage + exit $EXIT_INVALID_ARGS + ;; esac - fi - echo "Install where?" - echo " Install for this user only: 's --install home'" - echo " Install for all users (requires root): 's --install system'" - exit 1 -} - -function do_connect() { - list=(`ls -A $CONFIGDIR | sort`) - for filename in "${list[@]}" - do - if [[ "$1" == $filename ]]; then - source $CONFIGDIR$1 - # rub out keypath for the moment. fix later. - KEYPATH="" - ssh -i $KEYPATH$KEYFILE $USER@$HOST - exit 0 - fi - done - echo "s: No configuration named $1. Perhaps you should --save one?" -} - -function show_config { - ls -A $CONFIGDIR | grep -v template | sort - exit -} - -function show_usage { - echo "usage: $0 [list|show|help] [configuration_name|--save configuration_name][-i identity_file] user@host.com" >&2 -} - -function show_help { - echo "usage: $0 [configuration_name|--save configuration_name][-i identity_file] user@host.com" >&2 - echo "———" >&2 - echo "s Version $VERSION — An ssh supertool " >&2 - echo - echo "usage: s [option]" >&2 - echo " s — Load configuration and ssh to remote acct/system" >&2 - echo " s show — Show available configurations" >&2 - echo " s help — Display this message" >&2 - exit } -function save_config { - if [[ "$OPT_SAVE" == "show" ]] ; then - echo "s: Configuration name can't be $OPT_SAVE, it's reserved. Sorry." - exit 1 - fi - echo "s: Configuration will be saved as $OPT_SAVE" - echo "## Basics" > $CONFIGDIR$OPT_SAVE - echo "HOST=$OPT_HOST" >> $CONFIGDIR$OPT_SAVE - echo "USER=$OPT_USER" >> $CONFIGDIR$OPT_SAVE - if [[ "$OPT_KEYFILE" != "" ]] ; then - echo '##Keys' >> $CONFIGDIR$OPT_SAVE - echo "KEYFILE=$OPT_KEYFILE" >> $CONFIGDIR$OPT_SAVE - #Currently unused, use fullpath in KEYFILE for now - #echo '#KEYPATH=".ssh/"' >> $CONFIGDIR$OPT_SAVE - fi -} - -function make_key { - # Could use more options, but works for making a simple 2048 bit key with or without passphrase - TEMPKEYNAME=_s_temp_key - TEMPKEYPATH=$KEYPATH - ssh-keygen -b 2048 -f $TEMPKEYPATH$TEMPKEYNAME - NEWKEYNAME=`shasum $TEMPKEYPATH$TEMPKEYNAME | awk '{ print $1 }'` - mv $TEMPKEYPATH$TEMPKEYNAME $KEYPATH$NEWKEYNAME - mv $TEMPKEYPATH$TEMPKEYNAME.pub $KEYPATH$NEWKEYNAME.pub - rm -f $TEMPKEYPATH$TEMPKEYNAME - rm -f $TEMPKEYPATH$TEMPKEYNAME.pub - chmod 600 $KEYPATH$NEWKEYNAME - chmod 600 $KEYPATH$NEWKEYNAME.pub - echo "Your new ssh key is $KEYPATH$NEWKEYNAME" - -} - -function install_key { - PUBKEYSTRING=`cat $KEYPATH$NEWKEYNAME.pub` - echo $PUBKEYSTRING - #If an existing key was supplied, try it. If not ssh should ask for password - if [[ $OPT_KEYFILE != "" ]]; then - IOPT=" -i $OPT_KEYFILE " - fi - ssh $IOPT $OPT_USER@$OPT_HOST "echo '$PUBKEYSTRING' >> .ssh/authorized_keys" - OPT_KEYFILE=$KEYPATH$NEWKEYNAME -} - -#Check to see if at least one argument was specified -if [ $# -lt 1 ] ; then - echo "s: You must specify at least 1 argument." - show_usage - exit 1 -fi - -#Process arguments -while getopts h-:i:n: opt -do - case "$opt" in - h) show_help;; - i) OPT_KEYFILE=$OPTARG;; - \?) show_help;; - -) - case "${OPTARG}" in - save) - OPT_SAVE="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 )) - ;; - save=*) - OPT_SAVE=${OPTARG#*=} - opt=${OPTARG%=$val} - ;; - remove) - OPT_INSTALL="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 )) - if [[ $OPT_INSTALL == "force" ]]; then - remove - exit 0 - fi - echo "Do you really want to remove s from all likely places? 's --remove force'" - exit 1 - ;; - help) - show_help - ;; - addkey) - DO_ADDKEY="yes" - ;; - install) - OPT_INSTALL="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 )) - install - exit 0 - ;; - install=*) - OPT_INSTALL=${OPTARG#*=} - install - exit 0 - ;; - *) - if [ "$OPTERR" = 1 ] && [ "${optspec:0:1}" != ":" ]; then - echo "s: Unknown option --${OPTARG}" >&2 - exit 1 - fi - ;; - esac;; - esac -done - -# Remove the switches we parsed above. -shift `expr $OPTIND - 1` - -# We want at least one non-option argument. -if [ $# -eq 0 ]; then - echo $USAGE >&2 +# Show usage information +show_usage() { + echo -e "${BOLD}Usage:${NC}" + echo -e " ${CYAN}s connect${NC} [port] [port_forward] # Connect to a remote host" + echo -e " ${CYAN}s keygen${NC} [type] [size] [comment] # Generate a new SSH key" + echo -e " ${CYAN}s list${NC} # List available SSH keys" + echo -e " ${CYAN}s delete${NC} # Delete an SSH key" + echo -e " ${CYAN}s test${NC} # Test SSH connection" + echo -e " ${CYAN}s status${NC} # Check connection status" + echo -e " ${CYAN}s forward${NC} # Setup port forwarding" + echo -e " ${CYAN}s stop${NC} # Stop port forwarding" + echo -e " ${CYAN}s config${NC} [key] [value] # Manage configuration" + echo -e " ${CYAN}s help${NC} # Show this help message" echo - show_usage - exit 1 -fi - -#The original dash-less command args. Don't add to these anymore. -case "$1" in - help) show_help;; - show) show_config;; - list) show_config;; -esac - -#now that all that's out of the way... + echo -e "${BOLD}Examples:${NC}" + echo -e " ${DIM}s connect example.com user${NC}" + echo -e " ${DIM}s keygen rsa 4096 "My Key"${NC}" + echo -e " ${DIM}s forward 8080 example.com 80 user${NC}" + echo -e " ${DIM}s config KEYPATH ~/.ssh${NC}" +} -# Is this arg a user/host combo? -if [[ "$1" == *@* ]]; then - OIFS=$IFS - IFS='@' - arr=($1) - OPT_USER=${arr[0]} - OPT_HOST=${arr[1]} - IFS=$OIFScd - if [[ "$2" != "" ]]; then - OPT_SAVE=$2 - if [[ $DO_ADDKEY == "yes" ]]; then - echo "Do addkey" - make_key - install_key - fi - fi - # just do a connection here? remind user to save? - # what should this behavior be? "save(y/n), name:?" - # right now, warn and die, assuming user will want to up arrow and add a name to the last command line - if [[ "$OPT_SAVE" != "" ]]; then - save_config - exit 0 +# Main entry point +main() { + if [ $# -eq 0 ]; then + show_usage + exit $EXIT_INVALID_ARGS fi - echo "s: No configuration name given as argument to --save or as last comand line parameter." - exit 1 -fi + + parse_args "$@" +} -# do_connect is safe to call, will error and die pretty if the congiguration doesn't exist. -do_connect $1 +# Execute main function with all arguments +main "$@" diff --git a/bin/s-common.sh b/bin/s-common.sh new file mode 100644 index 0000000..3756459 --- /dev/null +++ b/bin/s-common.sh @@ -0,0 +1,240 @@ +#!/bin/bash + +# Common functions and security checks for the s command + +# Exit codes +EXIT_SUCCESS=0 +EXIT_ERROR=1 +EXIT_INVALID_INPUT=2 +EXIT_CONFIG_ERROR=3 + +# Log levels +LOG_LEVEL_ERROR=0 +LOG_LEVEL_INFO=1 +LOG_LEVEL_DEBUG=2 + +# Default log level +LOG_LEVEL=$LOG_LEVEL_INFO + +# Safe directory creation +safe_mkdir() { + local dir="$1" + local perms="$2" + + if [ ! -d "$dir" ]; then + mkdir -p "$dir" || return 1 + chmod "$perms" "$dir" || return 1 + fi + return 0 +} + +# Log function +log() { + local level="$1" + local message="$2" + + case "$level" in + "ERROR") echo "ERROR: $message" >&2 ;; + "INFO") echo "INFO: $message" >&2 ;; + "DEBUG") [ "$LOG_LEVEL" -ge "$LOG_LEVEL_DEBUG" ] && echo "DEBUG: $message" >&2 ;; + *) echo "$message" >&2 ;; + esac +} + +# Default paths +if [ -z "$CONFIGDIR" ]; then + CONFIGDIR="$HOME/.s" +fi + +if [ -z "$KEYPATH" ]; then + KEYPATH="$HOME/.ssh" +fi + +# Create directories if they don't exist +mkdir -p "$CONFIGDIR/templates" +mkdir -p "$KEYPATH" + +# Set permissions +chmod 700 "$CONFIGDIR" +chmod 700 "$CONFIGDIR/templates" +chmod 700 "$KEYPATH" + +# Validation functions +validate_config_name() { + local name="$1" + + if [ -z "$name" ]; then + log "ERROR" "Configuration name is required" + return $EXIT_INVALID_INPUT + fi + + if [[ ! "$name" =~ ^[a-zA-Z0-9_-]+$ ]]; then + log "ERROR" "Invalid configuration name: $name" + return $EXIT_INVALID_INPUT + fi + + return $EXIT_SUCCESS +} + +validate_config_file() { + local file="$1" + + if [ ! -f "$file" ]; then + log "ERROR" "Configuration file not found: $file" + return $EXIT_CONFIG_ERROR + fi + + if [ ! -r "$file" ]; then + log "ERROR" "Configuration file not readable: $file" + return $EXIT_CONFIG_ERROR + fi + + return $EXIT_SUCCESS +} + +validate_ssh_key() { + local key_file="$1" + + if [ ! -f "$key_file" ]; then + log "ERROR" "SSH key not found: $key_file" + return $EXIT_ERROR + fi + + if [ ! -r "$key_file" ]; then + log "ERROR" "SSH key not readable: $key_file" + return $EXIT_ERROR + fi + + if [ "$(stat -c %a "$key_file" 2>/dev/null || stat -f "%Lp" "$key_file")" != "600" ]; then + log "ERROR" "SSH key has incorrect permissions: $key_file" + return $EXIT_ERROR + fi + + return $EXIT_SUCCESS +} + +validate_ssh_connection() { + local user="$1" + local host="$2" + local key_file="$3" + + if [ -z "$user" ] || [ -z "$host" ]; then + log "ERROR" "User and host are required" + return $EXIT_INVALID_INPUT + fi + + if [ -n "$key_file" ] && ! validate_ssh_key "$key_file"; then + return $EXIT_ERROR + fi + + return $EXIT_SUCCESS +} + +# File operations +safe_cp() { + local src="$1" + local dst="$2" + local perms="$3" + + if [ ! -f "$src" ]; then + log "ERROR" "Source file not found: $src" + return $EXIT_ERROR + fi + + if ! cp -f "$src" "$dst"; then + log "ERROR" "Failed to copy file from $src to $dst" + return $EXIT_ERROR + fi + + if ! chmod "$perms" "$dst"; then + log "ERROR" "Failed to set permissions on $dst" + return $EXIT_ERROR + fi + + return $EXIT_SUCCESS +} + +secure_file_operation() { + local operation="$1" + local file="$2" + local content="$3" + + # Create temporary file + local temp_file + temp_file="$(mktemp)" + + # Write content to temporary file + echo "$content" > "$temp_file" + + # Set correct permissions + chmod 600 "$temp_file" + + # Move temporary file to target + mv "$temp_file" "$file" + + # Set correct permissions on target + chmod 600 "$file" +} + +# Configuration management +save_config() { + local name="$1" + local config_file="$CONFIGDIR/$name" + + # Validate input + if ! validate_config_name "$name"; then + return $EXIT_INVALID_INPUT + fi + + # Check if config already exists + if [ -f "$config_file" ]; then + log "ERROR" "Configuration '$name' already exists" + return $EXIT_ERROR + fi + + # Create config file + secure_file_operation "create" "$config_file" "" + + log "INFO" "Configuration '$name' created" + return $EXIT_SUCCESS +} + +# Help message +show_help() { + cat << EOF +Usage: s [OPTIONS] [CONFIG] + +Options: + --version Show version + --help Show this help message + --save NAME Save current configuration + --delete NAME Delete configuration + --from-template TEMPLATE NAME Create config from template + --generate-key NAME Generate SSH key + --delete-key NAME Delete SSH key + --dry-run Show SSH command without executing + --port-forward LOCAL:REMOTE Enable port forwarding + --ssh-agent Enable SSH agent forwarding + --timeout SECONDS Set connection timeout + --retries COUNT Set connection retries + +Configuration Management: + s --save Save current configuration + s --delete Delete configuration + s --from-template