1 /** 2 * dconnection 3 * 4 * Client/server connection handler spawned 5 * by socket connection dequeue loop. 6 * 7 * Handles all interactions between 8 * the server and the specific client/server. 9 */ 10 11 module dnetd.dconnection; 12 13 import core.thread : Thread; 14 import std.socket : Socket; 15 import bmessage; 16 import tristanable.encoding : DataMessage; 17 import core.sync.mutex : Mutex; 18 import dnetd.dserver : DServer; 19 import std..string : split; 20 import dnetd.dchannel : DChannel; 21 import std.conv : to; 22 import std.stdio : writeln; 23 import std.algorithm : reverse; 24 25 public class DConnection : Thread 26 { 27 /* The connection type */ 28 public enum ConnectionType 29 { 30 CLIENT, SERVER, UNSPEC 31 } 32 33 /* Command types */ 34 public enum Command 35 { 36 JOIN, 37 PART, 38 AUTH, 39 LINK, 40 REGISTER, 41 LIST, 42 MSG, 43 MEMBER_COUNT, 44 MEMBER_LIST, 45 SERVER_INFO, 46 MOTD, 47 UNKNOWN 48 } 49 50 /** 51 * Connection information 52 */ 53 private DServer server; 54 private Socket socket; 55 private bool hasAuthed; 56 private ConnectionType connType; 57 private string username; 58 59 /* Write lock for socket */ 60 /* TODO: Forgot how bmessage works, might need, might not, if multipel calls 61 * then yes, if single then no as it is based off (well glibc's write) 62 * thread safe code 63 */ 64 private Mutex writeLock; 65 66 /* Reserved tag for push notifications */ 67 private long notificationTag = 0; 68 69 this(DServer server, Socket socket) 70 { 71 /* Set the function to be called on thread start */ 72 super(&worker); 73 74 /* Set the associated server */ 75 this.server = server; 76 77 /* Set the socket */ 78 this.socket = socket; 79 80 /* Set the default state */ 81 connType = ConnectionType.UNSPEC; 82 83 /* Initialize locks */ 84 initLocks(); 85 86 /* Start the connection handler */ 87 start(); 88 } 89 90 /** 91 * Initializes mutexes 92 */ 93 private void initLocks() 94 { 95 /* Initialie the socket write lock */ 96 writeLock = new Mutex(); 97 } 98 99 /** 100 * Byte dequeue loop 101 */ 102 private void worker() 103 { 104 /* Received bytes (for bformat) */ 105 byte[] receivedBytes; 106 107 /* Received message */ 108 DataMessage receivedMessage; 109 110 while(true) 111 { 112 /** 113 * Block to receive a bformat message 114 * 115 * (Does decoding for bformat too) 116 */ 117 writeln("waiting"); 118 bool status = receiveMessage(socket, receivedBytes); 119 120 /* TODO: Check status */ 121 if(status) 122 { 123 /* Decode the tristanable message (tagged message) */ 124 receivedMessage = DataMessage.decode(receivedBytes); 125 126 /* Process the message */ 127 process(receivedMessage); 128 129 /* TODO: Tristanable needs reserved-tag support (client-side concern) */ 130 } 131 else 132 { 133 /* TODO: Error handling */ 134 writeln("Error with receive: "~to!(string)(this)); 135 break; 136 } 137 } 138 139 /* Clean up */ 140 cleanUp(); 141 } 142 143 private void cleanUp() 144 { 145 writeln(to!(string)(this)~" Cleaning up connection..."); 146 147 /* Remove this user from all channels he is in */ 148 DChannel[] channels = server.getChannels(); 149 150 /* Loop through each channel */ 151 foreach(DChannel currentChannel; channels) 152 { 153 /* Check if you are a member of it */ 154 if(currentChannel.isMember(this)) 155 { 156 /* Leave the channel */ 157 currentChannel.leave(this); 158 writeln(to!(string)(this)~" Leaving '"~currentChannel.getName()~"'..."); 159 } 160 } 161 162 /* Remove this user from the connection queue */ 163 server.removeConnection(this); 164 165 writeln(to!(string)(this)~" Connection cleaned up"); 166 } 167 168 /* TODO: add mutex for writing with message and funciton for doing so */ 169 170 /** 171 * Write to socket 172 * 173 * Encodes the byte array as a tristanable tagged 174 * message and then encodes that as a bformat 175 * message 176 * 177 * Locks the writeLock mutex, sends it over the 178 * socket to the client/server, and unlocks the 179 * mutex 180 */ 181 public bool writeSocket(long tag, byte[] data) 182 { 183 /* Send status */ 184 bool status; 185 186 /* Create the tagged message */ 187 DataMessage message = new DataMessage(tag, data); 188 189 writeln("writeSocket: mutex lock"); 190 /* Lock the write mutex */ 191 writeLock.lock(); 192 193 /* Send the message */ 194 writeln("writeSocket: Data: "~to!(string)(data)~" Tag: "~to!(string)(tag)); 195 status = sendMessage(socket, message.encode()); 196 197 /* Unlock the write mutex */ 198 writeLock.unlock(); 199 200 writeln("writeSocket: mutex unlock"); 201 202 return status; 203 } 204 205 private Command getCommand(byte commandByte) 206 { 207 Command command = Command.UNKNOWN; 208 209 if(commandByte == cast(ulong)0) 210 { 211 command = Command.AUTH; 212 } 213 else if(commandByte == cast(ulong)1) 214 { 215 command = Command.LINK; 216 } 217 else if(commandByte == cast(ulong)2) 218 { 219 command = Command.REGISTER; 220 } 221 else if(commandByte == cast(ulong)3) 222 { 223 command = Command.JOIN; 224 } 225 else if(commandByte == cast(ulong)4) 226 { 227 command = Command.PART; 228 } 229 else if(commandByte == cast(ulong)5) 230 { 231 command = Command.MSG; 232 } 233 else if(commandByte == cast(ulong)6) 234 { 235 command = Command.LIST; 236 } 237 else if(commandByte == cast(ulong)7) 238 { 239 command = Command.MSG; 240 } 241 else if(commandByte == cast(ulong)8) 242 { 243 command = Command.MEMBER_COUNT; 244 } 245 else if(commandByte == cast(ulong)9) 246 { 247 command = Command.MEMBER_LIST; 248 } 249 else if(commandByte == cast(ulong)10) 250 { 251 command = Command.SERVER_INFO; 252 } 253 else if(commandByte == cast(ulong)11) 254 { 255 command = Command.MOTD; 256 } 257 258 259 260 261 return command; 262 } 263 264 /** 265 * Process the received message 266 */ 267 private void process(DataMessage message) 268 { 269 /* Get the tag */ 270 /** 271 * TODO: Client side will always do 1, because we don't have 272 * multi-thread job processing, only need this to differentiate 273 * between commands and async notifications 274 */ 275 long tag = message.tag; 276 writeln("tag:", tag); 277 278 /* The reply */ 279 byte[] reply; 280 281 /* Get the command */ 282 byte commandByte = message.data[0]; 283 Command command = getCommand(commandByte); 284 writeln(to!(string)(this)~" ~> "~to!(string)(command)); 285 286 /* If `auth` command (requires: unauthed) */ 287 if(command == Command.AUTH && !hasAuthed) 288 { 289 /* Get the length of the username */ 290 byte usernameLength = message.data[1]; 291 292 /* Get the username and password */ 293 string username = cast(string)message.data[2..cast(ulong)2+usernameLength]; 294 string password = cast(string)message.data[cast(ulong)2+usernameLength..message.data.length]; 295 296 /* Authenticate */ 297 bool status = authenticate(username, password); 298 299 /* TODO: What to do on bad authetication? */ 300 301 /* Set the username */ 302 this.username = username; 303 304 /* Set the type of this connection to `client` */ 305 connType = ConnectionType.CLIENT; 306 hasAuthed = true; 307 308 /* Encode the reply */ 309 reply = [status]; 310 } 311 /* If `link` command (requires: unauthed) */ 312 else if(command == Command.LINK && !hasAuthed) 313 { 314 /* TODO: Implement me later */ 315 316 317 /* Set the type of this connection to `server` */ 318 connType = ConnectionType.SERVER; 319 hasAuthed = true; 320 } 321 /* If `register` command (requires: unauthed, client) */ 322 else if(command == Command.REGISTER && !hasAuthed && connType == ConnectionType.CLIENT) 323 { 324 325 } 326 /* If `join` command (requires: authed, client) */ 327 else if(command == Command.JOIN && hasAuthed && connType == ConnectionType.CLIENT) 328 { 329 /* Get the channel names */ 330 string channelList = cast(string)message.data[1..message.data.length]; 331 string[] channels = split(channelList, ","); 332 333 /** 334 * Loop through each channel, check if it 335 * exists, if so join it, else create it 336 * and then join it 337 */ 338 bool isPresentInfo = false; 339 foreach(string channelName; channels) 340 { 341 /** 342 * Finds the channel, if it exists then it returns it, 343 * if it does not exist then it will create it and then 344 * return it 345 */ 346 DChannel channel = server.getChannel(this, channelName); 347 348 /* Join the channel */ 349 isPresentInfo = channel.join(this); 350 } 351 352 /* TODO: Do reply */ 353 /* Encode the reply */ 354 reply = [isPresentInfo]; 355 } 356 /* If `part` command (requires: authed, client) */ 357 else if(command == Command.PART && hasAuthed && connType == ConnectionType.CLIENT) 358 { 359 /* Get the channel names */ 360 string channelList = cast(string)message.data[1..message.data.length]; 361 string[] channels = split(channelList, ","); 362 363 /** 364 * Loop through each channel, check if it 365 * exists, if so leave it 366 */ 367 foreach(string channelName; channels) 368 { 369 /* Attempt to find the channel */ 370 DChannel channel = server.getChannelByName(channelName); 371 372 /* Leave a channel the channel only if it exists */ 373 if(!(channel is null)) 374 { 375 channel.leave(this); 376 } 377 } 378 379 /* TODO: Do reply */ 380 /* Encode the reply */ 381 reply = [true]; 382 } 383 /* If `list` command (requires: authed, client) */ 384 else if(command == Command.LIST && hasAuthed && connType == ConnectionType.CLIENT) 385 { 386 /* Get all channels */ 387 DChannel[] channels = server.getChannels(); 388 389 /* Generate a list of channel names (CSV) */ 390 string channelList; 391 for(ulong i = 0; i < channels.length; i++) 392 { 393 if(i == channels.length-1) 394 { 395 channelList ~= channels[i].getName(); 396 } 397 else 398 { 399 channelList ~= channels[i].getName()~","; 400 } 401 } 402 403 /* TODO: Reply */ 404 /* Encode the reply */ 405 reply = [true]; 406 reply ~= channelList; 407 } 408 /* If `msg` command (requires: authed, client) */ 409 else if(command == Command.MSG && hasAuthed && connType == ConnectionType.CLIENT) 410 { 411 /* Status */ 412 bool status = true; 413 414 /* Get the type of message */ 415 byte messageType = message.data[1]; 416 417 /* Get the location length */ 418 byte locationLength = message.data[2]; 419 420 /* Get the channel/person name */ 421 string destination = cast(string)message.data[3..cast(ulong)3+locationLength]; 422 423 /* Get the message */ 424 string msg = cast(string)message.data[cast(ulong)3+locationLength..message.data.length]; 425 426 /* Send status */ 427 bool sendStatus; 428 429 /* If we are sending to a user */ 430 if(messageType == cast(byte)0) 431 { 432 /* Send the message to the user */ 433 sendStatus = sendUserMessage(destination, msg); 434 } 435 /* If we are sending to a channel */ 436 else if(messageType == cast(ubyte)1) 437 { 438 /* The channel wanting to send to */ 439 DChannel channel = server.getChannelByName(destination); 440 441 /* If the channel exists */ 442 if(channel) 443 { 444 /* TODO Implemet me */ 445 sendStatus = channel.sendMessage(this, msg); 446 } 447 /* If the channel does not exist */ 448 else 449 { 450 status = false; 451 } 452 } 453 /* Unknown destination type */ 454 else 455 { 456 status = false; 457 } 458 459 /* TODO: Handling here, should we make the user wait? */ 460 461 /* Encode the reply */ 462 /* TODO: */ 463 reply = [status]; 464 } 465 /* If `membercount` command (requires: authed, client) */ 466 else if(command == Command.MEMBER_COUNT && hasAuthed && connType == ConnectionType.CLIENT) 467 { 468 /* Status */ 469 bool status = true; 470 471 /* Get the channel name */ 472 string channelName = cast(string)message.data[1..message.data.length]; 473 474 /* The memebr count */ 475 long memberCount; 476 477 /* Get the member count */ 478 status = getMemberCount(channelName, memberCount); 479 480 /* Encode the status */ 481 reply = [status]; 482 483 /* If there was no error fetching the member count */ 484 if(status) 485 { 486 /* Data bytes */ 487 byte[] numberBytes; 488 numberBytes.length = 8; 489 490 /* Encode the length (Big Endian) from Little Endian */ 491 numberBytes[0] = *((cast(byte*)&memberCount)+7); 492 numberBytes[1] = *((cast(byte*)&memberCount)+6); 493 numberBytes[2] = *((cast(byte*)&memberCount)+5); 494 numberBytes[3] = *((cast(byte*)&memberCount)+4); 495 numberBytes[4] = *((cast(byte*)&memberCount)+3); 496 numberBytes[5] = *((cast(byte*)&memberCount)+2); 497 numberBytes[6] = *((cast(byte*)&memberCount)+1); 498 numberBytes[7] = *((cast(byte*)&memberCount)+0); 499 500 /* Append the length */ 501 reply ~= numberBytes; 502 } 503 } 504 /* If `memberlist` command (requires: authed, client) */ 505 else if(command == Command.MEMBER_LIST && hasAuthed && connType == ConnectionType.CLIENT) 506 { 507 /* Status */ 508 bool status = true; 509 510 /* Get the channel name */ 511 string channelName = cast(string)message.data[1..message.data.length]; 512 513 /* Get the channel */ 514 DChannel channel = server.getChannelByName(channelName); 515 516 /* Encode the status */ 517 reply ~= [channel !is null]; 518 519 /* If the channel exists */ 520 if(channel) 521 { 522 /* Get the list of members in the channel */ 523 DConnection[] members = channel.getMembers(); 524 525 /* Construct a CSV string of the members */ 526 string memberString; 527 528 for(ulong i = 0; i < members.length; i++) 529 { 530 if(i == members.length-1) 531 { 532 memberString ~= members[i].getUsername(); 533 } 534 else 535 { 536 memberString ~= members[i].getUsername()~","; 537 } 538 } 539 540 /* Encode the string into the reply */ 541 reply ~= cast(byte[])memberString; 542 } 543 /* If the channel does not exist */ 544 else 545 { 546 status = false; 547 } 548 549 550 551 552 553 554 555 556 } 557 /* If `serverinfo` command (requires: authed, !unspec) */ 558 else if(command == Command.SERVER_INFO && hasAuthed && connType != ConnectionType.UNSPEC) 559 { 560 /* Status */ 561 bool status = true; 562 563 /* Get the server info */ 564 string serverInfo = server.getServerInfo(); 565 566 /* Encode the reply */ 567 reply ~= [status]; 568 reply ~= serverInfo; 569 } 570 /* If `motd` command (requires: _nothing_) */ 571 else if(command == Command.MOTD) 572 { 573 /* Status */ 574 bool status = true; 575 576 /* Get the message of the day */ 577 string motd = server.getConfig().getGeneral().getMotd(); 578 579 /* Encode the reply */ 580 reply ~= [status]; 581 reply ~= motd; 582 } 583 /* If no matching built-in command was found */ 584 else 585 { 586 /* TODO: Check plugins */ 587 bool isPlugin = false; 588 589 /* A matching plugin was found */ 590 if(isPlugin) 591 { 592 /* TODO: Implement me */ 593 } 594 /* The command was invalid */ 595 else 596 { 597 /* Write error message */ 598 reply = [2]; 599 } 600 } 601 602 /* Write the response */ 603 writeSocket(tag, reply); 604 } 605 606 /** 607 * Authenticate 608 * 609 * Login as a user with the given credentials 610 */ 611 private bool authenticate(string username, string password) 612 { 613 /* TODO: Implement me */ 614 return true; 615 } 616 617 /** 618 * Get member count 619 * 620 * Gets the member count of a given channel 621 */ 622 private bool getMemberCount(string channelName, ref long count) 623 { 624 /* Status of operation */ 625 bool status; 626 627 /* The channel */ 628 DChannel channel = server.getChannelByName(channelName); 629 630 /* Check if the channel exists */ 631 if(channel) 632 { 633 /* Get the channel count */ 634 count = channel.getMemberCount(); 635 636 637 status = true; 638 } 639 /* If the channel does not exist */ 640 else 641 { 642 status = false; 643 } 644 645 return status; 646 } 647 648 /** 649 * Send user a message 650 * 651 * Sends the provided user the specified message 652 */ 653 private bool sendUserMessage(string username, string message) 654 { 655 /* Find the user to send to */ 656 DConnection user = server.findUser(username); 657 658 writeln("sendUserMessage(): ", user); 659 660 /* If the user was found */ 661 if(user) 662 { 663 /* The protocol data to send */ 664 byte[] protocolData; 665 666 /* Set the sub-type (ntype=0) */ 667 protocolData ~= [0]; 668 669 /* Encode the sender's length */ 670 protocolData ~= [cast(byte)username.length]; 671 672 /* Encode the username */ 673 protocolData ~= cast(byte[])username; 674 675 /* Encode the message */ 676 protocolData ~= cast(byte[])message; 677 678 /* Send the messge */ 679 bool sendStatus = user.writeSocket(0, protocolData); 680 681 return sendStatus; 682 } 683 /* If the user was not found */ 684 else 685 { 686 return false; 687 } 688 } 689 690 public string getUsername() 691 { 692 return username; 693 } 694 695 public ConnectionType getConnectionType() 696 { 697 return connType; 698 } 699 700 public override string toString() 701 { 702 string toStr = "["~to!(string)(connType)~" ("; 703 toStr ~= socket.remoteAddress.toString(); 704 705 706 toStr ~= ")]: "; 707 708 709 if(connType == ConnectionType.CLIENT) 710 { 711 toStr = toStr ~ getUsername(); 712 } 713 else 714 { 715 /* TODO Implement me */ 716 } 717 718 return toStr; 719 } 720 }