There's A Middleware For That

by Matthew Weier O'Phinney
Principal Engineer
Rogue Wave Software, Inc.


ZendCon, Las Vegas, 24 Oct 2017

A Play in 5 Acts

  • Act I: What is Middleware?
  • Act II: A Middleware for Every Request
  • Act III: A Fetching Resource
  • Act IV: The Up-To-Date Resource
  • Act V: Exeunt

Act I

What is Middleware?

Introducing Our Actors

  • PSR-7: provides our HTTP request and response messages.
  • Request Handlers: turn a request into a response.
  • Middleware: acts as a request handler, but optionally delegates creation of a response.

Request Handlers


public function handle(
    ServerRequestInterface $request
) : ResponseInterface
          

Middleware


public function process(
    ServerRequestInterface $request,
    RequestHandlerInterface $delegate
) : ResponseInterface
          

Delegation


// RequestHandlerInterface
$response = $delegate->handle($request);

// DelegateInterface:
$response = $delegate->process($request);
          

Middleware as Request Handlers


public function process(ServerRequest $request, RequestHandler $delegate)
{
    $item = $this->repository->fetchById(
        $request->getAttribute('id')
    );
    return new JsonResponse($item);
}
          

Why Use Middleware?

  • To fill out HTTP web server functionality
  • To create custom application workflows

Act II

A Middleware for Every Request

Setting the Stage

  • We are using Expressive.
  • We have written an API.

Log Requests


$ composer require middlewares/access-log
          

https://github.com/middlewares/access-log

Configuring Logging


use Middlewares\AccessLog;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;

function (ContainerInterface $container) : AccessLog
{
    $middleware = new AccessLog(
        $container->get(LoggerInterface::class)
    );
    return $middleware;
}
          

Restrict to HTTPS


$ composer require middlewares/https
          

https://github.com/middlewares/https

Configuring HTTPS redirects


use Middlewares\Https;
use Psr\Container\ContainerInterface;

function (ContainerInterface $container) : Https
{
    $middleware = new Https();
    $middleware->checkHttpsForward(true);
    return $middleware;
}
          

Strip Trailing Slashes


$ composer require middlewares/trailing-slash
          

https://github.com/middlewares/trailing-slash

Configuring Trailing Slashes


use Middlewares\TrailingSlash;
use Psr\Container\ContainerInterface;

function (ContainerInterface $container) : TrailingSlash
{
    $middleware = new TrailingSlash();
    $middleware->redirect(true);
    return $middleware;
}
          

Gzip Content


$ composer require middlewares/encoder
          

https://github.com/middlewares/encoder

Configuring Gzip


use Middlewares\GzipEncoder;
use Psr\Container\ContainerInterface;

function (ContainerInterface $container) : GzipEncoder
{
    $middleware = new GzipEncoder();
    return $middleware;
}
          

Content-Security-Policy


$ composer require middlewares/csp
          

https://github.com/middlewares/csp

Build a CSP


{
    "img-src": {
        "self": true,
        "data": true
    },
    "script-src": {
        "allow": [ "https://www.google-analytics.com" ],
        "self": true,
        "unsafe-inline": false,
        "unsafe-eval": false
    }
}
          

Configure CSP Middleware


use Middlewares\Csp;
use ParagonIE\CSPBuilder\CSPBuilder;
use Psr\Container\ContainerInterface;

function (ContainerInterface $container) : Csp
{
    $config = $container->get('config');
    $csp = CSPBuilder::fromFile($config['csp-file']);
    $middleware = new Csp($csp);
    return $middleware;
}
          

Cross Origin Request Security


$ composer require bairwell/middleware-cors
          

https://github.com/bairwell/middleware-cors

Configuring CORS


// In a config file somewhere...
return [
    'cors' => [
        'origin' => ['*.example.com', 'example.com'],
        'allowCredentials' => true,
        'allowHeaders' => ['Accept', /* ... */],
    ],
];
          

Determine Methods Allowed


$allowedMethods = function (ServerRequestInterface $request) {
    $routeResult = $request->getAttribute(RouteResult::class);
    $route = $routeResult->getMatchedRoute();
    return $route->getAllowedMethods();
};
          

