Merge branch '2.0' of https://github.com/phpseclib/phpseclib into openssl-support-2.0

Conflicts:
	phpseclib/Net/SSH2.php
This commit is contained in:
terrafrost 2015-04-02 05:37:04 -05:00
commit 023fef8d8b
4 changed files with 328 additions and 83 deletions

View File

@ -100,9 +100,10 @@ class SSH2
* @see \phpseclib\Net\SSH2::_get_channel_packet() * @see \phpseclib\Net\SSH2::_get_channel_packet()
* @access private * @access private
*/ */
const CHANNEL_EXEC = 0; // PuTTy uses 0x100 const CHANNEL_EXEC = 0; // PuTTy uses 0x100
const CHANNEL_SHELL = 1; const CHANNEL_SHELL = 1;
const CHANNEL_SUBSYSTEM = 2; const CHANNEL_SUBSYSTEM = 2;
const CHANNEL_AGENT_FORWARD = 3;
/**#@-*/ /**#@-*/
/**#@+ /**#@+
@ -798,21 +799,6 @@ class SSH2
*/ */
var $port; 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 * Number of columns for terminal window size
* *
@ -845,6 +831,14 @@ class SSH2
*/ */
var $crypto_engine = false; 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. * Default Constructor.
* *
@ -929,7 +923,7 @@ class SSH2
$this->host = $host; $this->host = $host;
$this->port = $port; $this->port = $port;
$this->connectionTimeout = $timeout; $this->timeout = $timeout;
} }
/** /**
@ -960,36 +954,24 @@ class SSH2
$this->bitmap |= self::MASK_CONSTRUCTOR; $this->bitmap |= self::MASK_CONSTRUCTOR;
$timeout = $this->connectionTimeout; $this->curTimeout = $this->timeout;
$host = $this->host . ':' . $this->port; $host = $this->host . ':' . $this->port;
$this->last_packet = microtime(true); $this->last_packet = microtime(true);
$start = 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) { if (!$this->fsock) {
user_error(rtrim("Cannot connect to $host. Error $errno. $errstr")); user_error(rtrim("Cannot connect to $host. Error $errno. $errstr"));
return false; return false;
} }
$elapsed = microtime(true) - $start; $elapsed = microtime(true) - $start;
$timeout-= $elapsed; $this->curTimeout-= $elapsed;
if ($timeout <= 0) { if ($this->curTimeout <= 0) {
user_error("Cannot connect to $host. Timeout error"); $this->is_timeout = true;
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 <https://bugs.php.net/42682>
if (!@stream_select($read, $write, $except, $sec, $usec) && !count($read)) {
user_error("Cannot connect to $host. Banner timeout");
return false; return false;
} }
@ -1007,6 +989,27 @@ class SSH2
$extra.= $temp; $extra.= $temp;
$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 <https://bugs.php.net/42682>
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); $temp.= fgets($this->fsock, 255);
} }
@ -2096,6 +2099,7 @@ class SSH2
*/ */
function _ssh_agent_login($username, $agent) function _ssh_agent_login($username, $agent)
{ {
$this->agent = $agent;
$keys = $agent->requestIdentities(); $keys = $agent->requestIdentities();
foreach ($keys as $key) { foreach ($keys as $key) {
if ($this->_privatekey_login($username, $key)) { if ($this->_privatekey_login($username, $key)) {
@ -2275,6 +2279,7 @@ class SSH2
if (!$this->_send_binary_packet($packet)) { if (!$this->_send_binary_packet($packet)) {
return false; return false;
} }
$response = $this->_get_binary_packet(); $response = $this->_get_binary_packet();
if ($response === false) { if ($response === false) {
user_error('Connection closed by server'); 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 * 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 case NET_SSH2_MSG_CHANNEL_OPEN: // see http://tools.ietf.org/html/rfc4254#section-5.1
$this->_string_shift($payload, 1); $this->_string_shift($payload, 1);
extract(unpack('Nlength', $this->_string_shift($payload, 4))); extract(unpack('Nlength', $this->_string_shift($payload, 4)));
$this->errors[] = 'SSH_MSG_CHANNEL_OPEN: ' . utf8_decode($this->_string_shift($payload, $length)); $data = $this->_string_shift($payload, $length);
$this->_string_shift($payload, 4); // skip over client channel
extract(unpack('Nserver_channel', $this->_string_shift($payload, 4))); 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*', extract(unpack('Nremote_window_size', $this->_string_shift($payload, 4)));
NET_SSH2_MSG_REQUEST_FAILURE, $server_channel, NET_SSH2_OPEN_ADMINISTRATIVELY_PROHIBITED, 0, '', 0, ''); extract(unpack('Nremote_maximum_packet_size', $this->_string_shift($payload, 4)));
if (!$this->_send_binary_packet($packet)) { $this->packet_size_client_to_server[$new_channel] = $remote_window_size;
return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); $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(); $payload = $this->_get_binary_packet();
break; break;
case NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST: case NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST:
@ -2947,48 +2993,59 @@ class SSH2
return ''; 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); if ($type == NET_SSH2_MSG_CHANNEL_OPEN) {
extract(unpack('Nlength', $this->_string_shift($response, 4)));
// resize the window, if appropriate } else {
if ($this->window_size_server_to_client[$channel] < 0) { extract(unpack('Nchannel', $this->_string_shift($response, 4)));
$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;
} }
switch ($this->channel_status[$channel]) { // will not be setup yet on incoming channel open request
case NET_SSH2_MSG_CHANNEL_OPEN: if (isset($channel) && isset($this->channel_status[$channel]) && isset($this->window_size_server_to_client[$channel])) {
switch ($type) { $this->window_size_server_to_client[$channel]-= strlen($response);
case NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION:
extract(unpack('Nserver_channel', $this->_string_shift($response, 4))); // resize the window, if appropriate
$this->server_channels[$channel] = $server_channel; if ($this->window_size_server_to_client[$channel] < 0) {
extract(unpack('Nwindow_size', $this->_string_shift($response, 4))); $packet = pack('CNN', NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST, $this->server_channels[$channel], $this->window_size);
$this->window_size_client_to_server[$channel] = $window_size; if (!$this->_send_binary_packet($packet)) {
$temp = unpack('Npacket_size_client_to_server', $this->_string_shift($response, 4)); return false;
$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);
} }
break; $this->window_size_server_to_client[$channel]+= $this->window_size;
case NET_SSH2_MSG_CHANNEL_REQUEST: }
switch ($type) {
case NET_SSH2_MSG_CHANNEL_SUCCESS: switch ($this->channel_status[$channel]) {
return true; case NET_SSH2_MSG_CHANNEL_OPEN:
case NET_SSH2_MSG_CHANNEL_FAILURE: switch ($type) {
return false; case NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION:
default: extract(unpack('Nserver_channel', $this->_string_shift($response, 4)));
user_error('Unable to fulfill channel request'); $this->server_channels[$channel] = $server_channel;
return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); extract(unpack('Nwindow_size', $this->_string_shift($response, 4)));
} $this->window_size_client_to_server[$channel] = $window_size;
case NET_SSH2_MSG_CHANNEL_CLOSE: $temp = unpack('Npacket_size_client_to_server', $this->_string_shift($response, 4));
return $type == NET_SSH2_MSG_CHANNEL_CLOSE ? true : $this->_get_channel_packet($client_channel, $skip_extended); $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 // ie. $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_DATA
@ -3006,6 +3063,15 @@ class SSH2
*/ */
extract(unpack('Nlength', $this->_string_shift($response, 4))); extract(unpack('Nlength', $this->_string_shift($response, 4)));
$data = $this->_string_shift($response, $length); $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) { if ($client_channel == $channel) {
return $data; return $data;
} }
@ -3442,6 +3508,22 @@ class SSH2
return $this->log_boundary . str_pad(dechex(ord($matches[0])), 2, '0', STR_PAD_LEFT); 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 * Returns all errors
* *

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Pure-PHP ssh-agent client. * Pure-PHP ssh-agent client.
* *
@ -61,6 +62,19 @@ class Agent
const SSH_AGENT_SIGN_RESPONSE = 14; const SSH_AGENT_SIGN_RESPONSE = 14;
/**#@-*/ /**#@-*/
/**@+
* Agent forwarding status
*
* @access private
*/
// no forwarding requested and not active
const FORWARD_NONE = 0;
// request agent forwarding when opportune
const FORWARD_REQUEST = 1;
// forwarding has been request and is active
const FORWARD_ACTIVE = 2;
/**#@-*/
/** /**
* Unused * Unused
*/ */
@ -74,6 +88,29 @@ class Agent
*/ */
var $fsock; var $fsock;
/**
* Agent forwarding status
*
* @access private
*/
var $forward_status = self::FORWARD_NONE;
/**
* Buffer for accumulating forwarded authentication
* agent data arriving on SSH data channel destined
* for agent unix socket
*
* @access private
*/
var $socket_buffer = '';
/**
* Tracking the number of bytes we are expecting
* to arrive for the agent socket on the SSH data
* channel
*/
var $expected_bytes = 0;
/** /**
* Default Constructor * Default Constructor
* *
@ -156,4 +193,107 @@ class Agent
return $identities; return $identities;
} }
/**
* Signal that agent forwarding should
* be requested when a channel is opened
*
* @param Net_SSH2 $ssh
* @return Boolean
* @access public
*/
function startSSHForwarding($ssh)
{
if ($this->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);
}
} }

View File

@ -30,5 +30,26 @@ class Functional_Net_SSH2AgentTest extends PhpseclibFunctionalTestCase
$ssh->login($this->getEnv('SSH_USERNAME'), $agent), $ssh->login($this->getEnv('SSH_USERNAME'), $agent),
'SSH2 login using Agent failed.' '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;
} }
} }

View File

@ -28,4 +28,6 @@ ssh-add "$HOME/.ssh/id_rsa"
# Allow the private key of the travis user to log in as phpseclib user # Allow the private key of the travis user to log in as phpseclib user
sudo mkdir -p "/home/$USERNAME/.ssh/" sudo mkdir -p "/home/$USERNAME/.ssh/"
sudo cp "$HOME/.ssh/id_rsa.pub" "/home/$USERNAME/.ssh/authorized_keys" 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 sudo chown "$USERNAME:$USERNAME" "/home/$USERNAME/.ssh/" -R