Skip to content

Commit

Permalink
Introduce middlewares
Browse files Browse the repository at this point in the history
  • Loading branch information
phpbg committed Dec 2, 2018
1 parent 9d6ac45 commit 82045bd
Show file tree
Hide file tree
Showing 12 changed files with 713 additions and 3 deletions.
56 changes: 56 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
This is a library that allow you to build a [RTSP/1.0](https://www.ietf.org/rfc/rfc2326.txt) server.

# Examples

## Bare minimum server

This is a very minimal server that will
* dump requests
* reply with 200 OK (which is not very useful :-)):
Expand Down Expand Up @@ -32,6 +35,59 @@ echo "Open any decent video player (e.g. vlc, mpv) and open rtsp://localhost:554
$loop->run();
```

## Server with middleware stack
This is almost the same example as the previous one, but with a more flexible and trendy middleware stack.

`phpbg/rtsp` comes with some handy middlewares that will help you build a rfc compliant application:
* `AutoCseq`: automatically add cseq header to your responses
* `AutoContentLength`: automatically add content-length to your responses

```php
require __DIR__ . '/../vendor/autoload.php';

$loop = React\EventLoop\Factory::create();

//Normal port is 554 but we use 5540 instead to avoid root required
$socket = new \React\Socket\TcpServer('tcp://0.0.0.0:5540', $loop);

$middlewares = [
function (\PhpBg\Rtsp\Message\Request $request, \React\Socket\ConnectionInterface $connection, $next) {
// echo request middleware
echo $request;
return $next($request, $connection);
},
function (\PhpBg\Rtsp\Message\Request $request, \React\Socket\ConnectionInterface $connection, $next) {
// echo response middleware
$response = $next($request, $connection);
if (!($response instanceof \React\Promise\PromiseInterface)) {
$response = new \React\Promise\FulfilledPromise($response);
}
return $response->then(function($resolvedResponse) {
echo $resolvedResponse."\r\n";
return $resolvedResponse;
});
},
new \PhpBg\Rtsp\Middleware\AutoCseq(),
new \PhpBg\Rtsp\Middleware\AutoContentLength(),
function (\PhpBg\Rtsp\Message\Request $request, \React\Socket\ConnectionInterface $connection) {
// 200 OK response middleware
return \PhpBg\Rtsp\Message\MessageFactory::response();
}
];

$server = new \PhpBg\Rtsp\Server(new \PhpBg\Rtsp\Middleware\MiddlewareStack($middlewares));

$server->on('error', function (\Exception $e) {
echo $e->getMessage() . "\r\n";
echo $e->getTraceAsString() . "\r\n";
});

$server->listen($socket);
echo "Server started\r\n";
echo "Open any decent video player (e.g. vlc, mpv) and open rtsp://localhost:5540\r\n";
$loop->run();
```

# Install

```
Expand Down
67 changes: 67 additions & 0 deletions src/Middleware/AutoContentLength.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

/**
* MIT License
*
* Copyright (c) 2018 Samuel CHEMLA
*
* 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.
*/

namespace PhpBg\Rtsp\Middleware;

use PhpBg\Rtsp\Message\Request;
use PhpBg\Rtsp\Message\Response;
use React\Promise\FulfilledPromise;
use React\Promise\PromiseInterface;
use React\Socket\ConnectionInterface;

/**
* Middleware that automatically add content-length header to responses that contains a body.
* As explained in https://www.ietf.org/rfc/rfc2326.txt, the presence of a body implies
* * either you close the connection after the body, to let the receiver detect the end of the body
* * either you set a content-length header: this is what this middleware does
*
* If no body is present then this header won't be added because its value is implicitly 0.
*/
class AutoContentLength
{

/**
* This middleware just looks like a standard middleware: invokable, receive request and return response
*
* @param Request $request
* @param ConnectionInterface $connection
* @param callable $next
* @return Response|PromiseInterface
*/
public function __invoke(Request $request, ConnectionInterface $connection, callable $next)
{
$response = $next($request, $connection);
if (!($response instanceof PromiseInterface)) {
$response = new FulfilledPromise($response);
}
return $response->then(function (Response $resolvedResponse) use ($request) {
if (isset($resolvedResponse->body) && !$resolvedResponse->hasHeader('content-length')) {
$resolvedResponse->setHeader('content-length', strlen($resolvedResponse->body));
}
return $resolvedResponse;
});
}
}
62 changes: 62 additions & 0 deletions src/Middleware/AutoCseq.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

