=pod
	Net::SFTP::Expect
	by Jonas Bull
	ripped off from Net::SSH::Expect and 90% of the credit goes there
	Anything that is broken due to lack up understanding Expect is attributable to me
=cut
package Net::SFTP::Expect;
use 5.008000;
use warnings;
use strict;
use fields qw(
	host user password port no_terminal escape_char ssh_option
	raw_pty exp_internal exp_debug log_file log_stdout restart_timeout_upon_receive
	timeout terminator expect debug next_line before match after binary prompt
);
use Expect;
use Carp;
use POSIX qw(:signal_h WNOHANG);

our $VERSION = '0.9';

# error contants
use constant ILLEGAL_STATE => "IllegalState";
use constant ILLEGAL_STATE_NO_SFTP_CONNECTION => "IllegalState: you don't have a valid SSH connection to the server";
use constant ILLEGAL_ARGUMENT => "IllegalArgument";
use constant SFTP_AUTHENTICATION_ERROR => "SSHAuthenticationError";
use constant SFTP_PROCESS_ERROR => "SSHProcessError";
use constant SFTP_CONNECTION_ERROR => "SSHConnectionError";
use constant SFTP_CONNECTION_ABORTED => "SSHConnectionAborted";

sub new {
    my $type = shift;
	my %args = @_;
    my Net::SFTP::Expect $self = fields::new(ref $type || $type);
	
	# Options used to configure the SFTP command
    $self->{host} 			= $args{host}|| undef; 
    $self->{user}  			= $args{user} || $ENV{'USER'}; 
    $self->{password} 		= $args{password} || undef;
	$self->{port}			= $args{port} || undef;			# ssh -p
	$self->{no_terminal}	= $args{no_terminal} || 1; 		# ssh -T
	$self->{escape_char}	= $args{escape_char} || undef; 	# ssh -e
	$self->{ssh_option}		= $args{ssh_option} || undef;	# arbitrary ssh options
	$self->{binary}			= $args{binary} || "sftp"; 		# path to SFTP binary.
	
	# Options used to configure the Expect object
	$self->{raw_pty}		= $args{raw_pty} || 0;
	$self->{exp_internal}	= $args{exp_internal} || 0;
	$self->{exp_debug}		= $args{exp_debug} || 0;
	$self->{log_file} 		= $args{log_file} || undef;
	$self->{log_stdout}		= $args{log_stdout} || 0;
	$self->{restart_timeout_upon_receive} = $args{restart_timeout_upon_receive} || 0;

	# Attributes for this module 
	$self->timeout(defined $args{timeout} ? $args{timeout} : 2);
	$self->{terminator} 	= $args{terminator} || "\n";
	$self->{next_line}		= "";
	$self->{expect}			= undef;	# this will hold the Expect instance
	$self->{debug}			= $args{debug} || 0;
	$self->{before}			= "";
	$self->{match}			=  $args{match} || '';
	$self->{prompt}			=  $args{prompt} || '';
	$self->{after}			= "";

	# validating the user input
	foreach my $key (keys %args) {
		if (! exists $self->{$key} ) {
			croak ILLEGAL_ARGUMENT . " attribute '$key' is not a valid constructor argument.";
		}
	}

	return $self;
}

