From 09016d835c717991b0e955332a8af3b2184ec114 Mon Sep 17 00:00:00 2001 From: Oleksiy Krivoshey Date: Sat, 13 Feb 2016 23:24:42 +0200 Subject: [PATCH] Better logging --- lib/Constants.js | 7 -- lib/FtpConnection.js | 151 ++++++++++++++++++++++--------------------- lib/FtpServer.js | 35 +++------- lib/logger.js | 133 +++++++++++++++++++++++++++++++++++++ package.json | 3 +- test/cwd-cdup.js | 9 +-- test/init.js | 13 ++-- test/lib/common.js | 44 ------------- test/mkd-rmd.js | 12 ++-- test/tricky-paths.js | 16 +++-- 10 files changed, 254 insertions(+), 169 deletions(-) create mode 100644 lib/logger.js diff --git a/lib/Constants.js b/lib/Constants.js index 9d0f028..dfd4f57 100644 --- a/lib/Constants.js +++ b/lib/Constants.js @@ -21,11 +21,4 @@ module.exports = { RETR: true, STOR: true, }, - LOG_LEVELS: { - ERROR: 0, - WARN: 1, - INFO: 2, - DEBUG: 3, - TRACE: 4, - }, }; diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js index 828e1a8..8051d4b 100644 --- a/lib/FtpConnection.js +++ b/lib/FtpConnection.js @@ -21,7 +21,6 @@ var leftPad = require('./helpers/leftPad'); var EventEmitter = events.EventEmitter; // Use LOG for brevity. -var LOG = Constants.LOG_LEVELS; var DOES_NOT_REQUIRE_AUTH = Constants.DOES_NOT_REQUIRE_AUTH; var REQUIRES_CONFIGURED_DATA = Constants.REQUIRES_CONFIGURED_DATA; @@ -39,19 +38,15 @@ FtpConnection.prototype.respond = function(message, callback) { return this._writeText(this.socket, message + '\r\n', callback); }; -FtpConnection.prototype._logIf = function(verbosity, message) { - return this.server._logIf(verbosity, message, this); -}; - // We don't want to use setEncoding because it screws up TLS, but we // also don't want to explicitly specify ASCII encoding for every call to 'write' // with a string argument. FtpConnection.prototype._writeText = function(socket, data, callback) { if (!socket || !socket.writable) { - this._logIf(LOG.DEBUG, 'Attempted writing to a closed socket:\n>> ' + data.trim()); + this.debug('Attempted writing to a closed socket:\n>> ' + data.trim()); return; } - this._logIf(LOG.TRACE, '>> ' + data.trim()); + this.trace('>> ' + JSON.stringify(data)); return socket.write(data, 'utf8', callback); }; @@ -77,21 +72,21 @@ FtpConnection.prototype._createPassiveServer = function() { return net.createServer(function(psocket) { // This is simply a connection listener. // TODO: Should we keep track of *all* connections, or enforce just one? - self._logIf(LOG.INFO, 'Passive data event: connect'); + self.log('Passive data event: connect'); if (self.secure) { - self._logIf(LOG.INFO, 'Upgrading passive connection to TLS'); + self.log('Upgrading passive connection to TLS'); starttls.starttlsServer(psocket, self.server.options.tlsOptions, function(err, cleartext) { if (err) { - self._logIf(LOG.ERROR, 'Error upgrading passive connection to TLS:' + util.inspect(err)); + self.error('Error upgrading passive connection to TLS:', err); self._closeSocket(psocket, true); self.dataConfigured = false; } else if (!cleartext.authorized) { if (self.server.options.allowUnauthorizedTls) { - self._logIf(LOG.INFO, 'Allowing unauthorized passive connection (allowUnauthorizedTls is on)'); + self.log('Allowing unauthorized passive connection (allowUnauthorizedTls is on)'); switchToSecure(); } else { - self._logIf(LOG.INFO, 'Closing unauthorized passive connection (allowUnauthorizedTls is off)'); + self.log('Closing unauthorized passive connection (allowUnauthorizedTls is off)'); self._closeSocket(self.socket, true); self.dataConfigured = false; } @@ -100,7 +95,7 @@ FtpConnection.prototype._createPassiveServer = function() { } function switchToSecure() { - self._logIf(LOG.INFO, 'Secure passive connection started'); + self.log('Secure passive connection started'); // TODO: Check for existing dataSocket. self.dataSocket = cleartext; setupPassiveListener(); @@ -116,7 +111,7 @@ FtpConnection.prototype._createPassiveServer = function() { if (self.dataListener) { self.dataListener.emit('ready'); } else { - self._logIf(LOG.WARN, 'Passive connection initiated, but no data listener'); + self.warn('Passive connection initiated, but no data listener'); } // Responses are not guaranteed to have an 'end' event @@ -126,16 +121,17 @@ FtpConnection.prototype._createPassiveServer = function() { self.dataSocket.on('end', allOver('end')); function allOver(ename) { return function(err) { - self._logIf( - (err ? LOG.ERROR : LOG.DEBUG), - 'Passive data event: ' + ename + (err ? ' due to error' : '') - ); + if (err) { + self.error('Passive data event:', ename, 'due to error', err); + } else { + self.debug('Passive data event:', ename); + } self.dataSocket = null; }; } self.dataSocket.on('error', function(err) { - self._logIf(LOG.ERROR, 'Passive data event: error: ' + err); + self.error('Passive data event: error:', err); // TODO: Can we can rely on self.dataSocket having been closed? self.dataSocket = null; self.dataConfigured = false; @@ -151,19 +147,19 @@ FtpConnection.prototype._whenDataReady = function(callback) { // how many data connections are allowed? // should still be listening since we created a server, right? if (self.dataSocket) { - self._logIf(LOG.DEBUG, 'A data connection exists'); + self.debug('A data connection exists'); callback(self.dataSocket); } else { - self._logIf(LOG.DEBUG, 'Currently no data connection; expecting client to connect to pasv server shortly...'); + self.debug('Currently no data connection; expecting client to connect to pasv server shortly...'); self.dataListener.once('ready', function() { - self._logIf(LOG.DEBUG, '...client has connected now'); + self.debug('...client has connected now'); callback(self.dataSocket); }); } } else { // Do we need to open the data connection? if (self.dataSocket) { // There really shouldn't be an existing connection - self._logIf(LOG.DEBUG, 'Using existing non-passive dataSocket'); + self.debug('Using existing non-passive dataSocket'); callback(self.dataSocket); } else { self._initiateData(function(sock) { @@ -189,27 +185,28 @@ FtpConnection.prototype._initiateData = function(callback) { sock.on('close', allOver); function allOver(err) { self.dataSocket = null; - self._logIf( - err ? LOG.ERROR : LOG.DEBUG, - 'Non-passive data connection ended' + (err ? 'due to error: ' + util.inspect(err) : '') - ); + if (err) { + self.error('Non-passive data connection ended due to error:', err); + } else { + self.debug('Non-passive data connection ended'); + } } sock.on('error', function(err) { self._closeSocket(sock, true); - self._logIf(LOG.ERROR, 'Data connection error: ' + util.inspect(err)); + self.error('Data connection error:', err); self.dataSocket = null; self.dataConfigured = false; }); }; FtpConnection.prototype._onError = function(err) { - this._logIf(LOG.ERROR, 'Client connection error: ' + util.inspect(err)); + this.error('Client connection error:', err); this._closeSocket(this.socket, true); }; FtpConnection.prototype._onEnd = function() { - this._logIf(LOG.DEBUG, 'Client connection ended'); + this.debug('Client connection ended'); }; FtpConnection.prototype._onClose = function(hadError) { @@ -228,8 +225,8 @@ FtpConnection.prototype._onClose = function(hadError) { this.pasv.close(); this.pasv = null; } - // TODO: LOG.DEBUG? - this._logIf(LOG.INFO, 'Client connection closed'); + // TODO: DEBUG? + this.log('Client connection closed'); }; FtpConnection.prototype._onData = function(data) { @@ -240,11 +237,9 @@ FtpConnection.prototype._onData = function(data) { } data = data.toString('utf-8').trim(); - self._logIf(LOG.TRACE, '<< ' + data); + self.trace('<< ' + JSON.stringify(data)); // Don't want to include passwords in logs. - self._logIf(LOG.INFO, 'FTP command: ' + - data.replace(/^PASS [\s\S]*$/i, 'PASS ***') - ); + self.log('FTP command: ' + data.replace(/^PASS [\s\S]*$/i, 'PASS ***')); var command; var commandArg; @@ -310,18 +305,18 @@ FtpConnection.prototype._command_AUTH = function(commandArg) { } self.respond('234 Honored', function() { - self._logIf(LOG.INFO, 'Establishing secure connection...'); + self.log('Establishing secure connection...'); starttls.starttlsServer(self.socket, self.server.options.tlsOptions, function(err, cleartext) { if (err) { - self._logIf(LOG.ERROR, 'Error upgrading connection to TLS: ' + util.inspect(err)); + self.error('Error upgrading connection to TLS:', err); self._closeSocket(self.socket, true); } else if (!cleartext.authorized) { - self._logIf(LOG.INFO, 'Secure socket not authorized: ' + util.inspect(cleartext.authorizationError)); + self.log('Secure socket not authorized:', util.inspect(cleartext.authorizationError)); if (self.server.options.allowUnauthorizedTls) { - self._logIf(LOG.INFO, 'Allowing unauthorized connection (allowUnauthorizedTls is on)'); + self.log('Allowing unauthorized connection (allowUnauthorizedTls is on)'); switchToSecure(); } else { - self._logIf(LOG.INFO, 'Closing unauthorized connection (allowUnauthorizedTls is off)'); + self.log('Closing unauthorized connection (allowUnauthorizedTls is off)'); self._closeSocket(self.socket, true); } } else { @@ -329,7 +324,7 @@ FtpConnection.prototype._command_AUTH = function(commandArg) { } function switchToSecure() { - self._logIf(LOG.INFO, 'Secure connection started'); + self.log('Secure connection started'); self.socket = cleartext; self.socket.on('data', function(data) { self._onData(data); @@ -356,10 +351,10 @@ FtpConnection.prototype._command_CWD = function(pathRequest) { var pathEscaped = pathEscape(pathServer); this.fs.stat(pathFs, function(err, stats) { if (err) { - this._logIf(LOG.ERROR, 'CWD ' + pathRequest + ': ' + err); + this.error('CWD ' + pathRequest + ':', err); this.respond('550 Directory not found.'); } else if (!stats.isDirectory()) { - this._logIf(LOG.WARN, 'Attempt to CWD to non-directory'); + this.warn('Attempt to CWD to non-directory'); this.respond('550 Not a directory'); } else { this.cwd = pathServer; @@ -375,7 +370,7 @@ FtpConnection.prototype._command_DELE = function(commandArg) { var filename = withCwd(self.cwd, commandArg); self.fs.unlink(pathModule.join(self.root, filename), function(err) { if (err) { - self._logIf(LOG.ERROR, 'Error deleting file: ' + filename + ', ' + err); + self.error('Error deleting file:', filename, err); // write error to socket self.respond('550 Permission denied'); } else { @@ -462,7 +457,7 @@ FtpConnection.prototype._LIST = function(commandArg, detailed, cmd) { glob.setMaxStatsAtOnce(self.server.options.maxStatsAtOnce); glob.glob(pathModule.join(self.root, dir), self.fs, function(err, files) { if (err) { - self._logIf(LOG.ERROR, 'Error sending file list, reading directory: ' + err); + self.error('Error sending file list, reading directory:', err); self.respond('550 Not a directory'); return; } @@ -475,7 +470,7 @@ FtpConnection.prototype._LIST = function(commandArg, detailed, cmd) { }); } - self._logIf(LOG.INFO, 'Directory has ' + files.length + ' files'); + self.log('Directory has ' + files.length + ' files'); if (files.length === 0) { return self._listFiles([], detailed, cmd); } @@ -507,7 +502,7 @@ FtpConnection.prototype._LIST = function(commandArg, detailed, cmd) { self.server.getUsernameFromUid(files[ii].stats.uid, function(e1, uname) { self.server.getGroupFromGid(files[ii].stats.gid, function(e2, gname) { if (e1 || e2) { - self._logIf(LOG.WARN, 'Error getting user/group name for file: ' + util.inspect(e1 || e2)); + self.warn('Error getting user/group name for file:', util.inspect(e1 || e2)); fileInfos.push({ file: files[ii], uname: null, @@ -595,7 +590,7 @@ FtpConnection.prototype._listFiles = function(fileInfos, detailed, cmd) { } } - self._logIf(LOG.DEBUG, 'Sending file list'); + self.debug('Sending file list'); for (var i = 0; i < fileInfos.length; ++i) { var fileInfo = fileInfos[i]; @@ -639,7 +634,7 @@ FtpConnection.prototype._command_MKD = function(pathRequest) { var pathFs = pathModule.join(this.root, pathServer); this.fs.mkdir(pathFs, 0755, function(err) { if (err) { - this._logIf(LOG.ERROR, 'MKD ' + pathRequest + ': ' + err); + this.error('MKD ' + pathRequest + ':', err); this.respond('550 "' + pathEscaped + '" directory NOT created'); } else { this.respond('257 "' + pathEscaped + '" directory created'); @@ -716,7 +711,7 @@ FtpConnection.prototype._PORT = function(commandArg, command) { self.dataConfigured = true; self.dataHost = host; self.dataPort = port; - self._logIf(LOG.DEBUG, 'self.dataHost, self.dataPort set to ' + self.dataHost + ':' + self.dataPort); + self.debug('self.dataHost, self.dataPort set to ' + self.dataHost + ':' + self.dataPort); self.respond('200 OK'); }; @@ -744,10 +739,10 @@ FtpConnection.prototype._PASV = function(commandArg, command) { } if (self.dataListener) { - self._logIf(LOG.DEBUG, 'Telling client that they can connect now'); + self.debug('Telling client that they can connect now'); self._writePASVReady(command); } else { - self._logIf(LOG.DEBUG, 'Setting up listener for passive connections'); + self.debug('Setting up listener for passive connections'); self._setupNewPASV(commandArg, command); } @@ -776,7 +771,7 @@ FtpConnection.prototype._setupNewPASV = function(commandArg, command) { var portRangeErrorHandler; function normalErrorHandler(e) { - self._logIf(LOG.WARN, 'Error with passive data listener: ' + util.inspect(e)); + self.warn('Error with passive data listener:', util.inspect(e)); self.respond('421 Server was unable to open passive connection listener'); self.dataConfigured = false; self.dataListener = null; @@ -795,7 +790,7 @@ FtpConnection.prototype._setupNewPASV = function(commandArg, command) { if (e.code === 'EADDRINUSE' && i < self.server.options.pasvPortRangeEnd) { pasv.listen(++i); } else { - self._logIf(LOG.DEBUG, 'Passing on error from portRangeErrorHandler to normalErrorHandler:' + JSON.stringify(e)); + self.debug('Passing on error from portRangeErrorHandler to normalErrorHandler:' + JSON.stringify(e)); normalErrorHandler(e); } }; @@ -814,17 +809,17 @@ FtpConnection.prototype._setupNewPASV = function(commandArg, command) { pasv.addListener('error', normalErrorHandler); } - self._logIf(LOG.DEBUG, 'Passive data connection beginning to listen'); + self.debug('Passive data connection beginning to listen'); var port = pasv.address().port; self.dataListener = new PassiveListener(); - self._logIf(LOG.DEBUG, 'Passive data connection listening on port ' + port); + self.debug('Passive data connection listening on port ' + port); self._writePASVReady(command); }); pasv.on('close', function() { self.pasv = null; self.dataListener = null; - self._logIf(LOG.DEBUG, 'Passive data listener closed'); + self.debug('Passive data listener closed'); }); }; @@ -887,7 +882,7 @@ FtpConnection.prototype._command_QUIT = function() { self.hasQuit = true; self.respond('221 Goodbye', function(err) { if (err) { - self._logIf(LOG.ERROR, "Error writing 'Goodbye' message following QUIT"); + self.error('Error writing "Goodbye" message following QUIT', err); } self._closeSocket(self.socket, true); self._closeDataConnections(); @@ -935,9 +930,10 @@ FtpConnection.prototype._RETR_usingCreateReadStream = function(commandArg, filen self.respond('550 Not Found'); } else { // Who knows what's going on here... self.respond('550 Not Accessible'); - self._logIf(LOG.ERROR, "Error at read of '" + filename + "' other than ENOENT " + err); + self.error('Error at read of "' + filename + '" other than ENOENT', err); } } else { + self.debug('File opened for reading:', filename); afterOk(function() { self._whenDataReady(function(pasvconn) { var readLength = 0; @@ -945,6 +941,7 @@ FtpConnection.prototype._RETR_usingCreateReadStream = function(commandArg, filen var rs = self.fs.createReadStream(null, {fd: fd}); rs.pause(); rs.once('error', function(err) { + self.error('Failed to read file', filename, err); self.emit('file:retr', 'close', { user: self.username, file: filename, @@ -965,6 +962,7 @@ FtpConnection.prototype._RETR_usingCreateReadStream = function(commandArg, filen rs.on('end', function() { var now = new Date(); + self.debug('Sent', rs.bytesRead, 'of', filename); self.emit('file:retr', 'close', { user: self.username, file: filename, @@ -1019,7 +1017,7 @@ FtpConnection.prototype._RETR_usingReadFile = function(commandArg, filename) { self.respond('550 Not Found'); } else { // Who knows what's going on here... self.respond('550 Not Accessible'); - self._logIf(LOG.ERROR, "Error at read of '" + filename + "' other than ENOENT " + err); + self.error('Error at read of "' + filename + '" other than ENOENT', err); } } else { afterOk(function() { @@ -1054,7 +1052,7 @@ FtpConnection.prototype._command_RMD = function(pathRequest) { var pathFs = pathModule.join(this.root, pathServer); this.fs.rmdir(pathFs, function(err) { if (err) { - this._logIf(LOG.ERROR, 'RMD ' + pathRequest + ': ' + err); + this.error('RMD ' + pathRequest + ':', err); this.respond('550 Delete operation failed'); } else { this.respond('250 "' + pathServer + '" directory removed'); @@ -1066,7 +1064,7 @@ FtpConnection.prototype._command_RMD = function(pathRequest) { FtpConnection.prototype._command_RNFR = function(commandArg) { var self = this; self.filefrom = withCwd(self.cwd, commandArg); - self._logIf(LOG.DEBUG, 'Rename from ' + self.filefrom); + self.debug('Rename from ' + self.filefrom); self.respond('350 Ready for destination name'); }; @@ -1075,7 +1073,7 @@ FtpConnection.prototype._command_RNTO = function(commandArg) { var fileto = withCwd(self.cwd, commandArg); self.fs.rename(pathModule.join(self.root, self.filefrom), pathModule.join(self.root, fileto), function(err) { if (err) { - self._logIf(LOG.ERROR, 'Error renaming file from ' + self.filefrom + ' to ' + fileto); + self.error('Error renaming file from', self.filefrom, 'to', fileto, err); self.respond('550 Rename failed' + (err.code === 'ENOENT' ? '; file does not exist' : '')); } else { self.respond('250 File renamed successfully'); @@ -1089,7 +1087,7 @@ FtpConnection.prototype._command_SIZE = function(commandArg) { var filename = withCwd(self.cwd, commandArg); self.fs.stat(pathModule.join(self.root, filename), function(err, s) { if (err) { - self._logIf(LOG.ERROR, "Error getting size of file '" + filename + "' "); + self.error('Error getting size of file "' + filename + '"', err); self.respond('450 Failed to get size of file'); return; } @@ -1139,8 +1137,8 @@ FtpConnection.prototype._STOR_usingCreateWriteStream = function(filename, initia self._whenDataReady(handleUpload); storeStream.on('open', function() { - self._logIf(LOG.DEBUG, 'File opened/created: ' + filename); - self._logIf(LOG.DEBUG, 'Told client ok to send file data'); + self.debug('File opened/created for writing: ' + filename); + self.debug('Told client ok to send file data'); // Adding event emitter for upload start time self.emit('file:stor', 'open', { user: self.username, @@ -1151,7 +1149,8 @@ FtpConnection.prototype._STOR_usingCreateWriteStream = function(filename, initia self.respond('150 Ok to send data'); }); - storeStream.on('error', function() { + storeStream.on('error', function(err) { + self.error('Error writing file', filename, err); self.emit('file:stor', 'error', { user: self.username, file: filename, @@ -1172,6 +1171,7 @@ FtpConnection.prototype._STOR_usingCreateWriteStream = function(filename, initia }); storeStream.on('finish', function() { + self.debug('Written', storeStream.bytesWritten, 'to', filename); // Adding event emitter for completed upload. self.emit('file:stor', 'close', { user: self.username, @@ -1194,7 +1194,7 @@ FtpConnection.prototype._STOR_usingCreateWriteStream = function(filename, initia dataSocket.pipe(storeStream); dataSocket.on('error', function(err) { notErr = false; - self._logIf(LOG.ERROR, 'Data connection error: ' + util.inspect(err)); + self.error('Data connection error:', err); }); } }; @@ -1240,7 +1240,7 @@ FtpConnection.prototype._STOR_usingWriteFile = function(filename, flag) { // Otherwise, we call _STOR_usingWriteStream, and tell it to prepend the stuff // that we've buffered so far to the file. - self._logIf(LOG.WARN, 'uploadMaxSlurpSize exceeded; falling back to createWriteStream'); + self.warn('uploadMaxSlurpSize exceeded; falling back to createWriteStream'); self._STOR_usingCreateWriteStream(filename, [slurpBuf.slice(0, totalBytes), buf]); self.dataSocket.removeListener('data', dataHandler); self.dataSocket.removeListener('error', errorHandler); @@ -1283,7 +1283,7 @@ FtpConnection.prototype._STOR_usingWriteFile = function(filename, flag) { }); if (err) { erroredOut = true; - self._logIf(LOG.ERROR, 'Error writing file. ' + err); + self.error('Error writing file', filename, err); if (self.dataSocket) { self._closeSocket(self.dataSocket, true); } @@ -1345,7 +1345,7 @@ FtpConnection.prototype._command_PASS = function(password) { self.emit('command:pass', password, function success(username, userFsModule) { function panic(error, method) { - self._logIf(LOG.ERROR, method + ' signaled error ' + util.inspect(error)); + self.error(method + ' signaled error', error); self.respond('421 Service not available, closing control connection.', function() { self._closeSocket(self.socket, true); }); @@ -1408,4 +1408,11 @@ FtpConnection.prototype._closeSocket = function(socket, shouldDestroy) { } }; +['log', 'debug', 'warn', 'error', 'trace'].forEach(function(m) { + FtpConnection.prototype[m] = function() { + var _username = this.username || '-'; + this.server.logger[m].apply(null, [this.remoteAddress, _username].concat(Array.prototype.slice.call(arguments))); + }; +}); + module.exports = FtpConnection; diff --git a/lib/FtpServer.js b/lib/FtpServer.js index 2ff206e..92a96a7 100644 --- a/lib/FtpServer.js +++ b/lib/FtpServer.js @@ -2,13 +2,10 @@ var net = require('net'); var util = require('util'); var events = require('events'); var FtpConnection = require('./FtpConnection'); -var Constants = require('./Constants'); +var Logger = require('./logger'); var EventEmitter = events.EventEmitter; -// Use LOG for brevity. -var LOG = Constants.LOG_LEVELS; - function FtpServer(host, options) { var self = this; EventEmitter.call(self); @@ -36,7 +33,11 @@ function FtpServer(host, options) { self.getGroupFromGid = options.getGroupFromGid || function(gid, c) { c(null, 'ftp'); }; - self.debugging = options.logLevel || 0; + self.logger = Logger({ + logLevel: options.logLevel || 0, + logFunction: options.logFunction, + ttyColors: options.logTtyColors, + }); self.useWriteFile = options.useWriteFile; self.useReadFile = options.useReadFile; self.uploadMaxSlurpSize = options.uploadMaxSlurpSize || 0; @@ -67,6 +68,7 @@ FtpServer.prototype._onConnection = function(socket) { var conn = new FtpConnection({ server: this, socket: socket, + remoteAddress: socket.remoteAddress, // save it for logs for the lifetime of this connection pasv: null, // passive listener server allowedCommands: allowedCommands, // subset of allowed commands for this server dataPort: 20, @@ -97,7 +99,7 @@ FtpServer.prototype._onConnection = function(socket) { socket.setTimeout(0); socket.setNoDelay(); - this._logIf(LOG.INFO, 'Accepted a new client connection'); + this.logger.log('-', '-', 'Accepted a new client connection'); conn.respond('220 FTP server (nodeftpd) ready'); socket.on('data', function(buf) { @@ -121,25 +123,4 @@ FtpServer.prototype._onConnection = function(socket) { }; }); -FtpServer.prototype._logIf = function(verbosity, message, conn) { - if (verbosity > this.debugging) { - return; - } - // TODO: Move this to FtpConnection.prototype._logIf. - var peerAddr = (conn && conn.socket && conn.socket.remoteAddress); - if (peerAddr) { - message = '<' + peerAddr + '> ' + message; - } - if (verbosity === LOG.ERROR) { - message = 'ERROR: ' + message; - } else if (verbosity === LOG.WARN) { - message = 'WARNING: ' + message; - } - console.log(message); - var isError = (verbosity === LOG.ERROR); - if (isError && this.debugging === LOG.TRACE) { - console.trace('Trace follows'); - } -}; - module.exports = FtpServer; diff --git a/lib/logger.js b/lib/logger.js new file mode 100644 index 0000000..1bc4b8d --- /dev/null +++ b/lib/logger.js @@ -0,0 +1,133 @@ +var util = require('util'); +var tty = require('tty'); + +// is it a tty or file? +var isatty = tty.isatty(2) && tty.isatty(1); +var stdout = process.stdout; +var stderr = process.stderr; + +var colors = { + // text style + bold : ['\x1B[1m', '\x1B[22m'], + italic : ['\x1B[3m', '\x1B[23m'], + underline : ['\x1B[4m', '\x1B[24m'], + inverse : ['\x1B[7m', '\x1B[27m'], + strikethrough : ['\x1B[9m', '\x1B[29m'], + // text colors + white : ['\x1B[37m', '\x1B[39m'], + grey : ['\x1B[38;5;240m', '\x1B[39m'], + black : ['\x1B[30m', '\x1B[39m'], + blue : ['\x1B[34m', '\x1B[39m'], + cyan : ['\x1B[36m', '\x1B[39m'], + green : ['\x1B[32m', '\x1B[39m'], + magenta : ['\x1B[35m', '\x1B[39m'], + red : ['\x1B[31m', '\x1B[39m'], + yellow : ['\x1B[33m', '\x1B[39m'], + // background colors + whiteBG : ['\x1B[47m', '\x1B[49m'], + greyBG : ['\x1B[49;5;8m', '\x1B[49m'], + blackBG : ['\x1B[40m', '\x1B[49m'], + blueBG : ['\x1B[44m', '\x1B[49m'], + cyanBG : ['\x1B[46m', '\x1B[49m'], + greenBG : ['\x1B[42m', '\x1B[49m'], + magentaBG : ['\x1B[45m', '\x1B[49m'], + redBG : ['\x1B[41m', '\x1B[49m'], + yellowBG : ['\x1B[43m', '\x1B[49m'], +}; + +var levels = { + DEBUG : 'blue', + TRACE : 'magenta', + INFO : 'green', + WARN : 'yellow', + ERROR : 'red', +}; + +function colored(str, color) { + return colors[color][0] + str + colors[color][1]; +} + +module.exports = function(options) { + options = options || {}; + options.logLevel = options.logLevel !== undefined ? options.logLevel : 255; + options.ttyColors = isatty && (options.ttyColors !== undefined ? options.ttyColors : true); + + options.logLevel = process.env.NODEFTPD_LOG_LEVEL ? parseInt(process.env.NODEFTPD_LOG_LEVEL, 10) : options.logLevel; + + var log = function log(level) { + level = level || 'INFO'; + + var ts = new Date().toISOString(); + var args = Array.prototype.slice.call(arguments, 1); + + if (isatty && options.ttyColors) { + level = colored(level, levels[level]); + ts = colored(ts, 'grey'); + } + + if (typeof args[0] === 'string') { + args[0] = ts + ' ' + level + ' ' + args[0]; + } else { + args = [ts, level].concat(args); + } + + if (level === 'ERROR') { + stderr.write(util.format.apply(null, args) + '\n'); + } else { + stdout.write(util.format.apply(null, args) + '\n'); + } + }; + + if (typeof options.logFunction === 'function') { + log = options.logFunction; + } + + return { + log: function() { + if (options.logLevel > 1) { + log.apply(null, ['INFO'].concat(Array.prototype.slice.call(arguments))); + } + }, + + debug: function() { + if (options.logLevel > 2) { + log.apply(null, ['DEBUG'].concat(Array.prototype.slice.call(arguments))); + } + }, + + trace: function() { + if (options.logLevel > 3) { + log.apply(null, ['TRACE'].concat(Array.prototype.slice.call(arguments))); + } + }, + + warn: function() { + if (options.logLevel > 0) { + log.apply(null, ['WARN'].concat(Array.prototype.slice.call(arguments))); + } + }, + + error: function error() { + // capture error() call location + var stackErr = new Error(); + Error.captureStackTrace(stackErr, error); + var loggedAt = '[' + stackErr.stack.split('\n')[1].trim() + ']'; + + var args = Array.prototype.slice.call(arguments); + + for (var i = 0; i < args.length; i++) { + if (args[i] instanceof Error) { + var err = args[i]; + args[i] = err.toString() + '\n' + util.inspect(err, false, 10, options.ttyColors); + if (err.stack) { + args[i] += '\n' + err.stack.split('\n').splice(1).join('\n'); + } + } + } + + args.push('\n' + loggedAt); + + log.apply(null, ['ERROR'].concat(args)); + }, + }; +}; diff --git a/package.json b/package.json index 2332189..aa98dc0 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,7 @@ "jsftp": "git://github.com/sergi/jsftp.git#master", "mocha": "^2.3.4", "should": "~3.1.2", - "ftp": "^0.3.10" + "ftp": "^0.3.10", + "sinon": "^1.17.3" } } diff --git a/test/cwd-cdup.js b/test/cwd-cdup.js index 3a6d9c0..bc12346 100644 --- a/test/cwd-cdup.js +++ b/test/cwd-cdup.js @@ -1,4 +1,7 @@ var common = require('./lib/common'); +var sinon = require('sinon'); + +var logSpy = sinon.spy(); describe('CWD/CDUP commands', function() { 'use strict'; @@ -20,7 +23,7 @@ describe('CWD/CDUP commands', function() { } beforeEach(function(done) { - server = common.server(); + server = common.server({logFunction: logSpy, logTtyColors: false}); client = common.client(done); }); @@ -41,11 +44,9 @@ describe('CWD/CDUP commands', function() { it('should not change to non-existent directory', function(done) { client.raw('CWD', pathExisting, function(error, response) { response.code.should.equal(250); - server.suppressExpecteErrMsgs.push( - /^CWD \S+: Error: ENOENT/ - ); client.raw('CWD', pathExisting, function(error) { error.code.should.equal(550); + sinon.assert.calledWithMatch(logSpy, 'ERROR', sinon.match.any, sinon.match.any, sinon.match.any, 'ENOENT'); done(); }); }); diff --git a/test/init.js b/test/init.js index 2a20262..36dff5c 100644 --- a/test/init.js +++ b/test/init.js @@ -1,5 +1,8 @@ var common = require('./lib/common'); var Client = require('jsftp'); +var sinon = require('sinon'); + +var logSpy = sinon.spy(); describe('initialization', function() { 'use strict'; @@ -37,15 +40,16 @@ describe('initialization', function() { it('should bail if getRoot fails', function(done) { server = common.server({ + logFunction: logSpy, + logTtyColors: false, getRoot: function(connection, callback) { - server.suppressExpecteErrMsgs.push( - 'getRoot signaled error [Error: intentional failure]'); callback(new Error('intentional failure')); }, }); client = new Client(options); client.auth(options.user, options.pass, function(error) { error.code.should.eql(421); + sinon.assert.calledWithMatch(logSpy, 'ERROR', sinon.match.any, sinon.match.any, sinon.match.any, 'intentional failure'); done(); }); }); @@ -84,15 +88,16 @@ describe('initialization', function() { it('should bail if getInitialCwd fails', function(done) { server = common.server({ + logFunction: logSpy, + logTtyColors: false, getInitialCwd: function(connection, callback) { - server.suppressExpecteErrMsgs.push( - 'getInitialCwd signaled error [Error: intentional failure]'); callback(new Error('intentional failure')); }, }); client = new Client(options); client.auth(options.user, options.pass, function(error) { error.code.should.eql(421); + sinon.assert.calledWithMatch(logSpy, 'ERROR', sinon.match.any, sinon.match.any, sinon.match.any, 'intentional failure'); done(); }); }); diff --git a/test/lib/common.js b/test/lib/common.js index e41847b..ffbf0f0 100644 --- a/test/lib/common.js +++ b/test/lib/common.js @@ -8,24 +8,8 @@ var Client = require('jsftp'); var should = require('should'); var Server = ftpd.FtpServer; -var LogLevels = ftpd.LOG_LEVELS; -var LogLevelNames = Object.keys(LogLevels).reduce(function(map, name) { - var value = LogLevels[name]; - map[value] = name; - return map; -}, {}); - var fixturesPath = path.join(__dirname, '../../fixture'); -function toString(value) { - var isPrimitive = Object(value) !== value; - if (isPrimitive) { - return JSON.stringify(value); - } else { - return ('toString' in value) ? value.toString() : Object.prototype.toString(value); - } -} - var options = { host: process.env.IP || '127.0.0.1', port: process.env.port || 7002, @@ -79,34 +63,6 @@ var common = module.exports = { } }); }); - var origLogIf = server._logIf; - server.suppressExpecteErrMsgs = []; - server._logIf = function logIfNotExpected(verbosity, message, conn) { - var expecteErrMsgs = server.suppressExpecteErrMsgs; - message = String(message).split(fixturesPath).join('fixture:/'); - if ((expecteErrMsgs.length > 0) && (verbosity < LogLevels.LOG_INFO)) { - var expected = expecteErrMsgs.shift(); - if (message === expected) { - return; - } - if ((expected instanceof RegExp) && expected.test(message)) { - return; - } - if ((typeof expected) === 'function') { - message = expected(message); - if (message === '') { - return; - } - } else { - console.error( - '\nExpected log message:\n' + toString(expected) + '\n' + - 'did not match [' + LogLevelNames[verbosity] + ']:\n' + - JSON.stringify(message) - ); - } - } - return origLogIf.call(this, verbosity, message, conn); - }; server.listen(customOptions.port); return server; }, diff --git a/test/mkd-rmd.js b/test/mkd-rmd.js index 0dee870..ce6d2b1 100644 --- a/test/mkd-rmd.js +++ b/test/mkd-rmd.js @@ -1,4 +1,7 @@ var common = require('./lib/common'); +var sinon = require('sinon'); + +var logSpy = sinon.spy(); describe('MKD/RMD commands', function() { 'use strict'; @@ -8,7 +11,7 @@ describe('MKD/RMD commands', function() { var directory = '/testdir'; beforeEach(function(done) { - server = common.server(); + server = common.server({logFunction: logSpy, logTtyColors: false}); client = common.client(done); }); @@ -22,11 +25,9 @@ describe('MKD/RMD commands', function() { }); it('should not create a duplicate directory', function(done) { - server.suppressExpecteErrMsgs.push( - /^MKD \S+: Error: EEXIST/ - ); client.raw('MKD', directory, function(error) { error.code.should.equal(550); + sinon.assert.calledWithMatch(logSpy, 'ERROR', sinon.match.any, sinon.match.any, sinon.match.any, 'EEXIST'); done(); }); }); @@ -42,10 +43,9 @@ describe('MKD/RMD commands', function() { }); it('should not delete a non-existent directory', function(done) { - server.suppressExpecteErrMsgs.push( - /^RMD \S+: Error: ENOENT/); client.raw('RMD', directory, function(error) { error.code.should.equal(550); + sinon.assert.calledWithMatch(logSpy, 'ERROR', sinon.match.any, sinon.match.any, sinon.match.any, 'ENOENT'); done(); }); }); diff --git a/test/tricky-paths.js b/test/tricky-paths.js index aa3d6c4..dff1607 100644 --- a/test/tricky-paths.js +++ b/test/tricky-paths.js @@ -5,6 +5,9 @@ var common = require('./lib/common'); var async = require('async'); var collectStream = require('collect-stream'); +var sinon = require('sinon'); + +var logSpy = sinon.spy(); describe('Tricky paths', function() { var client; @@ -16,7 +19,10 @@ describe('Tricky paths', function() { describe('with useReadFile = ' + useReadFile, function() { beforeEach(function(done) { - server = common.server({useReadFile: useReadFile}); + server = common.server({ + useReadFile: useReadFile, + logFunction: logSpy, + logTtyColors: false}); client = common.client(done); }); @@ -38,12 +44,14 @@ describe('Tricky paths', function() { async.waterfall([ function strangePathRedundantEscape(nxt) { var dirRfcQuoted = dirPath.replace(/"/g, '""'); - server.suppressExpecteErrMsgs.push( - /^CWD [\S\s]+: Error: ENOENT/ - ); client.raw('CWD', dirRfcQuoted, function(error) { common.should.exist(error); error.code.should.equal(550); + sinon.assert.calledWithMatch(logSpy, 'ERROR', + sinon.match.any, + sinon.match.any, + sinon.match.any, + 'ENOENT'); nxt(); }); },