From f241cda820f4c92752c1afc3480af3cb3047d40b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 22 May 2025 22:26:20 +0200 Subject: [PATCH] Support `AuthSwitchRequest` to switch authentication plugin --- src/Commands/AuthenticateCommand.php | 23 +++++++++--- src/Io/Parser.php | 18 ++++++++- tests/Io/ParserTest.php | 56 +++++++++++++++++++++++++++- 3 files changed, 90 insertions(+), 7 deletions(-) diff --git a/src/Commands/AuthenticateCommand.php b/src/Commands/AuthenticateCommand.php index 2fe0364..00b5ecc 100644 --- a/src/Commands/AuthenticateCommand.php +++ b/src/Commands/AuthenticateCommand.php @@ -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 | @@ -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 diff --git a/src/Io/Parser.php b/src/Io/Parser.php index f0496f4..b6aff65 100644 --- a/src/Io/Parser.php +++ b/src/Io/Parser.php @@ -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) { @@ -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 diff --git a/tests/Io/ParserTest.php b/tests/Io/ParserTest.php index 740221b..b1efa40 100644 --- a/tests/Io/ParserTest.php +++ b/tests/Io/ParserTest.php @@ -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(); @@ -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());