# boolean run_sftp() - forks the ssh client process opening an ssh connection to the SFTP server.
#
#	This method has three roles:
#	1) 	Instantiate a new Expect object configuring it with all the defaults and user-defined
#		settings.
#	2)	Define the ssh command line using the defaults and user-defined settings
#	3)	Fork the ssh process using the spawn() method of the Expect instance we created. 
#		The SFTP connection is established on this step using the user account set in the 'user'
#		constructor attribute. No password is sent here, that happens only in the login() method.
#
#	This method is run internally by the login() method so you don't need to run it yourself
#	in most of the cases. You'll run this method alone if you had set up public-key authentication 
#	between the ssh client and the ssh server. In this case you only need to call this method
#	to have an authenticated ssh connection, you won't call login(). Note that when you 
#	use public-key authentication you won't need to set the 'password' constructor attribute
#	but you still need to define the 'user' attribute.
#	If you don't know how to setup public-key authentication there's a good guide at
#	http://sial.org/howto/openssh/publickey-auth/
#		
# returns:
#	boolean: 1 if the ssh ran OK or 0 otherwise. In case of failures, use $! to do get info.
sub run_sftp {
	my Net::SFTP::Expect $self = shift;

	my $user = $self->{user};
	my $host = $self->{host};

	croak(ILLEGAL_STATE . " field 'host' is not set.") unless $host;
	croak(ILLEGAL_STATE . " field 'user' is not set.") unless $user;

	my $log_file = $self->{log_file};
	my $log_stdout = $self->{log_stdout};
	my $exp_internal = $self->{exp_internal};
	my $exp_debug = $self->{exp_debug};
	my $no_terminal = $self->{no_terminal};
	my $raw_pty = $self->{raw_pty};
	my $escape_char = $self->{escape_char};
	my $ssh_option = $self->{ssh_option};
	my $port = $self->{port};
	my $rtup = $self->{restart_timeout_upon_receive};

	# Gather flags.
	my $flags = "";
	#$flags .= $escape_char ? "-e '$escape_char' " : "-e none ";
	$flags .= "-p $port " if $port;
	$flags .= $ssh_option if $ssh_option;
	# this sets the ssh command line
	my $ssh_string = $self->{binary} . " $flags $user\@$host";
#	warn "SSH string set: $ssh_string";
	# creating the Expect object
	my $exp = new Expect();
	# saving this instance
	$self->{expect} = $exp; 
	# configuring the expect object
	$exp->log_stdout($log_stdout);
	$exp->log_file($log_file, "w") if $log_file;
	$exp->exp_internal($exp_internal);
	$exp->debug($exp_debug);
	$exp->raw_pty($raw_pty);	
	$exp->restart_timeout_upon_receive($rtup);
	my $success = $exp->spawn($ssh_string); 
	return (defined $success);
}

sub login {
    my Net::SFTP::Expect $self = shift;

	# setting the default values for the parameters
	my ($login_prompt, $password_prompt, $test_success) = ( qr/ogin:\s*$/, qr/[Pp]assword.*?:|[Pp]assphrase.*?:/, 0);
	
	# attributing the user defined values
	if (@_ == 2 || @_ == 3) {
		$login_prompt = shift;
		$password_prompt = shift;
	}
	if (@_ == 1) {
		$test_success = shift;
	}
	my $user = $self->{user};
	my $password = $self->{password};
	my $timeout = $self->{timeout};
	my $t = $self->{terminator};

	croak(ILLEGAL_STATE . " field 'user' is not set.") unless $user;
	croak(ILLEGAL_STATE . " field 'password' is not set.") unless $password;

	# spawns the ssh process if this wasn't done yet
	if (! defined($self->{expect})) {
		$self->run_sftp() or croak SFTP_PROCESS_ERROR . " Couldn't start ssh: $!\n";
	}

	my $exp = $self->get_expect();
	# loggin in
	$self->_sec_expect($timeout,
		[ qr/\(yes\/no\)\?\s*$/ => sub { $exp->send("yes$t"); exp_continue; } ],
		[ $password_prompt		=> sub { $exp->send("$password$t"); } ],
		[ $login_prompt         => sub { $exp->send("$user$t"); exp_continue; } ],
		[ qr/REMOTE HOST IDEN/  => sub { print "FIX: .ssh/known_hosts\n"; exp_continue; } ],
		[ timeout				=> sub 
			{ 
				croak SFTP_AUTHENTICATION_ERROR . " Login timed out. " .
				"The input stream currently has the contents bellow: " .
				$self->peek();
			} 
		]
	);
	# verifying if we failed to logon
	if ($test_success) {
		$self->_sec_expect($timeout, 
			[ $password_prompt  => 			
				sub { 
					my $error = $self->peek();
					croak(SFTP_AUTHENTICATION_ERROR . " Error: Bad password [$error]");
				}
			]
		);
	}
   	# swallows any output the server wrote to my input stream after loging in	
	return $self->read_all();
}
sub waitfor {
	my Net::SFTP::Expect $self = shift;
	my $pattern = shift;
	my $timeout = @_ ? shift : $self->{timeout};
	my $match_type = @_ ? shift : '-re';
	croak ( ILLEGAL_ARGUMENT . "match_type '$match_type' is invalid." )
		unless ($match_type eq '-re' || $match_type eq '-ex');

	my ($pos, $error);
	($pos, $error, $self->{match}, $self->{before}, $self->{after}) 
		= $self->_sec_expect($timeout, $match_type, $pattern);

	my $debug = $self->{debug};

	# sanity verification
	# Enforcing that match before and after have correct values
	if (! defined $pos) { # if the pattern failed to match
		# match should be undef
		if (defined $self->{match}) {
			if ($debug) {
				carp ("The last expect() didn't match but \$exp->match() returned content '". $self->{match} ."'." .
						" We'll set \$self->{match} to undef explicitly;");
			}
			$self->{match} = undef;
		}
		# after should be undef
		if (defined $self->{after}) {
			if ($debug) {
				carp ("The last expect() didn't match but \$exp->after() returned content '". $self->{after} ."'." .
						" We'll set \$self->{after} to undef explicitly;");
			}
			$self->{after} = undef;
		}
	} 
	
	return (defined $pos);
}

