unrealircd.js 13 KB

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