hexo/node_modules/@icetee/ftp/lib/connection.js

1132 lines
32 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,
StringDecoder = require('string_decoder').StringDecoder;
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_EPSV = /([\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._featUtf8 = false;
this._featEpsv = false;
this._featEprt = false;
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,
forcePasv: 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 ||
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;
this.options.forcePasv = options.forcePasv || false;
if (typeof options.debug === 'function')
this._debug = options.debug;
var secureOptions,
debug = this._debug,
socket = new Socket();
socket.setTimeout(0);
socket.setKeepAlive(options.keepalive > 0);
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.cmd !== 'NOOP') {
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;
}
this._socket = socket;
if (this.options.secure === 'implicit')
socket = tls.connect(secureOptions, onconnect);
else {
socket.once('connect', onconnect);
}
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.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);
self._featUtf8 = self._feat.indexOf('UTF8') > -1; // RFC #2640
self._featEpsv = self._feat.indexOf('EPSV') > -1;
self._featEprt = self._feat.indexOf('EPRT') > -1;
}
if (self._featUtf8) {
// required by MS IIS 7.x FTP implementation which think based on
// http://tools.ietf.org/html/draft-ietf-ftpext-utf-8-option-00
cmd = 'OPTS';
self._send('OPTS UTF8 ON', reentry, true);
} else {
cmd = 'TYPE';
self._send('TYPE I', reentry, true);
}
} else if (cmd === 'OPTS') { // ignore OPTS UTF8 result
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.listCmd = function(path, zcomp, cmd, cb) {
var self = this;
var parse = cmd === 'MLSD' ? Parser.parseMlsdEntry : Parser.parseListEntry;
if (typeof path === 'function') {
// list(function() {})
cb = path;
path = undefined;
zcomp = false;
} else if (typeof path === 'boolean') {
// list(true, function() {})
cb = zcomp;
zcomp = path;
path = undefined;
} else if (typeof zcomp === 'function') {
// list('/foo', function() {})
cb = zcomp;
cmd += ' ' + path;
zcomp = false;
} else
cmd += ' ' + 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;
var decoder = new StringDecoder('utf8');
if (zcomp) {
source = zlib.createInflate();
sock.pipe(source);
}
source.on('data', function(chunk) {
buffer += chunk.toString(self._featUtf8 ? 'utf8' : 'binary');
});
source.once('error', function(err) {
if (!sock.aborting)
sockerr = err;
});
source.once('end', ondone);
source.once('close', ondone);
function ondone() {
if (decoder) {
buffer += decoder.end();
decoder = null;
}
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 = parse(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.list = function(path, zcomp, cb) {
return this.listCmd(path, zcomp, 'LIST', cb);
};
FTP.prototype.mlsd = function(path, zcomp, cb) {
return this.listCmd(path, zcomp, 'MLSD', cb);
};
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);
} 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);
};
// FTP Extensions for IPv6 and NATs
// https://tools.ietf.org/html/rfc2428
// Must run EPSV before running [ IPv6 passive mod ]
FTP.prototype._eprt = function(response, cb) {
var tcpPrt = RE_EPSV.exec(response)[0],
netPrt = (this._socket.remoteFamily === 'IPv4') ? 1 : 2,
eprtParam = [netPrt, this._socket.remoteAddress, tcpPrt].join('|');
self._send('EPRT |' + eprtParam + '|', function(eprtErr, eprtText) {
if (eprtErr) return cb(eprtErr);
console.log(eprtText);
});
};
// Private/Internal methods
FTP.prototype._pasv = function(cb) {
var self = this, first = true, ip, port;
var pasvCmd = (self._featEpsv && !this.options.forcePasv) ? 'EPSV' : 'PASV';
this._send(pasvCmd, function(err, text) {
if (err)
return cb(err);
self._curReq = undefined;
if (first) {
if (pasvCmd === 'EPSV') {
var tcpPrt = RE_EPSV.exec(text)[0];
ip = self._socket.remoteAddress;
port = tcpPrt;
} else {
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;
}