enricodias / ftp-server
A pure PHP FTP server for non-filesystem backends with multi-node cluster support
Requires
- php: >=7.3
- psr/log: ^1.0 || ^2.0 || ^3.0
- react/socket: ^1.17
- react/stream: ^1.4
Requires (Dev)
- phpunit/phpunit: ^8.5 || ^9.0
Suggests
- monolog/monolog: Recommended PSR-3 logger implementation.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-08-23 22:54:32 UTC
README
A pure PHP FTP server for non-filesystem backends with multi-node cluster support.
This package implements the FTP protocol and passive-mode data transfer. It does not implement storage or authentication itself: the consumer provides those by implementing a small set of interfaces, so the same server can expose an FTP interface over anything (a database-backed virtual filesystem, cloud storage, etc), not just the local disk.
Only passive mode is supported.
Requirements
- PHP 7.3 or later
Installation
composer require enricodias/ftp-server
Configuration
The Config object describes how the server should run. It can be used in three ways, depending on how much of the package's functionality you need:
Single server, plain FTP
The simplest setup: one server, no encryption, no clustering.
use enricodias\FtpServer\Config\Config; use enricodias\FtpServer\Config\PortRange; $config = new Config( 21, // main (control) port new PortRange(50000, 50100) // passive port range );
Single server with FTPS
Supplying a TlsCertificate enables explicit FTPS (AUTH TLS) on the control connection.
use enricodias\FtpServer\Config\Config; use enricodias\FtpServer\Config\PortRange; use enricodias\FtpServer\Config\TlsCertificate; $config = new Config( 21, new PortRange(50000, 50100), new TlsCertificate('/path/to/cert.pem', '/path/to/key.pem') );
Multiple servers in a cluster
To let this node redirect passive transfers to other nodes that own the requested file, provide the full cluster group together: this node's id, the node coordination channel's port, every node in the cluster (including this one), and a shared secret. A TLS certificate is required in this mode, since the inter-node coordination channel is always encrypted; the same certificate also secures the client-facing FTPS connection.
use enricodias\FtpServer\Config\ClusterNode; use enricodias\FtpServer\Config\Config; use enricodias\FtpServer\Config\PortRange; use enricodias\FtpServer\Config\TlsCertificate; $config = new Config( 21, new PortRange(50000, 50100), new TlsCertificate('/path/to/cert.pem', '/path/to/key.pem'), 'node-a', // this node's id 2121, // node channel port, used for inter node communication [ new ClusterNode('node-a', '192.168.1.1'), new ClusterNode('node-b', '192.168.1.2'), ], 'a-shared-secret' );
The cluster arguments (node id, node channel port, node list, shared secret) are all-or-nothing: providing some but not all of them raises InvalidConfigException, as does any other invalid combination.
Implementing the storage and authentication contracts
Everything the server needs from your application lives under enricodias\FtpServer\Contract:
AuthenticatorInterfacevalidates credentials and returns anAuthResultInterface.AuthResultInterfacereports whether authentication succeeded, the authenticatedUserInterface, whether the client's data-connection IP should be validated against its control connection (some ISPs use IP pools that change mid-session), and an optional starting guess at which cluster node the user's files are probably on.UserInterfaceis an opaque handle for the authenticated user; the package never inspects it, only threads it through to the other contracts.DirectoryReaderInterfacelists, creates, deletes, and renames paths, returningDirectoryEntryInterfaceitems (name, whether it's a directory, size, last-modified time) fromlist().FileReaderInterface/FileWriterInterfaceopen read/write streams for file content, with offset and append support for resumable transfers.FileLocatorInterfaceresolves which cluster node currently owns a given file, returning aNodeLocationInterface(used only in clustered mode).
Every method that touches a path receives the authenticated UserInterface, so implementations can enforce per-user access rules.
Running the server
Single server
use enricodias\FtpServer\Server; $server = new Server($config, $authenticator, $directoryReader, $fileReader, $fileWriter); $server->run(); // serves connections until the process is stopped
Cluster
A FileLocatorInterface implementation is required as the sixth argument when clustering is enabled in $config (the server throws InvalidConfigException otherwise); pass null for a single, non-clustered server:
$server = new Server($config, $authenticator, $directoryReader, $fileReader, $fileWriter, $fileLocator);
The control connection will try to open data connections from the node that it believes the user's files are stored. This is a guess because the ftp protocol requires the data connection to be opened before sending the file path it needs. If the requested file is not found in the node who opened the data connection, the file will be streamed between nodes to fulfill the request to the client. The new node will be remembered for this user and used for subsequent requests, even if the client reconnects, until the server restarts.
Logging
Passing a PSR-3 logger as a seventh argument logs server lifecycle events (startup, bind failures, client connect/disconnect, timeouts, unexpected errors). Without it, nothing is logged:
$server = new Server($config, $authenticator, $directoryReader, $fileReader, $fileWriter, $fileLocator, $logger);
Trying it out with a real FTP client
examples/serve.php runs a demo server backed by an in-memory virtual filesystem, so you can point any FTP client at it without writing your own storage backend:
composer install composer serve
This starts a server on 127.0.0.1:2121 with username demo and password demo (all overridable: php examples/serve.php [port] [username] [password]).