215 lines
6.3 KiB
PHP
215 lines
6.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
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 Throwable;
|
|
|
|
use function base64_decode;
|
|
use function implode;
|
|
use function str_split;
|
|
|
|
class SftpConnectionProvider implements ConnectionProvider
|
|
{
|
|
|
|
/**
|
|
* @var SFTP|null
|
|
*/
|
|
private $connection;
|
|
|
|
/**
|
|
* @var ConnectivityChecker
|
|
*/
|
|
private $connectivityChecker;
|
|
|
|
public function __construct(
|
|
private string $host,
|
|
private string $username,
|
|
private ?string $password = null,
|
|
private ?string $privateKey = null,
|
|
private ?string $passphrase = null,
|
|
private int $port = 22,
|
|
private bool $useAgent = false,
|
|
private int $timeout = 10,
|
|
private int $maxTries = 4,
|
|
private ?string $hostFingerprint = null,
|
|
?ConnectivityChecker $connectivityChecker = null,
|
|
private array $preferredAlgorithms = [],
|
|
private bool $disableStatCache = true,
|
|
) {
|
|
$this->connectivityChecker = $connectivityChecker ?? new SimpleConnectivityChecker();
|
|
}
|
|
|
|
public function provideConnection(): SFTP
|
|
{
|
|
$tries = 0;
|
|
start:
|
|
$tries++;
|
|
|
|
try {
|
|
$connection = $this->connection instanceof SFTP
|
|
? $this->connection
|
|
: $this->setupConnection();
|
|
} catch (Throwable $exception) {
|
|
if ($tries <= $this->maxTries) {
|
|
goto start;
|
|
}
|
|
|
|
if ($exception instanceof FilesystemException) {
|
|
throw $exception;
|
|
}
|
|
|
|
throw UnableToConnectToSftpHost::atHostname($this->host, $exception);
|
|
}
|
|
|
|
if ( ! $this->connectivityChecker->isConnected($connection)) {
|
|
$connection->disconnect();
|
|
$this->connection = null;
|
|
|
|
if ($tries <= $this->maxTries) {
|
|
goto start;
|
|
}
|
|
|
|
throw UnableToConnectToSftpHost::atHostname($this->host);
|
|
}
|
|
|
|
return $this->connection = $connection;
|
|
}
|
|
|
|
public function disconnect(): void
|
|
{
|
|
if ($this->connection) {
|
|
$this->connection->disconnect();
|
|
$this->connection = null;
|
|
}
|
|
}
|
|
|
|
private function setupConnection(): SFTP
|
|
{
|
|
$connection = new SFTP($this->host, $this->port, $this->timeout);
|
|
$connection->setPreferredAlgorithms($this->preferredAlgorithms);
|
|
$this->disableStatCache && $connection->disableStatCache();
|
|
|
|
try {
|
|
$this->checkFingerprint($connection);
|
|
$this->authenticate($connection);
|
|
} catch (Throwable $exception) {
|
|
$connection->disconnect();
|
|
throw $exception;
|
|
}
|
|
|
|
return $connection;
|
|
}
|
|
|
|
private function checkFingerprint(SFTP $connection): void
|
|
{
|
|
if ( ! $this->hostFingerprint) {
|
|
return;
|
|
}
|
|
|
|
$publicKey = $connection->getServerPublicHostKey();
|
|
|
|
if ($publicKey === false) {
|
|
throw UnableToEstablishAuthenticityOfHost::becauseTheAuthenticityCantBeEstablished($this->host);
|
|
}
|
|
|
|
$fingerprint = $this->getFingerprintFromPublicKey($publicKey);
|
|
|
|
if (0 !== strcasecmp($this->hostFingerprint, $fingerprint)) {
|
|
throw UnableToEstablishAuthenticityOfHost::becauseTheAuthenticityCantBeEstablished($this->host);
|
|
}
|
|
}
|
|
|
|
private function getFingerprintFromPublicKey(string $publicKey): string
|
|
{
|
|
$content = explode(' ', $publicKey, 3);
|
|
$algo = $content[0] === 'ssh-rsa' ? 'md5' : 'sha512';
|
|
|
|
return implode(':', str_split(hash($algo, base64_decode($content[1])), 2));
|
|
}
|
|
|
|
private function authenticate(SFTP $connection): void
|
|
{
|
|
if ($this->privateKey !== null) {
|
|
$this->authenticateWithPrivateKey($connection);
|
|
} elseif ($this->useAgent) {
|
|
$this->authenticateWithAgent($connection);
|
|
} else {
|
|
$this->authenticateWithUsernameAndPassword($connection);
|
|
}
|
|
}
|
|
|
|
private function authenticateWithUsernameAndPassword(SFTP $connection): void
|
|
{
|
|
if ( ! $connection->login($this->username, $this->password)) {
|
|
throw UnableToAuthenticate::withPassword($connection->getLastError());
|
|
}
|
|
}
|
|
|
|
public static function fromArray(array $options): SftpConnectionProvider
|
|
{
|
|
return new SftpConnectionProvider(
|
|
$options['host'],
|
|
$options['username'],
|
|
$options['password'] ?? null,
|
|
$options['privateKey'] ?? null,
|
|
$options['passphrase'] ?? null,
|
|
$options['port'] ?? 22,
|
|
$options['useAgent'] ?? false,
|
|
$options['timeout'] ?? 10,
|
|
$options['maxTries'] ?? 4,
|
|
$options['hostFingerprint'] ?? null,
|
|
$options['connectivityChecker'] ?? null,
|
|
$options['preferredAlgorithms'] ?? [],
|
|
);
|
|
}
|
|
|
|
private function authenticateWithPrivateKey(SFTP $connection): void
|
|
{
|
|
$privateKey = $this->loadPrivateKey();
|
|
|
|
if ($connection->login($this->username, $privateKey)) {
|
|
return;
|
|
}
|
|
|
|
if ($this->password !== null && $connection->login($this->username, $this->password)) {
|
|
return;
|
|
}
|
|
|
|
throw UnableToAuthenticate::withPrivateKey($connection->getLastError());
|
|
}
|
|
|
|
private function loadPrivateKey(): AsymmetricKey
|
|
{
|
|
if (("---" !== substr($this->privateKey, 0, 3) || "PuTTY" !== substr($this->privateKey, 0, 5)) && is_file($this->privateKey)) {
|
|
$this->privateKey = file_get_contents($this->privateKey);
|
|
}
|
|
|
|
try {
|
|
if ($this->passphrase !== null) {
|
|
return PublicKeyLoader::load($this->privateKey, $this->passphrase);
|
|
}
|
|
|
|
return PublicKeyLoader::load($this->privateKey);
|
|
} catch (NoKeyLoadedException $exception) {
|
|
throw new UnableToLoadPrivateKey(null, $exception);
|
|
}
|
|
}
|
|
|
|
private function authenticateWithAgent(SFTP $connection): void
|
|
{
|
|
$agent = new Agent();
|
|
|
|
if ( ! $connection->login($this->username, $agent)) {
|
|
throw UnableToAuthenticate::withSshAgent($connection->getLastError());
|
|
}
|
|
}
|
|
}
|