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 <vladimir@thecybershadow.net> 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 29 import ae.net.irc.common; 30 31 alias std..string.indexOf indexOf; 32 33 class IrcServer 34 { 35 // This class is currently intentionally written for readability, not performance. 36 // Performance and scalability could be greatly improved by using numeric indices for users and channels 37 // instead of associative arrays. 38 39 /// Server configuration 40 string hostname, password, network; 41 string nicknameValidationPattern = "^[a-zA-Z][a-zA-Z0-9\\-`\\|]{0,14}$"; 42 string serverVersion = "ae.net.irc.server"; 43 string[] motd; 44 string chanTypes = "#&"; 45 SysTime creationTime; 46 string operPassword; 47 48 /// Channels can't be created by users, and don't disappear when they're empty 49 bool staticChannels; 50 /// If set, masks all IPs to the given mask 51 string addressMask; 52 53 Logger log; 54 55 /// Client connection and information 56 final static class Client 57 { 58 /// Registration details 59 string nickname, password; 60 string username, hostname, servername, realname; 61 bool identified; 62 string prefix, publicPrefix; /// Full nick!user@host 63 string away; 64 65 bool registered; 66 bool[char.max] modeFlags; 67 68 Channel[] joinedChannels() 69 { 70 Channel[] result; 71 foreach (channel; server.channels) 72 if (nickname.normalized in channel.members) 73 result ~= channel; 74 return result; 75 } 76 77 string realHostname() { return conn.remoteAddress.toAddrString; } 78 string publicHostname() { return server.addressMask ? server.addressMask : realHostname; } 79 80 private: 81 IrcServer server; 82 IrcSocket conn; 83 84 this(IrcServer server, IrcSocket incoming) 85 { 86 this.server = server; 87 88 conn = incoming; 89 conn.handleReadLine = &onReadLine; 90 conn.handleInactivity = &onInactivity; 91 conn.handleDisconnect = &onDisconnect; 92 93 server.log("New IRC connection from " ~ incoming.remoteAddress.toString); 94 } 95 96 void onReadLine(LineBufferedSocket sender, string line) 97 { 98 try 99 { 100 enforce(line.indexOf('\0')<0 && line.indexOf('\r')<0 && line.indexOf('\n')<0, "Forbidden character"); 101 102 auto parameters = line.strip.ircSplit(); 103 if (!parameters.length) 104 return; 105 106 auto command = parameters.shift.toUpper(); 107 108 switch (command) 109 { 110 case "PASS": 111 if (registered) 112 return sendReply(Reply.ERR_ALREADYREGISTRED, "You may not reregister"); 113 if (parameters.length != 1) 114 return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters"); 115 password = parameters[0]; 116 break; 117 case "NICK": 118 if (registered) 119 return sendServerNotice("Nick change not allowed."); // TODO. NB: update prefix, nicknames and Channel.members keys 120 if (parameters.length != 1) 121 return sendReply(Reply.ERR_NONICKNAMEGIVEN, "No nickname given"); 122 nickname = parameters[0]; 123 checkRegistration(); 124 break; 125 case "USER": 126 if (registered) 127 return sendReply(Reply.ERR_ALREADYREGISTRED, "You may not reregister"); 128 if (parameters.length != 4) 129 return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters"); 130 username = parameters[0]; 131 hostname = parameters[1]; 132 servername = parameters[2]; 133 realname = parameters[3]; 134 checkRegistration(); 135 break; 136 137 case "PING": 138 sendReply("PONG", parameters); 139 break; 140 case "PONG": 141 break; 142 case "QUIT": 143 if (parameters.length) 144 disconnect("Quit: " ~ parameters[0]); 145 else 146 disconnect("Quit"); 147 break; 148 case "JOIN": 149 if (!registered) 150 return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered"); 151 if (parameters.length < 1) 152 return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters"); 153 string[] keys = parameters.length > 1 ? parameters[1].split(",") : null; 154 foreach (i, channame; parameters[0].split(",")) 155 { 156 auto key = i < keys.length ? keys[i] : null; 157 if (!server.isChannelName(channame)) 158 { sendReply(Reply.ERR_NOSUCHCHANNEL, channame, "No such channel"); continue; } 159 auto normchan = channame.normalized; 160 auto pchannel = normchan in server.channels; 161 Channel channel; 162 if (pchannel) 163 channel = *pchannel; 164 else 165 { 166 if (server.staticChannels) 167 { sendReply(Reply.ERR_NOSUCHCHANNEL, channame, "No such channel"); continue; } 168 else 169 channel = server.createChannel(channame); 170 } 171 if (nickname.normalized in channel.members) 172 continue; // already on channel 173 if (channel.modeStrings['k'] && channel.modeStrings['k'] != key) 174 { sendReply(Reply.ERR_BADCHANNELKEY, channame, "Cannot join channel (+k)"); continue; } 175 if (channel.modeMasks['b'].any!(mask => prefix.maskMatch(mask))) 176 { sendReply(Reply.ERR_BANNEDFROMCHAN, channame, "Cannot join channel (+b)"); continue; } 177 join(channel); 178 } 179 break; 180 case "PART": 181 if (!registered) 182 return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered"); 183 if (parameters.length < 1) // TODO: part reason 184 return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters"); 185 foreach (channame; parameters[0].split(",")) 186 { 187 auto pchan = channame.normalized in server.channels; 188 if (!pchan) 189 { sendReply(Reply.ERR_NOSUCHCHANNEL, channame, "No such channel"); continue; } 190 auto chan = *pchan; 191 if (nickname.normalized !in chan.members) 192 { sendReply(Reply.ERR_NOTONCHANNEL, channame, "You're not on that channel"); continue; } 193 part(chan); 194 } 195 break; 196 case "MODE": 197 if (!registered) 198 return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered"); 199 if (parameters.length < 1) 200 return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters"); 201 auto target = parameters.shift; 202 if (server.isChannelName(target)) 203 { 204 auto pchannel = target.normalized in server.channels; 205 if (!pchannel) 206 return sendReply(Reply.ERR_NOSUCHNICK, target, "No such nick/channel"); 207 auto channel = *pchannel; 208 auto pmember = nickname.normalized in channel.members; 209 if (!pmember) 210 return sendReply(Reply.ERR_NOTONCHANNEL, target, "You're not on that channel"); 211 if (!parameters.length) 212 return sendChannelModes(channel); 213 if ((pmember.modes & Channel.Member.Modes.op) == 0) 214 return sendReply(Reply.ERR_CHANOPRIVSNEEDED, target, "You're not channel operator"); 215 return setChannelModes(channel, parameters); 216 } 217 else 218 { 219 auto pclient = target.normalized in server.nicknames; 220 if (!pclient) 221 return sendReply(Reply.ERR_NOSUCHNICK, target, "No such nick/channel"); 222 auto client = *pclient; 223 if (parameters.length) 224 { 225 if (client !is this) 226 return sendReply(Reply.ERR_USERSDONTMATCH, "Cannot change mode for other users"); 227 return setUserModes(parameters); 228 } 229 else 230 return sendUserModes(client); 231 } 232 case "LIST": 233 if (!registered) 234 return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered"); 235 foreach (channel; server.channels) 236 if (!(channel.modeFlags['p'] || channel.modeFlags['s']) || nickname.normalized in channel.members) 237 sendReply(Reply.RPL_LIST, channel.name, channel.members.length.text, channel.topic ? channel.topic : ""); 238 sendReply(Reply.RPL_LISTEND, "End of LIST"); 239 break; 240 case "WHO": 241 { 242 if (!registered) 243 return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered"); 244 auto mask = parameters.length ? parameters[0] != "*" && parameters[0] != "0" ? parameters[0] : null : null; 245 string[string] result; 246 foreach (channel; server.channels) 247 { 248 auto inChannel = nickname.normalized in channel.members; 249 if (!inChannel && channel.modeFlags['s']) 250 continue; 251 foreach (member; channel.members) 252 if (inChannel || !member.client.modeFlags['i']) 253 if (!mask || member.client.publicPrefix.maskMatch(mask)) 254 { 255 auto phit = member.client.nickname in result; 256 if (phit) 257 *phit = "*"; 258 else 259 result[member.client.nickname] = channel.name; 260 } 261 } 262 263 foreach (client; server.nicknames) 264 if (!client.modeFlags['i']) 265 if (!mask || client.publicPrefix.maskMatch(mask)) 266 if (client.nickname !in result) 267 result[client.nickname] = "*"; 268 269 foreach (nickname, channel; result) 270 { 271 auto client = server.nicknames[nickname.normalized]; 272 sendReply(Reply.RPL_WHOREPLY, 273 channel, 274 client.username, 275 this is client ? client.realHostname : client.publicHostname, 276 server.hostname, 277 nickname, 278 "H", 279 "0 " ~ client.realname, 280 ); 281 } 282 sendReply(Reply.RPL_ENDOFWHO, mask ? mask : "*", "End of WHO list"); 283 break; 284 } 285 case "TOPIC": 286 { 287 if (!registered) 288 return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered"); 289 if (parameters.length < 1) 290 return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters"); 291 auto target = parameters.shift; 292 auto pchannel = target.normalized in server.channels; 293 if (!pchannel) 294 return sendReply(Reply.ERR_NOSUCHNICK, target, "No such nick/channel"); 295 auto channel = *pchannel; 296 auto pmember = nickname.normalized in channel.members; 297 if (!pmember) 298 return sendReply(Reply.ERR_NOTONCHANNEL, target, "You're not on that channel"); 299 if (!parameters.length) 300 return sendTopic(channel); 301 if (channel.modeFlags['t'] && (pmember.modes & Channel.Member.Modes.op) == 0) 302 return sendReply(Reply.ERR_CHANOPRIVSNEEDED, target, "You're not channel operator"); 303 return setChannelTopic(channel, parameters[0]); 304 } 305 case "ISON": 306 if (!registered) 307 return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered"); 308 sendReply(Reply.RPL_ISON, parameters.filter!(nick => nick.normalized in server.nicknames).join(" ")); 309 break; 310 case "USERHOST": 311 { 312 if (!registered) 313 return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered"); 314 string[] replies; 315 foreach (nick; parameters) 316 { 317 auto pclient = nick.normalized in server.nicknames; 318 if (!pclient) 319 continue; 320 auto client = *pclient; 321 replies ~= "%s%s=%s%s@%s".format( 322 nick, 323 client.modeFlags['o'] ? "*" : "", 324 client.away ? "+" : "-", 325 client.username, 326 this is client ? client.realHostname : client.publicHostname, 327 ); 328 } 329 sendReply(Reply.RPL_USERHOST, replies.join(" ")); 330 break; 331 } 332 case "PRIVMSG": 333 case "NOTICE": 334 if (!registered) 335 return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered"); 336 if (parameters.length < 2) 337 return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters"); 338 auto message = parameters[1]; 339 foreach (target; parameters[0].split(",")) 340 { 341 if (server.isChannelName(target)) 342 { 343 auto pchannel = target.normalized in server.channels; 344 if (!pchannel) 345 { sendReply(Reply.ERR_NOSUCHNICK, target, "No such nick/channel"); continue; } 346 auto channel = *pchannel; 347 auto pmember = nickname.normalized in channel.members; 348 if (pmember) // On channel? 349 { 350 if (channel.modeFlags['m'] && (pmember.modes & Channel.Member.Modes.bypassM) == 0) 351 { sendReply(Reply.ERR_CANNOTSENDTOCHAN, target, "Cannot send to channel"); continue; } 352 } 353 else 354 { 355 if (channel.modeFlags['n']) // No external messages 356 { sendReply(Reply.ERR_NOTONCHANNEL, target, "You're not on that channel"); continue; } 357 } 358 sendToChannel(channel, command, message); 359 } 360 else 361 { 362 auto pclient = target.normalized in server.nicknames; 363 if (!pclient) 364 { sendReply(Reply.ERR_NOSUCHNICK, target, "No such nick/channel"); continue; } 365 sendToClient(*pclient, command, message); 366 } 367 } 368 break; 369 case "OPER": 370 if (!registered) 371 return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered"); 372 if (parameters.length < 1) 373 return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters"); 374 if (!server.operPassword || parameters[$-1] != server.operPassword) 375 return sendReply(Reply.ERR_PASSWDMISMATCH, "Password incorrect"); 376 modeFlags['o'] = true; 377 sendReply(Reply.RPL_YOUREOPER, "You are now an IRC operator"); 378 sendUserModes(this); 379 foreach (channel; server.channels) 380 if (nickname.normalized in channel.members) 381 setChannelMode(channel, nickname, Channel.Member.Mode.op, true); 382 break; 383 384 default: 385 if (registered) 386 return sendReply(Reply.ERR_UNKNOWNCOMMAND, command, "Unknown command"); 387 } 388 } 389 catch (Exception e) 390 { 391 disconnect(e.msg); 392 } 393 } 394 395 void onInactivity(IrcSocket sender) 396 { 397 sendReply("PING", Clock.currTime.stdTime.text); 398 } 399 400 void disconnect(string why) 401 { 402 if (registered) 403 unregister(why); 404 sendLine("ERROR :Closing Link: %s[%s@%s] (%s)".format(nickname, username, realHostname, why)); 405 conn.disconnect(why); 406 } 407 408 void onDisconnect(ClientSocket sender, string reason, DisconnectType type) 409 { 410 if (registered) 411 unregister(reason); 412 server.log("IRC: %s disconnecting: %s".format(conn.remoteAddress, reason)); 413 } 414 415 void checkRegistration() 416 { 417 assert(!registered); 418 419 if (nickname.length && username.length) 420 { 421 if (server.password && password != server.password) 422 { 423 password = null; 424 return sendReply(Reply.ERR_PASSWDMISMATCH, "Password incorrect"); 425 } 426 if (nickname.normalized in server.nicknames) 427 { 428 scope(exit) nickname = null; 429 return sendReply(Reply.ERR_NICKNAMEINUSE, nickname, "Nickname is already in use"); 430 } 431 if (!nickname.match(server.nicknameValidationPattern)) 432 { 433 scope(exit) nickname = null; 434 return sendReply(Reply.ERR_ERRONEUSNICKNAME, nickname, "Erroneus nickname"); 435 } 436 if (!username.match(`[a-zA-Z]+`)) 437 return disconnect("Invalid username"); 438 439 // All OK 440 register(); 441 } 442 } 443 444 void register() 445 { 446 if (!identified) 447 username = "~" ~ username; 448 prefix = "%s!%s@%s".format(nickname, username, realHostname ); 449 publicPrefix = "%s!%s@%s".format(nickname, username, publicHostname); 450 451 registered = true; 452 server.nicknames[nickname.normalized] = this; 453 auto userCount = server.nicknames.length; 454 if (server.maxUsers < userCount) 455 server.maxUsers = userCount; 456 sendReply(Reply.RPL_WELCOME , "Welcome, %s!".format(nickname)); 457 sendReply(Reply.RPL_YOURHOST , "Your host is %s, running %s".format(server.hostname, server.serverVersion)); 458 sendReply(Reply.RPL_CREATED , "This server was created %s".format(server.creationTime)); 459 sendReply(Reply.RPL_MYINFO , server.hostname, server.serverVersion, UserModes.supported, ChannelModes.supported, null); 460 sendReply(cast(Reply) 005, server.capabilities ~ ["are supported by this server"]); 461 sendReply(Reply.RPL_LUSERCLIENT , "There are %d users and %d invisible on %d servers".format(userCount, 0, 1)); 462 sendReply(Reply.RPL_LUSEROP , 0.text, "IRC Operators online"); // TODO: OPER 463 sendReply(Reply.RPL_LUSERCHANNELS, server.channels.length.text, "channels formed"); 464 sendReply(Reply.RPL_LUSERME , "I have %d clients and %d servers".format(userCount, 0)); 465 sendReply(cast(Reply) 265, "Current local users: %d Max: %d".format(userCount, server.maxUsers)); 466 sendReply(cast(Reply) 266, "Current global users: %d Max: %d".format(userCount, server.maxUsers)); 467 sendReply(cast(Reply) 250, "Highest connection count: %d (%d clients) (%d since server was (re)started)".format(server.maxUsers, server.maxUsers, server.totalConnections)); 468 sendMotd(); 469 } 470 471 void unregister(string why) 472 { 473 assert(registered); 474 auto channels = joinedChannels; 475 foreach (client; server.allClientsInChannels(joinedChannels)) 476 client.sendCommand(this, "QUIT", why); 477 foreach (channel; channels) 478 channel.remove(this); 479 server.nicknames.remove(nickname.normalized); 480 registered = false; 481 } 482 483 void sendMotd() 484 { 485 sendReply(Reply.RPL_MOTDSTART , "- %s Message of the Day - ".format(server.hostname)); 486 foreach (line; server.motd) 487 sendReply(Reply.RPL_MOTD, "- %s".format(line)); 488 sendReply(Reply.RPL_ENDOFMOTD , "End of /MOTD command."); 489 } 490 491 void join(Channel channel) 492 { 493 channel.add(this); 494 foreach (member; channel.members) 495 member.client.sendCommand(this, "JOIN", channel.name); 496 sendTopic(channel); 497 sendNames(channel); 498 auto pmember = nickname.normalized in channel.members; 499 // Sync OPER status with (initial) channel op status 500 if (server.staticChannels || modeFlags['o']) 501 setChannelMode(channel, nickname, Channel.Member.Mode.op, modeFlags['o']); 502 } 503 504 // For server-imposed mode changes. 505 void setChannelMode(Channel channel, string nickname, Channel.Member.Mode mode, bool value) 506 { 507 auto pmember = nickname.normalized in channel.members; 508 if (pmember.modeSet(mode) == value) 509 return; 510 511 pmember.setMode(Channel.Member.Mode.op, value); 512 auto c = ChannelModes.memberModeChars[mode]; 513 foreach (member; channel.members) 514 member.client.sendCommand(server.hostname, "MODE", channel.name, [value ? '+' : '-', c], nickname, null); 515 } 516 517 void part(Channel channel) 518 { 519 foreach (member; channel.members) 520 member.client.sendCommand(this, "PART", channel.name); 521 channel.remove(this); 522 } 523 524 void sendToChannel(Channel channel, string command, string message) 525 { 526 foreach (member; channel.members) 527 if (member.client !is this) 528 member.client.sendCommand(this, command, channel.name, message); 529 } 530 531 void sendToClient(Client client, string command, string message) 532 { 533 client.sendCommand(this, command, client.nickname, message); 534 } 535 536 void sendTopic(Channel channel) 537 { 538 if (channel.topic) 539 sendReply(Reply.RPL_TOPIC, channel.name, channel.topic); 540 else 541 sendReply(Reply.RPL_NOTOPIC, channel.name, "No topic is set"); 542 } 543 544 void sendNames(Channel channel) 545 { 546 foreach (chunk; channel.members.values.chunks(10)) // can't use byValue - http://j.mp/IUhhGC (Issue 11761) 547 sendReply(Reply.RPL_NAMREPLY, channel.modeFlags['s'] ? "@" : channel.modeFlags['p'] ? "*" : "=", channel.name, chunk.map!q{a.displayName}.join(" ")); 548 sendReply(Reply.RPL_ENDOFNAMES, channel.name, "End of /NAMES list"); 549 } 550 551 void sendChannelModes(Channel channel) 552 { 553 string modes = "+"; 554 string[] modeParams; 555 foreach (c; 0..char.max) 556 final switch (ChannelModes.modeTypes[c]) 557 { 558 case ChannelModes.Type.none: 559 case ChannelModes.Type.member: 560 break; 561 case ChannelModes.Type.mask: 562 // sent after RPL_CHANNELMODEIS 563 break; 564 case ChannelModes.Type.flag: 565 if (channel.modeFlags[c]) 566 modes ~= c; 567 break; 568 case ChannelModes.Type.str: 569 if (channel.modeStrings[c]) 570 { 571 modes ~= c; 572 modeParams ~= channel.modeStrings[c]; 573 } 574 break; 575 case ChannelModes.Type.number: 576 if (channel.modeNumbers[c]) 577 { 578 modes ~= c; 579 modeParams ~= channel.modeNumbers[c].text; 580 } 581 break; 582 } 583 sendReply(Reply.RPL_CHANNELMODEIS, channel.name, ([modes] ~ modeParams).join(" "), null); 584 sendChannelMaskList(channel, channel.modeMasks['b'], Reply.RPL_BANLIST, Reply.RPL_ENDOFBANLIST, "End of channel ban list"); 585 } 586 587 void sendChannelMaskList(Channel channel, string[] masks, Reply lineReply, Reply endReply, string endText) 588 { 589 foreach (mask; masks) 590 sendReply(lineReply, channel.name, mask, null); 591 sendReply(endReply, channel.name, endText); 592 } 593 594 void setChannelTopic(Channel channel, string topic) 595 { 596 channel.topic = topic; 597 foreach (ref member; channel.members) 598 member.client.sendCommand(this, "TOPIC", channel.name, topic); 599 } 600 601 void setChannelModes(Channel channel, string[] modes) 602 { 603 string[2] effectedChars; 604 string[][2] effectedParams; 605 606 scope(exit) // Broadcast effected options 607 { 608 string[] parameters; 609 foreach (adding; 0..2) 610 if (effectedChars[adding].length) 611 parameters ~= [(adding ? "+" : "-") ~ effectedChars[adding]] ~ effectedParams[adding]; 612 if (parameters.length) 613 { 614 parameters = ["MODE", channel.name] ~ parameters ~ null; 615 foreach (ref member; channel.members) 616 member.client.sendCommand(this, parameters); 617 } 618 } 619 620 while (modes.length) 621 { 622 auto chars = modes.shift; 623 624 bool adding = true; 625 foreach (c; chars) 626 if (c == '+') 627 adding = true; 628 else 629 if (c == '-') 630 adding = false; 631 else 632 final switch (ChannelModes.modeTypes[c]) 633 { 634 case ChannelModes.Type.none: 635 sendReply(Reply.ERR_UNKNOWNMODE, [c], "is unknown mode char to me for %s".format(channel.name)); 636 break; 637 case ChannelModes.Type.flag: 638 if (adding != channel.modeFlags[c]) 639 { 640 channel.modeFlags[c] = adding; 641 effectedChars[adding] ~= c; 642 } 643 break; 644 case ChannelModes.Type.member: 645 { 646 if (!modes.length) 647 { sendReply(Reply.ERR_NEEDMOREPARAMS, "MODE", "Not enough parameters"); continue; } 648 auto memberName = modes.shift; 649 auto pmember = memberName.normalized in channel.members; 650 if (!pmember) 651 { sendReply(Reply.ERR_USERNOTINCHANNEL, memberName, channel.name, "They aren't on that channel"); continue; } 652 auto mode = ChannelModes.memberModes[c]; 653 if (pmember.modeSet(mode) != adding) 654 { 655 pmember.setMode(mode, adding); 656 effectedChars[adding] ~= c; 657 effectedParams[adding] ~= memberName; 658 } 659 break; 660 } 661 case ChannelModes.Type.mask: 662 { 663 if (!modes.length) 664 { sendReply(Reply.ERR_NEEDMOREPARAMS, "MODE", "Not enough parameters"); continue; } 665 auto mask = modes.shift; 666 if (adding) 667 { 668 if (channel.modeMasks[c].canFind(mask)) 669 continue; 670 channel.modeMasks[c] ~= mask; 671 } 672 else 673 { 674 auto index = channel.modeMasks[c].countUntil(mask); 675 if (index < 0) 676 continue; 677 channel.modeMasks[c] = channel.modeMasks[c][0..index] ~ channel.modeMasks[c][index+1..$]; 678 } 679 effectedChars[adding] ~= c; 680 effectedParams[adding] ~= mask; 681 break; 682 } 683 case ChannelModes.Type.str: 684 if (adding) 685 { 686 if (!modes.length) 687 { sendReply(Reply.ERR_NEEDMOREPARAMS, "MODE", "Not enough parameters"); continue; } 688 auto str = modes.shift; 689 if (channel.modeStrings[c] == str) 690 continue; 691 channel.modeStrings[c] = str; 692 effectedChars[adding] ~= c; 693 effectedParams[adding] ~= str; 694 } 695 else 696 { 697 if (!channel.modeStrings[c]) 698 continue; 699 channel.modeStrings[c] = null; 700 effectedChars[adding] ~= c; 701 } 702 break; 703 case ChannelModes.Type.number: 704 if (adding) 705 { 706 if (!modes.length) 707 { sendReply(Reply.ERR_NEEDMOREPARAMS, "MODE", "Not enough parameters"); continue; } 708 auto numText = modes.shift; 709 auto num = numText.to!long; 710 if (channel.modeNumbers[c] == num) 711 continue; 712 channel.modeNumbers[c] = num; 713 effectedChars[adding] ~= c; 714 effectedParams[adding] ~= numText; 715 } 716 else 717 { 718 if (!channel.modeNumbers[c]) 719 continue; 720 channel.modeNumbers[c] = 0; 721 effectedChars[adding] ~= c; 722 } 723 break; 724 } 725 } 726 } 727 728 void setUserModes(string[] modes) 729 { 730 while (modes.length) 731 { 732 auto chars = modes.shift; 733 734 bool adding = true; 735 foreach (c; chars) 736 if (c == '+') 737 adding = true; 738 else 739 if (c == '-') 740 adding = false; 741 else 742 final switch (UserModes.modeTypes[c]) 743 { 744 case UserModes.Type.none: 745 sendReply(Reply.ERR_UMODEUNKNOWNFLAG, "Unknown MODE flag"); 746 break; 747 case UserModes.Type.flag: 748 if (UserModes.isSettable[c]) 749 modeFlags[c] = adding; 750 break; 751 } 752 } 753 } 754 755 void sendUserModes(Client client) 756 { 757 string modeString = "+"; 758 foreach (char c, on; modeFlags) 759 if (on) 760 modeString ~= c; 761 return sendReply(Reply.RPL_UMODEIS, modeString, null); 762 } 763 764 void sendCommand(Client from, string[] parameters...) 765 { 766 return sendCommand(this is from ? prefix : from.publicPrefix, parameters); 767 } 768 769 void sendCommand(string from, string[] parameters...) 770 { 771 assert(parameters.length, "At least one parameter expected"); 772 if (parameters[$-1] is null) 773 parameters = parameters[0..$-1]; 774 else 775 parameters = parameters[0..$-1] ~ [":" ~ parameters[$-1]]; 776 auto line = ":%s %-(%s %)".format(from, parameters); 777 sendLine(line); 778 } 779 780 void sendReply(Reply reply, string[] parameters...) 781 { 782 return sendReply("%03d".format(reply), parameters); 783 } 784 785 void sendReply(string command, string[] parameters...) 786 { 787 return sendCommand(server.hostname, [command, nickname] ~ parameters); 788 } 789 790 void sendServerNotice(string text) 791 { 792 sendReply("NOTICE", "*** Notice -- " ~ text); 793 } 794 795 void sendLine(string line) 796 { 797 conn.send(line); 798 } 799 } 800 801 Client[string] nicknames; 802 803 /// Statistics 804 ulong maxUsers, totalConnections; 805 806 final class Channel 807 { 808 string name; 809 string topic; 810 811 bool[char.max] modeFlags; 812 string[char.max] modeStrings; 813 long[char.max] modeNumbers; 814 string[][char.max] modeMasks; 815 816 struct Member 817 { 818 enum Mode 819 { 820 op, 821 voice, 822 max 823 } 824 825 enum Modes 826 { 827 none = 0, 828 op = 1 << Mode.op, 829 voice = 1 << Mode.voice, 830 831 bypassM = op | voice, // which modes bypass +m 832 } 833 834 Client client; 835 Modes modes; 836 837 bool modeSet(Mode mode) { return (modes & (1 << mode)) != 0; } 838 void setMode(Mode mode, bool value) 839 { 840 auto modeMask = 1 << mode; 841 if (value) 842 modes |= modeMask; 843 else 844 modes &= ~modeMask; 845 } 846 847 string modeChar() 848 { 849 foreach (mode; Mode.init..Mode.max) 850 if ((1 << mode) & modes) 851 return [ChannelModes.memberModePrefixes[mode]]; 852 return ""; 853 } 854 string displayName() { return modeChar ~ client.nickname; } 855 } 856 857 Member[string] members; 858 859 this(string name) 860 { 861 this.name = name; 862 modeFlags['t'] = modeFlags['n'] = true; 863 } 864 865 void add(Client client) 866 { 867 auto modes = members.length ? Member.Modes.none : Member.Modes.op; 868 members[client.nickname.normalized] = Member(client, modes); 869 } 870 871 void remove(Client client) 872 { 873 members.remove(client.nickname.normalized); 874 if (!staticChannels && !members.length) 875 channels.remove(name.normalized); 876 } 877 } 878 879 Channel[string] channels; 880 881 IrcServerSocket conn; 882 883 this() 884 { 885 conn = new IrcServerSocket; 886 conn.handleAccept = &onAccept; 887 888 hostname = Socket.hostName; 889 creationTime = Clock.currTime; 890 } 891 892 ushort listen(ushort port=6667, string addr = null) 893 { 894 port = conn.listen(port, addr); 895 return port; 896 } 897 898 Channel createChannel(string name) 899 { 900 return channels[name.normalized] = new Channel(name); 901 } 902 903 private: 904 void onAccept(IrcSocket incoming) 905 { 906 new Client(this, incoming); 907 totalConnections++; 908 } 909 910 Client[] allClientsInChannels(Channel[] channels) 911 { 912 Client[string] result; 913 foreach (channel; channels) 914 foreach (ref member; channel.members) 915 result[member.client.nickname.normalized] = member.client; 916 return result.values; 917 } 918 919 Client[] inSameChannelAs(Client client) 920 { 921 return allClientsInChannels(client.joinedChannels); 922 } 923 924 bool isChannelName(string target) 925 { 926 foreach (prefix; chanTypes) 927 if (target.startsWith(prefix)) 928 return true; 929 return false; 930 } 931 932 string[] capabilities() 933 { 934 string[] result; 935 result ~= "PREFIX=(%s)%s".format(ChannelModes.memberModeChars, ChannelModes.memberModePrefixes); 936 result ~= "CHANTYPES=" ~ chanTypes; 937 result ~= "CHANMODES=%-(%s,%)".format( 938 [ChannelModes.Type.mask, ChannelModes.Type.str, ChannelModes.Type.number, ChannelModes.Type.flag].map!(type => ChannelModes.byType(type)) 939 ); 940 if (network) 941 result ~= "NETWORK=" ~ network; 942 result ~= "CASEMAPPING=rfc1459"; 943 return result; 944 } 945 } 946 947 private: 948 949 bool maskMatch(string subject, string mask) 950 { 951 import std.path; 952 return globMatch!(CaseSensitive.no)(subject, mask); 953 } 954 955 alias rfc1459toUpper normalized; 956 957 string[] ircSplit(string line) 958 { 959 auto colon = line.indexOf(":"); 960 if (colon < 0) 961 return line.split; 962 else 963 return line[0..colon].strip.split ~ [line[colon+1..$]]; 964 } 965 966 mixin template CommonModes() 967 { 968 Type[char.max] modeTypes; 969 string supported() { return modeTypes.length.iota.filter!(m => modeTypes[m] ).map!(m => cast(char)m).array.assumeUnique; } 970 string byType(Type type) { return modeTypes.length.iota.filter!(m => modeTypes[m] == type).map!(m => cast(char)m).array.assumeUnique; } 971 } 972 973 struct ChannelModes 974 { 975 static: 976 enum Type { none, flag, member, mask, str, number } 977 mixin CommonModes; 978 IrcServer.Channel.Member.Mode[char.max] memberModes; 979 char[IrcServer.Channel.Member.Mode.max] memberModeChars, memberModePrefixes; 980 981 static this() 982 { 983 foreach (c; "ntps") 984 modeTypes[c] = Type.flag; 985 foreach (c; "ov") 986 modeTypes[c] = Type.member; 987 foreach (c; "b") 988 modeTypes[c] = Type.mask; 989 foreach (c; "k") 990 modeTypes[c] = Type.str; 991 992 memberModes['o'] = IrcServer.Channel.Member.Mode.op; 993 memberModes['v'] = IrcServer.Channel.Member.Mode.voice; 994 995 memberModeChars = "ov"; 996 memberModePrefixes = "@+"; 997 } 998 } 999 1000 struct UserModes 1001 { 1002 static: 1003 enum Type { none, flag } 1004 mixin CommonModes; 1005 Type[char.max] modeTypes; 1006 bool[char.max] isSettable; 1007 1008 static this() 1009 { 1010 foreach (c; "io") 1011 modeTypes[c] = Type.flag; 1012 foreach (c; "i") 1013 isSettable[c] = true; 1014 } 1015 }