diff --git a/README.md b/README.md index e8b53a01..adef7b00 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,9 @@ handle multiple concurrent connections without blocking. * [ConnectionInterface](#connectioninterface) * [getRemoteAddress()](#getremoteaddress) * [getLocalAddress()](#getlocaladdress) + * [UnixConnection](#connectioninterface) + * [getRemotePid()](#getremotepid) + * [getLocalPid()](#getlocalpid) * [Server usage](#server-usage) * [ServerInterface](#serverinterface) * [connection event](#connection-event) @@ -195,6 +198,36 @@ If your system has multiple interfaces (e.g. a WAN and a LAN interface), you can use this method to find out which interface was actually used for this connection. + +### UnixConnection +The `UnixConnection` is a specific implementation of a `ConnectionInterface` used to represent any +incoming and outgoing connection over a Unix domain socket (UDS). + +#### getRemotePid() +The `getRemotePid(): ?int` method returns the PID where this connection has been established with. + +```php +$pid = $connection->getRemotePid(); +echo 'Remote PID: ' . $pid . PHP_EOL; +``` + +If the remote PID can not be determined or is unknown at this time (such as +after the connection has been closed), it MAY return a `NULL` value instead. +If the remote PID was already requested before the connection was closed the PID will still be cached. +The return value of this method should NOT be used to determine if the remote side of the socket is +The return value of this method should NOT be used to determine if the remote side of the socket is +still connected. + +If the remote PID is known, it will return the PID as an integer value, such as `1640`. + +#### getLocalPid() +The `getLocalPid(): ?int` method returns the PID where this connection has been established from (the current process). + +```php +$pid = $connection->getLocalPid(); +echo 'Local PID: ' . $pid . PHP_EOL; +``` + ## Server usage ### ServerInterface @@ -706,11 +739,12 @@ $first = new UnixServer('/tmp/same.sock', $loop); $second = new UnixServer('/tmp/same.sock', $loop); ``` -Whenever a client connects, it will emit a `connection` event with a connection -instance implementing [`ConnectionInterface`](#connectioninterface): +Whenever a client connects, it will emit a `connection` event with a `UnixConnection` +instance implementing [`ConnectionInterface`](#connectioninterface). The `UnixConnection` has extra methods for +retrieving the PID (process identifier) from both sides of the socket (remote and local). ```php -$server->on('connection', function (ConnectionInterface $connection) { +$server->on('connection', function (UnixConnection $connection) { echo 'New connection' . PHP_EOL; $connection->write('hello there!' . PHP_EOL); @@ -900,7 +934,7 @@ In order to create a local Unix domain socket connection, you can use the `unix://` URI scheme like this: ```php -$connector->connect('unix:///tmp/demo.sock')->then(function (ConnectionInterface $connection) { +$connector->connect('unix:///tmp/demo.sock')->then(function (UnixConnection $connection) { $connection->write('...'); $connection->end(); }); @@ -1300,7 +1334,7 @@ Unix domain socket (UDS) paths like this: ```php $connector = new React\Socket\UnixConnector($loop); -$connector->connect('/tmp/demo.sock')->then(function (ConnectionInterface $connection) { +$connector->connect('/tmp/demo.sock')->then(function (UnixConnection $connection) { $connection->write("HELLO\n"); }); diff --git a/src/Connection.php b/src/Connection.php index c6267ccc..14306891 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -19,13 +19,6 @@ */ class Connection extends EventEmitter implements ConnectionInterface { - /** - * Internal flag whether this is a Unix domain socket (UDS) connection - * - * @internal - */ - public $unix = false; - /** * Internal flag whether encryption has been enabled on this connection * @@ -144,28 +137,12 @@ public function getLocalAddress() return $this->parseAddress(@stream_socket_get_name($this->stream, false)); } - private function parseAddress($address) + protected function parseAddress($address) { if ($address === false) { return null; } - if ($this->unix) { - // remove trailing colon from address for HHVM < 3.19: https://3v4l.org/5C1lo - // note that technically ":" is a valid address, so keep this in place otherwise - if (substr($address, -1) === ':' && defined('HHVM_VERSION_ID') && HHVM_VERSION_ID < 31900) { - $address = (string)substr($address, 0, -1); - } - - // work around unknown addresses should return null value: https://3v4l.org/5C1lo and https://bugs.php.net/bug.php?id=74556 - // PHP uses "\0" string and HHVM uses empty string (colon removed above) - if ($address === '' || $address[0] === "\x00" ) { - return null; - } - - return 'unix://' . $address; - } - // check if this is an IPv6 address which includes multiple colons but no square brackets $pos = strrpos($address, ':'); if ($pos !== false && strpos($address, ':') < $pos && substr($address, 0, 1) !== '[') { diff --git a/src/UnixConnection.php b/src/UnixConnection.php new file mode 100644 index 00000000..0f4a8ab2 --- /dev/null +++ b/src/UnixConnection.php @@ -0,0 +1,120 @@ +remote_pid !== null) { + return $this->remote_pid; + } + + if (get_resource_type($this->stream) !== self::RESOURCE_TYPE_STREAM) { + return null; + } + + $socket = socket_import_stream($this->stream); + + if ($socket === false || $socket === null) { + return null; + } + + // Get the PID of the remote side of the socket. + $pid = socket_get_option($socket, SOL_SOCKET, self::SO_PEERCRED); + + if ($pid === false) { + return null; + } + + $this->remote_pid = (int)$pid; + + return $this->remote_pid; + } + + /** + * Retrieve the PID (process identifier) of the local side of the Connection. + * + * Warning: Process IDs are not unique, thus they are a weak entropy source. Relying on pids in security-dependent + * contexts should be avoided. + * + * @return int|null + */ + public function getLocalPid() + { + if ($this->local_pid !== null) { + return $this->local_pid; + } + + $pid = getmypid(); + if ($pid === false) { + return null; + } + + $this->local_pid = $pid; + + return $this->local_pid; + } +} diff --git a/src/UnixConnector.php b/src/UnixConnector.php index 9b84ab01..262bb93e 100644 --- a/src/UnixConnector.php +++ b/src/UnixConnector.php @@ -36,8 +36,7 @@ public function connect($path) return Promise\reject(new RuntimeException('Unable to connect to unix domain socket "' . $path . '": ' . $errstr, $errno)); } - $connection = new Connection($resource, $this->loop); - $connection->unix = true; + $connection = new UnixConnection($resource, $this->loop); return Promise\resolve($connection); } diff --git a/src/UnixServer.php b/src/UnixServer.php index 8f1ed983..7d768d81 100644 --- a/src/UnixServer.php +++ b/src/UnixServer.php @@ -120,8 +120,7 @@ public function close() /** @internal */ public function handleConnection($socket) { - $connection = new Connection($socket, $this->loop); - $connection->unix = true; + $connection = new UnixConnection($socket, $this->loop); $this->emit('connection', array( $connection diff --git a/tests/FunctionalUnixServerTest.php b/tests/FunctionalUnixServerTest.php new file mode 100644 index 00000000..d60cb3ef --- /dev/null +++ b/tests/FunctionalUnixServerTest.php @@ -0,0 +1,135 @@ +getRandomSocketUri(), $loop); + $server->on('connection', $this->expectCallableOnce()); + + $connector = new UnixConnector($loop); + $promise = $connector->connect($server->getAddress()); + + $promise->then($this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + } + + public function testEmitsNoConnectionForNewConnectionWhenPaused() + { + $loop = Factory::create(); + + $server = new UnixServer($this->getRandomSocketUri(), $loop); + $server->on('connection', $this->expectCallableNever()); + $server->pause(); + + $connector = new UnixConnector($loop); + $promise = $connector->connect($server->getAddress()); + + $promise->then($this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + } + + public function testEmitsConnectionForNewConnectionWhenResumedAfterPause() + { + $loop = Factory::create(); + + $server = new UnixServer($this->getRandomSocketUri(), $loop); + $server->on('connection', $this->expectCallableOnce()); + $server->pause(); + $server->resume(); + + $connector = new UnixConnector($loop); + $promise = $connector->connect($server->getAddress()); + + $promise->then($this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + } + + public function testConnectionDetailsCanBeRetrieved() + { + $loop = Factory::create(); + + $server = new UnixServer($this->getRandomSocketUri(), $loop); + $remote_address = null; + $remote_pid = null; + $local_address = null; + $local_pid = null; + $server->on('connection', function (UnixConnection $conn) use (&$remote_address, &$remote_pid, &$local_address, &$local_pid) { + $remote_address = $conn->getRemoteAddress(); + $remote_pid = $conn->getRemotePid(); + $local_address = $conn->getLocalAddress(); + $local_pid = $conn->getLocalPid(); + }); + + $connector = new UnixConnector($loop); + $promise = $connector->connect($server->getAddress()); + + $promise->then($this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + + $this->assertNull($remote_address); + $this->assertValidPid($remote_pid); + $this->assertContains('unix:///tmp/', $local_address); + $this->assertSame($server->getAddress(), $local_address); + $this->assertValidPid($local_pid); + } + + public function testRemotePidCannotBeRetrievedAfterConnectionIsClosedLocally() + { + $loop = Factory::create(); + + $server = new UnixServer($this->getRandomSocketUri(), $loop); + $remote_pid = false; + $server->on('connection', function (UnixConnection $conn) use (&$remote_pid) { + $conn->close(); + $remote_pid = $conn->getRemotePid(); + }); + + $connector = new UnixConnector($loop); + $promise = $connector->connect($server->getAddress()); + + $promise->then($this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + + // Will be null since the connection was closed before the pid was requested. + $this->assertNull($remote_pid); + } + + public function testRemotePidCanBeRetrievedAfterConnectionIsClosedLocallyIfRemotePidWasCachedBeforeClose() + { + $loop = Factory::create(); + + $server = new UnixServer($this->getRandomSocketUri(), $loop); + $remote_pid = false; + $server->on('connection', function (UnixConnection $conn) use (&$remote_pid) { + $conn->getRemotePid(); + $conn->close(); + $remote_pid = $conn->getRemotePid(); + }); + + $connector = new UnixConnector($loop); + $promise = $connector->connect($server->getAddress()); + + $promise->then($this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + + // The cached value will be used so it is known. + $this->assertValidPid($remote_pid); + } +} diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 14fdb2c8..9c08b363 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -165,9 +165,4 @@ public function testDoesNotEmitSecureConnectionForNewPlainConnection() Block\sleep(0.1, $loop); } - - private function getRandomSocketUri() - { - return "unix://" . sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid(rand(), true) . '.sock'; - } } diff --git a/tests/TestCase.php b/tests/TestCase.php index e87fc2f1..b6aa9ae5 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -51,6 +51,12 @@ protected function expectCallableNever() return $mock; } + protected function assertValidPid($pid) + { + $this->assertGreaterThan(1, $pid); + $this->assertInternalType("int", $pid); + } + protected function createCallableMock() { return $this->getMockBuilder('React\Tests\Socket\Stub\CallableStub')->getMock(); @@ -98,4 +104,9 @@ public function setExpectedException($exception, $exceptionMessage = '', $except parent::setExpectedException($exception, $exceptionMessage, $exceptionCode); } } + + protected function getRandomSocketUri() + { + return "unix://" . sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid(rand(), true) . '.sock'; + } } diff --git a/tests/UnixConnectorTest.php b/tests/UnixConnectorTest.php index 1564064f..dda28ae5 100644 --- a/tests/UnixConnectorTest.php +++ b/tests/UnixConnectorTest.php @@ -2,12 +2,16 @@ namespace React\Tests\Socket; -use React\Socket\ConnectionInterface; +use PHPUnit\Framework\MockObject\MockObject; +use React\EventLoop\LoopInterface; +use React\Socket\UnixConnection; use React\Socket\UnixConnector; class UnixConnectorTest extends TestCase { + /** @var LoopInterface|MockObject */ private $loop; + /** @var UnixConnector */ private $connector; public function setUp() @@ -38,7 +42,7 @@ public function testValid() // skip test if we can not create a test server (Windows etc.) if (!$server) { - $this->markTestSkipped('Unable to create socket "' . $path . '": ' . $errstr . '(' . $errno .')'); + $this->markTestSkipped('Unable to create socket "' . $path . '": ' . $errstr . '(' . $errno . ')'); return; } @@ -46,11 +50,16 @@ public function testValid() $promise = $this->connector->connect($path); $promise->then($this->expectCallableOnce()); - // remember remote and local address of this connection and close again - $remote = $local = false; - $promise->then(function(ConnectionInterface $conn) use (&$remote, &$local) { - $remote = $conn->getRemoteAddress(); - $local = $conn->getLocalAddress(); + // remember remote and local address and pid of this connection and close again + $remote_address = false; + $remote_pid = false; + $local_address = false; + $local_pid = false; + $promise->then(function (UnixConnection $conn) use (&$remote_address, &$remote_pid, &$local_address, &$local_pid) { + $remote_address = $conn->getRemoteAddress(); + $remote_pid = $conn->getRemotePid(); + $local_address = $conn->getLocalAddress(); + $local_pid = $conn->getLocalPid(); $conn->close(); }); @@ -58,7 +67,10 @@ public function testValid() fclose($server); unlink($path); - $this->assertNull($local); - $this->assertEquals('unix://' . $path, $remote); + $this->assertNull($local_address); + $this->assertSame('unix://' . $path, $remote_address); + + $this->assertValidPid($local_pid); + $this->assertValidPid($remote_pid); } } diff --git a/tests/UnixServerTest.php b/tests/UnixServerTest.php index 10f7e4f6..9b61d750 100644 --- a/tests/UnixServerTest.php +++ b/tests/UnixServerTest.php @@ -271,11 +271,6 @@ public function tearDown() } } - private function getRandomSocketUri() - { - return "unix://" . sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid(rand(), true) . '.sock'; - } - private function tick() { Block\sleep(0, $this->loop);