SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Helpers/Cors.php

  1. <?php
  2. /*
  3.  * Copyright (C) 2017 Karmabunny Pty Ltd.
  4.  *
  5.  * This file is a part of SproutCMS.
  6.  *
  7.  * SproutCMS is free software: you can redistribute it and/or modify it under the terms
  8.  * of the GNU General Public License as published by the Free Software Foundation, either
  9.  * version 2 of the License, or (at your option) any later version.
  10.  *
  11.  * For more information, visit <http://getsproutcms.com>.
  12.  */
  13.  
  14. namespace Sprout\Helpers;
  15.  
  16. use Kohana;
  17. use Kohana_Exception;
  18.  
  19. /**
  20.  * Cross Origin Resource Sharing.
  21.  *
  22.  * TODO conditional safe headers: content-type, accept, accept-language, content-language
  23.  * TODO conditional safe is unsafe: >128 characters
  24.  * TODO restricting origins
  25.  * TODO support for max-age, caching 'options' requests
  26.  *
  27.  */
  28. class Cors
  29. {
  30.  
  31. // Default (limited) config for handling CORS.
  32. const DEFAULT_CONFIG = [
  33. 'origins' => ['*'],
  34. 'methods' => [
  35. 'get',
  36. ],
  37. 'headers' => [
  38. 'accept',
  39. 'accept-language',
  40. 'content-type',
  41. 'x-requested-with',
  42. ],
  43. 'allow_credentials' => false,
  44. ];
  45.  
  46. // https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header
  47. const SAFE_HEADERS = [
  48. // Browser headers.
  49. 'if-modified-since',
  50. 'upgrade-insecure-requests',
  51. 'accept-encoding',
  52. 'connection',
  53. 'user-agent',
  54. 'host',
  55. 'referer',
  56.  
  57. // CORS safe headers.
  58. 'cache-control',
  59. 'content-language',
  60. 'content-length',
  61. 'content-type',
  62. 'expires',
  63. 'last-modified',
  64. 'pragma',
  65.  
  66. // CORS headers.
  67. 'origin',
  68. 'access-control-request-headers',
  69. 'access-control-request-method',
  70. ];
  71.  
  72.  
  73. /**
  74.   * Handling a CORS request.
  75.   *
  76.   * This is a work-in-progress. CORS is a big thing so there's some edge
  77.   * cases that this doesn't yet handle.
  78.   *
  79.   * @param array $config [ headers, methods ]
  80.   * @return void exits if invalid or pre-flight
  81.   * @throws Kohana_Exception
  82.   */
  83. public static function handleCors($config = [])
  84. {
  85. $origin = @$_SERVER['HTTP_ORIGIN'] ?: '';
  86.  
  87. // Skip everything if we don't have a origin.
  88. // CORS is purely a browser protection and doesn't extend to
  89. // non-browser API calls or modified or out-of-date browsers.
  90. if (!$origin) return;
  91.  
  92. $config = array_merge(self::DEFAULT_CONFIG, $config);
  93.  
  94. if (@$config['allow_credentials']) {
  95. $config['headers'][] = 'authorization';
  96. $config['headers'][] = 'cookie';
  97. }
  98.  
  99. $headers = Request::getHeaders();
  100. $method = Request::method();
  101.  
  102. $errors = [];
  103.  
  104. // Preflight checks.
  105. if ($method === 'options') {
  106. $method = strtolower(@$headers['access-control-request-method'] ?: '');
  107. $headers = explode(',', @$headers['access-control-request-headers'] ?: '');
  108.  
  109. // Tidy up.
  110. foreach ($headers as &$header) {
  111. $header = strtolower(trim($header));
  112. }
  113. unset($header);
  114. } else {
  115. // Clear out safe headers before validation.
  116. foreach (self::SAFE_HEADERS as $name) {
  117. unset($headers[$name]);
  118. }
  119. unset($name);
  120. $headers = array_keys($headers);
  121. }
  122.  
  123. // TODO Validate origins here.
  124. // $errors[] = 'bad origin';
  125.  
  126. // Validate permitted headers.
  127. if (count(array_intersect($config['headers'], $headers)) !== count($headers)) {
  128. $errors[] = 'bad headers';
  129. }
  130.  
  131. // Validate permitted methods.
  132. if (!in_array($method, $config['methods'])) {
  133. $errors[] = 'bad method';
  134. }
  135.  
  136. // Toss it and quit on errors.
  137. if ($errors) {
  138. Kohana::closeBuffers(false);
  139. http_response_code(400);
  140.  
  141. // Some debugging info in the response headers.
  142. // Browsers don't like to show the contents on a bad CORS request.
  143. if (!IN_PRODUCTION) {
  144. header('x-debug-config: ' . json_encode($config));
  145. header('x-debug-headers: ' . implode(',', $headers));
  146. header('x-debug-method: ' . $method);
  147. header('x-debug-origin: ' . $origin);
  148. }
  149.  
  150. }
  151.  
  152. header('Access-Control-Allow-Origin: ' . $origin);
  153. header('Access-Control-Allow-Headers: ' . implode(',', $config['headers']));
  154. header('Access-Control-Allow-Methods: ' . implode(',', $config['methods']));
  155. header('Vary: origin,access-control-request-headers,access-control-request-method');
  156.  
  157. if (@$config['allow_credentials']) {
  158. header('Access-Control-Allow-Credentials: true');
  159. }
  160.  
  161. // An options request stops here, sends 'no content'.
  162. $method = Request::method();
  163.  
  164. if ($method === 'options') {
  165. Kohana::closeBuffers(false);
  166. http_response_code(204);
  167. }
  168. }
  169. }
  170.  
  171.  
  172.