UNPKG

nitpin

Version:
1,324 lines (1,055 loc) 26.6 kB
var Blast = __Protoblast, ResponseStream = require('./response_stream'), MultilineStream = require('./multiline_stream'), CompressedStream = require('./compressed_stream'), sids = 0, Fn = Blast.Bound.Function; /** * The Nitpin Worker class. * Underscored methods have to be queued yourself, * but they always have a regular method that is queued already. * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.3 */ NitpinWorker = Blast.Collection.Function.inherits('Informer', function NitpinWorker(parent) { var that = this, sockType = require(this.secure ? 'tls' : 'net'), attempts = 0, socket; // Socket id (not related to the parent array) this.id = sids++; // Workers add themselves to the parent parent.sockets.push(this); // Emit an event to let the parent know a new worker has been made parent.emit('worker', this); // Create a reference to the parent this.parent = parent; // Initialize the socket this.socket = socket = new sockType.Socket(); // Create the initial connection makeConnection(); // Are we connected? this.connected = false; // Are we authenticated? this.authenticated = false; // New queue this.comboqueue = Fn.createQueue(); // Socket communication queue this.commqueue = Fn.createQueue(); // Allow maximum 1 running function at a time this.comboqueue.limit = 1; this.commqueue.limit = 1; // The communication queue can start now this.commqueue.start(); // Current group this.currentGroup = null; // Current group info this.groupInfo = {}; // Authenticate that.authenticate(); // Has this been explicitly set as busy? this.explicitBusy = null; // When was the last activity? this.lastActivity = Date.now(); // Server info this.server = { host: parent.host, secure: parent.secure, port: parent.port, user: parent.user, pass: parent.pass }; function makeConnection() { if (attempts > 1) { that.emit('error', new Error('Reconnection attempt ' + attempts)); } socket.connect(parent.port, parent.host); attempts++; } // Listen to timeout messages this.socket.on('timeout', function onTimeout() { that.socket.end(); }); // Listen to error messages this.socket.on('error', function onError(err) { // See if this is an EMFILE error if (err.message.indexOf('EMFILE') > -1) { return setTimeout(makeConnection, 1000); } that.emit('error', err); that.cleanup(); }); // Clean up when the server closes the connection this.socket.on('end', function onEnd(e) { that.cleanup(); }); // Listen to the initial message this.socket.once('data', function initialData(data) { // Initial message received, connection has been made that.connected = true; // Start the queue that.comboqueue.start(); // Emit the connected event that.emit('connected'); }); // Remove this worker after 30 seconds of innactivity this.intervalId = setInterval(function removingWorker() { // Only remove this if it isn't busy and if there are other connected sockets if (!that.busy && parent.sockets.length > 1 && (Date.now() - that.lastActivity) > 30000) { // Submit the QUIT command that.submit('QUIT', function gotQuitResponse(err, response) { // Destroy the actual socket that.socket.destroy(); }); that.cleanup(); } }, 31000); }); /** * Debug method * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.4 * @version 0.1.4 */ NitpinWorker.setMethod(function debug() { if (!this.parent.debugMode) { return false; } return this.parent.debug('__debug__', 'NITPINWORKER', arguments); }); /** * Destroy this worker * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.3 * @version 0.1.3 */ NitpinWorker.setMethod(function destroy() { // Submit the QUIT command this.socket.write('QUIT\r\n'); // Destroy the socket this.socket.destroy(); // Cleanup this.cleanup(); }); /** * Clean up this worker because the connection has been closed * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.0 */ NitpinWorker.setMethod(function cleanup() { var sockid; // Get the id of this socket in the parent's array sockid = this.parent.sockets.indexOf(this); if (sockid > -1) { // Remove it from that array this.parent.sockets.splice(sockid, 1); } // Clear the interval clearInterval(this.intervalId); }); /** * Get server specific info * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.0 * * @param {String} key The key name of the value to get * @param {Mixed} value Optional value when setting */ NitpinWorker.setMethod(function info(key, value) { if (arguments.length == 1) { return this.parent.info(this.server.host, key); } else { return this.parent.info(this.server.host, key, value); } }); /** * Busy property, how busy a worker is (lower is better) * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.0 * * @type {Number} */ NitpinWorker.setProperty(function busy() { var count; // Explicit busy should not be used if (this.explicitBusy) { return 10; } // Count combo's currently running and in the queue count = this.comboqueue.running + this.comboqueue._queue.length; if (count) { return count; } // Count communications currently running and in the queue count = this.commqueue.running + this.commqueue._queue.length; if (count) { return count; } return 0; }, function setBusy(val) { this.explicitBusy = val; }); /** * Hit the activity counter * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.0 */ NitpinWorker.setMethod(function poke() { this.lastActivity = Date.now(); }); /** * Announce this worker if it's free * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.2 */ NitpinWorker.setMethod(function announce() { var that = this; Blast.setImmediate(function doAnnounce() { if (that.comboqueue._queue.length == 0 && that.comboqueue.running == 0) { // Emit an event this worker is no longer busy that.parent.emit('freeworker', that); } }); }); /** * Queue a combination of commands * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.0 * * @param {Function} callback The function to call when done * @param {Function} fnc The function that needs to be run now */ NitpinWorker.setMethod(function queue(callback, fnc) { var that = this; if (typeof fnc !== 'function') { fnc = callback; callback = null; } if (typeof callback !== 'function') { callback = Blast.Bound.Function.thrower; } this.comboqueue.add(function doCommandInQueue(done) { // Execute the given function fnc.call(that, function whenDone(err, response) { // We got a response, call the user's callback callback.apply(that, arguments); // Indicate this queue entry is finished done(); // Announce if it's free that.announce(); }); }); }); /** * Submit a command on the socket, even when not authenticated * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.4 * * @param {String} command Command without newlines * @param {Boolean} multiline Will the response be multiple messages? * @param {Boolean} compressed Will the response be compressed? * @param {Function} callback Where to deliver final response */ NitpinWorker.setMethod(function _submit(command, multiline, compressed, callback) { var that = this, socket = this.socket, pipe = socket; if (typeof multiline == 'function') { callback = multiline; compressed = false; multiline = false; } else if (typeof compressed == 'function') { callback = compressed; compressed = false; } // Use the commqueue so only 1 message-response can happen at a time this.commqueue.add(function doCommand(done) { var response, bomb, len; // Create finish function that handles everything when done function finish(err) { var buf, i; // Defuse the bomb (does nothing when called after explosion) bomb.defuse(); // Remove connected pipes socket.unpipe(); // Remove listeners socket.removeAllListeners('data'); socket.removeAllListeners('error'); // Make sure the newline is removed from the end of the buffer if (response && response.buffer) { buf = response.buffer; len = buf.length; if (buf[len-2] == 13 && buf[len-1] == 10) { buf = buf.slice(0, -2); } // Remove dot stuffing for (i = 2; i < buf.length; i++) { if (buf[i-2] == 13 && buf[i-1] == 10 && buf[i] == 46 && buf[i+1] == 46) { buf = Buffer.concat([buf.slice(0, i), buf.slice(i+1)]); } } // Convert all \r\n to \n if wanted if (that.parent.convertNewline) { for (i = 2; i < buf.length; i++) { if (buf[i-2] == 13 && buf[i-1] == 10) { buf = Buffer.concat([buf.slice(0, i-2), buf.slice(i-1)]); } } } if (response) { response.buffer = buf; } } // Call queue done done(); // If there's an error, callback with that if (err) { // If the error is a timeout, destroy this worker just in case if (err && err.message && err.message.indexOf('Timeout') > -1) { that.destroy(); } callback(err); that.debug('Instance', that.id, 'error:', err); } else { callback(null, response); } } // Make sure finish only gets called one time finish = Fn.regulate(finish); // Create a timebomb: explode when we haven't called back in 1 minute bomb = Fn.timebomb(60000, finish); if (compressed) { pipe = pipe.pipe(new CompressedStream()); pipe.on('error', finish); } if (multiline) { pipe = pipe.pipe(new MultilineStream()); pipe.on('error', finish); } pipe = pipe.pipe(new ResponseStream(multiline)); // Receive response object (same one on each push) pipe.on('data', function gotData(data) { response = data; }); // When the end event has fired, the response is complete pipe.on('end', finish); pipe.on('error', finish); // Reset the activity counter that.poke(); // Write the command to the socket socket.write(command + '\r\n'); }); }); /** * Parse an article * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.1 * * @param {String} article Raw article * @param {Boolean} hasHead * @param {Boolean} hadBody * * @return {Object} */ NitpinWorker.setMethod(function parseArticle(article, hasHead, hasBody) { var headers, lastkey, lines, head, temp, body, id; if (typeof article != 'string') { if (Array.isArray(article)) { article = article.join('\r\n'); } else { if (article.buffer) { article = article.buffer; } if (Buffer.isBuffer(article)) { article = article.toString('binary'); } } } if (hasHead == null) { hasHead = true; } if (hasBody == null) { hasBody = true; } if (hasHead) { // Parsed headers go here headers = {}; id = article.indexOf('\r\n\r\n'); head = article.slice(0, id); body = article.slice(id + 4); lines = head.split('\n'); for (i = 1; i < lines.length; i++) { temp = lines[i]; if (temp.indexOf(':') == -1) { headers[lastkey] += '\n' + temp; continue; } else { temp = temp.split(':'); } lastkey = temp[0].toLowerCase().trim(); headers[lastkey] = temp[1].trim(); } } else { body = article; } return { headers: headers, head: head, body: body }; }); /** * Normalize article id * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.0 * * @param {String} id * * @return {Object} */ NitpinWorker.setMethod(function normalizeArticleId(id) { var error; if (typeof id == 'number') { // Make sure this article number is actually available if (id < this.groupInfo.low || id > this.groupInfo.high) { error = 'This article number is not available'; } } else { if (id[0] !== '<') { id = '<' + id + '>'; } } return { id: id, error: error }; }); /** * Submit a command after we've authenticated * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.0 * * @param {String} command Command without newlines * @param {Boolean} multiline Will the response be multiple messages? * @param {Boolean} compressed Will the response be compressed? * @param {Function} callback Where to deliver final response */ NitpinWorker.setMethod(function submit(command, multiline, compressed, callback) { this.after('authenticated', function authenticated() { this._submit(command, multiline, compressed, callback); }); }); /** * Authenticate * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.4 * * @param {Buffer} data */ NitpinWorker.setMethod(function authenticate(callback) { var that = this; function finish(err, response) { that.announce(); if (callback) callback(err, response); } if (!this.parent.user) { this.emit('authenticated'); this.authenticated = true; return finish(null); } this.queue(callback, function gotQueue(done) { that._submit('AUTHINFO USER ' + that.parent.user, false, false, function gotUserResponse(err, response) { if (err) { return done(err); } if (response.status === 381) { if (that.parent.pass == null) { return done(new Error('A password is required')); } return that._submit('AUTHINFO PASS ' + that.parent.pass, false, false, function gotPassResponse(err, response) { if (err) { return done(err); } that.authenticated = true; done(null); that.emit('authenticated'); }); } that.authenticated = true; done(null, response); that.emit('authenticated'); }); }); }); /** * Stat an article * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.0 * * @param {String} group * @param {String} id * @param {Function} callback */ NitpinWorker.setMethod(function _stat(group, id, callback) { var that = this; Fn.series(function changeGroup(next) { that._group(group, false, next); }, function doStat() { var cmd = 'STAT ', art = that.normalizeArticleId(id); if (art.error) { return callback(new Error(art.error)); } cmd += art.id; that.submit(cmd, false, false, function(err, response) { var temp; if (err) { return callback(err); } temp = response.message.split(' '); callback(null, Number(temp[0]), temp[1]); }); }); }); /** * Stat an article * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.0 */ NitpinWorker.setMethod(function stat(group, id, callback) { this.queue(callback, function(done) { this._stat(group, id, done); }); }); /** * Get the head of an article * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.4 * * @param {Function} callback Where to deliver final response */ NitpinWorker.setMethod(function _getHead(group, id, callback) { var that = this; Fn.series(function changeGroup(next) { that._group(group, false, next); }, function getHead() { var cmd = 'HEAD ', art = that.normalizeArticleId(id); if (art.error) { return callback(new Error(art.error)); } cmd += art.id; that.submit(cmd, true, false, function submittedHeadCmd(err, response) { var temp; if (err) { return callback(err); } temp = that.parseArticle(response, true, false); callback(null, temp.headers, temp.head); }); }); }); /** * Get an article head, queued * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.0 */ NitpinWorker.setMethod(function getHead(group, id, callback) { this.queue(callback, function(done) { this._getHead(group, id, done); }); }); /** * Get the head of an article * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.4 * * @param {Function} callback Where to deliver final response */ NitpinWorker.setMethod(function _getBody(group, id, callback) { var that = this; Fn.series(function changeGroup(next) { that._group(group, false, next); }, function getHead() { var cmd = 'BODY ', art = that.normalizeArticleId(id); if (art.error) { return callback(new Error(art.error)); } cmd += art.id; that.submit(cmd, true, false, function submittedBodyCmd(err, response) { var temp; if (err) { return callback(err); } temp = that.parseArticle(response, false, true); callback(null, temp.body); temp = null; response = null; }); }); }); /** * Get an article body, queued * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.4 */ NitpinWorker.setMethod(function getBody(group, id, callback) { this.queue(callback, function doneGettingBody(done) { this._getBody(group, id, done); }); }); /** * Get an article, unqueued * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.4 */ NitpinWorker.setMethod(function _getArticle(group, id, callback) { var that = this; Fn.series(function changeGroup(next) { that._group(group, false, next); }, function getArticle() { var cmd = 'ARTICLE ', art = that.normalizeArticleId(id); if (art.error) { return callback(new Error(art.error)); } cmd += art.id; that.submit(cmd, true, false, function submittedArticleCmd(err, response) { var temp; if (err) { return callback(err); } temp = that.parseArticle(response); callback(null, temp.headers, temp.body); }); }); }); /** * Get an article, queued * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.4 */ NitpinWorker.setMethod(function getArticle(group, id, callback) { this.queue(callback, function gotArticle(done) { this._getArticle(group, id, done); }); }); /** * Get a list of available newsgroups * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.0 * * @param {String} wildmat Optional wildmat * @param {Boolean} force * @param {Function} callback */ NitpinWorker.setMethod(function _list(wildmat, force, callback) { var that = this, cache, cmd; if (typeof wildmat == 'function') { callback = wildmat; wildmat = false; force = false; } else if (typeof wildmat == 'boolean') { callback = force; force = wildmat; wildmat = false; } else if (typeof wildmat == 'string') { if (typeof force == 'function') { callback = force; force = false; } } cmd = 'LIST ACTIVE'; if (wildmat) { cmd += ' ' + wildmat; } else if (!force) { cache = this.info('activelist'); if (cache) { callback(null, cache); } } this.submit(cmd, true, false, function gotList(err, response) { var result, temp, type, rec, i; if (err) { return callback(err); } result = {}; for (i = 0; i < response.lines.length; i++) { temp = response.lines[i].split(' '); rec = { name: temp[0], high: temp[1], low: temp[2] }; if (temp[3] == 'n') { rec.post = false; rec.moderated = false; } else if (temp[3] == 'y') { rec.post = true; rec.moderated = false; } else if (temp[3] == 'm') { rec.post = true; rec.moderated = true; } result[temp[0]] = rec; } if (!wildmat) { that.info('activelist', result); } callback(null, result); }); }); /** * Get a list of available newsgroups * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.4 */ NitpinWorker.setMethod(function list(wildmat, force, callback) { if (typeof wildmat == 'function') { callback = wildmat; wildmat = ''; force = false; } else if (typeof wildmat == 'boolean') { callback = force; force = wildmat; wildmat = ''; } else if (typeof wildmat == 'string') { if (typeof force == 'function') { callback = force; force = false; } } this.queue(callback, function gotLists(done) { this._list(wildmat, force, done); }); }); /** * Change to the given group, unqueued * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.0 * * @param {String} groupname Groupname to switch to * @param {Boolean} force Force the command, even if already in here * @param {Function} callback */ NitpinWorker.setMethod(function _group(groupname, force, callback) { var that = this; if (typeof force == 'function') { callback = force; force = false; } // Send cached data when we're already in this group if (!force && that.currentGroup == groupname) { return callback(null, this.groupInfo); } this.submit('GROUP ' + groupname, function changedGroup(err, response) { var info, temp; if (err) { return callback(err); } info = {}; temp = response.message.split(' '); // Get available number of articles info.available = Number(temp[0]); // Get lowest number info.low = Number(temp[1]); // Get highest number info.high = Number(temp[2]); // Add groupname info.name = groupname; that.groupInfo = info; that.currentGroup = groupname; callback(null, info); }); }); /** * Change to the given group, queued * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.0 * * @param {String} groupname */ NitpinWorker.setMethod(function group(groupname, force, callback) { var that = this; if (typeof force == 'function') { callback = force; force = false; } this.queue(callback, function(done) { that._group(groupname, force, done); }); }); /** * Get the server capabilities * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.0 * * @param {Boolean} force * @param {Function} callback */ NitpinWorker.setMethod(function _capabilities(force, callback) { var that = this, cap; if (typeof force == 'function') { callback = force; force = false; } cap = this.info('capabilities'); if (!force && cap) { return callback(null, cap); } this.submit('CAPABILITIES', true, false, function gotCapabilities(err, response) { var result, temp, i; if (err) { return callback(err); } result = {}; for (i = 0; i < response.lines.length; i++) { temp = response.lines[i].toLowerCase(); that.info('cap.' + temp, true); result[temp] = true; } that.info('capabilities', result); callback(null, result); }); }); /** * Get the server capabilities * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.0 * * @param {Boolean} force * @param {Function} callback */ NitpinWorker.setMethod(function capabilities(force, callback) { if (typeof force == 'function') { callback = force; force = false; } this.queue(callback, function(done) { this._capabilities(force, done); }); }); /** * Get the List Overview Format * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.0 * * @param {Function} callback */ NitpinWorker.setMethod(function _format(callback) { var that = this, val = this.info('listformat'); if (val) { return callback(null, val); } // Get the format this.submit('LIST OVERVIEW.FMT', true, false, function gotFormat(err, response) { var temp, i; if (err) { return callback(err); } val = [{name: 'id', flag: ''}]; for (i = 0; i < response.lines.length; i++) { temp = response.lines[i]; if (!temp) { continue; } temp = temp.split(':'); if (temp[0]) { val.push({ name: temp[0].toLowerCase(), flag: temp[1].toLowerCase() }); } else { val.push({ name: temp[1].toLowerCase(), flag: 'meta' }); } } that.info('listformat', val); callback(null, val); }); }); /** * Get overview * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.4 * * @param {String} groupname */ NitpinWorker.setMethod(function _over(group, first, last, callback) { var that = this, hasXzver = this.info('cap.xzver'), format, lines, r; Fn.series(function changeGroup(next) { that._group(group, false, next); }, function getFormat(next) { that._format(next); }, function Xzver(next) { // Get the set listformat format = that.info('listformat'); // Try Xzver if (hasXzver == null || hasXzver) { that.submit('XZVER ' + first + '-' + last, true, true, function gotXzverResponse(err, response) { if (err) { // Xzver failed that.info('cap.xzver', false); return next(); } r = response; lines = response.lines; next(); }); } else { next(); } }, function Xover(next) { if (!lines) { that.submit('XOVER ' + first + '-' + last, true, false, function gotXoverResponse(err, response) { if (err) { return next(err); } r = response; lines = response.lines; next(); }); } else { next(); } }, function done(err) { var results, record, field, temp, line, conf, i, j; if (err) { return callback(err); } results = []; for (i = 0; i < lines.length; i++) { line = lines[i].split('\t'); record = {}; for (j = 0; j < line.length; j++) { temp = line[j]; // Get the field format config conf = format[j]; // See if the name is part of the field if (conf.flag == 'full') { temp = Blast.Bound.String.after(temp, ':').trim(); } record[conf.name] = temp; } results.push(record); } callback(null, results); }); }); /** * Get overview * * @author Jelle De Loecker <jelle@kipdola.be> * @since 0.1.0 * @version 0.1.0 * * @param {String} group */ NitpinWorker.setMethod(function over(group, first, last, callback) { var that = this; if (typeof first == 'function') { callback = first; first = 1; last = 10; } else if (typeof last == 'function') { callback = last; last = Number(first) + 10; } this.queue(callback, function gotOverResponse(done) { that._over(group, first, last, done); }); }); module.exports = NitpinWorker;