Laravel 5.6, SFTP, connection hygiene, and you!
Laravel 5.6 introduced SFTP as a Storage method. Do you know whether it closes the opened connections or leaves them hanging? Read this! (yes, it does close them)
As part of one of the projects I’m working on, I was tasked to create an in-between Laravel app that takes data from CSV files uploaded to an SFTP server and a site’s WooCommerce API. Occasionally the requests to the SFTP server would fail with “connection closed prematurely”. I’ve reached out to the team maintaining the server to ask about it, and there was a question I couldn’t actually answer: “Are you closing your SFTP connections?”
Decided to look it up. Below is the tale of how I get to the answer. Which is “yes, at latest when the current php process concludes”.
Primer on Laravel 5.6 SFTP
The above version introduced sftp as a kind of disk we can connect to. You can read more about it on the documentation about filesystem in Laravel, but once the config is set, using it is about as hard as this:
Storage::disk('sftp')->files('directory');
That will give us the list of files in the directory named “directory”. However it does NOT close the connection once it’s done with that (at least not yet). To find out what happens, I had to figure out what Storage::disk('sftp')
is. The dd
utility function comes in super handy. It told me that Storage::disk('sftp')
is the following:
Illuminate\Filesystem\FilesystemAdapter {#685
#driver: League\Flysystem\Filesystem {#683
#adapter: League\Flysystem\Sftp\SftpAdapter {#681
#connection: null
#port: 22
...
The files
call lands on the FilesystemAdapter
which forwards the request to the Filesystem::listContents
, which then just forwards the call to the adapter itself (SftpAdapter
in this case).
The FilesystemAdapter
also has a __call
method, which will forward any unknown calls to the driver.
class FilesystemAdapter implements FilesystemContract, CloudFilesystemContract
{
...
public function files($directory = null, $recursive = false)
{
$contents = $this->driver->listContents($directory, $recursive);
return $this->filterContentsByType($contents, 'file');
}
...
public function __call($method, array $parameters)
{
return call_user_func_array([$this->driver, $method], $parameters);
}
...
}
So the question is: how far do I need to go until I find a disconnect
method on these?
Down the rabbit hole
The earliest we have is on SftpAdapter
, which just sets the connection
property to null.
class SftpAdapter extends AbstractFtpAdapter
{
...
/**
* Disconnect.
*/
public function disconnect()
{
$this->connection = null;
}
...
}
Looking into what the connection
property is, we find that it is a new SFTP
, which is phpseclib\Net\SFTP
by its full name.
That class has a _disconnect
method, which calls its parent’s _disconnect
method. The parent is phpseclib\Net\SSH2
, where the _disconnect
method actually sends a binary bye
command to the server! But how does that get called?
class SFTP extends SSH2
{
...
function _disconnect($reason)
{
$this->pwd = false;
parent::_disconnect($reason);
}
...
}
class SSH2
{
...
function _disconnect($reason)
{
if ($this->bitmap & self::MASK_CONNECTED) {
$data = pack('CNNa*Na*', NET_SSH2_MSG_DISCONNECT, $reason, 0, '', 0, '');
$this->_send_binary_packet($data);
$this->bitmap = 0;
fclose($this->fsock);
return false;
}
}
...
}
Turns out the SSH2
class also has a __destruct
method, which calls the disconnect
method (no leading underscore) on itself, and because SFTP extends SSH2
, and SFTP
does not have its own disconnect
method, it’s going to use SSH2
’s.
That will call _disconnect
, which SFTP
does have, and SFTP::_disconnect
then calls SSH2::_disconnect
, and the connection is closed.
class SSH2
{
function disconnect()
{
$this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION);
if (isset($this->realtime_log_file) && is_resource($this->realtime_log_file)) {
fclose($this->realtime_log_file);
}
}
function __destruct()
{
$this->disconnect();
}
...
}
Going back a few levels, when the SftpAdapter::disconnect
method is called, and $this->connection = null
happens, that just means that the variable previously there gets destructed, so the connection gets closed automatically.
However nothing is calling the disconnect
method on the adapter.
Turns out that’s not even needed, because at the end of the php script, everything is garbage collected, which means everything in the session is destructed, including the connection, so the __destruct
method is called, so as soon as the running php script ends, the connection is closed.
Automatically.
And now we know.
P.S.
You can still close the connection manually using this:
Storage::disk('sftp')->getAdapter()->disconnect();
Photo by Randall Bruder on Unsplash.