diff --git a/lib/Cake/Network/CakeSocket.php b/lib/Cake/Network/CakeSocket.php index ce482174f..1fa0cd0c9 100644 --- a/lib/Cake/Network/CakeSocket.php +++ b/lib/Cake/Network/CakeSocket.php @@ -101,6 +101,14 @@ class CakeSocket { // @codingStandardsIgnoreEnd ); +/** + * Used to capture connection warnings which can happen when there are + * SSL errors for example. + * + * @var array + */ + protected $_connectionErrors = array(); + /** * Constructor. * @@ -130,17 +138,19 @@ class CakeSocket { $scheme = 'ssl://'; } - if (!empty($this->config['request']['context'])) { - $context = stream_context_create($this->config['request']['context']); + if (!empty($this->config['context'])) { + $context = stream_context_create($this->config['context']); } else { $context = stream_context_create(); } $connectAs = STREAM_CLIENT_CONNECT; if ($this->config['persistent']) { - $connectAs = STREAM_CLIENT_PERSISTENT; + $connectAs |= STREAM_CLIENT_PERSISTENT; } - $this->connection = @stream_socket_client( + + set_error_handler(array($this, '_connectionErrorHandler')); + $this->connection = stream_socket_client( $scheme . $this->config['host'] . ':' . $this->config['port'], $errNum, $errStr, @@ -148,12 +158,18 @@ class CakeSocket { $connectAs, $context ); + restore_error_handler(); if (!empty($errNum) || !empty($errStr)) { $this->setLastError($errNum, $errStr); throw new SocketException($errStr, $errNum); } + if (!$this->connection && $this->_connectionErrors) { + $message = implode("\n", $this->_connectionErrors); + throw new SocketException($message, E_WARNING); + } + $this->connected = is_resource($this->connection); if ($this->connected) { stream_set_timeout($this->connection, $this->config['timeout']); @@ -161,12 +177,28 @@ class CakeSocket { return $this->connected; } +/** + * socket_stream_client() does not populate errNum, or $errStr when there are + * connection errors, as in the case of SSL verification failure. + * + * Instead we need to handle those errors manually. + * + * @param int $code + * @param string $message + */ + protected function _connectionErrorHandler($code, $message) { + $this->_connectionErrors[] = $message; + } + /** * Get the connection context. * - * @return array + * @return null|array Null when there is no connnection, an array when there is. */ public function context() { + if (!$this->connection) { + return; + } return stream_context_get_options($this->connection); } diff --git a/lib/Cake/Network/Http/HttpSocket.php b/lib/Cake/Network/Http/HttpSocket.php index 600b3db85..a0dae10f6 100644 --- a/lib/Cake/Network/Http/HttpSocket.php +++ b/lib/Cake/Network/Http/HttpSocket.php @@ -18,6 +18,7 @@ */ App::uses('CakeSocket', 'Network'); App::uses('Router', 'Routing'); +App::uses('Hash', 'Utility'); /** * Cake network socket connection class. @@ -64,7 +65,7 @@ class HttpSocket extends CakeSocket { ), 'raw' => null, 'redirect' => false, - 'cookies' => array() + 'cookies' => array(), ); /** @@ -92,6 +93,9 @@ class HttpSocket extends CakeSocket { 'protocol' => 'tcp', 'port' => 80, 'timeout' => 30, + 'ssl_verify_peer' => true, + 'ssl_verify_depth' => 5, + 'ssl_verify_host' => true, 'request' => array( 'uri' => array( 'scheme' => array('http', 'https'), @@ -99,7 +103,7 @@ class HttpSocket extends CakeSocket { 'port' => array(80, 443) ), 'redirect' => false, - 'cookies' => array() + 'cookies' => array(), ) ); @@ -348,6 +352,8 @@ class HttpSocket extends CakeSocket { return false; } + $this->_configContext($this->request['uri']['host']); + $this->request['raw'] = ''; if ($this->request['line'] !== false) { $this->request['raw'] = $this->request['line']; @@ -395,6 +401,7 @@ class HttpSocket extends CakeSocket { throw new SocketException(__d('cake_dev', 'Class %s not found.', $this->responseClass)); } $this->response = new $responseClass($response); + if (!empty($this->response->cookies)) { if (!isset($this->config['request']['cookies'][$Host])) { $this->config['request']['cookies'][$Host] = array(); @@ -643,6 +650,33 @@ class HttpSocket extends CakeSocket { return true; } +/** + * Configure the socket's context. Adds in configuration + * that can not be declared in the class definition. + * + * @param string $host The host you're connecting to. + * @return void + */ + protected function _configContext($host) { + foreach ($this->config as $key => $value) { + if (substr($key, 0, 4) !== 'ssl_') { + continue; + } + $contextKey = substr($key, 4); + if (empty($this->config['context']['ssl'][$contextKey])) { + $this->config['context']['ssl'][$contextKey] = $value; + } + unset($this->config[$key]); + } + if (empty($this->_context['ssl']['cafile'])) { + $this->config['context']['ssl']['cafile'] = CAKE . 'Config' . DS . 'cacert.pem'; + } + if (!empty($this->config['context']['ssl']['verify_host'])) { + $this->config['context']['ssl']['CN_match'] = $host; + unset($this->config['context']['ssl']['verify_host']); + } + } + /** * Takes a $uri array and turns it into a fully qualified URL string * diff --git a/lib/Cake/Test/Case/Network/Http/HttpSocketTest.php b/lib/Cake/Test/Case/Network/Http/HttpSocketTest.php index 3fc70d742..8914b833f 100644 --- a/lib/Cake/Test/Case/Network/Http/HttpSocketTest.php +++ b/lib/Cake/Test/Case/Network/Http/HttpSocketTest.php @@ -253,6 +253,9 @@ class HttpSocketTest extends CakeTestCase { 'protocol' => 'tcp', 'port' => 23, 'timeout' => 30, + 'ssl_verify_peer' => true, + 'ssl_verify_depth' => 5, + 'ssl_verify_host' => true, 'request' => array( 'uri' => array( 'scheme' => 'https', @@ -260,7 +263,7 @@ class HttpSocketTest extends CakeTestCase { 'port' => 23 ), 'redirect' => false, - 'cookies' => array() + 'cookies' => array(), ) ); $this->assertEquals($expected, $this->Socket->config); @@ -278,6 +281,9 @@ class HttpSocketTest extends CakeTestCase { 'protocol' => 'tcp', 'port' => 80, 'timeout' => 30, + 'ssl_verify_peer' => true, + 'ssl_verify_depth' => 5, + 'ssl_verify_host' => true, 'request' => array( 'uri' => array( 'scheme' => 'http', @@ -285,7 +291,7 @@ class HttpSocketTest extends CakeTestCase { 'port' => 80 ), 'redirect' => false, - 'cookies' => array() + 'cookies' => array(), ) ); $this->assertEquals($expected, $this->Socket->config); @@ -311,6 +317,15 @@ class HttpSocketTest extends CakeTestCase { $response = $this->Socket->request(true); $this->assertFalse($response); + $context = array( + 'ssl' => array( + 'verify_peer' => true, + 'verify_depth' => 5, + 'CN_match' => 'www.cakephp.org', + 'cafile' => CAKE . 'Config' . DS . 'cacert.pem' + ) + ); + $tests = array( array( 'request' => 'http://www.cakephp.org/?foo=bar', @@ -321,6 +336,7 @@ class HttpSocketTest extends CakeTestCase { 'protocol' => 'tcp', 'port' => 80, 'timeout' => 30, + 'context' => $context, 'request' => array( 'uri' => array( 'scheme' => 'http', @@ -1668,4 +1684,37 @@ class HttpSocketTest extends CakeTestCase { } $this->assertEquals(true, $return); } + +/** + * test configuring the context from the flat keys. + * + * @return void + */ + public function testConfigContext() { + $this->Socket->reset(); + $this->Socket->request('http://example.com'); + $this->assertTrue($this->Socket->config['context']['ssl']['verify_peer']); + $this->assertEquals(5, $this->Socket->config['context']['ssl']['verify_depth']); + $this->assertEquals('example.com', $this->Socket->config['context']['ssl']['CN_match']); + $this->assertArrayNotHasKey('ssl_verify_peer', $this->Socket->config); + $this->assertArrayNotHasKey('ssl_verify_host', $this->Socket->config); + $this->assertArrayNotHasKey('ssl_verify_depth', $this->Socket->config); + } + +/** + * Test that requests fail when peer verification fails. + * + * @return void + */ + public function testVerifyPeer() { + $socket = new HttpSocket(); + try { + $result = $socket->get('https://typography.com'); + $this->markTestSkipped('Found valid certificate, was expecting invalid certificate.'); + } catch (SocketException $e) { + $message = $e->getMessage(); + $this->assertContains('Peer certificate CN', $message); + $this->assertContains('Failed to enable crypto', $message); + } + } }