| 1 | <?php |
| 2 | /************************************************************* |
| 3 | # 076 License |
| 4 | |
| 5 | Copyright (c) テクニカル諏訪子 |
| 6 | |
| 7 | Permission is hereby granted to any person obtaining a copy of the software |
| 8 | Little Beast (the "Software") to use, modify, merge, copy, publish, distribute, |
| 9 | sublicense, and/or sell copies of the Software, subject to the following conditions: |
| 10 | |
| 11 | 1. **Origin Attribution**: |
| 12 | - You must not misrepresent the origin of the Software; you must not claim |
| 13 | you created the original Software. |
| 14 | - If the Software is used in a product, you must either: |
| 15 | a. Provide clear attribution in the product's documentation, user interface, |
| 16 | or other visible areas, **OR** |
| 17 | b. Pay the original developers a fee they specify in writing. |
| 18 | 2. **Usage Restriction**: |
| 19 | - The Software, or any derivative works, dependencies, or libraries |
| 20 | incorporating it, must not be used for censorship or to suppress freedom of |
| 21 | speech, expression, or creativity. Prohibited uses include, but are not |
| 22 | limited to: |
| 23 | - Censorship of so-called "hate speech", visuals, non-mainstream opinions, |
| 24 | ideas, or objective reality. |
| 25 | - Tools or systems designed to restrict access to information or |
| 26 | artistic works. |
| 27 | 3. **Notice Preservation**: |
| 28 | - This license and the above copyright notice must remain intact in all copies |
| 29 | of the source code. |
| 30 | |
| 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
| 32 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A |
| 33 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT |
| 34 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF |
| 35 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE |
| 36 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 37 | */ |
| 38 | namespace Std\Test; |
| 39 | |
| 40 | require_once __DIR__.'/../../../autoload.php'; |
| 41 | |
| 42 | use Std\Lib\Tester; |
| 43 | use Std\Lib\Curl; |
| 44 | use Std\Test\Helper\MockServer; |
| 45 | use Std\Test\Helper\TestCert; |
| 46 | |
| 47 | $test = new Tester([ |
| 48 | 'colorOutput' => true, |
| 49 | 'verboseOutput' => true |
| 50 | ]); |
| 51 | |
| 52 | // 偽物のサーバーに繋ぐCurl(127.0.0.1なので非公開アドレスの拒否を外す) |
| 53 | $local = fn(string $url) => (new Curl($url))->setBlockPrivateAddresses(false)->setTimeout(5); |
| 54 | |
| 55 | $test->describe('Curlの基本的な機能性', function($test) use ($local): void { |
| 56 | if (!CURL_ENABLED) { |
| 57 | $test->skipAll('CURL_ENABLEDが無効'); |
| 58 | } |
| 59 | |
| 60 | $test->it('メソッドチェーニングを対応するはず', function($test): void { |
| 61 | $curl = new Curl(); |
| 62 | $result = $curl->setUrl('https://076.moe') |
| 63 | ->setMethod('get') |
| 64 | ->setTimeout(30) |
| 65 | ->setHeaders(['Accept' => 'text/html']) |
| 66 | ->addHeader('X-Test', '1') |
| 67 | ->setUserAgent('LoliTest/1.0') |
| 68 | ->setFollowRedirects(false) |
| 69 | ->setBlockPrivateAddresses(true); |
| 70 | $test->assertSame($curl, $result); |
| 71 | }); |
| 72 | |
| 73 | $test->it('URLが無ければエラーになるはず', function($test): void { |
| 74 | $res = (new Curl())->execute(); |
| 75 | $test->assertFalse($res->isSuccess); |
| 76 | $test->assertEquals('URLがありません', $res->message); |
| 77 | }); |
| 78 | |
| 79 | $test->it('不正なURLとスキームを拒否するはず', function($test): void { |
| 80 | $test->assertStringContains('無効なURL', (new Curl("http://exa mple.com/"))->execute()->message); |
| 81 | $test->assertStringContains('未対応のスキーム', (new Curl('ftp://076.moe/'))->execute()->message); |
| 82 | $test->assertStringContains('無効なURL', (new Curl('/relative/only'))->execute()->message); |
| 83 | }); |
| 84 | |
| 85 | $test->it('非公開アドレスへの接続を既定で拒否するはず', function($test): void { |
| 86 | 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) { |
| 87 | $res = (new Curl($url))->execute(); |
| 88 | $test->assertFalse($res->isSuccess, "{$url}が拒否されなかった"); |
| 89 | $test->assertStringContains('非公開アドレス', $res->message); |
| 90 | } |
| 91 | }); |
| 92 | |
| 93 | $test->it('改行を含むヘッダーを拒否するはず(ヘッダー注入)', function($test) use ($local): void { |
| 94 | // ヘッダーは接続の前に確認するので、サーバーは要らない |
| 95 | $res = $local('http://127.0.0.1:9/')->addHeader('X-Evil', "a\r\nHost: evil")->execute(); |
| 96 | $test->assertFalse($res->isSuccess); |
| 97 | $test->assertStringContains('不正なヘッダー', $res->message); |
| 98 | |
| 99 | $res = $local('http://127.0.0.1:9/')->setHeaders(["Bad Name: x"])->execute(); |
| 100 | $test->assertStringContains('不正なヘッダー', $res->message); |
| 101 | |
| 102 | $res = $local('http://127.0.0.1:9/')->setReferer("https://a/\r\nX: y")->execute(); |
| 103 | $test->assertStringContains('不正なヘッダー', $res->message); |
| 104 | }); |
| 105 | |
| 106 | $test->it('名前解決に失敗したら分かるはず', function($test): void { |
| 107 | $res = (new Curl('https://does-not-exist.invalid/'))->execute(); |
| 108 | $test->assertFalse($res->isSuccess); |
| 109 | $test->assertStringContains('名前解決に失敗', $res->message); |
| 110 | }); |
| 111 | }); |
| 112 | |
| 113 | $test->describe('Curl HTTPリクエスト(偽物のサーバー)', function($test) use ($local): void { |
| 114 | $srv = CURL_ENABLED ? MockServer::start('http-server.php') : null; |
| 115 | if ($srv === null) { |
| 116 | $test->skipAll('偽物のサーバーを起動出来ない(proc_openが無効?)'); |
| 117 | } |
| 118 | |
| 119 | $test->it('GETリクエストを送れるはず', function($test) use ($srv, $local): void { |
| 120 | $curl = $local($srv->url('/echo?a=1&b=%E3%81%82')); |
| 121 | $res = $curl->execute(); |
| 122 | $test->assertTrue($res->isSuccess); |
| 123 | $test->assertEquals(200, $curl->getResponseCode()); |
| 124 | |
| 125 | $req = json_decode($curl->getResponseBody(), true); |
| 126 | $test->assertEquals('GET', $req['method']); |
| 127 | $test->assertEquals('/echo?a=1&b=%E3%81%82', $req['target']); |
| 128 | $test->assertEquals("127.0.0.1", $req['headers']['host']); |
| 129 | $test->assertEquals('LittleBeast/1.0', $req['headers']['user-agent']); |
| 130 | $test->assertEquals('application/json', $curl->getResponseHeaders()['Content-Type']); |
| 131 | }); |
| 132 | |
| 133 | $test->it('POSTのフォームと生データを送れるはず', function($test) use ($srv, $local): void { |
| 134 | $curl = $local($srv->url('/echo'))->setMethod('POST')->setPostFields(['name' => '山田太郎', 'email' => 't.yamada@example.com']); |
| 135 | $curl->execute(); |
| 136 | $req = json_decode($curl->getResponseBody(), true); |
| 137 | $test->assertEquals('POST', $req['method']); |
| 138 | $test->assertEquals('application/x-www-form-urlencoded', $req['headers']['content-type']); |
| 139 | parse_str($req['body'], $fields); |
| 140 | $test->assertEquals(['name' => '山田太郎', 'email' => 't.yamada@example.com'], $fields); |
| 141 | |
| 142 | $curl = $local($srv->url('/echo'))->setMethod('POST')->setPostRaw('{"x":1}')->addHeader('Content-Type', 'application/json'); |
| 143 | $curl->execute(); |
| 144 | $req = json_decode($curl->getResponseBody(), true); |
| 145 | $test->assertEquals('{"x":1}', $req['body']); |
| 146 | $test->assertEquals('7', $req['headers']['content-length']); |
| 147 | $test->assertEquals('application/json', $req['headers']['content-type']); |
| 148 | }); |
| 149 | |
| 150 | $test->it('ヘッダーは両方の形式を受け付けるはず', function($test) use ($srv, $local): void { |
| 151 | $curl = $local($srv->url('/echo'))->setHeaders(['X-A' => '1', 'X-B: 2', 'Accept' => 'text/plain', 'User-Agent: Foo/2']); |
| 152 | $curl->execute(); |
| 153 | $h = json_decode($curl->getResponseBody(), true)['headers']; |
| 154 | $test->assertEquals('1', $h['x-a']); |
| 155 | $test->assertEquals('2', $h['x-b']); |
| 156 | $test->assertEquals('text/plain', $h['accept']); |
| 157 | $test->assertEquals('Foo/2', $h['user-agent']); |
| 158 | }); |
| 159 | |
| 160 | $test->it('Basic・Bearer認証とクッキーを送れるはず', function($test) use ($srv, $local): void { |
| 161 | $curl = $local($srv->url('/echo'))->setBasicAuth('user', 'pa:ss'); |
| 162 | $curl->execute(); |
| 163 | $h = json_decode($curl->getResponseBody(), true)['headers']; |
| 164 | $test->assertEquals('Basic '.base64_encode('user:pa:ss'), $h['authorization']); |
| 165 | |
| 166 | $curl = $local($srv->url('/echo'))->setBearerAuth('abc')->setCookies(['a' => '1 2', 'b' => 'x']); |
| 167 | $curl->execute(); |
| 168 | $h = json_decode($curl->getResponseBody(), true)['headers']; |
| 169 | $test->assertEquals('Bearer abc', $h['authorization']); |
| 170 | $test->assertEquals('a=1+2; b=x', $h['cookie']); |
| 171 | |
| 172 | $curl = $local($srv->url('/echo'))->setBearerAuth('abc')->unsetAuth(); |
| 173 | $curl->execute(); |
| 174 | $test->assertFalse(isset(json_decode($curl->getResponseBody(), true)['headers']['authorization'])); |
| 175 | }); |
| 176 | |
| 177 | $test->it('チャンク転送をデコードするはず', function($test) use ($srv, $local): void { |
| 178 | $curl = $local($srv->url('/chunked')); |
| 179 | $curl->execute(); |
| 180 | $test->assertEquals('Hello, world!', $curl->getResponseBody()); |
| 181 | }); |
| 182 | |
| 183 | $test->it('リダイレクトを追うはず', function($test) use ($srv, $local): void { |
| 184 | $curl = $local($srv->url('/redirect-301')); |
| 185 | $curl->execute(); |
| 186 | $test->assertEquals(200, $curl->getResponseCode()); |
| 187 | $test->assertEquals('/echo?from=301', json_decode($curl->getResponseBody(), true)['target']); |
| 188 | $test->assertEquals(1, $curl->getInfo()['redirect_count']); |
| 189 | |
| 190 | $curl = $local($srv->url('/redirect-301'))->setFollowRedirects(false); |
| 191 | $curl->execute(); |
| 192 | $test->assertEquals(301, $curl->getResponseCode()); |
| 193 | }); |
| 194 | |
| 195 | $test->it('303ではGETに変わり、本文を送らないはず', function($test) use ($srv, $local): void { |
| 196 | $curl = $local($srv->url('/redirect-303'))->setMethod('POST')->setPostRaw('secret=1'); |
| 197 | $curl->execute(); |
| 198 | $req = json_decode($curl->getResponseBody(), true); |
| 199 | $test->assertEquals('GET', $req['method']); |
| 200 | $test->assertEquals('', $req['body']); |
| 201 | $test->assertFalse(isset($req['headers']['content-length'])); |
| 202 | }); |
| 203 | |
| 204 | $test->it('別のオリジンへのリダイレクトでは認証情報とクッキーを送らないはず', function($test) use ($srv, $local): void { |
| 205 | $to = urlencode($srv->url('/echo', 'localhost')); |
| 206 | $curl = $local($srv->url("/redirect?to={$to}"))->setBearerAuth('secret')->addCookie('session', 'x') |
| 207 | ->addHeader('Authorization', 'Bearer secret2'); |
| 208 | $curl->execute(); |
| 209 | $h = json_decode($curl->getResponseBody(), true)['headers']; |
| 210 | $test->assertEquals('localhost', $h['host']); |
| 211 | $test->assertFalse(isset($h['authorization'])); |
| 212 | $test->assertFalse(isset($h['cookie'])); |
| 213 | }); |
| 214 | |
| 215 | $test->it('リダイレクトの回数に上限があるはず', function($test) use ($srv, $local): void { |
| 216 | $curl = $local($srv->url('/loop'))->setMaxRedirects(3); |
| 217 | $res = $curl->execute(); |
| 218 | $test->assertEquals(302, $curl->getResponseCode()); |
| 219 | $test->assertEquals(3, $curl->getInfo()['redirect_count']); |
| 220 | }); |
| 221 | |
| 222 | $test->it('大き過ぎるレスポンスを拒否するはず', function($test) use ($srv, $local): void { |
| 223 | $res = $local($srv->url('/big'))->setMaxResponseSize(1000)->execute(); |
| 224 | $test->assertFalse($res->isSuccess); |
| 225 | $test->assertStringContains('大き過ぎます', $res->message); |
| 226 | }); |
| 227 | |
| 228 | $test->it('タイムアウトするはず', function($test) use ($srv, $local): void { |
| 229 | $start = microtime(true); |
| 230 | $res = $local($srv->url('/slow'))->setTimeout(1)->execute(); |
| 231 | $test->assertFalse($res->isSuccess); |
| 232 | $test->assertStringContains('タイムアウト', $res->message); |
| 233 | $test->assertTrue(microtime(true) - $start < 2.5, 'タイムアウトに時間が掛かり過ぎ'); |
| 234 | }); |
| 235 | |
| 236 | $test->it('エラーのステータスもレスポンスとして返すはず', function($test) use ($srv, $local): void { |
| 237 | $curl = $local($srv->url('/missing')); |
| 238 | $res = $curl->execute(); |
| 239 | $test->assertTrue($res->isSuccess); |
| 240 | $test->assertEquals(404, $curl->getResponseCode()); |
| 241 | $test->assertEquals(['desc' => 'Not found'], json_decode($curl->getResponseBody(), true)); |
| 242 | }); |
| 243 | |
| 244 | $test->it('接続出来ない場合はエラーになるはず', function($test) use ($local): void { |
| 245 | // 使われていないポートを探す |
| 246 | $probe = stream_socket_server('tcp://127.0.0.1:0'); |
| 247 | $port = (int)substr(strrchr(stream_socket_get_name($probe, false), ':'), 1); |
| 248 | fclose($probe); |
| 249 | $res = $local("http://127.0.0.1:{$port}/")->setTimeout(2)->execute(); |
| 250 | $test->assertFalse($res->isSuccess); |
| 251 | $test->assertStringContains('接続出来ません', $res->message); |
| 252 | }); |
| 253 | |
| 254 | $srv?->stop(); |
| 255 | }); |
| 256 | |
| 257 | $test->describe('Curl HTTPS(PHP純正のTLS 1.3、偽物のサーバー)', function($test) use ($local): void { |
| 258 | $cert = TestCert::get('ec'); |
| 259 | $srv = CURL_ENABLED ? MockServer::start('http-server.php', [$cert['cert'], $cert['key']]) : null; |
| 260 | if ($srv === null) { |
| 261 | $test->skipAll('偽物のサーバーを起動出来ない(proc_openが無効?)'); |
| 262 | return; |
| 263 | } |
| 264 | $url = fn(string $path, string $host = '127.0.0.1') => "https://{$host}:{$srv->port}{$path}"; |
| 265 | |
| 266 | $test->it('HTTPSでリクエスト出来るはず(証明書を確認する)', function($test) use ($local, $url, $cert): void { |
| 267 | foreach (['127.0.0.1', 'localhost'] as $host) { |
| 268 | $curl = $local($url('/echo?x=1', $host))->setCaInfo($cert['ca'])->setMethod('POST')->setPostRaw('{"暗号":true}'); |
| 269 | $res = $curl->execute(); |
| 270 | $test->assertTrue($res->isSuccess, (string)$res->message); |
| 271 | $req = json_decode($curl->getResponseBody(), true); |
| 272 | $test->assertEquals('/echo?x=1', $req['target']); |
| 273 | $test->assertEquals('{"暗号":true}', $req['body']); |
| 274 | } |
| 275 | }); |
| 276 | |
| 277 | $test->it('信頼出来ない証明書は拒否して、確認しない設定なら繋がるはず', function($test) use ($local, $url, $cert): void { |
| 278 | $res = $local($url('/echo'))->setCaInfo(TestCert::get('self')['cert'])->execute(); |
| 279 | $test->assertFalse($res->isSuccess); |
| 280 | $test->assertStringContains('証明書', (string)$res->message); |
| 281 | |
| 282 | $res = $local($url('/echo'))->setCaInfo(TestCert::get('self')['cert'])->setVerifySSL(false)->execute(); |
| 283 | $test->assertTrue($res->isSuccess, (string)$res->message); |
| 284 | }); |
| 285 | |
| 286 | $srv->stop(); |
| 287 | }); |
| 288 | |
| 289 | $test->printSummary(); |