There's A Middleware For That

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

A Play in 5 Acts

  • Act I: What is Middleware?
  • Act II: Middleware for Every Request
  • Act III: Fetching a Resource
  • Act IV: Updating a Resource
  • Act V: Exeunt

Act I

What is Middleware?

PSR-15

HTTP Request Handlers

Core Concepts

  • PSR-7: provides our HTTP request and response messages.
  • Request Handlers: turn a request into a response.
  • Middleware: sits between the request and the request handler, either producing a response, or delegating to the handler.

Request Handlers


namespace Psr\Http\Server;

use Psr\Http\Message;

interface RequestHandlerInterface
{
    public function handle(
        Message\ServerRequestInterface $request
    ) : Message\ResponseInterface
}
          

Middleware


namespace Psr\Http\Server;

use Psr\Http\Message;

interface MiddlewareInterface
{
    public function process(
        Message\ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ) : Message\ResponseInterface
}
          

Delegation within Middleware


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

Middleware as Request Handler


public function process(ServerRequest $request, RequestHandler $handler)
{
    $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

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
{
    return new GzipEncoder();
}
          

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 tuupola/cors-middleware
          

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

Configuring CORS


// In a config file somewhere...
return [
    'cors' => [
        'origin' => ['*.example.com', 'example.com'],
        'methods' => 'Api\introspectAllowedMethods'
        'credentials' => true,
        'headers.allow' => ['Accept', /* ... */],
    ],
];
          

Determine Methods Allowed


namespace Api;

use Psr\Http\Message\ServerRequestInterface;
use Zend\Expressive\Router\RouteResult;

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

Configuring CORS Middleware


use Tuupola\Middleware\CorsMiddleware;
use Psr\Container\ContainerInterface;

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

Act III

Fetching A Resource

Our Setting


use Zend\Expressive\Authentication\UserInterface;

public function handle(ServerRequestInterface $request)
{
    $id = $request->getAttribute('id', false);

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

    $user = $request->getAttribute(UserInterface::class);
    $book = $this->repository->fetch($id, $user->getIdentity()); 

    $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 ProblemDetailsExceptionInterface {
    use CommonProblemDetailsExceptionTrait;
    public static function create() : self {
        $e = new self('Missing book identifier');
        $e->status = 400;
        $e->detail = 'Your request was missing a book identifier value';
        $e->type = '/api/error/missing-book-identifier';
        $e->title = 'Missing Book Identifier';
        return $e;
    }
}
          

Configuring Problem Details Middleware


use Psr\Container\ContainerInterface;
use Zend\ProblemDetails\ProblemDetailsMiddleware;
use Zend\ProblemDetails\ProblemDetailsResponseFactory;

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

Authenticating Users


$ composer require zendframework/zend-expressive-authentication
$ composer require zendframework/zend-expressive-authentication-basic
          

https://docs.zendframework.com/zend-expressive-authentication

Configuring Users


// In a config file:
return [
    'authentication' => [
        'htpasswd' => 'path/to/htpasswd',
    ],
];
          

# In data/htpasswd:
matthew:$2y$10$zUW9br6jbkMz.6vpn0sAQeH0Jx74jKjyVmb83eQrFZ2mz4GzgNjPu
          

Configuring Authentication


use Psr\Container\ContainerInterface;
use Zend\Expressive\Authentication\AuthenticationMiddleware;
use Zend\Expressive\Authentication\Basic\BasicAccess;
use Zend\Expressive\Authentication\UserRepository\Htpasswd;
use Zend\ProblemDetails\ProblemDetailsResponseFactory;

function (ContainerInterface $container) : BasicAuthentication {
    $config = $container->get('config')['authentication'] ?? [];
    $repository = new Htpasswd($config['htpasswd'] ?? 'data/htpasswd');
    $adapter = new BasicAccess(
        $repository,
        'api',
        $container->get(ProblemDetailsResponseFactory::class)
    );
    $middleware = new AuthenticationMiddleware($adapter);
    return $middleware;
}
          

Rate Limiting Requests


$ composer require los/los-rate-limit
          

https://github.com/Lansoweb/LosRateLimit

Configuration


// In a config file:
return [
    'los-rate-limit' => [
        'max_requests' => '100',
        'reset_time' => '3600',
        'api_header' => 'X-Api-Key',
        'headers' => [
            'limit' => 'X-RateLimit-Limit',
            'remaining' => 'X-RateLimit-Remaining',
            'reset' => 'X-RateLimit-Reset',
        ],
    ],
];
          

Configuring Rate Limiting


use LosMiddleware\RateLimit\RateLimitMiddleware;
use LosMiddleware\RateLimit\Storage\FileStorage;
use Psr\Container\ContainerInterface;
use Zend\ProblemDetails\ProblemDetailsResponseFactory;

function (ContainerInterface $container) : RateLimitMiddleware
{
    $config = $container->get('config')['los-rate-limit'] ?? [];
    $storageFile = $config['storage_file'] ?? 'data/rate-limits.json';
    $middleware = new RateLimitMiddleware(
        new FileStorage($storageFile),
        $container->get(ProblemDetailsResponseFactory()),
        $config
    );
    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;
use Zend\Diactoros\Response;
use Zend\Stratigility\doublePassMiddleware;

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

Act IV

Updating A Resource

Our Setting


use Zend\Expressive\Authentication\UserInterface;

public function handle(ServerRequestInterface $request)
{
    $id = $request->getAttribute('id');
    $values = $request->getParsedBody();
    $book = new Book($id, $values);
    $user = $request->getAttribute(UserInterface::class);

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

    $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;
}
          

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 ProblemDetailsExceptionInterface {
    use CommonProblemDetailsExceptionTrait;
    public static function fromAssertion($prev) : self {
        $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' => $e->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, RequestHandler $handler) {
        $data = $request->getParsedBody();
        foreach ($this->validators as $key => $validator) {
            try {
                $validator->assert($data[$key] ?? null);
            } catch (\Throwable $e) {
                throw InvalidDataException::fromAssertion($e);
            }
        }
        return $handler->handle($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);

$app->pipe(\Zend\Expressive\Router\Middleware\RouteMiddleware::class);

// CORS is dependent on routing...
$app->pipe(\Tuupola\Middleware\CorsMiddleware::class);

$app->pipe(\Zend\Expressive\Router\Middleware\DispatchMiddleware::class);
$app->pipe(\Zend\Expressive\Handler\NotFoundHandler::class);
          

Routing our Fetchable Resource


// config/routes.php
$app->get('/api/books/{id:\d+}', [
    \Zend\ProblemDetails\ProblemDetailsMiddleware::class,
    \Zend\Expressive\Authentication\AuthenticationMiddleware::class,
    \LosMiddleware\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,
    \Zend\Expressive\Authentication\AuthenticationMiddleware::class,
    \LosMiddleware\RateLimit\RateLimitMiddleware::class,
    \Zend\Expressive\Helper\BodyParams\BodyParamsMiddleware::class,
    ValidationMiddleware::class,
    UpdateBookHandler::class,
]);
          

Credits

Resources

Or find them all at http://linkbun.ch/07afq

Thank You!

Contact me: matthew [at] zend.com

Follow me: @mwop