httpUtil.php 2.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. <?php
  2. declare(strict_types=1);
  3. final class FreshRSS_http_Util {
  4. private const RETRY_AFTER_PATH = DATA_PATH . '/Retry-After/';
  5. private static function getRetryAfterFile(string $url): string {
  6. $domain = parse_url($url, PHP_URL_HOST);
  7. if (!is_string($domain) || $domain === '') {
  8. return '';
  9. }
  10. $port = parse_url($url, PHP_URL_PORT);
  11. if (is_int($port)) {
  12. $domain .= ':' . $port;
  13. }
  14. return self::RETRY_AFTER_PATH . urlencode($domain) . '.txt';
  15. }
  16. /**
  17. * Clean up old Retry-After files
  18. */
  19. private static function cleanRetryAfters(): void {
  20. if (!is_dir(self::RETRY_AFTER_PATH)) {
  21. return;
  22. }
  23. $files = glob(self::RETRY_AFTER_PATH . '*.txt', GLOB_NOSORT);
  24. if ($files === false) {
  25. return;
  26. }
  27. foreach ($files as $file) {
  28. if (@filemtime($file) < time()) {
  29. @unlink($file);
  30. }
  31. }
  32. }
  33. /**
  34. * Check whether the URL needs to wait for a Retry-After period.
  35. * @return int The timestamp of when the Retry-After expires, or 0 if not set.
  36. */
  37. public static function getRetryAfter(string $url): int {
  38. if (rand(0, 30) === 1) { // Remove old files once in a while
  39. self::cleanRetryAfters();
  40. }
  41. $txt = self::getRetryAfterFile($url);
  42. if ($txt === '') {
  43. return 0;
  44. }
  45. $retryAfter = @filemtime($txt) ?: 0;
  46. if ($retryAfter <= 0) {
  47. return 0;
  48. }
  49. if ($retryAfter < time()) {
  50. @unlink($txt);
  51. return 0;
  52. }
  53. return $retryAfter;
  54. }
  55. /**
  56. * Store the HTTP Retry-After header value of an HTTP `429 Too Many Requests` or `503 Service Unavailable` response.
  57. */
  58. public static function setRetryAfter(string $url, string $retryAfter): int {
  59. $txt = self::getRetryAfterFile($url);
  60. if ($txt === '') {
  61. return 0;
  62. }
  63. $limits = FreshRSS_Context::systemConf()->limits;
  64. if (ctype_digit($retryAfter)) {
  65. $retryAfter = time() + (int)$retryAfter;
  66. } else {
  67. $retryAfter = \SimplePie\Misc::parse_date($retryAfter) ?:
  68. (time() + max(600, $limits['retry_after_default'] ?? 0));
  69. }
  70. $retryAfter = min($retryAfter, time() + max(3600, $limits['retry_after_max'] ?? 0));
  71. @mkdir(self::RETRY_AFTER_PATH);
  72. if (!touch($txt, $retryAfter)) {
  73. Minz_Log::error('Failed to set Retry-After for ' . $url);
  74. return 0;
  75. }
  76. return $retryAfter;
  77. }
  78. }