1 /** 2 * A simple IRC server. 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <ae@cy.md> 12 */ 13 14 module ae.net.irc.server; 15 16 import std.algorithm; 17 import std.conv; 18 import std.datetime; 19 import std.exception; 20 import std.range; 21 import std.regex; 22 import std.socket; 23 import std..string; 24 25 import ae.net.asockets; 26 import ae.utils.array; 27 import ae.sys.log; 28 import ae.utils.exception; 29 import ae.utils.meta; 30 import ae.utils.text; 31 32 import ae.net.irc.common; 33 34 private alias std..string.indexOf indexOf; 35 36 /// IRC server. 37 class IrcServer 38 { 39 // This class is currently intentionally written for readability, not performance. 40 // Performance and scalability could be greatly improved by using numeric indices for users and channels 41 // instead of associative arrays. 42 43 string hostname; /// Hostname to announce. Defaults to the current machine's hostname. 44 string password; /// If set, require this password to be specified using PASS. 45 string network; /// If set, announce as the "NETWORK=". 46 /// Require that nicknames match the given regular expression. 47 string nicknameValidationPattern = "^[a-zA-Z][a-zA-Z0-9\\-`\\|\\[\\]\\{\\}_^]{0,14}$"; 48 uint nicknameMaxLength = 15; /// For the announced capabilities. 49 string serverVersion = "ae.net.irc.server"; /// Announced in MOTD. 50 string[] motd; /// Additional MOTD lines to send. 51 string chanTypes = "#&"; /// Character prefixes indicating channels. 52 SysTime creationTime; /// Announced in MOTD. Defaults to class construction time. 53 string operPassword; /// If set, allow obtaining OPER status using the specified password. 54 55 /// Channels can't be created by users, and don't disappear when they're empty 56 bool staticChannels; 57 /// If set, masks all IPs to the given mask 58 string addressMask; 59 60 Logger log; /// Optional log. 61 62 /// Abstract client connection and information. 63 abstract static class Client 64 { 65 /// How to convert the IRC 8-bit data to and from UTF-8 (D strings must be valid UTF-8). 66 string function(in char[]) decoder = &rawToUTF8, encoder = &UTF8ToRaw; 67 68 /// Registration details 69 string nickname, password; 70 /// ditto 71 string username, hostname, servername, realname; 72 bool identified; /// Pretend that we obtained the username from an `ident` server? 73 /// Full `"nick!user@host"`. `publicPrefix` is what everyone except the user themself and opers see. 74 string prefix, publicPrefix; 75 /// Away reason, if away. 76 string away; 77 78 bool registered; /// Registration completed successfully? 79 Modes modes; /// User modes. 80 MonoTime lastActivity; /// 81 82 Channel[] getJoinedChannels() 83 { 84 Channel[] result; 85 foreach (channel; server.channels) 86 if (nickname.normalized in channel.members) 87 result ~= channel; 88 return result; 89 } /// 90 91 string realHostname() { return remoteAddress.toAddrString; } /// 92 string publicHostname() { return server.addressMask ? server.addressMask : realHostname; } /// 93 bool realHostnameVisibleTo(Client viewer) 94 { 95 return server.addressMask is null 96 || viewer is this 97 || viewer.modes.flags['o']; // Oper 98 } /// 99 string hostnameAsVisibleTo(Client viewer) { return realHostnameVisibleTo(viewer) ? realHostname : publicHostname; } /// 100 string prefixAsVisibleTo(Client viewer) { return realHostnameVisibleTo(viewer) ? prefix : publicPrefix; } /// 101 102 protected: 103 IrcServer server; 104 Address remoteAddress; 105 106 this(IrcServer server, Address remoteAddress) 107 { 108 this.server = server; 109 lastActivity = MonoTime.currTime; 110 server.clients.add(this); 111 112 this.remoteAddress = remoteAddress; 113 114 server.log("New IRC connection from " ~ remoteAddress.toString); 115 } 116 117 void onReadLine(string line) 118 { 119 try 120 { 121 if (decoder) line = decoder(line); 122 123 if (!connConnected()) 124 return; // A previous line in the same buffer caused a disconnect 125 126 enforce(line.indexOf('\0')<0 && line.indexOf('\r')<0 && line.indexOf('\n')<0, "Forbidden character"); 127 128 auto parameters = line.ircSplit(); 129 if (!parameters.length) 130 return; 131 132 auto command = parameters.shift.toUpper(); 133 onCommand(command, parameters); 134 } 135 catch (CaughtException e) 136 { 137 if (connConnected()) 138 disconnect(e.msg); 139 } 140 } 141 142 void onCommand(string command, scope string[] parameters...) 143 { 144 switch (command) 145 { 146 case "PASS": 147 if (registered) 148 return sendReply(Reply.ERR_ALREADYREGISTRED, "You may not reregister"); 149 if (parameters.length != 1) 150 return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters"); 151 password = parameters[0]; 152 break; 153 case "NICK": 154 if (parameters.length != 1) 155 return sendReply(Reply.ERR_NONICKNAMEGIVEN, "No nickname given"); 156 if (!registered) 157 { 158 nickname = parameters[0]; 159 checkRegistration(); 160 } 161 else 162 { 163 auto newNick = parameters[0]; 164 if (!newNick.match(server.nicknameValidationPattern)) 165 return sendReply(Reply.ERR_ERRONEUSNICKNAME, newNick, "Erroneous nickname"); 166 if (newNick.normalized in server.nicknames) 167 { 168 if (newNick.normalized != nickname.normalized) 169 sendReply(Reply.ERR_NICKNAMEINUSE, newNick, "Nickname is already in use"); 170 return; 171 } 172 173 changeNick(newNick); 174 } 175 break; 176 case "USER": 177 if (registered) 178 return sendReply(Reply.ERR_ALREADYREGISTRED, "You may not reregister"); 179 if (parameters.length != 4) 180 return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters"); 181 username = parameters[0]; 182 hostname = parameters[1]; 183 servername = parameters[2]; 184 realname = parameters[3]; 185 checkRegistration(); 186 break; 187 188 case "PING": 189 if (!registered) 190 return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered"); // KVIrc needs this. 191 sendReply("PONG", parameters); 192 break; 193 case "PONG": 194 break; 195 case "QUIT": 196 if (parameters.length) 197 disconnect("Quit: " ~ parameters[0]); 198 else 199 disconnect("Quit"); 200 break; 201 case "JOIN": 202 if (!registered) 203 return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered"); 204 if (parameters.length < 1) 205 return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters"); 206 string[] keys = parameters.length > 1 ? parameters[1].split(",") : null; 207 foreach (i, channame; parameters[0].split(",")) 208 { 209 auto key = i < keys.length ? keys[i] : null; 210 if (!server.isChannelName(channame)) 211 { sendReply(Reply.ERR_NOSUCHCHANNEL, channame, "No such channel"); continue; } 212 auto normchan = channame.normalized; 213 if (!mayJoin(normchan)) 214 continue; 215 auto pchannel = normchan in server.channels; 216 Channel channel; 217 if (pchannel) 218 channel = *pchannel; 219 else 220 { 221 if (server.staticChannels) 222 { sendReply(Reply.ERR_NOSUCHCHANNEL, channame, "No such channel"); continue; } 223 else 224 channel = server.createChannel(channame); 225 } 226 if (nickname.normalized in channel.members) 227 continue; // already on channel 228 if (channel.modes.strings['k'] && channel.modes.strings['k'] != key) 229 { sendReply(Reply.ERR_BADCHANNELKEY, channame, "Cannot join channel (+k)"); continue; } 230 if (channel.modes.masks['b'].any!(mask => prefix.maskMatch(mask))) 231 { sendReply(Reply.ERR_BANNEDFROMCHAN, channame, "Cannot join channel (+b)"); continue; } 232 join(channel); 233 } 234 lastActivity = MonoTime.currTime; 235 break; 236 case "PART": 237 if (!registered) 238 return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered"); 239 if (parameters.length < 1) // TODO: part reason 240 return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters"); 241 string reason = parameters.length < 2 ? null : parameters[1]; 242 foreach (channame; parameters[0].split(",")) 243 { 244 auto pchan = channame.normalized in server.channels; 245 if (!pchan) 246 { sendReply(Reply.ERR_NOSUCHCHANNEL, channame, "No such channel"); continue; } 247 auto chan = *pchan; 248 if (nickname.normalized !in chan.members) 249 { sendReply(Reply.ERR_NOTONCHANNEL, channame, "You're not on that channel"); continue; } 250 part(chan, reason); 251 } 252 lastActivity = MonoTime.currTime; 253 break; 254 case "MODE": 255 if (!registered) 256 return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered"); 257 if (parameters.length < 1) 258 return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters"); 259 auto target = parameters.shift; 260 if (server.isChannelName(target)) 261 { 262 auto pchannel = target.normalized in server.channels; 263 if (!pchannel) 264 return sendReply(Reply.ERR_NOSUCHNICK, target, "No such nick/channel"); 265 auto channel = *pchannel; 266 auto pmember = nickname.normalized in channel.members; 267 if (!pmember) 268 return sendReply(Reply.ERR_NOTONCHANNEL, target, "You're not on that channel"); 269 if (!parameters.length) 270 return sendChannelModes(channel); 271 return setChannelModes(channel, parameters); 272 } 273 else 274 { 275 auto pclient = target.normalized in server.nicknames; 276 if (!pclient) 277 return sendReply(Reply.ERR_NOSUCHNICK, target, "No such nick/channel"); 278 auto client = *pclient; 279 if (parameters.length) 280 { 281 if (client !is this) 282 return sendReply(Reply.ERR_USERSDONTMATCH, "Cannot change mode for other users"); 283 return setUserModes(parameters); 284 } 285 else 286 return sendUserModes(client); 287 } 288 case "LIST": 289 if (!registered) 290 return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered"); 291 foreach (channel; getChannelList()) 292 if (!(channel.modes.flags['p'] || channel.modes.flags['s']) || nickname.normalized in channel.members) 293 sendReply(Reply.RPL_LIST, channel.name, channel.members.length.text, channel.topic ? channel.topic : ""); 294 sendReply(Reply.RPL_LISTEND, "End of LIST"); 295 break; 296 case "MOTD": 297 if (!registered) 298 return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered"); 299 sendMotd(); 300 break; 301 case "NAMES": 302 if (!registered) 303 return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered"); 304 if (parameters.length < 1) 305 return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters"); 306 foreach (channame; parameters[0].split(",")) 307 { 308 auto pchan = channame.normalized in server.channels; 309 if (!pchan) 310 { sendReply(Reply.ERR_NOSUCHCHANNEL, channame, "No such channel"); continue; } 311 auto channel = *pchan; 312 auto pmember = nickname.normalized in channel.members; 313 if (!pmember) 314 { sendReply(Reply.ERR_NOTONCHANNEL, channame, "You're not on that channel"); continue; } 315 sendNames(channel); 316 } 317 break; 318 case "WHO": 319 { 320 if (!registered) 321 return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered"); 322 auto mask = parameters.length ? parameters[0].among("", "*", "0") ? null : parameters[0] : null; 323 string[string] result; 324 foreach (channel; server.channels) 325 { 326 auto inChannel = nickname.normalized in channel.members; 327 if (!inChannel && channel.modes.flags['s']) 328 continue; 329 foreach (member; channel.members) 330 if (inChannel || !member.client.modes.flags['i']) 331 if (!mask || channel.name.maskMatch(mask) || member.client.nickname.maskMatch(mask) || member.client.hostnameAsVisibleTo(this).maskMatch(mask)) 332 { 333 auto phit = member.client.nickname in result; 334 if (phit) 335 *phit = "*"; 336 else 337 result[member.client.nickname] = channel.name; 338 } 339 } 340 341 foreach (client; server.nicknames) 342 if (!client.modes.flags['i']) 343 if (!mask || client.nickname.maskMatch(mask) || client.hostnameAsVisibleTo(this).maskMatch(mask)) 344 if (client.nickname !in result) 345 result[client.nickname] = "*"; 346 347 foreach (nickname, channel; result) 348 { 349 auto client = server.nicknames[nickname.normalized]; 350 sendReply(Reply.RPL_WHOREPLY, 351 channel, 352 client.username, 353 safeHostname(client.hostnameAsVisibleTo(this)), 354 server.hostname, 355 nickname, 356 "H", 357 "0 " ~ client.realname, 358 ); 359 } 360 sendReply(Reply.RPL_ENDOFWHO, mask ? mask : "*", "End of WHO list"); 361 break; 362 } 363 case "WHOIS": 364 { 365 if (!registered) 366 return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered"); 367 // Contrary to the RFC, and similar to e.g. Freenode, we don't support masks here. 368 if (parameters.length < 1) 369 return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters"); 370 foreach (nick; parameters[0].split(",")) 371 { 372 auto pclient = nick.normalized in server.nicknames; 373 if (!pclient) 374 { sendReply(Reply.ERR_NOSUCHNICK, nick, "No such nick/channel"); continue; } 375 auto client = *pclient; 376 377 // RPL_WHOISUSER 378 sendReply(Reply.RPL_WHOISUSER, 379 client.nickname, 380 client.username, 381 safeHostname(client.hostnameAsVisibleTo(this)), 382 "*", 383 client.realname, 384 ); 385 // RPL_WHOISCHANNELS 386 server.channels.byValue 387 // Channel contents visible? 388 .filter!(channel => !channel.modes.flags['s'] || this.nickname.normalized in channel.members) 389 // Get channel member mode + name if target in channel, or null 390 .map!(channel => (nick.normalized in channel.members).I!(pmember => pmember ? pmember.modeChar() ~ channel.name : null)) 391 .filter!(name => name !is null) 392 .chunks(10) 393 .each!(chunk => sendReply(Reply.RPL_WHOISCHANNELS, client.nickname, chunk.join(" "))); 394 // RPL_WHOISOPERATOR 395 if (client.modes.flags['o']) 396 sendReply(Reply.RPL_WHOISOPERATOR, client.nickname, "is an IRC operator"); 397 // RPL_WHOISIDLE 398 sendReply(Reply.RPL_WHOISIDLE, client.nickname, 399 (MonoTime.currTime - client.lastActivity).total!"seconds".text, 400 "seconds idle"); 401 } 402 // RPL_ENDOFWHOIS 403 sendReply(Reply.RPL_ENDOFWHOIS, parameters[0], "End of WHOIS list"); 404 break; 405 } 406 case "TOPIC": 407 { 408 if (!registered) 409 return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered"); 410 if (parameters.length < 1) 411 return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters"); 412 auto target = parameters.shift; 413 auto pchannel = target.normalized in server.channels; 414 if (!pchannel) 415 return sendReply(Reply.ERR_NOSUCHNICK, target, "No such nick/channel"); 416 auto channel = *pchannel; 417 auto pmember = nickname.normalized in channel.members; 418 if (!pmember) 419 return sendReply(Reply.ERR_NOTONCHANNEL, target, "You're not on that channel"); 420 if (!parameters.length) 421 return sendTopic(channel); 422 if (channel.modes.flags['t'] && (pmember.modes & Channel.Member.Modes.op) == 0) 423 return sendReply(Reply.ERR_CHANOPRIVSNEEDED, target, "You're not channel operator"); 424 return setChannelTopic(channel, parameters[0]); 425 } 426 case "ISON": 427 if (!registered) 428 return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered"); 429 sendReply(Reply.RPL_ISON, parameters.filter!(nick => nick.normalized in server.nicknames).join(" ")); 430 break; 431 case "USERHOST": 432 { 433 if (!registered) 434 return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered"); 435 string[] replies; 436 foreach (nick; parameters) 437 { 438 auto pclient = nick.normalized in server.nicknames; 439 if (!pclient) 440 continue; 441 auto client = *pclient; 442 replies ~= "%s%s=%s%s@%s".format( 443 nick, 444 client.modes.flags['o'] ? "*" : "", 445 client.away ? "+" : "-", 446 client.username, 447 client.hostnameAsVisibleTo(this), 448 ); 449 } 450 sendReply(Reply.RPL_USERHOST, replies.join(" ")); 451 break; 452 } 453 case "LUSERS": 454 sendLusers(); 455 break; 456 case "PRIVMSG": 457 case "NOTICE": 458 { 459 if (!registered) 460 return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered"); 461 if (parameters.length < 2) 462 return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters"); 463 auto message = parameters[1]; 464 if (!message.length) 465 return sendReply(Reply.ERR_NOTEXTTOSEND, command, "No text to send"); 466 foreach (target; parameters[0].split(",")) 467 { 468 if (server.isChannelName(target)) 469 { 470 auto pchannel = target.normalized in server.channels; 471 if (!pchannel) 472 { sendReply(Reply.ERR_NOSUCHNICK, target, "No such nick/channel"); continue; } 473 auto channel = *pchannel; 474 auto pmember = nickname.normalized in channel.members; 475 if (pmember) // On channel? 476 { 477 if (channel.modes.flags['m'] && (pmember.modes & Channel.Member.Modes.bypassM) == 0) 478 { sendReply(Reply.ERR_CANNOTSENDTOCHAN, target, "Cannot send to channel"); continue; } 479 } 480 else 481 { 482 if (channel.modes.flags['n']) // No external messages 483 { sendReply(Reply.ERR_NOTONCHANNEL, target, "You're not on that channel"); continue; } 484 } 485 sendToChannel(channel, command, message); 486 } 487 else 488 { 489 auto pclient = target.normalized in server.nicknames; 490 if (!pclient) 491 { sendReply(Reply.ERR_NOSUCHNICK, target, "No such nick/channel"); continue; } 492 sendToClient(*pclient, command, message); 493 } 494 } 495 lastActivity = MonoTime.currTime; 496 break; 497 } 498 case "OPER": 499 if (!registered) 500 return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered"); 501 if (parameters.length < 1) 502 return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters"); 503 if (!server.operPassword || parameters[$-1] != server.operPassword) 504 return sendReply(Reply.ERR_PASSWDMISMATCH, "Password incorrect"); 505 modes.flags['o'] = true; 506 sendReply(Reply.RPL_YOUREOPER, "You are now an IRC operator"); 507 sendUserModes(this); 508 foreach (channel; server.channels) 509 if (nickname.normalized in channel.members) 510 setChannelMode(channel, nickname, Channel.Member.Mode.op, true); 511 break; 512 513 default: 514 if (registered) 515 return sendReply(Reply.ERR_UNKNOWNCOMMAND, command, "Unknown command"); 516 } 517 } 518 519 final void onInactivity() 520 { 521 sendLine("PING %s".format(Clock.currTime.stdTime)); 522 } 523 524 void disconnect(string why) 525 { 526 if (registered) 527 unregister(why); 528 sendLine("ERROR :Closing Link: %s[%s@%s] (%s)".format(nickname, username, realHostname, why)); 529 connDisconnect(why); 530 } 531 532 void onDisconnect(string reason, DisconnectType type) 533 { 534 if (registered) 535 unregister(reason); 536 server.clients.remove(this); 537 server.log("IRC: %s disconnecting: %s".format(remoteAddress, reason)); 538 } 539 540 void checkRegistration() 541 { 542 assert(!registered); 543 544 if (nickname.length && username.length) 545 { 546 if (server.password && password != server.password) 547 { 548 password = null; 549 return sendReply(Reply.ERR_PASSWDMISMATCH, "Password incorrect"); 550 } 551 if (nickname.normalized in server.nicknames) 552 { 553 scope(exit) nickname = null; 554 return sendReply(Reply.ERR_NICKNAMEINUSE, nickname, "Nickname is already in use"); 555 } 556 if (!nickname.match(server.nicknameValidationPattern)) 557 { 558 scope(exit) nickname = null; 559 return sendReply(Reply.ERR_ERRONEUSNICKNAME, nickname, "Erroneous nickname"); 560 } 561 if (!username.match(`[a-zA-Z]+`)) 562 return disconnect("Invalid username"); 563 564 // All OK 565 register(); 566 } 567 } 568 569 void register() 570 { 571 if (!identified) 572 username = "~" ~ username; 573 update(); 574 575 registered = true; 576 server.nicknames[nickname.normalized] = this; 577 auto userCount = server.nicknames.length; 578 if (server.maxUsers < userCount) 579 server.maxUsers = userCount; 580 sendReply(Reply.RPL_WELCOME , "Welcome, %s!".format(nickname)); 581 sendReply(Reply.RPL_YOURHOST , "Your host is %s, running %s".format(server.hostname, server.serverVersion)); 582 sendReply(Reply.RPL_CREATED , "This server was created %s".format(server.creationTime)); 583 sendReply(Reply.RPL_MYINFO , server.hostname, server.serverVersion, UserModes.supported, ChannelModes.supported, null); 584 sendReply(cast(Reply) 005, server.capabilities ~ ["are supported by this server"]); 585 sendLusers(); 586 sendReply(cast(Reply) 265, "Current local users: %d Max: %d".format(userCount, server.maxUsers)); 587 sendReply(cast(Reply) 266, "Current global users: %d Max: %d".format(userCount, server.maxUsers)); 588 sendReply(cast(Reply) 250, "Highest connection count: %d (%d clients) (%d since server was (re)started)".format(server.maxUsers, server.maxUsers, server.totalConnections)); 589 sendMotd(); 590 } 591 592 void update() 593 { 594 prefix = "%s!%s@%s".format(nickname, username, realHostname ); 595 publicPrefix = "%s!%s@%s".format(nickname, username, publicHostname); 596 } 597 598 void unregister(string why) 599 { 600 assert(registered); 601 auto channels = getJoinedChannels(); 602 foreach (channel; channels) 603 channel.remove(this); 604 foreach (client; server.allClientsInChannels(channels)) 605 client.sendCommand(this, "QUIT", why); 606 server.nicknames.remove(nickname.normalized); 607 registered = false; 608 } 609 610 void changeNick(string newNick) 611 { 612 auto channels = getJoinedChannels(); 613 auto witnesses = server.whoCanSee(this); 614 615 foreach (channel; channels) 616 { 617 auto pmember = nickname.normalized in channel.members; 618 assert(pmember); 619 auto member = *pmember; 620 channel.members.remove(nickname.normalized); 621 channel.members[newNick.normalized] = member; 622 } 623 624 foreach (client; witnesses) 625 client.sendCommand(this, "NICK", newNick, null); 626 627 server.nicknames.remove(nickname.normalized); 628 server.nicknames[newNick.normalized] = this; 629 630 nickname = newNick; 631 update(); 632 } 633 634 void sendMotd() 635 { 636 sendReply(Reply.RPL_MOTDSTART , "- %s Message of the Day - ".format(server.hostname)); 637 foreach (line; server.motd) 638 sendReply(Reply.RPL_MOTD, "- %s".format(line)); 639 sendReply(Reply.RPL_ENDOFMOTD , "End of /MOTD command."); 640 } 641 642 void sendLusers() 643 { 644 sendReply(Reply.RPL_LUSERCLIENT , "There are %d users and %d services on %d servers".format( 645 server.clients.byKey.filter!(client => cast(NetworkClient)client && client.registered).walkLength, 646 server.clients.byKey.filter!(client => !cast(NetworkClient)client).walkLength, 647 1, 648 )); 649 sendReply(Reply.RPL_LUSEROP , server.clients.byKey.filter!(client => client.modes.flags['o']).walkLength.text, "IRC Operators online"); 650 sendReply(Reply.RPL_LUSERUNKNOWN , server.clients.byKey.filter!(client => cast(NetworkClient)client && !client.registered).walkLength.text, "unknown connection(s)"); 651 sendReply(Reply.RPL_LUSERCHANNELS, server.channels.length.text, "channels formed"); 652 sendReply(Reply.RPL_LUSERME , "I have %d clients and %d servers".format( 653 server.clients.byKey.filter!(client => client.registered).walkLength, 654 0, 655 )); 656 } 657 658 bool mayJoin(string name) 659 { 660 return true; 661 } 662 663 void join(Channel channel) 664 { 665 channel.add(this); 666 foreach (member; channel.members) 667 member.client.sendCommand(this, "JOIN", channel.name); 668 sendTopic(channel); 669 sendNames(channel); 670 auto pmember = nickname.normalized in channel.members; 671 // Sync OPER status with (initial) channel op status 672 if (server.staticChannels || modes.flags['o']) 673 setChannelMode(channel, nickname, Channel.Member.Mode.op, modes.flags['o']); 674 } 675 676 // For server-imposed mode changes. 677 void setChannelMode(Channel channel, string nickname, Channel.Member.Mode mode, bool value) 678 { 679 auto pmember = nickname.normalized in channel.members; 680 if (pmember.modeSet(mode) == value) 681 return; 682 683 pmember.setMode(mode, value); 684 auto c = ChannelModes.memberModeChars[mode]; 685 foreach (member; channel.members) 686 member.client.sendCommand(server.hostname, "MODE", channel.name, [value ? '+' : '-', c], nickname, null); 687 server.channelChanged(channel); 688 } 689 690 void part(Channel channel, string reason=null) 691 { 692 foreach (member; channel.members) 693 member.client.sendCommand(this, "PART", channel.name, reason); 694 channel.remove(this); 695 } 696 697 void sendToChannel(Channel channel, string command, string message) 698 { 699 foreach (member; channel.members) 700 if (member.client !is this) 701 member.client.sendCommand(this, command, channel.name, message); 702 } 703 704 void sendToClient(Client client, string command, string message) 705 { 706 client.sendCommand(this, command, client.nickname, message); 707 } 708 709 void sendTopic(Channel channel) 710 { 711 if (channel.topic) 712 sendReply(Reply.RPL_TOPIC, channel.name, channel.topic); 713 else 714 sendReply(Reply.RPL_NOTOPIC, channel.name, "No topic is set"); 715 } 716 717 void sendNames(Channel channel) 718 { 719 foreach (chunk; channel.members.values.chunks(10)) // can't use byValue - https://issues.dlang.org/show_bug.cgi?id=11761 720 sendReply(Reply.RPL_NAMREPLY, channel.modes.flags['s'] ? "@" : channel.modes.flags['p'] ? "*" : "=", channel.name, chunk.map!q{a.displayName}.join(" ")); 721 sendReply(Reply.RPL_ENDOFNAMES, channel.name, "End of /NAMES list"); 722 } 723 724 /// For LIST 725 Channel[] getChannelList() 726 { 727 return server.channels.values; 728 } 729 730 void sendChannelModes(Channel channel) 731 { 732 string modes = "+"; 733 string[] modeParams; 734 foreach (char c; 0..char.max) 735 final switch (ChannelModes.modeTypes[c]) 736 { 737 case ChannelModes.Type.none: 738 case ChannelModes.Type.member: 739 break; 740 case ChannelModes.Type.mask: 741 // sent after RPL_CHANNELMODEIS 742 break; 743 case ChannelModes.Type.flag: 744 if (channel.modes.flags[c]) 745 modes ~= c; 746 break; 747 case ChannelModes.Type.str: 748 if (channel.modes.strings[c]) 749 { 750 modes ~= c; 751 auto value = channel.modes.strings[c]; 752 assert(value.length, "Empty string channel parameter: " ~ c); 753 modeParams ~= value; 754 } 755 break; 756 case ChannelModes.Type.number: 757 if (channel.modes.numbers[c]) 758 { 759 modes ~= c; 760 modeParams ~= channel.modes.numbers[c].text; 761 } 762 break; 763 } 764 sendReply(Reply.RPL_CHANNELMODEIS, [channel.name, modes] ~ modeParams ~ [string.init]); 765 } 766 767 void sendChannelModeMasks(Channel channel, char mode) 768 { 769 switch (mode) 770 { 771 case 'b': 772 sendChannelMaskList(channel, channel.modes.masks[mode], Reply.RPL_BANLIST, Reply.RPL_ENDOFBANLIST, "End of channel ban list"); 773 break; 774 default: 775 assert(false); 776 } 777 } 778 779 void sendChannelMaskList(Channel channel, string[] masks, Reply lineReply, Reply endReply, string endText) 780 { 781 foreach (mask; masks) 782 sendReply(lineReply, channel.name, mask, null); 783 sendReply(endReply, channel.name, endText); 784 } 785 786 void setChannelTopic(Channel channel, string topic) 787 { 788 channel.topic = topic; 789 foreach (ref member; channel.members) 790 member.client.sendCommand(this, "TOPIC", channel.name, topic); 791 server.channelChanged(channel); 792 } 793 794 void setChannelModes(Channel channel, string[] modes) 795 { 796 auto pself = nickname.normalized in channel.members; 797 bool op = (pself.modes & Channel.Member.Modes.op) != 0; 798 799 string[2] effectedChars; 800 string[][2] effectedParams; 801 802 scope(exit) // Broadcast effected options 803 { 804 string[] parameters; 805 foreach (adding; 0..2) 806 if (effectedChars[adding].length) 807 parameters ~= [(adding ? "+" : "-") ~ effectedChars[adding]] ~ effectedParams[adding]; 808 if (parameters.length) 809 { 810 assert(op); 811 parameters = ["MODE", channel.name] ~ parameters ~ [string.init]; 812 foreach (ref member; channel.members) 813 member.client.sendCommand(this, parameters); 814 } 815 } 816 817 while (modes.length) 818 { 819 auto chars = modes.shift; 820 821 bool adding = true; 822 foreach (c; chars) 823 if (c == '+') 824 adding = true; 825 else 826 if (c == '-') 827 adding = false; 828 else 829 final switch (ChannelModes.modeTypes[c]) 830 { 831 case ChannelModes.Type.none: 832 sendReply(Reply.ERR_UNKNOWNMODE, [c], "is unknown mode char to me for %s".format(channel.name)); 833 break; 834 case ChannelModes.Type.flag: 835 if (!op) return sendReply(Reply.ERR_CHANOPRIVSNEEDED, channel.name, "You're not channel operator"); 836 if (adding != channel.modes.flags[c]) 837 { 838 channel.modes.flags[c] = adding; 839 effectedChars[adding] ~= c; 840 } 841 break; 842 case ChannelModes.Type.member: 843 { 844 if (!op) return sendReply(Reply.ERR_CHANOPRIVSNEEDED, channel.name, "You're not channel operator"); 845 if (!modes.length) 846 { sendReply(Reply.ERR_NEEDMOREPARAMS, "MODE", "Not enough parameters"); continue; } 847 auto memberName = modes.shift; 848 auto pmember = memberName.normalized in channel.members; 849 if (!pmember) 850 { sendReply(Reply.ERR_USERNOTINCHANNEL, memberName, channel.name, "They aren't on that channel"); continue; } 851 auto mode = ChannelModes.memberModes[c]; 852 if (pmember.modeSet(mode) != adding) 853 { 854 pmember.setMode(mode, adding); 855 effectedChars[adding] ~= c; 856 effectedParams[adding] ~= memberName; 857 } 858 break; 859 } 860 case ChannelModes.Type.mask: 861 { 862 if (!modes.length) 863 return sendChannelModeMasks(channel, c); 864 if (!op) return sendReply(Reply.ERR_CHANOPRIVSNEEDED, channel.name, "You're not channel operator"); 865 auto mask = modes.shift; 866 if (adding) 867 { 868 if (channel.modes.masks[c].canFind(mask)) 869 continue; 870 channel.modes.masks[c] ~= mask; 871 } 872 else 873 { 874 auto index = channel.modes.masks[c].countUntil(mask); 875 if (index < 0) 876 continue; 877 channel.modes.masks[c] = channel.modes.masks[c][0..index] ~ channel.modes.masks[c][index+1..$]; 878 } 879 effectedChars[adding] ~= c; 880 effectedParams[adding] ~= mask; 881 break; 882 } 883 case ChannelModes.Type.str: 884 if (!op) return sendReply(Reply.ERR_CHANOPRIVSNEEDED, channel.name, "You're not channel operator"); 885 if (adding) 886 { 887 if (!modes.length) 888 { sendReply(Reply.ERR_NEEDMOREPARAMS, "MODE", "Not enough parameters"); continue; } 889 auto str = modes.shift; 890 if (channel.modes.strings[c] == str) 891 continue; 892 channel.modes.strings[c] = str; 893 effectedChars[adding] ~= c; 894 effectedParams[adding] ~= str; 895 } 896 else 897 { 898 if (!channel.modes.strings[c]) 899 continue; 900 channel.modes.strings[c] = null; 901 effectedChars[adding] ~= c; 902 } 903 break; 904 case ChannelModes.Type.number: 905 if (!op) return sendReply(Reply.ERR_CHANOPRIVSNEEDED, channel.name, "You're not channel operator"); 906 if (adding) 907 { 908 if (!modes.length) 909 { sendReply(Reply.ERR_NEEDMOREPARAMS, "MODE", "Not enough parameters"); continue; } 910 auto numText = modes.shift; 911 auto num = numText.to!long; 912 if (channel.modes.numbers[c] == num) 913 continue; 914 channel.modes.numbers[c] = num; 915 effectedChars[adding] ~= c; 916 effectedParams[adding] ~= numText; 917 } 918 else 919 { 920 if (!channel.modes.numbers[c]) 921 continue; 922 channel.modes.numbers[c] = 0; 923 effectedChars[adding] ~= c; 924 } 925 break; 926 } 927 } 928 server.channelChanged(channel); 929 } 930 931 void setUserModes(string[] modes) 932 { 933 while (modes.length) 934 { 935 auto chars = modes.shift; 936 937 bool adding = true; 938 foreach (c; chars) 939 if (c == '+') 940 adding = true; 941 else 942 if (c == '-') 943 adding = false; 944 else 945 final switch (UserModes.modeTypes[c]) 946 { 947 case UserModes.Type.none: 948 sendReply(Reply.ERR_UMODEUNKNOWNFLAG, "Unknown MODE flag"); 949 break; 950 case UserModes.Type.flag: 951 if (UserModes.isSettable[c]) 952 this.modes.flags[c] = adding; 953 break; 954 } 955 } 956 } 957 958 void sendUserModes(Client client) 959 { 960 string modeString = "+"; 961 foreach (char c, on; modes.flags) 962 if (on) 963 modeString ~= c; 964 return sendReply(Reply.RPL_UMODEIS, modeString, null); 965 } 966 967 void sendCommand(Client from, string[] parameters...) 968 { 969 return sendCommand(from.prefixAsVisibleTo(this), parameters); 970 } 971 972 void sendCommand(string from, string[] parameters...) 973 { 974 assert(parameters.length, "At least one parameter expected"); 975 foreach (parameter; parameters[0..$-1]) 976 assert(parameter.length && parameter[0] != ':' && parameter.indexOf(' ') < 0, "Invalid parameter: " ~ parameter); 977 if (parameters[$-1] is null) 978 parameters = parameters[0..$-1]; 979 else 980 parameters = parameters[0..$-1] ~ [":" ~ parameters[$-1]]; 981 auto line = ":%s %-(%s %)".format(from, parameters); 982 sendLine(line); 983 } 984 985 void sendReply(Reply reply, string[] parameters...) 986 { 987 return sendReply("%03d".format(reply), parameters); 988 } 989 990 void sendReply(string command, string[] parameters...) 991 { 992 return sendCommand(server.hostname, [command, nickname ? nickname : "*"] ~ parameters); 993 } 994 995 void sendServerNotice(string text) 996 { 997 sendReply("NOTICE", "*** Notice -- " ~ text); 998 } 999 1000 void sendLine(string line) 1001 { 1002 if (encoder) line = encoder(line); 1003 connSendLine(line); 1004 } 1005 1006 abstract bool connConnected(); 1007 abstract void connSendLine(string line); 1008 abstract void connDisconnect(string reason); 1009 } 1010 1011 /// `Client` implementation backed by a real network connection. 1012 static class NetworkClient : Client 1013 { 1014 protected: 1015 IrcConnection conn; 1016 1017 this(IrcServer server, IrcConnection incoming, Address remoteAddress) 1018 { 1019 super(server, remoteAddress); 1020 1021 conn = incoming; 1022 conn.handleReadLine = &onReadLine; 1023 conn.handleInactivity = &onInactivity; 1024 conn.handleDisconnect = &onDisconnect; 1025 } 1026 1027 override bool connConnected() 1028 { 1029 return conn.state == ConnectionState.connected; 1030 } 1031 1032 override void connSendLine(string line) 1033 { 1034 conn.send(line); 1035 } 1036 1037 override void connDisconnect(string reason) 1038 { 1039 conn.disconnect(reason); 1040 } 1041 } 1042 1043 HashSet!Client clients; /// All clients 1044 Client[string] nicknames; /// Registered clients only 1045 1046 /// Statistics 1047 ulong maxUsers, totalConnections; 1048 1049 /// IRC channel information. 1050 final class Channel 1051 { 1052 string name; /// Channel name (including any leading `'#'`). 1053 string topic; /// Channel topic, if any. 1054 1055 Modes modes; /// Channel modes. 1056 1057 /// Channel member (entry for a user who is in the channel). 1058 struct Member 1059 { 1060 /// A mode that a user may or may not have when in a channel. 1061 enum Mode 1062 { 1063 op, /// Channel operator. Can change channel properties. 1064 voice, /// Has voice. May speak even when banned or the channel is moderated. 1065 } 1066 1067 /// Bitmask for modes that a user has in the channel. 1068 enum Modes 1069 { 1070 none = 0, /// 1071 op = 1 << Mode.op, /// 1072 voice = 1 << Mode.voice, /// 1073 1074 bypassM = op | voice, /// Modes which bypass +m. 1075 } 1076 1077 Client client; /// 1078 Modes modes; /// 1079 1080 /// Does this member have the given mode? 1081 bool modeSet(Mode mode) { return (modes & (1 << mode)) != 0; } 1082 void setMode(Mode mode, bool value) 1083 { 1084 auto modeMask = 1 << mode; 1085 if (value) 1086 modes |= modeMask; 1087 else 1088 modes &= ~modeMask; 1089 } /// Set (enable or disable) the given mode for this channel member. 1090 1091 /// Returns the character used to indicate the user's highest channel mode 1092 /// (e.g. `'@'` or `'+'`). 1093 string modeChar() 1094 { 1095 foreach (mode; Mode.init..enumLength!Mode) 1096 if ((1 << mode) & modes) 1097 return [ChannelModes.memberModePrefixes[mode]]; 1098 return ""; 1099 } 1100 /// Returns this member's name as it would appear in a RPL_NAMREPLY listing, 1101 /// i.e. `modeChar` plus nickname. 1102 string displayName() { return modeChar ~ client.nickname; } 1103 } 1104 1105 Member[string] members; /// Channel members. The key is the normalized nickname. 1106 1107 this(string name) 1108 { 1109 this.name = name; 1110 modes.flags['t'] = modes.flags['n'] = true; 1111 } /// 1112 1113 void add(Client client) 1114 { 1115 auto modes = staticChannels || members.length ? Member.Modes.none : Member.Modes.op; 1116 members[client.nickname.normalized] = Member(client, modes); 1117 } /// 1118 1119 void remove(Client client) 1120 { 1121 members.remove(client.nickname.normalized); 1122 if (!staticChannels && !members.length && !modes.flags['P']) 1123 channels.remove(name.normalized); 1124 } /// 1125 } 1126 1127 Channel[string] channels; /// All channels on this server. 1128 1129 TcpServer conn; /// Listening socket. 1130 1131 this() 1132 { 1133 conn = new TcpServer; 1134 conn.handleAccept = &onAccept; 1135 1136 hostname = Socket.hostName; 1137 creationTime = Clock.currTime; 1138 } /// 1139 1140 /// Listen on the given address. 1141 /// If port is 0, listen on a random available port. 1142 /// Returns the actual listening port. 1143 ushort listen(ushort port=6667, string addr = null) 1144 { 1145 port = conn.listen(port, addr); 1146 return port; 1147 } 1148 1149 /// Creates a new channel. The default modes are "+nt". 1150 Channel createChannel(string name) 1151 { 1152 return channels[name.normalized] = new Channel(name); 1153 } 1154 1155 /// Stop listening and disconnect all clients. 1156 void close(string reason) 1157 { 1158 conn.close(); 1159 foreach (client; clients.keys) 1160 client.disconnect("Server is shutting down" ~ (reason.length ? ": " ~ reason : "")); 1161 } 1162 1163 protected: 1164 Client createClient(TcpConnection incoming) 1165 { 1166 return new NetworkClient(this, new IrcConnection(incoming), incoming.remoteAddress); 1167 } 1168 1169 void onAccept(TcpConnection incoming) 1170 { 1171 createClient(incoming); 1172 totalConnections++; 1173 } 1174 1175 Client[string] allClientsInChannels(Channel[] channels) 1176 { 1177 Client[string] result; 1178 foreach (channel; channels) 1179 foreach (ref member; channel.members) 1180 result[member.client.nickname.normalized] = member.client; 1181 return result; 1182 } 1183 1184 /// Clients who can see the given client (are in the same channer). 1185 /// Includes the target client himself. 1186 Client[string] whoCanSee(Client who) 1187 { 1188 auto clients = allClientsInChannels(who.getJoinedChannels()); 1189 clients[who.nickname.normalized] = who; 1190 return clients; 1191 } 1192 1193 bool isChannelName(string target) 1194 { 1195 foreach (prefix; chanTypes) 1196 if (target.startsWith(prefix)) 1197 return true; 1198 return false; 1199 } 1200 1201 string[] capabilities() 1202 { 1203 string[] result; 1204 result ~= "PREFIX=(%s)%s".format(ChannelModes.memberModeChars, ChannelModes.memberModePrefixes); 1205 result ~= "CHANTYPES=" ~ chanTypes; 1206 result ~= "CHANMODES=%-(%s,%)".format( 1207 [ChannelModes.Type.mask, ChannelModes.Type.str, ChannelModes.Type.number, ChannelModes.Type.flag].map!(type => ChannelModes.byType(type)) 1208 ); 1209 if (network) 1210 result ~= "NETWORK=" ~ network; 1211 result ~= "CASEMAPPING=rfc1459"; 1212 result ~= "NICKLEN=" ~ text(nicknameMaxLength); 1213 return result; 1214 } 1215 1216 /// Persistence hook 1217 void channelChanged(Channel channel) 1218 { 1219 } 1220 } 1221 1222 /// Check if the given string matches the given mask 1223 /// (e.g. when enforcing +b modes). 1224 bool maskMatch(string subject, string mask) 1225 { 1226 import std.path; 1227 return globMatch!(CaseSensitive.no)(subject, mask); 1228 } 1229 1230 /// Encode a host name to be sent in a WHO / WHOIS reply. 1231 string safeHostname(string s) 1232 { 1233 assert(s.length); 1234 if (s[0] == ':') 1235 s = '0' ~ s; 1236 return s; 1237 } 1238 1239 /// The method used when normalizing user and channel names for lookup. 1240 alias rfc1459toUpper normalized; 1241 1242 /// Split an IRC line into parameters. 1243 string[] ircSplit(string line) 1244 { 1245 auto colon = line.indexOf(":"); 1246 if (colon < 0) 1247 return line.split; 1248 else 1249 return line[0..colon].strip.split ~ [line[colon+1..$]]; 1250 } 1251 1252 /// Represents channel modes. 1253 struct Modes 1254 { 1255 /// A mode may be a flag (e.g. +t), a string (e.g. +k), a number (e.g. +l), or a list of strings (generally masks, e.g. +b). 1256 bool[char.max] flags; 1257 string[char.max] strings; /// ditto 1258 long[char.max] numbers; /// ditto 1259 string[][char.max] masks; /// ditto 1260 } 1261 1262 /// Common declarations for `ChannelModes` and `UserModes`. 1263 mixin template CommonModes() 1264 { 1265 //static immutable: 1266 /// The type of the given mode character. 1267 Type[char.max] modeTypes; 1268 /// List of modes supported by this module. 1269 string supported() pure { return modeTypes.length.iota.filter!(m => modeTypes[m] ).map!(m => cast(char)m).array; } 1270 /// List of modes of the given type supported by this module. 1271 string byType(Type type) pure { return modeTypes.length.iota.filter!(m => modeTypes[m] == type).map!(m => cast(char)m).array; } 1272 } 1273 1274 /// Encodes static information about channel modes supported by this module. 1275 struct ChannelModes 1276 { 1277 static immutable: 1278 enum Type 1279 { 1280 none, /// 1281 flag, /// 1282 member, /// 1283 mask, /// 1284 str, /// 1285 number, /// 1286 } /// Mode types. 1287 mixin CommonModes; 1288 IrcServer.Channel.Member.Mode[char.max] memberModes; /// Mappings from channel to member modes. 1289 /// ditto 1290 char[enumLength!(IrcServer.Channel.Member.Mode)] memberModeChars, memberModePrefixes; 1291 1292 shared static this() 1293 { 1294 foreach (c; "ntpsP") 1295 modeTypes[c] = Type.flag; 1296 foreach (c; "ov") 1297 modeTypes[c] = Type.member; 1298 foreach (c; "b") 1299 modeTypes[c] = Type.mask; 1300 foreach (c; "k") 1301 modeTypes[c] = Type.str; 1302 1303 memberModes['o'] = IrcServer.Channel.Member.Mode.op; 1304 memberModes['v'] = IrcServer.Channel.Member.Mode.voice; 1305 1306 memberModeChars = "ov"; 1307 memberModePrefixes = "@+"; 1308 } 1309 } 1310 1311 /// Encodes static information about user modes supported by this module. 1312 struct UserModes 1313 { 1314 static immutable: 1315 enum Type 1316 { 1317 none, /// 1318 flag, /// 1319 } /// Mode types. 1320 mixin CommonModes; 1321 bool[char.max] isSettable; /// Can users change this mode for themselves? 1322 1323 shared static this() 1324 { 1325 foreach (c; "io") 1326 modeTypes[c] = Type.flag; 1327 foreach (c; "i") 1328 isSettable[c] = true; 1329 } 1330 }