Configuring CORS Middleware


use Bairwell\MiddlewareCors;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Container\ContainerInterface;

function (ContainerInterface $container) : MiddlewareCors use ($allowedMethods)
{
    $config = $container->get('config')['cors'] ?? [];
    $config['allowedMethods'] = $allowedMethods;
    $middleware = new MiddlewareCors($config);
    return $middleware;
}
          

Act III

A Fetching Resource

Our Setting


public function process(ServerRequest $request, RequestHandler $delegate)
{
    $id = $request->getAttribute('id', false);

    if (false === $id) {
        throw new MissingBookIdentifierException();
    }

    $user = $request->getAttribute('user');
    $book = $this->repository->fetch($id, $user);

    $resource = get_object_vars($user);
    $resource['_links']['self']['href'] = (string) $request->getUri();
    return new JsonResponse($resource);
}
          

Problem Details


$ composer require zendframework/zend-problem-details
          

https://github.com/zendframework/zend-problem-details

Create an Exception


use Zend\ProblemDetails\Exception\ProblemDetailsExceptionInterface;
use Zend\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait;

class MissingBookIdentifierException
    extends \DomainException
    implements ProblemDetailsExceptionInterfaces
{
    use CommonProblemDetailsExceptionTrait;
    public function __construct()
    {
        parent::__construct('Missing book identifier');
        $this->status = 400;
        $this->detail = 'Your request was missing a book identifier value';
        $this->type = '/api/error/missing-book-identifier';
        $this->title = 'Missing Book Identifier';
    }
}
          

Rate Limiting Requests


$ composer require prezto/rate-limit
          

https://github.com/Prezto/RateLimit

Redis Configuration


// In a config file:
return [
    'rate-limit' => [
        'server'   => '10.0.242.42',
        'port'     => 6379,
        'password' => getenv('REDIS_PASSWORD')
    ],
];
          

Configuring Rate Limiting


use Prezto\RateLimit\RateLimitMiddleware;
use Psr\Container\ContainerInterface;

function (ContainerInterface $container) : RateLimitMiddleware
{
    $config = $container->get('config')['rate-limit'] ?? [];
    $middleware = new RateLimitMiddleware(
        $config['server'] ?? 'localhost',
        $config['port'] ?? 6379,
        $config['password'] ?? ''
    );
    return $middleware;
}
          

Provide HTTP Caching


$ composer require slim/http-cache
          

https://github.com/slimphp/Slim-HttpCache

Configuring HTTP Caching


use Psr\Container\ContainerInterface;
use Slim\HttpCache\Cache;

function (ContainerInterface $container) : Cache
{
    $middleware = new Cache(
        'public',
        $ttl = 86400,
        $revalidate = false
    );
    return $middleware;
}
          

Act IV

The Up-To-Date Resource

Our Setting


public function process(ServerRequest $request, RequestHandler $delegate)
{
    $id = $request->getAttribute('id');
    $values = $request->getParsedBody();
    $book = new Book($id, $values);
    $user = $request->getAttribute('user');

    $book = $this->repository->update($book, $user);

    $resource = get_object_vars($user);
    $resource['_links']['self']['href'] = (string) $request->getUri();
    return new JsonResponse($resource);
}
          

Honoring X-Http-Method-Override


$ composer require middlewares/method-override
          

https://github.com/middlewares/method-override

Configuring Overrides


use Middlewares\MethodOverride;
use Psr\Container\ContainerInterface;

function (ContainerInterface $container) : MethodOverride
{
    $middleware = new MethodOverride();
    $middleware->get(['HEAD', 'OPTIONS']);
    $middleware->post(['PATCH', 'PUT', 'DELETE']);
    return $middleware;
}
          

Authenticating Users


$ composer require middlewares/http-authentication
          

https://github.com/middlewares/http-authentication

Configuring Users


// In a config file... maybe!
return [
    'authentication' => [
        'users' => [
            'samvimes' => 'thewatch',
            'ridcully' => 'unseenuniversity',
        ],
    ],
];
          

Configuring Authentication


use Middlewares\BasicAuthentication;
use Psr\Container\ContainerInterface;

