X-Git-Url: https://vcs.fsf.org/?a=blobdiff_plain;f=server%2Firc%2Fconnection.js;h=a61e2c4319bd9cd2803cfcb14a2001619b876780;hb=441f18aa71c9f6c424deb07dc44a7b0589af42a3;hp=e7a4895a6621311b80d542ffffc4bdd815dba89c;hpb=36108ca96210e3baea115facc5a5812b82948aa5;p=KiwiIRC.git diff --git a/server/irc/connection.js b/server/irc/connection.js index e7a4895..a61e2c4 100644 --- a/server/irc/connection.js +++ b/server/irc/connection.js @@ -1,135 +1,333 @@ -var net = require('net'), - tls = require('tls'), - events = require('events'), - util = require('util'), - _ = require('underscore'); +var net = require('net'), + tls = require('tls'), + util = require('util'), + _ = require('lodash'), + EventEmitter2 = require('eventemitter2').EventEmitter2, + EventBinder = require('./eventbinder.js'), + IrcServer = require('./server.js'), + IrcChannel = require('./channel.js'), + IrcUser = require('./user.js'); -var IrcConnection = function (hostname, port, ssl, nick, user, pass) { + +var IrcConnection = function (hostname, port, ssl, nick, user, pass, state) { var that = this; - events.EventEmitter.call(this); + + EventEmitter2.call(this,{ + wildcard: true, + delimiter: ':' + }); + this.setMaxListeners(0); + + // Socket state + this.connected = false; + + // If registeration with the IRCd has completed + this.registered = false; + + // If we are in the CAP negotiation stage + this.cap_negotiation = true; + + // User information + this.nick = nick; + this.user = user; // Contains users real hostname and address + this.username = this.nick.replace(/[^0-9a-zA-Z\-_.]/, ''); + this.password = pass; + + // State object + this.state = state; + + // IrcServer object + this.server = new IrcServer(this, hostname, port); + + // IrcUser objects + this.irc_users = Object.create(null); + + // IrcChannel objects + this.irc_channels = Object.create(null); + + // IRC connection information + this.irc_host = {hostname: hostname, port: port}; + this.ssl = !(!ssl); + + // Options sent by the IRCd + this.options = Object.create(null); + this.cap = {requested: [], enabled: []}; + + // Is SASL supported on the IRCd + this.sasl = false; - if (ssl) { + // Buffers for data sent from the IRCd + this.hold_last = false; + this.held_data = ''; + + this.applyIrcEvents(); + + // Call any modules before making the connection + global.modules.emit('irc:connecting', {connection: this}) + .done(function () { + that.connect(); + }); +}; +util.inherits(IrcConnection, EventEmitter2); + +module.exports.IrcConnection = IrcConnection; + + + +IrcConnection.prototype.applyIrcEvents = function () { + // Listen for events on the IRC connection + this.irc_events = { + 'server:*:connect': onServerConnect, + 'channel:*:join': onChannelJoin, + + // TODO: uncomment when using an IrcUser per nick + //'user:*:privmsg': onUserPrivmsg, + 'channel:*:part': onUserParts, + 'channel:*:quit': onUserParts, + 'channel:*:kick': onUserParts + }; + + EventBinder.bindIrcEvents('', this.irc_events, this, this); +}; + + +/** + * Start the connection to the IRCd + */ +IrcConnection.prototype.connect = function () { + var that = this; + + // The socket connect event to listener for + var socket_connect_event_name = 'connect'; + + + // Make sure we don't already have an open connection + this.disposeSocket(); + + // Open either a secure or plain text socket + if (this.ssl) { this.socket = tls.connect({ - host: hostname, - port: port, + host: this.irc_host.hostname, + port: this.irc_host.port, rejectUnauthorized: global.config.reject_unauthorised_certificates - }, function () { - connect_handler.apply(that, arguments); }); + + socket_connect_event_name = 'secureConnect'; + } else { - this.socket = net.createConnection(port, hostname); - this.socket.on('connect', function () { - connect_handler.apply(that, arguments); + this.socket = net.connect({ + host: this.irc_host.hostname, + port: this.irc_host.port }); } - + + this.socket.setEncoding('utf-8'); + + this.socket.on(socket_connect_event_name, function () { + that.connected = true; + socketConnectHandler.apply(that, arguments); + }); + this.socket.on('error', function (event) { that.emit('error', event); + }); - this.socket.setEncoding('utf-8'); - this.socket.on('data', function () { parse.apply(that, arguments); }); - this.socket.on('close', function () { + this.socket.on('close', function (had_error) { + that.connected = false; that.emit('close'); + + // Close the whole socket down + that.disposeSocket(); }); - - this.connected = false; - this.registered = false; - this.nick = nick; - this.user = user; - this.irc_host = {hostname: hostname, port: port}; - this.ssl = !(!ssl); - this.options = Object.create(null); - - this.password = pass; - this.hold_last = false; - this.held_data = ''; }; -util.inherits(IrcConnection, events.EventEmitter); - -module.exports.IrcConnection = IrcConnection; +/** + * Send an event to the client + */ +IrcConnection.prototype.clientEvent = function (event_name, data, callback) { + data.server = this.con_num; + this.state.sendIrcCommand(event_name, data, callback); +}; +/** + * Write a line of data to the IRCd + */ IrcConnection.prototype.write = function (data, callback) { - write.call(this, data + '\r\n', 'utf-8', callback); + this.socket.write(data + '\r\n', 'utf-8', callback); }; + + +/** + * Close the connection to the IRCd after sending one last line + */ IrcConnection.prototype.end = function (data, callback) { - end.call(this, data + '\r\n', 'utf-8', callback); + if (data) + this.write(data); + + this.socket.end(); }; + + +/** + * Clean up this IrcConnection instance and any sockets + */ IrcConnection.prototype.dispose = function () { + _.each(this.irc_users, function (user) { + user.dispose(); + }); + _.each(this.irc_channels, function (chan) { + chan.dispose(); + }); + this.irc_users = null; + this.irc_channels = null; + + EventBinder.unbindIrcEvents('', this.irc_events, this); + + this.disposeSocket(); this.removeAllListeners(); }; -var write = function (data, encoding, callback) { - this.socket.write(data, encoding, callback); -}; -var end = function (data, encoding, callback) { - this.socket.end(data, encoding, callback); +/** + * Clean up any sockets for this IrcConnection + */ +IrcConnection.prototype.disposeSocket = function () { + if (this.socket) { + this.socket.removeAllListeners(); + this.socket = null; + } }; -var connect_handler = function () { + +function onChannelJoin(event) { + var chan; + + // Only deal with ourselves joining a channel + if (event.nick !== this.nick) + return; + + // We should only ever get a JOIN command for a channel + // we're not already a member of.. but check we don't + // have this channel in case something went wrong somewhere + // at an earlier point + if (!this.irc_channels[event.channel]) { + chan = new IrcChannel(this, event.channel); + this.irc_channels[event.channel] = chan; + chan.irc_events.join.call(chan, event); + } +} + + +function onServerConnect(event) { + this.nick = event.nick; + + // TODO: use `event.nick` instead of `'*'` when using an IrcUser per nick + this.irc_users[event.nick] = new IrcUser(this, '*'); +} + + +function onUserPrivmsg(event) { + var user; + + // Only deal with messages targetted to us + if (event.channel !== this.nick) + return; + + if (!this.irc_users[event.nick]) { + user = new IrcUser(this, event.nick); + this.irc_users[event.nick] = user; + user.irc_events.privmsg.call(user, event); + } +} + + +function onUserParts(event) { + // Only deal with ourselves leaving a channel + if (event.nick !== this.nick) + return; + + if (this.irc_channels[event.channel]) { + this.irc_channels[event.channel].dispose(); + delete this.irc_channels[event.channel]; + } +} + + + + +/** + * Handle the socket connect event, starting the IRCd registration + */ +var socketConnectHandler = function () { var that = this, connect_data; // Build up data to be used for webirc/etc detection connect_data = { - user: this.user, - nick: this.nick, - realname: '[www.kiwiirc.com] ' + this.nick, - username: this.nick.replace(/[^0-9a-zA-Z\-_.]/, ''), - irc_host: this.irc_host + connection: this, + + // Array of lines to be sent to the IRCd before anything else + prepend_data: [] }; // Let the webirc/etc detection modify any required parameters - connect_data = findWebIrc(connect_data); + connect_data = findWebIrc.call(this, connect_data); - // Send any initial data for webirc/etc - if (connect_data.prepend_data) { - _.each(connect_data.prepend_data, function(data) { - that.write(data); - }); - } + global.modules.emit('irc:authorize', connect_data).done(function () { + // Send any initial data for webirc/etc + if (connect_data.prepend_data) { + _.each(connect_data.prepend_data, function(data) { + that.write(data); + }); + } - if (this.password) { - this.write('PASS ' + this.password); - } - - this.write('NICK ' + connect_data.nick); - this.write('USER ' + connect_data.username + ' 0 0 :' + connect_data.realname); - - this.connected = true; - this.emit('connected'); -}; + that.write('CAP LS'); + if (that.password) + that.write('PASS ' + that.password); + + that.write('NICK ' + that.nick); + that.write('USER ' + that.username + ' 0 0 :' + '[www.kiwiirc.com] ' + that.nick); + + that.emit('connected'); + }); +}; +/** + * Load any WEBIRC or alternative settings for this connection + * Called in scope of the IrcConnection instance + */ function findWebIrc(connect_data) { - var webirc_pass = global.config.webirc_pass; - var ip_as_username = global.config.ip_as_username; - var tmp; + var webirc_pass = global.config.webirc_pass, + ip_as_username = global.config.ip_as_username, + tmp; + // Do we have a WEBIRC password for this? - if (webirc_pass && webirc_pass[connect_data.irc_host.hostname]) { - tmp = 'WEBIRC ' + webirc_pass[connect_data.irc_host.hostname] + ' KiwiIRC '; - tmp += connect_data.user.hostname + ' ' + connect_data.user.address; + if (webirc_pass && webirc_pass[this.irc_host.hostname]) { + // Build the WEBIRC line to be sent before IRC registration + tmp = 'WEBIRC ' + webirc_pass[this.irc_host.hostname] + ' KiwiIRC '; + tmp += this.user.hostname + ' ' + this.user.address; + connect_data.prepend_data = [tmp]; } // Check if we need to pass the users IP as its username/ident - if (ip_as_username && ip_as_username.indexOf(connect_data.irc_host.hostname) > -1) { + if (ip_as_username && ip_as_username.indexOf(this.irc_host.hostname) > -1) { // Get a hex value of the clients IP - connect_data.username = connect_data.user.address.split('.').map(function(i, idx){ + this.username = this.user.address.split('.').map(function(i, idx){ return parseInt(i, 10).toString(16); }).join(''); @@ -140,47 +338,73 @@ function findWebIrc(connect_data) { -parse_regex = /^(?::(?:([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)|([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)!([a-z0-9~\.\-_|]+)@?([a-z0-9\.\-:\/]+)?) )?(\S+)(?: (?!:)(.+?))?(?: :(.+))?$/i; +/** + * The regex that parses a line of data from the IRCd + * Deviates from the RFC a little to support the '/' character now used in some + * IRCds + */ +var parse_regex = /^(?:(?:(?:(@[^ ]+) )?):(?:([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)|([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)!([a-z0-9~\.\-_|]+)@?([a-z0-9\.\-:\/_]+)?) )?(\S+)(?: (?!:)(.+?))?(?: :(.+))?$/i; + var parse = function (data) { var i, msg, - msg2, - trm; + msg2, + trm, + j, + tags = [], + tag; - if ((this.hold_last) && (this.held_data !== '')) { + if (this.hold_last && this.held_data !== '') { data = this.held_data + data; this.hold_last = false; this.held_data = ''; } + + // If the last line is incomplete, hold it until we have more data if (data.substr(-1) !== '\n') { this.hold_last = true; } + + // Process our data line by line data = data.split("\n"); for (i = 0; i < data.length; i++) { - if (data[i]) { - if ((this.hold_last) && (i === data.length - 1)) { - this.held_data = data[i]; - break; - } + if (!data[i]) break; + + // If flagged to hold the last line, store it and move on + if (this.hold_last && (i === data.length - 1)) { + this.held_data = data[i]; + break; + } + + // Parse the complete line, removing any carriage returns + msg = parse_regex.exec(data[i].replace(/^\r+|\r+$/, '')); - // We have a complete line of data, parse it! - msg = parse_regex.exec(data[i].replace(/^\r+|\r+$/, '')); - if (msg) { - msg = { - prefix: msg[1], - nick: msg[2], - ident: msg[3], - hostname: msg[4] || '', - command: msg[5], - params: msg[6] || '', - trailing: (msg[7]) ? msg[7].trim() : '' - }; - msg.params = msg.params.split(' '); - - this.emit('irc_' + msg.command.toUpperCase(), msg); - } else { - console.log("Malformed IRC line: " + data[i].replace(/^\r+|\r+$/, '')); + if (msg) { + if (msg[1]) { + tags = msg[1].split(';'); + for (j = 0; j < tags.length; j++) { + tag = tags[j].split('='); + tags[j] = {tag: tag[0], value: tag[1]}; + } } + msg = { + tags: tags, + prefix: msg[2], + nick: msg[3], + ident: msg[4], + hostname: msg[5] || '', + command: msg[6], + params: msg[7] || '', + trailing: (msg[8]) ? msg[8].trim() : '' + }; + msg.params = msg.params.split(' '); + + this.emit('irc_' + msg.command.toUpperCase(), msg); + + } else { + + // The line was not parsed correctly, must be malformed + console.log("Malformed IRC line: " + data[i].replace(/^\r+|\r+$/, '')); } } };