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('^(?\\d{4})(?\\d{2})(?\\d{2})(?\\d{2})(?\\d{2})(?\\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; }