1071 lines
29 KiB
JavaScript
1071 lines
29 KiB
JavaScript
|
var fs = require('fs'),
|
||
|
tls = require('tls'),
|
||
|
zlib = require('zlib'),
|
||
|
Socket = require('net').Socket,
|
||
|
EventEmitter = require('events').EventEmitter,
|
||
|
inherits = require('util').inherits,
|
||
|
inspect = require('util').inspect;
|
||
|
|
||
|
var Parser = require('./parser');
|
||
|
var XRegExp = require('xregexp').XRegExp;
|
||
|
|
||
|
var REX_TIMEVAL = XRegExp.cache('^(?<year>\\d{4})(?<month>\\d{2})(?<date>\\d{2})(?<hour>\\d{2})(?<minute>\\d{2})(?<second>\\d+)(?:.\\d+)?$'),
|
||
|
RE_PASV = /([\d]+),([\d]+),([\d]+),([\d]+),([-\d]+),([-\d]+)/,
|
||
|
RE_EOL = /\r?\n/g,
|
||
|
RE_WD = /"(.+)"(?: |$)/,
|
||
|
RE_SYST = /^([^ ]+)(?: |$)/;
|
||
|
|
||
|
var /*TYPE = {
|
||
|
SYNTAX: 0,
|
||
|
INFO: 1,
|
||
|
SOCKETS: 2,
|
||
|
AUTH: 3,
|
||
|
UNSPEC: 4,
|
||
|
FILESYS: 5
|
||
|
},*/
|
||
|
RETVAL = {
|
||
|
PRELIM: 1,
|
||
|
OK: 2,
|
||
|
WAITING: 3,
|
||
|
ERR_TEMP: 4,
|
||
|
ERR_PERM: 5
|
||
|
},
|
||
|
/*ERRORS = {
|
||
|
421: 'Service not available, closing control connection',
|
||
|
425: 'Can\'t open data connection',
|
||
|
426: 'Connection closed; transfer aborted',
|
||
|
450: 'Requested file action not taken / File unavailable (e.g., file busy)',
|
||
|
451: 'Requested action aborted: local error in processing',
|
||
|
452: 'Requested action not taken / Insufficient storage space in system',
|
||
|
500: 'Syntax error / Command unrecognized',
|
||
|
501: 'Syntax error in parameters or arguments',
|
||
|
502: 'Command not implemented',
|
||
|
503: 'Bad sequence of commands',
|
||
|
504: 'Command not implemented for that parameter',
|
||
|
530: 'Not logged in',
|
||
|
532: 'Need account for storing files',
|
||
|
550: 'Requested action not taken / File unavailable (e.g., file not found, no access)',
|
||
|
551: 'Requested action aborted: page type unknown',
|
||
|
552: 'Requested file action aborted / Exceeded storage allocation (for current directory or dataset)',
|
||
|
553: 'Requested action not taken / File name not allowed'
|
||
|
},*/
|
||
|
bytesNOOP = new Buffer('NOOP\r\n');
|
||
|
|
||
|
var FTP = module.exports = function() {
|
||
|
if (!(this instanceof FTP))
|
||
|
return new FTP();
|
||
|
|
||
|
this._socket = undefined;
|
||
|
this._pasvSock = undefined;
|
||
|
this._feat = undefined;
|
||
|
this._curReq = undefined;
|
||
|
this._queue = [];
|
||
|
this._secstate = undefined;
|
||
|
this._debug = undefined;
|
||
|
this._keepalive = undefined;
|
||
|
this._ending = false;
|
||
|
this._parser = undefined;
|
||
|
this.options = {
|
||
|
host: undefined,
|
||
|
port: undefined,
|
||
|
user: undefined,
|
||
|
password: undefined,
|
||
|
secure: false,
|
||
|
secureOptions: undefined,
|
||
|
connTimeout: undefined,
|
||
|
pasvTimeout: undefined,
|
||
|
aliveTimeout: undefined
|
||
|
};
|
||
|
this.connected = false;
|
||
|
};
|
||
|
inherits(FTP, EventEmitter);
|
||
|
|
||
|
FTP.prototype.connect = function(options) {
|
||
|
var self = this;
|
||
|
if (typeof options !== 'object')
|
||
|
options = {};
|
||
|
this.connected = false;
|
||
|
this.options.host = options.host || 'localhost';
|
||
|
this.options.port = options.port || 21;
|
||
|
this.options.user = options.user || 'anonymous';
|
||
|
this.options.password = options.password || 'anonymous@';
|
||
|
this.options.secure = options.secure || false;
|
||
|
this.options.secureOptions = options.secureOptions;
|
||
|
this.options.connTimeout = options.connTimeout || 10000;
|
||
|
this.options.pasvTimeout = options.pasvTimeout || 10000;
|
||
|
this.options.aliveTimeout = options.keepalive || 10000;
|
||
|
|
||
|
if (typeof options.debug === 'function')
|
||
|
this._debug = options.debug;
|
||
|
|
||
|
var secureOptions,
|
||
|
debug = this._debug,
|
||
|
socket = new Socket();
|
||
|
|
||
|
socket.setTimeout(0);
|
||
|
socket.setKeepAlive(true);
|
||
|
|
||
|
this._parser = new Parser({ debug: debug });
|
||
|
this._parser.on('response', function(code, text) {
|
||
|
var retval = code / 100 >> 0;
|
||
|
if (retval === RETVAL.ERR_TEMP || retval === RETVAL.ERR_PERM) {
|
||
|
if (self._curReq)
|
||
|
self._curReq.cb(makeError(code, text), undefined, code);
|
||
|
else
|
||
|
self.emit('error', makeError(code, text));
|
||
|
} else if (self._curReq)
|
||
|
self._curReq.cb(undefined, text, code);
|
||
|
|
||
|
// a hack to signal we're waiting for a PASV data connection to complete
|
||
|
// first before executing any more queued requests ...
|
||
|
//
|
||
|
// also: don't forget our current request if we're expecting another
|
||
|
// terminating response ....
|
||
|
if (self._curReq && retval !== RETVAL.PRELIM) {
|
||
|
self._curReq = undefined;
|
||
|
self._send();
|
||
|
}
|
||
|
|
||
|
noopreq.cb();
|
||
|
});
|
||
|
|
||
|
if (this.options.secure) {
|
||
|
secureOptions = {};
|
||
|
secureOptions.host = this.options.host;
|
||
|
for (var k in this.options.secureOptions)
|
||
|
secureOptions[k] = this.options.secureOptions[k];
|
||
|
secureOptions.socket = socket;
|
||
|
this.options.secureOptions = secureOptions;
|
||
|
}
|
||
|
|
||
|
if (this.options.secure === 'implicit')
|
||
|
this._socket = tls.connect(secureOptions, onconnect);
|
||
|
else {
|
||
|
socket.once('connect', onconnect);
|
||
|
this._socket = socket;
|
||
|
}
|
||
|
|
||
|
var noopreq = {
|
||
|
cmd: 'NOOP',
|
||
|
cb: function() {
|
||
|
clearTimeout(self._keepalive);
|
||
|
self._keepalive = setTimeout(donoop, self.options.aliveTimeout);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
function donoop() {
|
||
|
if (!self._socket || !self._socket.writable)
|
||
|
clearTimeout(self._keepalive);
|
||
|
else if (!self._curReq && self._queue.length === 0) {
|
||
|
self._curReq = noopreq;
|
||
|
debug&&debug('[connection] > NOOP');
|
||
|
self._socket.write(bytesNOOP);
|
||
|
} else
|
||
|
noopreq.cb();
|
||
|
}
|
||
|
|
||
|
function onconnect() {
|
||
|
clearTimeout(timer);
|
||
|
clearTimeout(self._keepalive);
|
||
|
self.connected = true;
|
||
|
self._socket = socket; // re-assign for implicit secure connections
|
||
|
|
||
|
var cmd;
|
||
|
|
||
|
if (self._secstate) {
|
||
|
if (self._secstate === 'upgraded-tls' && self.options.secure === true) {
|
||
|
cmd = 'PBSZ';
|
||
|
self._send('PBSZ 0', reentry, true);
|
||
|
} else {
|
||
|
cmd = 'USER';
|
||
|
self._send('USER ' + self.options.user, reentry, true);
|
||
|
}
|
||
|
} else {
|
||
|
self._curReq = {
|
||
|
cmd: '',
|
||
|
cb: reentry
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function reentry(err, text, code) {
|
||
|
if (err && (!cmd || cmd === 'USER' || cmd === 'PASS' || cmd === 'TYPE')) {
|
||
|
self.emit('error', err);
|
||
|
return self._socket && self._socket.end();
|
||
|
}
|
||
|
if ((cmd === 'AUTH TLS' && code !== 234 && self.options.secure !== true)
|
||
|
|| (cmd === 'AUTH SSL' && code !== 334)
|
||
|
|| (cmd === 'PBSZ' && code !== 200)
|
||
|
|| (cmd === 'PROT' && code !== 200)) {
|
||
|
self.emit('error', makeError(code, 'Unable to secure connection(s)'));
|
||
|
return self._socket && self._socket.end();
|
||
|
}
|
||
|
|
||
|
if (!cmd) {
|
||
|
// sometimes the initial greeting can contain useful information
|
||
|
// about authorized use, other limits, etc.
|
||
|
self.emit('greeting', text);
|
||
|
|
||
|
if (self.options.secure && self.options.secure !== 'implicit') {
|
||
|
cmd = 'AUTH TLS';
|
||
|
self._send(cmd, reentry, true);
|
||
|
} else {
|
||
|
cmd = 'USER';
|
||
|
self._send('USER ' + self.options.user, reentry, true);
|
||
|
}
|
||
|
} else if (cmd === 'USER') {
|
||
|
if (code !== 230) {
|
||
|
// password required
|
||
|
if (!self.options.password) {
|
||
|
self.emit('error', makeError(code, 'Password required'));
|
||
|
return self._socket && self._socket.end();
|
||
|
}
|
||
|
cmd = 'PASS';
|
||
|
self._send('PASS ' + self.options.password, reentry, true);
|
||
|
} else {
|
||
|
// no password required
|
||
|
cmd = 'PASS';
|
||
|
reentry(undefined, text, code);
|
||
|
}
|
||
|
} else if (cmd === 'PASS') {
|
||
|
cmd = 'FEAT';
|
||
|
self._send(cmd, reentry, true);
|
||
|
} else if (cmd === 'FEAT') {
|
||
|
if (!err)
|
||
|
self._feat = Parser.parseFeat(text);
|
||
|
cmd = 'TYPE';
|
||
|
self._send('TYPE I', reentry, true);
|
||
|
} else if (cmd === 'TYPE')
|
||
|
self.emit('ready');
|
||
|
else if (cmd === 'PBSZ') {
|
||
|
cmd = 'PROT';
|
||
|
self._send('PROT P', reentry, true);
|
||
|
} else if (cmd === 'PROT') {
|
||
|
cmd = 'USER';
|
||
|
self._send('USER ' + self.options.user, reentry, true);
|
||
|
} else if (cmd.substr(0, 4) === 'AUTH') {
|
||
|
if (cmd === 'AUTH TLS' && code !== 234) {
|
||
|
cmd = 'AUTH SSL';
|
||
|
return self._send(cmd, reentry, true);
|
||
|
} else if (cmd === 'AUTH TLS')
|
||
|
self._secstate = 'upgraded-tls';
|
||
|
else if (cmd === 'AUTH SSL')
|
||
|
self._secstate = 'upgraded-ssl';
|
||
|
socket.removeAllListeners('data');
|
||
|
socket.removeAllListeners('error');
|
||
|
socket._decoder = null;
|
||
|
self._curReq = null; // prevent queue from being processed during
|
||
|
// TLS/SSL negotiation
|
||
|
secureOptions.socket = self._socket;
|
||
|
secureOptions.session = undefined;
|
||
|
socket = tls.connect(secureOptions, onconnect);
|
||
|
socket.setEncoding('binary');
|
||
|
socket.on('data', ondata);
|
||
|
socket.once('end', onend);
|
||
|
socket.on('error', onerror);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
socket.on('data', ondata);
|
||
|
function ondata(chunk) {
|
||
|
debug&&debug('[connection] < ' + inspect(chunk.toString('binary')));
|
||
|
if (self._parser)
|
||
|
self._parser.write(chunk);
|
||
|
}
|
||
|
|
||
|
socket.on('error', onerror);
|
||
|
function onerror(err) {
|
||
|
clearTimeout(timer);
|
||
|
clearTimeout(self._keepalive);
|
||
|
self.emit('error', err);
|
||
|
}
|
||
|
|
||
|
socket.once('end', onend);
|
||
|
function onend() {
|
||
|
ondone();
|
||
|
self.emit('end');
|
||
|
}
|
||
|
|
||
|
socket.once('close', function(had_err) {
|
||
|
ondone();
|
||
|
self.emit('close', had_err);
|
||
|
});
|
||
|
|
||
|
var hasReset = false;
|
||
|
function ondone() {
|
||
|
if (!hasReset) {
|
||
|
hasReset = true;
|
||
|
clearTimeout(timer);
|
||
|
self._reset();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var timer = setTimeout(function() {
|
||
|
self.emit('error', new Error('Timeout while connecting to server'));
|
||
|
self._socket && self._socket.destroy();
|
||
|
self._reset();
|
||
|
}, this.options.connTimeout);
|
||
|
|
||
|
this._socket.connect(this.options.port, this.options.host);
|
||
|
};
|
||
|
|
||
|
FTP.prototype.end = function() {
|
||
|
if (this._queue.length)
|
||
|
this._ending = true;
|
||
|
else
|
||
|
this._reset();
|
||
|
};
|
||
|
|
||
|
FTP.prototype.destroy = function() {
|
||
|
this._reset();
|
||
|
};
|
||
|
|
||
|
// "Standard" (RFC 959) commands
|
||
|
FTP.prototype.ascii = function(cb) {
|
||
|
return this._send('TYPE A', cb);
|
||
|
};
|
||
|
|
||
|
FTP.prototype.binary = function(cb) {
|
||
|
return this._send('TYPE I', cb);
|
||
|
};
|
||
|
|
||
|
FTP.prototype.abort = function(immediate, cb) {
|
||
|
if (typeof immediate === 'function') {
|
||
|
cb = immediate;
|
||
|
immediate = true;
|
||
|
}
|
||
|
if (immediate)
|
||
|
this._send('ABOR', cb, true);
|
||
|
else
|
||
|
this._send('ABOR', cb);
|
||
|
};
|
||
|
|
||
|
FTP.prototype.cwd = function(path, cb, promote) {
|
||
|
this._send('CWD ' + path, function(err, text, code) {
|
||
|
if (err)
|
||
|
return cb(err);
|
||
|
var m = RE_WD.exec(text);
|
||
|
cb(undefined, m ? m[1] : undefined);
|
||
|
}, promote);
|
||
|
};
|
||
|
|
||
|
FTP.prototype.delete = function(path, cb) {
|
||
|
this._send('DELE ' + path, cb);
|
||
|
};
|
||
|
|
||
|
FTP.prototype.site = function(cmd, cb) {
|
||
|
this._send('SITE ' + cmd, cb);
|
||
|
};
|
||
|
|
||
|
FTP.prototype.status = function(cb) {
|
||
|
this._send('STAT', cb);
|
||
|
};
|
||
|
|
||
|
FTP.prototype.rename = function(from, to, cb) {
|
||
|
var self = this;
|
||
|
this._send('RNFR ' + from, function(err) {
|
||
|
if (err)
|
||
|
return cb(err);
|
||
|
|
||
|
self._send('RNTO ' + to, cb, true);
|
||
|
});
|
||
|
};
|
||
|
|
||
|
FTP.prototype.logout = function(cb) {
|
||
|
this._send('QUIT', cb);
|
||
|
};
|
||
|
|
||
|
FTP.prototype.listSafe = function(path, zcomp, cb) {
|
||
|
if (typeof path === 'string') {
|
||
|
var self = this;
|
||
|
// store current path
|
||
|
this.pwd(function(err, origpath) {
|
||
|
if (err) return cb(err);
|
||
|
// change to destination path
|
||
|
self.cwd(path, function(err) {
|
||
|
if (err) return cb(err);
|
||
|
// get dir listing
|
||
|
self.list(zcomp || false, function(err, list) {
|
||
|
// change back to original path
|
||
|
if (err) return self.cwd(origpath, cb);
|
||
|
self.cwd(origpath, function(err) {
|
||
|
if (err) return cb(err);
|
||
|
cb(err, list);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
} else
|
||
|
this.list(path, zcomp, cb);
|
||
|
};
|
||
|
|
||
|
FTP.prototype.list = function(path, zcomp, cb) {
|
||
|
var self = this, cmd;
|
||
|
|
||
|
if (typeof path === 'function') {
|
||
|
// list(function() {})
|
||
|
cb = path;
|
||
|
path = undefined;
|
||
|
cmd = 'LIST';
|
||
|
zcomp = false;
|
||
|
} else if (typeof path === 'boolean') {
|
||
|
// list(true, function() {})
|
||
|
cb = zcomp;
|
||
|
zcomp = path;
|
||
|
path = undefined;
|
||
|
cmd = 'LIST';
|
||
|
} else if (typeof zcomp === 'function') {
|
||
|
// list('/foo', function() {})
|
||
|
cb = zcomp;
|
||
|
cmd = 'LIST ' + path;
|
||
|
zcomp = false;
|
||
|
} else
|
||
|
cmd = 'LIST ' + path;
|
||
|
|
||
|
this._pasv(function(err, sock) {
|
||
|
if (err)
|
||
|
return cb(err);
|
||
|
|
||
|
if (self._queue[0] && self._queue[0].cmd === 'ABOR') {
|
||
|
sock.destroy();
|
||
|
return cb();
|
||
|
}
|
||
|
|
||
|
var sockerr, done = false, replies = 0, entries, buffer = '', source = sock;
|
||
|
|
||
|
if (zcomp) {
|
||
|
source = zlib.createInflate();
|
||
|
sock.pipe(source);
|
||
|
}
|
||
|
|
||
|
source.on('data', function(chunk) { buffer += chunk.toString('binary'); });
|
||
|
source.once('error', function(err) {
|
||
|
if (!sock.aborting)
|
||
|
sockerr = err;
|
||
|
});
|
||
|
source.once('end', ondone);
|
||
|
source.once('close', ondone);
|
||
|
|
||
|
function ondone() {
|
||
|
done = true;
|
||
|
final();
|
||
|
}
|
||
|
function final() {
|
||
|
if (done && replies === 2) {
|
||
|
replies = 3;
|
||
|
if (sockerr)
|
||
|
return cb(new Error('Unexpected data connection error: ' + sockerr));
|
||
|
if (sock.aborting)
|
||
|
return cb();
|
||
|
|
||
|
// process received data
|
||
|
entries = buffer.split(RE_EOL);
|
||
|
entries.pop(); // ending EOL
|
||
|
var parsed = [];
|
||
|
for (var i = 0, len = entries.length; i < len; ++i) {
|
||
|
var parsedVal = Parser.parseListEntry(entries[i]);
|
||
|
if (parsedVal !== null)
|
||
|
parsed.push(parsedVal);
|
||
|
}
|
||
|
|
||
|
if (zcomp) {
|
||
|
self._send('MODE S', function() {
|
||
|
cb(undefined, parsed);
|
||
|
}, true);
|
||
|
} else
|
||
|
cb(undefined, parsed);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (zcomp) {
|
||
|
self._send('MODE Z', function(err, text, code) {
|
||
|
if (err) {
|
||
|
sock.destroy();
|
||
|
return cb(makeError(code, 'Compression not supported'));
|
||
|
}
|
||
|
sendList();
|
||
|
}, true);
|
||
|
} else
|
||
|
sendList();
|
||
|
|
||
|
function sendList() {
|
||
|
// this callback will be executed multiple times, the first is when server
|
||
|
// replies with 150 and then a final reply to indicate whether the
|
||
|
// transfer was actually a success or not
|
||
|
self._send(cmd, function(err, text, code) {
|
||
|
if (err) {
|
||
|
sock.destroy();
|
||
|
if (zcomp) {
|
||
|
self._send('MODE S', function() {
|
||
|
cb(err);
|
||
|
}, true);
|
||
|
} else
|
||
|
cb(err);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// some servers may not open a data connection for empty directories
|
||
|
if (++replies === 1 && code === 226) {
|
||
|
replies = 2;
|
||
|
sock.destroy();
|
||
|
final();
|
||
|
} else if (replies === 2)
|
||
|
final();
|
||
|
}, true);
|
||
|
}
|
||
|
});
|
||
|
};
|
||
|
|
||
|
FTP.prototype.get = function(path, zcomp, cb) {
|
||
|
var self = this;
|
||
|
if (typeof zcomp === 'function') {
|
||
|
cb = zcomp;
|
||
|
zcomp = false;
|
||
|
}
|
||
|
|
||
|
this._pasv(function(err, sock) {
|
||
|
if (err)
|
||
|
return cb(err);
|
||
|
|
||
|
if (self._queue[0] && self._queue[0].cmd === 'ABOR') {
|
||
|
sock.destroy();
|
||
|
return cb();
|
||
|
}
|
||
|
|
||
|
// modify behavior of socket events so that we can emit 'error' once for
|
||
|
// either a TCP-level error OR an FTP-level error response that we get when
|
||
|
// the socket is closed (e.g. the server ran out of space).
|
||
|
var sockerr, started = false, lastreply = false, done = false,
|
||
|
source = sock;
|
||
|
|
||
|
if (zcomp) {
|
||
|
source = zlib.createInflate();
|
||
|
sock.pipe(source);
|
||
|
sock._emit = sock.emit;
|
||
|
sock.emit = function(ev, arg1) {
|
||
|
if (ev === 'error') {
|
||
|
if (!sockerr)
|
||
|
sockerr = arg1;
|
||
|
return;
|
||
|
}
|
||
|
sock._emit.apply(sock, Array.prototype.slice.call(arguments));
|
||
|
};
|
||
|
}
|
||
|
|
||
|
source._emit = source.emit;
|
||
|
source.emit = function(ev, arg1) {
|
||
|
if (ev === 'error') {
|
||
|
if (!sockerr)
|
||
|
sockerr = arg1;
|
||
|
return;
|
||
|
} else if (ev === 'end' || ev === 'close') {
|
||
|
if (!done) {
|
||
|
done = true;
|
||
|
ondone();
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
source._emit.apply(source, Array.prototype.slice.call(arguments));
|
||
|
};
|
||
|
|
||
|
function ondone() {
|
||
|
if (done && lastreply) {
|
||
|
self._send('MODE S', function() {
|
||
|
source._emit('end');
|
||
|
source._emit('close');
|
||
|
}, true);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
sock.pause();
|
||
|
|
||
|
if (zcomp) {
|
||
|
self._send('MODE Z', function(err, text, code) {
|
||
|
if (err) {
|
||
|
sock.destroy();
|
||
|
return cb(makeError(code, 'Compression not supported'));
|
||
|
}
|
||
|
sendRetr();
|
||
|
}, true);
|
||
|
} else
|
||
|
sendRetr();
|
||
|
|
||
|
function sendRetr() {
|
||
|
// this callback will be executed multiple times, the first is when server
|
||
|
// replies with 150, then a final reply after the data connection closes
|
||
|
// to indicate whether the transfer was actually a success or not
|
||
|
self._send('RETR ' + path, function(err, text, code) {
|
||
|
if (sockerr || err) {
|
||
|
sock.destroy();
|
||
|
if (!started) {
|
||
|
if (zcomp) {
|
||
|
self._send('MODE S', function() {
|
||
|
cb(sockerr || err);
|
||
|
}, true);
|
||
|
} else
|
||
|
cb(sockerr || err);
|
||
|
} else {
|
||
|
source._emit('error', sockerr || err);
|
||
|
source._emit('close', true);
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
// server returns 125 when data connection is already open; we treat it
|
||
|
// just like a 150
|
||
|
if (code === 150 || code === 125) {
|
||
|
started = true;
|
||
|
cb(undefined, source);
|
||
|
sock.resume();
|
||
|
} else {
|
||
|
lastreply = true;
|
||
|
ondone();
|
||
|
}
|
||
|
}, true);
|
||
|
}
|
||
|
});
|
||
|
};
|
||
|
|
||
|
FTP.prototype.put = function(input, path, zcomp, cb) {
|
||
|
this._store('STOR ' + path, input, zcomp, cb);
|
||
|
};
|
||
|
|
||
|
FTP.prototype.append = function(input, path, zcomp, cb) {
|
||
|
this._store('APPE ' + path, input, zcomp, cb);
|
||
|
};
|
||
|
|
||
|
FTP.prototype.pwd = function(cb) { // PWD is optional
|
||
|
var self = this;
|
||
|
this._send('PWD', function(err, text, code) {
|
||
|
if (code === 502) {
|
||
|
return self.cwd('.', function(cwderr, cwd) {
|
||
|
if (cwderr)
|
||
|
return cb(cwderr);
|
||
|
if (cwd === undefined)
|
||
|
cb(err);
|
||
|
else
|
||
|
cb(undefined, cwd);
|
||
|
}, true);
|
||
|
} else if (err)
|
||
|
return cb(err);
|
||
|
cb(undefined, RE_WD.exec(text)[1]);
|
||
|
});
|
||
|
};
|
||
|
|
||
|
FTP.prototype.cdup = function(cb) { // CDUP is optional
|
||
|
var self = this;
|
||
|
this._send('CDUP', function(err, text, code) {
|
||
|
if (code === 502)
|
||
|
self.cwd('..', cb, true);
|
||
|
else
|
||
|
cb(err);
|
||
|
});
|
||
|
};
|
||
|
|
||
|
FTP.prototype.mkdir = function(path, recursive, cb) { // MKD is optional
|
||
|
if (typeof recursive === 'function') {
|
||
|
cb = recursive;
|
||
|
recursive = false;
|
||
|
}
|
||
|
if (!recursive)
|
||
|
this._send('MKD ' + path, cb);
|
||
|
else {
|
||
|
var self = this, owd, abs, dirs, dirslen, i = -1, searching = true;
|
||
|
|
||
|
abs = (path[0] === '/');
|
||
|
|
||
|
var nextDir = function() {
|
||
|
if (++i === dirslen) {
|
||
|
// return to original working directory
|
||
|
return self._send('CWD ' + owd, cb, true);
|
||
|
}
|
||
|
if (searching) {
|
||
|
self._send('CWD ' + dirs[i], function(err, text, code) {
|
||
|
if (code === 550) {
|
||
|
searching = false;
|
||
|
--i;
|
||
|
} else if (err) {
|
||
|
// return to original working directory
|
||
|
return self._send('CWD ' + owd, function() {
|
||
|
cb(err);
|
||
|
}, true);
|
||
|
}
|
||
|
nextDir();
|
||
|
}, true);
|
||
|
} else {
|
||
|
self._send('MKD ' + dirs[i], function(err, text, code) {
|
||
|
if (err) {
|
||
|
// return to original working directory
|
||
|
return self._send('CWD ' + owd, function() {
|
||
|
cb(err);
|
||
|
}, true);
|
||
|
}
|
||
|
self._send('CWD ' + dirs[i], nextDir, true);
|
||
|
}, true);
|
||
|
}
|
||
|
};
|
||
|
this.pwd(function(err, cwd) {
|
||
|
if (err)
|
||
|
return cb(err);
|
||
|
owd = cwd;
|
||
|
if (abs)
|
||
|
path = path.substr(1);
|
||
|
if (path[path.length - 1] === '/')
|
||
|
path = path.substring(0, path.length - 1);
|
||
|
dirs = path.split('/');
|
||
|
dirslen = dirs.length;
|
||
|
if (abs)
|
||
|
self._send('CWD /', function(err) {
|
||
|
if (err)
|
||
|
return cb(err);
|
||
|
nextDir();
|
||
|
}, true);
|
||
|
else
|
||
|
nextDir();
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
|
||
|
FTP.prototype.rmdir = function(path, recursive, cb) { // RMD is optional
|
||
|
if (typeof recursive === 'function') {
|
||
|
cb = recursive;
|
||
|
recursive = false;
|
||
|
}
|
||
|
if (!recursive) {
|
||
|
return this._send('RMD ' + path, cb);
|
||
|
}
|
||
|
|
||
|
var self = this;
|
||
|
this.list(path, function(err, list) {
|
||
|
if (err) return cb(err);
|
||
|
var idx = 0;
|
||
|
|
||
|
// this function will be called once per listing entry
|
||
|
var deleteNextEntry;
|
||
|
deleteNextEntry = function(err) {
|
||
|
if (err) return cb(err);
|
||
|
if (idx >= list.length) {
|
||
|
if (list[0] && list[0].name === path) {
|
||
|
return cb(null);
|
||
|
} else {
|
||
|
return self.rmdir(path, cb);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var entry = list[idx++];
|
||
|
|
||
|
// get the path to the file
|
||
|
var subpath = null;
|
||
|
if (entry.name[0] === '/') {
|
||
|
// this will be the case when you call deleteRecursively() and pass
|
||
|
// the path to a plain file
|
||
|
subpath = entry.name;
|
||
|
} else {
|
||
|
if (path[path.length - 1] == '/') {
|
||
|
subpath = path + entry.name;
|
||
|
} else {
|
||
|
subpath = path + '/' + entry.name
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// delete the entry (recursively) according to its type
|
||
|
if (entry.type === 'd') {
|
||
|
if (entry.name === "." || entry.name === "..") {
|
||
|
return deleteNextEntry();
|
||
|
}
|
||
|
self.rmdir(subpath, true, deleteNextEntry);
|
||
|
} else {
|
||
|
self.delete(subpath, deleteNextEntry);
|
||
|
}
|
||
|
}
|
||
|
deleteNextEntry();
|
||
|
});
|
||
|
};
|
||
|
|
||
|
FTP.prototype.system = function(cb) { // SYST is optional
|
||
|
this._send('SYST', function(err, text) {
|
||
|
if (err)
|
||
|
return cb(err);
|
||
|
cb(undefined, RE_SYST.exec(text)[1]);
|
||
|
});
|
||
|
};
|
||
|
|
||
|
// "Extended" (RFC 3659) commands
|
||
|
FTP.prototype.size = function(path, cb) {
|
||
|
var self = this;
|
||
|
this._send('SIZE ' + path, function(err, text, code) {
|
||
|
if (code === 502) {
|
||
|
// Note: this may cause a problem as list() is _appended_ to the queue
|
||
|
return self.list(path, function(err, list) {
|
||
|
if (err)
|
||
|
return cb(err);
|
||
|
if (list.length === 1)
|
||
|
cb(undefined, list[0].size);
|
||
|
else {
|
||
|
// path could have been a directory and we got a listing of its
|
||
|
// contents, but here we echo the behavior of the real SIZE and
|
||
|
// return 'File not found' for directories
|
||
|
cb(new Error('File not found'));
|
||
|
}
|
||
|
}, true);
|
||
|
} else if (err)
|
||
|
return cb(err);
|
||
|
cb(undefined, parseInt(text, 10));
|
||
|
});
|
||
|
};
|
||
|
|
||
|
FTP.prototype.lastMod = function(path, cb) {
|
||
|
var self = this;
|
||
|
this._send('MDTM ' + path, function(err, text, code) {
|
||
|
if (code === 502) {
|
||
|
return self.list(path, function(err, list) {
|
||
|
if (err)
|
||
|
return cb(err);
|
||
|
if (list.length === 1)
|
||
|
cb(undefined, list[0].date);
|
||
|
else
|
||
|
cb(new Error('File not found'));
|
||
|
}, true);
|
||
|
} else if (err)
|
||
|
return cb(err);
|
||
|
var val = XRegExp.exec(text, REX_TIMEVAL), ret;
|
||
|
if (!val)
|
||
|
return cb(new Error('Invalid date/time format from server'));
|
||
|
ret = new Date(val.year + '-' + val.month + '-' + val.date + 'T' + val.hour
|
||
|
+ ':' + val.minute + ':' + val.second);
|
||
|
cb(undefined, ret);
|
||
|
});
|
||
|
};
|
||
|
|
||
|
FTP.prototype.restart = function(offset, cb) {
|
||
|
this._send('REST ' + offset, cb);
|
||
|
};
|
||
|
|
||
|
|
||
|
|
||
|
// Private/Internal methods
|
||
|
FTP.prototype._pasv = function(cb) {
|
||
|
var self = this, first = true, ip, port;
|
||
|
this._send('PASV', function reentry(err, text) {
|
||
|
if (err)
|
||
|
return cb(err);
|
||
|
|
||
|
self._curReq = undefined;
|
||
|
|
||
|
if (first) {
|
||
|
var m = RE_PASV.exec(text);
|
||
|
if (!m)
|
||
|
return cb(new Error('Unable to parse PASV server response'));
|
||
|
ip = m[1];
|
||
|
ip += '.';
|
||
|
ip += m[2];
|
||
|
ip += '.';
|
||
|
ip += m[3];
|
||
|
ip += '.';
|
||
|
ip += m[4];
|
||
|
port = (parseInt(m[5], 10) * 256) + parseInt(m[6], 10);
|
||
|
|
||
|
first = false;
|
||
|
}
|
||
|
self._pasvConnect(ip, port, function(err, sock) {
|
||
|
if (err) {
|
||
|
// try the IP of the control connection if the server was somehow
|
||
|
// misconfigured and gave for example a LAN IP instead of WAN IP over
|
||
|
// the Internet
|
||
|
if (self._socket && ip !== self._socket.remoteAddress) {
|
||
|
ip = self._socket.remoteAddress;
|
||
|
return reentry();
|
||
|
}
|
||
|
|
||
|
// automatically abort PASV mode
|
||
|
self._send('ABOR', function() {
|
||
|
cb(err);
|
||
|
self._send();
|
||
|
}, true);
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
cb(undefined, sock);
|
||
|
self._send();
|
||
|
});
|
||
|
});
|
||
|
};
|
||
|
|
||
|
FTP.prototype._pasvConnect = function(ip, port, cb) {
|
||
|
var self = this,
|
||
|
socket = new Socket(),
|
||
|
sockerr,
|
||
|
timedOut = false,
|
||
|
timer = setTimeout(function() {
|
||
|
timedOut = true;
|
||
|
socket.destroy();
|
||
|
cb(new Error('Timed out while making data connection'));
|
||
|
}, this.options.pasvTimeout);
|
||
|
|
||
|
socket.setTimeout(0);
|
||
|
|
||
|
socket.once('connect', function() {
|
||
|
self._debug&&self._debug('[connection] PASV socket connected');
|
||
|
if (self.options.secure === true) {
|
||
|
self.options.secureOptions.socket = socket;
|
||
|
self.options.secureOptions.session = self._socket.getSession();
|
||
|
//socket.removeAllListeners('error');
|
||
|
socket = tls.connect(self.options.secureOptions);
|
||
|
//socket.once('error', onerror);
|
||
|
socket.setTimeout(0);
|
||
|
}
|
||
|
clearTimeout(timer);
|
||
|
self._pasvSocket = socket;
|
||
|
cb(undefined, socket);
|
||
|
});
|
||
|
socket.once('error', onerror);
|
||
|
function onerror(err) {
|
||
|
sockerr = err;
|
||
|
}
|
||
|
socket.once('end', function() {
|
||
|
clearTimeout(timer);
|
||
|
});
|
||
|
socket.once('close', function(had_err) {
|
||
|
clearTimeout(timer);
|
||
|
if (!self._pasvSocket && !timedOut) {
|
||
|
var errmsg = 'Unable to make data connection';
|
||
|
if (sockerr) {
|
||
|
errmsg += '( ' + sockerr + ')';
|
||
|
sockerr = undefined;
|
||
|
}
|
||
|
cb(new Error(errmsg));
|
||
|
}
|
||
|
self._pasvSocket = undefined;
|
||
|
});
|
||
|
|
||
|
socket.connect(port, ip);
|
||
|
};
|
||
|
|
||
|
FTP.prototype._store = function(cmd, input, zcomp, cb) {
|
||
|
var isBuffer = Buffer.isBuffer(input);
|
||
|
|
||
|
if (!isBuffer && input.pause !== undefined)
|
||
|
input.pause();
|
||
|
|
||
|
if (typeof zcomp === 'function') {
|
||
|
cb = zcomp;
|
||
|
zcomp = false;
|
||
|
}
|
||
|
|
||
|
var self = this;
|
||
|
this._pasv(function(err, sock) {
|
||
|
if (err)
|
||
|
return cb(err);
|
||
|
|
||
|
if (self._queue[0] && self._queue[0].cmd === 'ABOR') {
|
||
|
sock.destroy();
|
||
|
return cb();
|
||
|
}
|
||
|
|
||
|
var sockerr, dest = sock;
|
||
|
sock.once('error', function(err) {
|
||
|
sockerr = err;
|
||
|
});
|
||
|
|
||
|
if (zcomp) {
|
||
|
self._send('MODE Z', function(err, text, code) {
|
||
|
if (err) {
|
||
|
sock.destroy();
|
||
|
return cb(makeError(code, 'Compression not supported'));
|
||
|
}
|
||
|
// draft-preston-ftpext-deflate-04 says min of 8 should be supported
|
||
|
dest = zlib.createDeflate({ level: 8 });
|
||
|
dest.pipe(sock);
|
||
|
sendStore();
|
||
|
}, true);
|
||
|
} else
|
||
|
sendStore();
|
||
|
|
||
|
function sendStore() {
|
||
|
// this callback will be executed multiple times, the first is when server
|
||
|
// replies with 150, then a final reply after the data connection closes
|
||
|
// to indicate whether the transfer was actually a success or not
|
||
|
self._send(cmd, function(err, text, code) {
|
||
|
if (sockerr || err) {
|
||
|
if (zcomp) {
|
||
|
self._send('MODE S', function() {
|
||
|
cb(sockerr || err);
|
||
|
}, true);
|
||
|
} else
|
||
|
cb(sockerr || err);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (code === 150 || code === 125) {
|
||
|
if (isBuffer)
|
||
|
dest.end(input);
|
||
|
else if (typeof input === 'string') {
|
||
|
// check if input is a file path or just string data to store
|
||
|
fs.stat(input, function(err, stats) {
|
||
|
if (err)
|
||
|
dest.end(input);
|
||
|
else
|
||
|
fs.createReadStream(input).pipe(dest);
|
||
|
});
|
||
|
} else {
|
||
|
input.pipe(dest);
|
||
|
input.resume();
|
||
|
}
|
||
|
} else {
|
||
|
if (zcomp)
|
||
|
self._send('MODE S', cb, true);
|
||
|
else
|
||
|
cb();
|
||
|
}
|
||
|
}, true);
|
||
|
}
|
||
|
});
|
||
|
};
|
||
|
|
||
|
FTP.prototype._send = function(cmd, cb, promote) {
|
||
|
clearTimeout(this._keepalive);
|
||
|
if (cmd !== undefined) {
|
||
|
if (promote)
|
||
|
this._queue.unshift({ cmd: cmd, cb: cb });
|
||
|
else
|
||
|
this._queue.push({ cmd: cmd, cb: cb });
|
||
|
}
|
||
|
var queueLen = this._queue.length;
|
||
|
if (!this._curReq && queueLen && this._socket && this._socket.readable) {
|
||
|
this._curReq = this._queue.shift();
|
||
|
if (this._curReq.cmd === 'ABOR' && this._pasvSocket)
|
||
|
this._pasvSocket.aborting = true;
|
||
|
this._debug&&this._debug('[connection] > ' + inspect(this._curReq.cmd));
|
||
|
this._socket.write(this._curReq.cmd + '\r\n');
|
||
|
} else if (!this._curReq && !queueLen && this._ending)
|
||
|
this._reset();
|
||
|
};
|
||
|
|
||
|
FTP.prototype._reset = function() {
|
||
|
if (this._pasvSock && this._pasvSock.writable)
|
||
|
this._pasvSock.end();
|
||
|
if (this._socket && this._socket.writable)
|
||
|
this._socket.end();
|
||
|
this._socket = undefined;
|
||
|
this._pasvSock = undefined;
|
||
|
this._feat = undefined;
|
||
|
this._curReq = undefined;
|
||
|
this._secstate = undefined;
|
||
|
clearTimeout(this._keepalive);
|
||
|
this._keepalive = undefined;
|
||
|
this._queue = [];
|
||
|
this._ending = false;
|
||
|
this._parser = undefined;
|
||
|
this.options.host = this.options.port = this.options.user
|
||
|
= this.options.password = this.options.secure
|
||
|
= this.options.connTimeout = this.options.pasvTimeout
|
||
|
= this.options.keepalive = this._debug = undefined;
|
||
|
this.connected = false;
|
||
|
};
|
||
|
|
||
|
// Utility functions
|
||
|
function makeError(code, text) {
|
||
|
var err = new Error(text);
|
||
|
err.code = code;
|
||
|
return err;
|
||
|
}
|