Commit | Line | Data |
---|---|---|
80c584e7 | 1 | /*jslint sloppy: true, continue: true, forin: true, regexp: true, undef: false, node: true, nomen: true, plusplus: true, maxerr: 50, indent: 4 */ |
2fc64ec2 | 2 | /*globals kiwi_root */ |
f236196a | 3 | /* Fuck you, git. */ |
fd779420 D |
4 | var tls = null; |
5 | var net = null; | |
6 | var http = null; | |
7 | var https = null; | |
8 | var fs = null; | |
9 | var url = null; | |
10 | var dns = null; | |
11 | var crypto = null; | |
12 | var ws = null; | |
13 | var jsp = null; | |
14 | var pro = null; | |
15 | var _ = null; | |
16 | var starttls = null; | |
17 | var kiwi = null; | |
18 | ||
19 | this.init = function (objs) { | |
1fce4b40 JA |
20 | tls = objs.tls; |
21 | net = objs.net; | |
22 | http = objs.http; | |
23 | https = objs.https; | |
24 | fs = objs.fs; | |
25 | url = objs.url; | |
26 | dns = objs.dns; | |
27 | crypto = objs.crypto; | |
28 | ws = objs.ws; | |
29 | jsp = objs.jsp; | |
30 | pro = objs.pro; | |
31 | _ = objs._; | |
32 | starttls = objs.starttls; | |
33 | kiwi = require('./kiwi.js'); | |
2fc64ec2 | 34 | }; |
fd779420 D |
35 | |
36 | ||
37 | ||
38 | ||
39 | ||
40 | ||
41 | /* | |
42 | * Some process changes | |
43 | */ | |
44 | this.setTitle = function () { | |
1fce4b40 | 45 | process.title = 'kiwiirc'; |
2fc64ec2 | 46 | }; |
fd779420 D |
47 | |
48 | this.changeUser = function () { | |
49 | if (typeof kiwi.config.group !== 'undefined' && kiwi.config.group !== '') { | |
50 | try { | |
51 | process.setgid(kiwi.config.group); | |
52 | } catch (err) { | |
53 | console.log('Failed to set gid: ' + err); | |
54 | process.exit(); | |
55 | } | |
56 | } | |
57 | ||
58 | if (typeof kiwi.config.user !== 'undefined' && kiwi.config.user !== '') { | |
59 | try { | |
60 | process.setuid(kiwi.config.user); | |
61 | } catch (e) { | |
62 | console.log('Failed to set uid: ' + e); | |
63 | process.exit(); | |
64 | } | |
65 | } | |
66 | }; | |
67 | ||
68 | ||
69 | ||
70 | ||
71 | ||
72 | ||
73 | ||
74 | ||
75 | ||
76 | var ircNumerics = { | |
77 | RPL_WELCOME: '001', | |
337c9866 | 78 | RPL_MYINFO: '004', |
fd779420 D |
79 | RPL_ISUPPORT: '005', |
80 | RPL_WHOISUSER: '311', | |
81 | RPL_WHOISSERVER: '312', | |
82 | RPL_WHOISOPERATOR: '313', | |
83 | RPL_WHOISIDLE: '317', | |
84 | RPL_ENDOFWHOIS: '318', | |
85 | RPL_WHOISCHANNELS: '319', | |
b10afa2d D |
86 | RPL_LISTSTART: '321', |
87 | RPL_LIST: '322', | |
88 | RPL_LISTEND: '323', | |
fd779420 D |
89 | RPL_NOTOPIC: '331', |
90 | RPL_TOPIC: '332', | |
91 | RPL_NAMEREPLY: '353', | |
92 | RPL_ENDOFNAMES: '366', | |
93 | RPL_MOTD: '372', | |
94 | RPL_WHOISMODES: '379', | |
95 | ERR_NOSUCHNICK: '401', | |
96 | ERR_CANNOTSENDTOCHAN: '404', | |
97 | ERR_TOOMANYCHANNELS: '405', | |
98 | ERR_NICKNAMEINUSE: '433', | |
99 | ERR_USERNOTINCHANNEL: '441', | |
100 | ERR_NOTONCHANNEL: '442', | |
101 | ERR_NOTREGISTERED: '451', | |
102 | ERR_LINKCHANNEL: '470', | |
103 | ERR_CHANNELISFULL: '471', | |
104 | ERR_INVITEONLYCHAN: '473', | |
105 | ERR_BANNEDFROMCHAN: '474', | |
106 | ERR_BADCHANNELKEY: '475', | |
107 | ERR_CHANOPRIVSNEEDED: '482', | |
108 | RPL_STARTTLS: '670' | |
109 | }; | |
110 | ||
111 | ||
112 | ||
113 | this.parseIRCMessage = function (websocket, ircSocket, data) { | |
114 | /*global ircSocketDataHandler */ | |
8343584e | 115 | var msg, regex, opts, options, opt, i, j, matches, nick, users, chan, channel, params, nicklist, caps, rtn, obj, tmp, namespace; |
337c9866 D |
116 | //regex = /^(?::(?:([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)|([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)!([a-z0-9~\.\-_|]+)@?([a-z0-9\.\-:\/]+)?) )?([a-z0-9]+)(?:(?: ([^:]+))?(?: :(.+))?)$/i; |
117 | //regex = /^(?::(\S+) )?(\S+)(?: (?!:)(.+?))?(?: :(.+))?$/i; | |
118 | regex = /^(?::(?:([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)|([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)!([a-z0-9~\.\-_|]+)@?([a-z0-9\.\-:\/]+)?) )?(\S+)(?: (?!:)(.+?))?(?: :(.+))?$/i; | |
119 | ||
fd779420 D |
120 | msg = regex.exec(data); |
121 | if (msg) { | |
122 | msg = { | |
123 | prefix: msg[1], | |
124 | nick: msg[2], | |
125 | ident: msg[3], | |
126 | hostname: msg[4] || '', | |
127 | command: msg[5], | |
128 | params: msg[6] || '', | |
129 | trailing: (msg[7]) ? msg[7].trim() : '' | |
130 | }; | |
8343584e | 131 | |
fd779420 D |
132 | switch (msg.command.toUpperCase()) { |
133 | case 'PING': | |
134 | websocket.sendServerLine('PONG ' + msg.trailing); | |
135 | break; | |
136 | case ircNumerics.RPL_WELCOME: | |
137 | if (ircSocket.IRC.CAP.negotiating) { | |
138 | ircSocket.IRC.CAP.negotiating = false; | |
139 | ircSocket.IRC.CAP.enabled = []; | |
140 | ircSocket.IRC.CAP.requested = []; | |
141 | ircSocket.IRC.registered = true; | |
142 | } | |
b73cccbd D |
143 | //regex = /([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)!([a-z0-9~\.\-_|]+)@?([a-z0-9\.\-:\/]+)/i; |
144 | //matches = regex.exec(msg.trailing); | |
145 | nick = msg.params.split(' ')[0]; | |
146 | websocket.sendClientEvent('connect', {connected: true, host: null, nick: nick}); | |
fd779420 D |
147 | break; |
148 | case ircNumerics.RPL_ISUPPORT: | |
149 | opts = msg.params.split(" "); | |
150 | options = []; | |
151 | for (i = 0; i < opts.length; i++) { | |
152 | opt = opts[i].split("=", 2); | |
153 | opt[0] = opt[0].toUpperCase(); | |
337c9866 | 154 | ircSocket.IRC.options[opt[0]] = (typeof opt[1] !== 'undefined') ? opt[1] : true; |
8343584e | 155 | if (_.include(['NETWORK', 'PREFIX', 'CHANTYPES', 'NAMESX'], opt[0])) { |
fd779420 D |
156 | if (opt[0] === 'PREFIX') { |
157 | regex = /\(([^)]*)\)(.*)/; | |
158 | matches = regex.exec(opt[1]); | |
159 | if ((matches) && (matches.length === 3)) { | |
160 | ircSocket.IRC.options[opt[0]] = []; | |
161 | for (j = 0; j < matches[2].length; j++) { | |
162 | //ircSocket.IRC.options[opt[0]][matches[2].charAt(j)] = matches[1].charAt(j); | |
163 | ircSocket.IRC.options[opt[0]].push({symbol: matches[2].charAt(j), mode: matches[1].charAt(j)}); | |
fd779420 | 164 | } |
337c9866 | 165 | |
fd779420 D |
166 | } |
167 | } | |
8343584e D |
168 | if (opt[0] === 'NAMESX') { |
169 | websocket.sendServerLine('PROTOCTL NAMESX'); | |
170 | } | |
fd779420 D |
171 | } |
172 | } | |
337c9866 | 173 | |
fd779420 D |
174 | websocket.sendClientEvent('options', {server: '', "options": ircSocket.IRC.options}); |
175 | break; | |
176 | case ircNumerics.RPL_WHOISUSER: | |
177 | case ircNumerics.RPL_WHOISSERVER: | |
178 | case ircNumerics.RPL_WHOISOPERATOR: | |
179 | case ircNumerics.RPL_ENDOFWHOIS: | |
180 | case ircNumerics.RPL_WHOISCHANNELS: | |
181 | case ircNumerics.RPL_WHOISMODES: | |
182 | websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing}); | |
183 | break; | |
b10afa2d D |
184 | |
185 | case ircNumerics.RPL_LISTSTART: | |
186 | (function () { | |
187 | websocket.sendClientEvent('list_start', {server: ''}); | |
5c7ac96f | 188 | websocket.kiwi.buffer.list = []; |
b10afa2d D |
189 | }()); |
190 | break; | |
191 | case ircNumerics.RPL_LISTEND: | |
192 | (function () { | |
5c7ac96f JA |
193 | if (websocket.kiwi.buffer.list.length > 0) { |
194 | websocket.kiwi.buffer.list = _.sortBy(websocket.kiwi.buffer.list, function (channel) { | |
195 | return channel.num_users; | |
196 | }); | |
197 | websocket.sendClientEvent('list_channel', {chans: websocket.kiwi.buffer.list}); | |
198 | websocket.kiwi.buffer.list = []; | |
199 | } | |
b10afa2d D |
200 | websocket.sendClientEvent('list_end', {server: ''}); |
201 | }()); | |
202 | break; | |
8343584e | 203 | |
b10afa2d D |
204 | case ircNumerics.RPL_LIST: |
205 | (function () { | |
206 | var parts, channel, num_users, modes, topic; | |
207 | ||
208 | parts = msg.params.split(' '); | |
209 | channel = parts[1]; | |
210 | num_users = parts[2]; | |
211 | modes = msg.trailing.split(' ', 1); | |
80c584e7 | 212 | topic = msg.trailing.substring(msg.trailing.indexOf(' ') + 1); |
b10afa2d | 213 | |
5c7ac96f JA |
214 | //websocket.sendClientEvent('list_channel', { |
215 | websocket.kiwi.buffer.list.push({ | |
b10afa2d D |
216 | server: '', |
217 | channel: channel, | |
218 | topic: topic, | |
219 | modes: modes, | |
5c7ac96f | 220 | num_users: parseInt(num_users, 10) |
b10afa2d | 221 | }); |
8343584e | 222 | |
5c7ac96f JA |
223 | if (websocket.kiwi.buffer.list.length > 200) { |
224 | websocket.kiwi.buffer.list = _.sortBy(websocket.kiwi.buffer.list, function (channel) { | |
225 | return channel.num_users; | |
226 | }); | |
227 | websocket.sendClientEvent('list_channel', {chans: websocket.kiwi.buffer.list}); | |
228 | websocket.kiwi.buffer.list = []; | |
229 | } | |
8343584e | 230 | |
b10afa2d D |
231 | }()); |
232 | break; | |
233 | ||
fd779420 D |
234 | case ircNumerics.RPL_WHOISIDLE: |
235 | params = msg.params.split(" ", 4); | |
236 | rtn = {server: '', nick: params[1], idle: params[2]}; | |
237 | if (params[3]) { | |
238 | rtn.logon = params[3]; | |
239 | } | |
240 | websocket.sendClientEvent('whois', rtn); | |
241 | break; | |
242 | case ircNumerics.RPL_MOTD: | |
243 | websocket.sendClientEvent('motd', {server: '', "msg": msg.trailing}); | |
244 | break; | |
245 | case ircNumerics.RPL_NAMEREPLY: | |
246 | params = msg.params.split(" "); | |
247 | nick = params[0]; | |
248 | chan = params[2]; | |
249 | users = msg.trailing.split(" "); | |
8343584e | 250 | nicklist = []; |
fd779420 D |
251 | i = 0; |
252 | _.each(users, function (user) { | |
8343584e D |
253 | var j, k, modes = []; |
254 | for (j = 0; j < user.length; j++) { | |
255 | for (k = 0; k < ircSocket.IRC.options.PREFIX.length; k++) { | |
256 | if (user.charAt(j) === ircSocket.IRC.options.PREFIX[k].symbol) { | |
257 | modes.push(ircSocket.IRC.options.PREFIX[k].mode); | |
258 | } | |
259 | } | |
fd779420 | 260 | } |
8343584e | 261 | nicklist.push({nick: user, modes: modes}); |
fd779420 D |
262 | if (i++ >= 50) { |
263 | websocket.sendClientEvent('userlist', {server: '', 'users': nicklist, channel: chan}); | |
8343584e | 264 | nicklist = []; |
fd779420 D |
265 | i = 0; |
266 | } | |
267 | }); | |
268 | if (i > 0) { | |
269 | websocket.sendClientEvent('userlist', {server: '', "users": nicklist, channel: chan}); | |
270 | } else { | |
271 | console.log("oops"); | |
272 | } | |
273 | break; | |
274 | case ircNumerics.RPL_ENDOFNAMES: | |
275 | websocket.sendClientEvent('userlist_end', {server: '', channel: msg.params.split(" ")[1]}); | |
276 | break; | |
277 | case ircNumerics.ERR_LINKCHANNEL: | |
8343584e | 278 | params = msg.params.split(" "); |
fd779420 D |
279 | websocket.sendClientEvent('channel_redirect', {from: params[1], to: params[2]}); |
280 | break; | |
281 | case ircNumerics.ERR_NOSUCHNICK: | |
282 | websocket.sendClientEvent('irc_error', {error: 'no_such_nick', nick: msg.params.split(" ")[1], reason: msg.trailing}); | |
283 | break; | |
284 | case 'JOIN': | |
285 | // Some BNC's send malformed JOIN causing the channel to be as a | |
286 | // parameter instead of trailing. | |
287 | if (typeof msg.trailing === 'string' && msg.trailing !== '') { | |
288 | channel = msg.trailing; | |
289 | } else if (typeof msg.params === 'string' && msg.params !== '') { | |
290 | channel = msg.params; | |
291 | } | |
292 | ||
293 | websocket.sendClientEvent('join', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: channel}); | |
294 | if (msg.nick === ircSocket.IRC.nick) { | |
295 | websocket.sendServerLine('NAMES ' + msg.trailing); | |
296 | } | |
297 | break; | |
298 | case 'PART': | |
299 | websocket.sendClientEvent('part', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), message: msg.trailing}); | |
300 | break; | |
301 | case 'KICK': | |
302 | params = msg.params.split(" "); | |
303 | websocket.sendClientEvent('kick', {kicked: params[1], nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: params[0].trim(), message: msg.trailing}); | |
304 | break; | |
305 | case 'QUIT': | |
306 | websocket.sendClientEvent('quit', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, message: msg.trailing}); | |
307 | break; | |
308 | case 'NOTICE': | |
309 | if ((msg.trailing.charAt(0) === String.fromCharCode(1)) && (msg.trailing.charAt(msg.trailing.length - 1) === String.fromCharCode(1))) { | |
310 | // It's a CTCP response | |
311 | websocket.sendClientEvent('ctcp_response', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing.substr(1, msg.trailing.length - 2)}); | |
312 | } else { | |
a5ffcf00 | 313 | websocket.sendClientEvent('notice', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, target: msg.params.trim(), msg: msg.trailing}); |
fd779420 D |
314 | } |
315 | break; | |
316 | case 'NICK': | |
317 | websocket.sendClientEvent('nick', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, newnick: msg.trailing}); | |
318 | break; | |
319 | case 'TOPIC': | |
320 | obj = {nick: msg.nick, channel: msg.params, topic: msg.trailing}; | |
321 | websocket.sendClientEvent('topic', obj); | |
322 | break; | |
323 | case ircNumerics.RPL_TOPIC: | |
324 | obj = {nick: '', channel: msg.params.split(" ")[1], topic: msg.trailing}; | |
325 | websocket.sendClientEvent('topic', obj); | |
326 | break; | |
327 | case ircNumerics.RPL_NOTOPIC: | |
328 | obj = {nick: '', channel: msg.params.split(" ")[1], topic: ''}; | |
329 | websocket.sendClientEvent('topic', obj); | |
330 | break; | |
331 | case 'MODE': | |
332 | opts = msg.params.split(" "); | |
333 | params = {nick: msg.nick}; | |
334 | switch (opts.length) { | |
335 | case 1: | |
336 | params.effected_nick = opts[0]; | |
337 | params.mode = msg.trailing; | |
338 | break; | |
339 | case 2: | |
340 | params.channel = opts[0]; | |
341 | params.mode = opts[1]; | |
342 | break; | |
343 | default: | |
344 | params.channel = opts[0]; | |
345 | params.mode = opts[1]; | |
346 | params.effected_nick = opts[2]; | |
347 | break; | |
348 | } | |
349 | websocket.sendClientEvent('mode', params); | |
350 | break; | |
351 | case 'PRIVMSG': | |
352 | if ((msg.trailing.charAt(0) === String.fromCharCode(1)) && (msg.trailing.charAt(msg.trailing.length - 1) === String.fromCharCode(1))) { | |
353 | // It's a CTCP request | |
354 | if (msg.trailing.substr(1, 6) === 'ACTION') { | |
355 | websocket.sendClientEvent('action', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing.substr(7, msg.trailing.length - 2)}); | |
abb46d2d | 356 | } else if (msg.trailing.substr(1, 4) === 'KIWI') { |
2fc64ec2 JA |
357 | tmp = msg.trailing.substr(6, msg.trailing.length - 2); |
358 | namespace = tmp.split(' ', 1)[0]; | |
359 | websocket.sendClientEvent('kiwi', {namespace: namespace, data: tmp.substr(namespace.length + 1)}); | |
8343584e | 360 | |
fd779420 D |
361 | } else if (msg.trailing.substr(1, 7) === 'VERSION') { |
362 | ircSocket.write('NOTICE ' + msg.nick + ' :' + String.fromCharCode(1) + 'VERSION KiwiIRC' + String.fromCharCode(1) + '\r\n'); | |
363 | } else { | |
364 | websocket.sendClientEvent('ctcp_request', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing.substr(1, msg.trailing.length - 2)}); | |
365 | } | |
366 | } else { | |
367 | obj = {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing}; | |
368 | websocket.sendClientEvent('msg', obj); | |
369 | } | |
370 | break; | |
371 | case 'CAP': | |
372 | caps = kiwi.config.cap_options; | |
373 | options = msg.trailing.split(" "); | |
374 | switch (_.last(msg.params.split(" "))) { | |
375 | case 'LS': | |
376 | opts = ''; | |
377 | _.each(_.intersect(caps, options), function (cap) { | |
378 | if (opts !== '') { | |
379 | opts += " "; | |
380 | } | |
381 | opts += cap; | |
382 | ircSocket.IRC.CAP.requested.push(cap); | |
383 | }); | |
384 | if (opts.length > 0) { | |
385 | websocket.sendServerLine('CAP REQ :' + opts); | |
386 | } else { | |
387 | websocket.sendServerLine('CAP END'); | |
388 | } | |
389 | // TLS is special | |
390 | /*if (_.include(options, 'tls')) { | |
391 | websocket.sendServerLine('STARTTLS'); | |
392 | ircSocket.IRC.CAP.requested.push('tls'); | |
393 | }*/ | |
394 | break; | |
395 | case 'ACK': | |
396 | _.each(options, function (cap) { | |
397 | ircSocket.IRC.CAP.enabled.push(cap); | |
398 | }); | |
399 | if (_.last(msg.params.split(" ")) !== '*') { | |
400 | ircSocket.IRC.CAP.requested = []; | |
401 | ircSocket.IRC.CAP.negotiating = false; | |
402 | websocket.sendServerLine('CAP END'); | |
403 | } | |
404 | break; | |
405 | case 'NAK': | |
406 | ircSocket.IRC.CAP.requested = []; | |
407 | ircSocket.IRC.CAP.negotiating = false; | |
408 | websocket.sendServerLine('CAP END'); | |
409 | break; | |
410 | } | |
411 | break; | |
412 | /*case ircNumerics.RPL_STARTTLS: | |
413 | try { | |
414 | IRC = ircSocket.IRC; | |
415 | listeners = ircSocket.listeners('data'); | |
416 | ircSocket.removeAllListeners('data'); | |
417 | ssl_socket = starttls(ircSocket, {}, function () { | |
418 | ssl_socket.on("data", function (data) { | |
419 | ircSocketDataHandler(data, websocket, ssl_socket); | |
420 | }); | |
421 | ircSocket = ssl_socket; | |
422 | ircSocket.IRC = IRC; | |
423 | _.each(listeners, function (listener) { | |
424 | ircSocket.addListener('data', listener); | |
425 | }); | |
426 | }); | |
427 | //console.log(ircSocket); | |
428 | } catch (e) { | |
429 | console.log(e); | |
430 | } | |
431 | break;*/ | |
432 | case ircNumerics.ERR_CANNOTSENDTOCHAN: | |
433 | websocket.sendClientEvent('irc_error', {error: 'cannot_send_to_chan', channel: msg.params.split(" ")[1], reason: msg.trailing}); | |
434 | break; | |
435 | case ircNumerics.ERR_TOOMANYCHANNELS: | |
436 | websocket.sendClientEvent('irc_error', {error: 'too_many_channels', channel: msg.params.split(" ")[1], reason: msg.trailing}); | |
437 | break; | |
438 | case ircNumerics.ERR_USERNOTINCHANNEL: | |
439 | params = msg.params.split(" "); | |
440 | websocket.sendClientEvent('irc_error', {error: 'user_not_in_channel', nick: params[0], channel: params[1], reason: msg.trainling}); | |
441 | break; | |
442 | case ircNumerics.ERR_NOTONCHANNEL: | |
443 | websocket.sendClientEvent('irc_error', {error: 'not_on_channel', channel: msg.params.split(" ")[1], reason: msg.trailing}); | |
444 | break; | |
445 | case ircNumerics.ERR_CHANNELISFULL: | |
446 | websocket.sendClientEvent('irc_error', {error: 'channel_is_full', channel: msg.params.split(" ")[1], reason: msg.trailing}); | |
447 | break; | |
448 | case ircNumerics.ERR_INVITEONLYCHAN: | |
449 | websocket.sendClientEvent('irc_error', {error: 'invite_only_channel', channel: msg.params.split(" ")[1], reason: msg.trailing}); | |
450 | break; | |
451 | case ircNumerics.ERR_BANNEDFROMCHAN: | |
452 | websocket.sendClientEvent('irc_error', {error: 'banned_from_channel', channel: msg.params.split(" ")[1], reason: msg.trailing}); | |
453 | break; | |
454 | case ircNumerics.ERR_BADCHANNELKEY: | |
455 | websocket.sendClientEvent('irc_error', {error: 'bad_channel_key', channel: msg.params.split(" ")[1], reason: msg.trailing}); | |
456 | break; | |
457 | case ircNumerics.ERR_CHANOPRIVSNEEDED: | |
458 | websocket.sendClientEvent('irc_error', {error: 'chanop_privs_needed', channel: msg.params.split(" ")[1], reason: msg.trailing}); | |
459 | break; | |
460 | case ircNumerics.ERR_NICKNAMEINUSE: | |
461 | websocket.sendClientEvent('irc_error', {error: 'nickname_in_use', nick: _.last(msg.params.split(" ")), reason: msg.trailing}); | |
462 | break; | |
463 | case 'ERROR': | |
464 | ircSocket.end(); | |
465 | websocket.sendClientEvent('irc_error', {error: 'error', reason: msg.trailing}); | |
466 | websocket.disconnect(); | |
467 | break; | |
468 | case ircNumerics.ERR_NOTREGISTERED: | |
469 | if (ircSocket.IRC.registered) { | |
470 | console.log('Kiwi thinks user is registered, but the IRC server thinks differently'); | |
471 | } | |
472 | break; | |
473 | default: | |
474 | console.log("Unknown command (" + String(msg.command).toUpperCase() + ")"); | |
475 | } | |
476 | } else { | |
d6e9bff7 | 477 | console.log("Malformed IRC line: " + data); |
fd779420 D |
478 | } |
479 | }; | |
480 | ||
481 | ||
482 | ||
483 | ||
484 | ||
485 | ||
486 | /* | |
487 | * NOTE: Some IRC servers or BNC's out there incorrectly use | |
488 | * only \n as a line splitter. | |
489 | */ | |
490 | this.ircSocketDataHandler = function (data, websocket, ircSocket) { | |
491 | var i; | |
492 | if ((ircSocket.holdLast) && (ircSocket.held !== '')) { | |
493 | data = ircSocket.held + data; | |
494 | ircSocket.holdLast = false; | |
495 | ircSocket.held = ''; | |
496 | } | |
497 | if (data.substr(-1) !== '\n') { | |
498 | ircSocket.holdLast = true; | |
499 | } | |
8343584e | 500 | data = data.split("\n"); |
fd779420 D |
501 | for (i = 0; i < data.length; i++) { |
502 | if (data[i]) { | |
503 | if ((ircSocket.holdLast) && (i === data.length - 1)) { | |
504 | ircSocket.held = data[i]; | |
505 | break; | |
506 | } | |
507 | ||
508 | // We have a complete line of data, parse it! | |
509 | kiwi.parseIRCMessage(websocket, ircSocket, data[i].replace(/^\r+|\r+$/, '')); | |
510 | } | |
511 | } | |
512 | }; | |
513 | ||
514 | ||
515 | ||
516 | ||
517 | ||
518 | this.httpHandler = function (request, response) { | |
8397d902 | 519 | var uri, uri_parts, subs, useragent, agent, server_set, server, nick, debug, touchscreen, hash, |
9f6954dc JA |
520 | min = {}, public_http_path, |
521 | secure = (typeof request.client.encrypted === 'object'); | |
0622748f D |
522 | |
523 | try { | |
524 | if (kiwi.config.handle_http) { | |
525 | uri = url.parse(request.url, true); | |
526 | uri_parts = uri.pathname.split('/'); | |
527 | ||
528 | subs = uri.pathname.substr(0, 4); | |
529 | if (uri.pathname === '/js/all.js') { | |
530 | if (kiwi.cache.alljs === '') { | |
531 | public_http_path = kiwi.kiwi_root + '/' + kiwi.config.public_http; | |
532 | ||
1fce4b40 | 533 | min.underscore = fs.readFileSync(public_http_path + 'js/underscore.min.js'); |
0622748f D |
534 | min.util = fs.readFileSync(public_http_path + 'js/util.js'); |
535 | min.gateway = fs.readFileSync(public_http_path + 'js/gateway.js'); | |
536 | min.front = fs.readFileSync(public_http_path + 'js/front.js'); | |
b41a381f D |
537 | min.front_events = fs.readFileSync(public_http_path + 'js/front.events.js'); |
538 | min.front_ui = fs.readFileSync(public_http_path + 'js/front.ui.js'); | |
0622748f | 539 | min.iscroll = fs.readFileSync(public_http_path + 'js/iscroll.js'); |
b41a381f | 540 | min.ast = jsp.parse(min.underscore + min.util + min.gateway + min.front + min.front_events + min.front_ui + min.iscroll); |
0622748f D |
541 | min.ast = pro.ast_mangle(min.ast); |
542 | min.ast = pro.ast_squeeze(min.ast); | |
543 | min.final_code = pro.gen_code(min.ast); | |
544 | kiwi.cache.alljs = min.final_code; | |
545 | hash = crypto.createHash('md5').update(kiwi.cache.alljs); | |
546 | kiwi.cache.alljs_hash = hash.digest('base64'); | |
547 | } | |
548 | if (request.headers['if-none-match'] === kiwi.cache.alljs_hash) { | |
549 | response.statusCode = 304; | |
550 | } else { | |
551 | response.setHeader('Content-type', 'application/javascript'); | |
552 | response.setHeader('ETag', kiwi.cache.alljs_hash); | |
553 | response.write(kiwi.cache.alljs); | |
554 | } | |
555 | response.end(); | |
556 | } else if ((subs === '/js/') || (subs === '/css') || (subs === '/img')) { | |
557 | request.addListener('end', function () { | |
558 | kiwi.fileServer.serve(request, response); | |
559 | }); | |
560 | } else if (uri.pathname === '/' || uri_parts[1] === 'client') { | |
561 | useragent = (typeof request.headers === 'string') ? request.headers['user-agent'] : ''; | |
562 | if (useragent.match(/android/i) !== -1) { | |
563 | agent = 'android'; | |
564 | touchscreen = true; | |
565 | } else if (useragent.match(/iphone/) !== -1) { | |
566 | agent = 'iphone'; | |
567 | touchscreen = true; | |
568 | } else if (useragent.match(/ipad/) !== -1) { | |
569 | agent = 'ipad'; | |
570 | touchscreen = true; | |
571 | } else if (useragent.match(/ipod/) !== -1) { | |
572 | agent = 'ipod'; | |
573 | touchscreen = true; | |
574 | } else { | |
575 | agent = 'normal'; | |
576 | touchscreen = false; | |
577 | } | |
fd779420 D |
578 | agent = 'normal'; |
579 | touchscreen = false; | |
91726016 | 580 | |
0622748f | 581 | debug = (typeof uri.query.debug !== 'undefined'); |
8343584e | 582 | |
0622748f D |
583 | if (uri_parts[1] !== 'client') { |
584 | if (uri.query) { | |
585 | server_set = ((typeof uri.query.server !== 'undefined') && (uri.query.server !== '')); | |
586 | server = uri.query.server || 'irc.anonnet.org'; | |
587 | nick = uri.query.nick || ''; | |
588 | } else { | |
589 | server_set = false; | |
590 | server = 'irc.anonnet.org'; | |
591 | nick = ''; | |
592 | } | |
8397d902 | 593 | } else { |
0622748f D |
594 | server_set = ((typeof uri_parts[2] !== 'undefined') && (uri_parts[2] !== '')); |
595 | server = server_set ? uri_parts[2] : 'irc.anonnet.org'; | |
596 | nick = uri.query.nick || ''; | |
8397d902 | 597 | } |
8397d902 | 598 | |
0622748f D |
599 | response.setHeader('X-Generated-By', 'KiwiIRC'); |
600 | hash = crypto.createHash('md5').update(touchscreen ? 't' : 'f') | |
601 | .update(debug ? 't' : 'f') | |
602 | .update(server_set ? 't' : 'f') | |
603 | .update(secure ? 't' : 'f') | |
604 | .update(server) | |
605 | .update(nick) | |
606 | .update(agent) | |
607 | .update(JSON.stringify(kiwi.config)) | |
608 | .digest('base64'); | |
609 | if (kiwi.cache.html[hash]) { | |
610 | if (request.headers['if-none-match'] === kiwi.cache.html[hash].hash) { | |
611 | response.statusCode = 304; | |
fd779420 | 612 | } else { |
0622748f D |
613 | response.setHeader('Etag', kiwi.cache.html[hash].hash); |
614 | response.setHeader('Content-type', 'text/html'); | |
615 | response.write(kiwi.cache.html[hash].html); | |
fd779420 D |
616 | } |
617 | response.end(); | |
0622748f D |
618 | } else { |
619 | fs.readFile(__dirname + '/client/index.html.jade', 'utf8', function (err, str) { | |
620 | var html, hash2; | |
621 | if (!err) { | |
622 | html = kiwi.jade.compile(str)({ "touchscreen": touchscreen, "debug": debug, "secure": secure, "server_set": server_set, "server": server, "nick": nick, "agent": agent, "config": kiwi.config }); | |
623 | hash2 = crypto.createHash('md5').update(html).digest('base64'); | |
624 | kiwi.cache.html[hash] = {"html": html, "hash": hash2}; | |
625 | if (request.headers['if-none-match'] === hash2) { | |
626 | response.statusCode = 304; | |
627 | } else { | |
628 | response.setHeader('Etag', hash2); | |
629 | response.setHeader('Content-type', 'text/html'); | |
630 | response.write(html); | |
631 | } | |
632 | } else { | |
633 | response.statusCode = 500; | |
634 | } | |
635 | response.end(); | |
636 | }); | |
637 | } | |
638 | } else if (uri.pathname.substr(0, 10) === '/socket.io') { | |
639 | return; | |
640 | } else { | |
641 | response.statusCode = 404; | |
642 | response.end(); | |
fd779420 | 643 | } |
fd779420 | 644 | } |
0622748f D |
645 | |
646 | } catch (e) { | |
647 | console.log('ERROR app.httpHandler()'); | |
648 | console.log(e); | |
fd779420 D |
649 | } |
650 | }; | |
651 | ||
652 | ||
653 | ||
654 | ||
9f6954dc JA |
655 | this.websocketListen = function (ports, host, handler, key, cert) { |
656 | if (kiwi.httpServers.length > 0) { | |
657 | _.each(kiwi.httpServers, function (hs) { | |
658 | hs.close(); | |
659 | }); | |
660 | kiwi.httpsServers = []; | |
fd779420 | 661 | } |
8343584e | 662 | |
9f6954dc JA |
663 | _.each(ports, function (port) { |
664 | var hs; | |
665 | if (port.secure === true) { | |
666 | hs = https.createServer({key: fs.readFileSync(__dirname + '/' + key), cert: fs.readFileSync(__dirname + '/' + cert)}, handler); | |
667 | kiwi.io.push(ws.listen(hs, {secure: true})); | |
668 | hs.listen(port.number); | |
0622748f | 669 | console.log("Listening on %s:%d with SSL", host, port.number); |
9f6954dc JA |
670 | } else { |
671 | hs = http.createServer(handler); | |
672 | kiwi.io.push(ws.listen(hs, {secure: false})); | |
673 | hs.listen(port.number); | |
0622748f | 674 | console.log("Listening on %s:%d without SSL", host, port.number); |
fd779420 | 675 | } |
9f6954dc | 676 | }); |
8343584e | 677 | |
9f6954dc JA |
678 | _.each(kiwi.io, function (io) { |
679 | io.set('log level', 1); | |
680 | io.enable('browser client minification'); | |
681 | io.enable('browser client etag'); | |
682 | io.set('transports', kiwi.config.transports); | |
1fce4b40 | 683 | |
9f6954dc JA |
684 | io.of('/kiwi').authorization(function (handshakeData, callback) { |
685 | var address = handshakeData.address.address; | |
686 | if (typeof kiwi.connections[address] === 'undefined') { | |
687 | kiwi.connections[address] = {count: 0, sockets: []}; | |
688 | } | |
689 | callback(null, true); | |
690 | }).on('connection', kiwi.websocketConnection); | |
691 | }); | |
fd779420 D |
692 | }; |
693 | ||
694 | ||
695 | ||
696 | ||
697 | ||
698 | ||
699 | this.websocketConnection = function (websocket) { | |
700 | var con; | |
9f6954dc | 701 | console.log("New connection!"); |
5c7ac96f | 702 | websocket.kiwi = {address: websocket.handshake.address.address, buffer: {list: []}}; |
fd779420 D |
703 | con = kiwi.connections[websocket.kiwi.address]; |
704 | ||
705 | if (con.count >= kiwi.config.max_client_conns) { | |
706 | websocket.emit('too_many_connections'); | |
707 | websocket.disconnect(); | |
708 | } else { | |
709 | con.count += 1; | |
710 | con.sockets.push(websocket); | |
711 | ||
712 | websocket.sendClientEvent = function (event_name, data) { | |
4f06269b | 713 | var ev = kiwi.kiwi_mod.run(event_name, data, {websocket: this}); |
2fc64ec2 JA |
714 | if (ev === null) { |
715 | return; | |
716 | } | |
4f06269b | 717 | |
fd779420 D |
718 | data.event = event_name; |
719 | websocket.emit('message', data); | |
720 | }; | |
721 | ||
722 | websocket.sendServerLine = function (data, eol) { | |
723 | eol = (typeof eol === 'undefined') ? '\r\n' : eol; | |
abb46d2d D |
724 | |
725 | try { | |
726 | websocket.ircSocket.write(data + eol); | |
727 | } catch (e) { } | |
fd779420 D |
728 | }; |
729 | ||
730 | websocket.on('irc connect', kiwi.websocketIRCConnect); | |
731 | websocket.on('message', kiwi.websocketMessage); | |
732 | websocket.on('disconnect', kiwi.websocketDisconnect); | |
733 | } | |
734 | }; | |
735 | ||
736 | ||
737 | ||
738 | ||
739 | ||
740 | this.websocketIRCConnect = function (websocket, nick, host, port, ssl, callback) { | |
741 | var ircSocket; | |
742 | //setup IRC connection | |
743 | if (!ssl) { | |
744 | ircSocket = net.createConnection(port, host); | |
745 | } else { | |
746 | ircSocket = tls.connect(port, host); | |
747 | } | |
748 | ircSocket.setEncoding('ascii'); | |
749 | ircSocket.IRC = {options: {}, CAP: {negotiating: true, requested: [], enabled: []}, registered: false}; | |
750 | ircSocket.on('error', function (e) { | |
751 | if (ircSocket.IRC.registered) { | |
752 | websocket.emit('disconnect'); | |
753 | } else { | |
754 | websocket.emit('error', e.message); | |
755 | } | |
756 | }); | |
757 | websocket.ircSocket = ircSocket; | |
758 | ircSocket.holdLast = false; | |
759 | ircSocket.held = ''; | |
8343584e | 760 | |
fd779420 D |
761 | ircSocket.on('data', function (data) { |
762 | kiwi.ircSocketDataHandler(data, websocket, ircSocket); | |
763 | }); | |
8343584e | 764 | |
fd779420 D |
765 | ircSocket.IRC.nick = nick; |
766 | // Send the login data | |
767 | dns.reverse(websocket.kiwi.address, function (err, domains) { | |
768 | //console.log(domains); | |
769 | websocket.kiwi.hostname = (err) ? websocket.kiwi.address : _.first(domains); | |
770 | if ((kiwi.config.webirc) && (kiwi.config.webirc_pass[host])) { | |
771 | websocket.sendServerLine('WEBIRC ' + kiwi.config.webirc_pass[host] + ' KiwiIRC ' + websocket.kiwi.hostname + ' ' + websocket.kiwi.address); | |
772 | } | |
773 | websocket.sendServerLine('CAP LS'); | |
774 | websocket.sendServerLine('NICK ' + nick); | |
8397d902 | 775 | websocket.sendServerLine('USER kiwi_' + nick.replace(/[^0-9a-zA-Z\-_.]/, '') + ' 0 0 :' + nick); |
fd779420 D |
776 | |
777 | if ((callback) && (typeof (callback) === 'function')) { | |
778 | callback(); | |
779 | } | |
780 | }); | |
781 | }; | |
782 | ||
783 | ||
784 | ||
785 | this.websocketMessage = function (websocket, msg, callback) { | |
786 | var args, obj; | |
787 | try { | |
788 | msg.data = JSON.parse(msg.data); | |
789 | args = msg.data.args; | |
790 | switch (msg.data.method) { | |
791 | case 'msg': | |
792 | if ((args.target) && (args.msg)) { | |
793 | obj = kiwi.kiwi_mod.run('msgsend', args, {websocket: websocket}); | |
794 | if (obj !== null) { | |
795 | websocket.sendServerLine('PRIVMSG ' + args.target + ' :' + args.msg); | |
796 | } | |
797 | } | |
798 | break; | |
799 | case 'action': | |
800 | if ((args.target) && (args.msg)) { | |
2fc64ec2 | 801 | websocket.sendServerLine('PRIVMSG ' + args.target + ' :' + String.fromCharCode(1) + 'ACTION ' + args.msg + String.fromCharCode(1)); |
fd779420 D |
802 | } |
803 | break; | |
abb46d2d D |
804 | |
805 | case 'kiwi': | |
806 | if ((args.target) && (args.data)) { | |
2fc64ec2 | 807 | websocket.sendServerLine('PRIVMSG ' + args.target + ' :' + String.fromCharCode(1) + 'KIWI ' + args.data + String.fromCharCode(1)); |
abb46d2d D |
808 | } |
809 | break; | |
810 | ||
fd779420 D |
811 | case 'raw': |
812 | websocket.sendServerLine(args.data); | |
813 | break; | |
814 | case 'join': | |
815 | if (args.channel) { | |
816 | _.each(args.channel.split(","), function (chan) { | |
817 | websocket.sendServerLine('JOIN ' + chan); | |
818 | }); | |
819 | } | |
820 | break; | |
821 | case 'topic': | |
822 | if (args.channel) { | |
823 | websocket.sendServerLine('TOPIC ' + args.channel + ' :' + args.topic); | |
824 | } | |
825 | break; | |
826 | case 'quit': | |
827 | websocket.ircSocket.end('QUIT :' + args.message + '\r\n'); | |
828 | websocket.sentQUIT = true; | |
829 | websocket.ircSocket.destroySoon(); | |
830 | websocket.disconnect(); | |
831 | break; | |
832 | case 'notice': | |
833 | if ((args.target) && (args.msg)) { | |
834 | websocket.sendServerLine('NOTICE ' + args.target + ' :' + args.msg); | |
835 | } | |
836 | break; | |
837 | default: | |
838 | } | |
839 | if ((callback) && (typeof (callback) === 'function')) { | |
840 | callback(); | |
841 | } | |
842 | } catch (e) { | |
843 | console.log("Caught error: " + e); | |
844 | } | |
845 | }; | |
846 | ||
847 | ||
848 | ||
849 | this.websocketDisconnect = function (websocket) { | |
1fce4b40 | 850 | var con; |
fd779420 D |
851 | |
852 | if ((!websocket.sentQUIT) && (websocket.ircSocket)) { | |
853 | try { | |
854 | websocket.ircSocket.end('QUIT :' + kiwi.config.quit_message + '\r\n'); | |
855 | websocket.sentQUIT = true; | |
856 | websocket.ircSocket.destroySoon(); | |
857 | } catch (e) { | |
858 | } | |
859 | } | |
860 | con = kiwi.connections[websocket.kiwi.address]; | |
861 | con.count -= 1; | |
862 | con.sockets = _.reject(con.sockets, function (sock) { | |
863 | return sock === websocket; | |
864 | }); | |
865 | }; | |
866 | ||
867 | ||
868 | ||
869 | ||
870 | ||
871 | ||
872 | this.rehash = function () { | |
873 | var changes, i, | |
874 | reload_config = kiwi.loadConfig(); | |
875 | ||
876 | // If loading the new config errored out, dont attempt any changes | |
877 | if (reload_config === false) { | |
878 | return false; | |
879 | } | |
8343584e | 880 | |
fd779420 D |
881 | // We just want the settings that have been changed |
882 | changes = reload_config[1]; | |
883 | ||
884 | if (Object.keys(changes).length !== 0) { | |
885 | console.log('%s config changes: \n', Object.keys(changes).length, changes); | |
886 | for (i in changes) { | |
887 | switch (i) { | |
9f6954dc | 888 | case 'ports': |
fd779420 | 889 | case 'bind_address': |
fd779420 D |
890 | case 'ssl_key': |
891 | case 'ssl_cert': | |
9f6954dc JA |
892 | kiwi.websocketListen(kiwi.config.ports, kiwi.config.bind_address, kiwi.httpHandler, kiwi.config.ssl_key, kiwi.config.ssl_cert); |
893 | delete changes.ports; | |
fd779420 | 894 | delete changes.bind_address; |
fd779420 D |
895 | delete changes.ssl_key; |
896 | delete changes.ssl_cert; | |
897 | break; | |
898 | case 'user': | |
899 | case 'group': | |
900 | kiwi.changeUser(); | |
901 | delete changes.user; | |
902 | delete changes.group; | |
903 | break; | |
904 | case 'module_dir': | |
905 | case 'modules': | |
906 | kiwi.kiwi_mod.loadModules(kiwi_root, kiwi.config); | |
907 | kiwi.kiwi_mod.printMods(); | |
908 | delete changes.module_dir; | |
909 | delete changes.modules; | |
910 | break; | |
911 | } | |
912 | } | |
913 | } | |
914 | ||
915 | // Also clear the kiwi.cached javascript and HTML | |
916 | if (kiwi.config.handle_http) { | |
917 | kiwi.cache = {alljs: '', html: []}; | |
918 | } | |
919 | ||
920 | return true; | |
921 | }; | |
922 | ||
923 | ||
924 | ||
925 | ||
926 | ||
927 | /* | |
928 | * KiwiIRC controlling via STDIN | |
929 | */ | |
87a6abbe | 930 | this.manageControll = function (data) { |
2fc64ec2 JA |
931 | var parts = data.toString().trim().split(' '), |
932 | connections_cnt = 0, | |
933 | i; | |
87a6abbe D |
934 | switch (parts[0]) { |
935 | case 'rehash': | |
936 | console.log('Rehashing...'); | |
937 | console.log(kiwi.rehash() ? 'Rehash complete' : 'Rehash failed'); | |
938 | break; | |
939 | ||
1fce4b40 | 940 | case 'recode': |
87a6abbe D |
941 | console.log('Recoding...'); |
942 | console.log(kiwi.recode() ? 'Recode complete' : 'Recode failed'); | |
943 | break; | |
944 | ||
945 | case 'mod': | |
946 | if (parts[1] === 'reload') { | |
947 | console.log('Reloading module (' + parts[2] + ')..'); | |
948 | kiwi.kiwi_mod.reloadModule(parts[2]); | |
949 | } | |
950 | break; | |
4f06269b | 951 | |
87a6abbe D |
952 | case 'cache': |
953 | if (parts[1] === 'clear') { | |
954 | kiwi.cache.html = {}; | |
604c5174 | 955 | kiwi.cache.alljs = ''; |
87a6abbe D |
956 | console.log('HTML cache cleared'); |
957 | } | |
958 | break; | |
959 | ||
960 | case 'status': | |
2fc64ec2 | 961 | for (i in kiwi.connections) { |
87a6abbe D |
962 | connections_cnt = connections_cnt + parseInt(kiwi.connections[i].count, 10); |
963 | } | |
964 | console.log(connections_cnt.toString() + ' connected clients'); | |
965 | break; | |
c89b9fdf | 966 | |
87a6abbe D |
967 | default: |
968 | console.log('Unknown command \'' + parts[0] + '\''); | |
969 | } | |
2fc64ec2 | 970 | }; |