unrealircd.js 11 KB

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