Skip to content

Commit ca529b6

Browse files
committed
Make MysqlClient constructor throw if given $uri is invalid
1 parent 32c455d commit ca529b6

File tree

5 files changed

+191
-171
lines changed

5 files changed

+191
-171
lines changed

src/Io/Factory.php

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -165,34 +165,23 @@ public function createConnection(
165165
#[\SensitiveParameter]
166166
$uri
167167
) {
168-
if (strpos($uri, '://') === false) {
169-
$uri = 'mysql://' . $uri;
170-
}
171-
172168
$parts = parse_url($uri);
173169
$uri = preg_replace('#:[^:/]*@#', ':***@', $uri);
174-
if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'mysql') {
175-
return \React\Promise\reject(new \InvalidArgumentException(
176-
'Invalid MySQL URI given (EINVAL)',
177-
\defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : 22
178-
));
179-
}
170+
assert(is_array($parts) && isset($parts['scheme'], $parts['host']));
171+
assert($parts['scheme'] === 'mysql');
180172

181173
$args = [];
182174
if (isset($parts['query'])) {
183175
parse_str($parts['query'], $args);
184176
}
185177

186-
try {
187-
$authCommand = new AuthenticateCommand(
188-
isset($parts['user']) ? rawurldecode($parts['user']) : 'root',
189-
isset($parts['pass']) ? rawurldecode($parts['pass']) : '',
190-
isset($parts['path']) ? rawurldecode(ltrim($parts['path'], '/')) : '',
191-
isset($args['charset']) ? $args['charset'] : 'utf8mb4'
192-
);
193-
} catch (\InvalidArgumentException $e) {
194-
return \React\Promise\reject($e);
195-
}
178+
/** @throws void already validated in MysqlClient ctor */
179+
$authCommand = new AuthenticateCommand(
180+
isset($parts['user']) ? rawurldecode($parts['user']) : 'root',
181+
isset($parts['pass']) ? rawurldecode($parts['pass']) : '',
182+
isset($parts['path']) ? rawurldecode(ltrim($parts['path'], '/')) : '',
183+
isset($args['charset']) ? $args['charset'] : 'utf8mb4'
184+
);
196185

197186
$connecting = $this->connector->connect(
198187
$parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 3306)

