flysystem-offload/vendor/league/flysystem-sftp-v3/SftpConnectionProvider.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());
}
}
}