Last active
October 26, 2015 18:52
-
-
Save myndzi/a1bacce5c871764ea55a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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(); | |
}; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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