[php-slim4] Add lazy CORS implementation (#11941)

* Add lazy CORS implementation

While Slim4 doc applies CORS headers via middleware but their code team
member recommends to use custom response emitter.

Ref: https://github.com/slimphp/Slim/issues/2999#issuecomment-1066839414

* Refresh samples
This commit is contained in:
Yuriy Belenko
2022-03-23 11:46:23 +03:00
committed by GitHub
parent 61245fc52b
commit 7b59e602ed
16 changed files with 420 additions and 6 deletions

View File

@@ -225,6 +225,7 @@ public class PhpSlim4ServerCodegen extends AbstractPhpCodegen {
supportingFiles.add(new SupportingFile("register_dependencies.mustache", toSrcPath(appPackage, srcBasePath), "RegisterDependencies.php"));
supportingFiles.add(new SupportingFile("register_middlewares.mustache", toSrcPath(appPackage, srcBasePath), "RegisterMiddlewares.php"));
supportingFiles.add(new SupportingFile("register_routes.mustache", toSrcPath(appPackage, srcBasePath), "RegisterRoutes.php"));
supportingFiles.add(new SupportingFile("response_emitter.mustache", toSrcPath(appPackage, srcBasePath), "ResponseEmitter.php"));
// don't generate phpunit config when tests generation disabled
if (Boolean.TRUE.equals(generateApiTests) || Boolean.TRUE.equals(generateModelTests)) {

View File

@@ -17,6 +17,7 @@
{{#isZendDiactoros}}
"laminas/laminas-diactoros": "^2.3.0",
{{/isZendDiactoros}}
"neomerx/cors-psr7": "^2.0",
{{#isNyholmPsr7}}
"nyholm/psr7": "^1.3.0",
"nyholm/psr7-server": "^0.4.1",

View File

@@ -32,6 +32,29 @@ return [
// Doesn't do anything when 'logErrors' is false.
'slim.logErrorDetails' => false,
// CORS settings
// @see https://github.com/neomerx/cors-psr7/blob/master/src/Strategies/Settings.php
'cors.settings' => [
isset($_SERVER['HTTPS']) ? 'https' : 'http', // serverOriginScheme
$_SERVER['SERVER_NAME'], // serverOriginHost
null, // serverOriginPort
false, // isPreFlightCanBeCached
0, // preFlightCacheMaxAge
false, // isForceAddMethods
false, // isForceAddHeaders
true, // isUseCredentials
true, // areAllOriginsAllowed
[], // allowedOrigins
true, // areAllMethodsAllowed
[], // allowedLcMethods
'GET, POST, PUT, PATCH, DELETE', // allowedMethodsList
true, // areAllHeadersAllowed
[], // allowedLcHeaders
'authorization, content-type, x-requested-with', // allowedHeadersList
'', // exposedHeadersList
true, // isCheckHost
],
// PDO
'pdo.dsn' => 'mysql:host=localhost;charset=utf8mb4',
'pdo.username' => 'root',

View File

@@ -32,6 +32,29 @@ return [
// Doesn't do anything when 'logErrors' is false.
'slim.logErrorDetails' => true,
// CORS settings
// https://github.com/neomerx/cors-psr7/blob/master/src/Strategies/Settings.php
'cors.settings' => [
isset($_SERVER['HTTPS']) ? 'https' : 'http', // serverOriginScheme
$_SERVER['SERVER_NAME'], // serverOriginHost
null, // serverOriginPort
true, // isPreFlightCanBeCached
86400, // preFlightCacheMaxAge
false, // isForceAddMethods
false, // isForceAddHeaders
true, // isUseCredentials
false, // areAllOriginsAllowed
[], // allowedOrigins
false, // areAllMethodsAllowed
[], // allowedLcMethods
'', // allowedMethodsList
false, // areAllHeadersAllowed
[], // allowedLcHeaders
'', // allowedHeadersList
'', // exposedHeadersList
true, // isCheckHost
],
// PDO
'pdo.dsn' => 'mysql:host=localhost;charset=utf8mb4',
'pdo.username' => 'root',

View File

@@ -14,8 +14,10 @@ use DI\ContainerBuilder;
use {{appPackage}}\RegisterDependencies;
use {{appPackage}}\RegisterRoutes;
use {{appPackage}}\RegisterMiddlewares;
use {{appPackage}}\ResponseEmitter;
use Neomerx\Cors\Contracts\AnalyzerInterface;
use Slim\Factory\ServerRequestCreatorFactory;
use Slim\ResponseEmitter;
use Slim\Middleware\ErrorMiddleware;
{{/apiInfo}}
// Instantiate PHP-DI ContainerBuilder
@@ -66,7 +68,15 @@ $routes($app);
$serverRequestCreator = ServerRequestCreatorFactory::create();
$request = $serverRequestCreator->createServerRequestFromGlobals();
// Get error middleware from container
// also anti-pattern, of course we know
$errorMiddleware = $container->get(ErrorMiddleware::class);
// Run App & Emit Response
$response = $app->handle($request);
$responseEmitter = new ResponseEmitter();
$responseEmitter = (new ResponseEmitter())
->setRequest($request)
->setErrorMiddleware($errorMiddleware)
->setAnalyzer($container->get(AnalyzerInterface::class));
$responseEmitter->emit($response);

View File

@@ -45,6 +45,12 @@ final class RegisterDependencies
->constructorParameter('logErrors', \DI\get('slim.logErrors', true))
->constructorParameter('logErrorDetails', \DI\get('slim.logErrorDetails', true)),
// CORS
\Neomerx\Cors\Contracts\AnalysisStrategyInterface::class => \DI\create(\Neomerx\Cors\Strategies\Settings::class)
->method('setData', \DI\get('cors.settings')),
\Neomerx\Cors\Contracts\AnalyzerInterface::class => \DI\factory([\Neomerx\Cors\Analyzer::class, 'instance']),
// PDO class for database managing
\PDO::class => \DI\create()
->constructor(

View File

@@ -11,6 +11,8 @@ declare(strict_types=1);
*/{{#apiInfo}}
namespace {{appPackage}};
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Exception\HttpNotImplementedException;
/**
@@ -109,8 +111,13 @@ class RegisterRoutes
*/
public function __invoke(\Slim\App $app): void
{
$app->options('/{routes:.*}', function (ServerRequestInterface $request, ResponseInterface $response) {
// CORS Pre-Flight OPTIONS Request Handler
return $response;
});
foreach ($this->operations as $operation) {
$callback = function ($request) use ($operation) {
$callback = function (ServerRequestInterface $request) use ($operation) {
$message = "How about extending {$operation['classname']} by {$operation['apiPackage']}\\{$operation['userClassname']} class implementing {$operation['operationId']} as a {$operation['httpMethod']} method?";
throw new HttpNotImplementedException($request, $message);
};

View File

@@ -0,0 +1,130 @@
<?php
{{>licenseInfo}}
declare(strict_types=1);
/**
* NOTE: This class is auto generated by the openapi generator program.
* https://github.com/openapitools/openapi-generator
* Do not edit the class manually.
*/{{#apiInfo}}
namespace {{appPackage}};
{{/apiInfo}}
use Neomerx\Cors\Contracts\AnalysisResultInterface;
use Neomerx\Cors\Contracts\AnalyzerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\RequestInterface;
use Slim\Exception\HttpBadRequestException;
use Slim\Middleware\ErrorMiddleware;
use Slim\ResponseEmitter as SlimResponseEmitter;
/**
* Custom response emitter to support lazy CORS.
* @see https://github.com/slimphp/Slim-Skeleton/blob/037cfa2b6885301fc32a5b18a00a251a534aac81/src/Application/ResponseEmitter/ResponseEmitter.php
*/
class ResponseEmitter extends SlimResponseEmitter
{
/**
* @var RequestInterface
*/
protected $request;
/**
* @var ErrorMiddleware
*/
protected $errorMiddleware;
/**
* @var AnalyzerInterface
*/
protected $analyzer;
/**
* Set request.
* @param RequestInterface $request
* @return ResponseEmitter
*/
public function setRequest(RequestInterface $request): ResponseEmitter
{
$this->request = $request;
return $this;
}
/**
* Set error middleware.
* @param ErrorMiddleware $errorMiddleware
* @return ResponseEmitter
*/
public function setErrorMiddleware(ErrorMiddleware $errorMiddleware): ResponseEmitter
{
$this->errorMiddleware = $errorMiddleware;
return $this;
}
/**
* Set CORS request analyzer.
* @param AnalyzerInterface $analyzer
* @return ResponseEmitter
*/
public function setAnalyzer(AnalyzerInterface $analyzer): ResponseEmitter
{
$this->analyzer = $analyzer;
return $this;
}
/**
* Send the response the client
*
* @param ResponseInterface $response
* @return void
*/
public function emit(ResponseInterface $response): void
{
// slightly modified CORS handler from package documentation example
// @see https://github.com/neomerx/cors-psr7#sample-usage
$cors = $this->analyzer->analyze($this->request);
$errorMsg = null;
switch ($cors->getRequestType()) {
case AnalysisResultInterface::ERR_NO_HOST_HEADER:
$errorMsg = 'CORS no host header in request';
break;
case AnalysisResultInterface::ERR_ORIGIN_NOT_ALLOWED:
$errorMsg = 'CORS request origin is not allowed';
break;
case AnalysisResultInterface::ERR_METHOD_NOT_SUPPORTED:
$errorMsg = 'CORS requested method is not supported';
break;
case AnalysisResultInterface::ERR_HEADERS_NOT_SUPPORTED:
$errorMsg = 'CORS requested header is not allowed';
break;
case AnalysisResultInterface::TYPE_REQUEST_OUT_OF_CORS_SCOPE:
// do nothing
break;
case AnalysisResultInterface::TYPE_PRE_FLIGHT_REQUEST:
default:
// actual CORS request
$corsHeaders = $cors->getResponseHeaders();
// add CORS headers to Response $response
foreach ($corsHeaders as $name => $value) {
$response = $response->withHeader($name, $value);
}
}
if (!empty($errorMsg)) {
$exception = new HttpBadRequestException($this->request, sprintf('%s.', $errorMsg));
$exception->setTitle(\sprintf('%d %s', $exception->getCode(), $errorMsg));
$exception->setDescription($exception->getMessage());
$response = $this->errorMiddleware->handleException($this->request, $exception);
}
if (ob_get_contents()) {
ob_clean();
}
parent::emit($response);
}
}

View File

@@ -11,6 +11,7 @@ lib/Api/AbstractUserApi.php
lib/App/RegisterDependencies.php
lib/App/RegisterMiddlewares.php
lib/App/RegisterRoutes.php
lib/App/ResponseEmitter.php
lib/Auth/AbstractAuthenticator.php
lib/BaseModel.php
lib/Model/ApiResponse.php

View File

@@ -10,6 +10,7 @@
"require": {
"php": "^7.4 || ^8.0",
"dyorg/slim-token-authentication": "dev-slim4",
"neomerx/cors-psr7": "^2.0",
"php-di/slim-bridge": "^3.2",
"slim/psr7": "^1.1.0",
"ybelenko/openapi-data-mocker": "^1.0",

View File

@@ -32,6 +32,29 @@ return [
// Doesn't do anything when 'logErrors' is false.
'slim.logErrorDetails' => false,
// CORS settings
// @see https://github.com/neomerx/cors-psr7/blob/master/src/Strategies/Settings.php
'cors.settings' => [
isset($_SERVER['HTTPS']) ? 'https' : 'http', // serverOriginScheme
$_SERVER['SERVER_NAME'], // serverOriginHost
null, // serverOriginPort
false, // isPreFlightCanBeCached
0, // preFlightCacheMaxAge
false, // isForceAddMethods
false, // isForceAddHeaders
true, // isUseCredentials
true, // areAllOriginsAllowed
[], // allowedOrigins
true, // areAllMethodsAllowed
[], // allowedLcMethods
'GET, POST, PUT, PATCH, DELETE', // allowedMethodsList
true, // areAllHeadersAllowed
[], // allowedLcHeaders
'authorization, content-type, x-requested-with', // allowedHeadersList
'', // exposedHeadersList
true, // isCheckHost
],
// PDO
'pdo.dsn' => 'mysql:host=localhost;charset=utf8mb4',
'pdo.username' => 'root',

View File

@@ -32,6 +32,29 @@ return [
// Doesn't do anything when 'logErrors' is false.
'slim.logErrorDetails' => true,
// CORS settings
// https://github.com/neomerx/cors-psr7/blob/master/src/Strategies/Settings.php
'cors.settings' => [
isset($_SERVER['HTTPS']) ? 'https' : 'http', // serverOriginScheme
$_SERVER['SERVER_NAME'], // serverOriginHost
null, // serverOriginPort
true, // isPreFlightCanBeCached
86400, // preFlightCacheMaxAge
false, // isForceAddMethods
false, // isForceAddHeaders
true, // isUseCredentials
false, // areAllOriginsAllowed
[], // allowedOrigins
false, // areAllMethodsAllowed
[], // allowedLcMethods
'', // allowedMethodsList
false, // areAllHeadersAllowed
[], // allowedLcHeaders
'', // allowedHeadersList
'', // exposedHeadersList
true, // isCheckHost
],
// PDO
'pdo.dsn' => 'mysql:host=localhost;charset=utf8mb4',
'pdo.username' => 'root',

View File

@@ -58,6 +58,12 @@ final class RegisterDependencies
->constructorParameter('logErrors', \DI\get('slim.logErrors', true))
->constructorParameter('logErrorDetails', \DI\get('slim.logErrorDetails', true)),
// CORS
\Neomerx\Cors\Contracts\AnalysisStrategyInterface::class => \DI\create(\Neomerx\Cors\Strategies\Settings::class)
->method('setData', \DI\get('cors.settings')),
\Neomerx\Cors\Contracts\AnalyzerInterface::class => \DI\factory([\Neomerx\Cors\Analyzer::class, 'instance']),
// PDO class for database managing
\PDO::class => \DI\create()
->constructor(

View File

@@ -24,6 +24,8 @@ declare(strict_types=1);
*/
namespace OpenAPIServer\App;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Exception\HttpNotImplementedException;
/**
@@ -838,8 +840,13 @@ class RegisterRoutes
*/
public function __invoke(\Slim\App $app): void
{
$app->options('/{routes:.*}', function (ServerRequestInterface $request, ResponseInterface $response) {
// CORS Pre-Flight OPTIONS Request Handler
return $response;
});
foreach ($this->operations as $operation) {
$callback = function ($request) use ($operation) {
$callback = function (ServerRequestInterface $request) use ($operation) {
$message = "How about extending {$operation['classname']} by {$operation['apiPackage']}\\{$operation['userClassname']} class implementing {$operation['operationId']} as a {$operation['httpMethod']} method?";
throw new HttpNotImplementedException($request, $message);
};

View File

@@ -0,0 +1,142 @@
<?php
/**
* OpenAPI Petstore
* PHP version 7.4
*
* @package OpenAPIServer
* @author OpenAPI Generator team
* @link https://github.com/openapitools/openapi-generator
*/
/**
* This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
* The version of the OpenAPI document: 1.0.0
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
declare(strict_types=1);
/**
* NOTE: This class is auto generated by the openapi generator program.
* https://github.com/openapitools/openapi-generator
* Do not edit the class manually.
*/
namespace OpenAPIServer\App;
use Neomerx\Cors\Contracts\AnalysisResultInterface;
use Neomerx\Cors\Contracts\AnalyzerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\RequestInterface;
use Slim\Exception\HttpBadRequestException;
use Slim\Middleware\ErrorMiddleware;
use Slim\ResponseEmitter as SlimResponseEmitter;
/**
* Custom response emitter to support lazy CORS.
* @see https://github.com/slimphp/Slim-Skeleton/blob/037cfa2b6885301fc32a5b18a00a251a534aac81/src/Application/ResponseEmitter/ResponseEmitter.php
*/
class ResponseEmitter extends SlimResponseEmitter
{
/**
* @var RequestInterface
*/
protected $request;
/**
* @var ErrorMiddleware
*/
protected $errorMiddleware;
/**
* @var AnalyzerInterface
*/
protected $analyzer;
/**
* Set request.
* @param RequestInterface $request
* @return ResponseEmitter
*/
public function setRequest(RequestInterface $request): ResponseEmitter
{
$this->request = $request;
return $this;
}
/**
* Set error middleware.
* @param ErrorMiddleware $errorMiddleware
* @return ResponseEmitter
*/
public function setErrorMiddleware(ErrorMiddleware $errorMiddleware): ResponseEmitter
{
$this->errorMiddleware = $errorMiddleware;
return $this;
}
/**
* Set CORS request analyzer.
* @param AnalyzerInterface $analyzer
* @return ResponseEmitter
*/
public function setAnalyzer(AnalyzerInterface $analyzer): ResponseEmitter
{
$this->analyzer = $analyzer;
return $this;
}
/**
* Send the response the client
*
* @param ResponseInterface $response
* @return void
*/
public function emit(ResponseInterface $response): void
{
// slightly modified CORS handler from package documentation example
// @see https://github.com/neomerx/cors-psr7#sample-usage
$cors = $this->analyzer->analyze($this->request);
$errorMsg = null;
switch ($cors->getRequestType()) {
case AnalysisResultInterface::ERR_NO_HOST_HEADER:
$errorMsg = 'CORS no host header in request';
break;
case AnalysisResultInterface::ERR_ORIGIN_NOT_ALLOWED:
$errorMsg = 'CORS request origin is not allowed';
break;
case AnalysisResultInterface::ERR_METHOD_NOT_SUPPORTED:
$errorMsg = 'CORS requested method is not supported';
break;
case AnalysisResultInterface::ERR_HEADERS_NOT_SUPPORTED:
$errorMsg = 'CORS requested header is not allowed';
break;
case AnalysisResultInterface::TYPE_REQUEST_OUT_OF_CORS_SCOPE:
// do nothing
break;
case AnalysisResultInterface::TYPE_PRE_FLIGHT_REQUEST:
default:
// actual CORS request
$corsHeaders = $cors->getResponseHeaders();
// add CORS headers to Response $response
foreach ($corsHeaders as $name => $value) {
$response = $response->withHeader($name, $value);
}
}
if (!empty($errorMsg)) {
$exception = new HttpBadRequestException($this->request, sprintf('%s.', $errorMsg));
$exception->setTitle(\sprintf('%d %s', $exception->getCode(), $errorMsg));
$exception->setDescription($exception->getMessage());
$response = $this->errorMiddleware->handleException($this->request, $exception);
}
if (ob_get_contents()) {
ob_clean();
}
parent::emit($response);
}
}

View File

@@ -27,8 +27,10 @@ use DI\ContainerBuilder;
use OpenAPIServer\App\RegisterDependencies;
use OpenAPIServer\App\RegisterRoutes;
use OpenAPIServer\App\RegisterMiddlewares;
use OpenAPIServer\App\ResponseEmitter;
use Neomerx\Cors\Contracts\AnalyzerInterface;
use Slim\Factory\ServerRequestCreatorFactory;
use Slim\ResponseEmitter;
use Slim\Middleware\ErrorMiddleware;
// Instantiate PHP-DI ContainerBuilder
$builder = new ContainerBuilder();
@@ -78,7 +80,15 @@ $routes($app);
$serverRequestCreator = ServerRequestCreatorFactory::create();
$request = $serverRequestCreator->createServerRequestFromGlobals();
// Get error middleware from container
// also anti-pattern, of course we know
$errorMiddleware = $container->get(ErrorMiddleware::class);
// Run App & Emit Response
$response = $app->handle($request);
$responseEmitter = new ResponseEmitter();
$responseEmitter = (new ResponseEmitter())
->setRequest($request)
->setErrorMiddleware($errorMiddleware)
->setAnalyzer($container->get(AnalyzerInterface::class));
$responseEmitter->emit($response);