/**
* MIT License
*
* Copyright (c) 2018 Samuel CHEMLA
*
* 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.
*/

namespace PhpBg\Rtsp\Middleware;

use PhpBg\Rtsp\Message\Request;
use PhpBg\Rtsp\Message\Response;
use React\Promise\FulfilledPromise;
use React\Promise\PromiseInterface;
use React\Socket\ConnectionInterface;

/**
* Middleware that automatically add cseq header to responses as required by https://www.ietf.org/rfc/rfc2326.txt
*/
class AutoCseq
{

/**
* This middleware just looks like a standard middleware: invokable, receive request and return response
*
* @param Request $request
* @param ConnectionInterface $connection
* @param callable $next
* @return Response|PromiseInterface
*/
public function __invoke(Request $request, ConnectionInterface $connection, callable $next)
{
$response = $next($request, $connection);
if (!($response instanceof PromiseInterface)) {
$response = new FulfilledPromise($response);
}
return $response->then(function (Response $resolvedResponse) use ($request) {
if (!$resolvedResponse->hasHeader('cseq') && $request->hasHeader('cseq')) {
$resolvedResponse->setHeader('cseq', $request->getHeader('cseq'));
}
return $resolvedResponse;
});
}
}
92 changes: 92 additions & 0 deletions src/Middleware/MiddlewareStack.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

/**
* MIT License
*
* Copyright (c) 2018 Samuel CHEMLA
*
* 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.
*/

namespace PhpBg\Rtsp\Middleware;

use PhpBg\Rtsp\Message\Request;
use PhpBg\Rtsp\Message\Response;
use React\Promise\PromiseInterface;
use React\Socket\ConnectionInterface;

/**
* Middleware that run a stack of middlewares
*/
class MiddlewareStack
{
/**
* @var callable[]
*/
protected $middlewares;

/**
* @param callable[] $middlewares Stack of middlewares you want to run
*/
public function __construct(array $middlewares)
{
if (empty($middlewares)) {
throw new \RuntimeException('No middleware to run');
}
$this->middlewares = array_values($middlewares);
}

/**
* This middleware just looks like a standard middleware: invokable, receive request and return response
* It has no $next handler because it is meant to be the last middleware in stack
*
* @param Request $request
* @param ConnectionInterface $connection
* @return Response|PromiseInterface
*/
public function __invoke(Request $request, ConnectionInterface $connection)
{
return $this->call($request, $connection, 0);
}

/**
* Recursively executes middlewares
*
* @param Request $request
* @param ConnectionInterface $connection
* @param int $position
* @return mixed
*/
protected function call(Request $request, ConnectionInterface $connection, int $position)
{
// final request handler will be invoked without a next handler
if (!isset($this->middlewares[$position + 1])) {
$handler = $this->middlewares[$position];
return $handler($request, $connection);
}

$next = function (Request $request, ConnectionInterface $connection) use ($position) {
return $this->call($request, $connection, $position + 1);
};

// invoke middleware request handler with next handler
$handler = $this->middlewares[$position];
return $handler($request, $connection, $next);
}
}
5 changes: 3 additions & 2 deletions src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,12 @@ protected function handleRequest(Request $request, ConnectionInterface $connecti
}
// Convert all non-promise response to promises
// There is little overhead for this, but this allows simpler code
if (!isset($response) || !$response instanceof PromiseInterface) {
if (!($response instanceof PromiseInterface)) {
$response = new FulfilledPromise($response);
}

$response->done(function ($resolvedResponse) use ($connection) {
// We should probably use done() instead of then(), but done() is only part of ExtendedPromiseInterface
$response->then(function ($resolvedResponse) use ($connection) {
if ($resolvedResponse instanceof Response) {
$connection->write($resolvedResponse->toTransport());
return;
Expand Down
Loading

0 comments on commit 82045bd

Please sign in to comment.