src/MysqlClient.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Evenement\EventEmitter;
66
use React\EventLoop\LoopInterface;
7+
use React\Mysql\Commands\AuthenticateCommand;
78
use React\Mysql\Io\Connection;
89
use React\Mysql\Io\Factory;
910
use React\Promise\Deferred;
@@ -80,13 +81,37 @@ class MysqlClient extends EventEmitter
8081
* @param string $uri
8182
* @param ?ConnectorInterface $connector
8283
* @param ?LoopInterface $loop
84+
* @throws \InvalidArgumentException if $uri is not a valid MySQL URI
8385
*/
8486
public function __construct(
8587
#[\SensitiveParameter]
8688
$uri,
8789
$connector = null,
8890
$loop = null
8991
) {
92+
if (strpos($uri, '://') === false) {
93+
$uri = 'mysql://' . $uri;
94+
}
95+
96+
$parts = parse_url($uri);
97+
if ($parts === false || !isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'mysql') {
98+
$uri = preg_replace('#:[^:/]*@#', ':***@', $uri);
99+
throw new \InvalidArgumentException(
100+
'Invalid MySQL URI "' . $uri . '" (EINVAL)',
101+
defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22
102+
);
103+
}
104+
105+
if (isset($parts['query'])) {
106+
$query = [];
107+
parse_str($parts['query'], $query);
108+
109+
// validate charset if given
110+
if (isset($query['charset'])) {
111+
new AuthenticateCommand('', '', '', $query['charset']);
112+
}
113+
}
114+
90115
if ($connector !== null && !$connector instanceof ConnectorInterface) { // manual type check to support legacy PHP < 7.1
91116
throw new \InvalidArgumentException('Argument #2 ($connector) expected null|React\Socket\ConnectorInterface');
92117
}

tests/BaseTestCase.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ protected function getConnectionString($params = [])
2525
{
2626
$parts = $params + $this->getConnectionOptions();
2727

28-
return rawurlencode($parts['user']) . ':' . rawurlencode($parts['passwd']) . '@' . $parts['host'] . ':' . $parts['port'] . '/' . rawurlencode($parts['dbname']);
28+
return 'mysql://' . rawurlencode($parts['user']) . ':' . rawurlencode($parts['passwd']) . '@' . $parts['host'] . ':' . $parts['port'] . '/' . rawurlencode($parts['dbname']);
2929
}
3030

3131
/**

tests/Io/FactoryTest.php

Lines changed: 9 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public function testConnectWillUseHostAndDefaultPort()
3030
$connector->expects($this->once())->method('connect')->with('127.0.0.1:3306')->willReturn($pending);
3131

3232
$factory = new Factory($loop, $connector);
33-
$factory->createConnection('127.0.0.1');
33+
$factory->createConnection('mysql://127.0.0.1');
3434
}
3535

3636
public function testConnectWillUseGivenScheme()
@@ -44,28 +44,6 @@ public function testConnectWillUseGivenScheme()
4444
$factory->createConnection('mysql://127.0.0.1');
4545
}
4646

47-
public function testConnectWillRejectWhenGivenInvalidScheme()
48-
{
49-
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
50-
$connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
51-
52-
$factory = new Factory($loop, $connector);
53-
54-
$promise = $factory->createConnection('foo://127.0.0.1');
55-
56-
$promise->then(null, $this->expectCallableOnceWith(
57-
$this->logicalAnd(
58-
$this->isInstanceOf('InvalidArgumentException'),
59-
$this->callback(function (\InvalidArgumentException $e) {
60-
return $e->getMessage() === 'Invalid MySQL URI given (EINVAL)';
61-
}),
62-
$this->callback(function (\InvalidArgumentException $e) {
63-
return $e->getCode() === (defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22);
64-
})
65-
)
66-
));
67-
}
68-
6947
public function testConnectWillUseGivenHostAndGivenPort()
7048
{
7149
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
@@ -74,7 +52,7 @@ public function testConnectWillUseGivenHostAndGivenPort()
7452
$connector->expects($this->once())->method('connect')->with('127.0.0.1:1234')->willReturn($pending);
7553

7654
$factory = new Factory($loop, $connector);
77-
$factory->createConnection('127.0.0.1:1234');
55+
$factory->createConnection('mysql://127.0.0.1:1234');
7856
}
7957

8058
public function testConnectWillUseGivenUserInfoAsDatabaseCredentialsAfterUrldecoding()
@@ -87,7 +65,7 @@ public function testConnectWillUseGivenUserInfoAsDatabaseCredentialsAfterUrldeco
8765
$connector->expects($this->once())->method('connect')->with('127.0.0.1:3306')->willReturn(\React\Promise\resolve($connection));
8866

8967
$factory = new Factory($loop, $connector);
90-
$promise = $factory->createConnection('user%[email protected]');
68+
$promise = $factory->createConnection('mysql://user%[email protected]');
9169

9270
$promise->then($this->expectCallableNever(), $this->expectCallableNever());
9371

@@ -104,48 +82,13 @@ public function testConnectWillUseGivenPathAsDatabaseNameAfterUrldecoding()
10482
$connector->expects($this->once())->method('connect')->with('127.0.0.1:3306')->willReturn(\React\Promise\resolve($connection));
10583

10684
$factory = new Factory($loop, $connector);
107-
$promise = $factory->createConnection('127.0.0.1/test%20database');
85+
$promise = $factory->createConnection('mysql://127.0.0.1/test%20database');
10886

10987
$promise->then($this->expectCallableNever(), $this->expectCallableNever());
11088

11189
$connection->emit('data', ["\x33\0\0\0" . "\x0a" . "mysql\0" . str_repeat("\0", 44)]);
11290
}
11391

114-
public function testConnectWithInvalidUriWillRejectWithoutConnecting()
115-
{
116-
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
117-
$connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
118-
$connector->expects($this->never())->method('connect');
119-
120-
$factory = new Factory($loop, $connector);
121-
$promise = $factory->createConnection('///');
122-
123-
$promise->then(null, $this->expectCallableOnceWith(
124-
$this->logicalAnd(
125-
$this->isInstanceOf('InvalidArgumentException'),
126-
$this->callback(function (\InvalidArgumentException $e) {
127-
return $e->getMessage() === 'Invalid MySQL URI given (EINVAL)';
128-
}),
129-
$this->callback(function (\InvalidArgumentException $e) {
130-
return $e->getCode() === (defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22);
131-
})
132-
)
133-
));
134-
}
135-
136-
public function testConnectWithInvalidCharsetWillRejectWithoutConnecting()
137-
{
138-
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
139-
$connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
140-
$connector->expects($this->never())->method('connect');
141-
142-
$factory = new Factory($loop, $connector);
143-
$promise = $factory->createConnection('localhost?charset=unknown');
144-
145-
$this->assertInstanceof('React\Promise\PromiseInterface', $promise);
146-
$promise->then(null, $this->expectCallableOnce());
147-
}
148-
14992
public function testConnectWithInvalidHostRejectsWithConnectionError()
15093
{
15194
$factory = new Factory();
@@ -204,7 +147,7 @@ public function testConnectWillRejectWhenServerClosesConnection()
204147
$this->logicalAnd(
205148
$this->isInstanceOf('RuntimeException'),
206149
$this->callback(function (\RuntimeException $e) use ($uri) {
207-
return $e->getMessage() === 'Connection to mysql://' . $uri . ' failed during authentication: Connection closed by peer (ECONNRESET)';
150+
return $e->getMessage() === 'Connection to ' . $uri . ' failed during authentication: Connection closed by peer (ECONNRESET)';
208151
}),
209152
$this->callback(function (\RuntimeException $e) {
210153
return $e->getCode() === (defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104);
@@ -244,7 +187,7 @@ public function testConnectWillRejectOnDefaultTimeoutFromIniDespiteValidAuth()
244187
{
245188
$factory = new Factory();
246189

247-
$uri = 'mysql://' . $this->getConnectionString();
190+
$uri = $this->getConnectionString();
248191

249192
$old = ini_get('default_socket_timeout');
250193
ini_set('default_socket_timeout', '0');
@@ -439,7 +382,7 @@ public function testlConnectWillRejectWhenUnderlyingConnectorRejects()
439382
$connector->expects($this->once())->method('connect')->willReturn(\React\Promise\reject(new \RuntimeException('Failed', 123)));
440383

441384
$factory = new Factory($loop, $connector);
442-
$promise = $factory->createConnection('user:[email protected]');
385+
$promise = $factory->createConnection('mysql://user:[email protected]');
443386

444387
$promise->then(null, $this->expectCallableOnceWith(
445388
$this->logicalAnd(
@@ -457,10 +400,6 @@ public function testlConnectWillRejectWhenUnderlyingConnectorRejects()
457400
public function provideUris()
458401
{
459402
return [
460-
[
461-
'localhost',
462-
'mysql://localhost'
463-
],
464403
[
465404
'mysql://localhost',
466405
'mysql://localhost'
@@ -520,7 +459,7 @@ public function testCancelConnectWillCancelPendingConnectionWithRuntimeException
520459
$connector->expects($this->once())->method('connect')->willReturn($pending);
521460

522461
$factory = new Factory($loop, $connector);
523-
$promise = $factory->createConnection('127.0.0.1');
462+
$promise = $factory->createConnection('mysql://127.0.0.1');
524463

525464
$promise->cancel();
526465

@@ -547,7 +486,7 @@ public function testCancelConnectDuringAuthenticationWillCloseConnection()
547486
$connector->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection));
548487

549488
$factory = new Factory($loop, $connector);
550-
$promise = $factory->createConnection('127.0.0.1');
489+
$promise = $factory->createConnection('mysql://127.0.0.1');
551490

552491
$promise->cancel();
553492

0 commit comments

Comments
 (0)