diff --git a/docker-compose.yml b/docker-compose.yml index 7e3efd07f..d4ec25630 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -62,3 +62,15 @@ services: - "2122:21" - "30000-30009:30000-30009" command: "/run.sh -l puredb:/etc/pure-ftpd/pureftpd.pdb -E -j -P localhost" + toxiproxy: + container_name: toxiproxy + restart: unless-stopped + image: ghcr.io/shopify/toxiproxy + command: "-host 0.0.0.0 -config /opt/toxiproxy/config.json" + volumes: + - ./test_files/toxiproxy/toxiproxy.json:/opt/toxiproxy/config.json:ro + ports: + - "8474:8474" # HTTP API + - "8222:8222" # SFTP + - "8121:8121" # FTP + - "8122:8122" # FTPD diff --git a/src/AdapterTestUtilities/ToxiproxyManagement.php b/src/AdapterTestUtilities/ToxiproxyManagement.php new file mode 100644 index 000000000..47a4b1721 --- /dev/null +++ b/src/AdapterTestUtilities/ToxiproxyManagement.php @@ -0,0 +1,75 @@ +apiClient = $apiClient; + } + + public static function forServer(string $apiUri = 'http://localhost:8474'): self + { + return new self( + new Client( + [ + 'base_uri' => $apiUri, + 'base_url' => $apiUri, // Compatibility with older versions of Guzzle + ] + ) + ); + } + + public function removeAllToxics(): void + { + $this->apiClient->post('/reset'); + } + + /** + * Simulates a peer reset on the client->server direction. + * + * @param RegisteredProxies $proxyName + */ + public function resetPeerOnRequest( + string $proxyName, + int $timeoutInMilliseconds + ): void { + $configuration = [ + 'type' => 'reset_peer', + 'stream' => 'upstream', + 'attributes' => ['timeout' => $timeoutInMilliseconds], + ]; + + $this->addToxic($proxyName, $configuration); + } + + /** + * Registers a network toxic for the given proxy. + * + * @param RegisteredProxies $proxyName + * @param Toxic $configuration + */ + private function addToxic(string $proxyName, array $configuration): void + { + $this->apiClient->post('/proxies/' . $proxyName . '/toxics', ['json' => $configuration]); + } +} diff --git a/src/PhpseclibV3/SftpConnectionProvider.php b/src/PhpseclibV3/SftpConnectionProvider.php index ee843ba85..5cd3f5f8d 100644 --- a/src/PhpseclibV3/SftpConnectionProvider.php +++ b/src/PhpseclibV3/SftpConnectionProvider.php @@ -4,12 +4,12 @@ namespace League\Flysystem\PhpseclibV3; +use League\Flysystem\FilesystemException; use phpseclib3\Crypt\Common\AsymmetricKey; use phpseclib3\Crypt\PublicKeyLoader; use phpseclib3\Exception\NoKeyLoadedException; use phpseclib3\Net\SFTP; use phpseclib3\System\SSH\Agent; -use RuntimeException; use Throwable; use function base64_decode; @@ -146,7 +146,10 @@ private function setupConnection(): SFTP $this->authenticate($connection); } catch (Throwable $exception) { $connection->disconnect(); - throw $exception; + + if ($exception instanceof FilesystemException) { + throw $exception; + } } return $connection; @@ -238,8 +241,6 @@ private function loadPrivateKey(): AsymmetricKey } catch (NoKeyLoadedException $exception) { throw new UnableToLoadPrivateKey(); } - - throw new RuntimeException(); } private function authenticateWithAgent(SFTP $connection): void diff --git a/src/PhpseclibV3/SftpConnectionProviderTest.php b/src/PhpseclibV3/SftpConnectionProviderTest.php index 97b81f64c..2dade1950 100644 --- a/src/PhpseclibV3/SftpConnectionProviderTest.php +++ b/src/PhpseclibV3/SftpConnectionProviderTest.php @@ -4,6 +4,7 @@ namespace League\Flysystem\PhpseclibV3; +use League\Flysystem\AdapterTestUtilities\ToxiproxyManagement; use phpseclib3\Exception\NoSupportedAlgorithmsException; use phpseclib3\Net\SFTP; use PHPUnit\Framework\TestCase; @@ -244,6 +245,52 @@ public function providing_an_invalid_password(): void $provider->provideConnection(); } + /** + * @test + */ + public function retries_several_times_until_failure(): void + { + $connectivityChecker = new class implements ConnectivityChecker { + /** @var int */ + public $calls = 0; + + public function isConnected(SFTP $connection): bool + { + ++$this->calls; + + return $connection->isConnected(); + } + }; + + $managesConnectionToxics = ToxiproxyManagement::forServer(); + $managesConnectionToxics->resetPeerOnRequest('sftp', 10); + + $maxTries = 5; + + $provider = SftpConnectionProvider::fromArray( + [ + 'host' => 'localhost', + 'username' => 'bar', + 'privateKey' => __DIR__ . '/../../test_files/sftp/id_rsa', + 'passphrase' => 'secret', + 'port' => 8222, + 'maxTries' => $maxTries, + 'timeout' => 1, + 'connectivityChecker' => $connectivityChecker, + ] + ); + + $this->expectException(UnableToConnectToSftpHost::class); + + try { + $provider->provideConnection(); + } finally { + $managesConnectionToxics->removeAllToxics(); + + self::assertSame($maxTries + 1, $connectivityChecker->calls); + } + } + /** * @test */ diff --git a/test_files/toxiproxy/toxiproxy.json b/test_files/toxiproxy/toxiproxy.json new file mode 100644 index 000000000..9396fb838 --- /dev/null +++ b/test_files/toxiproxy/toxiproxy.json @@ -0,0 +1,20 @@ +[ + { + "name": "sftp", + "listen": "[::]:8222", + "upstream": "sftp:22", + "enabled": true + }, + { + "name": "ftp", + "listen": "[::]:8121", + "upstream": "ftp:21", + "enabled": true + }, + { + "name": "ftpd", + "listen": "[::]:8122", + "upstream": "ftpd:21", + "enabled": true + } +]