1 /** 2 * Message definition and tooling 3 */ 4 module birchwood.protocol.messages; 5 6 import dlog; 7 8 import std.string; 9 import std.conv : to, ConvException; 10 import birchwood.protocol.constants : ReplyType; 11 12 import birchwood.client.exceptions; 13 import birchwood.config.conninfo : ChecksMode; 14 import birchwood.logging; 15 16 /* TODO: We could move these all to `package.d` */ 17 18 /** 19 * Encodes the provided message into a CRLF 20 * terminated byte array 21 * 22 * Params: 23 * messageIn = the message to encode 24 * Returns: the encoded message 25 */ 26 public ubyte[] encodeMessage(string messageIn) 27 { 28 ubyte[] messageOut = cast(ubyte[])messageIn; 29 messageOut~=[cast(ubyte)13, cast(ubyte)10]; 30 return messageOut; 31 } 32 33 public static string decodeMessage(ubyte[] messageIn) 34 { 35 /* TODO: We could do a chekc to ESNURE it is well encoded */ 36 37 return cast(string)messageIn[0..messageIn.length-2]; 38 // return null; 39 } 40 41 /** 42 * Checks if the provided message is valid (i.e.) 43 * does not contain any CR or LF characters in it 44 * 45 * Params: 46 * message = the message to test 47 * Returns: <code>true</code> if the message is valid, 48 * <code>false</code> false otherwise 49 */ 50 //TODO: Should we add an emptiness check here 51 public static bool isValidText(string message) 52 { 53 foreach(char character; message) 54 { 55 if(character == 13 || character == 10) 56 { 57 return false; 58 } 59 } 60 61 return true; 62 } 63 64 /** 65 * Message types 66 */ 67 public final class Message 68 { 69 /* Message contents */ 70 private string from; 71 private string command; 72 private string params; 73 74 /* The numeric reply (as per Section 6 of RFC 1459) */ 75 private bool isNumericResponse = false; 76 private ReplyType replyType = ReplyType.BIRCHWOOD_UNKNOWN_RESP_CODE; 77 private bool isError = false; 78 79 /** 80 * Constructs a new Message 81 * 82 * Params: 83 * from = the from parameter 84 * command = the command 85 * params = any optional parameters to the command 86 */ 87 this(string from, string command, string params = "") 88 { 89 this.from = from; 90 this.command = command; 91 this.params = params; 92 93 /* Check if this is a command reply */ 94 if(isNumeric(command)) 95 { 96 isNumericResponse = true; 97 98 //FIXME: SOmething is tripping it u, elts' see 99 try 100 { 101 /* Grab the code */ 102 replyType = to!(ReplyType)(to!(ulong)(command)); 103 // TODO: Add validity check on range of values here, if bad throw exception 104 // TODO: Add check for "6.3 Reserved numerics" or handling of SOME sorts atleast 105 106 /* Error codes are in range of [401, 502] */ 107 if(replyType >= 401 && replyType <= 502) 108 { 109 // TODO: Call error handler 110 isError = true; 111 } 112 /* Command replies are in range of [259, 395] */ 113 else if(replyType >= 259 && replyType <= 395) 114 { 115 // TODO: Call command-reply handler 116 isError = false; 117 } 118 } 119 catch(ConvException e) 120 { 121 DEBUG("<<< Unsupported response code (Error below) >>>"); 122 DEBUG(e); 123 } 124 } 125 126 /* Parse the parameters into key-value pairs (if any) and trailing text (if any) */ 127 parameterParse(); 128 } 129 130 /** 131 * Encodes this `Message` into a CRLF delimited 132 * byte array 133 * 134 * If `ChecksMode` is set to `EASY` (default) then 135 * any invalid characters will be stripped prior 136 * to encoding 137 * 138 * Params: 139 * mode = the `ChecksMode` to use 140 * 141 * Throws: 142 * `BirchwoodException` if `ChecksMode` is set to 143 * `HARDCORE` and invalid characters are present 144 * Returns: the encoded format 145 */ 146 public string encode(ChecksMode mode) 147 { 148 string fullLine; 149 150 /** 151 * Copy over the values (they might be updated and we 152 * want to leave the originals intact) 153 */ 154 string fFrom = from, fCommand = command, fParams = params; 155 156 /** 157 * If in `HARDCORE` mode then and illegal characters 158 * are present, throw an exception 159 */ 160 if(mode == ChecksMode.HARDCORE && ( 161 hic(fFrom) || 162 hic(fCommand) || 163 hic(fParams) 164 )) 165 { 166 throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS, "Invalid characters present"); 167 } 168 /** 169 * If in `EASY` mode and illegal characters have 170 * been found, then fix them up 171 */ 172 else 173 { 174 // Strip illegal characters from all 175 fFrom = sic(fFrom); 176 fCommand = sic(fCommand); 177 fParams = sic(fParams); 178 } 179 180 /* Combine */ 181 fullLine = fFrom~" "~fCommand~" "~fParams; 182 183 184 185 return fullLine; 186 } 187 188 // TODO: comemnt 189 private alias sic = stripIllegalCharacters; 190 // TODO: comemnt 191 private alias hic = hasIllegalCharacters; 192 193 /** 194 * Checks whether the provided input string contains 195 * any invalid characters 196 * 197 * Params: 198 * input = the string to check 199 * Returns: `true` if so, `false` otherwise 200 */ 201 // TODO: Add unittest 202 public static bool hasIllegalCharacters(string input) 203 { 204 foreach(char character; input) 205 { 206 if(character == '\n' || character == '\r') 207 { 208 return true; 209 } 210 } 211 212 return false; 213 } 214 215 /** 216 * Provided an input string this will strip any illegal 217 * characters present within it 218 * 219 * Params: 220 * input = the string to filter 221 * Returns: the filtered string 222 */ 223 // TODO: Add unittest 224 public static string stripIllegalCharacters(string input) 225 { 226 string stripped; 227 foreach(char character; input) 228 { 229 if(character == '\n' || character == '\r') 230 { 231 continue; 232 } 233 234 stripped ~= character; 235 } 236 237 return stripped; 238 } 239 240 public static Message parseReceivedMessage(string message) 241 { 242 /* TODO: testing */ 243 244 /* From */ 245 string from; 246 247 /* Command */ 248 string command; 249 250 /* Params */ 251 string params; 252 253 254 255 /* Check if there is a PREFIX (according to RFC 1459) */ 256 if(message[0] == ':') 257 { 258 /* prefix ends after first space (we fetch servername, host/user) */ 259 //TODO: make sure not -1 260 long firstSpace = indexOf(message, ' '); 261 262 /* TODO: double check the condition */ 263 if(firstSpace > 0) 264 { 265 from = message[1..firstSpace]; 266 267 // DEBUG("from: "~from); 268 269 /* TODO: Find next space (what follows `from` is `' ' { ' ' }`) */ 270 ulong i = firstSpace; 271 for(; i < message.length; i++) 272 { 273 if(message[i] != ' ') 274 { 275 break; 276 } 277 } 278 279 // writeln("Yo"); 280 281 string rem = message[i..message.length]; 282 // writeln("Rem: "~rem); 283 long idx = indexOf(rem, " "); //TOOD: -1 check 284 285 /* Extract the command */ 286 command = rem[0..idx]; 287 // DEBUG("command: "~command); 288 289 /* Params are everything till the end */ 290 i = idx; 291 for(; i < rem.length; i++) 292 { 293 if(rem[i] != ' ') 294 { 295 break; 296 } 297 } 298 params = rem[i..rem.length]; 299 // DEBUG("params: "~params); 300 } 301 else 302 { 303 //TODO: handle 304 DEBUG("Malformed message start after :"); 305 assert(false); 306 } 307 308 309 } 310 /* In this case it is only `<command> <params>` */ 311 else 312 { 313 314 long firstSpace = indexOf(message, " "); //TODO: Not find check 315 316 command = message[0..firstSpace]; 317 318 ulong pos = firstSpace; 319 for(; pos < message.length; pos++) 320 { 321 if(message[pos] != ' ') 322 { 323 break; 324 } 325 } 326 327 params = message[pos..message.length]; 328 329 } 330 331 return new Message(from, command, params); 332 } 333 334 public override string toString() 335 { 336 return "(from: "~from~", command: "~command~", message: `"~params~"`)"; 337 } 338 339 /** 340 * Returns the sender of the message 341 * 342 * Returns: The `from` field 343 */ 344 public string getFrom() 345 { 346 return from; 347 } 348 349 /** 350 * Returns the command name 351 * 352 * Returns: The command itself 353 */ 354 public string getCommand() 355 { 356 return command; 357 } 358 359 /** 360 * Returns the optional paremeters (if any) 361 * 362 * Returns: The parameters 363 */ 364 public string getParams() 365 { 366 return params; 367 } 368 369 /** 370 * Retrieves the trailing text in the paramaters 371 * (if any) 372 * 373 * Returns: the trailing text 374 */ 375 public string getTrailing() 376 { 377 return ppTrailing; 378 } 379 380 /** 381 * Returns the parameters excluding the trailing text 382 * which are seperated by spaces but only those 383 * which are key-value pairs 384 * 385 * Returns: the key-value pair parameters 386 */ 387 public string[string] getKVPairs() 388 { 389 return ppKVPairs; 390 } 391 392 /** 393 * Returns the parameters excluding the trailing text 394 * which are seperated by spaces 395 * 396 * Returns: the parameters 397 */ 398 public string[] getPairs() 399 { 400 return ppPairs; 401 } 402 403 private string ppTrailing; 404 private string[string] ppKVPairs; 405 private string[] ppPairs; 406 407 408 version(unittest) 409 { 410 import std.stdio; 411 } 412 413 unittest 414 { 415 string testInput = "A:=1 A=2 :Hello this is text"; 416 writeln("Input: ", testInput); 417 418 bool hasTrailer; 419 string[] splitted = splitting(testInput, hasTrailer); 420 writeln("Input (split): ", splitted); 421 422 423 424 assert(cmp(splitted[0], "A:=1") == 0); 425 assert(cmp(splitted[1], "A=2") == 0); 426 427 /* Trailer test */ 428 assert(hasTrailer); 429 assert(cmp(splitted[2], "Hello this is text") == 0); 430 } 431 432 unittest 433 { 434 string testInput = ":Hello this is text"; 435 bool hasTrailer; 436 string[] splitted = splitting(testInput, hasTrailer); 437 438 /* Trailer test */ 439 assert(hasTrailer); 440 assert(cmp(splitted[0], "Hello this is text") == 0); 441 } 442 443 /** 444 * Imagine: `A:=1 A=2 :Hello` 445 * 446 * Params: 447 * input = 448 * Returns: 449 */ 450 private static string[] splitting(string input, ref bool hasTrailer) 451 { 452 string[] splits; 453 454 bool trailingMode; 455 string buildUp; 456 for(ulong idx = 0; idx < input.length; idx++) 457 { 458 /* Get current character */ 459 char curCHar = input[idx]; 460 461 462 if(trailingMode) 463 { 464 buildUp ~= curCHar; 465 continue; 466 } 467 468 if(buildUp.length == 0) 469 { 470 if(curCHar == ':') 471 { 472 trailingMode = true; 473 continue; 474 } 475 } 476 477 478 if(curCHar == ' ') 479 { 480 /* Flush */ 481 splits ~= buildUp; 482 buildUp = ""; 483 } 484 else 485 { 486 buildUp ~= curCHar; 487 } 488 } 489 490 if(buildUp.length) 491 { 492 splits ~= buildUp; 493 } 494 495 hasTrailer = trailingMode; 496 497 return splits; 498 } 499 500 /** 501 * NOTE: This needs more work with trailing support 502 * we must make sure we only look for lastInex of `:` 503 * where it is first cyaracter after space but NOT within 504 * an active parameter 505 */ 506 private void parameterParse() 507 { 508 /* Only parse if there are params */ 509 if(params.length) 510 { 511 /* Trailing text */ 512 string trailing; 513 514 /* Split the `<params>` */ 515 bool hasTrailer; 516 string[] paramsSplit = splitting(params, hasTrailer); 517 518 // logger.debug_("ParamsSPlit direct:", paramsSplit); 519 520 521 522 /* Extract the trailer as the last item in the array (if it exists) */ 523 if(hasTrailer) 524 { 525 trailing = paramsSplit[paramsSplit.length-1]; 526 527 /* Remove it from the parameters */ 528 paramsSplit = paramsSplit[0..$-1]; 529 530 // logger.debug_("GOt railer ", trailing); 531 } 532 533 ppPairs = paramsSplit; 534 535 536 /* Generate the key-value pairs */ 537 foreach(string pair; paramsSplit) 538 { 539 /* Only do this if we have an `=` in the current pair */ 540 if(indexOf(pair, "=") > -1) 541 { 542 string key = split(pair, "=")[0]; 543 string value = split(pair, "=")[1]; 544 ppKVPairs[key] = value; 545 } 546 } 547 548 /* Save the trailing */ 549 ppTrailing = trailing; 550 551 // logger.debug_("ppTrailing: ", ppTrailing); 552 } 553 } 554 555 /** 556 * Returns whether or not this message was 557 * a numeric response 558 * 559 * Returns: `true` if numeric response 560 * `false` otherwise 561 */ 562 public bool isResponseMessage() 563 { 564 return isNumericResponse; 565 } 566 567 /** 568 * Returns whether or not this message is 569 * an error kind-of numeric response 570 * 571 * Returns: `true` if numeric response 572 * is an error, `false` otherwise 573 */ 574 public bool isResponseError() 575 { 576 return isError; 577 } 578 579 /** 580 * Returns the type of reply (if this message 581 * was a numeric response) 582 * 583 * Returns: the ReplyType 584 */ 585 public ReplyType getReplyType() 586 { 587 return replyType; 588 } 589 } 590 591 version(unittest) 592 { 593 // Contains illegal characters 594 string badString1 = "doos"~"bruh"~"lek"~cast(string)[10]~"ker"; 595 string badString2 = "doos"~"bruh"~"lek"~cast(string)[13]~"ker"; 596 } 597 598 /** 599 * Tests the detection of illegal characters in messages 600 */ 601 unittest 602 { 603 assert(Message.hasIllegalCharacters(badString1) == true); 604 assert(Message.hasIllegalCharacters(badString2) == true); 605 } 606 607 /** 608 * Tests if a message containing bad characters, 609 * once stripped, is then valid. 610 * 611 * Essentially, tests the stripper. 612 */ 613 unittest 614 { 615 assert(Message.hasIllegalCharacters(Message.stripIllegalCharacters(badString1)) == false); 616 assert(Message.hasIllegalCharacters(Message.stripIllegalCharacters(badString2)) == false); 617 } 618 619 /** 620 * Tests the ability, at the `Message`-level, to detect 621 * illegal characters and automatically strip them when 622 * in `ChecksMode.EASY` 623 */ 624 unittest 625 { 626 Message message = new Message(badString1, "fine", "fine"); 627 628 try 629 { 630 string encoded = message.encode(ChecksMode.EASY); 631 assert(Message.hasIllegalCharacters(encoded) == false); 632 } 633 catch(BirchwoodException e) 634 { 635 assert(false); 636 } 637 } 638 639 /** 640 * Tests the ability, at the `Message`-level, to detect 641 * illegal characters and throw an exception when in 642 * `ChecksMode.HARDCORE` 643 */ 644 unittest 645 { 646 Message message = new Message(badString1, "fine", "fine"); 647 648 try 649 { 650 message.encode(ChecksMode.HARDCORE); 651 assert(false); 652 } 653 catch(BirchwoodException e) 654 { 655 assert(e.getType() == ErrorType.ILLEGAL_CHARACTERS); 656 } 657 }