Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 66 |
|
0.00% |
0 / 4 |
CRAP | |
0.00% |
0 / 1 |
RateLimiterManagerFactory | |
0.00% |
0 / 66 |
|
0.00% |
0 / 4 |
132 | |
0.00% |
0 / 1 |
__invoke | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
6 | |||
getRateLimiter | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
createCache | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
20 | |||
createRedisCache | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | /** |
4 | * Rate limiter manager factory. |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) The National Library of Finland 2024. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package Service |
25 | * @author Ere Maijala <ere.maijala@helsinki.fi> |
26 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
27 | * @link https://vufind.org/wiki/development Wiki |
28 | */ |
29 | |
30 | namespace VuFind\RateLimiter; |
31 | |
32 | use Closure; |
33 | use Laminas\Cache\Psr\CacheItemPool\CacheItemPoolDecorator; |
34 | use Laminas\Cache\Storage\Capabilities; |
35 | use Laminas\ServiceManager\Exception\ServiceNotCreatedException; |
36 | use Laminas\ServiceManager\Exception\ServiceNotFoundException; |
37 | use Laminas\ServiceManager\Factory\FactoryInterface; |
38 | use Psr\Container\ContainerExceptionInterface as ContainerException; |
39 | use Psr\Container\ContainerInterface; |
40 | use stdClass; |
41 | use Symfony\Component\RateLimiter\LimiterInterface; |
42 | use Symfony\Component\RateLimiter\RateLimiterFactory; |
43 | use Symfony\Component\RateLimiter\Storage\CacheStorage; |
44 | use Symfony\Component\RateLimiter\Storage\StorageInterface; |
45 | use VuFind\RateLimiter\Storage\CredisStorage; |
46 | |
47 | /** |
48 | * Rate limiter manager factory. |
49 | * |
50 | * @category VuFind |
51 | * @package Service |
52 | * @author Ere Maijala <ere.maijala@helsinki.fi> |
53 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
54 | * @link https://vufind.org/wiki/development Wiki |
55 | */ |
56 | class RateLimiterManagerFactory implements FactoryInterface |
57 | { |
58 | /** |
59 | * Service locator |
60 | * |
61 | * @var ContainerInterface |
62 | */ |
63 | protected $serviceLocator; |
64 | |
65 | /** |
66 | * Create an object |
67 | * |
68 | * @param ContainerInterface $container Service manager |
69 | * @param string $requestedName Service being created |
70 | * @param null|array $options Extra options (optional) |
71 | * |
72 | * @return object |
73 | * |
74 | * @throws ServiceNotFoundException if unable to resolve the service. |
75 | * @throws ServiceNotCreatedException if an exception is raised when |
76 | * creating a service. |
77 | * @throws ContainerException&\Throwable if any other error occurs |
78 | */ |
79 | public function __invoke( |
80 | ContainerInterface $container, |
81 | $requestedName, |
82 | array $options = null |
83 | ) { |
84 | if (!empty($options)) { |
85 | throw new \Exception('Unexpected options sent to factory.'); |
86 | } |
87 | |
88 | $this->serviceLocator = $container; |
89 | |
90 | $yamlReader = $container->get(\VuFind\Config\YamlReader::class); |
91 | $config = $yamlReader->get('RateLimiter.yaml'); |
92 | |
93 | $authManager = $container->get(\VuFind\Auth\Manager::class); |
94 | $request = $container->get('Request'); |
95 | |
96 | return new $requestedName( |
97 | $config, |
98 | $request->getServer('REMOTE_ADDR'), |
99 | $authManager->getUserObject()?->getId(), |
100 | Closure::fromCallable([$this, 'getRateLimiter']), |
101 | $container->get(\VuFind\Net\IpAddressUtils::class) |
102 | ); |
103 | } |
104 | |
105 | /** |
106 | * Get rate limiter |
107 | * |
108 | * @param array $config Rate limiter configuration |
109 | * @param string $policyId Policy ID |
110 | * @param string $clientIp Client's IP address |
111 | * @param ?string $userId User ID or null if not logged in |
112 | * |
113 | * @return LimiterInterface |
114 | */ |
115 | protected function getRateLimiter( |
116 | array $config, |
117 | string $policyId, |
118 | string $clientIp, |
119 | ?string $userId |
120 | ): LimiterInterface { |
121 | $policy = $config['Policies'][$policyId] ?? []; |
122 | $rateLimiterConfig = $policy['rateLimiterSettings'] ?? []; |
123 | $rateLimiterConfig['id'] = $policyId; |
124 | if (null !== $userId && !($policy['preferIPAddress'] ?? false)) { |
125 | $clientId = "u:$userId"; |
126 | } else { |
127 | $clientId = "ip:$clientIp"; |
128 | } |
129 | $factory = new RateLimiterFactory($rateLimiterConfig, $this->createCache($config)); |
130 | return $factory->create($clientId); |
131 | } |
132 | |
133 | /** |
134 | * Create cache for the rate limiter |
135 | * |
136 | * @param array $config Rate limiter configuration |
137 | * |
138 | * @return ?StorageInterface |
139 | */ |
140 | protected function createCache(array $config): StorageInterface |
141 | { |
142 | $storageConfig = $config['Storage'] ?? []; |
143 | $adapter = $storageConfig['adapter'] ?? 'memcached'; |
144 | $storageConfig['options']['namespace'] ??= 'RateLimiter'; |
145 | |
146 | // Handle Redis cache separately: |
147 | $adapterLc = strtolower($adapter); |
148 | if ('redis' === $adapterLc) { |
149 | return $this->createRedisCache($storageConfig); |
150 | } |
151 | |
152 | if ('vufind' === $adapterLc) { |
153 | // Use cache manager for "VuFind" cache (only for testing purposes): |
154 | $cacheManager = $this->serviceLocator->get(\VuFind\Cache\Manager::class); |
155 | $laminasCache = $cacheManager->getCache('object', $storageConfig['options']['namespace']); |
156 | // Fake the capabilities to include static TTL support: |
157 | $eventManager = $laminasCache->getEventManager(); |
158 | $eventManager->attach( |
159 | 'getCapabilities.post', |
160 | function ($event) use ($laminasCache) { |
161 | $oldCapacities = $event->getResult(); |
162 | $newCapacities = new Capabilities( |
163 | $laminasCache, |
164 | new stdClass(), |
165 | ['staticTtl' => true], |
166 | $oldCapacities |
167 | ); |
168 | $event->setResult($newCapacities); |
169 | } |
170 | ); |
171 | } else { |
172 | if ('memcached' === $adapterLc) { |
173 | $storageConfig['options']['servers'] ??= 'localhost:11211'; |
174 | } |
175 | |
176 | // Laminas cache: |
177 | $settings = [ |
178 | 'adapter' => $adapter, |
179 | 'options' => $storageConfig['options'], |
180 | ]; |
181 | $laminasCache = $this->serviceLocator |
182 | ->get(\Laminas\Cache\Service\StorageAdapterFactory::class) |
183 | ->createFromArrayConfiguration($settings); |
184 | } |
185 | |
186 | return new CacheStorage(new CacheItemPoolDecorator($laminasCache)); |
187 | } |
188 | |
189 | /** |
190 | * Create Redis cache for the rate limiter |
191 | * |
192 | * @param array $storageConfig Storage configuration |
193 | * |
194 | * @return ?StorageInterface |
195 | */ |
196 | protected function createRedisCache(array $storageConfig): StorageInterface |
197 | { |
198 | // Set defaults if nothing set in config file: |
199 | $options = $storageConfig['options']; |
200 | $host = $options['redis_host'] ?? 'localhost'; |
201 | $port = $options['redis_port'] ?? 6379; |
202 | $timeout = $options['redis_connection_timeout'] ?? 0.5; |
203 | $password = $options['redis_auth'] ?? null; |
204 | $username = $options['redis_user'] ?? null; |
205 | $redisDb = $options['redis_db'] ?? 0; |
206 | |
207 | // Create Credis client, the connection is established lazily: |
208 | $redis = new \Credis_Client($host, $port, $timeout, '', $redisDb, $password, $username); |
209 | if ($options['redis_standalone'] ?? true) { |
210 | $redis->forceStandalone(); |
211 | } |
212 | |
213 | return new CredisStorage($redis, $options); |
214 | } |
215 | } |