12 | * setNamespace('MyApplication');
18 | * $storage = new DoctrineCacheAdapter($apc);
19 | *
20 | *
21 | * @since 0.1
22 | * @author Benedict Etzel
12 | *
18 | *
19 | * @since 0.1
20 | * @author Benedict Etzel
21 | */
22 | class PredisStorage implements StorageInterface
23 | {
24 | /**
25 | * @var Client
26 | */
27 | protected $client;
28 |
29 | /**
30 | * Sets up the class using the redis instance connected to the predis $client.
31 | * @param Predis\Client $client
32 | * @param array $options an array of options
33 | * @see BehEh\Flaps\PredisStorage::configure
34 | */
35 | public function __construct(Client $client, array $options = array())
36 | {
37 | $this->client = $client;
38 | $this->configure($options);
39 | }
40 |
41 | /**
42 | * @var array
43 | */
44 | protected $options;
45 |
46 | /**
47 | * Configures this class with some options:
48 | *
49 | * prefix the prefix to apply to all unique keys
50 | *
51 | * @param array $options the key value pairs of options for this class
52 | */
53 | public function configure(array $options)
54 | {
55 | $this->options = array_merge(array(
56 | 'prefix' => 'flaps:'
57 | ), $options);
58 | }
59 |
60 | private function prefixKey($key)
61 | {
62 | return $this->options['prefix'].$key;
63 | }
64 |
65 | private function prefixTimestamp($timestamp)
66 | {
67 | return $this->prefixKey($timestamp.':timestamp');
68 | }
69 |
70 | public function setValue($key, $value)
71 | {
72 | $this->client->set($this->prefixKey($key), intval($value));
73 | }
74 |
75 | public function incrementValue($key)
76 | {
77 | return intval($this->client->incr($this->prefixKey($key)));
78 | }
79 |
80 | public function getValue($key)
81 | {
82 | return intval($this->client->get($this->prefixKey($key)));
83 | }
84 |
85 | public function setTimestamp($key, $timestamp)
86 | {
87 | $this->client->set($this->prefixTimestamp($key), floatval($timestamp));
88 | }
89 |
90 | public function getTimestamp($key)
91 | {
92 | return floatval($this->client->get($this->prefixTimestamp($key)));
93 | }
94 |
95 | public function expire($key)
96 | {
97 | $this->client->del($this->prefixTimestamp($key));
98 | return (int) $this->client->del($this->prefixKey($key)) === 1;
99 | }
100 |
101 | public function expireIn($key, $seconds)
102 | {
103 | $redisTime = $this->client->time();
104 | $at = ceil($redisTime[0] + $seconds);
105 | $this->client->expireat($this->prefixTimestamp($key), $at);
106 | return (int) $this->client->expireat($this->prefixKey($key), $at) === 1;
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/Flaps/Flap.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | class Flap
13 | {
14 | /**
15 | * @var StorageInterface
16 | */
17 | protected $storage;
18 |
19 | /**
20 | * @var string
21 | */
22 | protected $name;
23 |
24 | /**
25 | *
26 | * @param StorageInterface $storage
27 | * @param string $name
28 | */
29 | public function __construct(StorageInterface $storage, $name)
30 | {
31 | $this->storage = $storage;
32 | $this->name = $name;
33 | }
34 |
35 | /**
36 | * @var ThrottlingStrategyInterface[]
37 | */
38 | protected $throttlingStrategies = array();
39 |
40 | /**
41 | * @var ViolationHandlerInterface
42 | */
43 | protected $violationHandler = null;
44 |
45 | /**
46 | * Adds the throttling strategy to the internal list of throttling strategies.
47 | * @param BehEh\Flaps\ViolationHandlerInterface
48 | */
49 | public function pushThrottlingStrategy(ThrottlingStrategyInterface $throttlingStrategy)
50 | {
51 | $throttlingStrategy->setStorage($this->storage);
52 | $this->throttlingStrategies[] = $throttlingStrategy;
53 | }
54 |
55 | /**
56 | * Sets the violation handler.
57 | * @param ViolationHandlerInterface $violationHandler the violation handler to use
58 | */
59 | public function setViolationHandler(ViolationHandlerInterface $violationHandler)
60 | {
61 | $this->violationHandler = $violationHandler;
62 | }
63 |
64 | /**
65 | * Returns the violation handler.
66 | * @return ViolationHandlerInterface the violation handler, if set
67 | */
68 | public function getViolationHandler()
69 | {
70 | return $this->violationHandler;
71 | }
72 |
73 | /**
74 | * Ensures a violation handler is set. If none is set, default to HttpViolationHandler.
75 | */
76 | protected function ensureViolationHandler()
77 | {
78 | if ($this->violationHandler === null) {
79 | $this->violationHandler = new HttpViolationHandler();
80 | }
81 | }
82 |
83 | /**
84 | * Requests violation handling from the violation handler if identifier violates any throttling strategy.
85 | * @param string $identifier
86 | * @return mixed true, if no throttling strategy is violated, otherwise the return value of the violation handler's handleViolation
87 | */
88 | public function limit($identifier)
89 | {
90 | if ($this->isViolator($identifier)) {
91 | $this->ensureViolationHandler();
92 | return $this->violationHandler->handleViolation();
93 | }
94 | return true;
95 | }
96 |
97 | /**
98 | * Checks whether the entity violates any throttling strategy.
99 | * @param string $identifier a unique string identifying the entity to check
100 | * @return bool true if the entity violates any strategy
101 | */
102 | public function isViolator($identifier)
103 | {
104 | $violation = false;
105 | foreach ($this->throttlingStrategies as $throttlingStrategy) {
106 | /** @var ThrottlingStrategyInterface $throttlingHandler */
107 | if ($throttlingStrategy->isViolator($this->name.':'.$identifier)) {
108 | $violation = true;
109 | }
110 | }
111 | return $violation;
112 | }
113 |
114 | }
115 |
--------------------------------------------------------------------------------
/src/Flaps/Throttling/LockStrategy.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | class LockStrategy implements ThrottlingStrategyInterface
16 | {
17 | /**
18 | * @var float
19 | */
20 | protected $timeSpan;
21 |
22 | /**
23 | * Sets the timespan in which the defined number of requests is allowed per single entity.
24 | * @param float|string $timeSpan
25 | * @throws InvalidArgumentException
26 | */
27 | public function setTimeSpan($timeSpan)
28 | {
29 | if (is_string($timeSpan)) {
30 | $timeSpan = self::parseTime($timeSpan);
31 | }
32 | if (!is_numeric($timeSpan)) {
33 | throw new InvalidArgumentException('timespan is not numeric');
34 | }
35 | $timeSpan = floatval($timeSpan);
36 | if ($timeSpan <= 0) {
37 | throw new InvalidArgumentException('timespan cannot be 0 or less');
38 | }
39 | $this->timeSpan = $timeSpan;
40 | }
41 |
42 | /**
43 | * Returns the previously set timespan.
44 | * @return float
45 | */
46 | public function getTimeSpan()
47 | {
48 | return (float) $this->timeSpan;
49 | }
50 |
51 | /**
52 | * Sets the lock for $lockDuration
53 | * @param int|string $lockDuration tither the amount of seconds or a string such as "10s", "5m" or "1h"
54 | * @throws InvalidArgumentException
55 | * @see LeakyBucketStrategy::setTimeSpan
56 | */
57 | public function __construct($lockDuration)
58 | {
59 | $this->setTimeSpan($lockDuration);
60 | }
61 |
62 | /**
63 | * @var StorageInterface
64 | */
65 | protected $storage;
66 |
67 | public function setStorage(StorageInterface $storage)
68 | {
69 | $this->storage = $storage;
70 | }
71 |
72 | /**
73 | * Parses a timespan string such as "10s", "5m" or "1h" and returns the amount of seconds.
74 | * @param string $timeSpan the time span to parse to seconds
75 | * @return float|null the number of seconds or null, if $timeSpan couldn't be parsed
76 | */
77 | public static function parseTime($timeSpan)
78 | {
79 | $times = array('s' => 1, 'm' => 60, 'h' => 3600, 'd' => 86400, 'w' => 604800);
80 | $matches = array();
81 | if (is_numeric($timeSpan)) {
82 | return $timeSpan;
83 | }
84 | if (preg_match('/^((\d+)?(\.\d+)?)('.implode('|', array_keys($times)).')$/',
85 | $timeSpan, $matches)) {
86 | return floatval($matches[1]) * $times[$matches[4]];
87 | }
88 | return null;
89 | }
90 |
91 | /**
92 | * Returns whether entity exceeds its allowed request capacity with this request.
93 | * @param string $identifier the identifer of the entity to check
94 | * @return bool true if this requests exceeds the number of requests allowed
95 | * @throws LogicException if no storage has been set
96 | */
97 | public function isViolator($identifier)
98 | {
99 | if ($this->storage === null) {
100 | throw new LogicException('no storage set');
101 | }
102 |
103 | $toCountOverflows = true;
104 |
105 | $time = microtime(true);
106 | $timestamp = $time;
107 | $toBlock = false;
108 |
109 | $timeSpan = $this->timeSpan;
110 | $isLockSet = $this->storage->getValue($identifier);
111 |
112 | if($isLockSet == 0) {
113 | // no lock is set yet
114 | // set the lock and return false
115 | $this->storage->setValue($identifier, 1);
116 | $this->storage->setTimestamp($identifier, $timestamp);
117 | $this->storage->expireIn($identifier, $this->timeSpan);
118 | return false;
119 | }
120 | return true;
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/tests/Flaps/Storage/PredisStorageTest.php:
--------------------------------------------------------------------------------
1 | client = new Client();
23 | $this->storage = new PredisStorage($this->client, array('prefix' => ''));
24 |
25 | $this->client->del('key');
26 | $this->client->del('key:timestamp');
27 | }
28 |
29 | /**
30 | * @covers BehEh\Flaps\Storage\PredisStorage::setValue
31 | * @covers BehEh\Flaps\Storage\PredisStorage::incrementValue
32 | * @covers BehEh\Flaps\Storage\PredisStorage::getValue
33 | * @covers BehEh\Flaps\Storage\PredisStorage::expire
34 | */
35 | public function testValue()
36 | {
37 | $this->assertEquals(0, $this->client->exists('key'));
38 | $this->assertSame(0, $this->storage->getValue('key'));
39 |
40 | $this->storage->setValue('key', 1);
41 | $this->assertEquals(1, $this->client->exists('key'));
42 | $this->assertSame(1, $this->storage->getValue('key'));
43 |
44 | $this->storage->incrementValue('key');
45 | $this->assertSame(2, $this->storage->getValue('key'));
46 |
47 | $this->storage->setValue('key', 5);
48 | $this->assertSame(5, $this->storage->getValue('key'));
49 |
50 | $this->storage->expire('key');
51 | $this->assertEquals(0, $this->client->exists('key'));
52 | }
53 |
54 | /**
55 | * @covers BehEh\Flaps\Storage\PredisStorage::setTimestamp
56 | * @covers BehEh\Flaps\Storage\PredisStorage::getTimestamp
57 | * @covers BehEh\Flaps\Storage\PredisStorage::expire
58 | */
59 | public function testTimestamp()
60 | {
61 | $this->assertEquals(0, $this->client->exists('key'));
62 | $this->assertSame(0.0, $this->storage->getTimestamp('key'));
63 |
64 | $this->storage->setTimestamp('key', 1425829426.0);
65 | $this->assertSame(1425829426.0, $this->storage->getTimestamp('key'));
66 |
67 | $this->storage->expire('key');
68 | $this->assertEquals(0, $this->client->exists('key:timestamp'));
69 | }
70 |
71 | /**
72 | * @covers BehEh\Flaps\Storage\PredisStorage::expire
73 | */
74 | public function testExpire()
75 | {
76 | $this->assertEquals(0, $this->client->exists('key'));
77 | $this->assertEquals(0, $this->client->exists('key:timestamp'));
78 |
79 | $this->storage->setValue('key', 1);
80 | $this->storage->setTimestamp('key', 1425829426.0);
81 |
82 | $this->assertEquals(1, $this->client->exists('key'));
83 | $this->assertEquals(1, $this->client->exists('key:timestamp'));
84 |
85 | $this->assertEquals(1, $this->storage->expire('key'));
86 |
87 | $this->assertEquals(0, $this->client->exists('key'));
88 | $this->assertEquals(0, $this->client->exists('key:timestamp'));
89 |
90 | $this->assertEquals(0, $this->storage->expire('key'));
91 | }
92 |
93 | /**
94 | * @covers BehEh\Flaps\Storage\PredisStorage::expireIn
95 | */
96 | public function testExpireIn()
97 | {
98 | $this->assertEquals(0, $this->client->exists('key'));
99 | $this->assertEquals(0, $this->client->exists('key:timestamp'));
100 |
101 | $this->storage->setValue('key', 1);
102 | $this->storage->setTimestamp('key', 1425829426.0);
103 |
104 | $this->assertEquals(1, $this->client->exists('key'));
105 | $this->assertEquals(1, $this->client->exists('key:timestamp'));
106 |
107 | $this->assertEquals(1, $this->storage->expireIn('key', 0));
108 |
109 | $this->assertEquals(0, $this->client->exists('key'));
110 | $this->assertEquals(0, $this->client->exists('key:timestamp'));
111 |
112 | $this->assertEquals(0, $this->storage->expireIn('key', 0));
113 | }
114 |
115 | protected function tearDown()
116 | {
117 | $this->client->del('key');
118 | $this->client->del('key:timestamp');
119 | }
120 |
121 | }
122 |
--------------------------------------------------------------------------------
/src/Flaps/Throttling/LeakyBucketStrategy.php:
--------------------------------------------------------------------------------
1 |
17 | */
18 | class LeakyBucketStrategy implements ThrottlingStrategyInterface
19 | {
20 | /**
21 | * @var int
22 | */
23 | protected $requestsPerTimeSpan;
24 |
25 | /**
26 | * Sets the maximum number of requests allowed per timespan for a single entity.
27 | * @param int $requests
28 | * @throws \InvalidArgumentException
29 | */
30 | public function setRequestsPerTimeSpan($requests)
31 | {
32 | if (!is_numeric($requests)) {
33 | throw new InvalidArgumentException('requests per timespan is not numeric');
34 | }
35 | $requests = (int) $requests;
36 | if ($requests < 1) {
37 | throw new InvalidArgumentException('requests per timespan cannot be smaller than 1');
38 | }
39 | $this->requestsPerTimeSpan = $requests;
40 | }
41 |
42 | /**
43 | * Returns the previously set number of requests per timespan.
44 | * @return int
45 | */
46 | public function getRequestsPerTimeSpan()
47 | {
48 | return $this->requestsPerTimeSpan;
49 | }
50 |
51 | /**
52 | * @var float
53 | */
54 | protected $timeSpan;
55 |
56 | /**
57 | * Sets the timespan in which the defined number of requests is allowed per single entity.
58 | * @param float|string $timeSpan
59 | * @throws InvalidArgumentException
60 | */
61 | public function setTimeSpan($timeSpan)
62 | {
63 | if (is_string($timeSpan)) {
64 | $timeSpan = self::parseTime($timeSpan);
65 | }
66 | if (!is_numeric($timeSpan)) {
67 | throw new InvalidArgumentException('timespan is not numeric');
68 | }
69 | $timeSpan = floatval($timeSpan);
70 | if ($timeSpan <= 0) {
71 | throw new InvalidArgumentException('timespan cannot be 0 or less');
72 | }
73 | $this->timeSpan = $timeSpan;
74 | }
75 |
76 | /**
77 | * Returns the previously set timespan.
78 | * @return float
79 | */
80 | public function getTimeSpan()
81 | {
82 | return (float) $this->timeSpan;
83 | }
84 |
85 | /**
86 | * Sets the strategy up with $requests allowed per $timeSpan
87 | * @param int $requests the requests allowed per time span
88 | * @param int|string $timeSpan tither the amount of seconds or a string such as "10s", "5m" or "1h"
89 | * @throws InvalidArgumentException
90 | * @see LeakyBucketStrategy::setRequestsPerTimeSpan
91 | * @see LeakyBucketStrategy::setTimeSpan
92 | */
93 | public function __construct($requests, $timeSpan)
94 | {
95 | $this->setRequestsPerTimeSpan($requests);
96 | $this->setTimeSpan($timeSpan);
97 | }
98 |
99 | /**
100 | * @var StorageInterface
101 | */
102 | protected $storage;
103 |
104 | public function setStorage(StorageInterface $storage)
105 | {
106 | $this->storage = $storage;
107 | }
108 |
109 | /**
110 | * Parses a timespan string such as "10s", "5m" or "1h" and returns the amount of seconds.
111 | * @param string $timeSpan the time span to parse to seconds
112 | * @return float|null the number of seconds or null, if $timeSpan couldn't be parsed
113 | */
114 | public static function parseTime($timeSpan)
115 | {
116 | $times = array('s' => 1, 'm' => 60, 'h' => 3600, 'd' => 86400, 'w' => 604800);
117 | $matches = array();
118 | if (is_numeric($timeSpan)) {
119 | return $timeSpan;
120 | }
121 | if (preg_match('/^((\d+)?(\.\d+)?)('.implode('|', array_keys($times)).')$/',
122 | $timeSpan, $matches)) {
123 | return floatval($matches[1]) * $times[$matches[4]];
124 | }
125 | return null;
126 | }
127 |
128 | /**
129 | * Returns whether entity exceeds its allowed request capacity with this request.
130 | * @param string $identifier the identifer of the entity to check
131 | * @return bool true if this requests exceeds the number of requests allowed
132 | * @throws LogicException if no storage has been set
133 | */
134 | public function isViolator($identifier)
135 | {
136 | if ($this->storage === null) {
137 | throw new LogicException('no storage set');
138 | }
139 |
140 | $toCountOverflows = true;
141 |
142 | $time = microtime(true);
143 | $timestamp = $time;
144 | $toBlock = false;
145 |
146 | $rate = (float) $this->requestsPerTimeSpan / $this->timeSpan;
147 | $identifier = 'leaky:'.sha1($rate.$identifier);
148 |
149 | $requestCount = $this->storage->getValue($identifier);
150 |
151 | $oldTimestamp = $this->storage->getTimestamp($identifier);
152 | if ($requestCount === 0) {
153 | $oldTimestamp = $time;
154 | }
155 |
156 | $secondsSince = $time - $oldTimestamp;
157 | $reduceBy = floor($this->requestsPerTimeSpan * ($secondsSince / $this->timeSpan));
158 | $requestCount = max($requestCount - $reduceBy, 0);
159 |
160 | if ($requestCount + 1 > $this->requestsPerTimeSpan) {
161 | $toBlock = true;
162 | if ($toCountOverflows) {
163 | return $toBlock;
164 | }
165 | }
166 |
167 | $requestCount++;
168 |
169 | $this->storage->setValue($identifier, $requestCount);
170 | $this->storage->setTimestamp($identifier, $timestamp);
171 | $this->storage->expireIn($identifier, $this->timeSpan - $secondsSince);
172 |
173 | return $toBlock;
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/tests/Flaps/Throttling/LeakyBucketStrategyTest.php:
--------------------------------------------------------------------------------
1 | strategy = new LeakyBucketStrategy(1, 1);
15 | }
16 |
17 | /**
18 | * @covers BehEh\Flaps\Throttling\LeakyBucketStrategy::setTimeSpan
19 | * @covers BehEh\Flaps\Throttling\LeakyBucketStrategy::getTimeSpan
20 | */
21 | public function testSetTimeSpan()
22 | {
23 | $this->strategy->setTimeSpan(1);
24 | $this->assertEquals(1, $this->strategy->getTimeSpan());
25 | $this->strategy->setTimeSpan(2);
26 | $this->assertEquals(2, $this->strategy->getTimeSpan());
27 | $this->strategy->setTimeSpan(2.5);
28 | $this->assertEquals(2.5, $this->strategy->getTimeSpan());
29 | $this->strategy->setTimeSpan(2.1);
30 | $this->assertEquals(2.1, $this->strategy->getTimeSpan());
31 | $this->strategy->setTimeSpan(2.9);
32 | $this->assertEquals(2.9, $this->strategy->getTimeSpan());
33 | $this->strategy->setTimeSpan('1m');
34 | $this->assertEquals(60, $this->strategy->getTimeSpan());
35 | }
36 |
37 | /**
38 | * @covers BehEh\Flaps\Throttling\LeakyBucketStrategy::setTimeSpan
39 | * @expectedException InvalidArgumentException
40 | */
41 | public function testSetTimeSpanWithZero()
42 | {
43 | $this->strategy->setTimeSpan(0);
44 | }
45 |
46 | /**
47 | * @covers BehEh\Flaps\Throttling\LeakyBucketStrategy::setTimeSpan
48 | * @expectedException InvalidArgumentException
49 | */
50 | public function testSetTimeSpanWithNegative()
51 | {
52 | $this->strategy->setTimeSpan(-1);
53 | }
54 |
55 | /**
56 | * @covers BehEh\Flaps\Throttling\LeakyBucketStrategy::setTimeSpan
57 | * @expectedException InvalidArgumentException
58 | */
59 | public function testSetTimeSpanWithArray()
60 | {
61 | $this->strategy->setTimeSpan(array());
62 | }
63 |
64 | /**
65 | * @covers BehEh\Flaps\Throttling\LeakyBucketStrategy::setRequestsPerTimeSpan
66 | * @covers BehEh\Flaps\Throttling\LeakyBucketStrategy::getRequestsPerTimeSpan
67 | */
68 | public function testSetRequestsPerTimeSpan()
69 | {
70 | $this->strategy->setRequestsPerTimeSpan(1);
71 | $this->assertEquals(1, $this->strategy->getRequestsPerTimeSpan());
72 | $this->strategy->setRequestsPerTimeSpan(2);
73 | $this->assertEquals(2, $this->strategy->getRequestsPerTimeSpan());
74 | $this->strategy->setRequestsPerTimeSpan(2.5);
75 | $this->assertEquals(2, $this->strategy->getRequestsPerTimeSpan());
76 | $this->strategy->setRequestsPerTimeSpan(2.1);
77 | $this->assertEquals(2, $this->strategy->getRequestsPerTimeSpan());
78 | $this->strategy->setRequestsPerTimeSpan(2.9);
79 | $this->assertEquals(2, $this->strategy->getRequestsPerTimeSpan());
80 | }
81 |
82 | /**
83 | * @covers BehEh\Flaps\Throttling\LeakyBucketStrategy::setRequestsPerTimeSpan
84 | * @expectedException InvalidArgumentException
85 | */
86 | public function testSetRequestsPerTimeSpanWithZero()
87 | {
88 | $this->strategy->setRequestsPerTimeSpan(0);
89 | }
90 |
91 | /**
92 | * @covers BehEh\Flaps\Throttling\LeakyBucketStrategy::setRequestsPerTimeSpan
93 | * @expectedException InvalidArgumentException
94 | */
95 | public function testSetRequestsPerTimeSpanWithNegative()
96 | {
97 | $this->strategy->setRequestsPerTimeSpan(-1);
98 | }
99 |
100 | /**
101 | * @covers BehEh\Flaps\Throttling\LeakyBucketStrategy::setRequestsPerTimeSpan
102 | * @expectedException InvalidArgumentException
103 | */
104 | public function testSetRequestsPerTimeSpanWithArray()
105 | {
106 | $this->strategy->setRequestsPerTimeSpan(array());
107 | }
108 |
109 | /**
110 | * @covers BehEh\Flaps\Throttling\LeakyBucketStrategy::parseTime
111 | */
112 | public function testParseTime()
113 | {
114 | $this->assertEquals(0, LeakyBucketStrategy::parseTime(0));
115 | $this->assertEquals(0, LeakyBucketStrategy::parseTime('0'));
116 | $this->assertEquals(0, LeakyBucketStrategy::parseTime('0s'));
117 | $this->assertEquals(1, LeakyBucketStrategy::parseTime(1));
118 | $this->assertEquals(1, LeakyBucketStrategy::parseTime('1'));
119 | $this->assertEquals(1, LeakyBucketStrategy::parseTime('1s'));
120 | $this->assertEquals(2, LeakyBucketStrategy::parseTime('2s'));
121 | $this->assertEquals(60, LeakyBucketStrategy::parseTime('1m'));
122 | $this->assertEquals(90, LeakyBucketStrategy::parseTime('1.5m'));
123 | $this->assertNull(LeakyBucketStrategy::parseTime('invalid'));
124 | $this->assertNull(LeakyBucketStrategy::parseTime('1 m'));
125 | $this->assertNull(LeakyBucketStrategy::parseTime('1k'));
126 | }
127 |
128 | /**
129 | * @covers BehEh\Flaps\Throttling\LeakyBucketStrategy::isViolator
130 | * @expectedException LogicException
131 | */
132 | public function testIsViolatorWithoutStorage()
133 | {
134 | $instance = new LeakyBucketStrategy(1, '1s');
135 | $instance->isViolator('BehEh');
136 | }
137 |
138 | /**
139 | * @covers BehEh\Flaps\Throttling\LeakyBucketStrategy::isViolator
140 | * @expectedException LogicException
141 | */
142 | public function testIsViolatorWithZeroRate()
143 | {
144 | $instance = new LeakyBucketStrategy(0, 0);
145 | $instance->setStorage(new MockStorage);
146 | $instance->isViolator('BehEh');
147 | }
148 |
149 | /**
150 | * @covers BehEh\Flaps\Throttling\LeakyBucketStrategy::isViolator
151 | */
152 | public function testIsViolator()
153 | {
154 | $instance = new LeakyBucketStrategy(1, '1s');
155 | $instance->setStorage(new MockStorage);
156 | $this->assertFalse($instance->isViolator('BehEh'));
157 | $this->assertTrue($instance->isViolator('BehEh'));
158 | usleep(500 * 1000);
159 | $this->assertTrue($instance->isViolator('BehEh'));
160 | usleep(500 * 1000);
161 | $this->assertFalse($instance->isViolator('BehEh'));
162 | }
163 |
164 | }
165 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Flaps
2 |
3 | [](https://travis-ci.org/beheh/flaps)
4 | [](https://scrutinizer-ci.com/g/beheh/flaps/?branch=master)
5 | [](https://scrutinizer-ci.com/g/beheh/flaps/?branch=master)
6 | [](https://packagist.org/packages/beheh/flaps)
7 | [](https://packagist.org/packages/beheh/flaps)
8 |
9 | Flaps is a modular library for rate limiting requests in PHP applications.
10 |
11 | The library supports custom storage backends, throttling strategies and violation handlers for flexible integration into any project.
12 |
13 | Developed by [@beheh](https://github.com/beheh) and licensed under the ISC license.
14 |
15 | ## Requirements
16 |
17 | - PHP 5.4 or newer
18 | - Persistent-ish storage (e.g. Redis, APC or anything supported by _[Doctrine\Cache](http://doctrine-common.readthedocs.org/en/latest/reference/caching.html)_)
19 | - Composer
20 |
21 | ## Basic usage
22 |
23 | ```php
24 | use Predis\Client;
25 | use BehEh\Flaps\Storage\PredisStorage;
26 | use BehEh\Flaps\Flaps;
27 | use BehEh\Flaps\Throttling\LeakyBucketStrategy;
28 |
29 | // setup with Redis as storage backend
30 | $storage = new PredisStorage(new Client());
31 | $flaps = new Flaps($storage);
32 |
33 | // allow 3 requests per 5 seconds
34 | $flaps->login->pushThrottlingStrategy(new LeakyBucketStrategy(3, '5s'));
35 | //or $flaps->__get('login')->pushThrottlingStrategy(...)
36 |
37 | // limit by ip (default: send "HTTP/1.1 429 Too Many Requests" and die() on violation)
38 | $flaps->login->limit($_SERVER['REMOTE_ADDR']);
39 | ```
40 |
41 | ## Why rate limit?
42 |
43 | There are many benefits from rate limiting your web application. At any point in time your server(s) could be hit by a huge number of requests from one or many clients. These could be:
44 | - Malicious clients trying to degrade your applications performance
45 | - Malicious clients bruteforcing user credentials
46 | - Bugged clients repeating requests over and over again
47 | - Automated web crawlers enumerating usernames or email adresses
48 | - Penetration frameworks testing for vulnerabilities
49 | - Bots registering a large number of users
50 | - Bots spamming links to malicious sites
51 |
52 | Most of these problems can be solved in a variety of ways, for example by using a spam filter or a fully configured firewall. Rate limiting is nevertheless a basic tool for improving application security, but offers no full protection.
53 |
54 | ## Advanced examples
55 |
56 | ### Application-handled violation
57 |
58 | ```php
59 | use BehEh\Flaps\Throttling\LeakyBucketStrategy;
60 | use BehEh\Flaps\Violation\PassiveViolationHandler;
61 |
62 | $flap = $flaps->__get('api');
63 | $flap->pushThrottlingStrategy(new LeakyBucketStrategy(15, '10s'));
64 | $flap->setViolationHandler(new PassiveViolationHandler);
65 | if (!$flap->limit(filter_var(INPUT_GET, 'api_key'))) {
66 | die(json_encode(array('error' => 'too many requests')));
67 | }
68 | ```
69 |
70 | ### Multiple throttling strategies
71 |
72 | ```php
73 | use BehEh\Flaps\Throttling\LeakyBucketStrategy;
74 |
75 | $flap = $flaps->__get('add_comment');
76 | $flap->pushThrottlingStrategy(new LeakyBucketStrategy(1, '30s'));
77 | $flap->pushThrottlingStrategy(new LeakyBucketStrategy(10, '10m'));
78 | $flap->limit($userid);
79 | ```
80 |
81 | ## Storage
82 |
83 | ### Redis
84 |
85 | The easiest storage system to get started is Redis (via [nrk/predis](https://github.com/nrk/predis)):
86 |
87 | ```php
88 | use Predis\Client;
89 | use BehEh\Flaps\Storage\PredisStorage;
90 | use BehEh\Flaps\Flaps;
91 |
92 | $storage = new PredisStorage(new Client('tcp://10.0.0.1:6379'));
93 | $flaps = new Flaps($storage);
94 | ```
95 |
96 | Don't forget to `composer require predis/predis`.
97 |
98 | ### Doctrine cache
99 |
100 | You can use any of the [Doctrine cache implementations](http://doctrine-common.readthedocs.org/en/latest/reference/caching.html) by using the _DoctrineCacheAdapter_:
101 |
102 | ```php
103 | use Doctrine\Common\Cache\ApcCache;
104 | use BehEh\Flaps\Storage\DoctrineCacheAdapter;
105 | use BehEh\Flaps\Flaps;
106 |
107 | $apc = new ApcCache();
108 | $apc->setNamespace('MyApplication');
109 | $storage = new DoctrineCacheAdapter($apc);
110 | $flaps = new Flaps($storage);
111 | ```
112 |
113 | The Doctrine caching implementations can be installed with `composer require doctrine/cache`.
114 |
115 | ### Custom storage
116 |
117 | Alternatively you can use your own storage system by implementing _BehEh\Flaps\StorageInterface_.
118 |
119 | ## Throttling strategies
120 |
121 | ### Leaky bucket strategy
122 |
123 | This strategy is based on the leaky bucket algorithm. Each unique identifier of a flap corresponds to a leaky bucket.
124 | Clients can now access the buckets as much as they like, inserting water for every request. If a request would cause the bucket to overflow, it is denied.
125 | In order to allow later requests, the bucket leaks at a fixed rate.
126 |
127 | ```php
128 | use BehEh\Flaps\Throttle\LeakyBucketStrategy;
129 |
130 | $flap->pushThrottlingStrategy(new LeakyBucketStrategy(60, '10m'));
131 | ```
132 |
133 | ### Custom throttling strategy
134 |
135 | Once again, you can supply your own throttling strategy by implementing _BehEh\Flaps\ThrottlingStrategyInterface_.
136 |
137 | ## Violation handler
138 |
139 | You can handle violations either using one of the included handlers or by writing your own.
140 |
141 | ## HTTP violation handler
142 |
143 | The HTTP violation handler is the most basic violation handler, recommended for simple scripts.
144 | It simply sends the correct HTTP header (status code 429) and die()s. This is not recommended for any larger application and should be replaced by one of the more customizable handlers.
145 |
146 | ```php
147 | use BehEh\Flaps\Violation\HttpViolationHandler;
148 |
149 | $flap->setViolationHandler(new HttpViolationHandler);
150 | $flap->limit($identifier); // send "HTTP/1.1 429 Too Many Requests" and die() on violation
151 | ```
152 |
153 | ## Passive violation handler
154 |
155 | The passive violation handler allows you to easily react to violations.
156 | `limit()` will return false if the requests violates any throttling strategy, so you are able to log the request or return a custom error page.
157 |
158 | ```php
159 | use BehEh\Flaps\Violation\PassiveViolationHandler;
160 |
161 | $flap->setViolationHandler(new PassiveViolationHandler);
162 | if (!$flap->limit($identifier)) {
163 | // violation
164 | }
165 | ```
166 |
167 | ### Exception violation handler
168 |
169 | The exception violation handler can be used in larger frameworks. It will throw a _ThrottlingViolationException_ whenever a _ThrottlingStrategy_ is violated.
170 | You should be able to setup your exception handler to catch any _ThrottlingViolationException_.
171 |
172 | ```php
173 | use BehEh\Flaps\Violation\ExceptionViolationHandler;
174 | use BehEh\Flaps\Violation\ThrottlingViolationException;
175 |
176 | $flap->setViolationHandler(new ExceptionViolationHandler);
177 | try {
178 | $flap->limit($identifier); // throws ThrottlingViolationException on violation
179 | }
180 | catch (ThrottlingViolationException $e) {
181 | // violation
182 | }
183 | ```
184 |
185 | ### Custom violation handler
186 |
187 | The corresponding interface for custom violation handlers is _BehEh\Flaps\ViolationHandlerInterface_.
188 |
189 | ### Default violation handler
190 |
191 | The `Flaps` object can pass a default violation handler to the flaps.
192 |
193 | ```php
194 | $flaps->setDefaultViolationHandler(new CustomViolationHandler);
195 |
196 | $flap = $flaps->__get('login');
197 | $flap->addThrottlingStrategy(new TimeBasedThrottlingStrategy(1, '1s'));
198 | $flap->limit($identifier); // will use CustomViolationHandler
199 | ```
200 |
--------------------------------------------------------------------------------