patrickjuchli / basic-ftp

FTP client for Node.js, supports FTPS over TLS, passive mode over IPv6, async/await, and Typescript.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Impossible to implement active mode with prepareTransfer

mcpiroman opened this issue · comments

The prepareTransfer callback is said to enable user to implement active mode themselves, but I find it problematic in my case because:

  1. prepareTransfer is expected to set dataSocket when it finishes.
  2. (At least my) ftp server establishes the data connection to the client when it's requested by the appropriate command, not after PORT command.

Thus, if prepareTransfer sends PORT command, it just informs the server that the client is listening, it's not requesting the server to actually connect. For that, the desired data command must be sent, e.g. RETR, but the library expects the dataSocket to be set-up before it can send that RETR.

My request conflicts with that:

basic-ftp/src/transfer.ts

Lines 251 to 259 in 18b24d2

export function downloadTo(destination: Writable, config: TransferConfig): Promise<FTPResponse> {
if (!config.ftp.dataSocket) {
throw new Error("Download will be initiated but no data connection is available.")
}
// It's possible that data transmission begins before the control socket
// receives the announcement. Start listening for data immediately.
config.ftp.dataSocket.pipe(destination)
const resolver = new TransferResolver(config.ftp, config.tracker)
return config.ftp.handle(config.command, (res, task) => {

However, I'm not sure it's necessary to pipe the socket this early. Maybe it's different for nodejs but generally it's very rare with things akin to sockets that the data is lost if you don't read it right away. Or maybe it's just for performance, to avoid buffering.

I'm not 100% sure the server is conformant with this behavior, though I haven't found any information it's not, i.e. that it should connect immediately after receiving PORT, nor any other command that would force the server to do so.

I was able to get active ftp working by hacking together a temporary socket. This is a very hacky way to do it so be warned if you decide to use this.

class MockSocket {
  socket: Socket;
  timeout: number;
  timeoutCallback: () => void;
  writable: Writable;
  onceHandlers: OnceHolder[] = [];
  server: Server;
  port: number;

  constructor() {}

  public setSocket(socket: Socket, server: Server, port: number) {
    this.socket = socket;
    this.server = server;
    this.port = port;
    this.socket.setTimeout(this.timeout, this.timeoutCallback);
    // pipe the set socket to this instance
    if (this.writable) {
      socket.pipe(this.writable);
    }
    this.onceHandlers.forEach(once => {
      socket.once(once.name, once.callback);
    });
  }

  public pipe(writable: Writable) {
    this.writable = writable;
    if (this.socket) {
      this.socket.pipe(writable);
    }
  }

  public once(name: string, callback: (...args) => void) {
    this.onceHandlers.push({ name, callback });
  }

  public destroy() {
    if (this.socket) {
      this.socket.destroy();
    }
    try {
      if (this.server) {
        this.server.close();
        releaseFtpPort(this.port);
      }
    } catch (ex) {
      console.error('There was a problem closing the server', ex);
    }
  }

  public setTimeout(timeout: number, callback?: () => void) {
    this.timeout = timeout;
    this.timeoutCallback = callback;
    if (this.socket) {
      this.socket.setTimeout(timeout, callback);
    }
  }

  public removeAllListeners() {
    this.onceHandlers = [];
    if (this.socket) {
      this.socket.removeAllListeners();
    }
  }
}


export async function prepareTransfer(ftp: FTPContext): Promise<FTPResponse> {
  // Get your external ip address somehow.
  const ip = await getExternalIpAddress();
  // Gets an open port.
  const port = await getFtpPort();
  if (port === null) {
    throw new Error('Could not get port in the specified timeout.');
  }
  // PORT 192,168,150,80,14,178
  // The first four octets are the IP address while the last two octets comprise the port that will be used for the data connection.
  // To find the actual port multiply the fifth octet by 256 and then add the sixth octet to the total.
  // Thus in the example below the port number is ( (14*256) + 178), or 3762
  const p1 = Math.floor(port / 256);
  const p2 = port % 256;
  const command = `PORT ${ip.replace(/\./g, ',')},${p1},${p2}`;

  // Data socket pipes before the connection so use the mock socket.
  const mockSocket = new MockSocket();
  ftp.dataSocket = mockSocket as any;

  let ftpResponse = new Promise<FTPResponse>((resolve, reject) => {
    let response: FTPResponse;
    const server = createServer(function (socket) {
      console.log('FTP Client connected');
      mockSocket.setSocket(socket, server, port);
    });
    if (port == null) {
      reject('Timeout waiting to get active server port.');
      return;
    }
    server.on('error', err => {
      reject(err);
    });
    server.listen(port, async () => {
      console.log(`socket server for ftp started at port ${port}`);
      // send the port request
      response = await ftp.request(command);
      if (response.code !== 200) {
        reject('Could not connect');
        server.close();
        releaseFtpPort(port);
      }
      resolve(response);
    });
  });

I've removed the section mentioning potential support for active mode. It's also not a goal to support this.