You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							1071 lines
						
					
					
						
							29 KiB
						
					
					
				
			
		
		
	
	
							1071 lines
						
					
					
						
							29 KiB
						
					
					
				| 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;
 | |
| }
 |