Implementing an IP Blocker in Symfony 4

Did you ever think about banning an IP address? Maybe because strange attacks or spam is coming from that specific address. Whatever reason you might have for banning or blocking an IP address completely from using your site(s), I’m certain it’s a good one.

So, the things you need in order to achieve the desired result are few:

  1. The entity (BlockedIP.php)
    <?php
    
    namespace App\Entity;
    
    use Doctrine\ORM\Mapping as ORM;
    
    /**
     * BlockedIP
     *
     * @ORM\Table(name="blocked_ip")
     * @ORM\Entity(repositoryClass="App\Repository\BlockedIPRepository")
     */
    class BlockedIP
    {
        /**
         * @var int
         *
         * @ORM\Column(name="id", type="integer")
         * @ORM\Id
         * @ORM\GeneratedValue(strategy="AUTO")
         */
        private $id;
    
        /**
         * @var integer
         * @ORM\Column(type="integer", unique=true)
         */
        private $ip;
        /**
         * @var \DateTime
         * @ORM\Column(type="datetime")
         */
        private $bannedUntil;
        /**
         * @var string
         * @ORM\Column(type="string", nullable=true)
         */
        private $reason;
        /**
         * @var \DateTime
         * @ORM\Column(type="datetime")
         */
        private $createdAt;
    
        public function __construct()
        {
            $this->ip = ip2long($_SERVER['REMOTE_ADDR']);
            $this->bannedUntil = new \DateTime();
            $this->createdAt = new \DateTime();
        }
    
        public function __toString()
        {
            return $this->getIp() . ' until ' . $this->getBannedUntil()->format('F m, Y H:i');
        }
    
        /**
         * Get id
         *
         * @return int
         */
        public function getId()
        {
            return $this->id;
        }
    
        /**
         * @return int
         */
        public function getIp()
        {
            return long2ip($this->ip);
        }
    
        /**
         * @param int $ip
         */
        public function setIp($ip)
        {
            $this->ip = ip2long($ip);
        }
    
        /**
         * @return \DateTime
         */
        public function getBannedUntil()
        {
            return $this->bannedUntil;
        }
    
        /**
         * @param \DateTime $bannedUntil
         */
        public function setBannedUntil($bannedUntil)
        {
            $this->bannedUntil = $bannedUntil;
        }
    
        /**
         * @return mixed
         */
        public function getReason()
        {
            return $this->reason;
        }
    
        /**
         * @param mixed $reason
         */
        public function setReason($reason)
        {
            $this->reason = $reason;
        }
    
        /**
         * @return \DateTime
         */
        public function getCreatedAt()
        {
            return $this->createdAt;
        }
    
        /**
         * @param \DateTime $createdAt
         */
        public function setCreatedAt($createdAt)
        {
            $this->createdAt = $createdAt;
        }
    }
    
    
  2. The EventListener. If you like HTML output, create the necessary template file.
    <?php
    
    namespace App\EventListener;
    
    use App\Entity\BlockedIP;
    use Doctrine\ORM\EntityManager;
    use Psr\Container\ContainerInterface;
    use Symfony\Component\HttpFoundation\Response;
    use Symfony\Component\HttpKernel\Event\GetResponseEvent;
    
    class IPBlockerListener
    {
        private $container;
        private $em;
        private $twig;
    
        public function __construct(ContainerInterface $container, EntityManager $em)
        {
            $this->container = $container;
            $this->em = $em;
            $this->twig = $container->get('twig');
        }
    
        public function onKernelRequest(GetResponseEvent $event)
        {
            if (!$event->isMasterRequest()) {
                return;
            }
    
            $qb = $this->em->createQueryBuilder();
            #$qb->select('i.reason as reason, count(i) as total, i.bannedUntil as bannedUntil')
            $qb
                ->select('i')
                ->from(BlockedIP::class, 'i')
                ->where('i.ip = :ip')
                ->andWhere('i.bannedUntil > CURRENT_TIMESTAMP()')
                ->setParameter('ip', ip2long(filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)))
            ;
            $res = $qb->getQuery()->getResult();
            #echo '<pre>';var_dump($res);die;
    
            if(count($res) > 0) {
                #$event->setResponse(new Response('You have been banned ¯\_(ツ)_/¯', 403));
                $event->setResponse(new Response($this->twig->render('misc/banned.html.twig', array()), 403));
            }
    
        }
    }
  3. An EventSubscriber (which is rather optional, actually). This is to remove existing entries with the same IP address. Just some candy.
    <?php
    
    namespace App\EventSubscriber;
    
    use App\Entity\BlockedIP;
    use Doctrine\Common\EventSubscriber;
    use Doctrine\ORM\Event\LifecycleEventArgs;
    use Doctrine\ORM\Events;
    use Symfony\Component\DependencyInjection\ContainerInterface;
    
    class BanIPSubscriber implements EventSubscriber
    {
        protected $container;
    
        /**
         * UserInvitationSubscriber constructor.
         * @param ContainerInterface $container
         */
        public function __construct(ContainerInterface $container)
        {
            $this->container = $container;
        }
        /**
         * Returns an array of events this subscriber wants to listen to.
         *
         * @return array
         */
        public function getSubscribedEvents()
        {
            return array(
                Events::prePersist,
            );
        }
    
        public function prePersist(LifecycleEventArgs $args)
        {
            $entity = $args->getEntity();
            if ($entity instanceof BlockedIP) {
                $em = $args->getEntityManager();
                $check = $em->getRepository(BlockedIP::class)->findBy(array('ip' => $entity->getIp()));
                if(count($check) > 0) {
                    $em->remove($check[0]);
                    $em->flush();
                }
            }
        }
    
    }
    
  4. In your services.yaml, rregister the Listener and the Subscriber and inject the necessary objects. Please notice that the priority is set to 100. That means it will be the first Listener to be fired, which makes kind of sense in this context.
    App\EventListener\IPBlockerListener:
        arguments: ['@service_container', '@doctrine.orm.entity_manager']
        tags:
            - { name: kernel.event_listener, event: kernel.request, priority: 100 }
    
    App\EventSubscriber\BanIPSubscriber:
        arguments: ['@service_container']
        tags:
            - { name: doctrine.event_subscriber, connection: default }
  5.  Also, a Sonata Admin will prove to be useful. You can also use this to edit existing entries.
    <?php
    
    declare(strict_types=1);
    
    namespace App\Admin;
    
    use Sonata\AdminBundle\Admin\AbstractAdmin;
    use Sonata\AdminBundle\Datagrid\DatagridMapper;
    use Sonata\AdminBundle\Datagrid\ListMapper;
    use Sonata\AdminBundle\Form\FormMapper;
    use Sonata\AdminBundle\Show\ShowMapper;
    use Symfony\Component\Form\Extension\Core\Type\TextType;
    
    final class BlockedIPAdmin extends AbstractAdmin
    {
    
        protected function configureDatagridFilters(DatagridMapper $datagridMapper): void
        {
            $datagridMapper
                ->add('id')
                ->add('ip')
                ->add('bannedUntil')
                ->add('reason')
                ->add('createdAt')
                ;
        }
    
        protected function configureListFields(ListMapper $listMapper): void
        {
            $listMapper
                ->add('id')
                ->add('ip')
                ->add('bannedUntil')
                ->add('reason')
                ->add('createdAt')
                ->add('_action', null, [
                    'actions' => [
                        'show' => [],
                        'edit' => [],
                        'delete' => [],
                    ],
                ]);
        }
    
        protected function configureFormFields(FormMapper $formMapper): void
        {
            $formMapper
                #->add('id')
                ->add('ip', TextType::class)
                ->add('bannedUntil')
                ->add('reason')
                #->add('createdAt')
                ;
        }
    
        protected function configureShowFields(ShowMapper $showMapper): void
        {
            $showMapper
                ->add('id')
                ->add('ip')
                ->add('bannedUntil')
                ->add('reason')
                ->add('createdAt')
                ;
        }
    }
    
  6. That’s about it. If you want to prevent certain IPs from using your site you can easily block them with this.

Leave a Reply

Your email address will not be published. Required fields are marked *

7 − 6 =