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:
prepareTransfer
is expected to setdataSocket
when it finishes.- (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:
Lines 251 to 259 in 18b24d2
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.