├── .atoum.php ├── .coke ├── .gitignore ├── .travis.yml ├── Annotation └── Firewall.php ├── Controller └── Listener.php ├── DependencyInjection ├── Configuration.php └── M6WebFirewallExtension.php ├── EventListener └── RequestListener.php ├── Firewall ├── Firewall.php ├── FirewallInterface.php ├── Provider.php └── ProviderInterface.php ├── LICENSE ├── M6WebFirewallBundle.php ├── README.md ├── Resources └── config │ └── services.yml ├── Tests ├── Units │ ├── Annotation │ │ └── Firewall.php │ ├── Controller │ │ └── Listener.php │ ├── DependencyInjection │ │ └── M6WebFirewallExtension.php │ ├── EventListener │ │ └── RequestListener.php │ └── Firewall │ │ └── Provider.php └── bootstrap.php └── composer.json /.atoum.php: -------------------------------------------------------------------------------- 1 | addTestsFromDirectory(__DIR__.'/Tests'); 4 | -------------------------------------------------------------------------------- /.coke: -------------------------------------------------------------------------------- 1 | # Configuration file for Coke, "Enjoy sniffing your code" : https://github.com/M6Web/Coke 2 | 3 | # Command used to launch PHP CodeSniffer (optional - default: phpcs) 4 | command=phpcs 5 | 6 | # Standard used by PHP CodeSniffer (required) 7 | standard=Symfony2 8 | 9 | # Verbose mode (optional - default: false) 10 | verbose=true 11 | 12 | # White list of files and directories (optional) 13 | Firewall/ 14 | EventListener/ 15 | Controller/ 16 | Annotation/ 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.0 5 | - 7.1 6 | 7 | before_script: 8 | - wget http://getcomposer.org/composer.phar 9 | - php -d memory_limit=1G composer.phar install 10 | 11 | script: 12 | - vendor/bin/atoum -d Tests 13 | -------------------------------------------------------------------------------- /Annotation/Firewall.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * @Annotation 10 | */ 11 | class Firewall 12 | { 13 | /** 14 | * @var string $config Name of the predefined configuration to use in the firewall 15 | */ 16 | public $config; 17 | 18 | /** 19 | * @var array $actions Controller methods where the firewall is applied 20 | */ 21 | public $actions; 22 | 23 | /** 24 | * @var array $options Firewall setting 25 | */ 26 | public $options; 27 | 28 | /** 29 | * Constructor, set the annotation data 30 | * 31 | * @param array $data Annotation data 32 | */ 33 | public function __construct(array $data) 34 | { 35 | $this->config = ( isset($data['config']) ? $data['config'] : null ); 36 | $this->actions = ( isset($data['actions']) ? $data['actions'] : null ); 37 | $this->options = array( 38 | "entries" => ( isset($data['entries']) ? $data['entries'] : array() ), 39 | "lists" => ( isset($data['lists']) ? $data['lists'] : array() ), 40 | "callback" => ( isset($data['callback']) ? $data['callback'] : null ), 41 | "default_state" => ( isset($data['default_state']) ? $data['default_state'] : null ), 42 | "error_message" => ( isset($data['error_message']) ? $data['error_message'] : null ), 43 | "error_code" => ( isset($data['error_code']) ? $data['error_code'] : null ), 44 | "throw_error" => ( isset($data['throw_error']) ? $data['throw_error'] : null ), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Controller/Listener.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class Listener 17 | { 18 | /** 19 | * @var Reader $reader Doctrine annotation reader 20 | */ 21 | protected $reader; 22 | 23 | /** 24 | * @var Provider $provider Firewall manager 25 | */ 26 | protected $provider; 27 | 28 | /** 29 | * Constructor 30 | * 31 | * @param Reader $reader Doctrine annotation reader 32 | * @param Provider $provider Firewall manager 33 | */ 34 | public function __construct(Reader $reader, Provider $provider) 35 | { 36 | $this->reader = $reader; 37 | $this->provider = $provider; 38 | } 39 | 40 | /** 41 | * Get the firewall manager 42 | * 43 | * @return Provider 44 | */ 45 | public function getProvider() 46 | { 47 | return $this->provider; 48 | } 49 | 50 | /** 51 | * Get the annotation reader 52 | * 53 | * @return Reader 54 | */ 55 | public function getReader() 56 | { 57 | return $this->reader; 58 | } 59 | 60 | /** 61 | * Event before action execution 62 | * 63 | * @param FilterControllerEvent $event Symfony trigger event 64 | */ 65 | public function onCoreController(FilterControllerEvent $event) 66 | { 67 | if (!is_array($controller = $event->getController())) { 68 | return; 69 | } 70 | 71 | $class = new \ReflectionClass($controller[0]); 72 | $method = $class->getMethod($controller[1]); 73 | 74 | if ($classAnnotations = $this->reader->getClassAnnotations($class)) { 75 | foreach ($classAnnotations as $classAnnotation) { 76 | if ($classAnnotation instanceof Firewall) { 77 | if (is_array($classAnnotation->actions) && in_array($method->name, $classAnnotation->actions)) { 78 | $this->loadFirewall($classAnnotation); 79 | } elseif ($classAnnotation->actions === null) { 80 | $this->loadFirewall($classAnnotation); 81 | } 82 | } 83 | } 84 | } 85 | 86 | if ($methodAnnotation = $this->reader->getMethodAnnotation($method, 'M6Web\Bundle\FirewallBundle\Annotation\Firewall')) { 87 | $this->loadFirewall($methodAnnotation); 88 | } 89 | } 90 | 91 | /** 92 | * Load firewall with a configuration 93 | * 94 | * @param \M6Web\Bundle\FirewallBundle\Annotation\Firewall $annotation Annotation with configuration data 95 | */ 96 | protected function loadFirewall($annotation) 97 | { 98 | $firewall = $this->provider->getFirewall($annotation->config, $annotation->options); 99 | $firewall->handle($annotation->options['callback']); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | root('m6web_firewall'); 22 | 23 | $rootNode 24 | ->children() 25 | // Add pre-defined configs management 26 | ->arrayNode('configs') 27 | ->useAttributeAsKey('name') 28 | ->prototype('array') 29 | ->fixXmlConfig('list', 'lists') 30 | ->fixXmlConfig('entry', 'entries') 31 | ->children() 32 | // Set firewall default state 33 | ->booleanNode('default_state')->defaultFalse()->end() 34 | ->scalarNode('error_code')->end() 35 | ->scalarNode('error_message')->end() 36 | ->booleanNode('throw_error')->defaultTrue()->end() 37 | // Set pre-defined list status 38 | ->arrayNode('lists') 39 | ->useAttributeAsKey('name') 40 | ->prototype('boolean')->defaultFalse()->end() 41 | ->end() 42 | // Set single entries status 43 | ->arrayNode('entries') 44 | ->useAttributeAsKey('template') 45 | ->prototype('boolean')->defaultFalse()->end() 46 | ->end() 47 | ->end() 48 | ->end() 49 | ->end() 50 | // Add pre-defined lists management 51 | ->arrayNode('lists') 52 | ->useAttributeAsKey('name') 53 | ->prototype('array') 54 | ->prototype('variable') 55 | ->validate() 56 | ->always(function ($list) { 57 | if (!is_array($list) && !is_string($list)) { 58 | throw new InvalidConfigurationException('Invalid configuration for path "m6web_firewall.lists": lists are any depth arrays of string values'); 59 | } 60 | 61 | return $list; 62 | }) 63 | ->end() 64 | ->cannotBeEmpty() 65 | ->end() 66 | ->end() 67 | ->end() 68 | // Add pattern support 69 | ->arrayNode('patterns') 70 | ->useAttributeAsKey('name') 71 | ->prototype('array') 72 | ->children() 73 | ->scalarNode('config')->end() 74 | ->scalarNode('path')->end() 75 | ->end() 76 | ->end() 77 | ->end() 78 | ->end(); 79 | 80 | return $treeBuilder; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /DependencyInjection/M6WebFirewallExtension.php: -------------------------------------------------------------------------------- 1 | flatLists($configs); 22 | 23 | $configuration = new Configuration(); 24 | $config = $this->processConfiguration($configuration, $configs); 25 | 26 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 27 | $loader->load('services.yml'); 28 | 29 | if (!empty($config['configs']) ) { 30 | $this->configsLoad($config['configs'], $container); 31 | } 32 | 33 | if (!empty($config['lists']) ) { 34 | $this->listsLoad($config['lists'], $container); 35 | } 36 | 37 | if (!empty($config['patterns']) ) { 38 | $this->patternsLoad($config['patterns'], $container); 39 | } 40 | } 41 | 42 | protected function configsLoad(array $config, ContainerBuilder $container) 43 | { 44 | $container->setParameter('m6web.firewall.configs', $config); 45 | } 46 | 47 | protected function listsLoad(array $config, ContainerBuilder $container) 48 | { 49 | $container->setParameter('m6web.firewall.lists', $config); 50 | } 51 | 52 | protected function patternsLoad(array $config, ContainerBuilder $container) 53 | { 54 | $container->setParameter('m6web.firewall.patterns', $config); 55 | } 56 | 57 | protected function flatLists(array &$configs) 58 | { 59 | foreach ($configs as &$config) { 60 | if (isset($config['lists'])) { 61 | foreach ($config['lists'] as &$list) { 62 | $this->flatList($list); 63 | } 64 | } 65 | } 66 | 67 | return $configs; 68 | } 69 | 70 | protected function flatList(array &$list) 71 | { 72 | $return = array(); 73 | array_walk_recursive($list, function($a) use (&$return) { 74 | $return[] = $a; 75 | }); 76 | $list = $return; 77 | 78 | return $list; 79 | } 80 | 81 | public function getAlias() 82 | { 83 | return 'm6web_firewall'; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /EventListener/RequestListener.php: -------------------------------------------------------------------------------- 1 | provider = $provider; 26 | } 27 | 28 | /** 29 | * on request event 30 | * @param GetResponseEvent $event event 31 | */ 32 | public function onRequest(GetResponseEvent $event) 33 | { 34 | if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { 35 | return; 36 | } 37 | $patterns = $this->provider->getPatterns(); 38 | if ($patterns) { 39 | foreach ($patterns as $pattern) { 40 | if ($pattern['matcher']->matches($event->getRequest())) { 41 | $firewall = $this->provider->getFirewall($pattern['config']); 42 | $firewall->handle(); 43 | } 44 | } 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /Firewall/Firewall.php: -------------------------------------------------------------------------------- 1 | provider = $provider; 45 | 46 | return $this; 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public function setRequest(Request $request) 53 | { 54 | $this->request = $request; 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * Add/remove a predefined list in the firewall 61 | * 62 | * @param string $listName List name 63 | * @param boolean|null $state List state in the firewall (true=white, false=black, null=undefined) 64 | * 65 | * @return $this 66 | */ 67 | public function setPresetList($listName, $state=null) 68 | { 69 | $lists = $this->provider->getLists(); 70 | 71 | if (is_array($listName)) { 72 | foreach ($listName as $list) { 73 | $this->setPresetList($list, $state); 74 | } 75 | } else { 76 | parent::setList($lists[$listName], $listName, $state); 77 | } 78 | 79 | return $this; 80 | } 81 | 82 | /** 83 | * Set a list as "black" (banned) 84 | * 85 | * @param string $listName List name 86 | * 87 | * @return $this 88 | */ 89 | public function setListBlack($listName) 90 | { 91 | $this->setPresetList($listName, false); 92 | 93 | return $this; 94 | } 95 | 96 | /** 97 | * Set a list as "white" (allowed) 98 | * 99 | * @param string $listName List name 100 | * 101 | * @return $this 102 | */ 103 | public function setListWhite($listName) 104 | { 105 | $this->setPresetList($listName, true); 106 | 107 | return $this; 108 | } 109 | 110 | /** 111 | * {@inheritdoc} 112 | */ 113 | public function setErrorCode($code) 114 | { 115 | if (is_int($code) || ctype_digit($code)) { 116 | $this->errorCode = (int) $code; 117 | } 118 | 119 | return $this; 120 | } 121 | 122 | /** 123 | * Get the error code 124 | * 125 | * @return integer 126 | */ 127 | public function getErrorCode() 128 | { 129 | return $this->errorCode; 130 | } 131 | 132 | /** 133 | * {@inheritdoc} 134 | */ 135 | public function setErrorMessage($message) 136 | { 137 | $this->errorMessage = (string) $message; 138 | 139 | return $this; 140 | } 141 | 142 | /** 143 | * Get the error message 144 | * 145 | * @return string 146 | */ 147 | public function getErrorMessage() 148 | { 149 | return $this->errorMessage; 150 | } 151 | 152 | /** 153 | * {@inheritdoc} 154 | */ 155 | public function setThrowError($throw) 156 | { 157 | if (is_bool($throw)) { 158 | $this->throwError = $throw; 159 | } else { 160 | throw new \InvalidArgumentException(sprintf("Firewall::setThrowError() Arg#1 must be a boolean, %s given.", gettype($throw))); 161 | } 162 | 163 | return $this; 164 | } 165 | 166 | /** 167 | * Check if an exception is throwed in error case 168 | * 169 | * @return boolean 170 | */ 171 | public function isThrowError() 172 | { 173 | return $this->throwError; 174 | } 175 | 176 | /** 177 | * {@inheritdoc} 178 | * 179 | * @throws HttpException If input is invalid and $throwError = true 180 | */ 181 | public function handle(callable $callBack = null) 182 | { 183 | $ipAddress = $this->request->getClientIp(); 184 | 185 | $this->setIpAddress($ipAddress); 186 | 187 | $isAllowed = parent::handle($callBack); 188 | 189 | if (!$isAllowed && $this->throwError) { 190 | throw new HttpException($this->errorCode, sprintf('%s [ip: %s]', $this->errorMessage, $ipAddress)); 191 | } 192 | 193 | return $isAllowed; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /Firewall/FirewallInterface.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class Provider implements ProviderInterface 15 | { 16 | /** 17 | * @var array|null $lists Lists of predefined named ip 18 | */ 19 | protected $lists; 20 | 21 | /** 22 | * @var array|null $configs Predefined configurations 23 | */ 24 | protected $configs; 25 | 26 | /** 27 | * @var array|null $configs Predefined patterns 28 | */ 29 | protected $patterns; 30 | 31 | /** 32 | * @var string $firewallClass Class for firewall objects 33 | */ 34 | protected $firewallClass = 'M6Web\Bundle\FirewallBundle\Firewall\Firewall'; 35 | 36 | /** 37 | * @var array $configModel Configuration model use to format configuration 38 | */ 39 | protected $configModel = array( 40 | "default_state" => array( 41 | "default" => null, 42 | "method" => "setDefaultState", 43 | ), 44 | "error_message" => array( 45 | "default" => null, 46 | "method" =>"setErrorMessage", 47 | ), 48 | "error_code" => array( 49 | "default" => null, 50 | "method" =>"setErrorCode", 51 | ), 52 | "throw_error" => array( 53 | "default" => null, 54 | "method" =>"setThrowError", 55 | ), 56 | "lists" => array( 57 | "default" => null, 58 | ), 59 | "entries" => array( 60 | "default" => array(), 61 | ), 62 | ); 63 | 64 | /** 65 | * @var array $firewalls Activated firewalls 66 | */ 67 | protected $firewalls = array(); 68 | 69 | /** 70 | * @var ContainerInterface $container Service container 71 | */ 72 | protected $container; 73 | 74 | /** 75 | * Constructor 76 | * 77 | * @param ContainerInterface $container Service container 78 | * @param array $lists Lists of predefined ip 79 | * @param array $configs Predefined configurations 80 | * @param string $patterns Pattern 81 | */ 82 | public function __construct(ContainerInterface $container, array $lists = null, array $configs = null, $patterns = null) 83 | { 84 | $this->lists = $lists; 85 | $this->container = $container; 86 | 87 | if (!empty($configs)) { 88 | foreach ($configs as $configName => $config) { 89 | $this->loadConfig($configName, $config); 90 | } 91 | } 92 | 93 | if (!empty($patterns)) { 94 | foreach ($patterns as $patternName => $pattern) { 95 | $this->loadPattern($patternName, $pattern); 96 | } 97 | } 98 | } 99 | 100 | /** 101 | * Load a formated configuration 102 | * 103 | * @param string $configName Configuration name in $this->configs 104 | * @param array $config Configuration data 105 | * 106 | * @return $this 107 | */ 108 | protected function loadConfig($configName, array $config) 109 | { 110 | $this->configs[$configName] = $this->normalizeConfig($config); 111 | 112 | return $this; 113 | } 114 | 115 | /** 116 | * Load a formated pattern 117 | * 118 | * @param string $patternName Patern name in $this->patterns 119 | * @param array $pattern Pattern data 120 | * 121 | * @return $this 122 | */ 123 | protected function loadPattern($patternName, array $pattern) 124 | { 125 | $pattern['matcher'] = new RequestMatcher($pattern['path']); 126 | 127 | $this->patterns[$patternName] = $pattern; 128 | 129 | return $this; 130 | } 131 | 132 | /** 133 | * Format an array of configurations with the model 134 | * 135 | * @param array $config Base configurations 136 | * 137 | * @return array Formated configurations 138 | */ 139 | protected function normalizeConfig(array $config) 140 | { 141 | foreach ($this->configModel as $elmt => $settings) { 142 | if (!isset($config[$elmt])) { 143 | $config[$elmt] = $settings['default']; 144 | } 145 | } 146 | 147 | foreach ($config as $elmtName => $value) { 148 | if (!isset($this->configModel[$elmtName])) { 149 | unset($config[$elmtName]); 150 | } 151 | } 152 | 153 | return $config; 154 | } 155 | 156 | /** 157 | * Get a firewall set with a predefined configuration or with additional setting 158 | * 159 | * @param string|null $configName Predefined configuration name 160 | * @param array $options Additional setting 161 | * @param Request $request Request 162 | * 163 | * @return FirewallInterface 164 | */ 165 | public function getFirewall($configName = null, array $options = array(), Request $request = null) 166 | { 167 | if (!$request) { 168 | if ($this->container->has('request_stack')) { 169 | $requestStack = $this->container->get('request_stack'); 170 | $request = $requestStack->getCurrentRequest(); 171 | } elseif ($this->container->has('request')) { 172 | $request = $this->container->get('request'); 173 | } else { 174 | throw new \Exception('Request object undefined'); 175 | } 176 | } 177 | 178 | $firewall = new $this->firewallClass(); 179 | $firewall->setProvider($this); 180 | $firewall->setRequest($request); 181 | 182 | $this->setDefaults($firewall, $configName, $options); 183 | 184 | $this->firewalls[] = $firewall; 185 | 186 | return $firewall; 187 | } 188 | 189 | /** 190 | * Set the firewall class 191 | * 192 | * @param string $firewallClass Class for firewall objects 193 | * 194 | * @return $this 195 | */ 196 | public function setFirewallClass($firewallClass) 197 | { 198 | $this->firewallClass = $firewallClass; 199 | 200 | return $this; 201 | } 202 | 203 | /** 204 | * Get lists of predefined ip 205 | * 206 | * @return array 207 | */ 208 | public function getLists() 209 | { 210 | return $this->lists; 211 | } 212 | 213 | /** 214 | * get predefined configuration 215 | * 216 | * @return array 217 | */ 218 | public function getConfigs() 219 | { 220 | return $this->configs; 221 | } 222 | 223 | /** 224 | * get predefined patterns 225 | * 226 | * @return array 227 | */ 228 | public function getPatterns() 229 | { 230 | return $this->patterns; 231 | } 232 | 233 | /** 234 | * Configure a firewall 235 | * 236 | * @param FirewallInterface $firewall Firewall 237 | * @param string $configName Predefined configuration name 238 | * @param array $options Additional setting 239 | * 240 | * @return $this 241 | */ 242 | protected function setDefaults(FirewallInterface $firewall, $configName = null, array $options = array()) 243 | { 244 | $config = $this->getNormalizedConfig($configName); 245 | 246 | $this->mergeOptions($config, $options); 247 | 248 | foreach ($config as $paramName => $paramValue) { 249 | $this->setDefault($firewall, $paramName, $paramValue, $config); 250 | } 251 | 252 | return $this; 253 | } 254 | 255 | /** 256 | * Set a default value for a configuration parameter 257 | * 258 | * @param Firewall $firewall Firewall 259 | * @param string $param Parameter name 260 | * @param string $value Parameter value 261 | * @param array $config Reference to the configuration 262 | * 263 | * @return $this 264 | */ 265 | protected function setDefault(FirewallInterface $firewall, $param, $value, $config) 266 | { 267 | $configModel = $this->configModel[$param]; 268 | 269 | switch ($param) { 270 | case 'lists': 271 | if (is_array($value)) { 272 | $this->setDefaultLists($firewall, $value); 273 | } 274 | break; 275 | case 'entries': 276 | $this->setEntries($firewall, $config, $value); 277 | break; 278 | default: 279 | if (!is_null($value)) { 280 | $method = $configModel['method']; 281 | $firewall->$method($value); 282 | } 283 | } 284 | 285 | return $this; 286 | } 287 | 288 | /** 289 | * Set the default lists of the firewall 290 | * 291 | * @param FirewallInterface $firewall Firewall 292 | * @param array $lists Input lists 293 | * 294 | * @return $this 295 | */ 296 | protected function setDefaultLists(FirewallInterface $firewall, array $lists) 297 | { 298 | foreach ($lists as $listName => $state) { 299 | if (isset($this->lists[$listName])) { 300 | $list = $this->lists[$listName]; 301 | $firewall->addList($list, $listName, $state); 302 | } else { 303 | throw new \Exception(sprintf('Firewall list "%s" not found.', $listName)); 304 | } 305 | } 306 | 307 | return $this; 308 | } 309 | 310 | /** 311 | * Set up firewall lists from independent input 312 | * 313 | * @param FirewallInterface $firewall Firewall interface 314 | * @param array $config Reference to the configuration 315 | * 316 | * @return $this 317 | */ 318 | protected function setEntries(FirewallInterface $firewall, $config) 319 | { 320 | $entries = (isset($config['entries']) ? $config['entries'] : array()); 321 | 322 | $blackEntries = array_keys($entries, false); 323 | $whiteEntries = array_keys($entries, true); 324 | 325 | if (count($blackEntries)) { 326 | $firewall->addList($blackEntries, 'blackedOptions', false); 327 | } 328 | 329 | if (count($whiteEntries)) { 330 | $firewall->addList($whiteEntries, 'whitedOptions', true); 331 | } 332 | 333 | return $this; 334 | } 335 | 336 | /** 337 | * Get the normalized configuration 338 | * 339 | * @param string|null $configName Configuration name 340 | * 341 | * @throws Exception If the configuration name is unknown 342 | * 343 | * @return array Normalized configuration 344 | */ 345 | protected function getNormalizedConfig($configName = null) 346 | { 347 | if (isset($this->configs[$configName])) { 348 | $config = $this->configs[$configName]; 349 | } elseif ($configName === null) { 350 | $arr = array(); 351 | $config = $this->normalizeConfig($arr); 352 | } else { 353 | throw new \Exception(sprintf('Firewall configuration "%s" not found.', $configName)); 354 | } 355 | 356 | return $config; 357 | } 358 | 359 | /** 360 | * Include on the fly generated options in the main configuration 361 | * 362 | * @param array &$config Reference to the configuration 363 | * @param array $options Options 364 | * 365 | * @return array Full configuration 366 | */ 367 | protected function mergeOptions(&$config, $options) 368 | { 369 | foreach ($config as $paramName => $paramValue) { 370 | if (isset($options[$paramName])) { 371 | if (is_array($paramValue)) { 372 | $config[$paramName] = array_merge($config[$paramName], $options[$paramName]); 373 | } else { 374 | $config[$paramName] = $options[$paramName]; 375 | } 376 | } 377 | } 378 | 379 | return $config; 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /Firewall/ProviderInterface.php: -------------------------------------------------------------------------------- 1 | true, 17 | 'error_code' => 403, 18 | 'error_message' => 'Interdit', 19 | 'throw_error' => false, 20 | 'callback' => 'testMyCallback', 21 | 'entries' => array( 22 | '192.168.25.3' => false, 23 | '127.0.0.1' => true, 24 | '10.36.25.*' => true, 25 | ), 26 | 'lists' => array( 27 | 'default' => false, 28 | 'partners' => true, 29 | ), 30 | 'config' => "default", 31 | 'actions' => array( 32 | 'indexAction', 33 | 'testAction', 34 | ), 35 | ); 36 | 37 | public function testConstructor() 38 | { 39 | $annotation = new FirewallAnnotation($this->data); 40 | 41 | foreach ($this->data as $name => $data) { 42 | switch ($name) { 43 | case 'config': 44 | case 'actions': 45 | $this->assert 46 | ->variable($this->data[$name]) 47 | ->isIdenticalTo($annotation->$name) 48 | ; 49 | break; 50 | 51 | default: 52 | $this->assert 53 | ->variable($this->data[$name]) 54 | ->isIdenticalTo($annotation->options[$name]) 55 | ; 56 | break; 57 | } 58 | } 59 | } 60 | 61 | public function testConstructorWithoutData() 62 | { 63 | $annotation = new FirewallAnnotation(array()); 64 | 65 | $this->assert 66 | ->variable($annotation->config) 67 | ->isNull() 68 | ->variable($annotation->actions) 69 | ->isNull() 70 | ; 71 | 72 | foreach ($annotation->options as $name => $value) { 73 | switch ($name) { 74 | case 'entries': 75 | case 'lists': 76 | $this->assert 77 | ->array($value) 78 | ->isEmpty() 79 | ; 80 | break; 81 | 82 | default: 83 | $this->assert 84 | ->variable($value) 85 | ->isNull() 86 | ; 87 | break; 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Tests/Units/Controller/Listener.php: -------------------------------------------------------------------------------- 1 | mockClass('Symfony\Component\DependencyInjection\ContainerInterface', '\Mock'); 24 | $this->mockClass('Symfony\Component\HttpFoundation\Request', '\Mock'); 25 | $this->mockClass('Doctrine\Common\Annotations\AnnotationReader', '\Mock'); 26 | 27 | $request = new \Mock\Request(); 28 | $container = new \Mock\ContainerInterface(); 29 | 30 | $container->getMockController()->get = function($serviceName) use ($request) { 31 | switch ($serviceName) { 32 | case 'request': 33 | return $request; 34 | } 35 | }; 36 | 37 | $provider = new Provider($container, array(), array()); 38 | $reader = new \Mock\AnnotationReader(); 39 | 40 | $listener = new Controller\Listener($reader, $provider); 41 | 42 | $this->assert 43 | ->object($provider) 44 | ->isIdenticalTo($listener->getProvider()) 45 | ; 46 | 47 | $this->assert 48 | ->object($reader) 49 | ->isIdenticalTo($listener->getReader()) 50 | ; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Tests/Units/DependencyInjection/M6WebFirewallExtension.php: -------------------------------------------------------------------------------- 1 | array( 20 | 'default' => array( 21 | 'default_state' => true, 22 | 'error_code' => 400, 23 | 'error_message' => 'Interdit', 24 | 'throw_error' => false, 25 | ), 26 | ), 27 | 'lists' => array( 28 | 'partners' => array( 29 | '10.20.30.40', 30 | '40.30.20.10', 31 | ), 32 | ), 33 | ), 34 | array( 35 | 'configs' => array( 36 | 'default' => array( 37 | 'lists' => array( 38 | 'default' => true, 39 | 'partners' => false, 40 | ), 41 | 'entries' => array( 42 | '127.0.0.1' => true, 43 | '192.168.1.1' => false, 44 | ), 45 | ), 46 | ), 47 | 'lists' => array( 48 | 'default' => array( 49 | 'ipv6' => array('::1'), 50 | 'ipv4' => array( 51 | '192.168.1.*', 52 | '192.168.0.0-192.168.0.254', 53 | ), 54 | ), 55 | ), 56 | ) 57 | ); 58 | 59 | /** 60 | * Configuration load test 61 | */ 62 | public function testLoad() 63 | { 64 | $container = new ContainerBuilder(); 65 | 66 | $loader = new FirewallExt(); 67 | $loader->load($this->configs, $container); 68 | 69 | $this->assert 70 | ->string($container->getParameter('m6web.firewall.class')) 71 | ->isEqualTo('M6Web\Bundle\FirewallBundle\Firewall\Firewall') 72 | ->array($container->getParameter('m6web.firewall.lists')) 73 | ->isEqualTo(array( 74 | 'default' => array( 75 | '::1', 76 | '192.168.1.*', 77 | '192.168.0.0-192.168.0.254', 78 | ), 79 | 'partners' => array( 80 | '10.20.30.40', 81 | '40.30.20.10', 82 | ), 83 | )) 84 | ->array($container->getParameter('m6web.firewall.configs')) 85 | ->isEqualTo(array( 86 | 'default' => array( 87 | 'default_state' => true, 88 | 'error_code' => 400, 89 | 'error_message' => 'Interdit', 90 | 'throw_error' => false, 91 | 'lists' => array( 92 | 'default' => true, 93 | 'partners' => false, 94 | ), 95 | 'entries' => array( 96 | '127.0.0.1' => true, 97 | '192.168.1.1' => false, 98 | ), 99 | ), 100 | )) 101 | 102 | ->object($providerDefinition = $container->getDefinition('m6web.firewall.provider')) 103 | ->string($providerDefinition->getClass()) 104 | ->isEqualTo('M6Web\Bundle\FirewallBundle\Firewall\Provider') 105 | ->object($providerDefinition->getArgument(0)) 106 | ->isInstanceOf('Symfony\Component\DependencyInjection\Reference') 107 | ->string((string) $providerDefinition->getArgument(0)) 108 | ->isEqualTo('service_container') 109 | ->string($providerDefinition->getArgument(1)) 110 | ->isEqualTo('%m6web.firewall.lists%') 111 | ->string($providerDefinition->getArgument(2)) 112 | ->isEqualTo('%m6web.firewall.configs%') 113 | ->array($providerDefinition->getMethodCalls()) 114 | ->hasSize(1) 115 | ->array($providerDefinition->getMethodCalls()[0]) 116 | ->isEqualTo(array('setFirewallClass', array('%m6web.firewall.class%'))) 117 | 118 | ->object($listenerDefinition = $container->getDefinition('m6web.firewall.controller_listener')) 119 | ->string($listenerDefinition->getClass()) 120 | ->isEqualTo('M6Web\Bundle\FirewallBundle\Controller\Listener') 121 | ->object($listenerDefinition->getArgument(0)) 122 | ->isInstanceOf('Symfony\Component\DependencyInjection\Reference') 123 | ->string((string) $listenerDefinition->getArgument(0)) 124 | ->isEqualTo('annotation_reader') 125 | ->object($listenerDefinition->getArgument(1)) 126 | ->isInstanceOf('Symfony\Component\DependencyInjection\Reference') 127 | ->string((string) $listenerDefinition->getArgument(1)) 128 | ->isEqualTo('m6web.firewall.provider') 129 | ->array($listenerDefinition->getTag('kernel.event_listener')) 130 | ->isEqualTo(array(array('event' => 'kernel.controller', 'method' => 'onCoreController', 'priority' => -255))) 131 | ; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Tests/Units/EventListener/RequestListener.php: -------------------------------------------------------------------------------- 1 | getMockedProvider(); 28 | $requestListener = new TestedClass($mockedProvider); 29 | 30 | $this->if($requestListener) 31 | ->then($requestListener->onRequest($event)) 32 | ->mock($mockedProvider) 33 | ->call('getPatterns') 34 | ->once() 35 | ->call('getFirewall') 36 | ->withArguments('configTest') 37 | ->once(); 38 | } 39 | 40 | /** 41 | * test on request with a request not matching pattern 42 | * 43 | * @return Request 44 | */ 45 | public function testOnRequestWithUnValidRequest() 46 | { 47 | $request = \Symfony\Component\HttpFoundation\Request::create('/toto'); 48 | $mockKernel = new \mock\Symfony\Component\HttpKernel\HttpKernelInterface(); 49 | $event = new \Symfony\Component\HttpKernel\Event\GetResponseEvent($mockKernel, $request, HttpKernelInterface::MASTER_REQUEST); 50 | $mockedProvider = $this->getMockedProvider(); 51 | $requestListener = new TestedClass($mockedProvider); 52 | 53 | $this->if($requestListener) 54 | ->then($requestListener->onRequest($event)) 55 | ->mock($mockedProvider) 56 | ->call('getPatterns') 57 | ->once() 58 | ->call('getFirewall') 59 | ->never(); 60 | } 61 | 62 | /** 63 | * test on request with a request not matching pattern 64 | * 65 | * @return Request 66 | */ 67 | public function testOnRequestWithSubRequest() 68 | { 69 | $request = \Symfony\Component\HttpFoundation\Request::create('/test'); 70 | $mockKernel = new \mock\Symfony\Component\HttpKernel\HttpKernelInterface(); 71 | $event = new \Symfony\Component\HttpKernel\Event\GetResponseEvent($mockKernel, $request, HttpKernelInterface::SUB_REQUEST); 72 | $mockedProvider = $this->getMockedProvider(); 73 | $requestListener = new TestedClass($mockedProvider); 74 | 75 | $this->if($requestListener) 76 | ->then($requestListener->onRequest($event)) 77 | ->mock($mockedProvider) 78 | ->call('getFirewall') 79 | ->never(); 80 | } 81 | 82 | /** 83 | * test on request with no patterns 84 | * 85 | * @return Request 86 | */ 87 | public function testOnRequestWithNoPatterns() 88 | { 89 | $request = \Symfony\Component\HttpFoundation\Request::create('/toto'); 90 | $mockKernel = new \mock\Symfony\Component\HttpKernel\HttpKernelInterface(); 91 | $event = new \Symfony\Component\HttpKernel\Event\GetResponseEvent($mockKernel, $request, HttpKernelInterface::MASTER_REQUEST); 92 | $mockedProvider = $this->getMockedProvider(); 93 | $mockedProvider->getMockController()->getPatterns = function() { 94 | return null; 95 | }; 96 | $requestListener = new TestedClass($mockedProvider); 97 | 98 | $this->if($requestListener) 99 | ->then($requestListener->onRequest($event)) 100 | ->mock($mockedProvider) 101 | ->call('getPatterns') 102 | ->once() 103 | ->call('getFirewall') 104 | ->never(); 105 | } 106 | 107 | protected function getMockedProvider() 108 | { 109 | $firewallProvider = new \mock\M6Web\Bundle\FirewallBundle\Firewall\ProviderInterface(); 110 | 111 | $firewallProvider->getMockController()->getPatterns = function() { 112 | return array( 113 | 'pattern1' => array( 114 | 'path' => '/test', 115 | 'config' => 'configTest', 116 | 'matcher' => new RequestMatcher('/test') 117 | ) 118 | ); 119 | }; 120 | 121 | $firewallProvider->getMockController()->getFirewall = function() { 122 | return new \mock\M6Web\Bundle\FirewallBundle\Firewall\FirewallInterface(); 123 | }; 124 | 125 | return $firewallProvider; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Tests/Units/Firewall/Provider.php: -------------------------------------------------------------------------------- 1 | array( 20 | 'default_state' => true, 21 | 'error_code' => 400, 22 | 'error_message' => 'Interdit', 23 | 'throw_error' => false, 24 | 'lists' => array( 25 | 'default' => true, 26 | 'partners' => false, 27 | ), 28 | 'entries' => array( 29 | '127.0.0.1' => true, 30 | '192.168.1.1' => false, 31 | ), 32 | ), 33 | 'empty' => array( 34 | 'toomuch' => true, 35 | ), 36 | ); 37 | 38 | protected $patterns = array( 39 | 'pattern1' => array( 40 | 'config' => 'default', 41 | 'path' => '/test' 42 | ) 43 | ); 44 | 45 | protected $expectedConfig = array( 46 | 'default' => array( 47 | 'default_state' => true, 48 | 'error_code' => 400, 49 | 'error_message' => 'Interdit', 50 | 'throw_error' => false, 51 | 'lists' => array( 52 | 'default' => true, 53 | 'partners' => false, 54 | ), 55 | 'entries' => array( 56 | '127.0.0.1' => true, 57 | '192.168.1.1' => false, 58 | ), 59 | ), 60 | 'empty' => array( 61 | 'default_state' => null, 62 | 'error_message' => null, 63 | 'error_code' => null, 64 | 'throw_error' => null, 65 | 'lists' => null, 66 | 'entries' => array(), 67 | ), 68 | ); 69 | 70 | protected $lists = array( 71 | 'default' => array( 72 | '::1', 73 | '192.168.1.*', 74 | '192.168.0.0-192.168.0.254', 75 | ), 76 | 'partners' => array( 77 | '10.20.30.40', 78 | '40.30.20.10', 79 | ), 80 | ); 81 | 82 | /** 83 | * Instantiation test 84 | */ 85 | public function testConstruct() 86 | { 87 | $provider = $this->getProvider(); 88 | 89 | $this->assert 90 | ->array($provider->getLists()) 91 | ->isEqualTo($this->lists) 92 | ->array($provider->getConfigs()) 93 | ->isEqualTo($this->expectedConfig) 94 | ->array($provider->getPatterns()) 95 | ->isEqualTo($this->getExpectedPatterns()); 96 | } 97 | 98 | /** 99 | * Generation test 100 | */ 101 | public function testGetFirewall() 102 | { 103 | $provider = $this->getProvider(); 104 | $provider->setFirewallClass('\mock\M6Web\Bundle\FirewallBundle\Firewall\FirewallInterface'); 105 | $controller = new \atoum\mock\controller(); 106 | $addListCalls = array(); 107 | $controller->addList = function () use (&$addListCalls) { 108 | $addListCalls[] = func_get_args(); 109 | }; 110 | 111 | $firewall = $provider->getFirewall('default', array( 112 | 'default_state' => false, 113 | 'error_message' => 'TestOptions', 114 | )); 115 | 116 | $this 117 | ->mock($firewall) 118 | ->call('setProvider') 119 | ->withIdenticalArguments($provider) 120 | ->once() 121 | ->call('setRequest') 122 | ->withIdenticalArguments($this->request) 123 | ->once() 124 | ->call('setDefaultState') 125 | ->withIdenticalArguments(false) 126 | ->once() 127 | ->call('setErrorMessage') 128 | ->withIdenticalArguments('TestOptions') 129 | ->once() 130 | ->call('setErrorCode') 131 | ->withIdenticalArguments(400) 132 | ->once() 133 | ->call('setThrowError') 134 | ->withIdenticalArguments(false) 135 | ->once(); 136 | 137 | $this->assert 138 | ->array($addListCalls) 139 | ->hasSize(4) 140 | ->array($addListCalls[0]) 141 | ->isEqualTo(array(array( 142 | '::1', 143 | '192.168.1.*', 144 | '192.168.0.0-192.168.0.254', 145 | ), 'default', true)) 146 | ->array($addListCalls[1]) 147 | ->isEqualTo(array(array( 148 | '10.20.30.40', 149 | '40.30.20.10', 150 | ), 'partners', false)) 151 | ->array($addListCalls[2]) 152 | ->isEqualTo(array(array( 153 | '192.168.1.1', 154 | ), 'blackedOptions', false)) 155 | ->array($addListCalls[3]) 156 | ->isEqualTo(array(array( 157 | '127.0.0.1', 158 | ), 'whitedOptions', true)); 159 | } 160 | 161 | protected function getExpectedPatterns() 162 | { 163 | return array( 164 | 'pattern1' => array( 165 | 'config' => 'default', 166 | 'path' => '/test', 167 | 'matcher' => new RequestMatcher('/test') 168 | ) 169 | ); 170 | } 171 | 172 | /** 173 | * Get a provider mock 174 | * 175 | * @return Firewall\Provider 176 | */ 177 | protected function getProvider() 178 | { 179 | $container = new \Mock\Symfony\Component\DependencyInjection\ContainerInterface(); 180 | $request = new \Mock\Symfony\Component\HttpFoundation\Request(); 181 | $this->request = $request; 182 | $container->getMockController()->has = function ($serviceName) { 183 | switch ($serviceName) { 184 | case 'request_stack': 185 | return true; 186 | case 'request': 187 | return false; 188 | } 189 | }; 190 | $container->getMockController()->get = function ($serviceName) use ($request) { 191 | switch ($serviceName) { 192 | case 'request_stack': 193 | $requestStack = new \Symfony\Component\HttpFoundation\RequestStack(); 194 | $requestStack->push($request); 195 | 196 | return $requestStack; 197 | } 198 | }; 199 | 200 | return new Firewall\Provider($container, $this->lists, $this->configs, $this->patterns); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /Tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | =7.0.0", 20 | "ext-bcmath": "*", 21 | "m6web/firewall": "~1.0" 22 | }, 23 | "require-dev": { 24 | "atoum/atoum": "^2.8|^3.0", 25 | "atoum/atoum-bundle": "@stable", 26 | "symfony/symfony": "^2.2|^3.0|^4.0" 27 | } 28 | } 29 | --------------------------------------------------------------------------------