Modern Extensible FTP Server
ftp-srv is a modern and extensible FTP server library for Node.js, currently stable at version 4.6.3. It provides a promise-based API for creating FTP and FTPS (Explicit & Implicit TLS) servers with support for both passive and active data transfers. The project maintains an active release cadence, frequently publishing bug fixes and minor features, as seen in recent updates like recursive directory creation and TypeScript declaration improvements. Its key differentiators include a highly extensible file system per connection, allowing for custom storage backends, and comprehensive support for various FTP commands and connection modes. It differentiates itself by offering detailed control over passive connection URLs and port ranges, addressing complexities often found in network deployments behind NATs or firewalls. It requires Node.js version 12 or higher.
Common errors
-
Error: listen EADDRINUSE: address already in use :::21
cause The specified port (e.g., 21 for FTP) is already being used by another application or a previously terminated instance of the FTP server that hasn't fully released the port.fixChange the `port` in the `url` option to an available port, or ensure no other process is listening on the desired port before starting `ftp-srv`. On Linux, you can use `lsof -i :<port>` to find the process. -
530 Not logged in. Invalid username or password.
cause This error is returned to the FTP client when the provided username and/or password do not pass the validation logic within your `ftpServer.on('login')` event handler.fixReview your `login` event handler to ensure it correctly resolves credentials. Verify that the username and password being sent by the FTP client match the expected values defined in your server-side logic. -
FTP client hangs during PASV data transfer or directory listing (e.g., after 'PASV' command).
cause This issue typically arises from an incorrect passive mode (`PASV`) configuration, often due to Network Address Translation (NAT), firewalls, or an improperly set `pasv_url` option, preventing the client from connecting to the server's data port.fixConfigure the `pasv_url` option with your server's external (WAN) IP address and ensure that the port range specified by `pasv_min` and `pasv_max` is open and accessible through your firewall.
Warnings
- breaking Version 4.3.4 introduced a critical security fix to disallow PORT connections to alternate hosts. Running older versions (prior to 4.3.4) exposes the server to potential security vulnerabilities (GHSA-jw37-5gqr-cf9j).
- gotcha When deploying `ftp-srv` behind a NAT or firewall, the `pasv_url` option is crucial for passive mode connections. It must be correctly configured with the external (WAN) IP address; otherwise, external clients may hang when attempting data transfers.
- gotcha The `url` option's hostname (e.g., `ftp://127.0.0.1:21`) must be `0.0.0.0` or the specific external IP address of the server to accept connections from outside the local machine. Using `127.0.0.1` will restrict client connections to localhost only.
- breaking `ftp-srv` requires Node.js version 12 or higher. Deployments on older Node.js runtimes will fail to start or operate correctly due to reliance on newer language features and APIs.
Install
-
npm install ftp-srv -
yarn add ftp-srv -
pnpm add ftp-srv
Imports
- FtpSrv
const FtpSrv = require('ftp-srv');import { FtpSrv } from 'ftp-srv'; - errors
const { errors } = require('ftp-srv');import { errors } from 'ftp-srv'; - FtpSrvOptions
import { FtpSrv, type FtpSrvOptions } from 'ftp-srv';
Quickstart
import { FtpSrv, errors } from 'ftp-srv';
import path from 'path';
import { promises as fs } from 'fs'; // For creating the dummy ftp_root directory
const port = 21;
const ftpServer = new FtpSrv({
url: `ftp://0.0.0.0:${port}`,
anonymous: true
});
ftpServer.on('login', ({ connection, username, password }, resolve, reject) => {
// In a real application, you would validate credentials against a database
// For this example, 'anonymous' allows access to a specific root path
if (username === 'anonymous' && password === 'anonymous') {
const userRoot = path.join(process.cwd(), 'ftp_root');
// A real scenario might dynamically create/select a user's directory
return resolve({ root: userRoot });
}
return reject(new errors.GeneralError('Invalid username or password', 401));
});
ftpServer.listen().then(() => {
console.log(`FTP server started on ftp://0.0.0.0:${port}`);
console.log('Connect with anonymous:anonymous to access', path.join(process.cwd(), 'ftp_root'));
}).catch((err: Error) => {
console.error('Failed to start FTP server:', err);
process.exit(1);
});
// Helper to ensure the FTP root directory exists and contains a sample file
async function ensureFtpRootDirectory() {
const rootPath = path.join(process.cwd(), 'ftp_root');
try {
await fs.mkdir(rootPath, { recursive: true });
const welcomeFilePath = path.join(rootPath, 'welcome.txt');
await fs.writeFile(welcomeFilePath, 'Welcome to the ftp-srv server!\nThis is a sample file.', { flag: 'wx' }); // 'wx' to only write if file doesn't exist
console.log(`Ensured FTP root directory at ${rootPath}`);
} catch (error: any) {
if (error.code !== 'EEXIST') { // Ignore if directory/file already exists
console.error('Error setting up FTP root directory:', error);
}
}
}
ensureFtpRootDirectory();