This commit is contained in:
Shohrat 2023-10-11 14:43:55 +05:00
parent 679bbb1d51
commit 16aed5f193
18 changed files with 818 additions and 0 deletions

1
plugins/offline/cors/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/vendor/

View File

@ -0,0 +1,7 @@
# How to contribute
Contributions to this project are highly welcome.
1. Submit your pull requests to the `develop` branch
1. Adhere to the [PSR-2 coding](http://www.php-fig.org/psr/psr-2/) standard
1. If you are not sure if your ideas are fit for this project, create an issue and ask

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 OFFLINE GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,49 @@
<?php namespace OFFLINE\CORS;
use OFFLINE\CORS\Classes\HandleCors;
use OFFLINE\CORS\Classes\HandlePreflight;
use OFFLINE\CORS\Classes\ServiceProvider;
use OFFLINE\CORS\Models\Settings;
use System\Classes\PluginBase;
class Plugin extends PluginBase
{
public function boot()
{
\App::register(ServiceProvider::class);
$this->app['Illuminate\Contracts\Http\Kernel']
->prependMiddleware(HandleCors::class);
if (request()->isMethod('OPTIONS')) {
$this->app['Illuminate\Contracts\Http\Kernel']
->prependMiddleware(HandlePreflight::class);
}
}
public function registerPermissions()
{
return [
'offline.cors.manage' => [
'label' => 'Can manage cors settings',
'tab' => 'CORS',
],
];
}
public function registerSettings()
{
return [
'cors' => [
'label' => 'CORS-Settings',
'description' => 'Manage CORS headers',
'category' => 'system::lang.system.categories.cms',
'icon' => 'icon-code',
'class' => Settings::class,
'order' => 500,
'keywords' => 'cors',
'permissions' => ['offline.cors.manage'],
],
];
}
}

View File

@ -0,0 +1,45 @@
# CORS plugin for October CMS
This plugin is based on [https://github.com/barryvdh/laravel-cors](https://github.com/barryvdh/laravel-cors/blob/master/config/cors.php).
All configuration for the plugin can be done via the backend settings.
The following cors headers are supported:
* Access-Control-Allow-Origin
* Access-Control-Allow-Headers
* Access-Control-Allow-Methods
* Access-Control-Allow-Credentials
* Access-Control-Expose-Headers
* Access-Control-Max-Age
Currently these headers are sent for every request. There is no per-route configuration possible at this time.
## Setup
After installing the plugin visit the CORS settings page in your October CMS backend settings.
You can add `*` as an entry to `Allowed origins`, `Allowed headers` and `Allowed methods` to allow any kind of CORS request from everywhere.
It is advised to be more explicit about these settings. You can add values for each header via the repeater fields.
> It is important to set these intial settings once for the plugin to work as excpected!
### Filesystem configuration
As an alternative to the backend settings you can create a `config/config.php` file in the plugins root directory to configure it.
The filesystem configuration will overwrite any defined backend setting.
```php
<?php
// plugins/offline/cors/config/config.php
return [
'supportsCredentials' => true,
'maxAge' => 3600,
'allowedOrigins' => ['*'],
'allowedHeaders' => ['*'],
'allowedMethods' => ['GET', 'POST'],
'exposedHeaders' => [''],
];
```

View File

@ -0,0 +1,71 @@
<?php
namespace OFFLINE\CORS\Classes;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* Based on asm89/stack-cors
*/
class Cors implements HttpKernelInterface
{
/**
* @var \Symfony\Component\HttpKernel\HttpKernelInterface
*/
private $app;
/**
* @var CorsService
*/
private $cors;
private $defaultOptions = [
'allowedHeaders' => [],
'allowedMethods' => [],
'allowedOrigins' => [],
'exposedHeaders' => false,
'maxAge' => false,
'supportsCredentials' => false,
];
/**
* Cors constructor.
*
* @param HttpKernelInterface $app
* @param array $options
*/
public function __construct(HttpKernelInterface $app, array $options = [])
{
$this->app = $app;
$this->cors = new CorsService(array_merge($this->defaultOptions, $options));
}
/**
* @param Request $request
* @param int $type
* @param bool $catch
*
* @return bool|Response
*/
public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
{
if ( ! $this->cors->isCorsRequest($request)) {
return $this->app->handle($request, $type, $catch);
}
if ($this->cors->isPreflightRequest($request)) {
return $this->cors->handlePreflightRequest($request);
}
if ( ! $this->cors->isActualRequestAllowed($request)) {
return new Response('Not allowed.', 403);
}
$response = $this->app->handle($request, $type, $catch);
return $this->cors->addActualRequestHeaders($response, $request);
}
}

View File

@ -0,0 +1,199 @@
<?php
namespace OFFLINE\CORS\Classes;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Based on asm89/stack-cors
*/
class CorsService
{
private $options;
public function __construct(array $options = [])
{
$this->options = $this->normalizeOptions($options);
}
private function normalizeOptions(array $options = [])
{
$options += [
'supportsCredentials' => false,
'maxAge' => 0,
];
// Make sure these values are arrays, if not specified in the backend settings.
$arrayKeys = [
'allowedOrigins',
'allowedHeaders',
'exposedHeaders',
'allowedMethods',
];
foreach ($arrayKeys as $key) {
if (!$options[$key]) {
$options[$key] = [];
}
}
// normalize array('*') to true
if (in_array('*', $options['allowedOrigins'])) {
$options['allowedOrigins'] = true;
}
if (in_array('*', $options['allowedHeaders'])) {
$options['allowedHeaders'] = true;
} else {
$options['allowedHeaders'] = array_map('strtolower', $options['allowedHeaders']);
}
if (in_array('*', $options['allowedMethods'])) {
$options['allowedMethods'] = true;
} else {
$options['allowedMethods'] = array_map('strtoupper', $options['allowedMethods']);
}
return $options;
}
public function isActualRequestAllowed(Request $request)
{
return $this->checkOrigin($request);
}
public function isCorsRequest(Request $request)
{
return $request->headers->has('Origin') && $request->headers->get('Origin') !== $request->getSchemeAndHttpHost();
}
public function isPreflightRequest(Request $request)
{
return $this->isCorsRequest($request)
&& $request->getMethod() === 'OPTIONS'
&& $request->headers->has('Access-Control-Request-Method');
}
public function addActualRequestHeaders(Response $response, Request $request)
{
if ( ! $this->checkOrigin($request)) {
return $response;
}
$response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin'));
if ( ! $response->headers->has('Vary')) {
$response->headers->set('Vary', 'Origin');
} else {
$response->headers->set('Vary', $response->headers->get('Vary') . ', Origin');
}
if ($this->options['supportsCredentials']) {
$response->headers->set('Access-Control-Allow-Credentials', 'true');
}
if ($this->options['exposedHeaders']) {
$response->headers->set('Access-Control-Expose-Headers', implode(', ', $this->options['exposedHeaders']));
}
return $response;
}
public function handlePreflightRequest(Request $request)
{
if (true !== $check = $this->checkPreflightRequestConditions($request)) {
return $check;
}
return $this->buildPreflightCheckResponse($request);
}
private function buildPreflightCheckResponse(Request $request)
{
$response = new Response();
if ($this->options['supportsCredentials']) {
$response->headers->set('Access-Control-Allow-Credentials', 'true');
}
$response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin'));
if ($this->options['maxAge']) {
$response->headers->set('Access-Control-Max-Age', $this->options['maxAge']);
}
$allowMethods = $this->options['allowedMethods'] === true
? strtoupper($request->headers->get('Access-Control-Request-Method'))
: implode(', ', $this->options['allowedMethods']);
$response->headers->set('Access-Control-Allow-Methods', $allowMethods);
$allowHeaders = $this->options['allowedHeaders'] === true
? strtoupper($request->headers->get('Access-Control-Request-Headers'))
: implode(', ', $this->options['allowedHeaders']);
$response->headers->set('Access-Control-Allow-Headers', $allowHeaders);
return $response;
}
private function checkPreflightRequestConditions(Request $request)
{
if ( ! $this->checkOrigin($request)) {
return $this->createBadRequestResponse(403, 'Origin not allowed');
}
if ( ! $this->checkMethod($request)) {
return $this->createBadRequestResponse(405, 'Method not allowed');
}
$requestHeaders = [];
// if allowedHeaders has been set to true ('*' allow all flag) just skip this check
if ($this->options['allowedHeaders'] !== true && $request->headers->has('Access-Control-Request-Headers')) {
$headers = strtolower($request->headers->get('Access-Control-Request-Headers'));
$requestHeaders = explode(',', $headers);
foreach ($requestHeaders as $header) {
if ( ! in_array(trim($header), $this->options['allowedHeaders'])) {
return $this->createBadRequestResponse(403, 'Header not allowed');
}
}
}
return true;
}
private function createBadRequestResponse($code, $reason = '')
{
return new Response($reason, $code);
}
private function checkOrigin(Request $request)
{
if ($this->options['allowedOrigins'] === true) {
// allow all '*' flag
return true;
}
$origin = $request->headers->get('Origin');
foreach ($this->options['allowedOrigins'] as $allowedOrigin) {
if (OriginMatcher::matches($allowedOrigin, $origin)) {
return true;
}
}
return false;
}
private function checkMethod(Request $request)
{
if ($this->options['allowedMethods'] === true) {
// allow all '*' flag
return true;
}
$requestMethod = strtoupper($request->headers->get('Access-Control-Request-Method'));
return in_array($requestMethod, $this->options['allowedMethods']);
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace OFFLINE\CORS\Classes;
use Closure;
class HandleCors
{
/**
* The CORS service
*
* @var CorsService
*/
protected $cors;
/**
* @param CorsService $cors
*/
public function __construct(CorsService $cors)
{
$this->cors = $cors;
}
/**
* Handle an incoming request. Based on Asm89\Stack\Cors by asm89
* @see https://github.com/asm89/stack-cors/blob/master/src/Asm89/Stack/Cors.php
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle($request, Closure $next)
{
if ( ! $this->cors->isCorsRequest($request)) {
return $next($request);
}
if ( ! $this->cors->isActualRequestAllowed($request)) {
abort(403);
}
/** @var \Illuminate\Http\Response $response */
$response = $next($request);
return $this->cors->addActualRequestHeaders($response, $request);
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace OFFLINE\CORS\Classes;
use Closure;
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Routing\Router;
class HandlePreflight
{
/**
* @param CorsService $cors
*/
public function __construct(CorsService $cors, Router $router, Kernel $kernel)
{
$this->cors = $cors;
$this->router = $router;
$this->kernel = $kernel;
}
/**
* Handle an incoming OPTIONS request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle($request, Closure $next)
{
$response = $next($request);
if ($this->cors->isPreflightRequest($request) && $this->hasMatchingCorsRoute($request)) {
$preflight = $this->cors->handlePreflightRequest($request);
$response->headers->add($preflight->headers->all());
}
$response->setStatusCode(204);
return $response;
}
/**
* Verify the current OPTIONS request matches a CORS-enabled route
*
* @param \Illuminate\Http\Request $request
*
* @return boolean
*/
private function hasMatchingCorsRoute($request)
{
// Check if CORS is added in a global middleware
if ($this->kernel->hasMiddleware(HandleCors::class)) {
return true;
}
// Check if CORS is added as a route middleware
$request = clone $request;
$request->setMethod($request->header('Access-Control-Request-Method'));
try {
$route = $this->router->getRoutes()->match($request);
// change of method name in laravel 5.3
if (method_exists($this->router, 'gatherRouteMiddleware')) {
$middleware = $this->router->gatherRouteMiddleware($route);
} else {
$middleware = $this->router->gatherRouteMiddlewares($route);
}
return in_array(HandleCors::class, $middleware);
} catch (\Exception $e) {
app('log')->error($e);
return false;
}
}
}

View File

@ -0,0 +1,100 @@
<?php
namespace OFFLINE\CORS\Classes;
class OriginMatcher
{
public static function matches($pattern, $origin)
{
if ($pattern === $origin) {
return true;
}
$scheme = parse_url($origin, PHP_URL_SCHEME);
$host = parse_url($origin, PHP_URL_HOST);
$port = parse_url($origin, PHP_URL_PORT);
$schemePattern = static::parseOriginPattern($pattern, PHP_URL_SCHEME);
$hostPattern = static::parseOriginPattern($pattern, PHP_URL_HOST);
$portPattern = static::parseOriginPattern($pattern, PHP_URL_PORT);
$schemeMatches = static::schemeMatches($schemePattern, $scheme);
$hostMatches = static::hostMatches($hostPattern, $host);
$portMatches = static::portMatches($portPattern, $port);
return $schemeMatches && $hostMatches && $portMatches;
}
public static function schemeMatches($pattern, $scheme)
{
return is_null($pattern) || $pattern === $scheme;
}
public static function hostMatches($pattern, $host)
{
$patternComponents = array_reverse(explode('.', $pattern));
$hostComponents = array_reverse(explode('.', $host));
foreach ($patternComponents as $index => $patternComponent) {
if ($patternComponent === '*') {
return true;
}
if ( ! isset($hostComponents[$index])) {
return false;
}
if ($hostComponents[$index] !== $patternComponent) {
return false;
}
}
return count($patternComponents) === count($hostComponents);
}
public static function portMatches($pattern, $port)
{
if ($pattern === "*") {
return true;
}
if ((string)$pattern === "") {
return (string)$port === "";
}
if (preg_match('/\A\d+\z/', $pattern)) {
return (string)$pattern === (string)$port;
}
if (preg_match('/\A(?P<from>\d+)-(?P<to>\d+)\z/', $pattern, $captured)) {
return $captured['from'] <= $port && $port <= $captured['to'];
}
throw new \InvalidArgumentException("Invalid port pattern: ${pattern}");
}
public static function parseOriginPattern($originPattern, $component = -1)
{
$matched = preg_match(
'!\A
(?: (?P<scheme> ([a-z][a-z0-9+\-.]*) ):// )?
(?P<host> (?:\*|[\w-]+)(?:\.[\w-]+)* )
(?: :(?P<port> (?: \*|\d+(?:-\d+)? ) ) )?
\z!x',
$originPattern,
$captured
);
if ( ! $matched) {
throw new \InvalidArgumentException("Invalid origin pattern ${originPattern}");
}
$components = [
'scheme' => $captured['scheme'] ?: null,
'host' => $captured['host'],
'port' => array_key_exists('port', $captured) ? $captured['port'] : null,
];
switch ($component) {
case -1:
return $components;
case PHP_URL_SCHEME:
return $components['scheme'];
case PHP_URL_HOST:
return $components['host'];
case PHP_URL_PORT:
return $components['port'];
}
throw new \InvalidArgumentException("Invalid component: ${component}");
}
}

View File

@ -0,0 +1,90 @@
<?php
namespace OFFLINE\CORS\Classes;
use October\Rain\Support\ServiceProvider as BaseServiceProvider;
use OFFLINE\CORS\Models\Settings;
class ServiceProvider extends BaseServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = false;
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->app->singleton(CorsService::class, function ($app) {
return new CorsService($this->getSettings());
});
}
/**
* Return default Settings
*/
protected function getSettings()
{
$supportsCredentials = (bool)$this->getConfigValue('supportsCredentials', false);
$maxAge = (int)$this->getConfigValue('maxAge', 0);
$allowedOrigins = $this->getConfigValue('allowedOrigins', []);
$allowedHeaders = $this->getConfigValue('allowedHeaders', []);
$allowedMethods = $this->getConfigValue('allowedMethods', []);
$exposedHeaders = $this->getConfigValue('exposedHeaders', []);
return compact(
'supportsCredentials',
'allowedOrigins',
'allowedHeaders',
'allowedMethods',
'exposedHeaders',
'maxAge'
);
}
/**
* Returns an effective config value.
*
* If a filesystem config is available it takes precedence
* over the backend settings values.
*
* @param $key
* @param null $default
*
* @return mixed
*/
public function getConfigValue($key, $default = null)
{
return $this->filesystemConfig($key) ?: $this->getValues(Settings::get($key, $default));
}
/**
* Return the filesystem config value if available.
*
* @param string $key
*
* @return mixed
*/
public function filesystemConfig($key)
{
return config('offline.cors::' . $key);
}
/**
* Extract the repeater field values.
*
* @param mixed $values
*
* @return array
*/
protected function getValues($values)
{
return \is_array($values) ? collect($values)->pluck('value')->toArray() : $values;
}
}

View File

@ -0,0 +1,13 @@
{
"name": "offline/oc-cors-plugin",
"description": "Setup and manage Cross-Origin Resource Sharing headers in October CMS",
"type": "october-plugin",
"license": "MIT",
"authors": [
{
"name": "Tobias Kündig",
"email": "tobias@offline.swiss"
}
],
"require": {}
}

View File

@ -0,0 +1,6 @@
<?php return [
'plugin' => [
'name' => 'CORS',
'description' => 'Verwalte Cross-Origin Resource Sharing Header in October CMS',
],
];

View File

@ -0,0 +1,6 @@
<?php return [
'plugin' => [
'name' => 'CORS',
'description' => 'Setup and manage Cross-Origin Resource Sharing headers',
],
];

View File

@ -0,0 +1,12 @@
<?php
namespace OFFLINE\CORS\Models;
use Model;
class Settings extends Model
{
public $implement = ['System.Behaviors.SettingsModel'];
public $settingsCode = 'offline_cors_settings';
public $settingsFields = 'fields.yaml';
}

View File

@ -0,0 +1,55 @@
fields:
supportsCredentials:
label: Supports credentials
type: switch
comment: 'Set Access-Control-Allow-Credentials header to true'
default: false
span: left
maxAge:
label: Max age
type: number
comment: 'Set Access-Control-Max-Age to this value'
default: 0
span: right
tabs:
fields:
allowedOrigins:
label: Allowed origins
tab: Allowed origins
type: repeater
span: left
form:
fields:
value:
type: text
label: Origin
allowedHeaders:
label: Allowed headers
tab: Allowed headers
type: repeater
span: left
form:
fields:
value:
type: text
label: Header
allowedMethods:
label: Allowed methods
tab: Allowed methods
type: repeater
span: left
form:
fields:
value:
type: text
label: Method
exposedHeaders:
label: Exposed headers
tab: Exposed headers
type: repeater
span: left
form:
fields:
value:
type: text
label: Header

View File

@ -0,0 +1,6 @@
plugin:
name: 'offline.cors::lang.plugin.name'
description: 'offline.cors::lang.plugin.description'
author: 'OFFLINE GmbH'
icon: oc-icon-code
homepage: ''

View File

@ -0,0 +1,14 @@
1.0.1:
- Initial release.
1.0.2:
- Fixed backend settings label (thanks to LukeTowers)
1.0.3:
- Added support for filesystem configuration file / Added plugin to Packagist (https://packagist.org/packages/offline/oc-cors-plugin)
1.0.4:
- Fixed minor bug when running the plugin without custom settings
1.0.5:
- "Return proper 204 response code for preflight requests (thanks to @adrian-marinescu-ch on GitHub)"
1.0.6:
- "Dummy release to sync with Packagist version"
1.0.7:
- "Optimized compatibility with October v2"