Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Verify SFTP retries when dealing with peer resets #1452

Merged
merged 2 commits into from
Apr 24, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,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
75 changes: 75 additions & 0 deletions src/AdapterTestUtilities/ToxiproxyManagement.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

namespace League\Flysystem\AdapterTestUtilities;

use GuzzleHttp\Client;

/**
* This class provides a client for the HTTP API provided by the proxy that simulates network issues.
frankdejonge marked this conversation as resolved.
Show resolved Hide resolved
*
* @see https://github.com/shopify/toxiproxy#http-api
*
* @phpstan-type RegisteredProxies 'ftp'|'sftp'|'ftpd'
* @phpstan-type StreamDirection 'upstream'|'downstream'
* @phpstan-type Type 'latency'|'bandwidth'|'slow_close'|'timeout'|'reset_peer'|'slicer'|'limit_data'
* @phpstan-type Attributes array{latency?: int, jitter?: int, rate?: int, delay?: int}
* @phpstan-type Toxic array{name?: string, type: Type, stream?: StreamDirection, toxicity?: float, attributes: Attributes}
*/
final class ToxiproxyManagement
{
/** @var Client */
private $apiClient;

public function __construct(Client $apiClient)
{
$this->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]);
}
}
47 changes: 47 additions & 0 deletions src/PhpseclibV3/SftpConnectionProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace League\Flysystem\PhpseclibV3;

use League\Flysystem\AdapterTestUtilities\ToxiproxyManagement;
use phpseclib3\Net\SFTP;
use PHPUnit\Framework\TestCase;

Expand Down Expand Up @@ -241,6 +242,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);
}
}

private function computeFingerPrint(string $publicKey): string
{
$content = explode(' ', $publicKey, 3);
Expand Down
20 changes: 20 additions & 0 deletions test_files/toxiproxy/toxiproxy.json
Original file line number Diff line number Diff line change
@@ -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
}
]