true, 'verboseOutput' => true ]); // 偽物のサーバーに繋ぐCurl(127.0.0.1なので非公開アドレスの拒否を外す) $local = fn(string $url) => (new Curl($url))->setBlockPrivateAddresses(false)->setTimeout(5); $test->describe('Curlの基本的な機能性', function($test) use ($local): void { if (!CURL_ENABLED) { $test->skipAll('CURL_ENABLEDが無効'); } $test->it('メソッドチェーニングを対応するはず', function($test): void { $curl = new Curl(); $result = $curl->setUrl('https://076.moe') ->setMethod('get') ->setTimeout(30) ->setHeaders(['Accept' => 'text/html']) ->addHeader('X-Test', '1') ->setUserAgent('LoliTest/1.0') ->setFollowRedirects(false) ->setBlockPrivateAddresses(true); $test->assertSame($curl, $result); }); $test->it('URLが無ければエラーになるはず', function($test): void { $res = (new Curl())->execute(); $test->assertFalse($res->isSuccess); $test->assertEquals('URLがありません', $res->message); }); $test->it('不正なURLとスキームを拒否するはず', function($test): void { $test->assertStringContains('無効なURL', (new Curl("http://exa mple.com/"))->execute()->message); $test->assertStringContains('未対応のスキーム', (new Curl('ftp://076.moe/'))->execute()->message); $test->assertStringContains('無効なURL', (new Curl('/relative/only'))->execute()->message); }); $test->it('非公開アドレスへの接続を既定で拒否するはず', function($test): void { foreach (['http://127.0.0.1/', 'http://10.0.0.1/', 'http://192.168.1.1/', 'http://[::1]/', 'http://169.254.169.254/'] as $url) { $res = (new Curl($url))->execute(); $test->assertFalse($res->isSuccess, "{$url}が拒否されなかった"); $test->assertStringContains('非公開アドレス', $res->message); } }); $test->it('改行を含むヘッダーを拒否するはず(ヘッダー注入)', function($test) use ($local): void { // ヘッダーは接続の前に確認するので、サーバーは要らない $res = $local('http://127.0.0.1:9/')->addHeader('X-Evil', "a\r\nHost: evil")->execute(); $test->assertFalse($res->isSuccess); $test->assertStringContains('不正なヘッダー', $res->message); $res = $local('http://127.0.0.1:9/')->setHeaders(["Bad Name: x"])->execute(); $test->assertStringContains('不正なヘッダー', $res->message); $res = $local('http://127.0.0.1:9/')->setReferer("https://a/\r\nX: y")->execute(); $test->assertStringContains('不正なヘッダー', $res->message); }); $test->it('名前解決に失敗したら分かるはず', function($test): void { $res = (new Curl('https://does-not-exist.invalid/'))->execute(); $test->assertFalse($res->isSuccess); $test->assertStringContains('名前解決に失敗', $res->message); }); }); $test->describe('Curl HTTPリクエスト(偽物のサーバー)', function($test) use ($local): void { $srv = CURL_ENABLED ? MockServer::start('http-server.php') : null; if ($srv === null) { $test->skipAll('偽物のサーバーを起動出来ない(proc_openが無効?)'); } $test->it('GETリクエストを送れるはず', function($test) use ($srv, $local): void { $curl = $local($srv->url('/echo?a=1&b=%E3%81%82')); $res = $curl->execute(); $test->assertTrue($res->isSuccess); $test->assertEquals(200, $curl->getResponseCode()); $req = json_decode($curl->getResponseBody(), true); $test->assertEquals('GET', $req['method']); $test->assertEquals('/echo?a=1&b=%E3%81%82', $req['target']); $test->assertEquals("127.0.0.1", $req['headers']['host']); $test->assertEquals('LittleBeast/1.0', $req['headers']['user-agent']); $test->assertEquals('application/json', $curl->getResponseHeaders()['Content-Type']); }); $test->it('POSTのフォームと生データを送れるはず', function($test) use ($srv, $local): void { $curl = $local($srv->url('/echo'))->setMethod('POST')->setPostFields(['name' => '山田太郎', 'email' => 't.yamada@example.com']); $curl->execute(); $req = json_decode($curl->getResponseBody(), true); $test->assertEquals('POST', $req['method']); $test->assertEquals('application/x-www-form-urlencoded', $req['headers']['content-type']); parse_str($req['body'], $fields); $test->assertEquals(['name' => '山田太郎', 'email' => 't.yamada@example.com'], $fields); $curl = $local($srv->url('/echo'))->setMethod('POST')->setPostRaw('{"x":1}')->addHeader('Content-Type', 'application/json'); $curl->execute(); $req = json_decode($curl->getResponseBody(), true); $test->assertEquals('{"x":1}', $req['body']); $test->assertEquals('7', $req['headers']['content-length']); $test->assertEquals('application/json', $req['headers']['content-type']); }); $test->it('ヘッダーは両方の形式を受け付けるはず', function($test) use ($srv, $local): void { $curl = $local($srv->url('/echo'))->setHeaders(['X-A' => '1', 'X-B: 2', 'Accept' => 'text/plain', 'User-Agent: Foo/2']); $curl->execute(); $h = json_decode($curl->getResponseBody(), true)['headers']; $test->assertEquals('1', $h['x-a']); $test->assertEquals('2', $h['x-b']); $test->assertEquals('text/plain', $h['accept']); $test->assertEquals('Foo/2', $h['user-agent']); }); $test->it('Basic・Bearer認証とクッキーを送れるはず', function($test) use ($srv, $local): void { $curl = $local($srv->url('/echo'))->setBasicAuth('user', 'pa:ss'); $curl->execute(); $h = json_decode($curl->getResponseBody(), true)['headers']; $test->assertEquals('Basic '.base64_encode('user:pa:ss'), $h['authorization']); $curl = $local($srv->url('/echo'))->setBearerAuth('abc')->setCookies(['a' => '1 2', 'b' => 'x']); $curl->execute(); $h = json_decode($curl->getResponseBody(), true)['headers']; $test->assertEquals('Bearer abc', $h['authorization']); $test->assertEquals('a=1+2; b=x', $h['cookie']); $curl = $local($srv->url('/echo'))->setBearerAuth('abc')->unsetAuth(); $curl->execute(); $test->assertFalse(isset(json_decode($curl->getResponseBody(), true)['headers']['authorization'])); }); $test->it('チャンク転送をデコードするはず', function($test) use ($srv, $local): void { $curl = $local($srv->url('/chunked')); $curl->execute(); $test->assertEquals('Hello, world!', $curl->getResponseBody()); }); $test->it('リダイレクトを追うはず', function($test) use ($srv, $local): void { $curl = $local($srv->url('/redirect-301')); $curl->execute(); $test->assertEquals(200, $curl->getResponseCode()); $test->assertEquals('/echo?from=301', json_decode($curl->getResponseBody(), true)['target']); $test->assertEquals(1, $curl->getInfo()['redirect_count']); $curl = $local($srv->url('/redirect-301'))->setFollowRedirects(false); $curl->execute(); $test->assertEquals(301, $curl->getResponseCode()); }); $test->it('303ではGETに変わり、本文を送らないはず', function($test) use ($srv, $local): void { $curl = $local($srv->url('/redirect-303'))->setMethod('POST')->setPostRaw('secret=1'); $curl->execute(); $req = json_decode($curl->getResponseBody(), true); $test->assertEquals('GET', $req['method']); $test->assertEquals('', $req['body']); $test->assertFalse(isset($req['headers']['content-length'])); }); $test->it('別のオリジンへのリダイレクトでは認証情報とクッキーを送らないはず', function($test) use ($srv, $local): void { $to = urlencode($srv->url('/echo', 'localhost')); $curl = $local($srv->url("/redirect?to={$to}"))->setBearerAuth('secret')->addCookie('session', 'x') ->addHeader('Authorization', 'Bearer secret2'); $curl->execute(); $h = json_decode($curl->getResponseBody(), true)['headers']; $test->assertEquals('localhost', $h['host']); $test->assertFalse(isset($h['authorization'])); $test->assertFalse(isset($h['cookie'])); }); $test->it('リダイレクトの回数に上限があるはず', function($test) use ($srv, $local): void { $curl = $local($srv->url('/loop'))->setMaxRedirects(3); $res = $curl->execute(); $test->assertEquals(302, $curl->getResponseCode()); $test->assertEquals(3, $curl->getInfo()['redirect_count']); }); $test->it('大き過ぎるレスポンスを拒否するはず', function($test) use ($srv, $local): void { $res = $local($srv->url('/big'))->setMaxResponseSize(1000)->execute(); $test->assertFalse($res->isSuccess); $test->assertStringContains('大き過ぎます', $res->message); }); $test->it('タイムアウトするはず', function($test) use ($srv, $local): void { $start = microtime(true); $res = $local($srv->url('/slow'))->setTimeout(1)->execute(); $test->assertFalse($res->isSuccess); $test->assertStringContains('タイムアウト', $res->message); $test->assertTrue(microtime(true) - $start < 2.5, 'タイムアウトに時間が掛かり過ぎ'); }); $test->it('エラーのステータスもレスポンスとして返すはず', function($test) use ($srv, $local): void { $curl = $local($srv->url('/missing')); $res = $curl->execute(); $test->assertTrue($res->isSuccess); $test->assertEquals(404, $curl->getResponseCode()); $test->assertEquals(['desc' => 'Not found'], json_decode($curl->getResponseBody(), true)); }); $test->it('接続出来ない場合はエラーになるはず', function($test) use ($local): void { // 使われていないポートを探す $probe = stream_socket_server('tcp://127.0.0.1:0'); $port = (int)substr(strrchr(stream_socket_get_name($probe, false), ':'), 1); fclose($probe); $res = $local("http://127.0.0.1:{$port}/")->setTimeout(2)->execute(); $test->assertFalse($res->isSuccess); $test->assertStringContains('接続出来ません', $res->message); }); $srv?->stop(); }); $test->describe('Curl HTTPS(PHP純正のTLS 1.3、偽物のサーバー)', function($test) use ($local): void { $cert = TestCert::get('ec'); $srv = CURL_ENABLED ? MockServer::start('http-server.php', [$cert['cert'], $cert['key']]) : null; if ($srv === null) { $test->skipAll('偽物のサーバーを起動出来ない(proc_openが無効?)'); return; } $url = fn(string $path, string $host = '127.0.0.1') => "https://{$host}:{$srv->port}{$path}"; $test->it('HTTPSでリクエスト出来るはず(証明書を確認する)', function($test) use ($local, $url, $cert): void { foreach (['127.0.0.1', 'localhost'] as $host) { $curl = $local($url('/echo?x=1', $host))->setCaInfo($cert['ca'])->setMethod('POST')->setPostRaw('{"暗号":true}'); $res = $curl->execute(); $test->assertTrue($res->isSuccess, (string)$res->message); $req = json_decode($curl->getResponseBody(), true); $test->assertEquals('/echo?x=1', $req['target']); $test->assertEquals('{"暗号":true}', $req['body']); } }); $test->it('信頼出来ない証明書は拒否して、確認しない設定なら繋がるはず', function($test) use ($local, $url, $cert): void { $res = $local($url('/echo'))->setCaInfo(TestCert::get('self')['cert'])->execute(); $test->assertFalse($res->isSuccess); $test->assertStringContains('証明書', (string)$res->message); $res = $local($url('/echo'))->setCaInfo(TestCert::get('self')['cert'])->setVerifySSL(false)->execute(); $test->assertTrue($res->isSuccess, (string)$res->message); }); $srv->stop(); }); $test->printSummary();