YourlsProxyTest.php 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. <?php declare(strict_types=1);
  2. use PHPUnit\Framework\TestCase;
  3. use PrivateBin\Configuration;
  4. use PrivateBin\Proxy\YourlsProxy;
  5. class YourlsProxyTest extends TestCase
  6. {
  7. private $_conf;
  8. private $_path;
  9. private $_mock_yourls_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_yourls_service = $this->_path . DIRECTORY_SEPARATOR . 'yourls.json';
  18. $options = parse_ini_file(CONF_SAMPLE, true);
  19. $options['main']['basepath'] = 'https://example.com/';
  20. $options['main']['urlshortener'] = 'https://example.com/shortenviayourls?link=';
  21. $options['yourls']['apiurl'] = $this->_mock_yourls_service;
  22. Helper::confBackup();
  23. Helper::createIniFile(CONF, $options);
  24. $this->_conf = new Configuration;
  25. }
  26. public function tearDown(): void
  27. {
  28. /* Tear Down Routine */
  29. unlink(CONF);
  30. Helper::confRestore();
  31. Helper::rmDir($this->_path);
  32. }
  33. public function testYourlsProxy()
  34. {
  35. // the real service answer is more complex, but we only look for the shorturl & statusCode
  36. file_put_contents($this->_mock_yourls_service, '{"shorturl":"https:\/\/example.com\/1","statusCode":200}');
  37. $yourls = new YourlsProxy($this->_conf, 'https://example.com/?foo#bar');
  38. $this->assertFalse($yourls->isError());
  39. $this->assertEquals($yourls->getUrl(), 'https://example.com/1');
  40. $yourls = new YourlsProxy($this->_conf, 'https://example.com/?@foreign.malicious.example?foo#bar');
  41. $this->assertFalse($yourls->isError());
  42. $this->assertEquals($yourls->getUrl(), 'https://example.com/1');
  43. }
  44. public function testYourlsProxyWithStringStatusCode(): void
  45. {
  46. // YOURLS API returns statusCode as a string "200", not integer 200
  47. file_put_contents($this->_mock_yourls_service, '{"shorturl":"https:\/\/example.com\/1","statusCode":"200"}');
  48. $yourls = new YourlsProxy($this->_conf, 'https://example.com/?foo#bar');
  49. $this->assertFalse($yourls->isError());
  50. $this->assertEquals($yourls->getUrl(), 'https://example.com/1');
  51. }
  52. /**
  53. * @dataProvider providerInvalidUrl
  54. */
  55. public function testImvalidUrl($url): void
  56. {
  57. $yourls = new YourlsProxy($this->_conf, $url);
  58. $this->assertTrue($yourls->isError());
  59. $this->assertEquals($yourls->getError(), 'Invalid URL given.');
  60. }
  61. public function providerInvalidUrl(): array
  62. {
  63. return [
  64. [''],
  65. [' '],
  66. ['foo'],
  67. ['https://'],
  68. ['https://example.com'], // missing path and query parameter,
  69. ['https://example.com/'], // missing query parameter
  70. ['https://example.com?paste=something'], // missing path parameter
  71. ['https://example.com@foreign.malicious.example?foo#bar'], // missing path parameter
  72. ];
  73. }
  74. /**
  75. * This tests for a trick using username of an URI, see:
  76. * {@see https://cloud.google.com/blog/topics/threat-intelligence/url-obfuscation-schema-abuse/?hl=en}
  77. *
  78. * @dataProvider providerForeignUrlUsernameTrick
  79. */
  80. public function testForeignUrlUsingUsernameTrick($url): void
  81. {
  82. $yourls = new YourlsProxy($this->_conf, $url);
  83. $this->assertTrue($yourls->isError());
  84. $this->assertEquals($yourls->getError(), 'Trying to shorten a URL that isn\'t pointing at our instance.');
  85. }
  86. public function providerForeignUrlUsernameTrick(): array
  87. {
  88. return [
  89. ['https://example.com@foreign.malicious.example/?foo#bar'],
  90. ['https://example.com/@foreign.malicious.example?foo#bar'],
  91. ];
  92. }
  93. /**
  94. * @dataProvider providerForeignUrl
  95. */
  96. public function testForeignUrl($url): void
  97. {
  98. $yourls = new YourlsProxy($this->_conf, $url);
  99. $this->assertTrue($yourls->isError());
  100. $this->assertEquals($yourls->getError(), 'Trying to shorten a URL that isn\'t pointing at our instance.');
  101. }
  102. public function providerForeignUrl(): array
  103. {
  104. return [
  105. ['ftp://example.com/?n=np'], // wrong protocol
  106. ['https://other.example.com/?foo#bar'], // wrong domain
  107. ['https://other.example.com/?q=https://example.com/?foo#bar'], // domain included inside string
  108. ];
  109. }
  110. public function testYourlsError()
  111. {
  112. // when statusCode is not 200, shorturl may not have been set
  113. file_put_contents($this->_mock_yourls_service, '{"statusCode":403}');
  114. $yourls = new YourlsProxy($this->_conf, 'https://example.com/?foo#bar');
  115. $this->assertTrue($yourls->isError());
  116. $this->assertEquals($yourls->getError(), 'Proxy error: Error parsing proxy response. This can be a configuration issue, like wrong or missing config keys.');
  117. }
  118. public function testYourlsSuccessWithoutShortUrl()
  119. {
  120. // YOURLS may reply with statusCode 200 but without a shorturl field;
  121. // this must be handled gracefully as an error instead of raising a
  122. // TypeError (the method is declared to return ?string). YOURLS returns
  123. // the status code as a string, so mirror that here.
  124. file_put_contents($this->_mock_yourls_service, '{"statusCode":"200"}');
  125. $yourls = new YourlsProxy($this->_conf, 'https://example.com/?foo#bar');
  126. $this->assertTrue($yourls->isError());
  127. $this->assertEquals($yourls->getError(), 'Proxy error: Error parsing proxy response. This can be a configuration issue, like wrong or missing config keys.');
  128. }
  129. public function testServerError()
  130. {
  131. // simulate some other server error that results in a non-JSON reply
  132. file_put_contents($this->_mock_yourls_service, '500 Internal Server Error');
  133. $yourls = new YourlsProxy($this->_conf, 'https://example.com/?foo#bar');
  134. $this->assertTrue($yourls->isError());
  135. $this->assertEquals($yourls->getError(), 'Proxy error: Error parsing proxy response. This can be a configuration issue, like wrong or missing config keys.');
  136. }
  137. }