From 82779f2f4d9e8f210d2d67e90d8e7f1beda467ce Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sat, 9 Jun 2018 23:05:43 -0400 Subject: [PATCH] Finish basic implementation and add live network tests. --- src/Http/Client/Adapter/Curl.php | 52 ++- .../TestCase/Http/Client/Adapter/CurlTest.php | 408 ++++-------------- 2 files changed, 124 insertions(+), 336 deletions(-) diff --git a/src/Http/Client/Adapter/Curl.php b/src/Http/Client/Adapter/Curl.php index 9af12b701c6..c0d41862b84 100644 --- a/src/Http/Client/Adapter/Curl.php +++ b/src/Http/Client/Adapter/Curl.php @@ -15,10 +15,11 @@ use Cake\Http\Client\AdapterInterface; use Cake\Http\Client\Request; +use Cake\Http\Client\Response; /** * Implements sending Cake\Http\Client\Request - * via CURL. + * via ext/curl. */ class Curl implements AdapterInterface { @@ -31,11 +32,13 @@ public function send(Request $request, array $options) $options = $this->buildOptions($request, $options); curl_setopt_array($ch, $options); - $bodyBuffer = tmpfile(); - curl_setopt(CURLOPT_FILE, $bodyBuffer); + $body = $this->exec($ch); - $this->exec($ch); + // TODO check for timeouts. + $responses = $this->createResponse($ch, $body); curl_close($ch); + + return $responses; } /** @@ -80,32 +83,55 @@ public function buildOptions(Request $request, array $options) $out[CURLOPT_POSTFIELDS] = $body->getContents(); } + if (empty($options['ssl_cafile'])) { + $options['ssl_cafile'] = CORE_PATH . 'config' . DIRECTORY_SEPARATOR . 'cacert.pem'; + } $optionMap = [ 'timeout' => CURLOPT_TIMEOUT, 'ssl_verify_peer' => CURLOPT_SSL_VERIFYPEER, 'ssl_verify_host' => CURLOPT_SSL_VERIFYHOST, - 'ssl_verify_depth' => 9000, - 'ssl_allow_self_signed' => false, + 'ssl_verify_status' => CURLOPT_SSL_VERIFYSTATUS, + 'ssl_cafile' => CURLOPT_CAINFO, + 'ssl_local_cert' => CURLOPT_SSLCERT, + 'ssl_passphrase' => CURLOPT_SSLCERTPASSWD, ]; - - if (isset($options['timeout'])) { - $out[CURLOPT_TIMEOUT] = $options['timeout']; + foreach ($optionMap as $option => $curlOpt) { + if (isset($options[$option])) { + $out[$curlOpt] = $options[$option]; + } } - if (isset($options['ssl_verify_peer'])) { - $out[CURLOPT_SSL_VERIFYPEER] = $options['ssl_verify_peer']; + if (isset($options['proxy']['proxy'])) { + $out[CURLOPT_PROXY] = $options['proxy']['proxy']; } return $out; } + /** + * Convert the raw curl response into an Http\Client\Response + * + * @param resource $handle Curl handle + * @param string $responseData string The response data from curl_exec + * @return \Cake\Http\Client\Response[] + */ + protected function createResponse($handle, $responseData) + { + $meta = curl_getinfo($handle); + $headers = trim(substr($responseData, 0, $meta['header_size'])); + $body = substr($responseData, $meta['header_size']); + $response = new Response(explode("\r\n", $headers), $body); + + return [$response]; + } + /** * Execute the curl handle. * * @param resource $ch Curl Resource handle - * @return void + * @return string */ protected function exec($ch) { - curl_exec($ch); + return curl_exec($ch); } } diff --git a/tests/TestCase/Http/Client/Adapter/CurlTest.php b/tests/TestCase/Http/Client/Adapter/CurlTest.php index 310ff108288..1e5b7b7cb20 100644 --- a/tests/TestCase/Http/Client/Adapter/CurlTest.php +++ b/tests/TestCase/Http/Client/Adapter/CurlTest.php @@ -15,6 +15,7 @@ use Cake\Http\Client\Adapter\Curl; use Cake\Http\Client\Request; +use Cake\Http\Client\Response; use Cake\TestSuite\TestCase; /** @@ -26,9 +27,8 @@ class CurlTest extends TestCase public function setUp() { parent::setUp(); - $this->curl = $this->getMockBuilder('Cake\Http\Client\Adapter\Curl') - ->setMethods(['exec']) - ->getMock(); + $this->curl = new Curl(); + $this->caFile = CORE_PATH . 'config' . DIRECTORY_SEPARATOR . 'cacert.pem'; } /** @@ -36,20 +36,47 @@ public function setUp() * * @return void */ - public function testSend() + public function testSendLive() { - $this->markTestIncomplete(); $request = new Request('http://localhost', 'GET', [ 'User-Agent' => 'CakePHP TestSuite', 'Cookie' => 'testing=value' ]); - try { $responses = $this->curl->send($request, []); } catch (\Cake\Core\Exception\Exception $e) { $this->markTestSkipped('Could not connect to localhost, skipping'); } - $this->assertInstanceOf('Cake\Http\Client\Response', $responses[0]); + $this->assertCount(1, $responses); + + $response = $responses[0]; + $this->assertInstanceOf(Response::class, $response); + $this->assertNotEmpty($response->getHeaders()); + $this->assertNotEmpty($response->getBody()->getContents()); + } + + /** + * Test the send method + * + * @return void + */ + public function testSendLiveResponseCheck() + { + $request = new Request('https://api.cakephp.org/3.0/', 'GET', [ + 'User-Agent' => 'CakePHP TestSuite', + ]); + try { + $responses = $this->curl->send($request, []); + } catch (\Cake\Core\Exception\Exception $e) { + $this->markTestSkipped('Could not connect to book.cakephp.org, skipping'); + } + $this->assertCount(1, $responses); + + $response = $responses[0]; + $this->assertInstanceOf(Response::class, $response); + $this->assertTrue($response->hasHeader('Date')); + $this->assertTrue($response->hasHeader('Content-type')); + $this->assertContains('getBody()->getContents()); } /** @@ -80,6 +107,7 @@ public function testBuildOptionsGet() ], CURLOPT_HTTPGET => true, CURLOPT_TIMEOUT => 5, + CURLOPT_CAINFO => $this->caFile, ]; $this->assertSame($expected, $result); } @@ -111,7 +139,8 @@ public function testBuildOptionsPost() 'Content-Type: application/x-www-form-urlencoded', ], CURLOPT_POST => true, - CURLOPT_POSTFIELDS => 'name=cakephp&yes=1' + CURLOPT_POSTFIELDS => 'name=cakephp&yes=1', + CURLOPT_CAINFO => $this->caFile, ]; $this->assertSame($expected, $result); } @@ -142,6 +171,7 @@ public function testBuildOptionsPut() ], CURLOPT_POST => true, CURLOPT_CUSTOMREQUEST => 'PUT', + CURLOPT_CAINFO => $this->caFile, ]; $this->assertSame($expected, $result); } @@ -151,16 +181,16 @@ public function testBuildOptionsPut() * * @return void */ - public function testBuildOptionsSsl() + public function testBuildOptionsJsonPost() { - $options = [ - 'ssl_verify_host' => true, - 'ssl_verify_peer' => true, - 'ssl_verify_peer_name' => true, - 'ssl_verify_depth' => 9000, - 'ssl_allow_self_signed' => false, - ]; - $request = new Request('http://localhost/things', 'GET'); + $options = []; + $content = json_encode(['a' => 1, 'b' => 2]); + $request = new Request( + 'http://localhost/things', + 'POST', + ['Content-type' => 'application/json'], + $content + ); $result = $this->curl->buildOptions($request, $options); $expected = [ CURLOPT_URL => 'http://localhost/things', @@ -168,346 +198,78 @@ public function testBuildOptionsSsl() CURLOPT_RETURNTRANSFER => true, CURLOPT_HEADER => true, CURLOPT_HTTPHEADER => [ + 'Content-type: application/json', 'Connection: close', 'User-Agent: CakePHP', ], - CURLOPT_HTTPGET => true, - CURLOPT_SSL_VERIFYPEER => true + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $content, + CURLOPT_CAINFO => $this->caFile, ]; $this->assertSame($expected, $result); } /** - * Test the send method by using cakephp:// protocol. - * - * @return void - */ - public function testSendByUsingCakephpProtocol() - { - $this->markTestIncomplete(); - $stream = new Stream(); - $request = new Request('http://dummy/'); - - $responses = $stream->send($request, []); - $this->assertInstanceOf('Cake\Http\Client\Response', $responses[0]); - - $this->assertEquals(20000, strlen($responses[0]->body())); - } - - /** - * Test stream error_handler cleanup when wrapper throws exception - * - * @return void - */ - public function testSendWrapperException() - { - $this->markTestIncomplete(); - $stream = new Stream(); - $request = new Request('http://throw_exception/'); - - $currentHandler = set_error_handler(function () { - }); - restore_error_handler(); - - try { - $stream->send($request, []); - } catch (\Exception $e) { - } - - $newHandler = set_error_handler(function () { - }); - restore_error_handler(); - - $this->assertEquals($currentHandler, $newHandler); - } - - /** - * Test building the context headers - * - * @return void - */ - public function testBuildingContextHeader() - { - $this->markTestIncomplete(); - $request = new Request( - 'http://localhost', - 'GET', - [ - 'User-Agent' => 'CakePHP TestSuite', - 'Content-Type' => 'application/json', - 'Cookie' => 'a=b; c=do%20it' - ] - ); - - $options = [ - 'redirect' => 20, - ]; - $this->stream->send($request, $options); - $result = $this->stream->contextOptions(); - $expected = [ - 'User-Agent: CakePHP TestSuite', - 'Content-Type: application/json', - 'Cookie: a=b; c=do%20it', - 'Connection: close', - ]; - $this->assertEquals(implode("\r\n", $expected), $result['header']); - $this->assertSame(0, $result['max_redirects']); - $this->assertTrue($result['ignore_errors']); - } - - /** - * Test send() + context options with string content. - * - * @return void - */ - public function testSendContextContentString() - { - $this->markTestIncomplete(); - $content = json_encode(['a' => 'b']); - $request = new Request( - 'http://localhost', - 'GET', - ['Content-Type' => 'application/json'], - $content - ); - - $options = [ - 'redirect' => 20 - ]; - $this->stream->send($request, $options); - $result = $this->stream->contextOptions(); - $expected = [ - 'Content-Type: application/json', - 'Connection: close', - 'User-Agent: CakePHP', - ]; - $this->assertEquals(implode("\r\n", $expected), $result['header']); - $this->assertEquals($content, $result['content']); - } - - /** - * Test send() + context options with array content. - * - * @return void - */ - public function testSendContextContentArray() - { - $this->markTestIncomplete(); - $request = new Request( - 'http://localhost', - 'GET', - [ - 'Content-Type' => 'application/json' - ], - ['a' => 'my value'] - ); - - $this->stream->send($request, []); - $result = $this->stream->contextOptions(); - $expected = [ - 'Content-Type: application/x-www-form-urlencoded', - 'Connection: close', - 'User-Agent: CakePHP', - ]; - $this->assertStringStartsWith(implode("\r\n", $expected), $result['header']); - $this->assertContains('a=my+value', $result['content']); - $this->assertContains('my+value', $result['content']); - } - - /** - * Test send() + context options with array content. - * - * @return void - */ - public function testSendContextContentArrayFiles() - { - $this->markTestIncomplete(); - $request = new Request( - 'http://localhost', - 'GET', - ['Content-Type' => 'application/json'], - ['upload' => fopen(CORE_PATH . 'VERSION.txt', 'r')] - ); - - $this->stream->send($request, []); - $result = $this->stream->contextOptions(); - $this->assertContains("Content-Type: multipart/form-data", $result['header']); - $this->assertContains("Connection: close\r\n", $result['header']); - $this->assertContains("User-Agent: CakePHP", $result['header']); - $this->assertContains('name="upload"', $result['content']); - $this->assertContains('filename="VERSION.txt"', $result['content']); - } - - /** - * Test send() + context options for SSL. + * Test converting client options into curl ones. * * @return void */ - public function testSendContextSsl() + public function testBuildOptionsSsl() { - $this->markTestIncomplete(); - $request = new Request('https://localhost.com/test.html'); $options = [ 'ssl_verify_host' => true, 'ssl_verify_peer' => true, 'ssl_verify_peer_name' => true, + // These options do nothing in curl. 'ssl_verify_depth' => 9000, 'ssl_allow_self_signed' => false, - 'proxy' => [ - 'proxy' => '127.0.0.1:8080' - ] ]; - - $this->stream->send($request, $options); - $result = $this->stream->contextOptions(); + $request = new Request('http://localhost/things', 'GET'); + $result = $this->curl->buildOptions($request, $options); $expected = [ - 'peer_name' => 'localhost.com', - 'verify_peer' => true, - 'verify_peer_name' => true, - 'verify_depth' => 9000, - 'allow_self_signed' => false, - 'proxy' => '127.0.0.1:8080', + CURLOPT_URL => 'http://localhost/things', + CURLOPT_HTTP_VERSION => '1.1', + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADER => true, + CURLOPT_HTTPHEADER => [ + 'Connection: close', + 'User-Agent: CakePHP', + ], + CURLOPT_HTTPGET => true, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => true, + CURLOPT_CAINFO => $this->caFile, ]; - foreach ($expected as $k => $v) { - $this->assertEquals($v, $result[$k]); - } - $this->assertIsReadable($result['cafile']); + $this->assertSame($expected, $result); } /** - * Test send() + context options for SSL. + * Test converting client options into curl ones. * * @return void */ - public function testSendContextSslNoVerifyPeerName() + public function testBuildOptionsProxy() { - $this->markTestIncomplete(); - $request = new Request('https://localhost.com/test.html'); $options = [ - 'ssl_verify_host' => true, - 'ssl_verify_peer' => true, - 'ssl_verify_peer_name' => false, - 'ssl_verify_depth' => 9000, - 'ssl_allow_self_signed' => false, 'proxy' => [ 'proxy' => '127.0.0.1:8080' ] ]; - - $this->stream->send($request, $options); - $result = $this->stream->contextOptions(); + $request = new Request('http://localhost/things', 'GET'); + $result = $this->curl->buildOptions($request, $options); $expected = [ - 'peer_name' => 'localhost.com', - 'verify_peer' => true, - 'verify_peer_name' => false, - 'verify_depth' => 9000, - 'allow_self_signed' => false, - 'proxy' => '127.0.0.1:8080', - ]; - foreach ($expected as $k => $v) { - $this->assertEquals($v, $result[$k]); - } - $this->assertIsReadable($result['cafile']); - } - - /** - * The PHP stream API returns ALL the headers for ALL the requests when - * there are redirects. - * - * @return void - */ - public function testCreateResponseWithRedirects() - { - $this->markTestIncomplete(); - $headers = [ - 'HTTP/1.1 302 Found', - 'Date: Mon, 31 Dec 2012 16:53:16 GMT', - 'Server: Apache/2.2.22 (Unix) DAV/2 PHP/5.4.9 mod_ssl/2.2.22 OpenSSL/0.9.8r', - 'X-Powered-By: PHP/5.4.9', - 'Location: http://localhost/cake3/tasks/second', - 'Content-Length: 0', - 'Connection: close', - 'Content-Type: text/html; charset=UTF-8', - 'Set-Cookie: first=value', - 'HTTP/1.1 302 Found', - 'Date: Mon, 31 Dec 2012 16:53:16 GMT', - 'Server: Apache/2.2.22 (Unix) DAV/2 PHP/5.4.9 mod_ssl/2.2.22 OpenSSL/0.9.8r', - 'X-Powered-By: PHP/5.4.9', - 'Location: http://localhost/cake3/tasks/third', - 'Content-Length: 0', - 'Connection: close', - 'Content-Type: text/html; charset=UTF-8', - 'Set-Cookie: second=val', - 'HTTP/1.1 200 OK', - 'Date: Mon, 31 Dec 2012 16:53:16 GMT', - 'Server: Apache/2.2.22 (Unix) DAV/2 PHP/5.4.9 mod_ssl/2.2.22 OpenSSL/0.9.8r', - 'X-Powered-By: PHP/5.4.9', - 'Content-Length: 22', - 'Connection: close', - 'Content-Type: text/html; charset=UTF-8', - 'Set-Cookie: third=works', - ]; - $content = 'This is the third page'; - - $responses = $this->stream->createResponses($headers, $content); - $this->assertCount(3, $responses); - $this->assertEquals('close', $responses[0]->getHeaderLine('Connection')); - $this->assertEquals('', (string)$responses[0]->getBody()); - $this->assertEquals('', (string)$responses[1]->getBody()); - $this->assertEquals($content, (string)$responses[2]->getBody()); - - $this->assertEquals(302, $responses[0]->getStatusCode()); - $this->assertEquals(302, $responses[1]->getStatusCode()); - $this->assertEquals(200, $responses[2]->getStatusCode()); - - $this->assertEquals('value', $responses[0]->getCookie('first')); - $this->assertNull($responses[0]->getCookie('second')); - $this->assertNull($responses[0]->getCookie('third')); - - $this->assertNull($responses[1]->getCookie('first')); - $this->assertEquals('val', $responses[1]->getCookie('second')); - $this->assertNull($responses[1]->getCookie('third')); - - $this->assertNull($responses[2]->getCookie('first')); - $this->assertNull($responses[2]->getCookie('second')); - $this->assertEquals('works', $responses[2]->getCookie('third')); - } - - /** - * Test that no exception is radied when not timed out. - * - * @return void - */ - public function testKeepDeadline() - { - $this->markTestIncomplete(); - $request = new Request('http://dummy/?sleep'); - $options = [ - 'timeout' => 5, - ]; - - $t = microtime(true); - $stream = new Stream(); - $stream->send($request, $options); - $this->assertLessThan(5, microtime(true) - $t); - } - - /** - * Test that an exception is raised when timed out. - * - * @return void - */ - public function testMissDeadline() - { - $this->markTestIncomplete(); - $this->expectException(\Cake\Http\Exception\HttpException::class); - $this->expectExceptionMessage('Connection timed out http://dummy/?sleep'); - $request = new Request('http://dummy/?sleep'); - $options = [ - 'timeout' => 2, + CURLOPT_URL => 'http://localhost/things', + CURLOPT_HTTP_VERSION => '1.1', + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADER => true, + CURLOPT_HTTPHEADER => [ + 'Connection: close', + 'User-Agent: CakePHP', + ], + CURLOPT_HTTPGET => true, + CURLOPT_CAINFO => $this->caFile, + CURLOPT_PROXY => '127.0.0.1:8080', ]; - - $stream = new Stream(); - $stream->send($request, $options); + $this->assertSame($expected, $result); } }