Skip to content

Commit 27927f8

Browse files
authored
Merge pull request #66 from clue-labs/authentication
Support authentication with URL-encoded special characters
2 parents bf10b89 + ad5d081 commit 27927f8

File tree

3 files changed

+125
-12
lines changed

3 files changed

+125
-12
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ $factory = new Clue\React\Ami\Factory($loop, $connector);
121121

122122
The `createClient(string $url): PromiseInterface<Client,Exception>` method can be used to
123123
create a new [`Client`](#client).
124+
124125
It helps with establishing a plain TCP/IP or secure TLS connection to the AMI
125126
and optionally issuing an initial `login` action.
126127

@@ -154,6 +155,18 @@ to pass a username and secret for your AMI login details like this:
154155
$factory->createClient('user:secret@localhost');
155156
```
156157

158+
Note that both the username and password must be URL-encoded (percent-encoded)
159+
if they contain special characters:
160+
161+
```php
162+
$user = 'he:llo';
163+
$pass = 'p@ss';
164+
165+
$promise = $factory->createClient(
166+
rawurlencode($user) . ':' . rawurlencode($pass) . '@localhost'
167+
);
168+
```
169+
157170
The `Factory` defaults to establishing a plaintext TCP connection.
158171
If you want to create a secure TLS connection, you can use the `tls` scheme
159172
(which defaults to port `5039`):

src/Factory.php

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
*/
3838
class Factory
3939
{
40-
private $loop;
4140
private $connector;
4241

4342
public function __construct(LoopInterface $loop, ConnectorInterface $connector = null)
@@ -46,12 +45,12 @@ public function __construct(LoopInterface $loop, ConnectorInterface $connector =
4645
$connector = new Connector($loop);
4746
}
4847

49-
$this->loop = $loop;
5048
$this->connector = $connector;
5149
}
5250

5351
/**
5452
* Create a new [`Client`](#client).
53+
*
5554
* It helps with establishing a plain TCP/IP or secure TLS connection to the AMI
5655
* and optionally issuing an initial `login` action.
5756
*
@@ -85,6 +84,18 @@ public function __construct(LoopInterface $loop, ConnectorInterface $connector =
8584
* $factory->createClient('user:secret@localhost');
8685
* ```
8786
*
87+
* Note that both the username and password must be URL-encoded (percent-encoded)
88+
* if they contain special characters:
89+
*
90+
* ```php
91+
* $user = 'he:llo';
92+
* $pass = 'p@ss';
93+
*
94+
* $promise = $factory->createClient(
95+
* rawurlencode($user) . ':' . rawurlencode($pass) . '@localhost'
96+
* );
97+
* ```
98+
*
8899
* The `Factory` defaults to establishing a plaintext TCP connection.
89100
* If you want to create a secure TLS connection, you can use the `tls` scheme
90101
* (which defaults to port `5039`):
@@ -116,8 +127,8 @@ public function createClient($url)
116127
$promise = $promise->then(function (Client $client) use ($parts) {
117128
$sender = new ActionSender($client);
118129

119-
return $sender->login($parts['user'], $parts['pass'])->then(
120-
function ($response) use ($client) {
130+
return $sender->login(rawurldecode($parts['user']), rawurldecode($parts['pass']))->then(
131+
function () use ($client) {
121132
return $client;
122133
},
123134
function ($error) use ($client) {

tests/FactoryTest.php

Lines changed: 97 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
class FactoryTest extends TestCase
99
{
10-
private $loop;
1110
private $tcp;
1211
private $factory;
1312

@@ -16,18 +15,22 @@ class FactoryTest extends TestCase
1615
*/
1716
public function setUpFactory()
1817
{
19-
$this->loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
18+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
2019
$this->tcp = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
2120

22-
$this->factory = new Factory($this->loop, $this->tcp);
21+
$this->factory = new Factory($loop, $this->tcp);
2322
}
2423

25-
/**
26-
* @doesNotPerformAssertions
27-
*/
28-
public function testDefaultCtor()
24+
public function testDefaultCtorCreatesConnectorAutomatically()
2925
{
30-
$this->factory = new Factory($this->loop);
26+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
27+
$this->factory = new Factory($loop);
28+
29+
$ref = new \ReflectionProperty($this->factory, 'connector');
30+
$ref->setAccessible(true);
31+
$connector = $ref->getValue($this->factory);
32+
33+
$this->assertInstanceOf('React\Socket\Connector', $connector);
3134
}
3235

3336
public function testCreateClientUsesDefaultPortForTcpConnection()
@@ -54,6 +57,92 @@ public function testCreateClientUsesTlsConnectorWithTlsLocation()
5457
$this->factory->createClient('tls://ami.local:1234');
5558
}
5659

60+
public function testCreateClientResolvesWithClientWhenConnectionResolves()
61+
{
62+
$connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
63+
$this->tcp->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
64+
65+
$promise = $this->factory->createClient('localhost');
66+
67+
$client = null;
68+
$promise->then(function ($value) use (&$client) {
69+
$client = $value;
70+
});
71+
72+
$this->assertInstanceOf('Clue\React\Ami\Client', $client);
73+
}
74+
75+
public function testCreateClientWithAuthenticationWillSendLoginActionWithDecodedUserInfo()
76+
{
77+
$promiseAuthenticated = $this->getMockBuilder('React\Promise\PromiseInterface')->getMock();
78+
79+
$clientConnected = null;
80+
$promiseClient = $this->getMockBuilder('React\Promise\PromiseInterface')->getMock();
81+
$promiseClient->expects($this->once())->method('then')->with($this->callback(function ($callback) use (&$clientConnected) {
82+
$clientConnected = $callback;
83+
return true;
84+
}))->willReturn($promiseAuthenticated);
85+
86+
$promiseConnecting = $this->getMockBuilder('React\Promise\PromiseInterface')->getMock();
87+
$promiseConnecting->expects($this->once())->method('then')->willReturn($promiseClient);
88+
$this->tcp->expects($this->once())->method('connect')->willReturn($promiseConnecting);
89+
90+
$action = $this->getMockBuilder('Clue\React\Ami\Protocol\Action')->getMock();
91+
$client = $this->getMockBuilder('Clue\React\Ami\Client')->disableOriginalConstructor()->getMock();
92+
$client->expects($this->once())->method('createAction')->with('Login', array('UserName' => 'user@host', 'Secret' => 'pass+word!', 'Events' => null))->willReturn($action);
93+
$client->expects($this->once())->method('request')->with($action)->willReturn($promiseAuthenticated);
94+
95+
$promise = $this->factory->createClient('user%40host:pass+word%21@localhost');
96+
97+
$this->assertSame($promiseAuthenticated, $promise);
98+
99+
$this->assertNotNull($clientConnected);
100+
$clientConnected($client);
101+
}
102+
103+
public function testCreateClientWithAuthenticationResolvesWhenAuthenticationSucceeds()
104+
{
105+
$action = $this->getMockBuilder('Clue\React\Ami\Protocol\Action')->getMock();
106+
$client = $this->getMockBuilder('Clue\React\Ami\Client')->disableOriginalConstructor()->getMock();
107+
$client->expects($this->once())->method('createAction')->willReturn($action);
108+
$client->expects($this->once())->method('request')->with($action)->willReturn(\React\Promise\resolve('ignored'));
109+
110+
$promiseConnecting = $this->getMockBuilder('React\Promise\PromiseInterface')->getMock();
111+
$promiseConnecting->expects($this->once())->method('then')->willReturn(\React\Promise\resolve($client));
112+
$this->tcp->expects($this->once())->method('connect')->willReturn($promiseConnecting);
113+
114+
$promise = $this->factory->createClient('user%40host:pass+word%21@localhost');
115+
116+
$client = null;
117+
$promise->then(function ($value) use (&$client) {
118+
$client = $value;
119+
});
120+
121+
$this->assertInstanceOf('Clue\React\Ami\Client', $client);
122+
}
123+
124+
public function testCreateClientWithAuthenticationWillCloseClientAndRejectWhenLoginRequestRejects()
125+
{
126+
$error = new \RuntimeException();
127+
$action = $this->getMockBuilder('Clue\React\Ami\Protocol\Action')->getMock();
128+
$client = $this->getMockBuilder('Clue\React\Ami\Client')->disableOriginalConstructor()->getMock();
129+
$client->expects($this->once())->method('createAction')->willReturn($action);
130+
$client->expects($this->once())->method('request')->with($action)->willReturn(\React\Promise\reject($error));
131+
$client->expects($this->once())->method('close');
132+
133+
$promiseConnecting = $this->getMockBuilder('React\Promise\PromiseInterface')->getMock();
134+
$promiseConnecting->expects($this->once())->method('then')->willReturn(\React\Promise\resolve($client));
135+
$this->tcp->expects($this->once())->method('connect')->willReturn($promiseConnecting);
136+
137+
$promise = $this->factory->createClient('user%40host:pass+word%21@localhost');
138+
139+
$exception = null;
140+
$promise->then(null, function ($reason) use (&$exception) {
141+
$exception = $reason;
142+
});
143+
$this->assertSame($error, $exception);
144+
}
145+
57146
public function testCreateClientWithInvalidUrlWillRejectPromise()
58147
{
59148
$promise = $this->factory->createClient('///');

0 commit comments

Comments
 (0)