From 24f4abc47ef769552d5bfa6f2a29e13414d9fe3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 26 Jul 2021 16:35:46 +0200 Subject: [PATCH] Add new SocketServer and deprecate Server to avoid class name collisions --- README.md | 100 ++++++------ examples/01-echo-server.php | 13 +- examples/02-chat-server.php | 20 +-- examples/03-http-server.php | 13 +- examples/91-benchmark-server.php | 13 +- src/Server.php | 38 +++++ src/ServerInterface.php | 18 +-- src/SocketServer.php | 93 ++++++++++++ tests/SocketServerTest.php | 252 +++++++++++++++++++++++++++++++ 9 files changed, 467 insertions(+), 93 deletions(-) create mode 100644 src/SocketServer.php create mode 100644 tests/SocketServerTest.php diff --git a/README.md b/README.md index f8e7ae2b..9fb2ec5d 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ handle multiple concurrent connections without blocking. * [pause()](#pause) * [resume()](#resume) * [close()](#close) - * [Server](#server) + * [SocketServer](#socketserver) * [Advanced server usage](#advanced-server-usage) * [TcpServer](#tcpserver) * [SecureServer](#secureserver) @@ -58,7 +58,7 @@ handle multiple concurrent connections without blocking. Here is a server that closes the connection if you send it anything: ```php -$socket = new React\Socket\Server('127.0.0.1:8080'); +$socket = new React\Socket\SocketServer('127.0.0.1:8080'); $socket->on('connection', function (React\Socket\ConnectionInterface $connection) { $connection->write("Hello " . $connection->getRemoteAddress() . "!\n"); @@ -214,7 +214,7 @@ The `connection` event will be emitted whenever a new connection has been established, i.e. a new client connects to this server socket: ```php -$server->on('connection', function (React\Socket\ConnectionInterface $connection) { +$socket->on('connection', function (React\Socket\ConnectionInterface $connection) { echo 'new connection' . PHP_EOL; }); ``` @@ -228,7 +228,7 @@ The `error` event will be emitted whenever there's an error accepting a new connection from a client. ```php -$server->on('error', function (Exception $e) { +$socket->on('error', function (Exception $e) { echo 'error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -243,7 +243,7 @@ The `getAddress(): ?string` method can be used to return the full address (URI) this server is currently listening on. ```php -$address = $server->getAddress(); +$address = $socket->getAddress(); echo 'Server listening on ' . $address . PHP_EOL; ``` @@ -260,7 +260,7 @@ If this is a TCP/IP based server and you only want the local port, you may use something like this: ```php -$address = $server->getAddress(); +$address = $socket->getAddress(); $port = parse_url($address, PHP_URL_PORT); echo 'Server listening on port ' . $port . PHP_EOL; ``` @@ -284,9 +284,9 @@ Once the server is paused, no futher `connection` events SHOULD be emitted. ```php -$server->pause(); +$socket->pause(); -$server->on('connection', assertShouldNeverCalled()); +$socket->on('connection', assertShouldNeverCalled()); ``` This method is advisory-only, though generally not recommended, the @@ -309,10 +309,10 @@ resume accepting new incoming connections. Re-attach the socket resource to the EventLoop after a previous `pause()`. ```php -$server->pause(); +$socket->pause(); -Loop::addTimer(1.0, function () use ($server) { - $server->resume(); +Loop::addTimer(1.0, function () use ($socket) { + $socket->resume(); }); ``` @@ -329,53 +329,55 @@ This will stop listening for new incoming connections on this socket. ```php echo 'Shutting down server socket' . PHP_EOL; -$server->close(); +$socket->close(); ``` Calling this method more than once on the same instance is a NO-OP. -### Server +### SocketServer -The `Server` class is the main class in this package that implements the + + +The `SocketServer` 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 React\Socket\Server(8080); -``` - -As above, the `$uri` parameter can consist of only a port, in which case the -server will default to listening on the localhost address `127.0.0.1`, -which means it will not be reachable from outside of this system. - -In order to use a random port assignment, you can use the port `0`: +In order to accept plaintext TCP/IP connections, you can simply pass a host +and port combination like this: ```php -$server = new React\Socket\Server(0); -$address = $server->getAddress(); +$socket = new React\Socket\SocketServer('127.0.0.1:8080'); ``` +Listening on the localhost address `127.0.0.1` means it will not be reachable from +outside of this system. In order to change the host the socket is listening on, you can provide an IP -address through the first parameter provided to the constructor, optionally -preceded by the `tcp://` scheme: +address of an interface or use the special `0.0.0.0` address to listen on all +interfaces: ```php -$server = new React\Socket\Server('192.168.0.1:8080'); +$socket = new React\Socket\SocketServer('0.0.0.0:8080'); ``` If you want to listen on an IPv6 address, you MUST enclose the host in square brackets: ```php -$server = new React\Socket\Server('[::1]:8080'); +$socket = new React\Socket\SocketServer('[::1]:8080'); +``` + +In order to use a random port assignment, you can use the port `0`: + +```php +$socket = new React\Socket\SocketServer('127.0.0.1:0'); +$address = $socket->getAddress(); ``` To listen on a Unix domain socket (UDS) path, you MUST prefix the URI with the `unix://` scheme: ```php -$server = new React\Socket\Server('unix:///tmp/server.sock'); +$socket = new React\Socket\SocketServer('unix:///tmp/server.sock'); ``` If the given URI is invalid, does not contain a port, any other scheme or if it @@ -383,7 +385,7 @@ contains a hostname, it will throw an `InvalidArgumentException`: ```php // throws InvalidArgumentException due to missing port -$server = new React\Socket\Server('127.0.0.1'); +$socket = new React\Socket\SocketServer('127.0.0.1'); ``` If the given URI appears to be valid, but listening on it fails (such as if port @@ -391,10 +393,10 @@ is already in use or port below 1024 may require root access etc.), it will throw a `RuntimeException`: ```php -$first = new React\Socket\Server(8080); +$first = new React\Socket\SocketServer('127.0.0.1:8080'); // throws RuntimeException because port is already in use -$second = new React\Socket\Server(8080); +$second = new React\Socket\SocketServer('127.0.0.1:8080'); ``` > Note that these error conditions may vary depending on your system and/or @@ -402,17 +404,11 @@ $second = new React\Socket\Server(8080); See the exception message and code for more details about the actual error condition. -This class takes an optional `LoopInterface|null $loop` parameter that can be used to -pass the event loop instance to use for this object. You can use a `null` value -here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). -This value SHOULD NOT be given unless you're sure you want to explicitly use a -given event loop instance. - Optionally, you can specify [TCP socket context options](https://www.php.net/manual/en/context.socket.php) for the underlying stream socket resource like this: ```php -$server = new React\Socket\Server('[::1]:8080', null, array( +$socket = new React\Socket\SocketServer('[::1]:8080', array( 'tcp' => array( 'backlog' => 200, 'so_reuseport' => true, @@ -426,8 +422,6 @@ $server = new React\Socket\Server('[::1]:8080', null, array( and/or PHP version. Passing unknown context options has no effect. The `backlog` context option defaults to `511` unless given explicitly. - For BC reasons, you can also pass the TCP socket context options as a simple - array without wrapping this in another array under the `tcp` key. You can start a secure TLS (formerly known as SSL) server by simply prepending the `tls://` URI scheme. @@ -438,7 +432,7 @@ which in its most basic form may look something like this if you're using a PEM encoded certificate file: ```php -$server = new React\Socket\Server('tls://127.0.0.1:8080', null, array( +$socket = new React\Socket\SocketServer('tls://127.0.0.1:8080', array( 'tls' => array( 'local_cert' => 'server.pem' ) @@ -454,7 +448,7 @@ If your private key is encrypted with a passphrase, you have to specify it like this: ```php -$server = new React\Socket\Server('tls://127.0.0.1:8000', null, array( +$socket = new React\Socket\SocketServer('tls://127.0.0.1:8000', array( 'tls' => array( 'local_cert' => 'server.pem', 'passphrase' => 'secret' @@ -467,7 +461,7 @@ SSLv2/SSLv3. As of PHP 5.6+ you can also explicitly choose the TLS version you want to negotiate with the remote side: ```php -$server = new React\Socket\Server('tls://127.0.0.1:8000', null, array( +$socket = new React\Socket\SocketServer('tls://127.0.0.1:8000', array( 'tls' => array( 'local_cert' => 'server.pem', 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER @@ -488,7 +482,7 @@ Whenever a client connects, it will emit a `connection` event with a connection instance implementing [`ConnectionInterface`](#connectioninterface): ```php -$server->on('connection', function (React\Socket\ConnectionInterface $connection) { +$socket->on('connection', function (React\Socket\ConnectionInterface $connection) { echo 'Plaintext connection from ' . $connection->getRemoteAddress() . PHP_EOL; $connection->write('hello there!' . PHP_EOL); @@ -498,10 +492,20 @@ $server->on('connection', function (React\Socket\ConnectionInterface $connection See also the [`ServerInterface`](#serverinterface) for more details. -> Note that the `Server` class is a concrete implementation for TCP/IP sockets. +This class takes an optional `LoopInterface|null $loop` parameter that can be used to +pass the event loop instance to use for this object. You can use a `null` value +here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). +This value SHOULD NOT be given unless you're sure you want to explicitly use a +given event loop instance. + +> Note that the `SocketServer` class is a concrete implementation for TCP/IP sockets. If you want to typehint in your higher-level protocol implementation, you SHOULD use the generic [`ServerInterface`](#serverinterface) instead. +> Changelog v1.9.0: This class has been added with an improved constructor signature + as a replacement for the previous `Server` class in order to avoid any ambiguities. + The previous name has been deprecated and should not be used anymore. + ### Advanced server usage #### TcpServer diff --git a/examples/01-echo-server.php b/examples/01-echo-server.php index 8a729009..1ec645de 100644 --- a/examples/01-echo-server.php +++ b/examples/01-echo-server.php @@ -3,7 +3,7 @@ // Just start this server and connect to it. Everything you send to it will be // sent back to you. // -// $ php examples/01-echo-server.php 8000 +// $ php examples/01-echo-server.php 127.0.0.1:8000 // $ telnet localhost 8000 // // You can also run a secure TLS echo server like this: @@ -16,22 +16,19 @@ // $ php examples/01-echo-server.php unix:///tmp/server.sock // $ nc -U /tmp/server.sock -use React\Socket\Server; -use React\Socket\ConnectionInterface; - require __DIR__ . '/../vendor/autoload.php'; -$server = new Server(isset($argv[1]) ? $argv[1] : 0, null, array( +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '127.0.0.1:0', array( 'tls' => array( 'local_cert' => isset($argv[2]) ? $argv[2] : (__DIR__ . '/localhost.pem') ) )); -$server->on('connection', function (ConnectionInterface $connection) { +$socket->on('connection', function (React\Socket\ConnectionInterface $connection) { echo '[' . $connection->getRemoteAddress() . ' connected]' . PHP_EOL; $connection->pipe($connection); }); -$server->on('error', 'printf'); +$socket->on('error', 'printf'); -echo 'Listening on ' . $server->getAddress() . PHP_EOL; +echo 'Listening on ' . $socket->getAddress() . PHP_EOL; diff --git a/examples/02-chat-server.php b/examples/02-chat-server.php index ede541d9..9027f28b 100644 --- a/examples/02-chat-server.php +++ b/examples/02-chat-server.php @@ -3,7 +3,7 @@ // Just start this server and connect with any number of clients to it. // Everything a client sends will be broadcasted to all connected clients. // -// $ php examples/02-chat-server.php 8000 +// $ php examples/02-chat-server.php 127.0.0.1:8000 // $ telnet localhost 8000 // // You can also run a secure TLS chat server like this: @@ -16,23 +16,19 @@ // $ php examples/02-chat-server.php unix:///tmp/server.sock // $ nc -U /tmp/server.sock -use React\Socket\Server; -use React\Socket\ConnectionInterface; -use React\Socket\LimitingServer; - require __DIR__ . '/../vendor/autoload.php'; -$server = new Server(isset($argv[1]) ? $argv[1] : 0, null, array( +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '127.0.0.1:0', array( 'tls' => array( 'local_cert' => isset($argv[2]) ? $argv[2] : (__DIR__ . '/localhost.pem') ) )); -$server = new LimitingServer($server, null); +$socket = new React\Socket\LimitingServer($socket, null); -$server->on('connection', function (ConnectionInterface $client) use ($server) { +$socket->on('connection', function (React\Socket\ConnectionInterface $client) use ($socket) { // whenever a new message comes in - $client->on('data', function ($data) use ($client, $server) { + $client->on('data', function ($data) use ($client, $socket) { // remove any non-word characters (just for the demo) $data = trim(preg_replace('/[^\w\d \.\,\-\!\?]/u', '', $data)); @@ -43,12 +39,12 @@ // prefix with client IP and broadcast to all connected clients $data = trim(parse_url($client->getRemoteAddress(), PHP_URL_HOST), '[]') . ': ' . $data . PHP_EOL; - foreach ($server->getConnections() as $connection) { + foreach ($socket->getConnections() as $connection) { $connection->write($data); } }); }); -$server->on('error', 'printf'); +$socket->on('error', 'printf'); -echo 'Listening on ' . $server->getAddress() . PHP_EOL; +echo 'Listening on ' . $socket->getAddress() . PHP_EOL; diff --git a/examples/03-http-server.php b/examples/03-http-server.php index 5b96646c..cc6440fb 100644 --- a/examples/03-http-server.php +++ b/examples/03-http-server.php @@ -12,7 +12,7 @@ // // Just start this server and send a request to it: // -// $ php examples/03-http-server.php 8000 +// $ php examples/03-http-server.php 127.0.0.1:8000 // $ curl -v http://localhost:8000/ // $ ab -n1000 -c10 http://localhost:8000/ // $ docker run -it --rm --net=host jordi/ab ab -n1000 -c10 http://localhost:8000/ @@ -29,24 +29,21 @@ // $ php examples/03-http-server.php unix:///tmp/server.sock // $ nc -U /tmp/server.sock -use React\Socket\Server; -use React\Socket\ConnectionInterface; - require __DIR__ . '/../vendor/autoload.php'; -$server = new Server(isset($argv[1]) ? $argv[1] : 0, null, array( +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '127.0.0.1:0', array( 'tls' => array( 'local_cert' => isset($argv[2]) ? $argv[2] : (__DIR__ . '/localhost.pem') ) )); -$server->on('connection', function (ConnectionInterface $connection) { +$socket->on('connection', function (React\Socket\ConnectionInterface $connection) { $connection->once('data', function () use ($connection) { $body = "

Hello world!

\r\n"; $connection->end("HTTP/1.1 200 OK\r\nContent-Length: " . strlen($body) . "\r\nConnection: close\r\n\r\n" . $body); }); }); -$server->on('error', 'printf'); +$socket->on('error', 'printf'); -echo 'Listening on ' . strtr($server->getAddress(), array('tcp:' => 'http:', 'tls:' => 'https:')) . PHP_EOL; +echo 'Listening on ' . strtr($socket->getAddress(), array('tcp:' => 'http:', 'tls:' => 'https:')) . PHP_EOL; diff --git a/examples/91-benchmark-server.php b/examples/91-benchmark-server.php index b54c5cfc..0e3e2025 100644 --- a/examples/91-benchmark-server.php +++ b/examples/91-benchmark-server.php @@ -4,7 +4,7 @@ // sent for each connection and will print the average throughput once the // connection closes. // -// $ php examples/91-benchmark-server.php 8000 +// $ php examples/91-benchmark-server.php 127.0.0.1:8000 // $ telnet localhost 8000 // $ echo hello world | nc -N localhost 8000 // $ dd if=/dev/zero bs=1M count=1000 | nc -N localhost 8000 @@ -22,18 +22,15 @@ // $ nc -N -U /tmp/server.sock // $ dd if=/dev/zero bs=1M count=1000 | nc -N -U /tmp/server.sock -use React\Socket\Server; -use React\Socket\ConnectionInterface; - require __DIR__ . '/../vendor/autoload.php'; -$server = new Server(isset($argv[1]) ? $argv[1] : 0, null, array( +$socket = new React\Socket\SocketServer(isset($argv[1]) ? $argv[1] : '127.0.0.1:0', array( 'tls' => array( 'local_cert' => isset($argv[2]) ? $argv[2] : (__DIR__ . '/localhost.pem') ) )); -$server->on('connection', function (ConnectionInterface $connection) { +$socket->on('connection', function (React\Socket\ConnectionInterface $connection) { echo '[connected]' . PHP_EOL; // count the number of bytes received from this connection @@ -50,6 +47,6 @@ }); }); -$server->on('error', 'printf'); +$socket->on('error', 'printf'); -echo 'Listening on ' . $server->getAddress() . PHP_EOL; +echo 'Listening on ' . $socket->getAddress() . PHP_EOL; diff --git a/src/Server.php b/src/Server.php index 193fe0d0..7d4111e8 100644 --- a/src/Server.php +++ b/src/Server.php @@ -7,10 +7,48 @@ use React\EventLoop\LoopInterface; use Exception; +/** + * @deprecated 1.9.0 See `SocketServer` instead + * @see SocketServer + */ final class Server extends EventEmitter implements ServerInterface { private $server; + /** + * [Deprecated] `Server` + * + * This class exists for BC reasons only and should not be used anymore. + * + * ```php + * // deprecated + * $socket = new React\Socket\Server(0); + * $socket = new React\Socket\Server('127.0.0.1:8000'); + * $socket = new React\Socket\Server('127.0.0.1:8000', null, $context); + * $socket = new React\Socket\Server('127.0.0.1:8000', $loop, $context); + * + * // new + * $socket = new React\Socket\SocketServer('127.0.0.1:0'); + * $socket = new React\Socket\SocketServer('127.0.0.1:8000'); + * $socket = new React\Socket\SocketServer('127.0.0.1:8000', $context); + * $socket = new React\Socket\SocketServer('127.0.0.1:8000', $context, $loop); + * ``` + * + * This class takes an optional `LoopInterface|null $loop` parameter that can be used to + * pass the event loop instance to use for this object. You can use a `null` value + * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). + * This value SHOULD NOT be given unless you're sure you want to explicitly use a + * given event loop instance. + * + * For BC reasons, you can also pass the TCP socket context options as a simple + * array without wrapping this in another array under the `tcp` key. + * + * @param string|int $uri + * @param LoopInterface $loop + * @param array $context + * @deprecated 1.9.0 See `SocketServer` instead + * @see SocketServer + */ public function __construct($uri, LoopInterface $loop = null, array $context = array()) { $loop = $loop ?: Loop::get(); diff --git a/src/ServerInterface.php b/src/ServerInterface.php index 694abbf1..aa79fa17 100644 --- a/src/ServerInterface.php +++ b/src/ServerInterface.php @@ -23,7 +23,7 @@ * established, i.e. a new client connects to this server socket: * * ```php - * $server->on('connection', function (React\Socket\ConnectionInterface $connection) { + * $socket->on('connection', function (React\Socket\ConnectionInterface $connection) { * echo 'new connection' . PHP_EOL; * }); * ``` @@ -36,7 +36,7 @@ * connection from a client. * * ```php - * $server->on('error', function (Exception $e) { + * $socket->on('error', function (Exception $e) { * echo 'error: ' . $e->getMessage() . PHP_EOL; * }); * ``` @@ -52,7 +52,7 @@ interface ServerInterface extends EventEmitterInterface * Returns the full address (URI) this server is currently listening on * * ```php - * $address = $server->getAddress(); + * $address = $socket->getAddress(); * echo 'Server listening on ' . $address . PHP_EOL; * ``` * @@ -68,7 +68,7 @@ interface ServerInterface extends EventEmitterInterface * use something like this: * * ```php - * $address = $server->getAddress(); + * $address = $socket->getAddress(); * $port = parse_url($address, PHP_URL_PORT); * echo 'Server listening on port ' . $port . PHP_EOL; * ``` @@ -94,9 +94,9 @@ public function getAddress(); * be emitted. * * ```php - * $server->pause(); + * $socket->pause(); * - * $server->on('connection', assertShouldNeverCalled()); + * $socket->on('connection', assertShouldNeverCalled()); * ``` * * This method is advisory-only, though generally not recommended, the @@ -122,10 +122,10 @@ public function pause(); * Re-attach the socket resource to the EventLoop after a previous `pause()`. * * ```php - * $server->pause(); + * $socket->pause(); * - * Loop::addTimer(1.0, function () use ($server) { - * $server->resume(); + * Loop::addTimer(1.0, function () use ($socket) { + * $socket->resume(); * }); * ``` * diff --git a/src/SocketServer.php b/src/SocketServer.php new file mode 100644 index 00000000..973bbaf8 --- /dev/null +++ b/src/SocketServer.php @@ -0,0 +1,93 @@ + array(), + 'tls' => array(), + 'unix' => array() + ); + + $scheme = 'tcp'; + $pos = \strpos($uri, '://'); + if ($pos !== false) { + $scheme = \substr($uri, 0, $pos); + } + + if ($scheme === 'unix') { + $server = new UnixServer($uri, $loop, $context['unix']); + } else { + if (preg_match('#^(?:\w+://)?\d+$#', $uri)) { + throw new \InvalidArgumentException('Invalid URI given'); + } + + $server = new TcpServer(str_replace('tls://', '', $uri), $loop, $context['tcp']); + + if ($scheme === 'tls') { + $server = new SecureServer($server, $loop, $context['tls']); + } + } + + $this->server = $server; + + $that = $this; + $server->on('connection', function (ConnectionInterface $conn) use ($that) { + $that->emit('connection', array($conn)); + }); + $server->on('error', function (\Exception $error) use ($that) { + $that->emit('error', array($error)); + }); + } + + public function getAddress() + { + return $this->server->getAddress(); + } + + public function pause() + { + $this->server->pause(); + } + + public function resume() + { + $this->server->resume(); + } + + public function close() + { + $this->server->close(); + } +} diff --git a/tests/SocketServerTest.php b/tests/SocketServerTest.php new file mode 100644 index 00000000..536709a6 --- /dev/null +++ b/tests/SocketServerTest.php @@ -0,0 +1,252 @@ +setAccessible(true); + $tcp = $ref->getValue($socket); + + $ref = new \ReflectionProperty($tcp, 'loop'); + $ref->setAccessible(true); + $loop = $ref->getValue($tcp); + + $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); + } + + public function testCreateServerWithZeroPortAssignsRandomPort() + { + $loop = Factory::create(); + + $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $this->assertNotEquals(0, $socket->getAddress()); + $socket->close(); + } + + public function testConstructorWithInvalidUriThrows() + { + $this->setExpectedException('InvalidArgumentException'); + new SocketServer('invalid URI'); + } + + public function testConstructorWithInvalidUriWithPortOnlyThrows() + { + $this->setExpectedException('InvalidArgumentException'); + new SocketServer('0'); + } + + public function testConstructorWithInvalidUriWithSchemaAndPortOnlyThrows() + { + $this->setExpectedException('InvalidArgumentException'); + new SocketServer('tcp://0'); + } + + public function testConstructorCreatesExpectedTcpServer() + { + $loop = Factory::create(); + + $socket = new SocketServer('127.0.0.1:0', array(), $loop); + + $connector = new TcpConnector($loop); + $connector->connect($socket->getAddress()) + ->then($this->expectCallableOnce(), $this->expectCallableNever()); + + $connection = Block\await($connector->connect($socket->getAddress()), $loop, self::TIMEOUT); + + $connection->close(); + $socket->close(); + } + + public function testConstructorCreatesExpectedUnixServer() + { + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on legacy HHVM'); + } + if (!in_array('unix', stream_get_transports())) { + $this->markTestSkipped('Unix domain sockets (UDS) not supported on your platform (Windows?)'); + } + + $loop = Factory::create(); + + $socket = new SocketServer($this->getRandomSocketUri(), array(), $loop); + + $connector = new UnixConnector($loop); + $connector->connect($socket->getAddress()) + ->then($this->expectCallableOnce(), $this->expectCallableNever()); + + $connection = Block\await($connector->connect($socket->getAddress()), $loop, self::TIMEOUT); + + $connection->close(); + $socket->close(); + } + + public function testConstructorThrowsForExistingUnixPath() + { + if (!in_array('unix', stream_get_transports())) { + $this->markTestSkipped('Unix domain sockets (UDS) not supported on your platform (Windows?)'); + } + + $loop = Factory::create(); + + try { + new SocketServer('unix://' . __FILE__, array(), $loop); + $this->fail(); + } catch (\RuntimeException $e) { + if ($e->getCode() === 0) { + // Zend PHP does not currently report a sane error + $this->assertStringEndsWith('Unknown error', $e->getMessage()); + } else { + $this->assertEquals(SOCKET_EADDRINUSE, $e->getCode()); + $this->assertStringEndsWith('Address already in use', $e->getMessage()); + } + } + } + + public function testEmitsErrorWhenUnderlyingTcpServerEmitsError() + { + $loop = Factory::create(); + + $socket = new SocketServer('127.0.0.1:0', array(), $loop); + + $ref = new \ReflectionProperty($socket, 'server'); + $ref->setAccessible(true); + $tcp = $ref->getvalue($socket); + + $error = new \RuntimeException(); + $socket->on('error', $this->expectCallableOnceWith($error)); + $tcp->emit('error', array($error)); + + $socket->close(); + } + + public function testEmitsConnectionForNewConnection() + { + $loop = Factory::create(); + + $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket->on('connection', $this->expectCallableOnce()); + + $peer = new Promise(function ($resolve, $reject) use ($socket) { + $socket->on('connection', $resolve); + }); + + $client = stream_socket_client($socket->getAddress()); + + Block\await($peer, $loop, self::TIMEOUT); + } + + public function testDoesNotEmitConnectionForNewConnectionToPausedServer() + { + $loop = Factory::create(); + + $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket->pause(); + $socket->on('connection', $this->expectCallableNever()); + + $client = stream_socket_client($socket->getAddress()); + + Block\sleep(0.1, $loop); + } + + public function testDoesEmitConnectionForNewConnectionToResumedServer() + { + $loop = Factory::create(); + + $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket->pause(); + $socket->on('connection', $this->expectCallableOnce()); + + $peer = new Promise(function ($resolve, $reject) use ($socket) { + $socket->on('connection', $resolve); + }); + + $client = stream_socket_client($socket->getAddress()); + + $socket->resume(); + + Block\await($peer, $loop, self::TIMEOUT); + } + + public function testDoesNotAllowConnectionToClosedServer() + { + $loop = Factory::create(); + + $socket = new SocketServer('127.0.0.1:0', array(), $loop); + $socket->on('connection', $this->expectCallableNever()); + $address = $socket->getAddress(); + $socket->close(); + + $client = @stream_socket_client($address); + + $this->assertFalse($client); + } + + public function testEmitsConnectionWithInheritedContextOptions() + { + if (defined('HHVM_VERSION') && version_compare(HHVM_VERSION, '3.13', '<')) { + // https://3v4l.org/hB4Tc + $this->markTestSkipped('Not supported on legacy HHVM < 3.13'); + } + + $loop = Factory::create(); + + $socket = new SocketServer('127.0.0.1:0', array( + 'tcp' => array( + 'backlog' => 4 + ) + ), $loop); + + $peer = new Promise(function ($resolve, $reject) use ($socket) { + $socket->on('connection', function (ConnectionInterface $connection) use ($resolve) { + $resolve(stream_context_get_options($connection->stream)); + }); + }); + + + $client = stream_socket_client($socket->getAddress()); + + $all = Block\await($peer, $loop, self::TIMEOUT); + + $this->assertEquals(array('socket' => array('backlog' => 4)), $all); + } + + public function testDoesNotEmitSecureConnectionForNewPlaintextConnectionThatIsIdle() + { + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Not supported on legacy HHVM'); + } + + $loop = Factory::create(); + + $socket = new SocketServer('tls://127.0.0.1:0', array( + 'tls' => array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + ) + ), $loop); + $socket->on('connection', $this->expectCallableNever()); + + $client = stream_socket_client(str_replace('tls://', '', $socket->getAddress())); + + Block\sleep(0.1, $loop); + } + + private function getRandomSocketUri() + { + return "unix://" . sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid(rand(), true) . '.sock'; + } +}