You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

90 lines
3.6 KiB

​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
  1. <?php
  2. namespace p3k\HTTP\Tests;
  3. use p3k\HTTP;
  4. use p3k\HTTP\Curl;
  5. use PHPUnit\Framework\TestCase;
  6. // Real requests through curl to PHP's built-in server on 127.0.0.1.
  7. class CurlPinningTest extends TestCase {
  8. private static $server;
  9. private static $port;
  10. public static function setUpBeforeClass(): void {
  11. self::$port = 20000 + random_int(0, 9999);
  12. // A command string rather than an array, which proc_open only accepts as
  13. // of PHP 7.4. exec replaces the shell with the server, so proc_terminate
  14. // stops the server itself rather than just the shell.
  15. self::$server = proc_open(
  16. 'exec ' . escapeshellarg(PHP_BINARY) . ' -S 127.0.0.1:' . self::$port . ' -t ' . escapeshellarg(__DIR__ . '/server'),
  17. [1 => ['file', '/dev/null', 'w'], 2 => ['file', '/dev/null', 'w']],
  18. $pipes
  19. );
  20. for($i = 0; $i < 50; $i++) {
  21. $socket = @fsockopen('127.0.0.1', self::$port);
  22. if($socket) { fclose($socket); return; }
  23. usleep(100000);
  24. }
  25. self::fail('The test server did not start');
  26. }
  27. public static function tearDownAfterClass(): void {
  28. proc_terminate(self::$server);
  29. }
  30. private function http() {
  31. $http = new HTTP('test');
  32. // "pinned.example" exists only in this resolver; curl can reach it only
  33. // through the pinned address.
  34. $http->set_safe_mode(true, ['127.0.0.1'], function($host) { return $host === 'pinned.example' ? ['127.0.0.1'] : []; });
  35. return $http;
  36. }
  37. public function testConnectsToThePinnedAddress() {
  38. $response = $this->http()->get('http://pinned.example:' . self::$port . '/');
  39. $this->assertSame(200, $response['code']);
  40. $this->assertSame('host=pinned.example:' . self::$port, $response['body']);
  41. }
  42. // curl keeps only the last CURLOPT_RESOLVE entry for a host and port, so
  43. // every resolved address has to go in a single entry. Otherwise a host with
  44. // both IPv6 and IPv4 addresses is only tried on the last one.
  45. public function testTriesEveryPinnedAddress() {
  46. $http = new HTTP('test');
  47. $http->set_safe_mode(true, ['127.0.0.0/8'], function($host) {
  48. // Nothing listens on 127.0.0.3, so this only succeeds if curl can fall back to 127.0.0.1
  49. return $host === 'pinned.example' ? ['127.0.0.1', '127.0.0.3'] : [];
  50. });
  51. $response = $http->get('http://pinned.example:' . self::$port . '/');
  52. $this->assertSame(200, $response['code']);
  53. $this->assertSame('host=pinned.example:' . self::$port, $response['body']);
  54. }
  55. public function testRedirectsAreCheckedHopByHop() {
  56. $port = self::$port;
  57. $response = $this->http()->get("http://pinned.example:$port/?to=" . rawurlencode("http://127.0.0.1:$port/"));
  58. $this->assertSame(200, $response['code']);
  59. $this->assertSame("http://127.0.0.1:$port/", $response['url']);
  60. $response = $this->http()->get("http://pinned.example:$port/?to=" . rawurlencode("http://127.0.0.2:$port/"));
  61. $this->assertSame('blocked_url', $response['error']);
  62. }
  63. public function testPinnedCurlRefusesOtherProtocols() {
  64. $curl = new Curl();
  65. $curl->pin_addresses([]);
  66. $response = $curl->get('dict://127.0.0.1:' . self::$port . '/info');
  67. $this->assertSame(0, $response['code']);
  68. $this->assertNotSame('', $response['error_description']);
  69. }
  70. // HTTP/2 is only negotiated over TLS. Plain http stays on HTTP/1.1 rather
  71. // than sending an h2c Upgrade request, which some servers mishandle (PHP
  72. // 7.3's built-in server drops the connection).
  73. public function testPlainHttpDoesNotAttemptHttp2Upgrade() {
  74. $response = (new HTTP('test'))->get('http://127.0.0.1:' . self::$port . '/?upgrade=1');
  75. $this->assertSame(200, $response['code']);
  76. $this->assertSame('upgrade=', $response['body']);
  77. }
  78. }