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)

Laravel 5.6, SFTP, connection hygiene, and you!

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.