/*
    cssh - cluster secure shell
    Copyright (C) 2015-2021  Andreas Baumann

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#define _FILE_OFFSET_BITS 64
 
#include <libssh/libssh.h> 
#include <libssh/callbacks.h>

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdbool.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <pwd.h>
#include <inttypes.h>
#include <unistd.h>
#include <netdb.h>
#include <assert.h>

#include "port.h"
#include "progressbar.h"

#include "options.h"
#include "version.h"

#include "linenoise.h"

#define BUFSIZE 4096
#define DEFAULT_SSH_PORT 22
#define DEFAULT_CMD_SIZE 2048
#define DEFAULT_CLI_HISTORY_LENGTH 1024
#define DEFAULT_PROMPT "cssh> "
#define HISTORY_FILE ".cssh_history"

typedef enum connection_state_e {
	CSSH_CONNECTION_STATE_UNCONNECTED,
	CSSH_CONNECTION_STATE_CONNECTED,
	CSSH_CONNECTION_STATE_ERROR
} connection_state_e;

typedef enum auth_state_e {
	CSSH_AUTH_INIT,
	CSSH_AUTH_VERIFY_HOST_DONE,
	CSSH_AUTH_NONE_DONE,
	CSSH_AUTH_BANNER_DONE,
	CSSH_AUTH_PUBKEY_DONE_SUCCESS,
	CSSH_AUTH_PUBKEY_DONE_FAILED,
	CSSH_AUTH_PASSWORD_DONE_SUCCESS,
	CSSH_AUTH_PASSWORD_DONE_FAILED,
	CSSH_AUTH_DONE_SUCCESS,
	CSSH_AUTH_DONE_FAILED
} auth_state_e;

typedef enum execution_mode_e {
	CSSH_EXECUTE_UNKNOWN,
	CSSH_EXECUTE_AS_SSH,
	CSSH_EXECUTE_AS_SCP,
	CSSH_EXECUTE_AS_CLI	
} execution_mode_e;

typedef struct ssh_data_t {
	ssh_channel channel;
	char *stdout_buf;
	char *stdout_bptr;
	char *stderr_buf;
	char *stderr_bptr;
	bool eof_sent;
	bool is_eof;
} ssh_data_t;

typedef enum copy_direction_e {
	CSSH_COPY_DIRECTION_UPLOAD,
	CSSH_COPY_DIRECTION_DOWNLOAD
} copy_direction_e;

typedef enum scp_read_state_e {
	CSSH_SCP_READ_STATE_IDLE,
	CSSH_SCP_READ_STATE_READING,
	CSSH_SCP_READ_STATE_EOF
} scp_read_state_e;

typedef struct scp_dir_stack_entry_t {
	char *dir;
	struct scp_dir_stack_entry_t *next;
} scp_dir_stack_entry_t;

typedef struct scp_dir_stack_t {
	struct scp_dir_stack_entry_t *head;
} scp_dir_stack_t;

typedef struct scp_data_t {
	ssh_scp scp;
	scp_read_state_e read_state;
	scp_dir_stack_t dir_stack;
	int fd;
	char *filename;
	char *buf;
	uint64_t size;
	uint64_t bytesReceived;
	cssh_progressbar_t progressbar;
} scp_data_t;

static int push_dir_stack( scp_dir_stack_t *stack, const char *dir )
{
	struct scp_dir_stack_entry_t *e = (struct scp_dir_stack_entry_t *)malloc( sizeof( scp_dir_stack_entry_t ) );
	if( e == NULL ) {
		return -1;
	}
	
	e->dir = strdup( dir );
	if( e->dir == NULL ) {
		free( e );
		return -1;
	}
	e->next = stack->head;
	stack->head = e;
	
	return 0;	
}

static void pop_dir_stack( scp_dir_stack_t *stack )
{
	if( stack->head != NULL ) {
		struct scp_dir_stack_entry_t *e = stack->head;
		stack->head = stack->head->next;
		free( e->dir );
		free( e );
	}
}

static char *top_dir_stack( scp_dir_stack_t *stack )
{
	if( stack->head != NULL ) {
		return stack->head->dir;
	} else {
		return NULL;
	}
}

static void free_dir_stack( scp_dir_stack_t *stack )
{
	while( stack->head != NULL ) {
		pop_dir_stack( stack );
	}
}

static void create_dir_stack( scp_dir_stack_t *stack )
{
	stack->head = NULL;
}

static execution_mode_e determine_execution_mode( const char *argv0 )
{
	char *b = ssh_basename( argv0 );
	if( b == NULL ) {
		return CSSH_EXECUTE_UNKNOWN;
	}

	if( strcmp( b, "cssh" ) == 0 ) {
		free( b );
		return CSSH_EXECUTE_AS_SSH;
	} else if( strcmp( b, "cscp" ) == 0 ) {
		free( b );
		return CSSH_EXECUTE_AS_SCP;
	}

	free( b );
	return CSSH_EXECUTE_UNKNOWN;
}

static int parse_options_and_arguments( int argc, char *argv[], struct gengetopt_args_info *args_info ) {
        cmdline_parser_init( args_info );

        if( cmdline_parser2( argc, argv, args_info, 1, 0, 1 ) != 0 ) {
        	cmdline_parser_free( args_info );
        	return 1;
        }

        return 0;
}

static bool get_yes_or_no( )
{
	char buf[10];
	
ASK_AGAIN:
	if( fgets( buf, sizeof( buf ), stdin ) == NULL ) {
		fprintf( stderr, "ERROR: fgets failed in 'accept_server'\n" );
		return -1;
	}
	
	if( strncmp( buf, "yes", 3 ) == 0 ) {
		return true;
	} else if( strncmp( buf, "no", 2 ) == 0 ) {
		return false;
	} else {
		fprintf( stderr, "ERROR: expecting 'yes' or 'no' as answer\n" );
		goto ASK_AGAIN;
	}
	
	return true;
}

static unsigned int get_default_ssh_port( )
{
	struct servent *ent = getservbyname( "ssh", "tcp" );
	if( ent != NULL ) {
		return (unsigned int)ntohs( ent->s_port );
	} else {
		return DEFAULT_SSH_PORT;
	}
}

static int accept_server( ssh_session session, unsigned char *hash, size_t hlen )
{
	char *hexa;
	
	hexa = ssh_get_hexa( hash, hlen );
	fprintf( stderr, "The server is unknown. Do you trust the host key?\n" );
	fprintf( stderr, "The server public hash key is: %s\n", hexa );
	ssh_string_free_char( hexa );
	
	if( !get_yes_or_no( ) ) {
		fprintf( stderr, "INFO: Server key not accepted.. terminating now.\n" );
		return -1;
	}
	
	fprintf( stderr, "Do you want to write this new key into the known host file for future usage?\n" );
	if( get_yes_or_no( ) ) {
		if( ssh_session_update_known_hosts( session ) != SSH_OK ) {
			fprintf( stderr, "ERROR: error while writing known host file: %s\n",
				strerror( errno ) );
			return -1;
		}
	}
	
	return 0;
}

static int verify_knownhost( ssh_session session )
{
	int state;

	state = ssh_session_is_known_server( session );

	ssh_key srv_pubkey;
	int rc = ssh_get_server_publickey( session, &srv_pubkey );
	if( rc < 0 ) {
		fprintf( stderr, "ERROR: Failed to get public key of host: %s\n",
			ssh_get_error( session ) );
		return -1;
	}

	unsigned char *hash;
	size_t hlen;
	rc = ssh_get_publickey_hash( srv_pubkey, SSH_PUBLICKEY_HASH_SHA1, &hash, &hlen );
	if( rc < 0 ) {
		fprintf( stderr, "ERROR: Failed to get hash of public key of host: %s\n",
			ssh_get_error( session ) );
		return -1;
	}

	ssh_key_free( srv_pubkey );

	switch( state ) {
		case SSH_SERVER_KNOWN_OK:
			ssh_clean_pubkey_hash( &hash );
			return 0;
		
		case SSH_SERVER_KNOWN_CHANGED: {
			char *hexa = ssh_get_hexa( hash, hlen );
			fprintf( stderr, "ERROR: Host key for server changed : server's one is now:\n%s\n", hexa );
			ssh_string_free_char( hexa );
			ssh_clean_pubkey_hash( &hash );
			} return -1;		

		case SSH_SERVER_FOUND_OTHER:
			fprintf( stderr, "ERROR: The server gave us an unexpected host key, this could be an attack\n" );
			return -1;
		
		case SSH_SERVER_FILE_NOT_FOUND:
			fprintf( stderr, "ERROR: Could not find a known host file.\n" );
			rc = accept_server( session, hash, hlen );
			ssh_clean_pubkey_hash( &hash );
			return -1;
			
		case SSH_SERVER_NOT_KNOWN:
			fprintf( stderr, "ERROR: The server is unknown.\n" );
			rc = accept_server( session, hash, hlen );
			ssh_clean_pubkey_hash( &hash );
			return -1;

		case SSH_SERVER_ERROR:
			fprintf( stderr, "ERROR: 'ssh_is_server_known' failed with: %s\n",
				ssh_get_error( session ) );
			ssh_clean_pubkey_hash( &hash );
			return -1;

		default:
			ssh_clean_pubkey_hash( &hash );
			fprintf( stderr, "ERROR: Unknown check host state %d\n", state );
			return -1;
			
	}
	
	return 0;
}

static int authenticate_pubkey( ssh_session session )
{
	int rc = ssh_userauth_publickey_auto( session, NULL, NULL );
	switch( rc ) {
		case SSH_AUTH_SUCCESS:
			return SSH_AUTH_SUCCESS;
			
		case SSH_AUTH_DENIED:
			fprintf( stderr, "ERROR: authentication with public key denied\n" );
			return SSH_AUTH_DENIED;
		
		case SSH_AUTH_PARTIAL:
			return SSH_AUTH_PARTIAL;
			
		case SSH_AUTH_AGAIN:
			return SSH_AUTH_AGAIN;
			
		case SSH_AUTH_ERROR:
			fprintf( stderr, "ERROR: authentication with public key failed: %s\n",
				ssh_get_error( session ) );
			return SSH_AUTH_ERROR;
	}
	
	return -1;
}

static int authenticate_password( ssh_session session, const char *host, const unsigned int port, const char *user )
{
	char prompt[128];
	char pass[128];

	const char *prompt_user;
	if( user != NULL ) {
		prompt_user = user;
	} else {
		uid_t uid = getuid( );
		struct passwd *pw = getpwuid( uid );
		if( pw != NULL ) {
			prompt_user = pw->pw_name;
		} else {
			prompt_user = "(unknown)";
		}		
	}
	
	if( strchr( host, '@' ) == NULL ) {
		snprintf( prompt, sizeof( prompt ), "%s@%s:%d's password:",
			prompt_user, host, port );
	} else {
		snprintf( prompt, sizeof( prompt ), "%s:%d's password:",
			host, port );
	}
	memset( pass, 0, sizeof( pass ) );
	if( ssh_getpass( prompt, pass, sizeof( pass ), 0, 0 ) < 0 ) {
		fprintf( stderr, "\nERROR: ssh_getpass failed\n" );
		return -1;
	}	

	int rc = ssh_userauth_password( session, user, pass );
		
	switch( rc ) {
		case SSH_AUTH_SUCCESS:
			return SSH_AUTH_SUCCESS;
		
		case SSH_AUTH_PARTIAL:
			return SSH_AUTH_PARTIAL;
			
		case SSH_AUTH_DENIED:
			fprintf( stderr, "ERROR: Password authentication failed.\n" );
			return SSH_AUTH_DENIED;
			
		case SSH_AUTH_AGAIN:
			return SSH_AUTH_AGAIN;
			
		case SSH_AUTH_ERROR:
			fprintf( stderr, "ERROR: Failed to authenticate with password: %s\n",
				ssh_get_error( session ) );
			return SSH_AUTH_ERROR;
	}
	
	return -1;
}

static int read_hosts_file( const char *hosts_file, unsigned int default_port, char ***host, unsigned int **port, unsigned int *nof_hosts )
{
	FILE *f;
	char buf[255];
	
	f = fopen( hosts_file, "r" );
	if( f == NULL ) {
		fprintf( stderr, "ERROR: Opening the host file '%s' failed: %s\n",
			hosts_file, strerror( errno ) );
		return -1;
	}
	
	*nof_hosts = 0;
	unsigned int size_hosts = 2;
	*host = (char **)calloc( size_hosts, sizeof( char * ) );
	*port = (unsigned int *)calloc( size_hosts, sizeof( unsigned int ) );
	if( *host == NULL || *port == NULL ) {
		fprintf( stderr, "ERROR: Memory allocation failed in 'read_hosts_file'\n" );
		return -1;
	}
	
	while( fgets( buf, sizeof( buf ), f ) != NULL ) {
		if( strlen( buf ) > 0 && buf[strlen( buf )-1] == '\n' ) {
			buf[strlen(buf)-1] = '\0';
		}
		
		// ignore comment lines
		if( buf[0] == '#' ) {
			continue;
		}
		
		// the user can be part of the hostname, libssh allows this,
		// but we must parse the port separately
		char *p = strchr( buf, ':' );
		if( p != NULL ) {
			(*port)[*nof_hosts] = atoi( p+1 );
			*p = '\0';
		} else { 
			(*port)[*nof_hosts] = default_port;
		}
		(*host)[*nof_hosts] = strdup( buf );
		(*nof_hosts)++;
		if( *nof_hosts >= size_hosts ) {
			size_hosts *= 2;
			*host = (char **)realloc( *host, size_hosts * sizeof( char *) );
			*port = (unsigned int *)realloc( *port, size_hosts * sizeof( unsigned int ) );
			if( *host == NULL || *port == NULL ) {
				fprintf( stderr, "ERROR: Memory allocation failed in 'read_hosts_file'\n" );
				return -1;
			}
		}
	}
	
	fclose( f );
	
	return 0;
}

// TODO: cleanup also progressbarpool
static void finalize( ssh_session **session, ssh_data_t **ssh_data, scp_data_t **scp_data, char **host, unsigned int *port, const int nof_sessions, bool verbose )
{
	for( unsigned int i = 0; i < nof_sessions; i++ ) {
		if( ssh_is_connected( (*session)[i] ) ) {
			if( verbose ) {
				fprintf( stderr, "Disconnecting from '%s', port %d..\n", host[i], port[i] );
			}
			if( ssh_data != NULL ) {
				ssh_channel *channel = &(*ssh_data)[i].channel;
				if( ssh_channel_is_open( *channel ) ) {
					ssh_channel_send_eof( *channel );
					ssh_channel_close( *channel );
					ssh_channel_free( *channel );	
				}
				char *stdout_buf = (*ssh_data)[i].stdout_buf;
				if( stdout_buf != NULL ) {
					free( stdout_buf );
					stdout_buf = NULL;
				}
				char *stderr_buf = (*ssh_data)[i].stderr_buf;
				if( stderr_buf != NULL ) {
					free( stderr_buf );
					stderr_buf = NULL;
				}
			}
			if( scp_data != NULL ) {
				ssh_scp *scp = &(*scp_data)[i].scp;
				if( scp != NULL ) {
					ssh_scp_close( *scp );
					ssh_scp_free( *scp );
					scp = NULL;
				}
				char *filename = (*scp_data)[i].filename;
				if( filename != NULL ) {
					free( filename );
					filename = NULL;
				}
				char *buf = (*scp_data)[i].buf;
				if( buf != NULL ) {
					free( buf );
					buf = NULL;
				}
				scp_dir_stack_t *dir_stack = &(*scp_data)[i].dir_stack;
				if( dir_stack != NULL ) {
					free_dir_stack( dir_stack );
					dir_stack = NULL;
				}
				
			}
			ssh_disconnect( (*session)[i] );
			if( verbose ) {
				fprintf( stderr, "Disconnected from '%s', port %d..\n", host[i], port[i] );
			}				
		}
		ssh_free( (*session)[i] );
	}
	if( ssh_data != NULL ) {
		free( *ssh_data );
		*ssh_data = NULL;
	}
	if( scp_data != NULL ) {
		free( *scp_data );
		*scp_data = NULL;
	}
	free( *session );
	for( unsigned int i = 0; i < nof_sessions; i++ ) {
		free( host[i] );
	}
	free( *host );
	free( port );

	ssh_finalize( );
}

static const char *buffer_contains_a_line( const char *buffer, size_t bufsize )
{
	for( const char *p = buffer; ( p < buffer + bufsize ) && *p != '\0'; p++ ) {
		if( *p == '\n' ) {
			return p;
		}
	}
	return NULL;
}

static ssize_t output_buffer( FILE *f, const char *host, const unsigned int port, const char *buffer, const size_t bufsize, const bool tagging )
{	
	const char *b = buffer;
	size_t bsize = bufsize;
	ssize_t nwritten = 0;
	const char *p;
	while( ( b < buffer + bufsize ) && ( p = buffer_contains_a_line( b, bsize ) ) != NULL) {
		if( tagging ) {
			if( port != get_default_ssh_port( ) ) {
				fprintf( f, "[%s:%d]: ", host, port );
			} else {
				fprintf( f, "[%s]: ", host );
			}
			fflush( f );
		}
		
		ssize_t n = p - b;
		ssize_t wrc = fwrite( b, 1, n, f );
		if( wrc < 0 ) {
			return wrc;
		}
		if( wrc != n ) {
			fprintf( stderr, "ERROR: Buffer write mismatch on %s (%zu != %zu)\n",
				( f == stdout ) ? "stdout" : "stderr",
				wrc, n );
			return -1;
		}
		nwritten += n + 1;
		bsize -= n + 1;
		p++;
		b = p;	
		
		fprintf( f, "\n" );
	}

	assert( nwritten <= bufsize );	
	return nwritten;
}

int main( int argc, char *argv[] )
{
	struct gengetopt_args_info args_info;
	ssh_session *session;
	int rc;

	execution_mode_e execution_mode = determine_execution_mode( argv[0] );
	switch( execution_mode ) {
		case CSSH_EXECUTE_UNKNOWN:
			fprintf( stderr, "ERROR: binary name unknown, can't determine execution mode\n" );
			exit( EXIT_FAILURE );

		case CSSH_EXECUTE_AS_SSH:
			break;

		case CSSH_EXECUTE_AS_SCP:
			break;

		case CSSH_EXECUTE_AS_CLI:
			break;
	}

	if( parse_options_and_arguments( argc, argv, &args_info ) != 0 ) {
		exit( EXIT_FAILURE );
	}

	ssh_init( );
	
	if( args_info.long_version_given ) {
		printf( "cssh version: %s, Copyright (c) 2015-2021, GPLv3, Andreas Baumann <mail at andreasbaumann dot cc>\n", CSSH_VERSION );
		printf( "libssh version: %s\n", ssh_copyright( ) );
		printf( "linenoise: BSD license, Copyright (c) 2010-2014, Salvatore Sanfilippo <antirez at gmail dot com> and "
			"Copyright (c) 2010-2013, Pieter Noordhuis <pcnoordhuis at gmail dot com>\n" );
		exit( EXIT_SUCCESS );
	}
	
	// command line parsing and reading of additional files like the hosts file
	
	unsigned int nof_sessions = 0;
	char **host = NULL;
	unsigned int *port = NULL;
	char *cmd = NULL;
	copy_direction_e copy_direction = CSSH_COPY_DIRECTION_DOWNLOAD;
	char *local_directory = NULL;
	char *remote_directory = NULL;
	bool first_interactive = true;
	char history_filename[1024];
	
	switch( execution_mode ) {
		case CSSH_EXECUTE_AS_SSH: {
			
			cmd = (char *)malloc( DEFAULT_CMD_SIZE );

			unsigned int command_pos = 0;

			// command line arguments are '[user@]host'
			if( !args_info.hosts_file_given ) {
				unsigned int default_port = get_default_ssh_port( );
				if( args_info.port_given ) {
					default_port = args_info.port_arg;
				}
				host = (char **)malloc( sizeof( char * ) );
				port = (unsigned int *)malloc( sizeof( unsigned int ) );
				if( args_info.inputs_num >= 1 ) {
					command_pos++;
					nof_sessions = 1;
					host[0] = strdup( args_info.inputs[0] );
					port[0] = default_port;
				} else {
					host[0] = strdup( "localhost" );
					port[0] = default_port;
				}
			}

			// compose command to be executed in SSH mode
			cmd[0] = '\0';
			if( args_info.inputs_num > 0 ) {
				for( int i = command_pos; i < args_info.inputs_num; i++ ) {
					if( i != command_pos ) {
						strncat( cmd, " ", DEFAULT_CMD_SIZE - strlen( cmd ) - 1 );
					}
					strncat( cmd, args_info.inputs[i], DEFAULT_CMD_SIZE - strlen( cmd ) - 1 );
				}
			}
			
			// later, this is the sign to get into CLI mode
			if( cmd[0] == '\0' ) {
				execution_mode = CSSH_EXECUTE_AS_CLI;
			}		
		} break;

		case CSSH_EXECUTE_AS_SCP: {
			// in SCP mode read source and destination
			if( args_info.inputs_num != 2 ) {
				if( args_info.inputs_num < 2 ) {
					fprintf( stderr, "ERROR: Expecting a source and a destination when copying, encountered less arguments\n" );
					exit( EXIT_FAILURE );
				} else {
					fprintf( stderr, "ERROR: Expecting a source and a destination when copying, encountered more arguments\n" );
					exit( EXIT_FAILURE );
				}
			}

			// parse 2 arguments and see where we have a local
			// path and where a host destination with path
			// host destination can be explicit (one without -H hosts
			// parameter) or it is a placeholder host user@host:/dira/dirb,
			// so :/adir is a way or user@:/adir or host:/adir or user@host:adir
			// also host: and user@host: for home dir on other side, this means
			// we also have user@: for home on all hosts in -H hosts or
			// just : for the home directory on all hosts in -H hosts.
			// seems quite logical (at least to me :-))

			unsigned int default_port = get_default_ssh_port( );
			host = (char **)malloc( sizeof( char * ) );
			port = (unsigned int *)malloc( sizeof( unsigned int ) );
			if( args_info.port_given ) {
				default_port = args_info.port_arg;
			}
			
			char *s = strdup( args_info.inputs[0] );
			char *s2 = NULL;
			char *p = s;
			while( *p != '\0' ) {
				if( *p == ':' ) {
					*p = '\0';
					s2 = p+1;
					host[0] = strdup( s );
					port[0] = default_port;
					nof_sessions = 1;
					copy_direction = CSSH_COPY_DIRECTION_DOWNLOAD;
				}
				p++;
			}
			if( s2 != NULL ) {
				if( nof_sessions > 0 ) {
					remote_directory = strdup( s2 );
				} else {
					local_directory = strdup( s2 );
				}
			} else {
				local_directory = strdup( s );
			}				
			free( s );
			
			s = strdup( args_info.inputs[1] );
			p = s;
			s2 = NULL;
			while( *p != '\0' ) {
				if( *p == ':' ) {
					if( nof_sessions == 0 ) {
						*p = '\0';
						s2 = p+1;
						host[0] = strdup( s );
						port[0] = default_port;
						nof_sessions = 1;
						copy_direction = CSSH_COPY_DIRECTION_UPLOAD;
					} else {
						free( s );
						fprintf( stderr, "ERROR: specified a host for source and destination, please use a local directory for one of them\n" );
						exit( EXIT_FAILURE );
					}
				}
				p++;
			}
			if( s2 != NULL ) {
				if( nof_sessions > 0 ) {
					remote_directory = strdup( s2 );
				} else {
					local_directory = strdup( s2 );
				}
			} else {
				local_directory = strdup( s );
			}
			free( s );			
						
		} break;
		
		case CSSH_EXECUTE_AS_CLI:
		case CSSH_EXECUTE_UNKNOWN:
		break;
	}
	
	// till now we have a default host, port and user and exactly one session,
	// now inspect the hosts file for hosts
	if( args_info.hosts_file_given ) {
		// the -l <user> and -p <port> parameters serve as defaults
		// for the hosts in the file containing hosts
		
		unsigned nof_hosts;
		unsigned int default_port = get_default_ssh_port( );
		if( args_info.port_given ) {
			default_port = args_info.port_arg;
		}		
		rc = read_hosts_file( args_info.hosts_file_arg, default_port, &host, &port, &nof_hosts );
		if( rc < 0 ) {
			exit( EXIT_SUCCESS );
		}
		nof_sessions = nof_hosts;
	}
	
	// initialize session with host, port and user parameters read
			
	session = (ssh_session *)calloc( nof_sessions, sizeof( ssh_session ) );
	if( session == NULL ) {
		fprintf( stderr, "ERROR: Memory allocation failed for ssh_sessions\n" );
		exit( EXIT_SUCCESS );
	}
	for( unsigned int i = 0; i < nof_sessions; i++ ) {
		session[i] = ssh_new( );
		if( session[i] == NULL ) {
			exit( EXIT_FAILURE );
		}
	}
 
	int verbosity = SSH_LOG_NOLOG;
	if( args_info.verbose_given > 1 ) {
		verbosity += args_info.verbose_given;
	}

	for( unsigned int i = 0; i < nof_sessions; i++ ) {
		ssh_set_blocking( session[i], 0 );
		ssh_options_set( session[i], SSH_OPTIONS_HOST, host[i] );
		ssh_options_set( session[i], SSH_OPTIONS_PORT, &port[i] );
		if( args_info.login_given > 0 ) {
			ssh_options_set( session[i], SSH_OPTIONS_USER, args_info.login_arg );
		}
		ssh_options_set( session[i], SSH_OPTIONS_LOG_VERBOSITY, &verbosity );
		ssh_options_set( session[i], SSH_OPTIONS_PUBLICKEY_ACCEPTED_TYPES, "rsa-sha2-256,rsa-sha2-512,ecdh-sha2-nistp256,ssh-rsa" );
	}

	for( unsigned int i = 0; i < nof_sessions; i++ ) {
		if( args_info.verbose_given ) {
			fprintf( stderr, "Connecting to '%s', port %d..\n", host[i], port[i] );
		}
	}
	
	// asynchonous connection phase
	
	connection_state_e *connection_state = (connection_state_e *)calloc( nof_sessions, sizeof( connection_state_e ) );
	if( connection_state == NULL ) {
		fprintf( stderr, "ERROR: Memory allocation failed for 'connection_state'\n" );
		finalize( &session, NULL, NULL, host, port, nof_sessions, args_info.verbose_given > 0 );
		exit( EXIT_SUCCESS );
	}
	unsigned int nof_connected = 0;
	while( nof_connected < nof_sessions ) {
		nof_connected = 0;
		for( unsigned int i = 0; i < nof_sessions; i++ ) {
			switch( connection_state[i] ) {
				case CSSH_CONNECTION_STATE_UNCONNECTED:
					rc = ssh_connect( session[i] );
					if( rc == SSH_OK ) {
						connection_state[i] = CSSH_CONNECTION_STATE_CONNECTED;
						//~ ssh_set_blocking( session[i], 1 );
						if( args_info.verbose_given ) {
							fprintf( stderr, "Connected to '%s', port %d..\n", host[i], port[i] );
						}
					} else if( rc == SSH_AGAIN ) {
						// not connected yet
					} else {
						fprintf( stderr, "ERROR: error connecting to '%s', port '%d': %s\n",
							host[i], port[i], ssh_get_error( session[i] ) );
						if( args_info.ignore_connect_errors_given ) {
							connection_state[i] = CSSH_CONNECTION_STATE_ERROR;
						} else {
							finalize( &session, NULL, NULL, host, port, nof_sessions, args_info.verbose_given > 0 );
							exit( EXIT_FAILURE );
						}
					}
					break;
				case CSSH_CONNECTION_STATE_CONNECTED:
				case CSSH_CONNECTION_STATE_ERROR:
					// good, already connected or we ignore errors
					nof_connected++;
					break;				
			}
		}
		
		cssh_msleep( 10 );
	}
	
	// authentication phase
	
	auth_state_e *auth_state = (auth_state_e *)calloc( nof_sessions, sizeof( auth_state_e ) );
	if( auth_state == NULL ) {
		fprintf( stderr, "ERROR: Memory allocation failed for 'auth_state'\n" );
		finalize( &session,  NULL, NULL, host, port, nof_sessions, args_info.verbose_given > 0 );
		exit( EXIT_FAILURE );
	}
	unsigned int nof_authenticated = 0;
	while( nof_authenticated < nof_connected ) {
		for( unsigned int i = 0; i < nof_sessions; i++ ) {
			if( connection_state[i] != CSSH_CONNECTION_STATE_CONNECTED ) {
				continue;
			}
			switch( auth_state[i] ) {
				case CSSH_AUTH_INIT:
					if( verify_knownhost( session[i] ) < 0 ) {
						fprintf( stderr, "ERROR: closing connection to '%s', port '%d' due to security reasons\n",
							host[i], port[i] );
						finalize( &session,  NULL, NULL, host, port, nof_sessions, args_info.verbose_given > 0 );
						exit( EXIT_FAILURE );
					}
					auth_state[i] = CSSH_AUTH_VERIFY_HOST_DONE;
					break;
					
				case CSSH_AUTH_VERIFY_HOST_DONE:
					// returns Failed to request "ssh-userauth" service, not handling SSH_AUTH_AGAIN correctly internally?
					ssh_set_blocking( session[i], 1 );
					rc = ssh_userauth_none( session[i], NULL );
					if( rc == SSH_AUTH_SUCCESS ) {
						auth_state[i] = CSSH_AUTH_NONE_DONE;
						ssh_set_blocking( session[i], 1 );
					} else if( rc == SSH_AUTH_AGAIN ) {
						// ok, wait next iteration
					} else if( rc == SSH_AUTH_DENIED ) {
						auth_state[i] = CSSH_AUTH_NONE_DONE;
						//~ ssh_set_blocking( session[i], 0 );
					} else {
						fprintf( stderr, "ERROR: ssh_userauth_none to '%s', port '%d' failed: %s\n",
							host[i], port[i], ssh_get_error( session[i] ) );
						finalize( &session, NULL, NULL, host, port, nof_sessions, args_info.verbose_given > 0 );
						exit( EXIT_FAILURE );
					}
					break;
				
				case CSSH_AUTH_NONE_DONE: {
					ssh_set_blocking( session[i], 1 );
					char *banner = ssh_get_issue_banner( session[i] );
					if( banner ) {
						puts( banner );
						free( banner );
					}
					auth_state[i] = CSSH_AUTH_BANNER_DONE;
					} break;
				
				case CSSH_AUTH_BANNER_DONE:
					rc = authenticate_pubkey( session[i] );

					if( rc == SSH_AUTH_SUCCESS ) {
						auth_state[i] = CSSH_AUTH_PUBKEY_DONE_SUCCESS;
					} else if( rc == SSH_AUTH_AGAIN ) {
						// ok, wait next iteration
					} else if( rc == SSH_AUTH_DENIED ) {
						auth_state[i] = CSSH_AUTH_PUBKEY_DONE_FAILED;
					} else {
						finalize( &session, NULL, NULL, host, port, nof_sessions, args_info.verbose_given > 0 );
						exit( EXIT_FAILURE );
					}
					break;
				
				case CSSH_AUTH_PUBKEY_DONE_SUCCESS:
					auth_state[i] = CSSH_AUTH_DONE_SUCCESS;
					break;
				
				case CSSH_AUTH_PUBKEY_DONE_FAILED:
					if( args_info.allow_password_authentication_given ) {
						rc = authenticate_password( session[i], host[i], port[i], args_info.login_given ? args_info.login_arg : NULL );
						if( rc == SSH_AUTH_SUCCESS ) {
							auth_state[i] = CSSH_AUTH_PASSWORD_DONE_SUCCESS;
						} else if( rc == SSH_AUTH_AGAIN ) {
							// ok, wait next iteration
						} else if( rc == SSH_AUTH_DENIED ) {
							auth_state[i] = CSSH_AUTH_PASSWORD_DONE_FAILED;
						} else {
							finalize( &session, NULL, NULL, host, port, nof_sessions, args_info.verbose_given > 0 );
							exit( EXIT_FAILURE );
						}
					} else {
						auth_state[i] = CSSH_AUTH_DONE_FAILED;
					}						
					break;
				
				case CSSH_AUTH_PASSWORD_DONE_SUCCESS:
					auth_state[i] = CSSH_AUTH_DONE_SUCCESS;
					break;

				case CSSH_AUTH_PASSWORD_DONE_FAILED:
					auth_state[i] = CSSH_AUTH_DONE_FAILED;
					break;
					
				case CSSH_AUTH_DONE_SUCCESS:
					// ok
					nof_authenticated++;
					break;
				
				case CSSH_AUTH_DONE_FAILED:
					// one authentication failed, bail out for now
					fprintf( stderr, "ERROR: authentication failed for one host ('%s'), aborting now\n", host[i] );
					if( args_info.ignore_authentication_errors_given ) {
						connection_state[i] = CSSH_CONNECTION_STATE_ERROR;
					} else {
						finalize( &session, NULL, NULL, host, port, nof_sessions, args_info.verbose_given > 0 );
						exit( EXIT_FAILURE );
					}
			}
		}
		
		cssh_msleep( 10 );
	}
	
	// rearange session data in case we ignore errornous hosts
	unsigned int j = 0;
	for( unsigned int i = 0; i < nof_sessions; i++ ) {
		if( connection_state[i] == CSSH_CONNECTION_STATE_CONNECTED ) {
			if( i != j ) {
				connection_state[j] = connection_state[i];
				auth_state[j] = auth_state[i];
				session[j] = session[i];
				host[j] = host[i];
				port[j] = port[i];
			}
			j++;
		} else {
			fprintf( stderr, "ERROR: host '%s' has errors, ignoring it\n", host[i] );
		}
	}
	if( j != nof_sessions ) {
		nof_sessions = j;
		connection_state = (connection_state_e *)realloc( connection_state, nof_sessions * sizeof( connection_state_e ) );
		auth_state = (auth_state_e *)realloc( auth_state, nof_sessions * sizeof( auth_state_e ) );
		session = (ssh_session *)realloc( session, nof_sessions * sizeof( ssh_session ) );
	}
	
	// in CLI mode get next command from linenoise

CLI_NEXT_CMD:	
	if( execution_mode == CSSH_EXECUTE_AS_CLI ) {
		if( first_interactive ) {
			puts( "Entering interactive shell mode.. Press Ctrl-D or Ctrl-C to terminate." );
			first_interactive = false;
			linenoiseSetMultiLine( 1 );
			linenoiseHistorySetMaxLen( DEFAULT_CLI_HISTORY_LENGTH );

			char *home = getenv( "HOME" );
			if( home != NULL ) {
				snprintf( history_filename, sizeof( history_filename ), "%s/%s", home, HISTORY_FILE );
				linenoiseHistoryLoad( history_filename );
			}
		}
		cmd = linenoise( DEFAULT_PROMPT );
		if( cmd == NULL ) {
			puts( "EOF received.. Terminating interactive mode." );
			if( cmd != NULL ) linenoiseFree( cmd );
			linenoiseHistorySave( history_filename );
			goto SHELL_EOF;
		}
		linenoiseHistoryAdd( cmd );
	}

	// execution/copy phase

	switch( execution_mode ) {
		case CSSH_EXECUTE_AS_SSH:
		case CSSH_EXECUTE_AS_CLI: {

			// explicit low-level handling of channels in SSH mode

			ssh_data_t *ssh_data = (ssh_data_t *)calloc( nof_sessions, sizeof( ssh_data_t ) );
			if( ssh_data == NULL ) {
				fprintf( stderr, "ERROR: Memory allocation failed for ssh_data array\n" );
				finalize( &session, NULL, NULL, host, port, nof_sessions, args_info.verbose_given > 0 );
				exit( EXIT_FAILURE );
			}
			
			for( unsigned int i = 0; i < nof_sessions; i++ ) {
				ssh_data[i].channel = ssh_channel_new( session[i] );
				if( ssh_data[i].channel == NULL ) {
					fprintf( stderr, "ERROR: Unable to open SSH channel for host '%s': %s\n",
						host[i], ssh_get_error( session[i] ) );
					finalize( &session, &ssh_data, NULL, host, port, nof_sessions, args_info.verbose_given > 0 );
					exit( EXIT_FAILURE );
				}
			}

			for( unsigned int i = 0; i < nof_sessions; i++ ) {
				rc = ssh_channel_open_session( ssh_data[i].channel );
				if( rc != SSH_OK ) {
					finalize( &session, &ssh_data, NULL, host, port, nof_sessions, args_info.verbose_given > 0 );
					exit( EXIT_FAILURE );
				}
			}

			// execute command on all channels
			for( unsigned int i = 0; i < nof_sessions; i++ ) {
				rc = ssh_channel_request_exec( ssh_data[i].channel, cmd );
				if( rc != SSH_OK ) {
					fprintf( stderr, "ERROR: Executing SSH command '%s' failed: %s\n",
						cmd, ssh_get_error( session[i] ) );
					finalize( &session, &ssh_data, NULL, host, port, nof_sessions, args_info.verbose_given > 0 );
					exit( EXIT_FAILURE );
				}
			}

			bool all_eof = false;

			for( unsigned int i = 0; i < nof_sessions; i++ ) {
				ssh_data[i].eof_sent = false;
				ssh_data[i].is_eof = false;
				ssh_data[i].stdout_buf = (char *)malloc( BUFSIZE );
				if( ssh_data[i].stdout_buf == NULL ) {
					fprintf( stderr, "ERROR: Memory allocation failed for receiving buffers of ssh_channels\n" );
					finalize( &session, &ssh_data, NULL, host, port, nof_sessions, args_info.verbose_given > 0 );
					exit( EXIT_FAILURE );
				}
				ssh_data[i].stderr_buf = (char *)malloc( BUFSIZE );
				if( ssh_data[i].stderr_buf == NULL ) {
					fprintf( stderr, "ERROR: Memory allocation failed for receiving buffers of ssh_channels\n" );
					finalize( &session, &ssh_data, NULL, host, port, nof_sessions, args_info.verbose_given > 0 );
					exit( EXIT_FAILURE );
				}
				ssh_data[i].stdout_bptr = ssh_data[i].stdout_buf;
				ssh_data[i].stderr_bptr = ssh_data[i].stderr_buf;
			}
			
			while( !all_eof ) {

				all_eof = true;
				for( unsigned int i = 0; i < nof_sessions; i++ ) {
					if( !ssh_channel_is_closed( ssh_data[i].channel ) && !ssh_channel_is_eof( ssh_data[i].channel ) ) {
						all_eof = false;
					}
				}
				
				// no stdin sent to commands on remote machines for now
				for( unsigned int i = 0; i < nof_sessions; i++ ) {
					if( ssh_data[i].is_eof && !ssh_data[i].eof_sent ) {
						ssh_channel_send_eof( ssh_data[i].channel );
						ssh_data[i].eof_sent = true;
						continue;
					}
				}
				
				for( unsigned int i = 0; i < nof_sessions; i++ ) {
					
					// handle stdout
					rc = ssh_channel_poll( ssh_data[i].channel, 0 );
					if( rc == SSH_ERROR ) {
						fprintf( stderr, "ERROR: ssh_channel_poll on stdout failed: %s\n",
							ssh_get_error( session[i] ) );
						finalize( &session, &ssh_data, NULL, host, port, nof_sessions, args_info.verbose_given > 0 );
						exit( EXIT_FAILURE );
					}
										
					if( rc > 0 ) {
						unsigned int nread = ssh_channel_read_nonblocking( ssh_data[i].channel, ssh_data[i].stdout_bptr, BUFSIZE - ( ssh_data[i].stdout_bptr - ssh_data[i].stdout_buf ), 0 );
						if( nread == SSH_ERROR ) {
							fprintf( stderr, "ERROR: ssh_channel_read_nonblocking on stdout failed: %s\n",
								ssh_get_error( session[i] ) );
							finalize( &session, &ssh_data, NULL, host, port, nof_sessions, args_info.verbose_given > 0 );
							exit( EXIT_FAILURE );
						}
						
						if( nread == 0 ) {
							ssh_data[i].is_eof = true;
						}

						if( nread > 0 ) {
							ssize_t wrc = output_buffer( stdout, host[i], port[i], ssh_data[i].stdout_buf, nread + ( ssh_data[i].stdout_bptr - ssh_data[i].stdout_buf ), args_info.tagging_given );
							if( wrc < 0 ) {
								fprintf( stderr, "ERROR: while writing to stdout: %s\n",
									strerror( errno ) );
								finalize( &session, &ssh_data, NULL, host, port, nof_sessions, args_info.verbose_given > 0 );
								exit( EXIT_FAILURE );
							}
							assert( wrc <= nread + ( ssh_data[i].stdout_bptr - ssh_data[i].stdout_buf ) );
							memmove( ssh_data[i].stdout_buf, ssh_data[i].stdout_buf + wrc, nread + ( ssh_data[i].stdout_bptr - ssh_data[i].stdout_buf ) - wrc );
							ssh_data[i].stdout_bptr = ssh_data[i].stdout_buf + nread + ( ssh_data[i].stdout_bptr - ssh_data[i].stdout_buf ) - wrc;
						}
					}
					
					// handle stderr
					rc = ssh_channel_poll( ssh_data[i].channel, 1 );
					if( rc == SSH_ERROR ) {
						fprintf( stderr, "ERROR: ssh_channel_poll on stderr failed: %s\n",
							ssh_get_error( session[i] ) );
						finalize( &session, &ssh_data, NULL, host, port, nof_sessions, args_info.verbose_given > 0 );
						exit( EXIT_FAILURE );
					}
							
					if( rc > 0 ) {
						unsigned int nread = ssh_channel_read_nonblocking( ssh_data[i].channel, ssh_data[i].stderr_bptr, BUFSIZE - ( ssh_data[i].stderr_bptr - ssh_data[i].stderr_buf ), 1 );
						if( nread == SSH_ERROR ) {
							fprintf( stderr, "ERROR: ssh_channel_read_nonblocking on stderr failed: %s\n",
								ssh_get_error( session[i] ) );
							finalize( &session, &ssh_data, NULL, host, port, nof_sessions, args_info.verbose_given > 0 );
							exit( EXIT_FAILURE );
						}

						if( nread > 0 ) {
							ssize_t wrc = output_buffer( stderr, host[i], port[i], ssh_data[i].stderr_buf, nread + ( ssh_data[i].stderr_bptr - ssh_data[i].stderr_buf ), args_info.tagging_given );
							if( wrc < 0 ) {
								fprintf( stderr, "ERROR: while writting to stderr: %s\n",
									strerror( errno ) );
								finalize( &session, &ssh_data, NULL, host, port, nof_sessions, args_info.verbose_given > 0 );
								exit( EXIT_FAILURE );
							}
							assert( wrc <= nread + ( ssh_data[i].stderr_bptr - ssh_data[i].stderr_buf ) );
							memmove( ssh_data[i].stderr_buf, ssh_data[i].stderr_buf + wrc, nread + ( ssh_data[i].stderr_bptr - ssh_data[i].stderr_buf ) - wrc );
							ssh_data[i].stderr_bptr = ssh_data[i].stderr_buf + nread + ( ssh_data[i].stderr_bptr - ssh_data[i].stderr_buf ) - wrc;
						}

						if( nread == 0 ) {
							ssh_data[i].is_eof = true;
						}
					}
				}
				
				cssh_msleep( 1 );
			}
			
			if( execution_mode == CSSH_EXECUTE_AS_CLI ) {
				if( cmd != NULL ) linenoiseFree( cmd );
				goto CLI_NEXT_CMD;
			}

SHELL_EOF:		
			finalize( &session, &ssh_data, NULL, host, port, nof_sessions, args_info.verbose_given > 0 );

			} break;

		case CSSH_EXECUTE_AS_SCP: {
			
			scp_data_t *scp_data = (scp_data_t *)calloc( nof_sessions, sizeof( scp_data_t ) );
			if( scp_data == NULL ) {
				fprintf( stderr, "ERROR: Memory allocation failed for scp_data array\n" );
				finalize( &session, NULL, NULL, host, port, nof_sessions, args_info.verbose_given > 0 );
				exit( EXIT_FAILURE );
			}
			for( unsigned int i = 0; i < nof_sessions; i++ ) {
				scp_data[i].read_state = CSSH_SCP_READ_STATE_IDLE;
			}
			
			for( unsigned int i = 0; i < nof_sessions; i++ ) {
				scp_data[i].scp = ssh_scp_new( session[i],
					( ( copy_direction == CSSH_COPY_DIRECTION_UPLOAD ) ? SSH_SCP_WRITE : SSH_SCP_READ ) |
					( ( args_info.recursive_given > 0 ) ? SSH_SCP_RECURSIVE : 0 ),
					remote_directory );
				if( scp_data[i].scp == NULL ) {
					fprintf( stderr, "ERROR: Unable to open SCP channel: %s\n",
						ssh_get_error( session[i] ) );
					finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );
					exit( EXIT_FAILURE );
				}
			}

			for( unsigned int i = 0; i < nof_sessions; i++ ) {
				rc = ssh_scp_init( scp_data[i].scp );
				if( rc != SSH_OK ) {
					fprintf( stderr, "ERROR: Unable to initialize SCP sessions: %s\n",
						ssh_get_error( session[i] ) );
					finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );
					exit( EXIT_FAILURE );
				}
			}

			for( unsigned int i = 0; i < nof_sessions; i++ ) {
				scp_data[i].buf = (char *)malloc( BUFSIZE );
				if( scp_data[i].buf == NULL ) {
					fprintf( stderr, "ERROR: Memory allocation failed for receiving buffers of ssh_scp\n" );
					finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );
					exit( EXIT_FAILURE );
				}
			}
			
			cssh_progressbar_pool_t progressbars;
			rc = create_progressbar_pool( &progressbars, nof_sessions );
			if( rc < 0 ) {
				fprintf( stderr, "ERROR: Creation of a pool of progress bars failed\n" );
				finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );
				exit( EXIT_FAILURE );
			}

			bool all_eof = false;

			for( unsigned int i = 0; i < nof_sessions; i++ ) {
				create_dir_stack( &scp_data[i].dir_stack );
				
				char *pwd = getcwd( NULL, 0 );
				if( pwd == NULL ) {
					fprintf( stderr, "ERROR: failed to determine working directory for SCP for host '%s': %s\n",
						host[i], strerror( errno ) );
					finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );
					exit( EXIT_FAILURE );
				}
				
				rc = push_dir_stack( &scp_data[i].dir_stack, pwd );
				if( rc < 0 ) {
					fprintf( stderr, "ERROR: failed to push current directory to stack for host '%s'\n",
						host[i] );
					free( pwd );
					finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );
					exit( EXIT_FAILURE );
				}
				
				if( nof_sessions > 1 ) {
					size_t len = strlen( pwd ) + 2 + strlen( host[i] );
					char *full_path = (char *)malloc( len );
					if( full_path == NULL ) {
						fprintf( stderr, "ERROR: Memory allocation failed for full path of base directory for host '%s'\n",
							host[i] );
						free( pwd );
						finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );
						exit( EXIT_FAILURE );
					}
					snprintf( full_path, len, "%s/%s", pwd, host[i] );
					rc = push_dir_stack( &scp_data[i].dir_stack, full_path );
					scp_data[i].filename = full_path;
					
					if( rc < 0 ) {
						fprintf( stderr, "ERROR: failed remember initial base directory '%s' in directory stack\n",
							pwd );
						free( pwd );
						finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );
						exit( EXIT_FAILURE );
					}										
					free( pwd );
					
					rc = mkdir( full_path, 0750 );
					if( rc < 0 ) {
						if( errno != EEXIST ) {
							fprintf( stderr, "ERROR: failed to create base directory '%s' for host '%s': %s\n",
								full_path, host[i], strerror( errno ) );
							finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );
							exit( EXIT_FAILURE );
						}
					}
				} else {
					scp_data[i].filename = pwd;
				}
			}

			while( !all_eof ) {

				switch( copy_direction ) {
					case CSSH_COPY_DIRECTION_UPLOAD: {
						fprintf( stderr, "ERROR: SCP upload has not been implemented yet.\n" );
						finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );
						exit( EXIT_FAILURE );
						} break;

					case CSSH_COPY_DIRECTION_DOWNLOAD: {
						for( unsigned int i = 0; i < nof_sessions; i++ ) {
							switch( scp_data[i].read_state ) {
								case CSSH_SCP_READ_STATE_IDLE:
									rc = ssh_scp_pull_request( scp_data[i].scp );
									switch( rc ) {
										case SSH_SCP_REQUEST_NEWDIR: {
											char *dir = top_dir_stack( &scp_data[i].dir_stack );
											
											rc = chdir( dir );
											if( rc < 0 ) {
												fprintf( stderr, "ERROR: failed to change to directory '%s': %s\n",
													dir, strerror( errno ) );
												finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );
												exit( EXIT_FAILURE );
											}

											const char *filename =  ssh_scp_request_get_filename( scp_data[i].scp );
											int mode = ssh_scp_request_get_permissions( scp_data[i].scp );
											if( args_info.verbose_given ) {
												fprintf( stderr, "Receiving directory '%s' with permissions '0%o'\n",
													filename, mode );
											}
											rc = ssh_scp_accept_request( scp_data[i].scp );
											if( rc != SSH_OK ) {
												fprintf( stderr, "ERROR: accepting request for directory '%s' failed: %s\n",
													filename, ssh_get_error( session[i] ) );
												finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );
												exit( EXIT_FAILURE );
											}
											
											rc = mkdir( filename, mode );
											if( rc < 0 ) {
												fprintf( stderr, "ERROR: failed to create directory '%s': %s\n",
													filename, strerror( errno ) );
												finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );
												exit( EXIT_FAILURE );
											}
											char *pwd = getcwd( NULL, 0 );
											if( pwd == NULL ) {
												fprintf( stderr, "ERROR: failed to determine working directory for SCP for host '%s': %s\n",
													host[i], strerror( errno ) );
												finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );
												exit( EXIT_FAILURE );
											}
											size_t len = strlen( pwd ) + 2 + strlen( filename );
											char *full_path = (char *)malloc( len );
											if( full_path == NULL ) {
												fprintf( stderr, "ERROR: Memory allocation failed for full path of directory '%s'\n",
													filename );
												free( pwd );
												finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );
												exit( EXIT_FAILURE );
											}
											snprintf( full_path, len, "%s/%s", pwd, filename );
											
											rc = push_dir_stack( &scp_data[i].dir_stack, full_path );
											free( pwd );
											if( rc < 0 ) {
												fprintf( stderr, "ERROR: failed remember directory '%s' in directory stack\n",
													full_path );
												free( full_path );
												finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );
												exit( EXIT_FAILURE );
											}
											free( full_path );							
											} break;

										case SSH_SCP_REQUEST_ENDDIR:
											pop_dir_stack( &scp_data[i].dir_stack );
											break;

										case SSH_SCP_REQUEST_NEWFILE: {
											const char *filename = ssh_scp_request_get_filename( scp_data[i].scp );
											int mode = ssh_scp_request_get_permissions( scp_data[i].scp );
											scp_data[i].size = ssh_scp_request_get_size64( scp_data[i].scp );
											if( args_info.verbose_given ) {
												fprintf( stderr, "Receiving file '%s' with permissions '0%o' of size '%"PRIu64"'\n",
												filename, mode, scp_data[i].size );
											}
											rc = ssh_scp_accept_request( scp_data[i].scp );
											if( rc != SSH_OK ) {
												fprintf( stderr, "ERROR: accepting request for file '%s' failed: %s\n",
													filename, ssh_get_error( session[i] ) );
												finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );
												exit( EXIT_FAILURE );
											}

											char *dir = top_dir_stack( &scp_data[i].dir_stack );
											rc = chdir( dir );
											if( rc < 0 ) {
												fprintf( stderr, "ERROR: failed to change to directory '%s': %s\n",
													dir, strerror( errno ) );
												finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );
												exit( EXIT_FAILURE );
											}
											
											size_t len = strlen( dir ) + 2 + strlen( filename );
											char *full_path = (char *)malloc( len );
											if( full_path == NULL ) {
												fprintf( stderr, "ERROR: Memory allocation failed for full path of directory '%s'\n",
													filename );
												free( dir );
												finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );
												exit( EXIT_FAILURE );
											}
											snprintf( full_path, len, "%s/%s", dir, filename );
											
											int fd = open( full_path, O_WRONLY | O_CREAT | O_TRUNC, mode );
											if( fd < 0 ) {
												fprintf( stderr, "ERROR: Unable to open file '%s': %s\n",
													full_path, strerror( errno ) );
												finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );
												exit( EXIT_FAILURE );
											}

											scp_data[i].fd = fd;
											scp_data[i].filename = full_path;
											scp_data[i].bytesReceived = 0;
											scp_data[i].read_state = CSSH_SCP_READ_STATE_READING;
											
											len = strlen( host[i] ) + strlen( full_path ) + 5;
											rc = create_progressbar( &scp_data[i].progressbar, 0,  scp_data[i].size, len,
												"[%s:%s]", host[i], full_path );
											if( rc < 0 ) {
												fprintf( stderr, "ERROR: Unable to create progress bar for host '%s' and file '%s'\n",
													host[i], full_path );
												finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );
												exit( EXIT_FAILURE );
											}
											rc = append_progressbar_to_pool( &progressbars, &scp_data[i].progressbar );
												
											} break;

										case SSH_SCP_REQUEST_EOF:
											scp_data[i].read_state = CSSH_SCP_READ_STATE_EOF;
											all_eof = true;
											for( unsigned int i = 0; i < nof_sessions; i++ ) {
												if( scp_data[i].read_state != CSSH_SCP_READ_STATE_EOF ) {
													all_eof = false;
													break;
												}
											}
											break;

										case SSH_ERROR:
											fprintf( stderr, "ERROR: error from remote host '%s': %s\n",
												host[i], ssh_get_error( session[i] ) );
											finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );
											exit( EXIT_FAILURE );
											break;

										case SSH_SCP_REQUEST_WARNING:
											fprintf( stderr, "WARNING: remote host '%s': %s\n",
												host[i], ssh_scp_request_get_warning( scp_data[i].scp ) );
											finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );
											exit( EXIT_FAILURE );
											break;
									}
									break;
									
								case CSSH_SCP_READ_STATE_READING:
									if( scp_data[i].bytesReceived < scp_data[i].size ) {
										rc = ssh_scp_read( scp_data[i].scp, scp_data[i].buf, BUFSIZE );									
										if( rc == SSH_ERROR ) {
											fprintf( stderr, "ERROR: reading data for file '%s' failed: %s\n",
												scp_data[i].filename, ssh_get_error( session[i] ) );
											finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );
											exit( EXIT_FAILURE );
										}
										scp_data[i].bytesReceived += rc;
										if( rc > 0 ) {
											ssize_t r = write( scp_data[i].fd, scp_data[i].buf, rc );
											if( r < 0 ) {
												fprintf( stderr, "ERROR: writing data to file '%s' failed: %s\n",
													scp_data[i].filename, strerror( errno ) );
												finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );
												exit( EXIT_FAILURE );
											}
										}
										if( args_info.progress_bar_given > 0 ) {
											set_value_of_progressbar( &scp_data[i].progressbar, scp_data[i].bytesReceived );
										}
									} else {
										rc = close( scp_data[i].fd );
										if( rc < 0 ) {
											fprintf( stderr, "ERROR: Unable to close file '%s': %s\n",
												scp_data[i].filename, strerror( errno ) );
											finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );
											exit( EXIT_FAILURE );
										}
										scp_data[i].read_state = CSSH_SCP_READ_STATE_IDLE;
										scp_data[i].fd = 0;
										free( scp_data[i].filename );
										scp_data[i].filename = NULL;
										remove_progressbar_from_pool( &progressbars, &scp_data[i].progressbar );
										free_progressbar( &scp_data[i].progressbar );
									}
									break;
									
								case CSSH_SCP_READ_STATE_EOF:
									break;
							}
						}
						if( args_info.progress_bar_given ) {
							redraw_progressbars( &progressbars );
						}
						} break;
				}
			}

			finalize( &session, NULL, &scp_data, host, port, nof_sessions, args_info.verbose_given > 0 );

			} break;

		case CSSH_EXECUTE_UNKNOWN:
			break;
	}

	// the end

	free( connection_state );
	free( auth_state );

	cmdline_parser_free( &args_info );
	
	exit( EXIT_SUCCESS );
}
