@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Update rate limiting for APCu and X-Forwarded-For

Summary:
Ref T12612. This updates the rate limiting code to:

- Support a customizable token, like the client's X-Forwarded-For address, rather than always using `REMOTE_ADDR`.
- Support APCu.
- Report a little more rate limiting information.
- Not reference nonexistent documentation (removed in D16403).

I'm planning to put this into production on `secure` for now and then we can deploy it more broadly if things work well.

Test Plan:
- Enabled it locally, used `ab -n 100` to hit the limit, saw the limit enforced.
- Waited a while, was allowed to browse again.

Reviewers: chad, amckinley

Reviewed By: amckinley

Maniphest Tasks: T12612

Differential Revision: https://secure.phabricator.com/D17758

+94 -27
+3 -4
src/aphront/configuration/AphrontApplicationConfiguration.php
··· 205 205 DarkConsoleXHProfPluginAPI::saveProfilerSample($access_log); 206 206 207 207 // Add points to the rate limits for this request. 208 - if (isset($_SERVER['REMOTE_ADDR'])) { 209 - $user_ip = $_SERVER['REMOTE_ADDR']; 210 - 208 + $rate_token = PhabricatorStartup::getRateLimitToken(); 209 + if ($rate_token !== null) { 211 210 // The base score for a request allows users to make 30 requests per 212 211 // minute. 213 212 $score = (1000 / 30); ··· 217 216 $score = $score / 5; 218 217 } 219 218 220 - PhabricatorStartup::addRateLimitScore($user_ip, $score); 219 + PhabricatorStartup::addRateLimitScore($rate_token, $score); 221 220 } 222 221 223 222 if ($processing_exception) {
+1 -2
src/docs/user/configuration/configuring_preamble.diviner
··· 1 1 @title Configuring a Preamble Script 2 2 @group config 3 3 4 - Adjust environmental settings (SSL, remote IP, rate limiting) using a preamble 5 - script. 4 + Adjust environmental settings (SSL, remote IPs) using a preamble script. 6 5 7 6 Overview 8 7 ========
+90 -21
support/PhabricatorStartup.php
··· 50 50 // iterate on it a bit for Conduit, some of the specific score levels, and 51 51 // to deal with NAT'd offices. 52 52 private static $maximumRate = 0; 53 + private static $rateLimitToken; 53 54 54 55 55 56 /* -( Accessing Request Information )-------------------------------------- */ ··· 137 138 // we can switch over to relying on our own exception recovery mechanisms. 138 139 ini_set('display_errors', 0); 139 140 140 - if (isset($_SERVER['REMOTE_ADDR'])) { 141 - self::rateLimitRequest($_SERVER['REMOTE_ADDR']); 141 + $rate_token = self::getRateLimitToken(); 142 + if ($rate_token !== null) { 143 + self::rateLimitRequest($rate_token); 142 144 } 143 145 144 146 self::normalizeInput(); ··· 681 683 682 684 683 685 /** 686 + * Set a token to identify the client for purposes of rate limiting. 687 + * 688 + * By default, the `REMOTE_ADDR` is used. If your install is behind a load 689 + * balancer, you may want to parse `X-Forwarded-For` and use that address 690 + * instead. 691 + * 692 + * @param string Client identity for rate limiting. 693 + */ 694 + public static function setRateLimitToken($token) { 695 + self::$rateLimitToken = $token; 696 + } 697 + 698 + 699 + /** 700 + * Get the current client identity for rate limiting. 701 + */ 702 + public static function getRateLimitToken() { 703 + if (self::$rateLimitToken !== null) { 704 + return self::$rateLimitToken; 705 + } 706 + 707 + if (isset($_SERVER['REMOTE_ADDR'])) { 708 + return $_SERVER['REMOTE_ADDR']; 709 + } 710 + 711 + return null; 712 + } 713 + 714 + 715 + /** 684 716 * Check if the user (identified by `$user_identity`) has issued too many 685 717 * requests recently. If they have, end the request with a 429 error code. 686 718 * ··· 699 731 } 700 732 701 733 $score = self::getRateLimitScore($user_identity); 702 - if ($score > (self::$maximumRate * self::getRateLimitBucketCount())) { 734 + $limit = self::$maximumRate * self::getRateLimitBucketCount(); 735 + if ($score > $limit) { 703 736 // Give the user some bonus points for getting rate limited. This keeps 704 737 // bad actors who keep slamming the 429 page locked out completely, 705 738 // instead of letting them get a burst of requests through every minute 706 739 // after a bucket expires. 707 - self::addRateLimitScore($user_identity, 50); 708 - self::didRateLimit($user_identity); 740 + $penalty = 50; 741 + 742 + self::addRateLimitScore($user_identity, $penalty); 743 + $score += $penalty; 744 + 745 + self::didRateLimit($user_identity, $score, $limit); 709 746 } 710 747 } 711 748 ··· 729 766 return; 730 767 } 731 768 769 + $is_apcu = (bool)function_exists('apcu_fetch'); 732 770 $current = self::getRateLimitBucket(); 733 771 734 - // There's a bit of a race here, if a second process reads the bucket before 735 - // this one writes it, but it's fine if we occasionally fail to record a 736 - // user's score. If they're making requests fast enough to hit rate 737 - // limiting, we'll get them soon. 772 + // There's a bit of a race here, if a second process reads the bucket 773 + // before this one writes it, but it's fine if we occasionally fail to 774 + // record a user's score. If they're making requests fast enough to hit 775 + // rate limiting, we'll get them soon enough. 738 776 739 777 $bucket_key = self::getRateLimitBucketKey($current); 740 - $bucket = apc_fetch($bucket_key); 778 + if ($is_apcu) { 779 + $bucket = apcu_fetch($bucket_key); 780 + } else { 781 + $bucket = apc_fetch($bucket_key); 782 + } 783 + 741 784 if (!is_array($bucket)) { 742 785 $bucket = array(); 743 786 } ··· 747 790 } 748 791 749 792 $bucket[$user_identity] += $score; 750 - apc_store($bucket_key, $bucket); 793 + 794 + if ($is_apcu) { 795 + apcu_store($bucket_key, $bucket); 796 + } else { 797 + apc_store($bucket_key, $bucket); 798 + } 751 799 } 752 800 753 801 ··· 761 809 * @task ratelimit 762 810 */ 763 811 private static function canRateLimit() { 812 + 764 813 if (!self::$maximumRate) { 765 814 return false; 766 815 } 767 816 768 - if (!function_exists('apc_fetch')) { 817 + if (!function_exists('apc_fetch') && !function_exists('apcu_fetch')) { 769 818 return false; 770 819 } 771 820 ··· 826 875 * @task ratelimit 827 876 */ 828 877 private static function getRateLimitScore($user_identity) { 878 + $is_apcu = (bool)function_exists('apcu_fetch'); 879 + 829 880 $min_key = self::getRateLimitMinKey(); 830 881 831 882 // Identify the oldest bucket stored in APC. 832 883 $cur = self::getRateLimitBucket(); 833 - $min = apc_fetch($min_key); 884 + if ($is_apcu) { 885 + $min = apcu_fetch($min_key); 886 + } else { 887 + $min = apc_fetch($min_key); 888 + } 834 889 835 890 // If we don't have any buckets stored yet, store the current bucket as 836 891 // the oldest bucket. 837 892 if (!$min) { 838 - apc_store($min_key, $cur); 893 + if ($is_apcu) { 894 + apcu_store($min_key, $cur); 895 + } else { 896 + apc_store($min_key, $cur); 897 + } 839 898 $min = $cur; 840 899 } 841 900 ··· 844 903 // up an old bucket once per minute. 845 904 $count = self::getRateLimitBucketCount(); 846 905 for ($cursor = $min; $cursor < ($cur - $count); $cursor++) { 847 - apc_delete(self::getRateLimitBucketKey($cursor)); 848 - apc_store($min_key, $cursor + 1); 906 + $bucket_key = self::getRateLimitBucketKey($cursor); 907 + if ($is_apcu) { 908 + apcu_delete($bucket_key); 909 + apcu_store($min_key, $cursor + 1); 910 + } else { 911 + apc_delete($bucket_key); 912 + apc_store($min_key, $cursor + 1); 913 + } 849 914 } 850 915 851 916 // Now, sum up the user's scores in all of the active buckets. 852 917 $score = 0; 853 918 for (; $cursor <= $cur; $cursor++) { 854 - $bucket = apc_fetch(self::getRateLimitBucketKey($cursor)); 919 + $bucket_key = self::getRateLimitBucketKey($cursor); 920 + if ($is_apcu) { 921 + $bucket = apcu_fetch($bucket_key); 922 + } else { 923 + $bucket = apc_fetch($bucket_key); 924 + } 855 925 if (isset($bucket[$user_identity])) { 856 926 $score += $bucket[$user_identity]; 857 927 } ··· 868 938 * @return exit This method **does not return**. 869 939 * @task ratelimit 870 940 */ 871 - private static function didRateLimit() { 941 + private static function didRateLimit($user_identity, $score, $limit) { 872 942 $message = 873 943 "TOO MANY REQUESTS\n". 874 - "You are issuing too many requests too quickly.\n". 875 - "To adjust limits, see \"Configuring a Preamble Script\" in the ". 876 - "documentation."; 944 + "You (\"{$user_identity}\") are issuing too many requests ". 945 + "too quickly.\n"; 877 946 878 947 header( 879 948 'Content-Type: text/plain; charset=utf-8',