diff --git a/Makefile b/Makefile index 4fb6f2c..42e32f4 100644 --- a/Makefile +++ b/Makefile @@ -14,5 +14,8 @@ build: clean: @rm -rf build +run: + @bash main.sh + test: @./test.sh diff --git a/README.md b/README.md index d9b6fdd..318157d 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,27 @@ -# SSH Tunnel Swarm +# ssh-tunnel-swarm ![ssh-tunnel-swarm](https://raw.githubusercontent.com/psyb0t/ssh-tunnel-swarm/master/assets/ssh-tunnel-swarm.png) -SSH Tunnel Swarm is a powerful shell script tool for managing multiple SSH tunnels concurrently. It simplifies the process of creating and managing both forward and reverse SSH tunnels by applying a predefined set of rules for each tunnel. +ssh-tunnel-swarm is a powerful shell script tool for managing multiple SSH tunnels concurrently. It simplifies the process of creating and managing both forward and reverse SSH tunnels by applying a predefined set of rules for each tunnel. The script supports the configuration of multiple SSH connections and can establish tunnels based on defined rules. ## Table of Contents -- [SSH Tunnel Swarm](#ssh-tunnel-swarm) - - [Features](#features) - - [Prerequisites](#prerequisites) - - [Installation](#installation) - - [Installing for the current user](#installing-for-the-current-user) - - [Installing for all users](#installing-for-all-users) - - [Configuration](#configuration) - - [Environment Variables](#environment-variables) - - [Tunnel Rules](#tunnel-rules) - - [Usage](#usage) - - [Logging](#logging) +- [Features](#features) +- [Prerequisites](#prerequisites) +- [Installation](#installation) + - [Installing for all users](#installing-for-all-users) + - [Installing for the current user](#installing-for-the-current-user) +- [Configuration](#configuration) + - [Environment Variables](#environment-variables) - [Supported Log Levels](#supported-log-levels) - - [Important Notes](#important-notes) - - [Contributing](#contributing) - - [License](#license) - - [TODO](#todo) - - [Glossary](#glossary) + - [Tunnel Rules](#tunnel-rules) + - [Example](#example) +- [Usage](#usage) +- [Important Notes](#important-notes) +- [License](#license) +- [TODO](#todo) ## Features @@ -45,76 +42,24 @@ This script is designed to be run in a Unix-like environment. ## Installation -To install `ssh-tunnel-swarm`, the first step is to check if `wget` is installed on your system by running `wget --version`. +Execute the following command to download `ssh-tunnel-swarm`: -If it is installed, the output should be similar to this: - -``` -GNU Wget 1.21.2 built on linux-gnu. - --cares +digest -gpgme +https +ipv6 +iri +large-file -metalink +nls -+ntlm +opie +psl +ssl/openssl - -Wgetrc: - /etc/wgetrc (system) -Locale: - /usr/share/locale -Compile: - gcc -DHAVE_CONFIG_H -DSYSTEM_WGETRC="/etc/wgetrc" - -DLOCALEDIR="/usr/share/locale" -I. -I../../src -I../lib - -I../../lib -Wdate-time -D_FORTIFY_SOURCE=2 -DHAVE_LIBSSL -DNDEBUG - -g -O2 -ffile-prefix-map=/build/wget-8g5eYO/wget-1.21.2=. - -flto=auto -ffat-lto-objects -flto=auto -ffat-lto-objects - -fstack-protector-strong -Wformat -Werror=format-security - -DNO_SSLv2 -D_FILE_OFFSET_BITS=64 -g -Wall -Link: - gcc -DHAVE_LIBSSL -DNDEBUG -g -O2 - -ffile-prefix-map=/build/wget-8g5eYO/wget-1.21.2=. -flto=auto - -ffat-lto-objects -flto=auto -ffat-lto-objects - -fstack-protector-strong -Wformat -Werror=format-security - -DNO_SSLv2 -D_FILE_OFFSET_BITS=64 -g -Wall -Wl,-Bsymbolic-functions - -flto=auto -ffat-lto-objects -flto=auto -Wl,-z,relro -Wl,-z,now - -lpcre2-8 -luuid -lidn2 -lssl -lcrypto -lz -lpsl ftp-opie.o - openssl.o http-ntlm.o ../lib/libgnu.a - -Copyright (C) 2015 Free Software Foundation, Inc. -License GPLv3+: GNU GPL version 3 or later -. -This is free software: you are free to change and redistribute it. -There is NO WARRANTY, to the extent permitted by law. - -Originally written by Hrvoje Niksic . -Please send bug reports and questions to . +```shell +wget -qO- https://raw.githubusercontent.com/psyb0t/ssh-tunnel-swarm/master/tools/downloader.sh | bash ``` -If `wget` is not installed, you can easily install it using the package manager for your operating system. Here are the installation commands for some known operating systems: - -- Debian/Ubuntu-based systems: `sudo apt-get install wget` -- Arch Linux-based systems: `sudo pacman -S wget` -- Fedora-based systems: `sudo dnf install wget` -- CentOS/RHEL-based systems: `sudo yum install wget` -- openSUSE-based systems: `sudo zypper install wget` -- Alpine Linux-based systems: `sudo apk add wget` -- FreeBSD-based systems: `sudo pkg install wget` -- NetBSD-based systems: `sudo pkgin install wget` -- OpenBSD-based systems: `sudo pkg_add wget` -- macOS with Homebrew installed: `brew install wget` -- Tiny Core Linux: `tce-load -wi wget` - -If your operating system is not listed above, you can visit the `wget` website at https://www.gnu.org/software/wget/ and download it from there. +After the download is complete, you can use `ssh-tunnel-swarm` from the current location by executing `./ssh-tunnel-swarm` but a true installation allows you to use it from any directory. -Once `wget` is installed, execute the following command to download `ssh-tunnel-swarm`: +### Installing for all users ```shell -wget -qO- https://raw.githubusercontent.com/psyb0t/ssh-tunnel-swarm/master/tools/downloader.sh | bash +sudo mv ssh-tunnel-swarm /usr/bin/ ``` -After the download is complete, you can use `ssh-tunnel-swarm` from the current location by executing `./ssh-tunnel-swarm` but a true installation allows you to use it from any directory. - ### Installing for the current user ```shell -mkdir ~/bin +mkdir -p ~/bin mv ssh-tunnel-swarm ~/bin/ ``` @@ -128,7 +73,7 @@ echo $PATH | grep -q "$HOME/bin" && echo "The $HOME/bin directory is already in This command will output a message indicating whether `$HOME/bin` is already in your path. -If it is not, execute the following command for either `bash` or `zsh`: +If it is not, add it to your shell profile file: For `bash`: @@ -144,12 +89,6 @@ echo 'export PATH="$HOME/bin:$PATH"' >> ~/.zshrc source ~/.zshrc ``` -## Installing for all users - -```shell -sudo mv ssh-tunnel-swarm /usr/bin/ -``` - ## Configuration ### Environment Variables @@ -160,17 +99,29 @@ sudo mv ssh-tunnel-swarm /usr/bin/ - **LOG_FILE**: This determines the output destination of the log messages. If set, log messages will be written to the specified file. If not set, logs will be printed to stdout. -- **LOG_LEVEL**: This determines the severity level of the messages to be logged. Messages with a severity level less than this will not be logged. For example, if `LOG_LEVEL` is set to `INFO`, then `DEBUG` messages won't be logged. Default value if not set is `DEBUG`. You can find all of the [supported log levels here](#supported-log-levels). +- **LOG_LEVEL**: This determines the severity level of the messages to be logged. Messages with a severity level less than this will not be logged. For example, if `LOG_LEVEL` is set to `INFO`, then `DEBUG` messages won't be logged. Default value if not set is `INFO`. + +#### Supported Log Levels + +The logger recognizes four levels of logging: + +- **DEBUG**: These are verbose-level messages and are usually useful during development or debugging sessions. They provide deep insights about what's going on. + +- **INFO**: These messages provide general feedback about the application processes and state. They are used to confirm that things are working as expected. + +- **ERROR**: These are messages that indicate a problem that prevented a function or process from completing successfully. + +- **FATAL**: These messages indicate a severe problem that has caused the application to stop. They require immediate attention. ### Tunnel Rules -SSH Tunnel Swarm reads the tunnel rules for each host from a file specified by the `RULES_FILE` environment variable. Each entry within the file should include the username, hostname, port, and the tunnels to establish. For instance: +ssh-tunnel-swarm reads the tunnel rules for each host from a file specified by the `RULES_FILE` environment variable. Each entry within the file should include the username, hostname, port, SSH private key and the tunnels to establish. For instance: ``` -aparker@host789:34567 +aparker@host789:34567=/home/aparker/.ssh/id_rsa reverse host789.example.com:5432:172.16.0.5:5432 -sjones@host012:67890 +sjones@host012:67890=/path/to/ssh/private/key reverse host012.example.com:3000:10.20.30.40:3000 forward 172.20.0.2:6060:host012.example.com:4444 reverse 10.0.0.5:5678:host789.example.com:1234 @@ -179,7 +130,9 @@ forward host789.example.com:9876:172.16.0.5:4321 Each block represents an SSH connection where: -- The first line is the username, host, and port to connect to. +- The first line is the username, host, port to connect to and the private SSH key to use. The syntax is: + `user@hostname:port=/path/to/private/ssh/key` + - The following lines are the SSH tunnels to establish for that connection, with one tunnel per line. The syntax is: `direction local-interface:local-port:remote-interface:remote-port` @@ -188,7 +141,7 @@ Each block represents an SSH connection where: **Set up a reverse tunnel from your local machine to a VPS having SSH listening on port 22** ``` -user@myvps.com:22 +user@myvps.com:22=/path/to/ssh/private/key reverse localhost:8080:0.0.0.0:80 ``` @@ -197,10 +150,10 @@ Now when you access http://myvps.com/ you'll access your local service. **Set up a reverse tunnel from your local machine to 2 VPSs having SSH listening on port 22** ``` -user@myvps.com:22 +user@myvps.com:22=/path/to/ssh/private/key reverse localhost:8080:0.0.0.0:80 -user@myothervps.com:22 +user@myothervps.com:22=/path/to/ssh/private/key reverse localhost:8080:0.0.0.0:80 ``` @@ -209,7 +162,7 @@ Now when you access both http://myvps.com/ and http://myothervps.com/ you'll acc **Set up a forward tunnel from a remote machine to your computer** ``` -user@enterprise.com:22 +user@enterprise.com:22=/path/to/ssh/private/key forward localhost:6366:10.137.82.201:636 ``` @@ -225,49 +178,19 @@ LOG_LEVEL=DEBUG \ ssh-tunnel-swarm ``` -## Logging - -SSH Tunnel Swarm includes a logging functionality that provides visibility into the operations and state of your SSH connections. - -It is designed to enable configurable log levels and output destinations and provides different levels of logging based on the severity of the log message. This is particularly useful in complex scripts or systems where detailed logging is beneficial for development, debugging, or ongoing system maintenance. - -### Supported Log Levels - -The logger recognizes four levels of logging: - -- **DEBUG**: These are verbose-level messages and are usually useful during development or debugging sessions. They provide deep insights about what's going on. - -- **INFO**: These messages provide general feedback about the application processes and state. They are used to confirm that things are working as expected. - -- **ERROR**: These are messages that indicate a problem that prevented a function or process from completing successfully. - -- **FATAL**: These messages indicate a severe problem that has caused the application to stop. They require immediate attention. - ## Important Notes -- SSH Tunnel Swarm does not handle SSH authentication(yet). Please ensure that the necessary SSH key is available(currently only the default one is used). +- ssh-tunnel-swarm does not handle SSH password authentication. -- Make sure you have the required permissions on your local and remote systems to establish SSH connections and tunnels. +- Always use this script responsibly make sure you have the required permissions on your local and remote systems to establish SSH connections and tunnels. -- All SSH connections are established with -o StrictHostKeyChecking=no for convenience. However, this option may expose you to potential security risks. +- All SSH connections are established with `-o StrictHostKeyChecking=yes`. -- Always use this script responsibly and ensure you have the permissions to establish tunnels with your target hosts. - -## Contributing - -I welcome your contributions. Please submit a pull request with your improvements. Make sure to adhere to the existing coding style and ensure all tests pass before submitting your PR. - -### Clone the repository: - -```shell -git clone https://github.com/psyb0t/ssh-tunnel-swarm.git -cd ssh-tunnel-swarm -make test -``` +- With `StrictHostKeyChecking=yes`, the client will refuse to connect to servers whose host key is not known or has changed since it was last recorded. This may lead to initial connection failure if the host key is not already in the known_hosts file. -If all tests run you're ready do go + It is your responsibility to ensure that the `known_hosts` file is up to date with the public keys of the remote hosts you wish to connect to. You can manually add a host's public key to your `known_hosts` file, or you can retrieve it using SSH on a trusted network before running ssh-tunnel-swarm. -To execute the script in development you can either just run `bash main.sh` or execute `make build` and run the compiled script `./build/ssh-tunnel-swarm` + Note that in some cases, you might have to manually remove outdated or changed host keys from your `known_hosts` file. This situation can arise if you're connecting to a server that has had its SSH keys regenerated. ## License @@ -277,20 +200,6 @@ By using this software, you agree to abide by the terms of the **WTFPL**. If you ## TODO -- add support for specifying keys for each host -- add more tests - -## Glossary - -- **SSH**: Secure Shell is a protocol used to securely connect to a remote server/system. -- **Tunnel**: In the context of SSH, a tunnel is a route through which the entirety of your data is going to pass. -- **Forward Tunnel (Local Port Forwarding)**: Forwarding calls for a specific IP and port from the client system to an IP and port on the server system. -- **Reverse Tunnel (Remote Port Forwarding)**: Allows the server to receive a connection as a client from the client system. -- **SSH Key**: A way of logging into an SSH/SFTP account using a cryptographic pair of keys, hence providing an alternative way to password-based logins. -- **Bash**: A shell, or command language interpreter, for the GNU operating system. -- **Shell Script**: A computer program designed to be run by the Unix shell, a command-line interpreter. -- **wget**: A free utility for non-interactive download of files from the web. It supports HTTP, HTTPS, and FTP protocols and can retrieve files through HTTP proxies. -- **PATH**: An environment variable on Unix-like operating systems, DOS, OS/2, and Microsoft Windows, specifying a set of directories where executable programs are located. -- **$HOME**: An environment variable that displays the path of the home directory of the current user. -- **WTFPL**: Do What The Fuck You Want To Public License, a very permissive license for software and other scientific or artistic works that offers a huge degree of freedom. -- **GNU**: Stands for GNU's Not Unix, an extensive collection of free software, which includes the GNU Project, the GNU Operating System, and the GNU General Public License. +- refactor +- better error handling +- better testing utils diff --git a/common_test.sh b/common_test.sh index 751aee9..8ff7d93 100644 --- a/common_test.sh +++ b/common_test.sh @@ -1,10 +1,11 @@ #!/bin/bash # Function to print a failure message print_failure() { - local message="$1" - local expected="$2" - local actual="$3" - local is_error_check="$4" + local function_name="$1" + local message="$2" + local expected="$3" + local actual="$4" + local is_error_check="$5" local check_type if [[ "$is_error_check" -eq 1 ]]; then @@ -13,15 +14,16 @@ print_failure() { check_type="Equality check" fi - echo "FAIL - Test failed: $message. $check_type. Expected '$expected', but got '$actual'" + echo "FAIL - ${function_name}: assert failed: $message. $check_type. Expected '$expected', but got '$actual'" exit 1 } # Function to print a success message print_success() { - local message="$1" + local function_name="$1" + local message="$2" - echo "OK - Test passed: $message" + echo "OK - ${function_name}: assert passed: $message" } # Assert function to check if actual and expected values are the same @@ -31,9 +33,9 @@ assert_equals() { local message="$3" if [[ "$actual" == "$expected" ]]; then - print_success "$message" + print_success "${FUNCNAME[0]}" "$message" else - print_failure "$message" "$expected" "$actual" 0 + print_failure "${FUNCNAME[0]}" "$message" "$expected" "$actual" 0 fi } @@ -44,9 +46,9 @@ assert_is_error() { local message="$3" if [[ "$actual" -eq "$expected" ]]; then - print_success "$message" + print_success "${FUNCNAME[0]}" "$message" else - print_failure "$message" "$expected" "$actual" 1 + print_failure "${FUNCNAME[0]}" "$message" "$expected" "$actual" 1 fi } @@ -56,8 +58,8 @@ assert_no_error() { local message="$2" if [[ "$actual" -eq 0 ]]; then - print_success "$message" + print_success "${FUNCNAME[0]}" "$message" else - print_failure "$message" "no error" "$actual" 1 + print_failure "${FUNCNAME[0]}" "$message" "no error" "$actual" 1 fi } diff --git a/logger.sh b/logger.sh index b401b12..49e3ba4 100644 --- a/logger.sh +++ b/logger.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Set default log level to DEBUG if it's not already set -: "${LOG_LEVEL:=DEBUG}" +# Set default log level to INFO if it's not already set +: "${LOG_LEVEL:=INFO}" # Set default log enabled flag only if it's not been set before : "${LOG_ENABLED:=1}" # Set default log file if it's not already set diff --git a/main.sh b/main.sh index bcac83b..7b54fe6 100755 --- a/main.sh +++ b/main.sh @@ -15,21 +15,26 @@ load_rules # $1 - user_host_port: A string containing the username, hostname, and port number to connect to, separated by colons. Example: "john@example.com:22" # $2 - raw_tunnel_rules: A string containing semicolon-separated tunnel rules, where each tunnel rule is in the format "direction interface_ports", separated by spaces. Example: "forward 8080:localhost:80;reverse 2222:localhost:22" establish_ssh_connection() { - local user_host_port="$1" + local user_host_port_key="$1" local raw_tunnel_rules="$2" + local user_host_port + local key local IFS=" " + logger "DEBUG" "Splitting user_host_port and key from: $user_host_port_key" + read -r user_host_port key <<<"$(split_user_host_port_key "$user_host_port_key")" + logger "DEBUG" "Got user_host_port and key: $user_host_port, $key" + logger "DEBUG" "Splitting user, host and port from: $user_host_port" - # Split the username, hostname, and port number from the user_host_port string read -r user_host port <<<"$(split_user_host_port "$user_host_port")" logger "DEBUG" "Got user_host and port: $user_host, $port" logger "DEBUG" "Splitting user and host from: $user_host" - # Split the username and hostname from the user_host string read -r user host <<<"$(split_user_host "$user_host")" logger "DEBUG" "Got user and host: $user, $host" logger "DEBUG" "Processing raw tunnel rules: $raw_tunnel_rules" + # Split the tunnel rules into an array IFS=';' read -ra tunnel_rules <<<"$raw_tunnel_rules" @@ -75,9 +80,9 @@ establish_ssh_connection() { # Convert the ssh_tunnel_parameters string and other connection details into the SSH command IFS=' ' read -ra split_ssh_tunnel_parameters <<<"$ssh_tunnel_parameters" - logger "DEBUG" "Executing ssh command ssh -N -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o StrictHostKeyChecking=no" "${split_ssh_tunnel_parameters[@]}" "-p" "${port}" "${user}@${host}" + logger "DEBUG" "Executing ssh command ssh -N -i ${key} -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o StrictHostKeyChecking=yes" "${split_ssh_tunnel_parameters[@]}" "-p" "${port}" "${user}@${host}" # Establish an SSH connection and create the tunnels - ssh -N -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o StrictHostKeyChecking=no "${split_ssh_tunnel_parameters[@]}" -p "${port}" "${user}@${host}" >"$temp_file" 2>&1 + ssh -N -i "$key" -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o StrictHostKeyChecking=yes "${split_ssh_tunnel_parameters[@]}" -p "${port}" "${user}@${host}" >"$temp_file" 2>&1 local ssh_exit_status=$? local ssh_output @@ -109,16 +114,16 @@ declare -A HOST_PIDS kill_ssh_connections() { logger "INFO" "Killing SSH connections" # Loop through each connection and kill the SSH process and its children - for user_host_port in "${!SUBSHELL_PIDS[@]}"; do - # Extract the hostname from the user_host_port string - local host=${user_host_port%:*} + for user_host_port_key in "${!SUBSHELL_PIDS[@]}"; do + # Extract the hostname from the user_host_port_key string + local user_host=${user_host_port_key%:*} # Get the subshell PID for the connection - local subshell_pid=${SUBSHELL_PIDS[$user_host_port]} + local subshell_pid=${SUBSHELL_PIDS[$user_host_port_key]} - logger "INFO" "Killing SSH process for host $host (PID: $subshell_pid)" + logger "INFO" "Killing SSH process for user_host $user_host (PID: $subshell_pid)" # Kill the subshell process and its children kill "$subshell_pid" 2>/dev/null - kill "${HOST_PIDS[$user_host_port]}" 2>/dev/null + kill "${HOST_PIDS[$user_host_port_key]}" 2>/dev/null done # Log an exiting message and exit the script @@ -128,17 +133,17 @@ kill_ssh_connections() { # Main function to start SSH connections based on the pre-defined rules main() { - # Loop through each user_host_port and start a connection in the background - for user_host_port in "${!RULES[@]}"; do + # Loop through each user_host_port_key and start a connection in the background + for user_host_port_key in "${!RULES[@]}"; do ( - establish_ssh_connection "$user_host_port" "${RULES[$user_host_port]}" + establish_ssh_connection "$user_host_port_key" "${RULES[$user_host_port_key]}" ) & # Record the process IDs for the connection - SUBSHELL_PIDS["$user_host_port"]=$! - HOST_PIDS["$user_host_port"]=$(pgrep -P "${SUBSHELL_PIDS[$user_host_port]}") + SUBSHELL_PIDS["$user_host_port_key"]=$! + HOST_PIDS["$user_host_port_key"]=$(pgrep -P "${SUBSHELL_PIDS[$user_host_port_key]}") - logger "DEBUG" "SSH connection started for host ${user_host_port%:*} (PID: ${SUBSHELL_PIDS[$user_host_port]}, HOST PID: ${HOST_PIDS[$user_host_port]})." + logger "DEBUG" "SSH connection started for host ${user_host_port_key%:*} (PID: ${SUBSHELL_PIDS[$user_host_port_key]}, HOST PID: ${HOST_PIDS[$user_host_port_key]})." done # Register the signal handlers for killing SSH connections diff --git a/rules.sh b/rules.sh index f641f74..ca86f12 100644 --- a/rules.sh +++ b/rules.sh @@ -12,24 +12,27 @@ declare -A RULES # This function checks if the rules file exists, is readable, and has non-zero size # It logs ERROR errors for any failed checks and returns a non-zero status +# Accepts one parameter: +# $1 - rules_file: A string containing the path to the rules file # Return values: # 0 - on success # 1 - if any of the checks fail (file not found, no read permissions, or empty file). check_rules_file() { + local rules_file="$1" logger "DEBUG" "Starting check_rules_file function" - if [ ! -f "$RULES_FILE" ]; then - logger "ERROR" "Rules file \"$RULES_FILE\" not found" + if [ ! -f "$rules_file" ]; then + logger "ERROR" "Rules file \"$rules_file\" not found" return 1 fi - if [ ! -r "$RULES_FILE" ]; then - logger "ERROR" "Current user does not have read permissions on rules file \"$RULES_FILE\"" + if [ ! -r "$rules_file" ]; then + logger "ERROR" "Current user does not have read permissions on rules file \"$rules_file\"" return 1 fi - if [ ! -s "$RULES_FILE" ]; then - logger "ERROR" "Rules file \"$RULES_FILE\" is empty" + if [ ! -s "$rules_file" ]; then + logger "ERROR" "Rules file \"$rules_file\" is empty" return 1 fi @@ -47,7 +50,7 @@ check_rules_file() { load_rules() { logger "DEBUG" "Starting load_rules function" - if ! check_rules_file; then + if ! check_rules_file "$RULES_FILE"; then logger "FATAL" "Rules file checks failed" fi @@ -70,23 +73,39 @@ load_rules() { # If the line matches the username@hostname:port pattern, extract the username, # hostname, and port and create a new key in RULES - if [[ "$line" =~ ^([a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+:[0-9]+)$ ]]; then + if [[ "$line" =~ ^([a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+:[0-9]+)=\/[a-zA-Z0-9._\/-]+$ ]]; then + local ssh_key_path + local username_host_port + # Extract the username@hostname:port combo and ssh key path + IFS='=' read -r username_host_port ssh_key_path <<<"$line" + + # Checking if ssh_key_path exists + if [ ! -f "$ssh_key_path" ]; then + logger "FATAL" "SSH private key file not found: $ssh_key_path" + fi + local username local host_port - # Extract the username and validate it - username="${line%@*}" + # Extract the username and host_port combo + IFS='@' read -r username host_port <<<"$username_host_port" + + # Validate username if ! is_valid_username "$username"; then logger "FATAL" "Invalid username: $username in line $line_no" fi - # Extract the hostname/IP address and validate it - host_port="${line#*@}" - if ! is_valid_hostname_or_ip "${host_port%:*}"; then + local host + local port + # Extract the host and port + IFS=':' read -r host port <<<"$host_port" + + # Validate host + if ! is_valid_hostname_or_ip "$host"; then logger "FATAL" "Invalid hostname/IP address: ${host_port%:*} in line $line_no" fi - # Extract the port and validate it - if ! is_valid_port "${host_port#*:}"; then + # Validate the port + if ! is_valid_port "$port"; then logger "FATAL" "Invalid port: ${host_port#*:} in line $line_no" fi diff --git a/rules_test.sh b/rules_test.sh index 586d41c..5901c5f 100644 --- a/rules_test.sh +++ b/rules_test.sh @@ -13,7 +13,7 @@ test_check_rules_file() { temp_file=$(mktemp /tmp/rules.XXXXXX) touch "$temp_file" chmod u+r "$temp_file" - assert_no_error "$(check_rules_file "$temp_file")" "${FUNCNAME[0]}: test with existing and readable file" + assert_no_error "$(check_rules_file "$temp_file")" "${FUNCNAME[0]}" rm "$temp_file" } test_check_rules_file_with_existing_readable_file @@ -27,7 +27,7 @@ test_check_rules_file() { assert_is_error "$( check_rules_file "$temp_file" echo $? - )" "${expected_return_code}" "${FUNCNAME[0]}: test with non-existent file" + )" "${expected_return_code}" "${FUNCNAME[0]}" } test_check_rules_file_with_nonexistent_file @@ -41,7 +41,7 @@ test_check_rules_file() { assert_is_error "$( check_rules_file "$temp_file" echo $? - )" "${expected_return_code}" "${FUNCNAME[0]}: test with non-readable file" + )" "${expected_return_code}" "${FUNCNAME[0]}" rm "$temp_file" } test_check_rules_file_with_nonreadable_file @@ -54,7 +54,7 @@ test_check_rules_file() { assert_is_error "$( check_rules_file "$temp_file" echo $? - )" "${expected_return_code}" "${FUNCNAME[0]}: test with empty file" + )" "${expected_return_code}" "${FUNCNAME[0]}" rm "$temp_file" } test_check_rules_file_with_empty_file diff --git a/splitters.sh b/splitters.sh index 92c3418..c387852 100644 --- a/splitters.sh +++ b/splitters.sh @@ -1,4 +1,21 @@ #!/bin/bash +# Function to split a user, host, port and key combination and returns the user/host/port and key separately. +# Accepts one parameter: +# $1 - user_host_port_key: A string containing the username, hostname, port number and key path, separated by equals sign. Example: "john@example.com:22=/path/to/key" +# Returns: +# 0 - on success +# 1 - if user_host_port or key is empty +split_user_host_port_key() { + local user_host_port_key="$1" + local IFS="=" + read -r user_host_port key <<<"${user_host_port_key}" + if [[ -z "$user_host_port" || -z "$key" ]]; then + return 1 + fi + echo "$user_host_port" "$key" + return 0 +} + # Function to split a user, host, and port combination and returns the user/host and port separately. # Accepts one parameter: # $1 - user_host_port: A string containing the username, hostname, and port number to connect to, separated by colons. Example: "john@example.com:22" diff --git a/splitters_test.sh b/splitters_test.sh index ab2b0f3..188c94e 100644 --- a/splitters_test.sh +++ b/splitters_test.sh @@ -5,13 +5,61 @@ source common_test.sh # Source the bash file containing the functions to be tested source splitters.sh +# Test function for split_user_host_port_key +test_split_user_host_port_key() { + # Test with valid user_host_port_key + test_split_user_host_port_key_with_valid_input() { + local input="valid-user@valid.host.com:1234=/valid/path/to/key" + local expected_output=("valid-user@valid.host.com:1234" "/valid/path/to/key") + assert_equals "$(split_user_host_port_key $input)" "${expected_output[*]}" "${FUNCNAME[0]}" + assert_no_error $? "${FUNCNAME[0]}" + } + test_split_user_host_port_key_with_valid_input + + # Test with empty input + test_split_user_host_port_key_with_empty_input() { + local input="" + local expected_output=() + local expected_return_code=1 + assert_is_error "$( + split_user_host_port_key "$input" + echo $? + )" "${expected_return_code}" "${FUNCNAME[0]}" + } + test_split_user_host_port_key_with_empty_input + + # Test with missing key + test_split_user_host_port_key_with_missing_key() { + local input="bob@example.com:22=" + local expected_output=() + local expected_return_code=1 + assert_is_error "$( + split_user_host_port_key $input + echo $? + )" "${expected_return_code}" "${FUNCNAME[0]}" + } + test_split_user_host_port_key_with_missing_key + + # Test with missing user_host_port + test_split_user_host_port_key_with_missing_user_host_port() { + local input="=/path/to/key" + local expected_output=() + local expected_return_code=1 + assert_is_error "$( + split_user_host_port_key $input + echo $? + )" "${expected_return_code}" "${FUNCNAME[0]}" + } + test_split_user_host_port_key_with_missing_user_host_port +} + # Test function for split_user_host_port test_split_user_host_port() { # Test with hostname test_split_user_host_port_with_hostname() { local input="john@example.com:22" local expected_output=("john@example.com" "22") - assert_equals "$(split_user_host_port $input)" "${expected_output[*]}" "${FUNCNAME[0]}: test with hostname" + assert_equals "$(split_user_host_port $input)" "${expected_output[*]}" "${FUNCNAME[0]}" assert_no_error $? "${FUNCNAME[0]}" } test_split_user_host_port_with_hostname @@ -20,7 +68,7 @@ test_split_user_host_port() { test_split_user_host_port_with_ip() { local input="alice@192.168.1.10:8080" local expected_output=("alice@192.168.1.10" "8080") - assert_equals "$(split_user_host_port $input)" "${expected_output[*]}" "${FUNCNAME[0]}: test with IP address" + assert_equals "$(split_user_host_port $input)" "${expected_output[*]}" "${FUNCNAME[0]}" assert_no_error $? "${FUNCNAME[0]}" } test_split_user_host_port_with_ip @@ -29,7 +77,7 @@ test_split_user_host_port() { test_split_user_host_port_with_localhost() { local input="root@localhost:3306" local expected_output=("root@localhost" "3306") - assert_equals "$(split_user_host_port $input)" "${expected_output[*]}" "${FUNCNAME[0]}: test with localhost" + assert_equals "$(split_user_host_port $input)" "${expected_output[*]}" "${FUNCNAME[0]}" assert_no_error $? "${FUNCNAME[0]}" } test_split_user_host_port_with_localhost @@ -42,7 +90,7 @@ test_split_user_host_port() { assert_is_error "$( split_user_host_port "$input" echo $? - )" "${expected_return_code}" "${FUNCNAME[0]}: test with empty input" + )" "${expected_return_code}" "${FUNCNAME[0]}" } test_split_user_host_port_with_empty_input @@ -54,7 +102,7 @@ test_split_user_host_port() { assert_is_error "$( split_user_host_port $input echo $? - )" "${expected_return_code}" "${FUNCNAME[0]}: test with missing port" + )" "${expected_return_code}" "${FUNCNAME[0]}" } test_split_user_host_port_with_missing_port } @@ -65,7 +113,7 @@ test_split_user_host() { test_split_user_host_with_hostname() { local input="john@example.com" local expected_output=("john" "example.com") - assert_equals "$(split_user_host $input)" "${expected_output[*]}" "${FUNCNAME[0]}: test with hostname" + assert_equals "$(split_user_host $input)" "${expected_output[*]}" "${FUNCNAME[0]}" assert_no_error $? "${FUNCNAME[0]}" } test_split_user_host_with_hostname @@ -74,7 +122,7 @@ test_split_user_host() { test_split_user_host_with_ip() { local input="alice@192.168.1.10" local expected_output=("alice" "192.168.1.10") - assert_equals "$(split_user_host $input)" "${expected_output[*]}" "${FUNCNAME[0]}: test with IP address" + assert_equals "$(split_user_host $input)" "${expected_output[*]}" "${FUNCNAME[0]}" assert_no_error $? "${FUNCNAME[0]}" } test_split_user_host_with_ip @@ -83,7 +131,7 @@ test_split_user_host() { test_split_user_host_with_localhost() { local input="root@localhost" local expected_output=("root" "localhost") - assert_equals "$(split_user_host $input)" "${expected_output[*]}" "${FUNCNAME[0]}: test with localhost" + assert_equals "$(split_user_host $input)" "${expected_output[*]}" "${FUNCNAME[0]}" assert_no_error $? "${FUNCNAME[0]}" } test_split_user_host_with_localhost @@ -96,7 +144,7 @@ test_split_user_host() { assert_is_error "$( split_user_host "$input" echo $? - )" "${expected_return_code}" "${FUNCNAME[0]}: test with empty input" + )" "${expected_return_code}" "${FUNCNAME[0]}" } test_split_user_host_with_empty_input @@ -108,7 +156,7 @@ test_split_user_host() { assert_is_error "$( split_user_host $input echo $? - )" "${expected_return_code}" "${FUNCNAME[0]}: test with missing hostname" + )" "${expected_return_code}" "${FUNCNAME[0]}" } test_split_user_host_port_with_missing_hostname } @@ -119,7 +167,7 @@ test_split_interface_ports() { test_split_interface_ports_ips() { local input="192.168.1.2:8080:192.168.1.100:80" local expected_output=("192.168.1.2" "8080" "192.168.1.100" "80") - assert_equals "$(split_interface_ports $input)" "${expected_output[*]}" "${FUNCNAME[0]}: test with IP addresses" + assert_equals "$(split_interface_ports $input)" "${expected_output[*]}" "${FUNCNAME[0]}" assert_no_error $? "${FUNCNAME[0]}" } test_split_interface_ports_ips @@ -128,7 +176,7 @@ test_split_interface_ports() { test_split_interface_ports_mixed_hostnames() { local input="localhost:80:test.com.test:2048" local expected_output=("localhost" "80" "test.com.test" "2048") - assert_equals "$(split_interface_ports $input)" "${expected_output[*]}" "${FUNCNAME[0]}: test with mixed hostnames" + assert_equals "$(split_interface_ports $input)" "${expected_output[*]}" "${FUNCNAME[0]}" assert_no_error $? "${FUNCNAME[0]}" } test_split_interface_ports_mixed_hostnames @@ -141,7 +189,7 @@ test_split_interface_ports() { assert_is_error "$( split_interface_ports "$input" echo $? - )" "${expected_return_code}" "${FUNCNAME[0]}: test with empty input" + )" "${expected_return_code}" "${FUNCNAME[0]}" } test_split_interface_ports_with_empty_input @@ -153,7 +201,7 @@ test_split_interface_ports() { assert_is_error "$( split_interface_ports $input echo $? - )" "${expected_return_code}" "${FUNCNAME[0]}: test with missing local port" + )" "${expected_return_code}" "${FUNCNAME[0]}" } test_split_interface_ports_with_missing_local_port @@ -165,12 +213,13 @@ test_split_interface_ports() { assert_is_error "$( split_interface_ports $input echo $? - )" "${expected_return_code}" "${FUNCNAME[0]}: test with missing remote interface" + )" "${expected_return_code}" "${FUNCNAME[0]}" } test_split_interface_ports_with_missing_remote_interface } # Run all test functions +test_split_user_host_port_key test_split_user_host_port test_split_user_host test_split_interface_ports