sub before {
	my Net::SFTP::Expect $self = shift;
	return $self->{before};
}

# string match() - returns the "match" data of the last waitfor() call, or undef if didn't match.
sub match {
	my Net::SFTP::Expect $self = shift;
	return $self->{match};
}

# string after() - returns the "after match" data of the last waitfor() call, or undef if didn't match.
sub after {
	my Net::SFTP::Expect $self = shift;
	return $self->{after};
}


# send ("string") - breaks on through to the other side.
sub send {
	my Net::SFTP::Expect $self = shift;
	my $send = shift;
	croak (ILLEGAL_ARGUMENT . " missing argument 'string'.") unless ($send);
	my $exp = $self->get_expect();
	my $t = $self->{terminator};
	$exp->send($send . $t);
}

# peek([$timeout]) - returns what is in the input stream without removing anything
#	params:
#		$timeout: how many seconds peek() will wait for input
# dies:
#	SFTP_CONNECTION_ABORTED if EOF is found (error type 2)
#	SFTP_PROCESS_ERROR if the ssh process has died (error type 3)
#	SFTP_CONNECTION_ERROR if unknown error (type 4) is found
sub peek {
	my Net::SFTP::Expect $self = shift;
	my $timeout = @_ ? shift : $self->{timeout};
	my $exp = $self->get_expect();
	$self->_sec_expect($timeout);
	return $exp->before();
}

#
sub eat {
	my Net::SFTP::Expect $self = shift;
	my $string = shift;
	unless (defined $string && $string ne "") {
		if ($self->{debug}) {
			carp ("eat(): param \$string is undef or empty string\n");
		}
		return $string;
	}

	my $exp = $self->get_expect();

	# the top of the input stream that will be removed from there and
	# returned to the user
	my $top;

	# eat $string from (hopefully) the head of the input stream
	$self->_sec_expect(0, '-ex', $string);
	$top .= $exp->match();

	# if before() returns any content, the $string passed is not in the beginning of the 
	# input stream.
	if (defined $exp->before() && !($exp->before() eq "") ) {
		if ($self->{debug}) {
			carp ("eat(): param \$string '$string' was found on the input stream ".
				"after '". $exp->before() . "'.");
		}
		$top = $exp->before() . $top; 
	}
	return $top;
}

