IrcConnection handling in State instead of Client
[KiwiIRC.git] / server / irc / connection.js
1 var net = require('net'),
2 tls = require('tls'),
3 events = require('events'),
4 util = require('util'),
5 _ = require('lodash');
6
7
8 var IrcConnection = function (hostname, port, ssl, nick, user, pass) {
9 var that = this;
10 events.EventEmitter.call(this);
11
12 // Socket state
13 this.connected = false;
14
15 // If registeration with the IRCd has completed
16 this.registered = false;
17
18 // If we are in the CAP negotiation stage
19 this.cap_negotiation = true;
20
21 // User information
22 this.nick = nick;
23 this.user = user; // Contains users real hostname and address
24 this.username = this.nick.replace(/[^0-9a-zA-Z\-_.]/, '');
25 this.password = pass;
26
27 // IRC connection information
28 this.irc_host = {hostname: hostname, port: port};
29 this.ssl = !(!ssl);
30
31 // Options sent by the IRCd
32 this.options = Object.create(null);
33 this.cap = {requested: [], enabled: []};
34
35 // Is SASL supported on the IRCd
36 this.sasl = false;
37
38 // Buffers for data sent from the IRCd
39 this.hold_last = false;
40 this.held_data = '';
41
42
43 // Call any modules before making the connection
44 global.modules.emit('irc:connecting', {connection: this})
45 .done(function () {
46 that.connect();
47 });
48 };
49 util.inherits(IrcConnection, events.EventEmitter);
50
51 module.exports.IrcConnection = IrcConnection;
52
53
54
55
56 /**
57 * Start the connection to the IRCd
58 */
59 IrcConnection.prototype.connect = function () {
60 var that = this;
61
62 // The socket connect event to listener for
63 var socket_connect_event_name = 'connect';
64
65
66 // Make sure we don't already have an open connection
67 this.disposeSocket();
68
69 // Open either a secure or plain text socket
70 if (this.ssl) {
71 this.socket = tls.connect({
72 host: this.irc_host.hostname,
73 port: this.irc_host.port,
74 rejectUnauthorized: global.config.reject_unauthorised_certificates
75 });
76
77 socket_connect_event_name = 'secureConnect';
78
79 } else {
80 this.socket = net.connect({
81 host: this.irc_host.hostname,
82 port: this.irc_host.port
83 });
84 }
85
86 this.socket.setEncoding('utf-8');
87
88 this.socket.on(socket_connect_event_name, function () {
89 that.connected = true;
90 socketConnectHandler.apply(that, arguments);
91 });
92
93 this.socket.on('error', function (event) {
94 that.emit('error', event);
95
96 });
97
98 this.socket.on('data', function () {
99 parse.apply(that, arguments);
100 });
101
102 this.socket.on('close', function (had_error) {
103 that.connected = false;
104 that.emit('close');
105
106 // Close the whole socket down
107 that.disposeSocket();
108 });
109 };
110
111
112
113 /**
114 * Write a line of data to the IRCd
115 */
116 IrcConnection.prototype.write = function (data, callback) {
117 this.socket.write(data + '\r\n', 'utf-8', callback);
118 };
119
120
121
122 /**
123 * Close the connection to the IRCd after sending one last line
124 */
125 IrcConnection.prototype.end = function (data, callback) {
126 if (data)
127 this.write(data);
128
129 this.socket.end();
130 };
131
132
133
134 /**
135 * Clean up this IrcConnection instance and any sockets
136 */
137 IrcConnection.prototype.dispose = function () {
138 this.disposeSocket();
139 this.removeAllListeners();
140 };
141
142
143
144 /**
145 * Clean up any sockets for this IrcConnection
146 */
147 IrcConnection.prototype.disposeSocket = function () {
148 if (this.socket) {
149 this.socket.removeAllListeners();
150 this.socket = null;
151 }
152 };
153
154
155
156 /**
157 * Handle the socket connect event, starting the IRCd registration
158 */
159 var socketConnectHandler = function () {
160 var that = this,
161 connect_data;
162
163 // Build up data to be used for webirc/etc detection
164 connect_data = {
165 connection: this,
166
167 // Array of lines to be sent to the IRCd before anything else
168 prepend_data: []
169 };
170
171 // Let the webirc/etc detection modify any required parameters
172 connect_data = findWebIrc.call(this, connect_data);
173
174 global.modules.emit('irc:authorize', connect_data).done(function () {
175 // Send any initial data for webirc/etc
176 if (connect_data.prepend_data) {
177 _.each(connect_data.prepend_data, function(data) {
178 that.write(data);
179 });
180 }
181
182 that.write('CAP LS');
183
184 if (that.password)
185 that.write('PASS ' + that.password);
186
187 that.write('NICK ' + that.nick);
188 that.write('USER ' + that.username + ' 0 0 :' + '[www.kiwiirc.com] ' + that.nick);
189
190 that.emit('connected');
191 });
192 };
193
194
195
196 /**
197 * Load any WEBIRC or alternative settings for this connection
198 * Called in scope of the IrcConnection instance
199 */
200 function findWebIrc(connect_data) {
201 var webirc_pass = global.config.webirc_pass,
202 ip_as_username = global.config.ip_as_username,
203 tmp;
204
205
206 // Do we have a WEBIRC password for this?
207 if (webirc_pass && webirc_pass[this.irc_host.hostname]) {
208 // Build the WEBIRC line to be sent before IRC registration
209 tmp = 'WEBIRC ' + webirc_pass[this.irc_host.hostname] + ' KiwiIRC ';
210 tmp += this.user.hostname + ' ' + this.user.address;
211
212 connect_data.prepend_data = [tmp];
213 }
214
215
216 // Check if we need to pass the users IP as its username/ident
217 if (ip_as_username && ip_as_username.indexOf(this.irc_host.hostname) > -1) {
218 // Get a hex value of the clients IP
219 this.username = this.user.address.split('.').map(function(i, idx){
220 return parseInt(i, 10).toString(16);
221 }).join('');
222
223 }
224
225 return connect_data;
226 }
227
228
229
230 /**
231 * The regex that parses a line of data from the IRCd
232 * Deviates from the RFC a little to support the '/' character now used in some
233 * IRCds
234 */
235 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;
236
237 var parse = function (data) {
238 var i,
239 msg,
240 msg2,
241 trm,
242 j,
243 tags = [],
244 tag;
245
246 if (this.hold_last && this.held_data !== '') {
247 data = this.held_data + data;
248 this.hold_last = false;
249 this.held_data = '';
250 }
251
252 // If the last line is incomplete, hold it until we have more data
253 if (data.substr(-1) !== '\n') {
254 this.hold_last = true;
255 }
256
257 // Process our data line by line
258 data = data.split("\n");
259 for (i = 0; i < data.length; i++) {
260 if (!data[i]) break;
261
262 // If flagged to hold the last line, store it and move on
263 if (this.hold_last && (i === data.length - 1)) {
264 this.held_data = data[i];
265 break;
266 }
267
268 // Parse the complete line, removing any carriage returns
269 msg = parse_regex.exec(data[i].replace(/^\r+|\r+$/, ''));
270
271 if (msg) {
272 if (msg[1]) {
273 tags = msg[1].split(';');
274 for (j = 0; j < tags.length; j++) {
275 tag = tags[j].split('=');
276 tags[j] = {tag: tag[0], value: tag[1]};
277 }
278 }
279 msg = {
280 tags: tags,
281 prefix: msg[2],
282 nick: msg[3],
283 ident: msg[4],
284 hostname: msg[5] || '',
285 command: msg[6],
286 params: msg[7] || '',
287 trailing: (msg[8]) ? msg[8].trim() : ''
288 };
289 msg.params = msg.params.split(' ');
290
291 this.emit('irc_' + msg.command.toUpperCase(), msg);
292
293 } else {
294
295 // The line was not parsed correctly, must be malformed
296 console.log("Malformed IRC line: " + data[i].replace(/^\r+|\r+$/, ''));
297 }
298 }
299 };