ChhotoProxyTest.php 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. <?php declare(strict_types=1);
  2. use PHPUnit\Framework\TestCase;
  3. use PrivateBin\Configuration;
  4. use PrivateBin\Proxy\ChhotoProxy;
  5. class ChhotoProxyTest extends TestCase
  6. {
  7. private $_conf;
  8. private $_path;
  9. private $_mock_chhoto_service;
  10. public function setUp(): void
  11. {
  12. /* Setup Routine */
  13. $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
  14. if (!is_dir($this->_path)) {
  15. mkdir($this->_path);
  16. }
  17. $this->_mock_chhoto_service = $this->_path . DIRECTORY_SEPARATOR . 'chhoto.json';
  18. $options = parse_ini_file(CONF_SAMPLE, true);
  19. $options['main']['basepath'] = 'https://example.com/';
  20. $options['main']['urlshortener'] = 'https://example.com/shortenviachhoto?link=';
  21. $options['chhoto']['apiurl'] = $this->_mock_chhoto_service;
  22. $options['chhoto']['apikey'] = 'test_api_key';
  23. Helper::confBackup();
  24. Helper::createIniFile(CONF, $options);
  25. $this->_conf = new Configuration;
  26. }
  27. public function tearDown(): void
  28. {
  29. /* Tear Down Routine */
  30. unlink(CONF);
  31. Helper::confRestore();
  32. Helper::rmDir($this->_path);
  33. }
  34. public function testChhotoProxy()
  35. {
  36. // Chhoto usually returns the full short URL in the "shorturl" field.
  37. file_put_contents($this->_mock_chhoto_service, '{"shorturl":"https:\/\/lix.sk\/abc123"}');
  38. $chhoto = new ChhotoProxy($this->_conf, 'https://example.com/?foo#bar');
  39. $this->assertFalse($chhoto->isError());
  40. $this->assertEquals($chhoto->getUrl(), 'https://lix.sk/abc123');
  41. // A URL that embeds a foreign host in the user-info part must still be
  42. // accepted, because the host of the link is our own instance.
  43. $chhoto = new ChhotoProxy($this->_conf, 'https://example.com/?@foreign.malicious.example?foo#bar');
  44. $this->assertFalse($chhoto->isError());
  45. $this->assertEquals($chhoto->getUrl(), 'https://lix.sk/abc123');
  46. }
  47. public function testChhotoProxyWithSlugFallback()
  48. {
  49. // Older Chhoto versions may return only the slug in "shortlink" instead
  50. // of a full "shorturl". The proxy then prepends the configured apiurl.
  51. file_put_contents($this->_mock_chhoto_service, '{"shortlink":"abc123"}');
  52. $chhoto = new ChhotoProxy($this->_conf, 'https://example.com/?foo#bar');
  53. $this->assertFalse($chhoto->isError());
  54. // apiurl (the mock file path) is prepended to the ltrimmed slug.
  55. $this->assertEquals($chhoto->getUrl(), $this->_mock_chhoto_service . 'abc123');
  56. }
  57. public function testChhotoProxyWithLeadingSlashSlug()
  58. {
  59. // A slug that already starts with a slash must not produce a double slash.
  60. file_put_contents($this->_mock_chhoto_service, '{"shortlink":"/abc123"}');
  61. $chhoto = new ChhotoProxy($this->_conf, 'https://example.com/?foo#bar');
  62. $this->assertFalse($chhoto->isError());
  63. $this->assertEquals($chhoto->getUrl(), $this->_mock_chhoto_service . 'abc123');
  64. }
  65. /**
  66. * @dataProvider providerInvalidUrl
  67. */
  68. public function testInvalidUrl($url): void
  69. {
  70. $chhoto = new ChhotoProxy($this->_conf, $url);
  71. $this->assertTrue($chhoto->isError());
  72. $this->assertEquals($chhoto->getError(), 'Invalid URL given.');
  73. }
  74. public function providerInvalidUrl(): array
  75. {
  76. return [
  77. [''],
  78. [' '],
  79. ['foo'],
  80. ['https://'],
  81. ['https://example.com'], // missing path and query parameter,
  82. ['https://example.com/'], // missing query parameter
  83. ['https://example.com?paste=something'], // missing path parameter
  84. ['https://example.com@foreign.malicious.example?foo#bar'], // missing path parameter
  85. ];
  86. }
  87. /**
  88. * This tests for a trick using username of an URI, see:
  89. * {@see https://cloud.google.com/blog/topics/threat-intelligence/url-obfuscation-schema-abuse/?hl=en}
  90. *
  91. * @dataProvider providerForeignUrlUsernameTrick
  92. */
  93. public function testForeignUrlUsingUsernameTrick($url): void
  94. {
  95. $chhoto = new ChhotoProxy($this->_conf, $url);
  96. $this->assertTrue($chhoto->isError());
  97. $this->assertEquals($chhoto->getError(), 'Trying to shorten a URL that isn\'t pointing at our instance.');
  98. }
  99. public function providerForeignUrlUsernameTrick(): array
  100. {
  101. return [
  102. ['https://example.com@foreign.malicious.example/?foo#bar'],
  103. ['https://example.com/@foreign.malicious.example?foo#bar'],
  104. ];
  105. }
  106. /**
  107. * @dataProvider providerForeignUrl
  108. */
  109. public function testForeignUrl($url): void
  110. {
  111. $chhoto = new ChhotoProxy($this->_conf, $url);
  112. $this->assertTrue($chhoto->isError());
  113. $this->assertEquals($chhoto->getError(), 'Trying to shorten a URL that isn\'t pointing at our instance.');
  114. }
  115. public function providerForeignUrl(): array
  116. {
  117. return [
  118. ['ftp://example.com/?n=np'], // wrong protocol
  119. ['https://other.example.com/?foo#bar'], // wrong domain
  120. ['https://other.example.com/?q=https://example.com/?foo#bar'], // domain included inside string
  121. ];
  122. }
  123. public function testChhotoError()
  124. {
  125. // Chhoto may reply with a body that contains neither "shorturl" nor
  126. // "shortlink"; this must be handled gracefully as an error instead of
  127. // raising a TypeError (the method is declared to return ?string).
  128. file_put_contents($this->_mock_chhoto_service, '{"message":"error"}');
  129. $chhoto = new ChhotoProxy($this->_conf, 'https://example.com/?foo#bar');
  130. $this->assertTrue($chhoto->isError());
  131. $this->assertEquals($chhoto->getError(), 'Proxy error: Error parsing proxy response. This can be a configuration issue, like wrong or missing config keys.');
  132. }
  133. public function testChhotoSuccessWithoutShortUrl()
  134. {
  135. // A 200-style reply that omits the short URL fields must be treated as
  136. // an error, not as a successful (empty) shortening.
  137. file_put_contents($this->_mock_chhoto_service, '{"status":"ok"}');
  138. $chhoto = new ChhotoProxy($this->_conf, 'https://example.com/?foo#bar');
  139. $this->assertTrue($chhoto->isError());
  140. $this->assertEquals($chhoto->getError(), 'Proxy error: Error parsing proxy response. This can be a configuration issue, like wrong or missing config keys.');
  141. }
  142. public function testServerError()
  143. {
  144. // simulate some other server error that results in a non-JSON reply
  145. file_put_contents($this->_mock_chhoto_service, '500 Internal Server Error');
  146. $chhoto = new ChhotoProxy($this->_conf, 'https://example.com/?foo#bar');
  147. $this->assertTrue($chhoto->isError());
  148. $this->assertEquals($chhoto->getError(), 'Proxy error: Error parsing proxy response. This can be a configuration issue, like wrong or missing config keys.');
  149. }
  150. }