BxAnalyticsRequestTest.php 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. <?php
  2. use PHPUnit\Framework\Attributes\DataProvider;
  3. /**
  4. * Client address behind proxies, country headers and privacy signals.
  5. */
  6. class BxAnalyticsRequestTest extends \PHPUnit\Framework\TestCase
  7. {
  8. public static function setUpBeforeClass(): void
  9. {
  10. require_once BX_DIRECTORY_PATH_MODULES . 'boonex/analytics/classes/BxAnalyticsConfig.php';
  11. require_once BX_DIRECTORY_PATH_MODULES . 'boonex/analytics/classes/BxAnalyticsRequest.php';
  12. }
  13. public static function providerIp()
  14. {
  15. return [
  16. 'direct visitor' => [['REMOTE_ADDR' => '81.2.69.142'], [], false, '81.2.69.142'],
  17. 'public client ignores forwarded headers' => [['REMOTE_ADDR' => '81.2.69.142', 'HTTP_X_FORWARDED_FOR' => '8.8.8.8'], [], false, '81.2.69.142'],
  18. 'private proxy with a chain' => [['REMOTE_ADDR' => '10.0.0.5', 'HTTP_X_FORWARDED_FOR' => '93.184.216.34, 10.0.0.1'], [], false, '93.184.216.34'],
  19. 'private hop first in the chain' => [['REMOTE_ADDR' => '10.0.0.5', 'HTTP_X_FORWARDED_FOR' => '192.168.1.2, 93.184.216.34'], [], false, '93.184.216.34'],
  20. 'Cloudflare header without X-Forwarded-For' => [['REMOTE_ADDR' => '172.16.0.2', 'HTTP_CF_CONNECTING_IP' => '2001:4860:4860::8888'], [], false, '2001:4860:4860::8888'],
  21. 'X-Forwarded-For before a header the proxy may pass through' => [['REMOTE_ADDR' => '172.16.0.2', 'HTTP_CF_CONNECTING_IP' => '8.8.8.8', 'HTTP_X_FORWARDED_FOR' => '93.184.216.34'], [], false, '93.184.216.34'],
  22. 'address the client sent is not taken' => [['REMOTE_ADDR' => '127.0.0.1', 'HTTP_X_FORWARDED_FOR' => '8.8.8.8, 93.184.216.34'], [], false, '93.184.216.34'],
  23. 'shared address space of a cloud load balancer' => [['REMOTE_ADDR' => '100.64.0.2', 'HTTP_X_FORWARDED_FOR' => '8.8.8.8, 93.184.216.34'], [], false, '93.184.216.34'],
  24. 'trusted hops skipped from the right' => [['REMOTE_ADDR' => '10.0.0.5', 'HTTP_X_FORWARDED_FOR' => '8.8.8.8, 93.184.216.34, 81.2.69.10'], ['81.2.69.0/24'], false, '93.184.216.34'],
  25. 'trusted public proxy' => [['REMOTE_ADDR' => '81.2.69.10', 'HTTP_X_FORWARDED_FOR' => '93.184.216.34'], ['81.2.69.0/24'], false, '93.184.216.34'],
  26. 'request with the API key' => [['REMOTE_ADDR' => '76.76.21.21', 'HTTP_X_FORWARDED_FOR' => '93.184.216.34'], [], true, '93.184.216.34'],
  27. 'address with a port' => [['REMOTE_ADDR' => '10.0.0.5', 'HTTP_X_FORWARDED_FOR' => '93.184.216.34:51234'], [], false, '93.184.216.34'],
  28. 'garbage header falls back' => [['REMOTE_ADDR' => '10.0.0.5', 'HTTP_X_FORWARDED_FOR' => 'unknown, <script>'], [], false, '10.0.0.5'],
  29. 'local network only' => [['REMOTE_ADDR' => '172.18.0.4', 'HTTP_X_FORWARDED_FOR' => '192.168.65.1'], [], false, '192.168.65.1'],
  30. ];
  31. }
  32. #[DataProvider('providerIp')]
  33. public function testGetIp($aServer, $aTrusted, $bTrustedRequest, $sExpected)
  34. {
  35. $this->assertSame($sExpected, (new BxAnalyticsRequest($aServer))->getIp($aTrusted, $bTrustedRequest));
  36. }
  37. public function testInList()
  38. {
  39. $this->assertTrue(BxAnalyticsRequest::inList('203.0.113.77', ['10.0.0.0/8', '203.0.113.0/24']));
  40. $this->assertTrue(BxAnalyticsRequest::inList('203.0.113.77', ['203.0.113.77']));
  41. $this->assertFalse(BxAnalyticsRequest::inList('203.0.114.1', ['203.0.113.0/24']));
  42. $this->assertTrue(BxAnalyticsRequest::inList('2001:db8::1', ['2001:db8::/32']));
  43. $this->assertFalse(BxAnalyticsRequest::inList('2001:db9::1', ['2001:db8::/32']));
  44. $this->assertTrue(BxAnalyticsRequest::inList('10.1.2.3', ['10.0.0.0/9']));
  45. $this->assertFalse(BxAnalyticsRequest::inList('10.200.2.3', ['10.0.0.0/9']));
  46. $this->assertFalse(BxAnalyticsRequest::inList('not-an-ip', ['0.0.0.0/0']));
  47. }
  48. public function testCountry()
  49. {
  50. $this->assertSame('DE', (new BxAnalyticsRequest(['HTTP_CF_IPCOUNTRY' => 'de']))->getCountry());
  51. $this->assertSame('AU', (new BxAnalyticsRequest(['HTTP_X_VERCEL_IP_COUNTRY' => 'AU']))->getCountry());
  52. $this->assertSame('', (new BxAnalyticsRequest(['HTTP_CF_IPCOUNTRY' => 'XX']))->getCountry(), 'unknown');
  53. $this->assertSame('', (new BxAnalyticsRequest(['HTTP_CF_IPCOUNTRY' => 'T1']))->getCountry(), 'Tor');
  54. $this->assertSame('São Paulo', (new BxAnalyticsRequest(['HTTP_X_VERCEL_IP_CITY' => 'S%C3%A3o%20Paulo']))->getCity());
  55. }
  56. public function testLocationFromOneEdgeOnly()
  57. {
  58. // behind Vercel, a client adds Cloudflare's headers, which Vercel passes through
  59. $oRequest = new BxAnalyticsRequest([
  60. 'HTTP_CF_IPCOUNTRY' => 'KP',
  61. 'HTTP_CF_IPCITY' => 'Pyongyang',
  62. 'HTTP_CF_IPLATITUDE' => '39.03',
  63. 'HTTP_CF_IPLONGITUDE' => '125.75',
  64. 'HTTP_X_VERCEL_IP_COUNTRY' => 'DE',
  65. 'HTTP_X_VERCEL_IP_COUNTRY_REGION' => 'BE',
  66. 'HTTP_X_VERCEL_IP_CITY' => 'Berlin',
  67. ]);
  68. $this->assertSame('KP', $oRequest->getCountry(), 'any edge: the first header found');
  69. $this->assertSame(['DE', 'BE', 'Berlin', null], [$oRequest->getCountry('vercel'), $oRequest->getRegion('vercel'), $oRequest->getCity('vercel'), $oRequest->getCoordinates('vercel')]);
  70. $this->assertSame(['header' => 'HTTP_X_VERCEL_IP_COUNTRY', 'country' => 'DE'], $oRequest->getCountryHeader('vercel'));
  71. $this->assertSame(['KP', '', 'Pyongyang', [39.0, 125.8]], [$oRequest->getCountry('cloudflare'), $oRequest->getRegion('cloudflare'), $oRequest->getCity('cloudflare'), $oRequest->getCoordinates('cloudflare')]);
  72. $this->assertSame(['', '', '', null], [$oRequest->getCountry('cloudfront'), $oRequest->getRegion('cloudfront'), $oRequest->getCity('cloudfront'), $oRequest->getCoordinates('cloudfront')]);
  73. $this->assertSame('', $oRequest->getCountry('unknown'), 'an unknown edge has no headers');
  74. $oRequest = new BxAnalyticsRequest(['HTTP_CLOUDFRONT_VIEWER_COUNTRY' => 'fr', 'HTTP_CLOUDFRONT_VIEWER_CITY' => 'Lyon', 'HTTP_CLOUDFRONT_VIEWER_LATITUDE' => '45.76', 'HTTP_CLOUDFRONT_VIEWER_LONGITUDE' => '4.83']);
  75. $this->assertSame(['FR', 'Lyon', [45.8, 4.8]], [$oRequest->getCountry('cloudfront'), $oRequest->getCity('cloudfront'), $oRequest->getCoordinates('cloudfront')]);
  76. }
  77. public static function providerPlatform()
  78. {
  79. return [
  80. 'cron' => [[], false, false, 'core'],
  81. 'classic page' => [['REQUEST_METHOD' => 'POST'], false, false, 'core-web'],
  82. 'NEO says so' => [['REQUEST_METHOD' => 'GET', 'HTTP_X_NEO_PLATFORM' => 'android'], true, false, 'rn-android'],
  83. 'NEO web server' => [['REQUEST_METHOD' => 'GET'], true, true, 'next-web'],
  84. 'iOS app' => [['REQUEST_METHOD' => 'GET', 'HTTP_ORIGIN' => 'neo://app', 'HTTP_USER_AGENT' => 'NEO/1 CFNetwork/1568 Darwin/24.0.0'], true, false, 'rn-ios'],
  85. 'Android app' => [['REQUEST_METHOD' => 'GET', 'HTTP_ORIGIN' => 'neo://app', 'HTTP_USER_AGENT' => 'okhttp/4.12.0'], true, false, 'rn-android'],
  86. 'other API client' => [['REQUEST_METHOD' => 'GET', 'HTTP_USER_AGENT' => 'curl/8.4.0'], true, false, 'api'],
  87. ];
  88. }
  89. #[DataProvider('providerPlatform')]
  90. public function testPlatform($aServer, $bApi, $bApiKey, $sExpected)
  91. {
  92. $this->assertSame($sExpected, (new BxAnalyticsRequest($aServer))->getPlatform($bApi, $bApiKey));
  93. }
  94. public function testPrivacySignals()
  95. {
  96. $this->assertTrue((new BxAnalyticsRequest(['HTTP_DNT' => '1']))->isDoNotTrack());
  97. $this->assertTrue((new BxAnalyticsRequest(['HTTP_SEC_GPC' => '1']))->isDoNotTrack());
  98. $this->assertFalse((new BxAnalyticsRequest(['HTTP_DNT' => '0']))->isDoNotTrack());
  99. $this->assertTrue((new BxAnalyticsRequest(['HTTP_SEC_FETCH_SITE' => 'cross-site']))->isCrossSite());
  100. $this->assertFalse((new BxAnalyticsRequest(['HTTP_SEC_FETCH_SITE' => 'same-origin']))->isCrossSite());
  101. $this->assertFalse((new BxAnalyticsRequest([]))->isCrossSite(), 'browsers without the header pass');
  102. }
  103. }