Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions src/Commands/AuthenticateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,6 @@ public function getId()
*/
public function authenticatePacket($scramble, $authPlugin, Buffer $buffer)
{
if ($authPlugin !== null && $authPlugin !== 'mysql_native_password' && $authPlugin !== 'caching_sha2_password') {
throw new \UnexpectedValueException('Unknown authentication plugin "' . addslashes($authPlugin) . '" requested by server');
}

$clientFlags = Constants::CLIENT_LONG_PASSWORD |
Constants::CLIENT_LONG_FLAG |
Constants::CLIENT_LOCAL_FILES |
Expand All @@ -102,11 +98,28 @@ public function authenticatePacket($scramble, $authPlugin, Buffer $buffer)
return pack('VVc', $clientFlags, $this->maxPacketSize, $this->charsetNumber)
. "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
. $this->user . "\x00"
. $buffer->buildStringLen($authPlugin === 'caching_sha2_password' ? $this->authCachingSha2Password($scramble) : $this->authMysqlNativePassword($scramble))
. $buffer->buildStringLen($this->authResponse($scramble, $authPlugin))
. $this->dbname . "\x00"
. ($authPlugin !== null ? $authPlugin . "\0" : '');
}

/**
* @param string $scramble
* @param ?string $authPlugin
* @return string
* @throws \UnexpectedValueException for unsupported authentication plugin
*/
public function authResponse($scramble, $authPlugin)
{
if ($authPlugin === null || $authPlugin === 'mysql_native_password') {
return $this->authMysqlNativePassword($scramble);
} elseif ($authPlugin === 'caching_sha2_password') {
return $this->authCachingSha2Password($scramble);
} else {
throw new \UnexpectedValueException('Unknown authentication plugin "' . addslashes($authPlugin) . '" requested by server');
}
}

/**
* @param string $scramble
* @return string
Expand Down
18 changes: 17 additions & 1 deletion src/Io/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ private function parsePacket(Buffer $packet)
$this->debug(sprintf("AffectedRows: %d, InsertId: %d, WarningCount:%d", $this->affectedRows, $this->insertId, $this->warningCount));
$this->onSuccess();
$this->nextRequest();
} elseif ($fieldCount === 0xFE) {
} elseif ($fieldCount === 0xFE && $this->phase !== self::PHASE_AUTH_SENT) {
// EOF Packet
$packet->skip(4); // warn, status
if ($this->rsState === self::RS_STATE_ROW) {
Expand All @@ -283,6 +283,22 @@ private function parsePacket(Buffer $packet)
$this->debug('Result set next part');
++$this->rsState;
}
} elseif ($fieldCount === 0xFE && $this->phase === self::PHASE_AUTH_SENT) {
// Protocol::AuthSwitchRequest packet
// https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_auth_switch_request.html
$this->authPlugin = $packet->readStringNull();
$this->scramble = $packet->read($packet->length() - 1);
$packet->skip(1); // 0x00
$this->debug('Switched to authentication plugin: ' . $this->authPlugin);

try {
assert($this->currCommand instanceof AuthenticateCommand);
$this->sendPacket($this->currCommand->authResponse($this->scramble, $this->authPlugin));
//$this->sendPacket($this->currCommand->authenticatePacket($this->scramble, $this->authPlugin, $this->buffer));
} catch (\UnexpectedValueException $e) {
$this->onError($e);
$this->stream->close();
}
} elseif ($fieldCount === 0x01 && $this->phase === self::PHASE_AUTH_SENT && $this->authPlugin === 'caching_sha2_password') {
// Protocol::AuthMoreData packet
// https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_auth_more_data.html
Expand Down
56 changes: 55 additions & 1 deletion tests/Io/ParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,60 @@ public function testUnexpectedAuthPluginShouldEmitErrorOnAuthenticateCommandAndC
$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");
}

public function testParseAuthSwitchRequestWillSendAuthSwitchResponsePacket()
{
$stream = new ThroughStream();
$stream->on('close', $this->expectCallableNever());

$outgoing = new ThroughStream();
$outgoing->on('data', $this->expectCallableOnceWith("\x09\0\0\x01" . "encrypted"));

$executor = new Executor();

$command = $this->getMockBuilder('React\Mysql\Commands\AuthenticateCommand')->disableOriginalConstructor()->getMock();
$command->expects($this->once())->method('authResponse')->with('scramble', 'caching_sha2_password')->willReturn('encrypted');

$parser = new Parser(new CompositeStream($stream, $outgoing), $executor);
$parser->start();

$ref = new \ReflectionProperty($parser, 'phase');
$ref->setAccessible(true);
$ref->setValue($parser, Parser::PHASE_AUTH_SENT);

$ref = new \ReflectionProperty($parser, 'currCommand');
$ref->setAccessible(true);
$ref->setValue($parser, $command);

$stream->write("\x20\0\0\0" . "\xfe" . "caching_sha2_password" . "\0" . "scramble" . "\0");
}

public function testParseAuthSwitchRequestWithUnexpectedAuthPluginWillEmitErrorAndCloseConnection()
{
$stream = new ThroughStream();
$stream->on('close', $this->expectCallableOnce());

$outgoing = new ThroughStream();
$outgoing->on('data', $this->expectCallableNever());

$command = new AuthenticateCommand('root', '', 'test', 'utf8mb4');
$command->on('error', $this->expectCallableOnceWith(new \UnexpectedValueException('Unknown authentication plugin "sha256_password" requested by server')));

$executor = new Executor();

$parser = new Parser(new CompositeStream($stream, $outgoing), $executor);
$parser->start();

$ref = new \ReflectionProperty($parser, 'phase');
$ref->setAccessible(true);
$ref->setValue($parser, Parser::PHASE_AUTH_SENT);

$ref = new \ReflectionProperty($parser, 'currCommand');
$ref->setAccessible(true);
$ref->setValue($parser, $command);

$stream->write("\x19\0\0\0" . "\xfe" . "sha256_password" . "\0" . "scramble" . "\0");
}

public function testParseAuthMoreDataWithFastAuthSuccessWillPrintDebugLogAndWaitForOkPacketWithoutSendingPacket()
{
$stream = new ThroughStream();
Expand Down Expand Up @@ -167,7 +221,7 @@ public function testParseAuthMoreDataWithCertificateWillSendEncryptedPassword()
$stream->write("\x04\0\0\0" . "\x01---");
}

public function testAuthMoreDataWithCertificateWillEmitErrorAndCloseConnectionWhenEncryptingPasswordThrows()
public function testParseAuthMoreDataWithCertificateWillEmitErrorAndCloseConnectionWhenEncryptingPasswordThrows()
{
$stream = new ThroughStream();
$stream->on('close', $this->expectCallableOnce());
Expand Down