<?php
declare(strict_types=1);
namespace League\Flysystem\PhpseclibV3;
use phpseclib3\Crypt\Common\AsymmetricKey;
use phpseclib3\Crypt\PublicKeyLoader;
use phpseclib3\Exception\NoKeyLoadedException;
use phpseclib3\Exception\UnableToConnectException;
use phpseclib3\Net\SFTP;
use phpseclib3\System\SSH\Agent;
use RuntimeException;
use Throwable;
use function base64_decode;
use function implode;
use function str_split;
class SftpConnectionProvider implements ConnectionProvider
{
/**
* @var string
*/
private $host;
/**
* @var string
*/
private $username;
/**
* @var string|null
*/
private $password;
/**
* @var bool
*/
private $useAgent;
/**
* @var int
*/
private $port;
/**
* @var int
*/
private $timeout;
/**
* @var SFTP|null
*/
private $connection;
/**
* @var ConnectivityChecker
*/
private $connectivityChecker;
/**
* @var string|null
*/
private $hostFingerprint;
/**
* @var string|null
*/
private $privateKey;
/**
* @var string|null
*/
private $passphrase;
/**
* @var int
*/
private $maxTries;
public function __construct(
string $host,
string $username,
string $password = null,
string $privateKey = null,
string $passphrase = null,
int $port = 22,
bool $useAgent = false,
int $timeout = 10,
int $maxTries = 4,
string $hostFingerprint = null,
ConnectivityChecker $connectivityChecker = null
) {
$this->host = $host;
$this->username = $username;
$this->password = $password;
$this->privateKey = $privateKey;
$this->passphrase = $passphrase;
$this->useAgent = $useAgent;
$this->port = $port;
$this->timeout = $timeout;
$this->hostFingerprint = $hostFingerprint;
$this->connectivityChecker = $connectivityChecker ?: new SimpleConnectivityChecker();
$this->maxTries = $maxTries;
}
public function provideConnection(): SFTP
{
$tries = 0;
start:
$connection = $this->connection instanceof SFTP
? $this->connection
: $this->setupConnection();
if ( ! $this->connectivityChecker->isConnected($connection)) {
$connection->disconnect();
$this->connection = null;
if ($tries < $this->maxTries) {
$tries++;
goto start;
}
throw UnableToConnectToSftpHost::atHostname($this->host);
}
return $this->connection = $connection;
}
private function setupConnection(): SFTP
{
try {
$connection = new SFTP($this->host, $this->port, $this->timeout);
$connection->disableStatCache();
$this->checkFingerprint($connection);
$this->authenticate($connection);
} catch (UnableToConnectException $exception) {
throw UnableToConnectToSftpHost::atHostname($exception->getMessage());
} catch (Throwable $exception) {
$connection->disconnect();
throw $exception;
}
return $connection;
}
private function checkFingerprint(SFTP $connection): void
{
if ( ! $this->hostFingerprint) {
return;
}
$publicKey = $connection->getServerPublicHostKey() ?: 'no-public-key';
$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
{
try {
if ($this->privateKey !== null) {
$this->authenticateWithPrivateKey($connection);
} elseif ($this->useAgent) {
$this->authenticateWithAgent($connection);
} elseif ( ! $connection->login($this->username, $this->password)) {
throw UnableToAuthenticate::withPassword();
}
} catch (Throwable $exception) {
$connection->disconnect();
throw $exception;
}
}
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
);
}
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();
}
private function loadPrivateKey(): AsymmetricKey
{
if ("---" !== substr($this->privateKey, 0, 3) && 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();
}
throw new RuntimeException();
}
private function authenticateWithAgent(SFTP $connection): void
{
$agent = new Agent();
if ( ! $connection->login($this->username, $agent)) {
throw UnableToAuthenticate::withSshAgent();
}
}
}