diff --git a/phpseclib/Net/SSH2.php b/phpseclib/Net/SSH2.php index 264457f6..d68ed242 100644 --- a/phpseclib/Net/SSH2.php +++ b/phpseclib/Net/SSH2.php @@ -100,9 +100,10 @@ class SSH2 * @see \phpseclib\Net\SSH2::_get_channel_packet() * @access private */ - const CHANNEL_EXEC = 0; // PuTTy uses 0x100 - const CHANNEL_SHELL = 1; - const CHANNEL_SUBSYSTEM = 2; + const CHANNEL_EXEC = 0; // PuTTy uses 0x100 + const CHANNEL_SHELL = 1; + const CHANNEL_SUBSYSTEM = 2; + const CHANNEL_AGENT_FORWARD = 3; /**#@-*/ /**#@+ @@ -798,21 +799,6 @@ class SSH2 */ var $port; - /** - * Timeout for initial connection - * - * Set by the constructor call. Calling setTimeout() is optional. If it's not called functions like - * exec() won't timeout unless some PHP setting forces it too. The timeout specified in the constructor, - * however, is non-optional. There will be a timeout, whether or not you set it. If you don't it'll be - * 10 seconds. It is used by fsockopen() and the initial stream_select in that function. - * - * @see \phpseclib\Net\SSH2::__construct() - * @see \phpseclib\Net\SSH2::_connect() - * @var Integer - * @access private - */ - var $connectionTimeout; - /** * Number of columns for terminal window size * @@ -845,6 +831,14 @@ class SSH2 */ var $crypto_engine = false; + /** + * A System_SSH_Agent for use in the SSH2 Agent Forwarding scenario + * + * @var System_SSH_Agent + * @access private + */ + var $agent; + /** * Default Constructor. * @@ -929,7 +923,7 @@ class SSH2 $this->host = $host; $this->port = $port; - $this->connectionTimeout = $timeout; + $this->timeout = $timeout; } /** @@ -960,36 +954,24 @@ class SSH2 $this->bitmap |= self::MASK_CONSTRUCTOR; - $timeout = $this->connectionTimeout; + $this->curTimeout = $this->timeout; + $host = $this->host . ':' . $this->port; $this->last_packet = microtime(true); $start = microtime(true); - $this->fsock = @fsockopen($this->host, $this->port, $errno, $errstr, $timeout); + $this->fsock = @fsockopen($this->host, $this->port, $errno, $errstr, $this->curTimeout); if (!$this->fsock) { user_error(rtrim("Cannot connect to $host. Error $errno. $errstr")); return false; } $elapsed = microtime(true) - $start; - $timeout-= $elapsed; + $this->curTimeout-= $elapsed; - if ($timeout <= 0) { - user_error("Cannot connect to $host. Timeout error"); - return false; - } - - $read = array($this->fsock); - $write = $except = null; - - $sec = floor($timeout); - $usec = 1000000 * ($timeout - $sec); - - // on windows this returns a "Warning: Invalid CRT parameters detected" error - // the !count() is done as a workaround for - if (!@stream_select($read, $write, $except, $sec, $usec) && !count($read)) { - user_error("Cannot connect to $host. Banner timeout"); + if ($this->curTimeout <= 0) { + $this->is_timeout = true; return false; } @@ -1007,6 +989,27 @@ class SSH2 $extra.= $temp; $temp = ''; } + + if ($this->curTimeout) { + if ($this->curTimeout < 0) { + $this->is_timeout = true; + return false; + } + $read = array($this->fsock); + $write = $except = null; + $start = microtime(true); + $sec = floor($this->curTimeout); + $usec = 1000000 * ($this->curTimeout - $sec); + // on windows this returns a "Warning: Invalid CRT parameters detected" error + // the !count() is done as a workaround for + if (!@stream_select($read, $write, $except, $sec, $usec) && !count($read)) { + $this->is_timeout = true; + return false; + } + $elapsed = microtime(true) - $start; + $this->curTimeout-= $elapsed; + } + $temp.= fgets($this->fsock, 255); } @@ -2096,6 +2099,7 @@ class SSH2 */ function _ssh_agent_login($username, $agent) { + $this->agent = $agent; $keys = $agent->requestIdentities(); foreach ($keys as $key) { if ($this->_privatekey_login($username, $key)) { @@ -2275,6 +2279,7 @@ class SSH2 if (!$this->_send_binary_packet($packet)) { return false; } + $response = $this->_get_binary_packet(); if ($response === false) { user_error('Connection closed by server'); @@ -2441,6 +2446,24 @@ class SSH2 } } + /** + * Return an available open channel + * + * @return Integer + * @access public + */ + function _get_open_channel() + { + $channel = self::CHANNEL_EXEC; + do { + if (isset($this->channel_status[$channel]) && $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_OPEN) { + return $channel; + } + } while ($channel++ < self::CHANNEL_SUBSYSTEM); + + return false; + } + /** * Returns the output of an interactive shell * @@ -2799,18 +2822,41 @@ class SSH2 case NET_SSH2_MSG_CHANNEL_OPEN: // see http://tools.ietf.org/html/rfc4254#section-5.1 $this->_string_shift($payload, 1); extract(unpack('Nlength', $this->_string_shift($payload, 4))); - $this->errors[] = 'SSH_MSG_CHANNEL_OPEN: ' . utf8_decode($this->_string_shift($payload, $length)); - - $this->_string_shift($payload, 4); // skip over client channel + $data = $this->_string_shift($payload, $length); extract(unpack('Nserver_channel', $this->_string_shift($payload, 4))); + switch($data) { + case 'auth-agent': + case 'auth-agent@openssh.com': + if (isset($this->agent)) { + $new_channel = self::CHANNEL_AGENT_FORWARD; - $packet = pack('CN3a*Na*', - NET_SSH2_MSG_REQUEST_FAILURE, $server_channel, NET_SSH2_OPEN_ADMINISTRATIVELY_PROHIBITED, 0, '', 0, ''); + extract(unpack('Nremote_window_size', $this->_string_shift($payload, 4))); + extract(unpack('Nremote_maximum_packet_size', $this->_string_shift($payload, 4))); - if (!$this->_send_binary_packet($packet)) { - return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); + $this->packet_size_client_to_server[$new_channel] = $remote_window_size; + $this->window_size_server_to_client[$new_channel] = $remote_maximum_packet_size; + $this->window_size_client_to_server[$new_channel] = $this->window_size; + + $packet_size = 0x4000; + + $packet = pack('CN4', + NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION, $server_channel, $new_channel, $packet_size, $packet_size); + + $this->server_channels[$new_channel] = $server_channel; + $this->channel_status[$new_channel] = NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION; + if (!$this->_send_binary_packet($packet)) { + return false; + } + } + break; + default: + $packet = pack('CN3a*Na*', + NET_SSH2_MSG_REQUEST_FAILURE, $server_channel, NET_SSH2_OPEN_ADMINISTRATIVELY_PROHIBITED, 0, '', 0, ''); + + if (!$this->_send_binary_packet($packet)) { + return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); + } } - $payload = $this->_get_binary_packet(); break; case NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST: @@ -2947,48 +2993,59 @@ class SSH2 return ''; } - extract(unpack('Ctype/Nchannel', $this->_string_shift($response, 5))); + extract(unpack('Ctype', $this->_string_shift($response, 1))); - $this->window_size_server_to_client[$channel]-= strlen($response); - - // resize the window, if appropriate - if ($this->window_size_server_to_client[$channel] < 0) { - $packet = pack('CNN', NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST, $this->server_channels[$channel], $this->window_size); - if (!$this->_send_binary_packet($packet)) { - return false; - } - $this->window_size_server_to_client[$channel]+= $this->window_size; + if ($type == NET_SSH2_MSG_CHANNEL_OPEN) { + extract(unpack('Nlength', $this->_string_shift($response, 4))); + } else { + extract(unpack('Nchannel', $this->_string_shift($response, 4))); } - switch ($this->channel_status[$channel]) { - case NET_SSH2_MSG_CHANNEL_OPEN: - switch ($type) { - case NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION: - extract(unpack('Nserver_channel', $this->_string_shift($response, 4))); - $this->server_channels[$channel] = $server_channel; - extract(unpack('Nwindow_size', $this->_string_shift($response, 4))); - $this->window_size_client_to_server[$channel] = $window_size; - $temp = unpack('Npacket_size_client_to_server', $this->_string_shift($response, 4)); - $this->packet_size_client_to_server[$channel] = $temp['packet_size_client_to_server']; - return $client_channel == $channel ? true : $this->_get_channel_packet($client_channel, $skip_extended); - //case NET_SSH2_MSG_CHANNEL_OPEN_FAILURE: - default: - user_error('Unable to open channel'); - return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); + // will not be setup yet on incoming channel open request + if (isset($channel) && isset($this->channel_status[$channel]) && isset($this->window_size_server_to_client[$channel])) { + $this->window_size_server_to_client[$channel]-= strlen($response); + + // resize the window, if appropriate + if ($this->window_size_server_to_client[$channel] < 0) { + $packet = pack('CNN', NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST, $this->server_channels[$channel], $this->window_size); + if (!$this->_send_binary_packet($packet)) { + return false; } - break; - case NET_SSH2_MSG_CHANNEL_REQUEST: - switch ($type) { - case NET_SSH2_MSG_CHANNEL_SUCCESS: - return true; - case NET_SSH2_MSG_CHANNEL_FAILURE: - return false; - default: - user_error('Unable to fulfill channel request'); - return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); - } - case NET_SSH2_MSG_CHANNEL_CLOSE: - return $type == NET_SSH2_MSG_CHANNEL_CLOSE ? true : $this->_get_channel_packet($client_channel, $skip_extended); + $this->window_size_server_to_client[$channel]+= $this->window_size; + } + + switch ($this->channel_status[$channel]) { + case NET_SSH2_MSG_CHANNEL_OPEN: + switch ($type) { + case NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION: + extract(unpack('Nserver_channel', $this->_string_shift($response, 4))); + $this->server_channels[$channel] = $server_channel; + extract(unpack('Nwindow_size', $this->_string_shift($response, 4))); + $this->window_size_client_to_server[$channel] = $window_size; + $temp = unpack('Npacket_size_client_to_server', $this->_string_shift($response, 4)); + $this->packet_size_client_to_server[$channel] = $temp['packet_size_client_to_server']; + $result = $client_channel == $channel ? true : $this->_get_channel_packet($client_channel, $skip_extended); + $this->_on_channel_open(); + return $result; + //case NET_SSH2_MSG_CHANNEL_OPEN_FAILURE: + default: + user_error('Unable to open channel'); + return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); + } + break; + case NET_SSH2_MSG_CHANNEL_REQUEST: + switch ($type) { + case NET_SSH2_MSG_CHANNEL_SUCCESS: + return true; + case NET_SSH2_MSG_CHANNEL_FAILURE: + return false; + default: + user_error('Unable to fulfill channel request'); + return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); + } + case NET_SSH2_MSG_CHANNEL_CLOSE: + return $type == NET_SSH2_MSG_CHANNEL_CLOSE ? true : $this->_get_channel_packet($client_channel, $skip_extended); + } } // ie. $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_DATA @@ -3006,6 +3063,15 @@ class SSH2 */ extract(unpack('Nlength', $this->_string_shift($response, 4))); $data = $this->_string_shift($response, $length); + + if ($channel == self::CHANNEL_AGENT_FORWARD) { + $agent_response = $this->agent->_forward_data($data); + if (!is_bool($agent_response)) { + $this->_send_channel_packet($channel, $agent_response); + } + break; + } + if ($client_channel == $channel) { return $data; } @@ -3442,6 +3508,22 @@ class SSH2 return $this->log_boundary . str_pad(dechex(ord($matches[0])), 2, '0', STR_PAD_LEFT); } + /** + * Helper function for agent->_on_channel_open() + * + * Used when channels are created to inform agent + * of said channel opening. Must be called after + * channel open confirmation received + * + * @access private + */ + function _on_channel_open() + { + if (isset($this->agent)) { + $this->agent->_on_channel_open($this); + } + } + /** * Returns all errors * diff --git a/phpseclib/System/SSH/Agent.php b/phpseclib/System/SSH/Agent.php index 731cd66e..bac565f2 100644 --- a/phpseclib/System/SSH/Agent.php +++ b/phpseclib/System/SSH/Agent.php @@ -1,4 +1,5 @@ forward_status == self::FORWARD_NONE) { + $this->forward_status = self::FORWARD_REQUEST; + } + } + + /** + * Request agent forwarding of remote server + * + * @param Net_SSH2 $ssh + * @return Boolean + * @access private + */ + function _request_forwarding($ssh) + { + $request_channel = $ssh->_get_open_channel(); + if ($request_channel === false) { + return false; + } + + $packet = pack('CNNa*C', + NET_SSH2_MSG_CHANNEL_REQUEST, $ssh->server_channels[$request_channel], strlen('auth-agent-req@openssh.com'), 'auth-agent-req@openssh.com', 1); + + $ssh->channel_status[$request_channel] = NET_SSH2_MSG_CHANNEL_REQUEST; + + if (!$ssh->_send_binary_packet($packet)) { + return false; + } + + $response = $ssh->_get_channel_packet($request_channel); + if ($response === false) { + return false; + } + + $ssh->channel_status[$request_channel] = NET_SSH2_MSG_CHANNEL_OPEN; + $this->forward_status = self::FORWARD_ACTIVE; + + return true; + } + + /** + * On successful channel open + * + * This method is called upon successful channel + * open to give the SSH Agent an opportunity + * to take further action. i.e. request agent forwarding + * + * @param Net_SSH2 $ssh + * @access private + */ + function _on_channel_open($ssh) + { + if ($this->forward_status == self::FORWARD_REQUEST) { + $this->_request_forwarding($ssh); + } + } + + /** + * Forward data to SSH Agent and return data reply + * + * @param String $data + * @return data from SSH Agent + * @access private + */ + function _forward_data($data) + { + if ($this->expected_bytes > 0) { + $this->socket_buffer.= $data; + $this->expected_bytes -= strlen($data); + } else { + $agent_data_bytes = current(unpack('N', $data)); + $current_data_bytes = strlen($data); + $this->socket_buffer = $data; + if ($current_data_bytes != $agent_data_bytes + 4) { + $this->expected_bytes = ($agent_data_bytes + 4) - $current_data_bytes; + return false; + } + } + + if (strlen($this->socket_buffer) != fwrite($this->fsock, $this->socket_buffer)) { + user_error('Connection closed attempting to forward data to SSH agent'); + } + + $this->socket_buffer = ''; + $this->expected_bytes = 0; + + $agent_reply_bytes = current(unpack('N', fread($this->fsock, 4))); + + $agent_reply_data = fread($this->fsock, $agent_reply_bytes); + $agent_reply_data = current(unpack('a*', $agent_reply_data)); + + return pack('Na*', $agent_reply_bytes, $agent_reply_data); + } } diff --git a/tests/Functional/Net/SSH2AgentTest.php b/tests/Functional/Net/SSH2AgentTest.php index ea10110e..f9b03b46 100644 --- a/tests/Functional/Net/SSH2AgentTest.php +++ b/tests/Functional/Net/SSH2AgentTest.php @@ -30,5 +30,26 @@ class Functional_Net_SSH2AgentTest extends PhpseclibFunctionalTestCase $ssh->login($this->getEnv('SSH_USERNAME'), $agent), 'SSH2 login using Agent failed.' ); + + return array('ssh' => $ssh, 'ssh-agent' => $agent); + } + + /** + * @depends testAgentLogin + */ + public function testAgentForward($args) + { + $ssh = $args['ssh']; + $agent = $args['ssh-agent']; + + $hostname = $this->getEnv('SSH_HOSTNAME'); + $username = $this->getEnv('SSH_USERNAME'); + + $this->assertEquals($username, trim($ssh->exec('whoami'))); + + $agent->startSSHForwarding($ssh); + $this->assertEquals($username, trim($ssh->exec("ssh " . $username . "@" . $hostname . ' \'whoami\''))); + + return $args; } } diff --git a/travis/setup-secure-shell.sh b/travis/setup-secure-shell.sh index fdcaaa0d..22395aa7 100755 --- a/travis/setup-secure-shell.sh +++ b/travis/setup-secure-shell.sh @@ -28,4 +28,6 @@ ssh-add "$HOME/.ssh/id_rsa" # Allow the private key of the travis user to log in as phpseclib user sudo mkdir -p "/home/$USERNAME/.ssh/" sudo cp "$HOME/.ssh/id_rsa.pub" "/home/$USERNAME/.ssh/authorized_keys" +sudo ssh-keyscan -t rsa localhost > "/tmp/known_hosts" +sudo cp "/tmp/known_hosts" "/home/$USERNAME/.ssh/known_hosts" sudo chown "$USERNAME:$USERNAME" "/home/$USERNAME/.ssh/" -R