Skip to content

Commit 2b2f857

Browse files
committed
Support Promise cancellation for SecureConnector
1 parent ba3c45d commit 2b2f857

File tree

4 files changed

+106
-8
lines changed

4 files changed

+106
-8
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,17 @@ $secureConnector->create('www.google.com', 443)->then(function (React\Stream\Str
135135
$loop->run();
136136
```
137137

138+
Pending connection attempts can be cancelled by cancelling its pending promise like so:
139+
140+
```php
141+
$promise = $secureConnector->create($host, $port);
142+
143+
$promise->cancel();
144+
```
145+
146+
Calling `cancel()` on a pending promise will cancel the underlying TCP/IP
147+
connection and/or the SSL/TLS negonation and reject the resulting promise.
148+
138149
You can optionally pass additional
139150
[SSL context options](http://php.net/manual/en/context.ssl.php)
140151
to the constructor like this:

src/SecureConnector.php

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use React\EventLoop\LoopInterface;
66
use React\Stream\Stream;
77
use React\Promise;
8+
use React\Promise\CancellablePromiseInterface;
89

910
class SecureConnector implements ConnectorInterface
1011
{
@@ -39,7 +40,7 @@ public function create($host, $port)
3940
}
4041

4142
$encryption = $this->streamEncryption;
42-
return $this->connector->create($host, $port)->then(function (Stream $stream) use ($context, $encryption) {
43+
return $this->connect($host, $port)->then(function (Stream $stream) use ($context, $encryption) {
4344
// (unencrypted) TCP/IP connection succeeded
4445

4546
// set required SSL/TLS context options
@@ -55,4 +56,30 @@ public function create($host, $port)
5556
});
5657
});
5758
}
59+
60+
private function connect($host, $port)
61+
{
62+
$promise = $this->connector->create($host, $port);
63+
64+
return new Promise\Promise(
65+
function ($resolve, $reject) use ($promise) {
66+
// resolve/reject with result of TCP/IP connection
67+
$promise->then($resolve, $reject);
68+
},
69+
function ($_, $reject) use ($promise) {
70+
// cancellation should reject connection attempt
71+
$reject(new \RuntimeException('Connection attempt cancelled during TCP/IP connection'));
72+
73+
// forefully close TCP/IP connection if it completes despite cancellation
74+
$promise->then(function (Stream $stream) {
75+
$stream->close();
76+
});
77+
78+
// (try to) cancel pending TCP/IP connection
79+
if ($promise instanceof CancellablePromiseInterface) {
80+
$promise->cancel();
81+
}
82+
}
83+
);
84+
}
5885
}

src/StreamEncryption.php

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@ public function toggle(Stream $stream, $toggle)
6666

6767
// TODO: add write() event to make sure we're not sending any excessive data
6868

69-
$deferred = new Deferred();
69+
$deferred = new Deferred(function ($_, $reject) use ($toggle) {
70+
// cancelling this leaves this stream in an inconsistent state…
71+
$reject(new \RuntimeException('Cancelled toggling encryption ' . $toggle ? 'on' : 'off'));
72+
});
7073

7174
// get actual stream socket from stream instance
7275
$socket = $stream->stream;
@@ -82,15 +85,18 @@ public function toggle(Stream $stream, $toggle)
8285
$wrap = $this->wrapSecure && $toggle;
8386
$loop = $this->loop;
8487

85-
return $deferred->promise()->then(function () use ($stream, $wrap, $loop) {
88+
return $deferred->promise()->then(function () use ($stream, $socket, $wrap, $loop) {
89+
$this->loop->removeReadStream($socket);
90+
8691
if ($wrap) {
8792
return new SecureStream($stream, $loop);
8893
}
8994

9095
$stream->resume();
9196

9297
return $stream;
93-
}, function($error) use ($stream) {
98+
}, function($error) use ($stream, $socket) {
99+
$this->loop->removeReadStream($socket);
94100
$stream->resume();
95101
throw $error;
96102
});
@@ -103,12 +109,8 @@ public function toggleCrypto($socket, Deferred $deferred, $toggle)
103109
restore_error_handler();
104110

105111
if (true === $result) {
106-
$this->loop->removeReadStream($socket);
107-
108112
$deferred->resolve();
109113
} else if (false === $result) {
110-
$this->loop->removeReadStream($socket);
111-
112114
$deferred->reject(new UnexpectedValueException(
113115
sprintf("Unable to complete SSL/TLS handshake: %s", $this->errstr),
114116
$this->errno

tests/SecureConnectorTest.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
namespace React\Tests\SocketClient;
4+
5+
use React\Promise;
6+
use React\SocketClient\SecureConnector;
7+
8+
class SecureConnectorTest extends TestCase
9+
{
10+
private $loop;
11+
private $tcp;
12+
private $connector;
13+
14+
public function setUp()
15+
{
16+
$this->loop = $this->getMock('React\EventLoop\LoopInterface');
17+
$this->tcp = $this->getMock('React\SocketClient\ConnectorInterface');
18+
$this->connector = new SecureConnector($this->tcp, $this->loop);
19+
}
20+
21+
public function testConnectionWillWaitForTcpConnection()
22+
{
23+
$pending = new Promise\Promise(function () { });
24+
$this->tcp->expects($this->once())->method('create')->with($this->equalTo('example.com'), $this->equalTo(80))->will($this->returnValue($pending));
25+
26+
$promise = $this->connector->create('example.com', 80);
27+
28+
$this->assertInstanceOf('React\Promise\PromiseInterface', $promise);
29+
}
30+
31+
public function testCancelDuringTcpConnectionCancelsTcpConnection()
32+
{
33+
$pending = new Promise\Promise(function () { }, $this->expectCallableOnce());
34+
$this->tcp->expects($this->once())->method('create')->with($this->equalTo('example.com'), $this->equalTo(80))->will($this->returnValue($pending));
35+
36+
$promise = $this->connector->create('example.com', 80);
37+
$promise->cancel();
38+
39+
$promise->then($this->expectCallableNever(), $this->expectCallableOnce());
40+
}
41+
42+
public function testCancelClosesStreamIfTcpResolvesDespiteCancellation()
43+
{
44+
$stream = $this->getMockBuilder('React\Stream\Stream')->disableOriginalConstructor()->setMethods(array('close'))->getMock();
45+
$stream->expects($this->once())->method('close');
46+
47+
$pending = new Promise\Promise(function () { }, function ($resolve) use ($stream) {
48+
$resolve($stream);
49+
});
50+
51+
$this->tcp->expects($this->once())->method('create')->with($this->equalTo('example.com'), $this->equalTo(80))->will($this->returnValue($pending));
52+
53+
$promise = $this->connector->create('example.com', 80);
54+
$promise->cancel();
55+
56+
$promise->then($this->expectCallableNever(), $this->expectCallableOnce());
57+
}
58+
}

0 commit comments

Comments
 (0)