vendor/nelmio/cors-bundle/EventListener/CorsListener.php line 59

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the NelmioCorsBundle.
  4.  *
  5.  * (c) Nelmio <hello@nelm.io>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Nelmio\CorsBundle\EventListener;
  11. use Nelmio\CorsBundle\Options\ResolverInterface;
  12. use Psr\Log\LoggerInterface;
  13. use Psr\Log\NullLogger;
  14. use Symfony\Component\HttpFoundation\Request;
  15. use Symfony\Component\HttpFoundation\Response;
  16. use Symfony\Component\HttpKernel\Event\RequestEvent;
  17. use Symfony\Component\HttpKernel\Event\ResponseEvent;
  18. use Symfony\Component\HttpKernel\HttpKernelInterface;
  19. /**
  20.  * Adds CORS headers and handles pre-flight requests
  21.  *
  22.  * @author Jordi Boggiano <j.boggiano@seld.be>
  23.  */
  24. class CorsListener
  25. {
  26.     const SHOULD_ALLOW_ORIGIN_ATTR '_nelmio_cors_should_allow_origin';
  27.     const SHOULD_FORCE_ORIGIN_ATTR '_nelmio_cors_should_force_origin';
  28.     /**
  29.      * Simple headers as defined in the spec should always be accepted
  30.      */
  31.     protected static $simpleHeaders = [
  32.         'accept',
  33.         'accept-language',
  34.         'content-language',
  35.         'origin',
  36.     ];
  37.     /** @var ResolverInterface */
  38.     protected $configurationResolver;
  39.     /** @var LoggerInterface */
  40.     private $logger;
  41.     public function __construct(ResolverInterface $configurationResolver, ?LoggerInterface $logger null)
  42.     {
  43.         $this->configurationResolver $configurationResolver;
  44.         if (null === $logger) {
  45.             $logger = new NullLogger();
  46.         }
  47.         $this->logger $logger;
  48.     }
  49.     public function onKernelRequest(RequestEvent $event): void
  50.     {
  51.         if (HttpKernelInterface::MAIN_REQUEST !== $event->getRequestType()) {
  52.             $this->logger->debug('Not a master type request, skipping CORS checks.');
  53.             return;
  54.         }
  55.         $request $event->getRequest();
  56.         if (!$options $this->configurationResolver->getOptions($request)) {
  57.             $this->logger->debug('Could not get options for request, skipping CORS checks.');
  58.             return;
  59.         }
  60.         // if the "forced_allow_origin_value" option is set, add a listener which will set or override the "Access-Control-Allow-Origin" header
  61.         if (!empty($options['forced_allow_origin_value'])) {
  62.             $this->logger->debug(sprintf(
  63.                 "The 'forced_allow_origin_value' option is set to '%s', adding a listener to set or override the 'Access-Control-Allow-Origin' header.",
  64.                 $options['forced_allow_origin_value']
  65.             ));
  66.             $request->attributes->set(self::SHOULD_FORCE_ORIGIN_ATTRtrue);
  67.         }
  68.         // skip if not a CORS request
  69.         if (!$request->headers->has('Origin')) {
  70.             $this->logger->debug("Request does not have 'Origin' header, skipping CORS.");
  71.             return;
  72.         }
  73.         if ($options['skip_same_as_origin'] && $request->headers->get('Origin') === $request->getSchemeAndHttpHost()) {
  74.             $this->logger->debug("The 'Origin' header of the request equals the scheme and host the request was sent to, skipping CORS.");
  75.             return;
  76.         }
  77.         // perform preflight checks
  78.         if ('OPTIONS' === $request->getMethod() &&
  79.             ($request->headers->has('Access-Control-Request-Method') ||
  80.                 $request->headers->has('Access-Control-Request-Private-Network'))
  81.         ) {
  82.             $this->logger->debug("Request is a preflight check, setting event response now.");
  83.             $event->setResponse($this->getPreflightResponse($request$options));
  84.             return;
  85.         }
  86.         if (!$this->checkOrigin($request$options)) {
  87.             $this->logger->debug("Origin check failed.");
  88.             return;
  89.         }
  90.         $this->logger->debug("Origin is allowed, proceed with adding CORS response headers.");
  91.         $request->attributes->set(self::SHOULD_ALLOW_ORIGIN_ATTRtrue);
  92.     }
  93.     public function onKernelResponse(ResponseEvent $event): void
  94.     {
  95.         if (HttpKernelInterface::MAIN_REQUEST !== $event->getRequestType()) {
  96.             $this->logger->debug("Not a master type request, skip adding CORS response headers.");
  97.             return;
  98.         }
  99.         $request $event->getRequest();
  100.         $shouldAllowOrigin $request->attributes->getBoolean(self::SHOULD_ALLOW_ORIGIN_ATTR);
  101.         $shouldForceOrigin $request->attributes->getBoolean(self::SHOULD_FORCE_ORIGIN_ATTR);
  102.         if (!$shouldAllowOrigin && !$shouldForceOrigin) {
  103.             $this->logger->debug("The origin should not be allowed and not be forced, skip adding CORS response headers.");
  104.             return;
  105.         }
  106.         if (!$options $this->configurationResolver->getOptions($request)) {
  107.             $this->logger->debug("Could not resolve options for request, skip adding CORS response headers.");
  108.             return;
  109.         }
  110.         if ($shouldAllowOrigin) {
  111.             $response $event->getResponse();
  112.             // add CORS response headers
  113.             $origin $request->headers->get('Origin');
  114.             $this->logger->debug(sprintf("Setting 'Access-Control-Allow-Origin' response header to '%s'."$origin));
  115.             $response->headers->set('Access-Control-Allow-Origin'$origin);
  116.             if ($options['allow_credentials']) {
  117.                 $this->logger->debug("Setting 'Access-Control-Allow-Credentials' to 'true'.");
  118.                 $response->headers->set('Access-Control-Allow-Credentials''true');
  119.             }
  120.             if ($options['expose_headers']) {
  121.                 $headers strtolower(implode(', '$options['expose_headers']));
  122.                 $this->logger->debug(sprintf("Setting 'Access-Control-Expose-Headers' response header to '%s'."$headers));
  123.                 $response->headers->set('Access-Control-Expose-Headers'$headers);
  124.             }
  125.         }
  126.         if ($shouldForceOrigin) {
  127.             $this->logger->debug(sprintf("Setting 'Access-Control-Allow-Origin' response header to '%s'."$options['forced_allow_origin_value']));
  128.             $event->getResponse()->headers->set('Access-Control-Allow-Origin'$options['forced_allow_origin_value']);
  129.         }
  130.     }
  131.     protected function getPreflightResponse(Request $request, array $options): Response
  132.     {
  133.         $response = new Response();
  134.         $response->setVary(['Origin']);
  135.         if ($options['allow_credentials']) {
  136.             $this->logger->debug("Setting 'Access-Control-Allow-Credentials' response header to 'true'.");
  137.             $response->headers->set('Access-Control-Allow-Credentials''true');
  138.         }
  139.         if ($options['allow_methods']) {
  140.             $methods implode(', '$options['allow_methods']);
  141.             $this->logger->debug(sprintf("Setting 'Access-Control-Allow-Methods' response header to '%s'."$methods));
  142.             $response->headers->set('Access-Control-Allow-Methods'$methods);
  143.         }
  144.         if ($options['allow_headers']) {
  145.             $headers $this->isWildcard($options'allow_headers')
  146.                 ? $request->headers->get('Access-Control-Request-Headers')
  147.                 : implode(', '$options['allow_headers']);
  148.             if ($headers) {
  149.                 $this->logger->debug(sprintf("Setting 'Access-Control-Allow-Headers' response header to '%s'."$headers));
  150.                 $response->headers->set('Access-Control-Allow-Headers'$headers);
  151.             }
  152.         }
  153.         if ($options['max_age']) {
  154.             $this->logger->debug(sprintf("Setting 'Access-Control-Max-Age' response header to '%d'."$options['max_age']));
  155.             $response->headers->set('Access-Control-Max-Age'$options['max_age']);
  156.         }
  157.         if (!$this->checkOrigin($request$options)) {
  158.             $this->logger->debug("Removing 'Access-Control-Allow-Origin' response header.");
  159.             $response->headers->remove('Access-Control-Allow-Origin');
  160.             return $response;
  161.         }
  162.         $origin $request->headers->get('Origin');
  163.         $this->logger->debug(sprintf("Setting 'Access-Control-Allow-Origin' response header to '%s'"$origin));
  164.         $response->headers->set('Access-Control-Allow-Origin'$origin);
  165.         // check private network access
  166.         if ($request->headers->has('Access-Control-Request-Private-Network')
  167.             && strtolower($request->headers->get('Access-Control-Request-Private-Network')) === 'true'
  168.         ) {
  169.             if ($options['allow_private_network']) {
  170.                 $this->logger->debug("Setting 'Access-Control-Allow-Private-Network' response header to 'true'.");
  171.                 $response->headers->set('Access-Control-Allow-Private-Network''true');
  172.             } else {
  173.                 $response->setStatusCode(400);
  174.                 $response->setContent('Private Network Access is not allowed.');
  175.             }
  176.         }
  177.         // check request method
  178.         $method strtoupper($request->headers->get('Access-Control-Request-Method'));
  179.         if (!in_array($method$options['allow_methods'], true)) {
  180.             $this->logger->debug(sprintf("Method '%s' is not allowed."$method));
  181.             $response->setStatusCode(405);
  182.             return $response;
  183.         }
  184.         /**
  185.          * We have to allow the header in the case-set as we received it by the client.
  186.          * Firefox f.e. sends the LINK method as "Link", and we have to allow it like this or the browser will deny the
  187.          * request.
  188.          */
  189.         if (!in_array($request->headers->get('Access-Control-Request-Method'), $options['allow_methods'], true)) {
  190.             $options['allow_methods'][] = $request->headers->get('Access-Control-Request-Method');
  191.             $response->headers->set('Access-Control-Allow-Methods'implode(', '$options['allow_methods']));
  192.         }
  193.         // check request headers
  194.         $headers $request->headers->get('Access-Control-Request-Headers');
  195.         if ($headers && !$this->isWildcard($options'allow_headers')) {
  196.             $headers strtolower(trim($headers));
  197.             foreach (preg_split('{, *}'$headers) as $header) {
  198.                 if (in_array($headerself::$simpleHeaderstrue)) {
  199.                     continue;
  200.                 }
  201.                 if (!in_array($header$options['allow_headers'], true)) {
  202.                     $sanitizedMessage htmlentities('Unauthorized header '.$headerENT_QUOTES'UTF-8');
  203.                     $response->setStatusCode(400);
  204.                     $response->setContent($sanitizedMessage);
  205.                     break;
  206.                 }
  207.             }
  208.         }
  209.         return $response;
  210.     }
  211.     protected function checkOrigin(Request $request, array $options): bool
  212.     {
  213.         // check origin
  214.         $origin $request->headers->get('Origin');
  215.         if ($this->isWildcard($options'allow_origin')) {
  216.             return true;
  217.         }
  218.         if ($options['origin_regex'] === true) {
  219.             // origin regex matching
  220.             foreach ($options['allow_origin'] as $originRegexp) {
  221.                 $this->logger->debug(sprintf("Matching origin regex '%s' to origin '%s'."$originRegexp$origin));
  222.                 if (preg_match('{'.$originRegexp.'}i'$origin)) {
  223.                     $this->logger->debug(sprintf("Origin regex '%s' matches origin '%s'."$originRegexp$origin));
  224.                     return true;
  225.                 }
  226.             }
  227.         } else {
  228.             // old origin matching
  229.             if (in_array($origin$options['allow_origin'])) {
  230.                 $this->logger->debug(sprintf("Origin '%s' is allowed."$origin));
  231.                 return true;
  232.             }
  233.         }
  234.         $this->logger->debug(sprintf("Origin '%s' is not allowed."$origin));
  235.         return false;
  236.     }
  237.     private function isWildcard(array $optionsstring $option): bool
  238.     {
  239.         $result $options[$option] === true || (is_array($options[$option]) && in_array('*'$options[$option]));
  240.         $this->logger->debug(sprintf("Option '%s' is %s a wildcard."$option$result '' 'not'));
  241.         return $result;
  242.     }
  243. }