From 9183948c25b24d11f88237cf16787601dbb6c0a3 Mon Sep 17 00:00:00 2001 From: krowinski Date: Wed, 31 Jan 2024 11:55:22 +0100 Subject: [PATCH] feat: auto authorisation packet switching --- CHANGELOG.md | 4 ++ .../BinLog/BinLogAuthPluginMode.php | 15 ++++ .../BinLog/BinLogServerInfo.php | 4 +- .../BinLog/BinLogSocketConnect.php | 68 ++++++++++++------- .../BinaryDataReader/BinaryDataReader.php | 14 ++++ 5 files changed, 78 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5cc6b1..a24af89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Release Notes +## v8.0.1 (2024-01-31) + +- Added: auto authorisation packet switching + ## v8.0.0 (2024-01-29) - Change: drop support for < 8.2 diff --git a/src/MySQLReplication/BinLog/BinLogAuthPluginMode.php b/src/MySQLReplication/BinLog/BinLogAuthPluginMode.php index 5ef6d2c..3008ffd 100644 --- a/src/MySQLReplication/BinLog/BinLogAuthPluginMode.php +++ b/src/MySQLReplication/BinLog/BinLogAuthPluginMode.php @@ -4,8 +4,23 @@ namespace MySQLReplication\BinLog; +use MySQLReplication\Exception\MySQLReplicationException; + enum BinLogAuthPluginMode: string { case MysqlNativePassword = 'mysql_native_password'; case CachingSha2Password = 'caching_sha2_password'; + + public static function make(string $authPluginName): self + { + $authPlugin = self::tryFrom($authPluginName); + if ($authPlugin === null) { + throw new MySQLReplicationException( + MySQLReplicationException::BINLOG_AUTH_NOT_SUPPORTED, + MySQLReplicationException::BINLOG_AUTH_NOT_SUPPORTED_CODE + ); + } + + return $authPlugin; + } } diff --git a/src/MySQLReplication/BinLog/BinLogServerInfo.php b/src/MySQLReplication/BinLog/BinLogServerInfo.php index 962303b..fbd7704 100644 --- a/src/MySQLReplication/BinLog/BinLogServerInfo.php +++ b/src/MySQLReplication/BinLog/BinLogServerInfo.php @@ -17,7 +17,7 @@ public function __construct( public string $serverVersion, public int $connectionId, public string $salt, - public ?BinLogAuthPluginMode $authPlugin, + public BinLogAuthPluginMode $authPlugin, public string $versionName, public float $versionRevision ) { @@ -94,7 +94,7 @@ public static function make(string $data, string $version): self $serverVersion, $connectionId, $salt, - BinLogAuthPluginMode::tryFrom($authPlugin), + BinLogAuthPluginMode::make($authPlugin), self::parseVersion($serverVersion), self::parseRevision($version) ); diff --git a/src/MySQLReplication/BinLog/BinLogSocketConnect.php b/src/MySQLReplication/BinLog/BinLogSocketConnect.php index ccdbaba..9fd34f2 100644 --- a/src/MySQLReplication/BinLog/BinLogSocketConnect.php +++ b/src/MySQLReplication/BinLog/BinLogSocketConnect.php @@ -6,7 +6,6 @@ use MySQLReplication\BinaryDataReader\BinaryDataReader; use MySQLReplication\Config\Config; -use MySQLReplication\Exception\MySQLReplicationException; use MySQLReplication\Gtid\GtidCollection; use MySQLReplication\Repository\RepositoryInterface; use MySQLReplication\Socket\SocketInterface; @@ -17,6 +16,7 @@ class BinLogSocketConnect private const COM_BINLOG_DUMP = 0x12; private const COM_REGISTER_SLAVE = 0x15; private const COM_BINLOG_DUMP_GTID = 0x1e; + private const AUTH_SWITCH_PACKET = 254; /** * https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase.html 00 FE */ @@ -47,7 +47,8 @@ public function __construct( 'Server version name: ' . $this->binLogServerInfo->versionName . ', revision: ' . $this->binLogServerInfo->versionRevision ); - $this->authenticate(); + + $this->authenticate($this->binLogServerInfo->authPlugin); $this->getBinlogStream(); } @@ -110,17 +111,10 @@ private function isWriteSuccessful(string $data): void } } - private function authenticate(): void + private function authenticate(BinLogAuthPluginMode $authPlugin): void { - if ($this->binLogServerInfo->authPlugin === null) { - throw new MySQLReplicationException( - MySQLReplicationException::BINLOG_AUTH_NOT_SUPPORTED, - MySQLReplicationException::BINLOG_AUTH_NOT_SUPPORTED_CODE - ); - } - $this->logger->info( - 'Trying to authenticate user: ' . $this->config->user . ' using ' . $this->binLogServerInfo->authPlugin->value . ' plugin' + 'Trying to authenticate user: ' . $this->config->user . ' using ' . $authPlugin->value . ' default plugin' ); $data = pack('L', self::getCapabilities()); @@ -128,38 +122,49 @@ private function authenticate(): void $data .= chr(33); $data .= str_repeat(chr(0), 23); $data .= $this->config->user . chr(0); - - $auth = ''; - if ($this->binLogServerInfo->authPlugin === BinLogAuthPluginMode::MysqlNativePassword) { - $auth = $this->authenticateMysqlNativePasswordPlugin(); - } elseif ($this->binLogServerInfo->authPlugin === BinLogAuthPluginMode::CachingSha2Password) { - $auth = $this->authenticateCachingSha2PasswordPlugin(); - } - + $auth = $this->getAuthData($authPlugin, $this->binLogServerInfo->salt); $data .= chr(strlen($auth)) . $auth; - $data .= $this->binLogServerInfo->authPlugin->value . chr(0); + $data .= $authPlugin->value . chr(0); $str = pack('L', strlen($data)); $s = $str[0] . $str[1] . $str[2]; $data = $s . chr(1) . $data; $this->socket->writeToSocket($data); - $this->getResponse(); + $response = $this->getResponse(); + + // Check for AUTH_SWITCH_PACKET + if (isset($response[0]) && ord($response[0]) === self::AUTH_SWITCH_PACKET) { + $this->switchAuth($response); + } $this->logger->info('User authenticated'); } - private function authenticateCachingSha2PasswordPlugin(): string + private function getAuthData(?BinLogAuthPluginMode $authPlugin, string $salt): string + { + if ($authPlugin === BinLogAuthPluginMode::MysqlNativePassword) { + return $this->authenticateMysqlNativePasswordPlugin($salt); + } + + if ($authPlugin === BinLogAuthPluginMode::CachingSha2Password) { + return $this->authenticateCachingSha2PasswordPlugin($salt); + } + + return ''; + } + + private function authenticateCachingSha2PasswordPlugin(string $salt): string { $hash1 = hash('sha256', $this->config->password, true); $hash2 = hash('sha256', $hash1, true); - $hash3 = hash('sha256', $hash2 . $this->binLogServerInfo->salt, true); + $hash3 = hash('sha256', $hash2 . $salt, true); return $hash1 ^ $hash3; } - private function authenticateMysqlNativePasswordPlugin(): string + private function authenticateMysqlNativePasswordPlugin(string $salt): string { $hash1 = sha1($this->config->password, true); - $hash2 = sha1($this->binLogServerInfo->salt . sha1(sha1($this->config->password, true), true), true); + $hash2 = sha1($salt . sha1(sha1($this->config->password, true), true), true); return $hash1 ^ $hash2; } @@ -316,4 +321,17 @@ private function setBinLogDump(): void $this->logger->info('Set binlog to start from: ' . $binFileName . ':' . $binFilePos); } + + private function switchAuth(string $response): void + { + // skip AUTH_SWITCH_PACKET byte + $offset = 1; + $authPluginSwitched = BinLogAuthPluginMode::make(BinaryDataReader::decodeNullLength($response, $offset)); + $salt = BinaryDataReader::decodeNullLength($response, $offset); + $auth = $this->getAuthData($authPluginSwitched, $salt); + + $this->logger->info('Auth switch packet received, switching to ' . $authPluginSwitched->value); + + $this->socket->writeToSocket(pack('L', (strlen($auth)) | (3 << 24)) . $auth); + } } diff --git a/src/MySQLReplication/BinaryDataReader/BinaryDataReader.php b/src/MySQLReplication/BinaryDataReader/BinaryDataReader.php index d854dd1..3a8119f 100644 --- a/src/MySQLReplication/BinaryDataReader/BinaryDataReader.php +++ b/src/MySQLReplication/BinaryDataReader/BinaryDataReader.php @@ -325,4 +325,18 @@ public static function unpack(string $format, string $string): array } return []; } + + public static function decodeNullLength(string $data, int &$offset = 0): string + { + $length = strpos($data, chr(0), $offset); + if ($length === false) { + return ''; + } + + $length -= $offset; + $result = substr($data, $offset, $length); + $offset += $length + 1; + + return $result; + } }