Skip to content

Instantly share code, notes, and snippets.

@myndzi
Last active October 26, 2015 18:52
Show Gist options
  • Save myndzi/a1bacce5c871764ea55a to your computer and use it in GitHub Desktop.
Save myndzi/a1bacce5c871764ea55a to your computer and use it in GitHub Desktop.
'use strict';
var assert = require('assert'),
debug = require('debug')('get-connection'),
FibonacciBackoff = require('simple-backoff').FibonacciBackoff;
module.exports = function (opts) {
opts = opts || { };
assert(opts.hasOwnProperty('net'), 'getConnection: opts.net is required');
assert(opts.hasOwnProperty('port'), 'getConnection: opts.port is required');
assert(opts.hasOwnProperty('host'), 'getConnection: opts.host is required');
assert(opts.hasOwnProperty('retries'), 'getConnection: opts.retries is required');
assert(opts.hasOwnProperty('timeout'), 'getConnection: opts.timeout is required');
var dbgOpts = Object.assign({ }, opts);
dbgOpts.net = (dbgOpts.net.TLSSocket ? 'tls' : 'net');
debug('Initialized getConnection with options:', dbgOpts);
var net = opts.net,
port = opts.port,
host = opts.host,
maxRetries = opts.retries,
connectTimeout = opts.timeout;
var backoff = new FibonacciBackoff(opts.backoff || { });
return function (cb) {
debug('getConnection()');
var tries = 0,
socket = null,
timer = null;
function destroy() {
debug('destroy()');
socket.removeAllListeners();
socket.destroy();
}
function retry(err, msg) {
if (err) { debug('Retrying due to error: %s', err.message); }
tries++;
if (tries > maxRetries) {
cb(new Error('getConnection(): Giving up: ' + msg));
}
setTimeout(connect, backoff.next());
}
function connect() {
debug('connect(%s, %s)', port, host);
socket = net.connect(port, host);
socket.once('error', _onError);
socket.once('connect', _onConnect);
timer = setTimeout(_onTimeout, connectTimeout);
}
function _cleanup() {
debug('cleanup()');
clearTimeout(timer);
socket.removeListener('error', _onError);
socket.removeListener('connect', _onConnect);
}
function _destroy() {
debug('_destroy()');
socket.destroy();
socket = null;
}
function _onError(err) {
debug('_onError(): %s', err.message);
_cleanup();
_destroy();
retry(err, 'exceeded max retries (' + maxRetries + ')');
}
function _onTimeout() {
debug('_onTimeout()');
_cleanup();
_destroy();
retry(null, 'connection timed out (' + connectTimeout +')');
}
function _onConnect() {
debug('_onConnect()');
_cleanup();
backoff.reset();
cb(null, socket);
}
connect();
};
};
'use strict';
const ELIST_TYPE = {
'm': 'mask search',
'n': '!mask search',
'u': 'usercount search (<>)',
'c': 'creation time search (C<C>)',
't': 'topic search (T<T>)'
};
const CHANMODES = 'chanModes';
const EXCEPTS = 'excepts';
const STATUSMSG = 'statusMsg';
const PREFIX = 'prefix';
const TARGMAX = 'targMax';
const MAXLIST = 'maxList';
const KEY_TABLE = {
'PREFIX': PREFIX,
'CHANTYPES': 'chanTypes',
'CHANMODES': CHANMODES,
'MODES': 'modes',
'MAXCHANNELS': 'maxChannels',
'CHANLIMIT': 'chanLimit',
'NICKLEN': 'nickLen',
'MAXBANS': 'maxBans',
'MAXLIST': MAXLIST,
'NETWORK': 'network',
'EXCEPTS': EXCEPTS,
'INVEX': 'invEx', // unused
'WALLCHOPS': 'wallChops', // unused
'WALLVOICES': 'wallVoices', // unused
'STATUSMSG': STATUSMSG,
'CASEMAPPING': 'caseMapping',
'ELIST': 'eList',
'TOPICLEN': 'topicLen',
'KICKLEN': 'kickLen',
'CHANNELLEN': 'chanLen',
'CHILDLEN': 'childLen',
'IDCHAN': 'idChan',
'STD': 'std',
'SILENCE': 'silence',
'RFC2812': 'rfc2812',
'PENALTY': 'penalty',
'FNC': 'fnc',
'SAFELIST': 'safeList',
'AWAYLEN': 'awayLen',
'NOQUIT': 'noQuit',
'USERIP': 'userIp',
'CPRIVMSG': 'cPrivmsg',
'CNOTICE': 'cNotice',
'MAXNICKLEN': 'maxNickLen',
'TARGMAX': TARGMAX,
'MAXTARGETS': 'maxTargets', // unused
'KNOCK': 'knock',
'VCHANS': 'vChans',
'WATCH': 'watch',
'WHOX': 'whoX',
'CALLERID': 'callerId',
'ACCEPT': 'accept',
'LANGUAGE': 'language',
'FLOODPARAMS': 'floodParams' // synthetic
};
function parseTokens(tokens) {
tokens = Object.assign({
'CASEMAPPING': 'rfc1459',
'PREFIX': '(ov)@+',
'CHANMODES': 'b,k,l,psitnm',
'CHANTYPES': '#'
}, tokens);
if (!('MAXLIST' in tokens)) {
var num = tokens.MAXBANS || 100;
tokens.MAXLIST = tokens.CHANMODES.split(',')[0].map(v => v+':'+num);
}
return Object.keys(tokens).reduce((newTokens, key) => {
var newKey = KEY_TABLE[key] || key.toLowerCase();
var value = tokens[key];
switch (key) {
case 'CHANTYPES':
newTokens[newKey] = value.split('')
.reduce((acc, v) => {
acc[v] = true;
return acc;
}, { });
break;
case 'CHANLIMIT':
newTokens[newKey] = value.split(',')
.reduce((acc, v) => {
var split = v.split(':');
acc[split[0]] = +split[1];
return acc;
}, { });
break;
case 'PREFIX':
newTokens[newKey] = {
bySymbol: { },
byMode: { }
};
var matches = value.match(/^\((.*?)\)(.*)$/);
if (matches) {
var letters = matches[1].split(''),
symbols = matches[2].split('');
letters.forEach((v, idx) => {
newTokens.prefix.bySymbol[v] = symbols[idx];
newTokens.prefix.byMode[symbols[idx]] = v;
});
}
break;
case 'STATUSMSG':
newTokens[STATUSMSG] = newTokens[STATUSMSG] || { };
value.split('').forEach(v => {
newTokens[STATUSMSG][v] = true;
});
break;
case 'CHANMODES':
var types = value.split(',');
newTokens[newKey] = newTokens[newKey] || { };
types[0] && types[0].split('').forEach(v => {
newTokens[newKey][v] = { type: 'list', setParam: true, unsetParam: true };
});
types[1] && types[1].split('').forEach(v => {
newTokens[newKey][v] = { type: 'setting', setParam: true, unsetParam: true };
});
types[2] && types[2].split('').forEach(v => {
newTokens[newKey][v] = { type: 'setting', setParam: true, unsetParam: false };
});
types[3] && types[3].split('').forEach(v => {
newTokens[newKey][v] = { type: 'setting', setParam: false, unsetParam: false };
});
break;
case 'MAXLIST':
newTokens[newKey] = value.split(',')
.reduce((acc, v) => {
var split = v.split(':');
acc[split[0]] = +split[1];
return acc;
}, { });
break;
case 'MAXBANS':
newTokens[MAXLIST] = newTokens[MAXLIST] || { };
newTokens[MAXLIST].b = value;
break;
case 'TARGMAX':
newTokens[TARGMAX] = newTokens[TARGMAX] || { };
value.split(',').forEach(v => {
var split = v.split(':');
newTokens[TARGMAX][split[0]] = +split[1] || Infinity;
});
break;
case 'MAXTARGETS':
newTokens[TARGMAX] = newTokens[TARGMAX] || { };
newTokens[TARGMAX].PRIVMSG = +value;
newTokens[TARGMAX].NOTICE = +value;
break;
case 'ELIST':
newTokens[newKey] = value.split('')
.reduce((acc, v) => {
acc[v] = ELIST_TYPE[v];
return acc;
}, { });
break;
case 'EXCEPTS':
newTokens[EXCEPTS] = newTokens[EXCEPTS] || { };
newTokens[EXCEPTS].e = 'b';
newTokens[CHANMODES].b.except = 'e';
break;
case 'INVEX':
newTokens[EXCEPTS] = newTokens[EXCEPTS] || { };
newTokens[EXCEPTS].I = 'i';
newTokens[CHANMODES].i.except = 'I';
break;
case 'WALLCHOPS':
newTokens[STATUSMSG] = newTokens[STATUSMSG] || { };
newTokens[STATUSMSG]['@'] = true;
break;
case 'WALLVOICES':
newTokens[STATUSMSG] = newTokens[STATUSMSG] || { };
newTokens[STATUSMSG]['+'] = true;
break;
default:
newTokens[newKey] = value;
break;
}
newTokens.floodParams = {
lineCost: 2, // +N per line
penaltyFactor: 120, // +1 for each N octets
threshold: 10 // value where throttling begins
};
return newTokens;
}, { });
}
module.exports = parseTokens;
var client = Transport.connect(6667, 'krypt.ca.us.dal.net');
client.on('transport:handshake', done => {
client.write('USER', 'foobot', '*', '*', 'Test bot');
client.write('NICK', 'foobot');
done();
});
client.on('transport:disconnected', () => {
client.ready = false;
client.connect();
});
client.on('transport:error', err => {
client.ready = false;
console.error(err.stack);
client.connect();
});
'use strict';
var net = require('net'),
tls = require('tls'),
util = require('util'),
assert = require('assert'),
debug = require('debug')('transport'),
split = require('binary-split-streams2'),
parse = require('irc-message').parse,
Deque = require('double-ended-queue'),
getConnection = require('./get-connection');
var Duplex = require('stream').Duplex;
var SOCKID = 0;
function Transport(port, host, opts) {
Duplex.call(this, { objectMode: true });
if (typeof port === 'string') {
opts = host;
host = port;
port = null;
}
opts = opts || { };
assert(!!host, "new Transport(): 'host' is required");
var secure = !!opts.secure;
this.host = host;
this.port = port || (secure ? 6697 : 6667);
this.secure = secure;
this.state = 'disconnected';
this.socket = null;
this.stream = null;
this.messages = new Deque();
this.getConnection = getConnection({
net: secure ? tls : net,
host: this.host,
port: this.port,
retries: opts.maxRetries || Infinity,
timeout: opts.connectTimeout || 30*1000
});
}
util.inherits(Transport, Duplex);
Transport.connect = function (port, host, opts) {
var transport = new Transport(port, host, opts);
setImmediate(() => transport.connect());
return transport;
};
Transport.prototype.setState = function (newState) {
debug('setState(%s)', newState);
this.state = newState;
};
Transport.prototype.connect = function () {
if (this.state !== 'disconnected') {
debug('Transport#connect(): already %s', this.state);
return;
}
debug('%s.connect(%s, %s)', this.secure ? 'net' : 'tls', this.port, this.host);
this.setState('connecting');
this.getConnection((err, socket) => {
if (err) {
this._fatalError(err);
return;
}
this._setup(socket);
this.emit('transport:connected');
try {
this.setState('handshaking');
this.emit('transport:handshake', err => {
if (err) { this._fatalError(err); }
else { this.setState('connected'); }
});
} catch(e) {
this._fatalError(e);
}
});
};
Transport.prototype.disconnect = function () {
debug('Transport.disconnect()');
this.setState('disconnected');
this._teardown();
this.emit('transport:disconnected');
};
Transport.prototype.reconnect = function () {
debug('Transport.reconnect()');
this.disconnect();
this.connect();
};
Transport.prototype._fatalError = function (err) {
this.setState('error');
this._teardown();
this.emit('transport:error', err);
};
Transport.prototype._setup = function (socket) {
debug('Transport._setup()');
socket.__id = SOCKID++;
this.socket = socket;
this.socket.on('error', err => this._fatalError(err));
this.socket.on('close', () => this.disconnect());
this.stream = this.socket.pipe(split('\r\n'));
this._read();
};
Transport.prototype._teardown = function () {
debug('Transport._teardown()');
if (this.stream !== null) {
this.stream.unpipe();
this.stream.removeAllListeners();
this.stream = null;
}
if (this.socket !== null) {
this.socket.unpipe();
this.socket.removeAllListeners();
this.socket.destroy();
this.socket = null;
}
};
Transport.prototype._read = function () {
if (!this.stream) { return; }
var chunk = this.stream.read(), str;
if (chunk === null) {
//debug('nothing to read, waiting');
this.stream.once('readable', () => this._read());
return;
}
while (chunk !== null) {
var parsed = parse(chunk.toString());
if (parsed === null) break;
debug('<< %s', parsed.raw);
this.push(parsed);
chunk = this.stream.read();
}
};
Transport.prototype.write = function () {
var i, x, pieces = [ ];
if (arguments.length === 1) {
pieces.push(arguments[0] + '\r\n');
} else {
for (i = 0, x = arguments.length - 1; i <= x; i++) {
pieces.push(arguments[i]);
}
if (pieces[x].indexOf(' ') > -1) {
pieces[x] = ':' + pieces[x];
}
}
var str = pieces.join(' ');
debug('>> %s', str);
Duplex.prototype.write.call(this, str + '\r\n');
};
Transport.prototype._write = function (chunk, encoding, callback) {
//debug('Transport._write()');
this.socket.write(chunk, callback);
};
module.exports = Transport;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment