Skip to content

Commit

Permalink
Merge pull request #120 from andig/unix
Browse files Browse the repository at this point in the history
Support Unix domain socket (UDS) server
  • Loading branch information
WyriHaximus authored Nov 16, 2017
2 parents fa0b187 + 9257632 commit 626bf5b
Show file tree
Hide file tree
Showing 8 changed files with 524 additions and 7 deletions.
49 changes: 48 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ handle multiple concurrent connections without blocking.
* [Advanced server usage](#advanced-server-usage)
* [TcpServer](#tcpserver)
* [SecureServer](#secureserver)
* [UnixServer](#unixserver)
* [LimitingServer](#limitingserver)
* [getConnections()](#getconnections)
* [Client usage](#client-usage)
Expand Down Expand Up @@ -255,7 +256,8 @@ If the address can not be determined or is unknown at this time (such as
after the socket has been closed), it MAY return a `NULL` value instead.

Otherwise, it will return the full address (URI) as a string value, such
as `tcp://127.0.0.1:8080`, `tcp://[::1]:80` or `tls://127.0.0.1:443`.
as `tcp://127.0.0.1:8080`, `tcp://[::1]:80`, `tls://127.0.0.1:443`
`unix://example.sock` or `unix:///path/to/example.sock`.
Note that individual URI components are application specific and depend
on the underlying transport protocol.

Expand Down Expand Up @@ -342,6 +344,7 @@ Calling this method more than once on the same instance is a NO-OP.
The `Server` class is the main class in this package that implements the
[`ServerInterface`](#serverinterface) and allows you to accept incoming
streaming connections, such as plaintext TCP/IP or secure TLS connection streams.
Connections can also be accepted on Unix domain sockets.

```php
$server = new Server(8080, $loop);
Expand Down Expand Up @@ -373,6 +376,13 @@ brackets:
$server = new Server('[::1]:8080', $loop);
```

To listen on a Unix domain socket (UDS) path, you MUST prefix the URI with the
`unix://` scheme:

```php
$server = new Server('unix:///tmp/server.sock', $loop);
```

If the given URI is invalid, does not contain a port, any other scheme or if it
contains a hostname, it will throw an `InvalidArgumentException`:

Expand Down Expand Up @@ -648,6 +658,43 @@ If you use a custom `ServerInterface` and its `connection` event does not
meet this requirement, the `SecureServer` will emit an `error` event and
then close the underlying connection.

#### UnixServer

The `UnixServer` class implements the [`ServerInterface`](#serverinterface) and
is responsible for accepting connections on Unix domain sockets (UDS).

```php
$server = new UnixServer('/tmp/server.sock', $loop);
```

As above, the `$uri` parameter can consist of only a socket path or socket path
prefixed by the `unix://` scheme.

If the given URI appears to be valid, but listening on it fails (such as if the
socket is already in use or the file not accessible etc.), it will throw a
`RuntimeException`:

```php
$first = new UnixServer('/tmp/same.sock', $loop);

// throws RuntimeException because socket is already in use
$second = new UnixServer('/tmp/same.sock', $loop);
```

Whenever a client connects, it will emit a `connection` event with a connection
instance implementing [`ConnectionInterface`](#connectioninterface):

```php
$server->on('connection', function (ConnectionInterface $connection) {
echo 'New connection' . PHP_EOL;

$connection->write('hello there!' . PHP_EOL);
});
```

See also the [`ServerInterface`](#serverinterface) for more details.

#### LimitingServer

The `LimitingServer` decorator wraps a given `ServerInterface` and is responsible
Expand Down
5 changes: 5 additions & 0 deletions examples/01-echo.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
//
// $ php examples/01-echo.php tls://127.0.0.1:8000 examples/localhost.pem
// $ openssl s_client -connect localhost:8000
//
// You can also run a Unix domain socket (UDS) server like this:
//
// $ php examples/01-echo.php unix:///tmp/server.sock
// $ nc -U /tmp/server.sock

use React\EventLoop\Factory;
use React\Socket\Server;
Expand Down
5 changes: 5 additions & 0 deletions examples/02-chat-server.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
//
// $ php examples/02-chat-server.php tls://127.0.0.1:8000 examples/localhost.pem
// $ openssl s_client -connect localhost:8000
//
// You can also run a Unix domain socket (UDS) server like this:
//
// $ php examples/02-chat-server.php unix:///tmp/server.sock
// $ nc -U /tmp/server.sock

use React\EventLoop\Factory;
use React\Socket\Server;
Expand Down
10 changes: 8 additions & 2 deletions examples/03-benchmark.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,21 @@
//
// $ php examples/03-benchmark.php 8000
// $ telnet localhost 8000
// $ echo hello world | nc -v localhost 8000
// $ dd if=/dev/zero bs=1M count=1000 | nc -v localhost 8000
// $ echo hello world | nc -N localhost 8000
// $ dd if=/dev/zero bs=1M count=1000 | nc -N localhost 8000
//
// You can also run a secure TLS benchmarking server like this:
//
// $ php examples/03-benchmark.php tls://127.0.0.1:8000 examples/localhost.pem
// $ openssl s_client -connect localhost:8000
// $ echo hello world | openssl s_client -connect localhost:8000
// $ dd if=/dev/zero bs=1M count=1000 | openssl s_client -connect localhost:8000
//
// You can also run a Unix domain socket (UDS) server benchmark like this:
//
// $ php examples/03-benchmark.php unix:///tmp/server.sock
// $ nc -N -U /tmp/server.sock
// $ dd if=/dev/zero bs=1M count=1000 | nc -N -U /tmp/server.sock

use React\EventLoop\Factory;
use React\Socket\Server;
Expand Down
13 changes: 9 additions & 4 deletions src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ final class Server extends EventEmitter implements ServerInterface
public function __construct($uri, LoopInterface $loop, array $context = array())
{
// sanitize TCP context options if not properly wrapped
if ($context && (!isset($context['tcp']) && !isset($context['tls']))) {
if ($context && (!isset($context['tcp']) && !isset($context['tls']) && !isset($context['unix']))) {
$context = array('tcp' => $context);
}

// apply default options if not explicitly given
$context += array(
'tcp' => array(),
'tls' => array(),
'unix' => array()
);

$scheme = 'tcp';
Expand All @@ -28,10 +29,14 @@ public function __construct($uri, LoopInterface $loop, array $context = array())
$scheme = substr($uri, 0, $pos);
}

$server = new TcpServer(str_replace('tls://', '', $uri), $loop, $context['tcp']);
if ($scheme === 'unix') {
$server = new UnixServer($uri, $loop, $context['unix']);
} else {
$server = new TcpServer(str_replace('tls://', '', $uri), $loop, $context['tcp']);

if ($scheme === 'tls') {
$server = new SecureServer($server, $loop, $context['tls']);
if ($scheme === 'tls') {
$server = new SecureServer($server, $loop, $context['tls']);
}
}

$this->server = $server;
Expand Down
130 changes: 130 additions & 0 deletions src/UnixServer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<?php

namespace React\Socket;

use Evenement\EventEmitter;
use React\EventLoop\LoopInterface;
use InvalidArgumentException;
use RuntimeException;

/**
* The `UnixServer` class implements the `ServerInterface` and
* is responsible for accepting plaintext connections on unix domain sockets.
*
* ```php
* $server = new UnixServer('unix:///tmp/app.sock', $loop);
* ```
*
* See also the `ServerInterface` for more details.
*
* @see ServerInterface
* @see ConnectionInterface
*/
final class UnixServer extends EventEmitter implements ServerInterface
{
private $master;
private $loop;
private $listening = false;

/**
* Creates a plaintext socket server and starts listening on the given unix socket
*
* This starts accepting new incoming connections on the given address.
* See also the `connection event` documented in the `ServerInterface`
* for more details.
*
* ```php
* $server = new UnixServer('unix:///tmp/app.sock', $loop);
* ```
*
* @param string $path
* @param LoopInterface $loop
* @param array $context
* @throws InvalidArgumentException if the listening address is invalid
* @throws RuntimeException if listening on this address fails (already in use etc.)
*/
public function __construct($path, LoopInterface $loop, array $context = array())
{
$this->loop = $loop;

if (strpos($path, '://') === false) {
$path = 'unix://' . $path;
} elseif (substr($path, 0, 7) !== 'unix://') {
throw new \InvalidArgumentException('Given URI "' . $path . '" is invalid');
}

$this->master = @stream_socket_server(
$path,
$errno,
$errstr,
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,
stream_context_create(array('socket' => $context))
);
if (false === $this->master) {
throw new RuntimeException('Failed to listen on unix domain socket "' . $path . '": ' . $errstr, $errno);
}
stream_set_blocking($this->master, 0);

$this->resume();
}

public function getAddress()
{
if (!is_resource($this->master)) {
return null;
}

return 'unix://' . stream_socket_get_name($this->master, false);
}

public function pause()
{
if (!$this->listening) {
return;
}

$this->loop->removeReadStream($this->master);
$this->listening = false;
}

public function resume()
{
if ($this->listening || !is_resource($this->master)) {
return;
}

$that = $this;
$this->loop->addReadStream($this->master, function ($master) use ($that) {
$newSocket = @stream_socket_accept($master);
if (false === $newSocket) {
$that->emit('error', array(new \RuntimeException('Error accepting new connection')));

return;
}
$that->handleConnection($newSocket);
});
$this->listening = true;
}

public function close()
{
if (!is_resource($this->master)) {
return;
}

$this->pause();
fclose($this->master);
$this->removeAllListeners();
}

/** @internal */
public function handleConnection($socket)
{
$connection = new Connection($socket, $this->loop);
$connection->unix = true;

$this->emit('connection', array(
$connection
));
}
}
41 changes: 41 additions & 0 deletions tests/ServerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@

use React\EventLoop\Factory;
use React\Socket\Server;
use React\Socket\TcpConnector;
use React\Socket\UnixConnector;
use Clue\React\Block;
use React\Socket\ConnectionInterface;

class ServerTest extends TestCase
{
const TIMEOUT = 0.1;

public function testCreateServer()
{
$loop = Factory::create();
Expand All @@ -26,6 +30,38 @@ public function testConstructorThrowsForInvalidUri()
$server = new Server('invalid URI', $loop);
}

public function testConstructorCreatesExpectedTcpServer()
{
$loop = Factory::create();

$server = new Server(0, $loop);

$connector = new TcpConnector($loop);
$connector->connect($server->getAddress())
->then($this->expectCallableOnce(), $this->expectCallableNever());

$connection = Block\await($connector->connect($server->getAddress()), $loop, self::TIMEOUT);

$connection->close();
$server->close();
}

public function testConstructorCreatesExpectedUnixServer()
{
$loop = Factory::create();

$server = new Server($this->getRandomSocketUri(), $loop);

$connector = new UnixConnector($loop);
$connector->connect($server->getAddress())
->then($this->expectCallableOnce(), $this->expectCallableNever());

$connection = Block\await($connector->connect($server->getAddress()), $loop, self::TIMEOUT);

$connection->close();
$server->close();
}

public function testEmitsConnectionForNewConnection()
{
$loop = Factory::create();
Expand Down Expand Up @@ -127,4 +163,9 @@ public function testDoesNotEmitSecureConnectionForNewPlainConnection()

Block\sleep(0.1, $loop);
}

private function getRandomSocketUri()
{
return "unix://" . sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid(rand(), true) . '.sock';
}
}
Loading

0 comments on commit 626bf5b

Please sign in to comment.