function (ContainerInterface $container) : BasicAuthentication
{
    $config = $container->get('config')['authentication']['users'] ?? [];
    $middleware = new BasicAuthentication($config);
    // Request attribute in which to store user:
    $middleware->attribute('user');
    return $middleware;
}
          

Parsing the Payload


$ composer require zendframework/zend-expressive-helpers
          

https://github.com/zendframework/zend-expressive-helpers

Configuring the Payload


use Psr\Container\ContainerInterface;
use Zend\Expressive\Helpers\BodyParams\BodyParamsMiddleware;

function (ContainerInterface $container) : BodyParamsMiddleware
{
    return new BodyParamsMiddleware();
}
          

Validate the Payload


$ composer require respect/validation
          

https://github.com/Respect/Validation

Creating Validation Rules


use Psr\Container\ContainerInterface;
use Respect\Validation\Validator as v;

function (ContainerInterface $container) : v[]
{
    $title = v::stringType()->notEmpty();
    $author = v::stringType()->notEmpty();

    return [
        'title' => $title,
        'author' => $author,
    ];
}
          

Create Validation Exceptions


use Zend\ProblemDetails\Exception\ProblemDetailsExceptionInterface;
use Zend\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait;

class InvalidDataException
    extends \DomainException
    implements ProblemDetailsExceptionInterfaces {
    use CommonProblemDetailsExceptionTrait;
    public static function fromAssertion($prev) {
        $e = new self($prev->getMessage(), $prev->getCode(), $prev);
        $e->status = 422;
        $e->detail = 'One or more data elements submitted were invalid';
        $e->type = '/api/error/invalid-data';
        $e->title = 'Invalid Data';
        $e->additional = ['messages' => $prev->getMessage()];
        return $e;
    }
}
          

Creating the Middleware


class ValidationMiddleware implements Middleware {
    private $validators;
    public function __construct(array $validators) {
        $this->validators = $validators;
    }
    public function process(Request $request, Delegator $delegator) {
        $data = $request->getParsedBody();
        foreach ($this->v as $key => $validator) {
            try {
                $validator->assert($data[$key] ?? null);
            } catch (\Throwable $e) {
                throw InvalidDataException::fromAssertion($e);
            }
        }
        return $delegator->process($request);
    }
}
          

Configuring the Middleware


use Psr\Container\ContainerInterface;

function (ContainerInterface $container)
{
    $validators = $container->get('BookValidators');
    $middleware = new ValidationMiddleware($validators);
    return $middleware;
}
          

Act V

Exeunt

Assembling our Troupe

  • We defined pipeline middleware in Act II.
  • We defined routed middleware in both Acts III and IV.
  • Some "routed" middleware actually belongs in the pipeline.

Assembling our Pipeline


// config/pipeline.php
$app->pipe(\Middlewares\AccessLog::class);
$app->pipe(\Middlewares\Https::class);
$app->pipe(\Middlewares\TrailingSlash::class);
$app->pipe(\Middlewares\GzipEncoder::class);
$app->pipe(\Middlewares\Csp::class);

// For our updatable resource:
$app->pipe(\Middlewares\MethodOverride::class);

// CORS is dependent on routing...
$app->pipeRoutingMiddleware();
$app->pipe(\Bairwell\MiddlewareCors::class);
          

Routing our Fetchable Resource


// config/routes.php
$app->get('/api/books/{id:\d+}', [
    \Zend\ProblemDetails\ProblemDetailsMiddleware::class,
    \Prezto\RateLimit\RateLimitMiddleware::class,
    \Slim\HttpCache\Cache::class,
    FetchBookHandler::class,
], 'book');
          

Routing our Updatable Resource


// config/routes.php
$app->patch('/api/books/{id:\d+}', [
    \Zend\ProblemDetails\ProblemDetailsMiddleware::class,
    \Prezto\RateLimit\RateLimitMiddleware::class,
    \Middlewares\BasicAuthentication::class,
    \Zend\Expressive\Helper\BodyParams\BodyParamsMiddleware::class,
    ValidationMiddleware::class,
    UpdateBookHandler::class,
]);
          

Credits

Resources

Or find them all at http://linkbun.ch/05dz0

Thank You!

Contact me: matthew [at] zend.com

Follow me: @mwop