1: <?php
2: /*
3: * SimpleID
4: *
5: * Copyright (C) Kelvin Mo 2014-2025
6: *
7: * This program is free software; you can redistribute it and/or
8: * modify it under the terms of the GNU General Public
9: * License as published by the Free Software Foundation; either
10: * version 2 of the License, or (at your option) any later version.
11: *
12: * This program is distributed in the hope that it will be useful,
13: * but WITHOUT ANY WARRANTY; without even the implied warranty of
14: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15: * General Public License for more details.
16: *
17: * You should have received a copy of the GNU General Public
18: * License along with this program; if not, write to the Free
19: * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20: */
21:
22: namespace SimpleID\Util;
23:
24: /**
25: * A rate limiter.
26: *
27: * A rate limiter limits the number of requests from a particular source
28: * to a particular limit over a defined period. Once the limit is reached,
29: * the server refuses to process any more requests until the defined period
30: * is rolled over.
31: *
32: * This rate limiter uses the FatFree Framework's cache to store rate
33: * limit information.
34: */
35: class RateLimiter {
36: /** @var string the identifier of the rate limited */
37: protected $key;
38:
39: /** @var int the maximum number of requests from a particular
40: * source over the specified period
41: */
42: protected $limit;
43:
44: /** @var int $interval the specified period in seconds */
45: protected $interval;
46:
47: /**
48: * Creates a rate limiter.
49: *
50: * @param string $key a string to identify the rate limiter
51: * @param int $limit the maximum number of requests from a particular
52: * source over the specified period
53: * @param int $interval the specified period in seconds
54: */
55: public function __construct($key, $limit = 10, $interval = 10) {
56: $this->limit = $limit;
57: $this->interval = $interval;
58: $this->key = $key;
59: }
60:
61: /**
62: * Increments the rate limiter with a new request from a particular
63: * source.
64: *
65: * The source is specified in the $src parameter. If $src is not
66: * specified, the IP address is used.
67: *
68: * The rate limiter is incremented by the amount specified by the
69: * $weight parameter.
70: *
71: * This function returns a boolean indicating whether the request
72: * should continue to be processed (true), or should be refused (false).
73: *
74: * @param string $src the source
75: * @param int $weight
76: * @return bool
77: */
78: public function throttle($src = null, $weight = 1) {
79: if ($src == null) $src = $this->getDefaultSource();
80:
81: $cache = \Cache::instance();
82: $cache_name = $this->getCacheName($src);
83: $entry = $cache->get($cache_name);
84: if (($entry === false) || (time() >= $entry['expires'])) $entry = ['count' => 0, 'expires' => time() + $this->interval ];
85: $entry['count'] += $weight;
86: if ($entry['count'] > $this->limit) { return false;}
87: $cache->set($cache_name, $entry, $this->interval);
88: return true;
89: }
90:
91: /**
92: * Returns the number of requests remaining for the source before the
93: * limit is reached.
94: *
95: * The source is specified in the $src parameter. If $src is not
96: * specified, the IP address is used.
97: *
98: * @param string $src the source
99: * @return int
100: */
101: public function remainder($src = null) {
102: if ($src == null) $src = $this->getDefaultSource();
103:
104: $cache = \Cache::instance();
105: $cache_name = $this->getCacheName($src);
106: $entry = $cache->get($cache_name);
107: if ($entry === false) return $this->limit;
108: return intval($this->limit - $entry['count']);
109: }
110:
111: /**
112: * Penalises the source by refusing further requests until the
113: * period rolls over.
114: *
115: * The source is specified in the $src parameter. If $src is not
116: * specified, the IP address is used.
117: *
118: * @param string $src the source
119: * @return void
120: */
121: public function penalize($src = null) {
122: if ($src == null) $src = $this->getDefaultSource();
123:
124: $cache = \Cache::instance();
125: $cache_name = $this->getCacheName($src);
126: $entry = $cache->get($cache_name);
127: if ($entry === false) {
128: $entry = ['count' => $this->limit, 'expires' => time() + $this->interval ];
129: } else {
130: $entry['count'] = $this->limit;
131: };
132: $cache->set($cache_name, $entry, $this->interval);
133: }
134:
135: /**
136: * Resets the rate limiter for a particular source. This allows
137: * the source to make further requests, subject to the limits
138: * defined by this rate limiter.
139: *
140: * The source is specified in the $src parameter. If $src is not
141: * specified, the IP address is used.
142: *
143: * @param string $src the source
144: * @return void
145: */
146: public function reset($src = null) {
147: if ($src == null) $src = $this->getDefaultSource();
148:
149: $cache = \Cache::instance();
150: $cache_name = $this->getCacheName($src);
151: $cache->reset($cache_name);
152: }
153:
154: /**
155: * Resets the rate limiter for all sources.
156: *
157: * @return void
158: */
159: public function resetAll() {
160: $this->reset('');
161: }
162:
163: /**
164: * Returns the key
165: *
166: * @return string the key
167: */
168: public function getKey() {
169: return $this->key;
170: }
171:
172: /**
173: * Returns the limit.
174: *
175: * @return int the limit
176: */
177: public function getLimit() {
178: return $this->limit;
179: }
180:
181: /**
182: * Returns the interval
183: *
184: * @return int the interval in seconds
185: */
186: public function getInterval() {
187: return $this->interval;
188: }
189:
190: /**
191: * Returns the IP address using the default source for the rate limiter.
192: *
193: * The default source is the {@link https://fatfreeframework.com/3.8/quick-reference#IP IP}
194: * hive variable from the FatFree Framework, which resolves reverse proxies
195: * using the `Client-IP` and the `X-Forwarded-For` headers.
196: *
197: * @return string the IP address
198: */
199: protected function getDefaultSource() {
200: $f3 = \Base::instance();
201: return $f3->get('IP');
202: }
203:
204: /**
205: * Returns the name of the cache for the rate limiter in
206: * relation to a specified source.
207: *
208: * @param string $src the source
209: * @return string the cache name
210: */
211: protected function getCacheName($src) {
212: return rawurlencode($src) . '.' . rawurlencode($this->key) . '.ratelimit';
213: }
214: }
215:
216: ?>
217: