Skip to content

Commit a134348

Browse files
authored
Merge pull request #208 from clue-labs/auth-switch
Support `AuthSwitchRequest` to switch authentication plugin
2 parents 3ef58d8 + f241cda commit a134348

File tree

3 files changed

+90
-7
lines changed

3 files changed

+90
-7
lines changed

src/Commands/AuthenticateCommand.php

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,6 @@ public function getId()
8282
*/
8383
public function authenticatePacket($scramble, $authPlugin, Buffer $buffer)
8484
{
85-
if ($authPlugin !== null && $authPlugin !== 'mysql_native_password' && $authPlugin !== 'caching_sha2_password') {
86-
throw new \UnexpectedValueException('Unknown authentication plugin "' . addslashes($authPlugin) . '" requested by server');
87-
}
88-
8985
$clientFlags = Constants::CLIENT_LONG_PASSWORD |
9086
Constants::CLIENT_LONG_FLAG |
9187
Constants::CLIENT_LOCAL_FILES |
@@ -102,11 +98,28 @@ public function authenticatePacket($scramble, $authPlugin, Buffer $buffer)
10298
return pack('VVc', $clientFlags, $this->maxPacketSize, $this->charsetNumber)
10399
. "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
104100
. $this->user . "\x00"
105-
. $buffer->buildStringLen($authPlugin === 'caching_sha2_password' ? $this->authCachingSha2Password($scramble) : $this->authMysqlNativePassword($scramble))
101+
. $buffer->buildStringLen($this->authResponse($scramble, $authPlugin))
106102
. $this->dbname . "\x00"
107103
. ($authPlugin !== null ? $authPlugin . "\0" : '');
108104
}
109105

106+
/**
107+
* @param string $scramble
108+
* @param ?string $authPlugin
109+
* @return string
110+
* @throws \UnexpectedValueException for unsupported authentication plugin
111+
*/
112+
public function authResponse($scramble, $authPlugin)
113+
{
114+
if ($authPlugin === null || $authPlugin === 'mysql_native_password') {
115+
return $this->authMysqlNativePassword($scramble);
116+
} elseif ($authPlugin === 'caching_sha2_password') {
117+
return $this->authCachingSha2Password($scramble);
118+
} else {
119+
throw new \UnexpectedValueException('Unknown authentication plugin "' . addslashes($authPlugin) . '" requested by server');
120+
}
121+
}
122+
110123
/**
111124
* @param string $scramble
112125
* @return string

src/Io/Parser.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ private function parsePacket(Buffer $packet)
269269
$this->debug(sprintf("AffectedRows: %d, InsertId: %d, WarningCount:%d", $this->affectedRows, $this->insertId, $this->warningCount));
270270
$this->onSuccess();
271271
$this->nextRequest();
272-
} elseif ($fieldCount === 0xFE) {
272+
} elseif ($fieldCount === 0xFE && $this->phase !== self::PHASE_AUTH_SENT) {
273273
// EOF Packet
274274
$packet->skip(4); // warn, status
275275
if ($this->rsState === self::RS_STATE_ROW) {
@@ -283,6 +283,22 @@ private function parsePacket(Buffer $packet)
283283
$this->debug('Result set next part');
284284
++$this->rsState;
285285
}
286+
} elseif ($fieldCount === 0xFE && $this->phase === self::PHASE_AUTH_SENT) {
287+
// Protocol::AuthSwitchRequest packet
288+
// https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_auth_switch_request.html
289+
$this->authPlugin = $packet->readStringNull();
290+
$this->scramble = $packet->read($packet->length() - 1);
291+
$packet->skip(1); // 0x00
292+
$this->debug('Switched to authentication plugin: ' . $this->authPlugin);
293+
294+
try {
295+
assert($this->currCommand instanceof AuthenticateCommand);
296+
$this->sendPacket($this->currCommand->authResponse($this->scramble, $this->authPlugin));
297+
//$this->sendPacket($this->currCommand->authenticatePacket($this->scramble, $this->authPlugin, $this->buffer));
298+
} catch (\UnexpectedValueException $e) {
299+
$this->onError($e);
300+
$this->stream->close();
301+
}
286302
} elseif ($fieldCount === 0x01 && $this->phase === self::PHASE_AUTH_SENT && $this->authPlugin === 'caching_sha2_password') {
287303
// Protocol::AuthMoreData packet
288304
// https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_auth_more_data.html

tests/Io/ParserTest.php

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,60 @@ public function testUnexpectedAuthPluginShouldEmitErrorOnAuthenticateCommandAndC
8383
$stream->write("\x43\0\0\0\x0a\x38\x2e\x34\x2e\x35\0\x5e\0\0\0\x08\x0c\x41\x44\x12\x5e\x69\x59\0\xff\xff\xff\x02\0\xff\xdf\x15\0\0\0\0\0\0\0\0\0\0\x3c\x2c\x5e\x54\x06\x04\x01\x61\x01\x20\x79\x1b\0\x73\x68\x61\x32\x35\x36\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\0");
8484
}
8585

86+
public function testParseAuthSwitchRequestWillSendAuthSwitchResponsePacket()
87+
{
88+
$stream = new ThroughStream();
89+
$stream->on('close', $this->expectCallableNever());
90+
91+
$outgoing = new ThroughStream();
92+
$outgoing->on('data', $this->expectCallableOnceWith("\x09\0\0\x01" . "encrypted"));
93+
94+
$executor = new Executor();
95+
96+
$command = $this->getMockBuilder('React\Mysql\Commands\AuthenticateCommand')->disableOriginalConstructor()->getMock();
97+
$command->expects($this->once())->method('authResponse')->with('scramble', 'caching_sha2_password')->willReturn('encrypted');
98+
99+
$parser = new Parser(new CompositeStream($stream, $outgoing), $executor);
100+
$parser->start();
101+
102+
$ref = new \ReflectionProperty($parser, 'phase');
103+
$ref->setAccessible(true);
104+
$ref->setValue($parser, Parser::PHASE_AUTH_SENT);
105+
106+
$ref = new \ReflectionProperty($parser, 'currCommand');
107+
$ref->setAccessible(true);
108+
$ref->setValue($parser, $command);
109+
110+
$stream->write("\x20\0\0\0" . "\xfe" . "caching_sha2_password" . "\0" . "scramble" . "\0");
111+
}
112+
113+
public function testParseAuthSwitchRequestWithUnexpectedAuthPluginWillEmitErrorAndCloseConnection()
114+
{
115+
$stream = new ThroughStream();
116+
$stream->on('close', $this->expectCallableOnce());
117+
118+
$outgoing = new ThroughStream();
119+
$outgoing->on('data', $this->expectCallableNever());
120+
121+
$command = new AuthenticateCommand('root', '', 'test', 'utf8mb4');
122+
$command->on('error', $this->expectCallableOnceWith(new \UnexpectedValueException('Unknown authentication plugin "sha256_password" requested by server')));
123+
124+
$executor = new Executor();
125+
126+
$parser = new Parser(new CompositeStream($stream, $outgoing), $executor);
127+
$parser->start();
128+
129+
$ref = new \ReflectionProperty($parser, 'phase');
130+
$ref->setAccessible(true);
131+
$ref->setValue($parser, Parser::PHASE_AUTH_SENT);
132+
133+
$ref = new \ReflectionProperty($parser, 'currCommand');
134+
$ref->setAccessible(true);
135+
$ref->setValue($parser, $command);
136+
137+
$stream->write("\x19\0\0\0" . "\xfe" . "sha256_password" . "\0" . "scramble" . "\0");
138+
}
139+
86140
public function testParseAuthMoreDataWithFastAuthSuccessWillPrintDebugLogAndWaitForOkPacketWithoutSendingPacket()
87141
{
88142
$stream = new ThroughStream();
@@ -167,7 +221,7 @@ public function testParseAuthMoreDataWithCertificateWillSendEncryptedPassword()
167221
$stream->write("\x04\0\0\0" . "\x01---");
168222
}
169223

170-
public function testAuthMoreDataWithCertificateWillEmitErrorAndCloseConnectionWhenEncryptingPasswordThrows()
224+
public function testParseAuthMoreDataWithCertificateWillEmitErrorAndCloseConnectionWhenEncryptingPasswordThrows()
171225
{
172226
$stream = new ThroughStream();
173227
$stream->on('close', $this->expectCallableOnce());

0 commit comments

Comments
 (0)