From 52787b6f2e566e38b7c2f36e642a631838417fb6 Mon Sep 17 00:00:00 2001 From: TingSong Xu Date: Tue, 24 Oct 2023 17:41:35 +0800 Subject: [PATCH] Disconnect the SFTP connection if `UnexpectedValueException` thrown in the SFTP class. - This exception indicated that the request/response packets are mismatched. --- src/PhpseclibV3/SftpAdapter.php | 208 +++++++++++++++++++++----------- 1 file changed, 138 insertions(+), 70 deletions(-) diff --git a/src/PhpseclibV3/SftpAdapter.php b/src/PhpseclibV3/SftpAdapter.php index b47ed0695..39682875e 100644 --- a/src/PhpseclibV3/SftpAdapter.php +++ b/src/PhpseclibV3/SftpAdapter.php @@ -15,6 +15,9 @@ use League\Flysystem\UnableToCheckFileExistence; use League\Flysystem\UnableToCopyFile; use League\Flysystem\UnableToCreateDirectory; +use League\Flysystem\UnableToDeleteDirectory; +use League\Flysystem\UnableToDeleteFile; +use League\Flysystem\UnableToListContents; use League\Flysystem\UnableToMoveFile; use League\Flysystem\UnableToReadFile; use League\Flysystem\UnableToRetrieveMetadata; @@ -27,20 +30,22 @@ use phpseclib3\Net\SFTP; use Throwable; +use UnexpectedValueException; use function rtrim; class SftpAdapter implements FilesystemAdapter { + private const UNEXPECTED_SFTP_PACKET_MESSAGE = 'SFTP server respond an unexpected packet.'; private VisibilityConverter $visibilityConverter; - private PathPrefixer $prefixer; - private MimeTypeDetector $mimeTypeDetector; + private PathPrefixer $prefixer; + private MimeTypeDetector $mimeTypeDetector; public function __construct( private ConnectionProvider $connectionProvider, - string $root, - VisibilityConverter $visibilityConverter = null, - MimeTypeDetector $mimeTypeDetector = null, - private bool $detectMimeTypeUsingPath = false, + string $root, + VisibilityConverter $visibilityConverter = null, + MimeTypeDetector $mimeTypeDetector = null, + private bool $detectMimeTypeUsingPath = false, ) { $this->prefixer = new PathPrefixer($root); $this->visibilityConverter = $visibilityConverter ?? new PortableVisibilityConverter(); @@ -50,9 +55,12 @@ public function __construct( public function fileExists(string $path): bool { $location = $this->prefixer->prefixPath($path); - + $connection = $this->connectionProvider->provideConnection(); try { - return $this->connectionProvider->provideConnection()->is_file($location); + return $connection->is_file($location); + } catch (UnexpectedValueException $exception) { + $connection->disconnect(); + throw UnableToCheckFileExistence::forLocation($path, $exception); } catch (Throwable $exception) { throw UnableToCheckFileExistence::forLocation($path, $exception); } @@ -61,9 +69,12 @@ public function fileExists(string $path): bool public function directoryExists(string $path): bool { $location = $this->prefixer->prefixDirectoryPath($path); - + $connection = $this->connectionProvider->provideConnection(); try { - return $this->connectionProvider->provideConnection()->is_dir($location); + return $connection->is_dir($location); + } catch (UnexpectedValueException $exception) { + $connection->disconnect(); + throw UnableToCheckDirectoryExistence::forLocation($path, $exception); } catch (Throwable $exception) { throw UnableToCheckDirectoryExistence::forLocation($path, $exception); } @@ -80,14 +91,19 @@ private function upload(string $path, $contents, Config $config): void { $this->ensureParentDirectoryExists($path, $config); $connection = $this->connectionProvider->provideConnection(); - $location = $this->prefixer->prefixPath($path); + try { + $location = $this->prefixer->prefixPath($path); - if ( ! $connection->put($location, $contents, SFTP::SOURCE_STRING)) { - throw UnableToWriteFile::atLocation($path, 'not able to write the file'); - } + if ( ! $connection->put($location, $contents, SFTP::SOURCE_STRING)) { + throw UnableToWriteFile::atLocation($path, $connection->getLastSFTPError()); + } - if ($visibility = $config->get(Config::OPTION_VISIBILITY)) { - $this->setVisibility($path, $visibility); + if ($visibility = $config->get(Config::OPTION_VISIBILITY)) { + $this->setVisibility($path, $visibility); + } + } catch (UnexpectedValueException $exception) { + $connection->disconnect(); + throw UnableToWriteFile::atLocation($path, self::UNEXPECTED_SFTP_PACKET_MESSAGE, $exception); } } @@ -109,16 +125,21 @@ private function makeDirectory(string $directory, ?string $visibility): void $location = $this->prefixer->prefixPath($directory); $connection = $this->connectionProvider->provideConnection(); - if ($connection->is_dir($location)) { - return; - } + try { + if ($connection->is_dir($location)) { + return; + } - $mode = $visibility ? $this->visibilityConverter->forDirectory( - $visibility - ) : $this->visibilityConverter->defaultForDirectories(); + $mode = $visibility ? $this->visibilityConverter->forDirectory( + $visibility + ) : $this->visibilityConverter->defaultForDirectories(); - if ( ! $connection->mkdir($location, $mode, true) && ! $connection->is_dir($location)) { - throw UnableToCreateDirectory::atLocation($directory); + if ( ! $connection->mkdir($location, $mode, true) && ! $connection->is_dir($location)) { + throw UnableToCreateDirectory::atLocation($directory, $connection->getLastSFTPError()); + } + } catch (UnexpectedValueException $exception) { + $connection->disconnect(); + throw UnableToCreateDirectory::atLocation($directory, self::UNEXPECTED_SFTP_PACKET_MESSAGE, $exception); } } @@ -148,13 +169,20 @@ public function read(string $path): string { $location = $this->prefixer->prefixPath($path); $connection = $this->connectionProvider->provideConnection(); - $contents = $connection->get($location); + try { + $contents = $connection->get($location); - if ( ! is_string($contents)) { - throw UnableToReadFile::fromLocation($path); - } + if ( ! is_string($contents)) { + throw UnableToReadFile::fromLocation($path, $connection->getLastSFTPError()); + } - return $contents; + return $contents; + } catch (UnexpectedValueException $exception) { + $connection->disconnect(); + throw UnableToReadFile::fromLocation($path, self::UNEXPECTED_SFTP_PACKET_MESSAGE, $exception); + } catch (Throwable $exception) { + throw UnableToReadFile::fromLocation($path, $exception->getMessage(), $exception); + } } public function readStream(string $path) @@ -164,29 +192,49 @@ public function readStream(string $path) /** @var resource $readStream */ $readStream = fopen('php://temp', 'w+'); - if ( ! $connection->get($location, $readStream)) { - fclose($readStream); - throw UnableToReadFile::fromLocation($path); - } + try { + if ( ! $connection->get($location, $readStream)) { + fclose($readStream); + throw UnableToReadFile::fromLocation($path, $connection->getLastSFTPError()); + } - rewind($readStream); + rewind($readStream); - return $readStream; + return $readStream; + } catch (UnexpectedValueException $exception) { + $connection->disconnect(); + fclose($readStream); + throw UnableToReadFile::fromLocation($path, self::UNEXPECTED_SFTP_PACKET_MESSAGE, $exception); + } catch (Throwable $exception) { + throw UnableToReadFile::fromLocation($path, $exception->getMessage(), $exception); + } } public function delete(string $path): void { $location = $this->prefixer->prefixPath($path); $connection = $this->connectionProvider->provideConnection(); - $connection->delete($location); + try { + $connection->delete($location); + } catch (UnexpectedValueException $exception) { + $connection->disconnect(); + throw UnableToDeleteFile::atLocation($path, self::UNEXPECTED_SFTP_PACKET_MESSAGE, $exception); + } catch (Throwable $exception) { + throw UnableToDeleteFile::atLocation($path, $exception->getMessage(), $exception); + } } public function deleteDirectory(string $path): void { $location = rtrim($this->prefixer->prefixPath($path), '/') . '/'; $connection = $this->connectionProvider->provideConnection(); - $connection->delete($location); - $connection->rmdir($location); + try { + $connection->delete($location); + $connection->rmdir($location); + } catch (UnexpectedValueException $exception) { + $connection->disconnect(); + throw UnableToDeleteDirectory::atLocation($path, self::UNEXPECTED_SFTP_PACKET_MESSAGE, $exception); + } } public function createDirectory(string $path, Config $config): void @@ -200,8 +248,13 @@ public function setVisibility(string $path, string $visibility): void $connection = $this->connectionProvider->provideConnection(); $mode = $this->visibilityConverter->forFile($visibility); - if ( ! $connection->chmod($mode, $location, false)) { - throw UnableToSetVisibility::atLocation($path); + try { + if ( ! $connection->chmod($mode, $location, false)) { + throw UnableToSetVisibility::atLocation($path, $connection->getLastSFTPError()); + } + } catch (UnexpectedValueException $exception) { + $connection->disconnect(); + throw UnableToSetVisibility::atLocation($path, self::UNEXPECTED_SFTP_PACKET_MESSAGE, $exception); } } @@ -209,19 +262,24 @@ private function fetchFileMetadata(string $path, string $type): FileAttributes { $location = $this->prefixer->prefixPath($path); $connection = $this->connectionProvider->provideConnection(); - $stat = $connection->stat($location); + try { + $stat = $connection->stat($location); - if ( ! is_array($stat)) { - throw UnableToRetrieveMetadata::create($path, $type); - } + if ( ! is_array($stat)) { + throw UnableToRetrieveMetadata::create($path, $type); + } - $attributes = $this->convertListingToAttributes($path, $stat); + $attributes = $this->convertListingToAttributes($path, $stat); - if ( ! $attributes instanceof FileAttributes) { - throw UnableToRetrieveMetadata::create($path, $type, 'path is not a file'); - } + if ( ! $attributes instanceof FileAttributes) { + throw UnableToRetrieveMetadata::create($path, $type, 'path is not a file'); + } - return $attributes; + return $attributes; + } catch (UnexpectedValueException $exception) { + $connection->disconnect(); + throw UnableToRetrieveMetadata::create($path, $type, self::UNEXPECTED_SFTP_PACKET_MESSAGE, $exception); + } } public function mimeType(string $path): FileAttributes @@ -259,29 +317,34 @@ public function visibility(string $path): FileAttributes public function listContents(string $path, bool $deep): iterable { $connection = $this->connectionProvider->provideConnection(); - $location = $this->prefixer->prefixPath(rtrim($path, '/')) . '/'; - $listing = $connection->rawlist($location, false); - - if (false === $listing) { - return; - } + try { + $location = $this->prefixer->prefixPath(rtrim($path, '/')) . '/'; + $listing = $connection->rawlist($location, false); - foreach ($listing as $filename => $attributes) { - if ($filename === '.' || $filename === '..') { - continue; + if (false === $listing) { + return; } - // Ensure numeric keys are strings. - $filename = (string) $filename; - $path = $this->prefixer->stripPrefix($location . ltrim($filename, '/')); - $attributes = $this->convertListingToAttributes($path, $attributes); - yield $attributes; + foreach ($listing as $filename => $attributes) { + if ($filename === '.' || $filename === '..') { + continue; + } + + // Ensure numeric keys are strings. + $filename = (string) $filename; + $path = $this->prefixer->stripPrefix($location . ltrim($filename, '/')); + $attributes = $this->convertListingToAttributes($path, $attributes); + yield $attributes; - if ($deep && $attributes->isDir()) { - foreach ($this->listContents($attributes->path(), true) as $child) { - yield $child; + if ($deep && $attributes->isDir()) { + foreach ($this->listContents($attributes->path(), true) as $child) { + yield $child; + } } } + } catch (UnexpectedValueException $exception) { + $connection->disconnect(); + throw UnableToListContents::atLocation($path, $deep, $exception); } } @@ -314,13 +377,18 @@ public function move(string $source, string $destination, Config $config): void try { $this->ensureParentDirectoryExists($destination, $config); + + if ( ! $connection->rename($sourceLocation, $destinationLocation)) { + throw UnableToMoveFile::fromLocationTo($source, $destination); + } + } catch (UnableToMoveFile $exception) { + throw $exception; + } catch (UnexpectedValueException $exception) { + $connection->disconnect(); + throw UnableToMoveFile::fromLocationTo($source, $destination, $exception); } catch (Throwable $exception) { throw UnableToMoveFile::fromLocationTo($source, $destination, $exception); } - - if ( ! $connection->rename($sourceLocation, $destinationLocation)) { - throw UnableToMoveFile::fromLocationTo($source, $destination); - } } public function copy(string $source, string $destination, Config $config): void