# string read_all([$timeout]) - reads and remove all the output from the input stream.
# The reading/removing process will be interrupted after $timeout seconds of inactivity
# on the input stream.
sub read_all {
	my Net::SFTP::Expect $self = shift;
	my $timeout = @_ ? shift : $self->{timeout};
	my $out;
	while ($self->_sec_expect($timeout, '-re', qr/[\s\S]+/)) {
		my $tmp= $self->get_expect()->match();
		$out.=$tmp;
	}
	return $out;
}


# boolean has_line([$timeout]) - tells if there is one more line on the input stream
sub has_line {
	my Net::SFTP::Expect $self = shift;
	my $timeout = @_ ? shift : $self->{timeout};
	$self->{next_line} = $self->read_line($timeout);
	return (defined $self->{next_line});
}

sub read_line {
	my Net::SFTP::Expect $self = shift;
	my $timeout = @_ ? shift : $self->{timeout};
	my $t = $self->{terminator};
	my $line = undef;
	if ( $self->waitfor($t, $timeout) ) {
		$line = $self->before();
	}
	return $line;
}

# string exec($cmd [,$timeout]) - executes a command, returns the complete output
sub exec {
	my Net::SFTP::Expect $self = shift;
	my $cmd = shift;
	my $timeout = @_ ? shift : $self->{timeout};
	$self->send($cmd);
	return $self->read_all($timeout);
}
sub ls{
	my Net::SFTP::Expect $self = shift;
	my $timeout = @_ ? shift : $self->{timeout};
	$self->send('ls');
	my $prompt=$self->{prompt};
        my @out;
	my $tmp;
        while ($self->_sec_expect($timeout, '-re', qr/[\s\S]+/)) {
                $tmp.= $self->get_expect()->match();
        }
	for (split(/\n/,$tmp)){
               next if ($_=~/$prompt/);
               push(@out,$_);
	}
	return @out;
}
sub get{
	my Net::SFTP::Expect $self = shift;
	my $remote = shift;
	unless ($remote){ 
		warn "You must at least provide a remote file to 'get'.";
		return undef;
	}
	my $local = @_ ? shift : '';
	my $timeout = @_ ? shift : $self->{timeout};
	$self->send("get $remote $local");
	$self->read_all($timeout)=~/100%/ ? 1 : undef ;
}
sub put{
	my Net::SFTP::Expect $self = shift;
	my $local = shift;
	unless ($local){
		warn "You must at least provide a local file to 'put'.";
		return undef;
	}
	my $remote = @_ ? shift : '';
	my $timeout = @_ ? shift : $self->{timeout};
	$self->send("put $local $remote");
	$self->read_all($timeout)=~/100%/ ? 1 : undef ;
}
sub rename{
	my Net::SFTP::Expect $self = shift;
	my $oldname = shift;
	my $newname = shift;
	unless ($oldname && $newname){
		warn "You must provide both oldname and newname.";
		return undef;
	}
	my $timeout = @_ ? shift : $self->{timeout};
	$self->send("rename $oldname $newname");
	my $prompt=$self->{prompt};
	$self->read_all($timeout)=~/^$prompt.*$/ ? 1 : undef ;
}
sub mv{ # mv is an alias to rename() - just so I can be right!
	my Net::SFTP::Expect $self = shift;
	return $self->rename(@_);
}
sub close {
	my Net::SFTP::Expect $self = shift;
	my $exp = $self->get_expect();
	$exp->hard_close();
	return 1;
}
# returns 
#	reference: the internal Expect object used to manage the ssh connection.
sub get_expect {
	my Net::SFTP::Expect $self = shift;
	my $exp = defined ($self->{expect}) ? $self->{expect} : 
		croak (ILLEGAL_STATE_NO_SFTP_CONNECTION);
	return $exp;
}

