Article summary
On a recent project, I had a need to create an FTP connection to a third-party server. Since we had no control over the server, I also wanted to setup a test FTP server that I could use to run our client against to make sure we could connect successfully. It turned out that implementing an FTP client and server in Node required a bit of work. We eventually solved this by using two libraries from NPM – jsftp for the client, and ftp-srv for the test server.
Setting Up the Client
After adding jsftp
to your package, you’re good to start developing your client. Below is the code I used to get ours up and running. The basic setup is pretty simple, and the constructor takes in the host and port to connect to, as well as the username and password to create that connection (you can look at the jsftp
documents here for more setup options). I wrote this in Typescript, so there are also some types at the bottom that expand what comes with the library that you can use as well.
const jsftp = require('jsftp');
export const createFtpClient = async (host: string, port: number, username: string, password: string): Promise => {
const client: FtpWithAuth = new jsftp({
host,
port,
});
return new Promise((resolve, reject) => {
client.on('connect', () => {
client.auth(username, password, (err, res) => {
if (err) {
quit(client);
reject(err);
} else {
resolve({
put: (sourcePath: string, pathToPut: string) => put(client, sourcePath, pathToPut),
ls: (pathToLs: string) => ls(client, pathToLs),
quit: () => quit(client),
});
}
});
});
});
});
interface FtpClient {
put: (sourcePath: string, pathToPut: string) => Promise;
ls: (pathToLs: string) => Promise<LsResponse[]>;
quit: () => void;
}
interface FtpWithAuth extends Ftp {
auth: (username: string, password: string, callback: (error, response) => void) => void;
}
interface LsResponse {
name: string;
}
With our client module setup, we should be good to make an FTP connection. Now all we need to do is setup our server.
Setting Up the Server
Below is the code I used to get the server running. Here are some quick notes to help make sense of it:
- You’ll notice that I manually create a logger (using the
bunyan
library) and pass that in. The reason is thatftp-srv
creates a logger by default whenever you initiate a new server, and it’s quite verbose. In order to get around this, you’ll have to make your own “quiet” logger. - The types that come with the library aren’t always correct. Both the
server.close()
andserver.listen()
functions are asynchronous, but this isn’t reflected by default. - The library, by default, tries to open a passive FTP connection for the server on port 23. This can cause access issues for some people and otherwise cause the server not to run, so we explicitly set the
pasv_range
when creating the server. - Similar to the client, first make sure that you have
ftp-srv
installed.
const ftpServer = require('ftp-srv');
const bunyan = require(‘bunyan’);
export const startFtpServer = async (host: string, port: number, user: string, pass: string) => {
const quietLog = bunyan.createLogger({
name: 'quiet-logger',
level: 60,
});
const server = new ftpServer(`ftp://${host}:${port}`, {
log: quietLog,
pasv_range: '8400-8500', // change this to an open port range for your app
});
server.on('login', ({ connection, username, password }, resolve, reject) => {
if (username === user && password === pass) {
// If connected, add a handler to confirm file uploads
connection.on('STOR', (error, fileName) => {
if (error) {
console.error(`FTP server error: could not receive file ${fileName} for upload ${error}`);
}
console.info(`FTP server: upload successfully received - ${fileName}`);
});
resolve();
} else {
reject(new Error('Unable to authenticate with FTP server: bad username or password'));
}
});
server.on('client-error', ({ context, error }) => {
console.error(`FTP server error: error interfacing with client ${context} ${error} on ftp://${host}:${port} ${JSON.stringify(error)}`);
});
const closeFtpServer = async () => {
await server.close();
};
// The types are incorrect here - listen returns a promise
await server.listen();
return {
shutdownFunc: async () => {
// server.close() returns a promise - another incorrect type
await closeFtpServer();
},
};
};
With all of that set up, you should be able to run your server and your client. Make sure that your client is trying to connect to the host and port that your server sets, and you should be good to go.
Happy Testing!
With both a client and a server set up, you’re ready to begin testing! In our app, we simply started a test FTP server on the same Node process that our tests were running. The ftp-srv
library also comes with a CLI, so you could use that to determine when your test server runs.
The process to get all of this setup isn’t ideal, but the current landscape of FTP libraries in Node lacks a comprehensive and well-maintained solution for an FTP server. We tried some other libraries but ultimately settled on this combination since they were the easiest to work with and provided the best functionality.
.
I was trying this today, one thing sure is changed
const server = new ftpServer({
url: ‘ftp://0.0.0.0:8001’,
// pasv_url: ‘ftp://0.0.0.0:8001’,
pasv_range: ‘8000-8500’, // change this to an open port range for your app
});
notice, the url parameter is passed as an option now