From 05b8161677541803bf1644f9a57057863c67937b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E5=94=81?= <52o@qq52o.cn> Date: Fri, 16 Jul 2021 14:32:36 +0800 Subject: [PATCH] Support CURLOPT_RESOLVE option for SWOOLE_HOOK_CURL (#107) * Support CURLOPT_RESOLVE option for SWOOLE_HOOK_CURL * Support CURLOPT_RESOLVE option for SWOOLE_HOOK_CURL * Update composer.json * Support CURLOPT_RESOLVE option for SWOOLE_HOOK_CURL * Support CURLOPT_RESOLVE option for SWOOLE_HOOK_CURL * Support CURLOPT_RESOLVE option for SWOOLE_HOOK_CURL * cs-fix * remove * update * Add more tests * Fix test --- composer.json | 6 +- src/constants.php | 1 + src/core/Curl/Handler.php | 37 ++++++++- src/ext/curl.php | 2 + tests/unit/Curl/HandlerTest.php | 139 ++++++++++++++++++++++++++++++++ 5 files changed, 180 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index d832c86e..ece595ac 100644 --- a/composer.json +++ b/composer.json @@ -17,14 +17,14 @@ }, "require": { "php": ">=7.2", - "ext-swoole": ">=4.6", - "friendsofphp/php-cs-fixer": "^2.18" + "ext-swoole": ">=4.6" }, "require-dev": { "ext-curl": "*", "ext-sockets": "*", "phpunit/phpunit": "~8.0", - "swoole/ide-helper": "dev-master" + "swoole/ide-helper": "~4.6", + "friendsofphp/php-cs-fixer": "^2.18" }, "suggest": { "ext-mysqli": "Required to use mysqli database", diff --git a/src/constants.php b/src/constants.php index 805789c8..e1644906 100644 --- a/src/constants.php +++ b/src/constants.php @@ -13,3 +13,4 @@ !defined('CURLOPT_HEADEROPT') && define('CURLOPT_HEADEROPT', 229); !defined('CURLOPT_PROXYHEADER') && define('CURLOPT_PROXYHEADER', 10228); +!defined('CURLOPT_RESOLVE') && define('CURLOPT_RESOLVE', 10203); diff --git a/src/core/Curl/Handler.php b/src/core/Curl/Handler.php index c203b136..92168641 100644 --- a/src/core/Curl/Handler.php +++ b/src/core/Curl/Handler.php @@ -130,6 +130,8 @@ final class Handler private $cookieJar = ''; + private $resolve = []; + public function __construct(string $url = '') { if ($url) { @@ -216,7 +218,15 @@ private function create(?array $urlInfo = null): void if ($urlInfo === null) { $urlInfo = $this->urlInfo; } - $this->client = new Client($urlInfo['host'], $urlInfo['port'], $urlInfo['scheme'] === 'https'); + $host = $urlInfo['host']; + $port = $urlInfo['port']; + if (isset($this->resolve[$host])) { + if (!$this->hasHeader('Host')) { + $this->setHeader('Host', $host); + } + $this->urlInfo['host'] = $host = $this->resolve[$host][$port] ?? null ?: $host; + } + $this->client = new Client($host, $port, $urlInfo['scheme'] === 'https'); } private function getUrl(): string @@ -304,7 +314,7 @@ private function setPort(int $port): void private function setError($code, $msg = ''): void { $this->errCode = $code; - $this->errMsg = $msg ? $msg : curl_strerror($code); + $this->errMsg = $msg ?: curl_strerror($code); } private function hasHeader(string $headerName): bool @@ -413,6 +423,25 @@ private function setOption(int $opt, $value): bool $this->nobody = boolval($value); $this->method = 'HEAD'; break; + case CURLOPT_RESOLVE: + foreach ((array) $value as $resolve) { + $flag = substr($resolve, 0, 1); + if ($flag === '+' || $flag === '-') { + // TODO: [+]HOST:PORT:ADDRESS + $resolve = substr($resolve, 1); + } + $tmpResolve = explode(':', $resolve, 3); + $host = $tmpResolve[0] ?? ''; + $port = $tmpResolve[1] ?? 0; + $ip = $tmpResolve[2] ?? ''; + if ($flag === '-') { + unset($this->resolve[$host][$port]); + } else { + // TODO: HOST:PORT:ADDRESS[,ADDRESS]... + $this->resolve[$host][$port] = explode(',', $ip)[0]; + } + } + break; case CURLOPT_IPRESOLVE: if ($value !== CURL_IPRESOLVE_WHATEVER and $value !== CURL_IPRESOLVE_V4) { throw new Swoole\Curl\Exception( @@ -803,6 +832,10 @@ private function execute() $this->info['redirect_time'] = microtime(true) - $redirectBeginTime; } + if (filter_var($this->urlInfo['host'], FILTER_VALIDATE_IP)) { + $this->info['primary_ip'] = $this->urlInfo['host']; + } + $headerContent = ''; if ($client->headers) { $cb = $this->headerFunction; diff --git a/src/ext/curl.php b/src/ext/curl.php index f2cb07fe..7a679ce9 100644 --- a/src/ext/curl.php +++ b/src/ext/curl.php @@ -63,6 +63,8 @@ function swoole_curl_getinfo(Swoole\Curl\Handler $obj, int $opt = 0) return $info['redirect_time']; case CURLINFO_HEADER_SIZE: return $info['header_size']; + case CURLINFO_PRIMARY_IP: + return $info['primary_ip']; case CURLINFO_PRIVATE: return $info['private']; default: diff --git a/tests/unit/Curl/HandlerTest.php b/tests/unit/Curl/HandlerTest.php index c99deff9..ae2dbea7 100644 --- a/tests/unit/Curl/HandlerTest.php +++ b/tests/unit/Curl/HandlerTest.php @@ -135,6 +135,145 @@ public function testWriteFunction() /** * @covers \Swoole\Curl\Handler::execute() */ + public function testResolve() + { + Coroutine\run(function () { + $host = 'httpbin.org'; + $url = 'https://httpbin.org/get'; + $ip = Coroutine::gethostbyname($host); + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_RESOLVE, ["{$host}:443:{$ip}"]); + + $data = curl_exec($ch); + $httpPrimaryIp = curl_getinfo($ch, CURLINFO_PRIMARY_IP); + curl_close($ch); + $body = json_decode($data, true); + self::assertSame($body['headers']['Host'], 'httpbin.org'); + self::assertEquals($body['url'], $url); + self::assertEquals($ip, $httpPrimaryIp); + }); + } + + /** + * @covers \Swoole\Curl\Handler::execute() + */ + public function testInvalidResolve() + { + Coroutine\run(function () { + $host = 'httpbin.org'; + $url = 'https://httpbin.org/get'; + $ip = '127.0.0.1'; // An incorrect IP in use. + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_RESOLVE, ["{$host}:443:{$ip}"]); + + $body = curl_exec($ch); + $httpPrimaryIp = curl_getinfo($ch, CURLINFO_PRIMARY_IP); + curl_close($ch); + self::assertFalse($body); + self::assertSame('', $httpPrimaryIp); + }); + } + + /** + * @covers \Swoole\Curl\Handler::execute() + */ + public function testResolve2() + { + Coroutine\run(function () { + $host = 'httpbin.org'; + $url = 'https://httpbin.org/get'; + $ip = Coroutine::gethostbyname($host); + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_RESOLVE, ["{$host}:443:127.0.0.1", "{$host}:443:{$ip}"]); + + $data = curl_exec($ch); + $httpPrimaryIp = curl_getinfo($ch, CURLINFO_PRIMARY_IP); + $body = json_decode($data, true); + self::assertSame($body['headers']['Host'], 'httpbin.org'); + self::assertEquals($body['url'], $url); + self::assertEquals($ip, $httpPrimaryIp); + }); + } + + /** + * @covers \Swoole\Curl\Handler::execute() + */ + public function testInvalidResolve2() + { + Coroutine\run(function () { + $host = 'httpbin.org'; + $url = 'https://httpbin.org/get'; + $ip = Coroutine::gethostbyname($host); + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_RESOLVE, ["{$host}:443:{$ip}", "+{$host}:443:127.0.0.1"]); + + $body = curl_exec($ch); + $httpPrimaryIp = curl_getinfo($ch, CURLINFO_PRIMARY_IP); + curl_close($ch); + self::assertFalse($body); + self::assertSame('', $httpPrimaryIp); + }); + } + + /** + * @covers \Swoole\Curl\Handler::execute() + */ + public function testInvalidResolve3() + { + Coroutine\run(function () { + $host = 'httpbin.org'; + $url = 'https://httpbin.org/get'; + $ip = Coroutine::gethostbyname($host); + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_RESOLVE, ["{$host}:443:{$ip}", "{$host}:443:127.0.0.1"]); + + $body = curl_exec($ch); + $httpPrimaryIp = curl_getinfo($ch, CURLINFO_PRIMARY_IP); + curl_close($ch); + self::assertFalse($body); + self::assertSame('', $httpPrimaryIp); + }); + } + + /** + * @covers \Swoole\Curl\Handler::execute() + */ + public function testResolve3() + { + Coroutine\run(function () { + $host = 'httpbin.org'; + $url = 'https://httpbin.org/get'; + $ip = Coroutine::gethostbyname($host); + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_RESOLVE, ["{$host}:443:{$ip}", "{$host}:443:127.0.0.1", "-{$host}:443:127.0.0.1"]); + + $data = curl_exec($ch); + $httpPrimaryIp = curl_getinfo($ch, CURLINFO_PRIMARY_IP); + $body = json_decode($data, true); + self::assertSame($body['headers']['Host'], 'httpbin.org'); + self::assertEquals($body['url'], $url); + self::assertSame('', $httpPrimaryIp); + }); + } + public function testOptPrivate() { Coroutine\run(function () {