unrealircd.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. var protocol = {
  2. connected: function(){
  3. ircSend(null, settings.ID, 'PASS', [settings.password]);
  4. ircSend(null, settings.ID, 'PROTOCTL', ['NICKv2', 'VHP', 'UMODE2', 'NICKIP', 'SJOIN', 'SJOIN2', 'SJ3', 'NOQUIT', 'TKLEXT', 'MLOCK', 'SID', 'MTAGS']);
  5. ircSend(null, settings.ID, 'PROTOCTL', ['EAUTH=' + settings.name + ',,,' + settings.version]);
  6. ircSend(null, settings.ID, 'PROTOCTL', ['SID=' + settings.ID]);
  7. ircSend(null, settings.ID, 'SERVER', [settings.name, '1', settings.description]);
  8. },
  9. processMessage: function(msg){
  10. if(!msg) return;
  11. if(msg.command in cmdBinds){
  12. cmdBinds[msg.command](msg);
  13. } else {
  14. console.log('Unhandled cmd: '+msg.command);
  15. console.log(msg);
  16. }
  17. },
  18. processSender: function(sender){
  19. if(sender.nick){
  20. if(server = handlers.findServer(sender.nick)){
  21. sender.nick = server.name;
  22. sender.server = server;
  23. } else if(user = handlers.findUser(sender.nick)){
  24. sender.nick = user.name
  25. sender.ident = user.ident;
  26. sender.host = user.host;
  27. sender.user = user;
  28. } else {
  29. handlers.killUser(sender.nick);
  30. return false;
  31. }
  32. }
  33. return sender;
  34. },
  35. setHandlers: function(newHandlers){
  36. handlers = newHandlers;
  37. },
  38. setSettings: function(newSettings){
  39. settings = newSettings;
  40. }
  41. }
  42. module.exports = protocol;
  43. var handlers = {
  44. sendHandler: null,
  45. findServer: null,
  46. findUser: null,
  47. killUser: null,
  48. newServer: null,
  49. newUser: null,
  50. getChannel: null,
  51. findChannel: null,
  52. quitUser: null,
  53. removeServer: null
  54. };
  55. var settings = {
  56. ID: null,
  57. password: null,
  58. name: null,
  59. description: null,
  60. version: null,
  61. me: null,
  62. maxUsers: null
  63. };
  64. var connection = {
  65. 'protoctl': {},
  66. 'prefixes': {},
  67. 'statusmodes': [],
  68. 'chmodes': [], // 0 - list modes, 1 - argument modes, 2 - argument modes (add only), 3 - standard modes
  69. 'umodes': {}
  70. };
  71. var chmodeMap = {
  72. 'b': { 'name': 'BAN', 'user': true, 'services': true },
  73. 'e': { 'name': 'EXCEPT', 'user': true, 'services': true },
  74. 'I': { 'name': 'INVEX', 'user': true, 'services': true },
  75. 'v': { 'name': 'VOICE', 'user': true, 'services': true },
  76. 'h': { 'name': 'HALFOP', 'user': true, 'services': true },
  77. 'o': { 'name': 'OP', 'user': true, 'services': true },
  78. 'a': { 'name': 'PROTECT', 'user': true, 'services': true },
  79. 'q': { 'name': 'OWNER', 'user': true, 'services': true },
  80. 'k': { 'name': 'KEY', 'user': true, 'services': true },
  81. 'L': { 'name': 'CHANNELLINK', 'user': true, 'services': true },
  82. 'f': { 'name': 'FLOODPROT', 'user': true, 'services': true },
  83. 'l': { 'name': 'LIMIT', 'user': true, 'services': true },
  84. 'H': { 'name': 'HISTORY', 'user': true, 'services': true },
  85. 'p': { 'name': 'PRIVATE', 'user': true, 'services': true },
  86. 's': { 'name': 'SECRET', 'user': true, 'services': true },
  87. 'm': { 'name': 'MODERATED', 'user': true, 'services': true },
  88. 'n': { 'name': 'NOEXTERNAL', 'user': true, 'services': true },
  89. 't': { 'name': 'TOPIC', 'user': true, 'services': true },
  90. 'i': { 'name': 'INVITE', 'user': true, 'services': true },
  91. 'r': { 'name': 'REGISTERED', 'user': false, 'services': true },
  92. 'z': { 'name': 'SECUREONLY', 'user': true, 'services': true },
  93. 'M': { 'name': 'NONREGMODERATED', 'user': true, 'services': true },
  94. 'Q': { 'name': 'NOKICK', 'user': true, 'services': true },
  95. 'N': { 'name': 'NONICK', 'user': true, 'services': true },
  96. 'R': { 'name': 'NONREGINVITE', 'user': true, 'services': true },
  97. 'T': { 'name': 'NOCTCPS', 'user': true, 'services': true },
  98. 'O': { 'name': 'OPERS', 'user': false, 'services': true },
  99. 'V': { 'name': 'NOINVITE', 'user': true, 'services': true },
  100. 'K': { 'name': 'NOKNOCK', 'user': true, 'services': true },
  101. 'D': { 'name': 'OLDDELAYJOIN', 'user': false, 'services': false },
  102. 'd': { 'name': 'DELAYJOIN', 'user': true, 'services': true },
  103. 'G': { 'name': 'CENSOR', 'user': true, 'services': true },
  104. 'P': { 'name': 'PERMANENT', 'user': false, 'services': true },
  105. 'Z': { 'name': 'SECURE', 'user': false, 'services': false },
  106. 'S': { 'name': 'COLORFILTER', 'user': true, 'services': true },
  107. 'C': { 'name': 'NOCTCP', 'user': true, 'services': true },
  108. 'c': { 'name': 'NOCOLOR', 'user': true, 'services': true }
  109. };
  110. var umodeMap = {
  111. 'i': { 'name': 'INVISIBLE', 'user': true, 'services': true },
  112. 'o': { 'name': 'OPER', 'user': false, 'services': false },
  113. 'w': { 'name': 'WALLOP', 'user': true, 'services': true },
  114. 'r': { 'name': 'REGISTEREDNICK', 'user': false, 'services': true },
  115. 's': { 'name': 'SNOTICE', 'user': true, 'services': true },
  116. 'x': { 'name': 'CLOAK', 'user': true, 'services': true },
  117. 'z': { 'name': 'SECUREUSER', 'user': false, 'services': false },
  118. 'd': { 'name': 'DEAF', 'user': true, 'services': true },
  119. 'H': { 'name': 'HIDEIRCOP', 'user': false, 'services': false },
  120. 't': { 'name': 'VHOST', 'user': false, 'services': true },
  121. 'I': { 'name': 'HIDEIDLE', 'user': false, 'services': false },
  122. 'D': { 'name': 'PRIVDEAF', 'user': true, 'services': true },
  123. 'Z': { 'name': 'SECUREMESSAGES', 'user': true, 'services': true },
  124. 'R': { 'name': 'REGISTEREDMESSAGES', 'user': true, 'services': true },
  125. 'q': { 'name': 'UNKICKABLE', 'user': false, 'services': false },
  126. 'p': { 'name': 'HIDECHANNELS', 'user': true, 'services': true },
  127. 'W': { 'name': 'WHOISNOTICE', 'user': false, 'services': false },
  128. 'G': { 'name': 'CENSORMESSAGES', 'user': true, 'services': true },
  129. 'T': { 'name': 'NOCTCPMESSAGES', 'user': true, 'services': true },
  130. 'S': { 'name': 'SERVICEUSER', 'user': false, 'services': true },
  131. 'B': { 'name': 'BOT', 'user': true, 'services': true }
  132. };
  133. function parseChannelModes(modes, args){
  134. var plus = true;
  135. var argIndex = 0;
  136. var output = {};
  137. output['status'] = [];
  138. output['list'] = [];
  139. for(var i=0; i<modes.length; i++){
  140. var c = modes.charAt(i);
  141. if(c in chmodeMap){
  142. var name = chmodeMap[c].name;
  143. } else {
  144. var name = c;
  145. }
  146. if(c == '+'){
  147. plus = true;
  148. } else if(c == '-'){
  149. plus = false;
  150. } else if(connection.chmodes[0].indexOf(c) >= 0){
  151. output['list'].push({ 'name': name, 'value': args[argIndex++], 'status': plus });
  152. } else if(connection.chmodes[1].indexOf(c) >= 0){
  153. if(plus){
  154. output[name] = args[argIndex++];
  155. } else {
  156. output[name] = false;
  157. argIndex++;
  158. }
  159. } else if(connection.chmodes[2].indexOf(c) >= 0){
  160. if(plus){
  161. output[name] = args[argIndex++];
  162. } else {
  163. output[name] = false;
  164. }
  165. } else if(connection.statusmodes.indexOf(c) >= 0){
  166. var user = handlers.findUser(args[argIndex++]);
  167. if(user){
  168. output['status'].push({ 'name': name, 'user': user, 'status': plus });
  169. } else {
  170. throw 'User not found';
  171. }
  172. } else { // treat unknown chars as type 3
  173. if(connection.chmodes[3].indexOf(c) < 0){
  174. console.log('Unknown mode '+name);
  175. }
  176. output[name] = plus;
  177. }
  178. }
  179. return output;
  180. }
  181. function parseUmodes(modeString){
  182. var plus = true;
  183. var modes = {};
  184. for(var i=0; i<modeString.length; i++){
  185. var c = modeString.charAt(i);
  186. switch(c){
  187. case '+': plus = true; break;
  188. case '-': plus = false; break;
  189. default:
  190. var modeName = umodeMap[c].name;
  191. if(!modeName) modeName = c;
  192. modes[modeName] = plus;
  193. break;
  194. }
  195. }
  196. return modes;
  197. }
  198. function parseModePrefix(text){
  199. var modes = {'status': [], 'list': [] };
  200. var isUser = false;
  201. var c = text.charAt(0);
  202. if(c == '&'){ // +b
  203. modes.list.push({ 'name': 'BAN', 'value': text.substring(1), 'status': true });
  204. } else if(c == '"'){ // +e
  205. modes.list.push({ 'name': 'EXCEPT', 'value': text.substring(1), 'status': true });
  206. } else if(c == '\''){ // +I
  207. modes.list.push({ 'name': 'INVEX', 'value': text.substring(1), 'status': true });
  208. } else {
  209. isUser = true;
  210. for(var i=0; i<text.length; i++){
  211. c = text.charAt(i);
  212. if(c in connection.prefixes){
  213. modes.status.push({ 'name': chmodeMap[connection.prefixes[c]].name, 'user': false /* setting it later */, 'status': true });
  214. // can be more than 1
  215. } else {
  216. break;
  217. }
  218. }
  219. }
  220. var user = null;
  221. if(isUser){
  222. var uid = text.substring(i);
  223. user = handlers.findUser(uid);
  224. if(!user){
  225. handlers.killUser(uid);
  226. modes.status = [];
  227. }
  228. for(var i=0; i<modes.status.length; i++){
  229. modes.status[i].user = user;
  230. }
  231. }
  232. return { 'modes': modes, 'user': user };
  233. }
  234. function ircSend(tags, from, cmd, args){
  235. handlers.send(tags, from, cmd, args);
  236. }
  237. var cmdBinds = {
  238. 'PROTOCTL': function(msg){
  239. for(var i=0; i<msg.args.length; i++){
  240. var arg = msg.args[i];
  241. if(arg.indexOf('=') >= 0){
  242. var arg = arg.split('=');
  243. connection.protoctl[arg[0]] = arg[1];
  244. switch(arg[0]){
  245. case 'PREFIX':
  246. var data = arg[1].substring(1).split(')');
  247. var modes = data[0].split('');
  248. var chars = data[1].split('');
  249. for(var i=0; i<modes.length; i++){
  250. connection.prefixes[chars[i]] = modes[i];
  251. connection.statusmodes.push(modes[i]);
  252. }
  253. break;
  254. case 'CHANMODES':
  255. connection.chmodes = arg[1].split(',');
  256. break;
  257. case 'USERMODES':
  258. connection.umodes = arg[1];
  259. break;
  260. }
  261. } else {
  262. connection.protoctl[arg] = true;
  263. }
  264. }
  265. },
  266. 'SERVER': function(msg){
  267. var expr = /^([^-]+)-([^-]+)-([^ ]+) (.*)$/;
  268. var match = expr.exec(msg.args[2]);
  269. if(match){
  270. handlers.newServer(msg.args[0], match[3], match[4], msg.args[1], settings.me);
  271. } else {
  272. throw 'Unknown SERVER message';
  273. }
  274. },
  275. 'MD': function(msg){
  276. switch(msg.args[0]){
  277. case 'client':
  278. var user = handlers.findUser(msg.args[1]);
  279. if(!user) return;
  280. switch(msg.args[2]){
  281. case 'certfp':
  282. if(!msg.args[3] || msg.args[3].length == 0) break;
  283. user.setSecure(true);
  284. user.setFingerprint(msg.args[3]);
  285. break;
  286. default: break;
  287. }
  288. break;
  289. default: break;
  290. }
  291. },
  292. 'SMOD': function(msg){ // ignore
  293. },
  294. 'EOS': function(msg){ // ?
  295. console.log(connection);
  296. },
  297. 'SINFO': function(msg){
  298. console.log(msg);
  299. },
  300. 'SID': function(msg){
  301. handlers.newServer(msg.args[0], msg.args[2], msg.args[3], msg.args[1], msg.sender.server);
  302. },
  303. 'UID': function(msg){
  304. var nick = msg.args[0];
  305. var distance = msg.args[1];
  306. var TS = msg.args[2];
  307. var ident = msg.args[3];
  308. var host = msg.args[4];
  309. var uid = msg.args[5];
  310. var account = msg.args[6];
  311. var umodes = msg.args[7];
  312. var vhost = msg.args[8];
  313. var cloakedHost = msg.args[9];
  314. var ip = msg.args[10];
  315. var realname = msg.args[11];
  316. if(ip != '*'){
  317. var ipBinary = Buffer.from(ip, 'base64');
  318. if(ipBinary.byteLength == 4){ // IPv4
  319. ip = ipBinary[0].toString(10) + '.' + ipBinary[1].toString(10) + '.' + ipBinary[2].toString(10) + '.' + ipBinary[3].toString(10);
  320. } else if(ipBinary.byteLength == 16){ // IPv6
  321. ip = '';
  322. for(var i=0; i<8; i++){
  323. if(i > 0) ip += ':';
  324. ip += ipBinary[i*2].toString(16).padStart(2, '0') + ipBinary[i*2+1].toString(16).padStart(2, '0');
  325. }
  326. } else {
  327. throw 'unknown IP format';
  328. }
  329. } else {
  330. ip = null;
  331. }
  332. if(vhost == '*'){
  333. vhost = null;
  334. }
  335. if(cloakedHost == '*'){
  336. cloakedHost = null;
  337. }
  338. if(account == '0'){
  339. account = null;
  340. }
  341. handlers.newUser(nick, distance, TS, ident, host, uid, account, parseUmodes(umodes), vhost, cloakedHost, ip, realname, msg.sender.server);
  342. },
  343. 'SJOIN': function(msg){
  344. var channel = handlers.getChannel(msg.args[1]);
  345. channel.setTS(msg.args[0]);
  346. if(msg.args.length > 3){
  347. var modeArgs = [];
  348. for(var i=3; i<msg.args.length - 1; i++){
  349. modeArgs.push(msg.args[i]);
  350. }
  351. channel.changeModes(parseChannelModes(msg.args[2], modeArgs));
  352. }
  353. var members = msg.text.split(' ');
  354. for(var i=0; i<members.length; i++){
  355. var member = members[i];
  356. var c = member.charAt(0);
  357. var udata = parseModePrefix(member);
  358. if(udata.user){
  359. console.log(udata.user.name + ' joined ' + msg.args[1]);
  360. channel.joinUser(udata.user);
  361. }
  362. channel.changeModes(udata.modes);
  363. }
  364. },
  365. 'SENDUMODE': function(msg){ // ignore
  366. },
  367. 'SENDSNO': function(msg){ // ignore
  368. },
  369. 'PASS': function(msg){ // ignore
  370. },
  371. 'SWHOIS': function(msg){
  372. console.log(msg);
  373. //TODO
  374. },
  375. 'MODE': function(msg){
  376. var channel = handlers.findChannel(msg.args[0]);
  377. if(channel){
  378. var modeArgs = [];
  379. for(var i=2; i<msg.args.length; i++){
  380. modeArgs.push(msg.args[i]);
  381. }
  382. var chmodes = parseChannelModes(msg.args[1], modeArgs);
  383. channel.changeModes(chmodes);
  384. }
  385. },
  386. 'TKL': function(msg){
  387. // console.log(msg);
  388. //TODO
  389. },
  390. 'METADATA': function(msg){
  391. console.log(msg);
  392. //TODO
  393. },
  394. 'NETINFO': function(msg){
  395. ircSend(null, null, 'NETINFO', [settings.maxUsers.toString(10), Math.floor(new Date() / 1000).toString(10), msg.args[2], msg.args[3], "0", "0", "0", msg.args[7]]);
  396. },
  397. 'SASL': function(msg){
  398. console.log(msg);
  399. //TODO
  400. },
  401. 'PING': function(msg){
  402. msg.reply(null, 'PONG', msg.args);
  403. },
  404. 'QUIT': function(msg){
  405. handlers.quitUser(msg.sender.user);
  406. },
  407. 'PART': function(msg){
  408. var channel = handlers.getChannel(msg.args[0]);
  409. channel.removeUser(msg.sender.user);
  410. },
  411. 'CHGHOST': function(msg){
  412. var user = handlers.findUser(msg.args[0]);
  413. if(!user) return;
  414. user.changeVHost(msg.args[1]);
  415. },
  416. 'CHGIDENT': function(msg){
  417. var user = handlers.findUser(msg.args[0]);
  418. if(!user) return;
  419. user.changeVIdent(msg.args[1]);
  420. },
  421. 'CHGNAME': function(msg){
  422. var user = handlers.findUser(msg.args[0]);
  423. if(!user) return;
  424. user.setRealname(msg.args[1]);
  425. },
  426. 'SETNAME': function(msg){
  427. msg.sender.user.setRealname(msg.args[0]);
  428. },
  429. 'SETHOST': function(msg){
  430. msg.sender.user.changeVHost(msg.args[0]);
  431. },
  432. 'SETIDENT': function(msg){
  433. msg.sender.user.changeVIdent(msg.args[0]);
  434. },
  435. 'NICK': function(msg){
  436. msg.sender.user.changeNick(msg.args[0]);
  437. },
  438. 'SDESC': function(msg){
  439. console.log(msg);
  440. //TODO
  441. },
  442. 'TOPIC': function(msg){
  443. console.log(msg);
  444. //TODO
  445. },
  446. /*
  447. messagedata {
  448. text: '0',
  449. args: [ '183.89.186.94', '0' ],
  450. tags: [],
  451. command: 'REPUTATION',
  452. sender: {
  453. nick: 'test1.pirc.pl',
  454. ident: '',
  455. host: '',
  456. server: server {
  457. name: 'test1.pirc.pl',
  458. sid: '093',
  459. description: 'serwer testowy!',
  460. distance: '3',
  461. uplink: [Object],
  462. introduce: [Function]
  463. },
  464. user: false
  465. },
  466. time: 2020-03-14T10:48:06.207Z,
  467. reply: [Function],
  468. originalString: ':093 REPUTATION 183.89.186.94 0'
  469. }
  470. */
  471. 'REPUTATION': function(msg){
  472. //TODO
  473. },
  474. 'UMODE2': function(msg){
  475. msg.sender.user.changeUmodes(parseUmodes(msg.args[0]));
  476. },
  477. 'KILL': function(msg){
  478. var user = handlers.findUser(msg.args[0]);
  479. if(!user) return;
  480. handlers.quitUser(user);
  481. },
  482. 'SQUIT': function(msg){
  483. var server = handlers.findServer(msg.args[0]);
  484. if(!server) return;
  485. handlers.removeServer(server);
  486. }
  487. };