Skip to content

Support Unix domain socket (UDS) server #120

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 16, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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']);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe export the Server constructing logic to a separate method like this: the-eater@2defb1b?diff=split

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functionally LGTM, however I don't currently see this covered in the test suite (plus minor CS issue above). Can you update this? :shipit: 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@clue what's your expectation here? Testing the type of the create private server member? Would need a hint how to do this due to the private nature.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class is a facade that mostly contains some glue code to make the underlying APIs more easily accessible. Just check the other high-level functional tests and the code coverage. IMHO it's sufficient to create an Unix domain socket server with this facade and check a client connection can be established.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. Tests added.

}

$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
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy