PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.22
MxChat – AI Chatbot & Content Generation for WordPress v3.2.22
3.2.25 3.2.24 3.2.23 3.2.22 3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 All 156 releases
mxchat-basic / tests / verify-rate-limit-284318.php

verify-rate-limit-284318.php in MxChat – AI Chatbot & Content Generation for WordPress 3.2.22, at tests/verify-rate-limit-284318.php

176 lines 10.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * RELEASE GUARD — per-visitor rate limiting (plan 284318).
4 *
5 * Three things must hold, or a logged-out cap does not hold either:
6 * (a) the counter is keyed on an address the visitor cannot choose —
7 * REMOTE_ADDR, or CF-Connecting-IP only when the request really came
8 * from a Cloudflare edge; Client-IP / X-Forwarded-For are never read;
9 * (b) the hourly reset parses counter names correctly and deletes a
10 * counter only once its window has elapsed — the old split-on-'_'
11 * parser mis-read every name and deleted every counter every hour;
12 * (c) counters are written without autoload and the number of visitor
13 * counters per bot is bounded.
14 *
15 * Run on a test site (it writes and removes its own rate-limit options and
16 * briefly rewrites mxchat_options.rate_limits, restoring the original):
17 *
18 * wp eval-file wp-content/plugins/mxchat-basic/tests/verify-rate-limit-284318.php
19 *
20 * A build that prints FAIL is NOT releasable.
21 */
22 if (!defined('ABSPATH')) {
23 exit;
24 }
25
26 global $mxchat_integrator, $wpdb;
27 $integrator = ($mxchat_integrator instanceof MxChat_Integrator) ? $mxchat_integrator : new MxChat_Integrator();
28
29 $results = array();
30 $check = function ($name, $pass, $detail = '') use (&$results) {
31 $results[] = array($name, (bool) $pass, (string) $detail);
32 };
33 $call_private = function ($obj, $method, ...$args) {
34 $m = new ReflectionMethod($obj, $method);
35 $m->setAccessible(true);
36 return $m->invoke($obj, ...$args);
37 };
38
39 // ---------------------------------------------------------------- (a) client IP
40 $saved_server = $_SERVER;
41 $set = function ($remote, $extra = array()) {
42 foreach (array('HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_CF_CONNECTING_IP', 'REMOTE_ADDR') as $k) {
43 unset($_SERVER[$k]);
44 }
45 if ($remote !== null) {
46 $_SERVER['REMOTE_ADDR'] = $remote;
47 }
48 foreach ($extra as $k => $v) {
49 $_SERVER[$k] = $v;
50 }
51 };
52
53 $set('198.51.100.7', array('HTTP_X_FORWARDED_FOR' => '203.0.113.9, 198.51.100.7', 'HTTP_CLIENT_IP' => '203.0.113.8'));
54 $check('a1 forged X-Forwarded-For / Client-IP ignored', MxChat_User::mxchat_get_client_ip() === '198.51.100.7', MxChat_User::mxchat_get_client_ip());
55 $check('a1b transcript identifier agrees', MxChat_User::mxchat_get_user_identifier() === '198.51.100.7', MxChat_User::mxchat_get_user_identifier());
56 $check('a1c integrator get_client_ip agrees', $call_private($integrator, 'get_client_ip') === '198.51.100.7');
57
58 $set('173.245.48.10', array('HTTP_CF_CONNECTING_IP' => '203.0.113.9'));
59 $check('a2 CF-Connecting-IP honoured from a Cloudflare IPv4 edge', MxChat_User::mxchat_get_client_ip() === '203.0.113.9', MxChat_User::mxchat_get_client_ip());
60
61 $set('2606:4700::1234', array('HTTP_CF_CONNECTING_IP' => '2001:db8::7'));
62 $check('a3 CF-Connecting-IP honoured from a Cloudflare IPv6 edge', MxChat_User::mxchat_get_client_ip() === '2001:db8::7', MxChat_User::mxchat_get_client_ip());
63
64 $set('198.51.100.7', array('HTTP_CF_CONNECTING_IP' => '203.0.113.9'));
65 $check('a4 CF-Connecting-IP ignored when not from Cloudflare', MxChat_User::mxchat_get_client_ip() === '198.51.100.7', MxChat_User::mxchat_get_client_ip());
66
67 $set('173.245.48.10', array('HTTP_CF_CONNECTING_IP' => 'not-an-ip"onclick'));
68 $check('a5 invalid CF-Connecting-IP falls back to REMOTE_ADDR', MxChat_User::mxchat_get_client_ip() === '173.245.48.10', MxChat_User::mxchat_get_client_ip());
69
70 $set('198.51.100.7');
71 $junk = function () { return 'garbage'; };
72 add_filter('mxchat_client_ip', $junk);
73 $check('a6 filter returning junk is ignored', MxChat_User::mxchat_get_client_ip() === '198.51.100.7', MxChat_User::mxchat_get_client_ip());
74 remove_filter('mxchat_client_ip', $junk);
75 $own = function () { return '203.0.113.77'; };
76 add_filter('mxchat_client_ip', $own);
77 $check('a7 filter returning a valid address is used', MxChat_User::mxchat_get_client_ip() === '203.0.113.77', MxChat_User::mxchat_get_client_ip());
78 remove_filter('mxchat_client_ip', $own);
79
80 $set(null);
81 $check('a8 no REMOTE_ADDR at all -> unknown', MxChat_User::mxchat_get_client_ip() === 'unknown', MxChat_User::mxchat_get_client_ip());
82
83 $check('a9 cidr: 104.23.255.255 in 104.16.0.0/13', MxChat_User::mxchat_ip_in_cidr('104.23.255.255', '104.16.0.0/13'));
84 $check('a10 cidr: 104.28.0.0 not in 104.16.0.0/13', !MxChat_User::mxchat_ip_in_cidr('104.28.0.0', '104.16.0.0/13'));
85 $check('a11 cidr: 172.71.1.1 is Cloudflare, 172.72.0.0 is not', MxChat_User::mxchat_ip_is_cloudflare('172.71.1.1') && !MxChat_User::mxchat_ip_is_cloudflare('172.72.0.0'));
86 $check('a12 cidr: 2a06:98c0::/29 covers 2a06:98c7::1, not 2a06:98c8::1', MxChat_User::mxchat_ip_in_cidr('2a06:98c7::1', '2a06:98c0::/29') && !MxChat_User::mxchat_ip_in_cidr('2a06:98c8::1', '2a06:98c0::/29'));
87 $check('a13 cidr: family mismatch is false', !MxChat_User::mxchat_ip_in_cidr('2606:4700::1', '104.16.0.0/13'));
88
89 $_SERVER = $saved_server;
90
91 // ---------------------------------------------------------------- name parser
92 $p = $integrator->mxchat_parse_rate_counter_name('mxchat_chat_limit_default_logged_out_203_0_113_1');
93 $check('p1 parse logged_out visitor', $p && $p['bot'] === 'default' && $p['role'] === 'logged_out' && $p['identifier'] === '203_0_113_1', json_encode($p));
94 $p = $integrator->mxchat_parse_rate_counter_name('mxchat_chat_limit_default_author_42');
95 $check('p2 parse author 42', $p && $p['bot'] === 'default' && $p['role'] === 'author' && $p['identifier'] === '42', json_encode($p));
96 $p = $integrator->mxchat_parse_rate_counter_name('mxchat_chat_limit_default_global');
97 $check('p3 parse global pool', $p && $p['bot'] === 'default' && $p['role'] === 'global', json_encode($p));
98 $p = $integrator->mxchat_parse_rate_counter_name('mxchat_chat_limit_sales_bot_2_logged_out_2001_db8__1');
99 $check('p4 parse bot id with underscores + IPv6 identifier', $p && $p['bot'] === 'sales_bot_2' && $p['role'] === 'logged_out' && $p['identifier'] === '2001_db8__1', json_encode($p));
100 $p = $integrator->mxchat_parse_rate_counter_name('mxchat_chat_limit_default_subscriber_7');
101 $check('p5 parse subscriber 7', $p && $p['role'] === 'subscriber' && $p['identifier'] === '7', json_encode($p));
102 $p = $integrator->mxchat_parse_rate_counter_name('mxchat_chat_limit_junk');
103 $check('p6 unparseable name -> null', $p === null, json_encode($p));
104
105 // ---------------------------------------------------------------- (b) the reset
106 $opts_before = get_option('mxchat_options', array());
107 $opts = $opts_before;
108 $opts['rate_limits'] = array(
109 'logged_out' => array('limit' => '50', 'timeframe' => 'daily'),
110 'author' => array('limit' => '100', 'timeframe' => 'daily'),
111 );
112 $opts['rate_limits_global'] = array('limit' => '1000', 'timeframe' => 'daily');
113 update_option('mxchat_options', $opts);
114 wp_cache_flush();
115
116 $now = time();
117 $rows = array(
118 'mxchat_chat_limit_default_logged_out_203_0_113_1' => array('count' => 3, 'timestamp' => $now - 60),
119 'mxchat_chat_limit_default_logged_out_203_0_113_2' => array('count' => 3, 'timestamp' => $now - 25 * 3600),
120 'mxchat_chat_limit_default_author_42' => array('count' => 1, 'timestamp' => $now - 60),
121 'mxchat_chat_limit_default_global' => array('count' => 9, 'timestamp' => $now - 60),
122 'mxchat_chat_limit_default_editor_5' => array('count' => 1, 'timestamp' => $now - 60), // role not configured -> orphan
123 );
124 foreach ($rows as $name => $data) {
125 delete_option($name);
126 $call_private($integrator, 'mxchat_save_rate_counter', $name, $data);
127 }
128 $integrator->mxchat_reset_rate_limits();
129 wp_cache_flush();
130
131 $check('b1 fresh logged_out counter survives the reset', get_option('mxchat_chat_limit_default_logged_out_203_0_113_1') !== false);
132 $check('b2 25h-old logged_out counter is removed', get_option('mxchat_chat_limit_default_logged_out_203_0_113_2') === false);
133 $check('b3 fresh author counter survives', get_option('mxchat_chat_limit_default_author_42') !== false);
134 $check('b4 fresh global pool survives', get_option('mxchat_chat_limit_default_global') !== false);
135 $check('b5 counter for an unconfigured role is the only orphan removed', get_option('mxchat_chat_limit_default_editor_5') === false);
136
137 // ---------------------------------------------------------------- (c) autoload + cap
138 $autoload = $wpdb->get_var($wpdb->prepare(
139 "SELECT autoload FROM {$wpdb->options} WHERE option_name = %s",
140 'mxchat_chat_limit_default_logged_out_203_0_113_1'
141 ));
142 $check('c1 counters are stored with autoload off', in_array($autoload, array('no', 'off'), true), var_export($autoload, true));
143
144 foreach (array_keys($rows) as $name) {
145 delete_option($name);
146 }
147 $cap = function () { return 5; };
148 add_filter('mxchat_rate_limit_max_visitor_counters', $cap);
149 for ($i = 1; $i <= 5; $i++) {
150 $call_private($integrator, 'mxchat_save_rate_counter', 'mxchat_chat_limit_default_logged_out_10_0_0_' . $i, array('count' => 1, 'timestamp' => $now));
151 }
152 $call_private($integrator, 'mxchat_cap_visitor_counters', 'default', 'logged_out');
153 remove_filter('mxchat_rate_limit_max_visitor_counters', $cap);
154 $left = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE 'mxchat\\_chat\\_limit\\_default\\_logged\\_out\\_10\\_0\\_0\\_%'");
155 $oldest_gone = get_option('mxchat_chat_limit_default_logged_out_10_0_0_1') === false;
156 $newest_kept = get_option('mxchat_chat_limit_default_logged_out_10_0_0_5') !== false;
157 $check('c2 at the cap the oldest visitor counter is evicted, the newest kept', $left === 4 && $oldest_gone && $newest_kept, "left=$left");
158 for ($i = 1; $i <= 5; $i++) {
159 delete_option('mxchat_chat_limit_default_logged_out_10_0_0_' . $i);
160 }
161
162 // ---------------------------------------------------------------- restore
163 update_option('mxchat_options', $opts_before);
164 wp_cache_flush();
165 $check('z restore: mxchat_options back to the original', get_option('mxchat_options') === $opts_before);
166
167 // ---------------------------------------------------------------- report
168 $fail = 0;
169 foreach ($results as $r) {
170 if (!$r[1]) {
171 $fail++;
172 }
173 echo ($r[1] ? 'PASS ' : 'FAIL ') . $r[0] . ($r[1] || $r[2] === '' ? '' : " -> " . $r[2]) . "\n";
174 }
175 echo "\n" . (count($results) - $fail) . '/' . count($results) . ' passed' . ($fail ? ' — NOT releasable' : ' — releasable') . "\n";
176