# void restart_timeout_upon_receive( 0 | 1 ) - changes the timeout counter behaviour
# params:
#	boolean: if true, sets the timeout to "inactivity timeout", if false
#			sets it to "absolute timeout".
# dies:
#	IllegalParamenter if argument is not given.
sub restart_timeout_upon_receive {
	my Net::SFTP::Expect $self = shift;
	my $value = @_ ? shift : croak (ILLEGAL_ARGUMENT . " missing argument.");
	$self->get_expect()->restart_timeout_upon_receive($value);
}

#
# Setter methods
#

sub host {
	my Net::SFTP::Expect $self = shift;
	croak(ILLEGAL_ARGUMENT . " No host supplied to 'host()' method") unless @_;
	$self->{host} = shift;
}

sub user {
	my Net::SFTP::Expect $self = shift;
	croak(ILLEGAL_ARGUMENT . " No user supplied to 'user()' method") unless @_;
	$self->{user} =shift;
} 

sub password{
	my Net::SFTP::Expect $self = shift;
	croak(ILLEGAL_ARGUMENT . " No password supplied to 'password()' method") unless @_;
	$self->{password} = shift;
}

sub port {
	my Net::SFTP::Expect $self = shift;
	croak(ILLEGAL_ARGUMENT . " No value passed to 'port()' method") unless @_;
	my $port = shift;
	croak (ILLEGAL_ARGUMENT . " Passed number '$port' is not a valid port number") 
		if ($port !~ /\A\d+\z/ || $port < 1 || $port > 65535);
	$self->{port} = $port;
}

sub terminator {
	my Net::SFTP::Expect $self = shift;
	$self->{terminator} = shift if (@_);
	return $self->{terminator};
}

# boolean debug([0|1]) - gets/sets the $exp->{debug} attribute.
sub debug {
	my Net::SFTP::Expect $self = shift;
	if (@_) {
		$self->{debug} = shift;
	}
	return $self->{debug};
}

# number timeout([$number]) - get/set the default timeout used for every method 
# that reads data from the input stream. 
# The only exception is eat() that has its timeout defined as 0.
sub timeout {
	my Net::SFTP::Expect $self = shift;
	if (! @_ ) {
		return $self->{timeout};
	}
	my $timeout = shift;
	if ( $timeout !~ /\A\d+\z/ || $timeout < 0) {
		croak (ILLEGAL_ARGUMENT . " timeout '$timeout' is not a positive number.");
	}
	$self->{timeout} = $timeout;
}

#
# Private Methods 
#

# _sec_expect(@params) - secure expect. runs expect with @params and croaks if problems happen
# Note: timeout is not considered a problem.
# params:
#	the same parameters as expect() accepts.
# returns:
# 	the same as expect() returns
# dies:
#	SFTP_CONNECTION_ABORTED if EOF is found (error type 2)
#	SFTP_PROCESS_ERROR if the ssh process has died (error type 3)
#	SFTP_CONNECTION_ERROR if unknown error is found (error type 4)
sub _sec_expect {
	my Net::SFTP::Expect $self = shift;
	my @params = @_ ? @_ : die ("\@params cannot be undefined.");
	my $exp = $self->get_expect();
	my ($pos, $error, $match, $before, $after) = $exp->expect(@params);
	if (defined $error) {
		my $error_first_digit = substr($error, 0, 1);
		if ($error_first_digit eq '2') {	
			# found eof
			croak (SFTP_CONNECTION_ABORTED);
		} elsif ($error_first_digit eq '3') {  
			# ssh process died
			croak (SFTP_PROCESS_ERROR . " The ssh process was terminated.");
		} elsif ($error_first_digit eq '4') {   
			# unknown reading error
			croak (SFTP_CONNECTION_ERROR . " Reading error type 4 found: $error");
		}
	}
	if (wantarray()) {
		return ($pos, $error, $match, $before, $after);
	} else {
		return $pos;
	}
}

1;



