Setting up a Test FTP Server in Node

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 that ftp-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() and server.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.